Index: trunk/extensions/MassEditRegex/MassEditRegex.alias.php |
— | — | @@ -0,0 +1,13 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Aliases for special pages |
| 5 | + * |
| 6 | + * @addtogroup Extensions |
| 7 | + */ |
| 8 | + |
| 9 | +$aliases = array(); |
| 10 | + |
| 11 | +/** English */ |
| 12 | +$aliases['en'] = array( |
| 13 | + 'MassEditRegex' => array('MassEditRegex'), |
| 14 | +); |
Index: trunk/extensions/MassEditRegex/MassEditRegex.i18n.php |
— | — | @@ -0,0 +1,38 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Internationalisation file for MassEditRegex extension |
| 5 | + * |
| 6 | + * @addtogroup Extensions |
| 7 | + */ |
| 8 | + |
| 9 | +$messages = array(); |
| 10 | + |
| 11 | +/** English |
| 12 | + * @author Adam Nielsen |
| 13 | + */ |
| 14 | +$messages['en'] = array( |
| 15 | + 'masseditregex' => 'Mass Edit using Regular Expressions', |
| 16 | + 'masseditregex-desc' => 'Use regular expressions to [[Special:MassEditRegex|edit many pages in one operation]]', |
| 17 | + 'masseditregextext' => 'Enter one or more regular expressions (one per line) for matching, and one or more expressions to replace each match with. The first match-expression, if successful, will be replaced with the first replace-expression, and so on. See the PHP function preg_replace() for details.', |
| 18 | + 'pagelisttxt' => 'Pages to edit:', |
| 19 | + 'matchtxt' => 'Search for:', |
| 20 | + 'replacetxt' => 'Replace with:', |
| 21 | + 'executebtn' => 'Execute', |
| 22 | + 'err-nopages' => 'You must specify at least one page to change.', |
| 23 | + |
| 24 | + 'before' => 'Before', |
| 25 | + 'after' => 'After', |
| 26 | + 'max-preview-diffs' => 'Preview has been limited to the first $1 matches.', |
| 27 | + |
| 28 | + 'num-changes' => 'changes', // e.g. "5 changes", can't use $1 or it'll be too slow |
| 29 | + 'num-articles-changed' => '$1 articles edited', |
| 30 | + 'view-full-summary' => 'View full edit summary', |
| 31 | + |
| 32 | + 'hint-intro' => 'Here are some hints and examples for accomplishing common tasks:', |
| 33 | + 'hint-headmatch' => 'Match', |
| 34 | + 'hint-headreplace' => 'Replace', |
| 35 | + 'hint-headeffect' => 'Effect', |
| 36 | + 'hint-toappend' => 'Append some text to the end of the article - great for adding pages to categories', |
| 37 | + 'hint-remove' => 'Remove some text from all the pages in the list', |
| 38 | + 'hint-removecat' => 'Remove all categories from an article (note the escaping of the square brackets in the wikicode.) The replacement values should not be escaped.' |
| 39 | +); |
Index: trunk/extensions/MassEditRegex/MassEditRegex.php |
— | — | @@ -0,0 +1,32 @@ |
| 2 | +<?php |
| 3 | +if ( ! defined( 'MEDIAWIKI' ) ) |
| 4 | + die(); |
| 5 | +/** |
| 6 | + * Allow users in the Bot group to edit many articles in one go by applying |
| 7 | + * regular expressions to a list of pages. |
| 8 | + * |
| 9 | + * @addtogroup Extensions |
| 10 | + * |
| 11 | + * @link http://www.mediawiki.org/wiki/Extension:MassEditRegex Documentation |
| 12 | + * |
| 13 | + * @author Adam Nielsen <malvineous@shikadi.net> |
| 14 | + * @copyright Copyright © 2009 Adam Nielsen |
| 15 | + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later |
| 16 | + */ |
| 17 | + |
| 18 | +$wgExtensionCredits['specialpage'][] = array( |
| 19 | + 'path' => __FILE__, |
| 20 | + 'name' => 'Mass Edit via Regular Expressions', |
| 21 | + 'version' => 'r2', |
| 22 | + 'author' => 'Adam Nielsen', |
| 23 | + 'url' => 'http://www.mediawiki.org/wiki/Extension:MassEditRegex', |
| 24 | + 'description' => 'Use regular expressions to [[Special:MassEditRegex|edit many pages in one operation]]', |
| 25 | + 'descriptionmsg' => 'masseditregex-desc' |
| 26 | +); |
| 27 | + |
| 28 | +$dir = dirname(__FILE__) . '/'; |
| 29 | +$wgExtensionMessagesFiles['MassEditRegex'] = $dir . 'MassEditRegex.i18n.php'; |
| 30 | +$wgExtensionAliasesFiles['MassEditRegex'] = $dir . 'MassEditRegex.alias.php'; |
| 31 | +$wgAutoloadClasses['MassEditRegex'] = $dir . 'MassEditRegex.class.php'; |
| 32 | +$wgSpecialPages['MassEditRegex'] = 'MassEditRegex'; |
| 33 | +$wgSpecialPageGroups['MassEditRegex'] = 'pagetools'; |
Index: trunk/extensions/MassEditRegex/README |
— | — | @@ -0,0 +1,19 @@ |
| 2 | +== MassEditRegex == |
| 3 | +Copyright © 2009 Adam Nielsen <malvineous@shikadi.net> |
| 4 | +GNU General Public License 2.0 or later |
| 5 | +http://www.gnu.org/copyleft/gpl.html |
| 6 | + |
| 7 | +Edit a list of pages in a single operation by applying a regular expression |
| 8 | +to each page's content. |
| 9 | + |
| 10 | +See http://www.mediawiki.org/wiki/Extension:MassEditRegex for full instructions. |
| 11 | + |
| 12 | +Briefly: |
| 13 | + |
| 14 | + 1. Add to LocalSettings.php: |
| 15 | + |
| 16 | + include_once("$IP/extensions/MassEditRegex/MassEditRegex.php"); |
| 17 | + |
| 18 | + 2. Go to [[Special:MassEditRegex]] |
| 19 | + |
| 20 | + 3. If you don't have access, go to [[Special:User rights management]] and add yourself to the Bot group. |
Index: trunk/extensions/MassEditRegex/MassEditRegex.class.php |
— | — | @@ -0,0 +1,327 @@ |
| 2 | +<?php |
| 3 | +if ( ! defined( 'MEDIAWIKI' ) ) |
| 4 | + die(); |
| 5 | +/** |
| 6 | + * Allow users in the Bot group to edit many articles in one go by applying |
| 7 | + * regular expressions to a list of pages. |
| 8 | + * |
| 9 | + * @addtogroup SpecialPage |
| 10 | + * |
| 11 | + * @link http://www.mediawiki.org/wiki/Extension:MassEditRegex Documentation |
| 12 | + * |
| 13 | + * @author Adam Nielsen <malvineous@shikadi.net> |
| 14 | + * @copyright Copyright © 2009 Adam Nielsen |
| 15 | + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later |
| 16 | + */ |
| 17 | + |
| 18 | +// Maximum number of pages/diffs to display when previewing the changes |
| 19 | +define('MER_MAX_PREVIEW_DIFFS', 10); |
| 20 | + |
| 21 | +/** Main class that define a new special page*/ |
| 22 | +class MassEditRegex extends SpecialPage { |
| 23 | + |
| 24 | + function MassEditRegex() { |
| 25 | + SpecialPage::SpecialPage('MassEditRegex', 'bot'); |
| 26 | + } |
| 27 | + |
| 28 | + function execute( $par ) { |
| 29 | + global $wgAllowSysopQueries, $wgUser, $wgRequest, $wgOut; |
| 30 | + wfLoadExtensionMessages('MassEditRegex'); |
| 31 | + |
| 32 | + if (!$wgUser->isBot()) { |
| 33 | + $wgOut->permissionRequired('bot'); |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + if ($wgRequest->wasPosted()) { |
| 38 | + $f = new MassEditRegexForm( |
| 39 | + $wgRequest->getText('wpPageList'), |
| 40 | + $wgRequest->getText('wpMatch'), |
| 41 | + $wgRequest->getText('wpReplace'), |
| 42 | + $wgRequest->getText('wpSummary') |
| 43 | + ); |
| 44 | + if ($wgRequest->getVal('wpPreviewBtn') !== NULL) { |
| 45 | + $f->showPreview(); |
| 46 | + } else if ($wgRequest->getVal('wpExecuteBtn') !== NULL) { |
| 47 | + $f->execute(); |
| 48 | + } |
| 49 | + } else { |
| 50 | + $f = new MassEditRegexForm(); |
| 51 | + $f->showForm(); |
| 52 | + $f->showHints(); |
| 53 | + } |
| 54 | + |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +/** |
| 59 | + * @access private |
| 60 | + * @addtogroup SpecialPage |
| 61 | + */ |
| 62 | + |
| 63 | +class MassEditRegexForm { |
| 64 | + private $aPageList; |
| 65 | + private $aMatch; |
| 66 | + private $aReplace; |
| 67 | + private $strReplace; // keep to avoid having to re-escape again |
| 68 | + private $strSummary; |
| 69 | + private $sk; |
| 70 | + |
| 71 | + function MassEditRegexForm( |
| 72 | + $strPageList = 'Sandbox', |
| 73 | + $strMatch = '/hello (.*)\n/', // defaults |
| 74 | + $strReplace = 'goodbye \1', |
| 75 | + $strSummary = '' |
| 76 | + ) { |
| 77 | + global $wgOut, $wgUser; |
| 78 | + $this->aPageList = split("\n", trim($strPageList)); |
| 79 | + //print_r($this->aPages); |
| 80 | + //if (count($this->aPages) == 0) $this->aPages[0] = $this->aPages; |
| 81 | + $this->aMatch = split("\n", trim($strMatch)); |
| 82 | + $this->strReplace = $strReplace; |
| 83 | + $this->aReplace = split("\n", $strReplace); |
| 84 | + $this->strSummary = $strSummary; |
| 85 | + |
| 86 | + $wgOut->setPagetitle(wfMsg('masseditregex')); |
| 87 | + |
| 88 | + $this->sk = $wgUser->getSkin(); |
| 89 | + |
| 90 | + // Replace \n in the match with an actual newline (since a newline can't |
| 91 | + // be typed in, it'll act as the splitter for the next regex) |
| 92 | + foreach ($this->aReplace as &$str) { |
| 93 | + // Convert \n into a newline, \\n into \n, \\\n into \<newline>, etc. |
| 94 | + $str = preg_replace(array( |
| 95 | + '/(^|[^\\\\])((\\\\)*)(\2)\\\\n/', |
| 96 | + '/(^|[^\\\\])((\\\\)*)(\2)n/' |
| 97 | + ), array( |
| 98 | + "\\1\\2\n", |
| 99 | + "\\1\\2n" |
| 100 | + ), $str); |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + function showForm($err = '') { |
| 105 | + global $wgOut, $wgUser, $wgLang; |
| 106 | + global $wgLogQueries; |
| 107 | + |
| 108 | + if ($err) { |
| 109 | + $wgOut->addHTML('<div class="wikierror">' . htmlspecialchars($err) . '</div>'); |
| 110 | + } |
| 111 | + |
| 112 | + $wgOut->addWikiText(wfMsg('masseditregextext')); |
| 113 | + |
| 114 | + $txtPageList = wfMsg('pagelisttxt'); |
| 115 | + $txtMatch = wfMsg('matchtxt'); |
| 116 | + $txtReplace = wfMsg('replacetxt'); |
| 117 | + $txtPreviewBtn = wfMsg('showpreview'); |
| 118 | + $txtExecuteBtn = wfMsg('executebtn'); |
| 119 | + |
| 120 | + $txtEditSummary = wfMsg('summary'); |
| 121 | + $txtSummaryPreview = wfMsg('summary-preview'); |
| 122 | + |
| 123 | + $titleObj = Title::makeTitle(NS_SPECIAL, 'MassEditRegex'); |
| 124 | + $action = $titleObj->escapeLocalURL('action=submit'); |
| 125 | + |
| 126 | + $htmlPageList = htmlspecialchars(join("\n", $this->aPageList)); |
| 127 | + $htmlMatch = htmlspecialchars(join("\n", $this->aMatch)); |
| 128 | + $htmlReplace = htmlspecialchars($this->strReplace); // use original value |
| 129 | + $htmlSummary = htmlspecialchars($this->strSummary); |
| 130 | + $htmlSummaryPreview = $this->sk->commentBlock($this->strSummary, $titleObj); |
| 131 | + |
| 132 | + $mainForm = <<<ENDFORM |
| 133 | +<form id="masseditregex" method="post" action="{$action}"> |
| 134 | +<p>{$txtPageList}</p> |
| 135 | +<!-- Newlines are important here - one after <textarea> but none |
| 136 | + before </textarea>, otherwise leading blank lines get cut |
| 137 | + off, or trailing newlines get added! Tested FF3 --> |
| 138 | +<textarea name="wpPageList" cols="80" rows="4" tabindex="1" style="width:100%;"> |
| 139 | +{$htmlPageList}</textarea> |
| 140 | + |
| 141 | +<table border="0" cellspacing="0" cellpadding="0" style="width: 100%;"> |
| 142 | +<tr><td> |
| 143 | +<p>{$txtMatch}</p> |
| 144 | +<textarea name="wpMatch" cols="80" rows="4" tabindex="1" style="width:95%;"> |
| 145 | +{$htmlMatch}</textarea> |
| 146 | +</td><td> |
| 147 | +<p>{$txtReplace}</p> |
| 148 | +<textarea name="wpReplace" cols="80" rows="4" tabindex="1" style="width:100%;"> |
| 149 | +{$htmlReplace}</textarea> |
| 150 | +</td></tr> |
| 151 | +</table> |
| 152 | +<p></p> |
| 153 | +<div class="editOptions"> |
| 154 | +<span id="wpSummaryLabel"><label for="wpSummary">{$txtEditSummary}</label></span> |
| 155 | +<input type="text" value="$htmlSummary" name="wpSummary" id="wpSummary" |
| 156 | +maxlength="200" size="60" /><br /> |
| 157 | + |
| 158 | +<div class="mw-summary-preview"> |
| 159 | +$txtSummaryPreview |
| 160 | +$htmlSummaryPreview |
| 161 | +</div> |
| 162 | +</div> |
| 163 | + |
| 164 | +<p> |
| 165 | + <input type="submit" name="wpPreviewBtn" value="{$txtPreviewBtn}"> |
| 166 | + <input type="submit" name="wpExecuteBtn" value="{$txtExecuteBtn}"> |
| 167 | +</p> |
| 168 | +</form> |
| 169 | +ENDFORM; |
| 170 | + $wgOut->addHTML($mainForm); |
| 171 | + return; |
| 172 | + } |
| 173 | + |
| 174 | + function showHints() |
| 175 | + { |
| 176 | + global $wgOut; |
| 177 | + $hintIntro = wfMsg('hint-intro'); |
| 178 | + $hintMatch = wfMsg('hint-headmatch'); |
| 179 | + $hintReplace = wfMsg('hint-headreplace'); |
| 180 | + $hintEffect = wfMsg('hint-headeffect'); |
| 181 | + $hintToAppend = wfMsg('hint-toappend'); |
| 182 | + $hintRemove = wfMsg('hint-remove'); |
| 183 | + $hintRemoveCat = wfMsg('hint-removecat'); |
| 184 | + |
| 185 | + $htmlHints = <<<ENDHINTS |
| 186 | +<p>{$hintIntro}</p> |
| 187 | +<table border="1" cellspacing="0" cellpadding="2" class="wikitable"> |
| 188 | +<thead><tr> |
| 189 | + <th style="width: 12em;">{$hintMatch}</th> |
| 190 | + <th style="width: 12em;">{$hintReplace}</th> |
| 191 | + <th>{$hintEffect}</th> |
| 192 | +</tr></thead> |
| 193 | +<tbody> |
| 194 | + <tr> |
| 195 | + <td>/$/<br/>/$/</td><td>abc<br/>\\n[[Category:New]]</td><td>{$hintToAppend}</td> |
| 196 | + </tr><tr> |
| 197 | + <td>{{OldTemplate}}</td><td></td><td>{$hintRemove}</td> |
| 198 | + </tr><tr> |
| 199 | + <td>\\[\\[Category:[^]]+\]\]</td><td></td><td>{$hintRemoveCat}</td> |
| 200 | + </tr> |
| 201 | +</tbody> |
| 202 | +</table> |
| 203 | +ENDHINTS; |
| 204 | + $wgOut->addHTML($htmlHints); |
| 205 | + |
| 206 | + return; |
| 207 | + } |
| 208 | + |
| 209 | + function showPreview() |
| 210 | + { |
| 211 | + $this->execute(false); |
| 212 | + return; |
| 213 | + } |
| 214 | + |
| 215 | + function getPages() |
| 216 | + { |
| 217 | + if (sizeof($this->aPageList) == 0) return NULL; |
| 218 | + $req = new FauxRequest(array( |
| 219 | + 'action' => 'query', |
| 220 | + 'titles' => join('|', $this->aPageList), |
| 221 | + 'prop' => 'info|revisions', |
| 222 | + 'intoken' => 'edit', |
| 223 | + 'rvprop' => 'content', |
| 224 | + //'rvlimit' => 1 // most recent revision only |
| 225 | + ), false); |
| 226 | + $processor = new ApiMain($req, true); |
| 227 | + $processor->execute(); |
| 228 | + $aPages = $processor->getResultData(); |
| 229 | + if (empty($aPages)) return NULL; // no pages match the titles given |
| 230 | + return $aPages['query']['pages']; |
| 231 | + } |
| 232 | + |
| 233 | + function execute($bPerformEdits = true) |
| 234 | + { |
| 235 | + global $wgOut, $wgUser; |
| 236 | + global $wgRequest, $wgTitle; |
| 237 | + |
| 238 | + $aPages = $this->getPages(); |
| 239 | + if ($aPages === NULL) { |
| 240 | + $this->showForm(wfMsg('err-nopages')); |
| 241 | + return; |
| 242 | + } |
| 243 | + |
| 244 | + // Show the form again ready for further editing if we're just previewing |
| 245 | + if (!$bPerformEdits) $this->showForm(); |
| 246 | + |
| 247 | + $diff = new DifferenceEngine(); |
| 248 | + $diff->showDiffStyle(); // send CSS link to the browser for diff colours |
| 249 | + |
| 250 | + $strChanges = wfMsg('num-changes'); |
| 251 | + |
| 252 | + if ($bPerformEdits) $wgOut->addHTML('<ul>'); |
| 253 | + |
| 254 | + // Save the state until the MW Edit API does it for us |
| 255 | + if ($bPerformEdits) { |
| 256 | + $o_wgOut = clone $wgOut; // need to do a deep copy here |
| 257 | + $wgOut->disable(); // not strictly necessary, but might speed things up |
| 258 | + $o_wgTitle = $wgTitle; |
| 259 | + } |
| 260 | + |
| 261 | + $iArticleCount = 0; |
| 262 | + foreach ($aPages as $p) { |
| 263 | + $iArticleCount++; |
| 264 | + if (!isset($p['revisions'])) { |
| 265 | + if ($bPerformEdits) { |
| 266 | + $o_wgOut->addHTML('<li> ' . $p['title'] . ' does not exist</li>'); |
| 267 | + } else { |
| 268 | + $wgOut->addHTML('<p>' . $p['title'] . ' does not exist</p>'); |
| 269 | + } |
| 270 | + continue; // empty page |
| 271 | + } |
| 272 | + $curContent = $p['revisions'][0]['*']; |
| 273 | + $iCount = 0; |
| 274 | + $newContent = @preg_replace($this->aMatch, $this->aReplace, $curContent, -1, $iCount); |
| 275 | + |
| 276 | + if ($bPerformEdits) { |
| 277 | + // Not in preview mode, make the edits |
| 278 | + //print_r($p); |
| 279 | + $o_wgOut->addHTML('<li> ' . $p['title'] . ': ' . $iCount . ' ' . $strChanges . '</li>'); |
| 280 | + $req = new FauxRequest(array( |
| 281 | + 'action' => 'edit', |
| 282 | + 'bot' => true, |
| 283 | + 'token' => $p['edittoken'], |
| 284 | + 'title' => $p['title'], |
| 285 | + 'summary' => $this->strSummary, |
| 286 | + 'text' => $newContent, |
| 287 | + 'basetimestamp' => $p['starttimestamp'] |
| 288 | + ), true); |
| 289 | + $processor = new ApiMain($req, true); |
| 290 | + try { |
| 291 | + $processor->execute(); |
| 292 | + } catch (UsageException $e) { |
| 293 | + $o_wgOut->addHTML('<ul><li>Edit failed: ' . $e . '</li></ul>'); |
| 294 | + } |
| 295 | + } else { |
| 296 | + // In preview mode, display the first few diffs |
| 297 | + $diff->setText($curContent, $newContent); |
| 298 | + $dtxt = $diff->getDiff('<b>' . $p['title'] . ' - ' . wfMsg('before') . '</b>', |
| 299 | + '<b>' . wfMsg('after') . '</b>'); |
| 300 | + $wgOut->addHTML($dtxt); |
| 301 | + |
| 302 | + if ($iArticleCount >= MER_MAX_PREVIEW_DIFFS) { |
| 303 | + $wgOut->addHTML('<p>' . wfMsg('max-preview-diffs', MER_MAX_PREVIEW_DIFFS) . '</p>'); |
| 304 | + break; |
| 305 | + } |
| 306 | + } |
| 307 | + |
| 308 | + } |
| 309 | + // Restore the state after the Edit API has messed with it |
| 310 | + if ($bPerformEdits) { |
| 311 | + $wgTitle = $o_wgTitle; |
| 312 | + $wgOut = $o_wgOut; |
| 313 | + } |
| 314 | + |
| 315 | + if ($bPerformEdits) { |
| 316 | + $wgOut->addHTML('</ul><p>' . wfMsg('num-articles-changed', $iArticleCount) |
| 317 | + . '</p>' . $this->sk->makeKnownLinkObj( |
| 318 | + SpecialPage::getSafeTitleFor('Contributions', $wgUser->getName()), |
| 319 | + wfMsg('view-full-summary') |
| 320 | + ) |
| 321 | + ); |
| 322 | + } |
| 323 | + |
| 324 | + return; |
| 325 | + } |
| 326 | + |
| 327 | +} |
| 328 | + |