Index: trunk/extensions/MirrorTools/MirrorTools.i18n.php |
— | — | @@ -0,0 +1,15 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Internationalisation file for the MirrorTools extension |
| 5 | + * @addtogroup Extensions |
| 6 | + */ |
| 7 | + |
| 8 | +$messages = array(); |
| 9 | + |
| 10 | +/* English |
| 11 | + * @author Tisane |
| 12 | + */ |
| 13 | +$messages['en'] = array( |
| 14 | + 'mirrortools' => 'MirrorTools', |
| 15 | + 'mirrortools-desc' => 'Allows edits to be made under any username' |
| 16 | +); |
\ No newline at end of file |
Index: trunk/extensions/MirrorTools/MirrorTools.php |
— | — | @@ -0,0 +1,47 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * MirrorTools extension by Tisane |
| 5 | + * URL: http://www.mediawiki.org/wiki/Extension:MirrorTools |
| 6 | + * |
| 7 | + * This program is free software; you can redistribute it and/or modify |
| 8 | + * it under the terms of the GNU General Public License as published by |
| 9 | + * the Free Software Foundation; either version 2 of the License, or |
| 10 | + * (at your option) any later version. |
| 11 | + * |
| 12 | + * This program is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | + * GNU General Public License for more details. |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU General Public License along |
| 18 | + * with this program; if not, write to the Free Software Foundation, Inc., |
| 19 | + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 20 | + * http://www.gnu.org/copyleft/gpl.html |
| 21 | + */ |
| 22 | + |
| 23 | +/* Alert the user that this is not a valid entry point to MediaWiki if they try to access the |
| 24 | +special pages file directly.*/ |
| 25 | + |
| 26 | +if ( !defined( 'MEDIAWIKI' ) ) { |
| 27 | + echo <<<EOT |
| 28 | + To install the MirrorTools extension, put the following line in LocalSettings.php: |
| 29 | + require( "extensions/MirrorTools/MirrorTools.php" ); |
| 30 | +EOT; |
| 31 | + exit( 1 ); |
| 32 | +} |
| 33 | + |
| 34 | +$wgExtensionCredits['other'][] = array( |
| 35 | + 'path' => __FILE__, |
| 36 | + 'name' => 'MirrorTools', |
| 37 | + 'author' => 'Tisane', |
| 38 | + 'url' => 'http://www.mediawiki.org/wiki/Extension:MirrorTools', |
| 39 | + 'descriptionmsg' => 'mirrortools-desc', |
| 40 | + 'version' => '1.0.0', |
| 41 | +); |
| 42 | + |
| 43 | +$dir = dirname( __FILE__ ) . '/'; |
| 44 | +$wgAutoloadClasses['ApiMirrorEditPage'] = $dir . 'APIMirrorTools.php'; |
| 45 | +$wgAutoloadClasses['MirrorEditPage'] = $dir . 'MirrorTools.classes.php'; |
| 46 | +$wgAPIModules['mirroredit'] = 'ApiMirrorEditPage'; |
| 47 | +$wgExtensionMessagesFiles['MirrorTools'] = $dir . 'MirrorEdit.i18n.php'; |
| 48 | +$wgGroupPermissions['MirrorTools']['mirroredit'] = true; |
\ No newline at end of file |
Index: trunk/extensions/MirrorTools/APIMirrorTools.php |
— | — | @@ -0,0 +1,490 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Created on August 16, 2007 |
| 6 | + * |
| 7 | + * API for MediaWiki 1.8+ |
| 8 | + * |
| 9 | + * Copyright © 2007 Iker Labarga <Firstname><Lastname>@gmail.com |
| 10 | + * |
| 11 | + * This program is free software; you can redistribute it and/or modify |
| 12 | + * it under the terms of the GNU General Public License as published by |
| 13 | + * the Free Software Foundation; either version 2 of the License, or |
| 14 | + * (at your option) any later version. |
| 15 | + * |
| 16 | + * This program is distributed in the hope that it will be useful, |
| 17 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 18 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 19 | + * GNU General Public License for more details. |
| 20 | + * |
| 21 | + * You should have received a copy of the GNU General Public License along |
| 22 | + * with this program; if not, write to the Free Software Foundation, Inc., |
| 23 | + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 24 | + * http://www.gnu.org/copyleft/gpl.html |
| 25 | + */ |
| 26 | + |
| 27 | +/** |
| 28 | + * A module that allows for editing and creating pages. |
| 29 | + * |
| 30 | + * Currently, this wraps around the EditPage class in an ugly way, |
| 31 | + * EditPage.php should be rewritten to provide a cleaner interface |
| 32 | + * @ingroup API |
| 33 | + */ |
| 34 | +class ApiMirrorEditPage extends ApiBase { |
| 35 | + |
| 36 | + public function __construct( $query, $moduleName ) { |
| 37 | + parent::__construct( $query, $moduleName ); |
| 38 | + } |
| 39 | + |
| 40 | + public function execute() { |
| 41 | + global $wgUser; |
| 42 | + $params = $this->extractRequestParams(); |
| 43 | + |
| 44 | + if ( is_null( $params['user'] ) ) { |
| 45 | + $this->dieUsageMsg( array( 'missingparam', 'user' ) ); |
| 46 | + } |
| 47 | + $user = $params['user']; |
| 48 | + |
| 49 | + if ( is_null( $params['title'] ) ) { |
| 50 | + $this->dieUsageMsg( array( 'missingparam', 'title' ) ); |
| 51 | + } |
| 52 | + |
| 53 | + if ( is_null( $params['text'] ) && is_null( $params['appendtext'] ) && |
| 54 | + is_null( $params['prependtext'] ) && |
| 55 | + $params['undo'] == 0 ) |
| 56 | + { |
| 57 | + $this->dieUsageMsg( array( 'missingtext' ) ); |
| 58 | + } |
| 59 | + |
| 60 | + $titleObj = Title::newFromText( $params['title'] ); |
| 61 | + if ( !$titleObj || $titleObj->isExternal() ) { |
| 62 | + $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); |
| 63 | + } |
| 64 | + |
| 65 | + // Some functions depend on $wgTitle == $ep->mTitle |
| 66 | + global $wgTitle; |
| 67 | + $wgTitle = $titleObj; |
| 68 | + |
| 69 | + if ( $params['createonly'] && $titleObj->exists() ) { |
| 70 | + $this->dieUsageMsg( array( 'createonly-exists' ) ); |
| 71 | + } |
| 72 | + if ( $params['nocreate'] && !$titleObj->exists() ) { |
| 73 | + $this->dieUsageMsg( array( 'nocreate-missing' ) ); |
| 74 | + } |
| 75 | + |
| 76 | + // Now let's check whether we're even allowed to do this |
| 77 | + $errors = $titleObj->getUserPermissionsErrors( 'mirroredit', $wgUser ); |
| 78 | + if ( !$titleObj->exists() ) { |
| 79 | + $errors = array_merge( $errors, $titleObj->getUserPermissionsErrors( 'create', $wgUser ) ); |
| 80 | + } |
| 81 | + if ( count( $errors ) ) { |
| 82 | + $this->dieUsageMsg( $errors[0] ); |
| 83 | + } |
| 84 | + |
| 85 | + $articleObj = new Article( $titleObj ); |
| 86 | + $toMD5 = $params['text']; |
| 87 | + if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) |
| 88 | + { |
| 89 | + // For non-existent pages, Article::getContent() |
| 90 | + // returns an interface message rather than '' |
| 91 | + // We do want getContent()'s behavior for non-existent |
| 92 | + // MediaWiki: pages, though |
| 93 | + if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) { |
| 94 | + $content = ''; |
| 95 | + } else { |
| 96 | + $content = $articleObj->getContent(); |
| 97 | + } |
| 98 | + |
| 99 | + if ( !is_null( $params['section'] ) ) { |
| 100 | + // Process the content for section edits |
| 101 | + global $wgParser; |
| 102 | + $section = intval( $params['section'] ); |
| 103 | + $content = $wgParser->getSection( $content, $section, false ); |
| 104 | + if ( $content === false ) { |
| 105 | + $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); |
| 106 | + } |
| 107 | + } |
| 108 | + $params['text'] = $params['prependtext'] . $content . $params['appendtext']; |
| 109 | + $toMD5 = $params['prependtext'] . $params['appendtext']; |
| 110 | + } |
| 111 | + |
| 112 | + if ( $params['undo'] > 0 ) { |
| 113 | + if ( $params['undoafter'] > 0 ) { |
| 114 | + if ( $params['undo'] < $params['undoafter'] ) { |
| 115 | + list( $params['undo'], $params['undoafter'] ) = |
| 116 | + array( $params['undoafter'], $params['undo'] ); |
| 117 | + } |
| 118 | + $undoafterRev = Revision::newFromID( $params['undoafter'] ); |
| 119 | + } |
| 120 | + $undoRev = Revision::newFromID( $params['undo'] ); |
| 121 | + if ( is_null( $undoRev ) || $undoRev->isDeleted( Revision::DELETED_TEXT ) ) |
| 122 | + { |
| 123 | + $this->dieUsageMsg( array( 'nosuchrevid', $params['undo'] ) ); |
| 124 | + } |
| 125 | + |
| 126 | + if ( $params['undoafter'] == 0 ) { |
| 127 | + $undoafterRev = $undoRev->getPrevious(); |
| 128 | + } |
| 129 | + if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( Revision::DELETED_TEXT ) ) |
| 130 | + { |
| 131 | + $this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) ); |
| 132 | + } |
| 133 | + |
| 134 | + if ( $undoRev->getPage() != $articleObj->getID() ) { |
| 135 | + $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) ); |
| 136 | + } |
| 137 | + if ( $undoafterRev->getPage() != $articleObj->getID() ) { |
| 138 | + $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) ); |
| 139 | + } |
| 140 | + |
| 141 | + $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev ); |
| 142 | + if ( $newtext === false ) { |
| 143 | + $this->dieUsageMsg( array( 'undo-failure' ) ); |
| 144 | + } |
| 145 | + $params['text'] = $newtext; |
| 146 | + // If no summary was given and we only undid one rev, |
| 147 | + // use an autosummary |
| 148 | + if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] ) |
| 149 | + { |
| 150 | + $params['summary'] = wfMsgForContent( 'undo-summary', $params['undo'], $undoRev->getUserText() ); |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + // See if the MD5 hash checks out |
| 155 | + if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] ) { |
| 156 | + $this->dieUsageMsg( array( 'hashcheckfailed' ) ); |
| 157 | + } |
| 158 | + |
| 159 | + $ep = new MirrorEditPage( $articleObj ); |
| 160 | + // EditPage wants to parse its stuff from a WebRequest |
| 161 | + // That interface kind of sucks, but it's workable |
| 162 | + $reqArr = array( |
| 163 | + 'wpTextbox1' => $params['text'], |
| 164 | + 'wpEditToken' => $params['token'], |
| 165 | + 'wpIgnoreBlankSummary' => '' |
| 166 | + ); |
| 167 | + |
| 168 | + if ( !is_null( $params['summary'] ) ) { |
| 169 | + $reqArr['wpSummary'] = $params['summary']; |
| 170 | + } |
| 171 | + |
| 172 | + // Watch out for basetimestamp == '' |
| 173 | + // wfTimestamp() treats it as NOW, almost certainly causing an edit conflict |
| 174 | + if ( !is_null( $params['basetimestamp'] ) && $params['basetimestamp'] != '' ) |
| 175 | + { |
| 176 | + $reqArr['wpEdittime'] = wfTimestamp( TS_MW, $params['basetimestamp'] ); |
| 177 | + } else { |
| 178 | + $reqArr['wpEdittime'] = $articleObj->getTimestamp(); |
| 179 | + } |
| 180 | + |
| 181 | + if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' ) { |
| 182 | + $reqArr['wpStarttime'] = wfTimestamp( TS_MW, $params['starttimestamp'] ); |
| 183 | + } else { |
| 184 | + $reqArr['wpStarttime'] = $reqArr['wpEdittime']; // Fake wpStartime |
| 185 | + } |
| 186 | + |
| 187 | + if ( $params['minor'] || ( !$params['notminor'] && $wgUser->getOption( 'minordefault' ) ) ) { |
| 188 | + $reqArr['wpMinoredit'] = ''; |
| 189 | + } |
| 190 | + |
| 191 | + if ( $params['recreate'] ) { |
| 192 | + $reqArr['wpRecreate'] = ''; |
| 193 | + } |
| 194 | + |
| 195 | + if ( !is_null( $params['section'] ) ) { |
| 196 | + $section = intval( $params['section'] ); |
| 197 | + if ( $section == 0 && $params['section'] != '0' && $params['section'] != 'new' ) |
| 198 | + { |
| 199 | + $this->dieUsage( "The section parameter must be set to an integer or 'new'", "invalidsection" ); |
| 200 | + } |
| 201 | + $reqArr['wpSection'] = $params['section']; |
| 202 | + } else { |
| 203 | + $reqArr['wpSection'] = ''; |
| 204 | + } |
| 205 | + |
| 206 | + $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj ); |
| 207 | + |
| 208 | + // Deprecated parameters |
| 209 | + if ( $params['watch'] ) { |
| 210 | + $watch = true; |
| 211 | + } elseif ( $params['unwatch'] ) { |
| 212 | + $watch = false; |
| 213 | + } |
| 214 | + |
| 215 | + if ( $watch ) { |
| 216 | + $reqArr['wpWatchthis'] = ''; |
| 217 | + } |
| 218 | + |
| 219 | + $req = new FauxRequest( $reqArr, true ); |
| 220 | + $ep->importFormData( $req ); |
| 221 | + |
| 222 | + // Run hooks |
| 223 | + // Handle CAPTCHA parameters |
| 224 | + global $wgRequest; |
| 225 | + if ( !is_null( $params['captchaid'] ) ) { |
| 226 | + $wgRequest->setVal( 'wpCaptchaId', $params['captchaid'] ); |
| 227 | + } |
| 228 | + if ( !is_null( $params['captchaword'] ) ) { |
| 229 | + $wgRequest->setVal( 'wpCaptchaWord', $params['captchaword'] ); |
| 230 | + } |
| 231 | + |
| 232 | + $r = array(); |
| 233 | + if ( !wfRunHooks( 'APIEditBeforeSave', array( $ep, $ep->textbox1, &$r ) ) ) |
| 234 | + { |
| 235 | + if ( count( $r ) ) { |
| 236 | + $r['result'] = 'Failure'; |
| 237 | + $this->getResult()->addValue( null, $this->getModuleName(), $r ); |
| 238 | + return; |
| 239 | + } else { |
| 240 | + $this->dieUsageMsg( array( 'hookaborted' ) ); |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + // Do the actual save |
| 245 | + $oldRevId = $articleObj->getRevIdFetched(); |
| 246 | + $result = null; |
| 247 | + // Fake $wgRequest for some hooks inside EditPage |
| 248 | + // FIXME: This interface SUCKS |
| 249 | + $oldRequest = $wgRequest; |
| 250 | + $wgRequest = $req; |
| 251 | + |
| 252 | + $retval = $ep->mirrorinternalAttemptSave( $result, $wgUser->isAllowed( 'bot' ) && $params['bot'], $user ); |
| 253 | + $wgRequest = $oldRequest; |
| 254 | + switch( $retval ) { |
| 255 | + case EditPage::AS_HOOK_ERROR: |
| 256 | + case EditPage::AS_HOOK_ERROR_EXPECTED: |
| 257 | + $this->dieUsageMsg( array( 'hookaborted' ) ); |
| 258 | + |
| 259 | + case EditPage::AS_IMAGE_REDIRECT_ANON: |
| 260 | + $this->dieUsageMsg( array( 'noimageredirect-anon' ) ); |
| 261 | + |
| 262 | + case EditPage::AS_IMAGE_REDIRECT_LOGGED: |
| 263 | + $this->dieUsageMsg( array( 'noimageredirect-logged' ) ); |
| 264 | + |
| 265 | + case EditPage::AS_SPAM_ERROR: |
| 266 | + $this->dieUsageMsg( array( 'spamdetected', $result['spam'] ) ); |
| 267 | + |
| 268 | + case EditPage::AS_FILTERING: |
| 269 | + $this->dieUsageMsg( array( 'filtered' ) ); |
| 270 | + |
| 271 | + case EditPage::AS_BLOCKED_PAGE_FOR_USER: |
| 272 | + $this->dieUsageMsg( array( 'blockedtext' ) ); |
| 273 | + |
| 274 | + case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED: |
| 275 | + case EditPage::AS_CONTENT_TOO_BIG: |
| 276 | + global $wgMaxArticleSize; |
| 277 | + $this->dieUsageMsg( array( 'contenttoobig', $wgMaxArticleSize ) ); |
| 278 | + |
| 279 | + case EditPage::AS_READ_ONLY_PAGE_ANON: |
| 280 | + $this->dieUsageMsg( array( 'noedit-anon' ) ); |
| 281 | + |
| 282 | + case EditPage::AS_READ_ONLY_PAGE_LOGGED: |
| 283 | + $this->dieUsageMsg( array( 'noedit' ) ); |
| 284 | + |
| 285 | + case EditPage::AS_READ_ONLY_PAGE: |
| 286 | + $this->dieReadOnly(); |
| 287 | + |
| 288 | + case EditPage::AS_RATE_LIMITED: |
| 289 | + $this->dieUsageMsg( array( 'actionthrottledtext' ) ); |
| 290 | + |
| 291 | + case EditPage::AS_ARTICLE_WAS_DELETED: |
| 292 | + $this->dieUsageMsg( array( 'wasdeleted' ) ); |
| 293 | + |
| 294 | + case EditPage::AS_NO_CREATE_PERMISSION: |
| 295 | + $this->dieUsageMsg( array( 'nocreate-loggedin' ) ); |
| 296 | + |
| 297 | + case EditPage::AS_BLANK_ARTICLE: |
| 298 | + $this->dieUsageMsg( array( 'blankpage' ) ); |
| 299 | + |
| 300 | + case EditPage::AS_CONFLICT_DETECTED: |
| 301 | + $this->dieUsageMsg( array( 'editconflict' ) ); |
| 302 | + |
| 303 | + // case EditPage::AS_SUMMARY_NEEDED: Can't happen since we set wpIgnoreBlankSummary |
| 304 | + case EditPage::AS_TEXTBOX_EMPTY: |
| 305 | + $this->dieUsageMsg( array( 'emptynewsection' ) ); |
| 306 | + |
| 307 | + case EditPage::AS_SUCCESS_NEW_ARTICLE: |
| 308 | + $r['new'] = ''; |
| 309 | + case EditPage::AS_SUCCESS_UPDATE: |
| 310 | + $r['result'] = 'Success'; |
| 311 | + $r['pageid'] = intval( $titleObj->getArticleID() ); |
| 312 | + $r['title'] = $titleObj->getPrefixedText(); |
| 313 | + // HACK: We create a new Article object here because getRevIdFetched() |
| 314 | + // refuses to be run twice, and because Title::getLatestRevId() |
| 315 | + // won't fetch from the master unless we select for update, which we |
| 316 | + // don't want to do. |
| 317 | + $newArticle = new Article( $titleObj ); |
| 318 | + $newRevId = $newArticle->getRevIdFetched(); |
| 319 | + if ( $newRevId == $oldRevId ) { |
| 320 | + $r['nochange'] = ''; |
| 321 | + } else { |
| 322 | + $r['oldrevid'] = intval( $oldRevId ); |
| 323 | + $r['newrevid'] = intval( $newRevId ); |
| 324 | + $r['newtimestamp'] = wfTimestamp( TS_ISO_8601, |
| 325 | + $newArticle->getTimestamp() ); |
| 326 | + } |
| 327 | + break; |
| 328 | + |
| 329 | + case EditPage::AS_END: |
| 330 | + // This usually means some kind of race condition |
| 331 | + // or DB weirdness occurred. |
| 332 | + if ( is_array( $result ) && count( $result ) > 0 ) { |
| 333 | + $this->dieUsageMsg( array( 'unknownerror', $result[0][0] ) ); |
| 334 | + } |
| 335 | + |
| 336 | + // Unknown error, but no specific error message |
| 337 | + // Fall through |
| 338 | + default: |
| 339 | + $this->dieUsageMsg( array( 'unknownerror', $retval ) ); |
| 340 | + } |
| 341 | + $this->getResult()->addValue( null, $this->getModuleName(), $r ); |
| 342 | + } |
| 343 | + |
| 344 | + public function mustBePosted() { |
| 345 | + return true; |
| 346 | + } |
| 347 | + |
| 348 | + public function isWriteMode() { |
| 349 | + return true; |
| 350 | + } |
| 351 | + |
| 352 | + protected function getDescription() { |
| 353 | + return 'Create and edit pages using any username.'; |
| 354 | + } |
| 355 | + |
| 356 | + public function getPossibleErrors() { |
| 357 | + global $wgMaxArticleSize; |
| 358 | + |
| 359 | + return array_merge( parent::getPossibleErrors(), array( |
| 360 | + array( 'missingparam', 'title' ), |
| 361 | + array( 'missingtext' ), |
| 362 | + array( 'invalidtitle', 'title' ), |
| 363 | + array( 'createonly-exists' ), |
| 364 | + array( 'nocreate-missing' ), |
| 365 | + array( 'nosuchrevid', 'undo' ), |
| 366 | + array( 'nosuchrevid', 'undoafter' ), |
| 367 | + array( 'revwrongpage', 'id', 'text' ), |
| 368 | + array( 'undo-failure' ), |
| 369 | + array( 'hashcheckfailed' ), |
| 370 | + array( 'hookaborted' ), |
| 371 | + array( 'noimageredirect-anon' ), |
| 372 | + array( 'noimageredirect-logged' ), |
| 373 | + array( 'spamdetected', 'spam' ), |
| 374 | + array( 'filtered' ), |
| 375 | + array( 'blockedtext' ), |
| 376 | + array( 'contenttoobig', $wgMaxArticleSize ), |
| 377 | + array( 'noedit-anon' ), |
| 378 | + array( 'noedit' ), |
| 379 | + array( 'actionthrottledtext' ), |
| 380 | + array( 'wasdeleted' ), |
| 381 | + array( 'nocreate-loggedin' ), |
| 382 | + array( 'blankpage' ), |
| 383 | + array( 'editconflict' ), |
| 384 | + array( 'emptynewsection' ), |
| 385 | + array( 'unknownerror', 'retval' ), |
| 386 | + array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ), |
| 387 | + array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ), |
| 388 | + ) ); |
| 389 | + } |
| 390 | + |
| 391 | + protected function getAllowedParams() { |
| 392 | + return array( |
| 393 | + 'user' => null, |
| 394 | + 'title' => null, |
| 395 | + 'section' => null, |
| 396 | + 'text' => null, |
| 397 | + 'token' => null, |
| 398 | + 'summary' => null, |
| 399 | + 'minor' => false, |
| 400 | + 'notminor' => false, |
| 401 | + 'bot' => false, |
| 402 | + 'basetimestamp' => null, |
| 403 | + 'starttimestamp' => null, |
| 404 | + 'recreate' => false, |
| 405 | + 'createonly' => false, |
| 406 | + 'nocreate' => false, |
| 407 | + 'captchaword' => null, |
| 408 | + 'captchaid' => null, |
| 409 | + 'watch' => array( |
| 410 | + ApiBase::PARAM_DFLT => false, |
| 411 | + ApiBase::PARAM_DEPRECATED => true, |
| 412 | + ), |
| 413 | + 'unwatch' => array( |
| 414 | + ApiBase::PARAM_DFLT => false, |
| 415 | + ApiBase::PARAM_DEPRECATED => true, |
| 416 | + ), |
| 417 | + 'watchlist' => array( |
| 418 | + ApiBase::PARAM_DFLT => 'preferences', |
| 419 | + ApiBase::PARAM_TYPE => array( |
| 420 | + 'watch', |
| 421 | + 'unwatch', |
| 422 | + 'preferences', |
| 423 | + 'nochange' |
| 424 | + ), |
| 425 | + ), |
| 426 | + 'md5' => null, |
| 427 | + 'prependtext' => null, |
| 428 | + 'appendtext' => null, |
| 429 | + 'undo' => array( |
| 430 | + ApiBase::PARAM_TYPE => 'integer' |
| 431 | + ), |
| 432 | + 'undoafter' => array( |
| 433 | + ApiBase::PARAM_TYPE => 'integer' |
| 434 | + ), |
| 435 | + ); |
| 436 | + } |
| 437 | + |
| 438 | + protected function getParamDescription() { |
| 439 | + $p = $this->getModulePrefix(); |
| 440 | + return array( |
| 441 | + 'user' => 'Username', |
| 442 | + 'title' => 'Page title', |
| 443 | + 'section' => 'Section number. 0 for the top section, \'new\' for a new section', |
| 444 | + 'text' => 'Page content', |
| 445 | + 'token' => 'Edit token. You can get one of these through prop=info', |
| 446 | + 'summary' => 'Edit summary. Also section title when section=new', |
| 447 | + 'minor' => 'Minor edit', |
| 448 | + 'notminor' => 'Non-minor edit', |
| 449 | + 'bot' => 'Mark this edit as bot', |
| 450 | + 'basetimestamp' => array( 'Timestamp of the base revision (gotten through prop=revisions&rvprop=timestamp).', |
| 451 | + 'Used to detect edit conflicts; leave unset to ignore conflicts.' |
| 452 | + ), |
| 453 | + 'starttimestamp' => array( 'Timestamp when you obtained the edit token.', |
| 454 | + 'Used to detect edit conflicts; leave unset to ignore conflicts' |
| 455 | + ), |
| 456 | + 'recreate' => 'Override any errors about the article having been deleted in the meantime', |
| 457 | + 'createonly' => 'Don\'t edit the page if it exists already', |
| 458 | + 'nocreate' => 'Throw an error if the page doesn\'t exist', |
| 459 | + 'watch' => 'Add the page to your watchlist', |
| 460 | + 'unwatch' => 'Remove the page from your watchlist', |
| 461 | + 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', |
| 462 | + 'captchaid' => 'CAPTCHA ID from previous request', |
| 463 | + 'captchaword' => 'Answer to the CAPTCHA', |
| 464 | + 'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.", |
| 465 | + 'If set, the edit won\'t be done unless the hash is correct' ), |
| 466 | + 'prependtext' => "Add this text to the beginning of the page. Overrides {$p}text", |
| 467 | + 'appendtext' => "Add this text to the end of the page. Overrides {$p}text", |
| 468 | + 'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext", |
| 469 | + 'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision', |
| 470 | + ); |
| 471 | + } |
| 472 | + |
| 473 | + public function getTokenSalt() { |
| 474 | + return ''; |
| 475 | + } |
| 476 | + |
| 477 | + protected function getExamples() { |
| 478 | + return array( |
| 479 | + 'Edit a page (anonymous user):', |
| 480 | + ' api.php?action=edit&title=Test&summary=test%20summary&text=article%20content&basetimestamp=20070824123454&token=%2B\\', |
| 481 | + 'Prepend __NOTOC__ to a page (anonymous user):', |
| 482 | + ' api.php?action=edit&title=Test&summary=NOTOC&minor&prependtext=__NOTOC__%0A&basetimestamp=20070824123454&token=%2B\\', |
| 483 | + 'Undo r13579 through r13585 with autosummary (anonymous user):', |
| 484 | + ' api.php?action=edit&title=Test&undo=13585&undoafter=13579&basetimestamp=20070824123454&token=%2B\\', |
| 485 | + ); |
| 486 | + } |
| 487 | + |
| 488 | + public function getVersion() { |
| 489 | + return __CLASS__ . ': $Id: ApiMirrorEditPage.php 68353 2010-06-21 13:13:32Z hartman $'; |
| 490 | + } |
| 491 | +} |
\ No newline at end of file |
Index: trunk/extensions/MirrorTools/MirrorTools.classes.php |
— | — | @@ -0,0 +1,318 @@ |
| 2 | +<?php |
| 3 | +class MirrorEditPage extends EditPage { |
| 4 | + |
| 5 | + /** |
| 6 | + * Attempt submission (no UI) |
| 7 | + * @return one of the constants describing the result |
| 8 | + */ |
| 9 | + function mirrorinternalAttemptSave( &$result, $bot = false, $mirrorUser ) { |
| 10 | + global $wgFilterCallback, $wgUser, $wgOut, $wgParser; |
| 11 | + global $wgMaxArticleSize; |
| 12 | + $user = User::newFromName ( $mirrorUser, true ); |
| 13 | + wfProfileIn( __METHOD__ ); |
| 14 | + wfProfileIn( __METHOD__ . '-checks' ); |
| 15 | + |
| 16 | + if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) { |
| 17 | + wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); |
| 18 | + return self::AS_HOOK_ERROR; |
| 19 | + } |
| 20 | + |
| 21 | + # Check image redirect |
| 22 | + if ( $this->mTitle->getNamespace() == NS_FILE && |
| 23 | + Title::newFromRedirect( $this->textbox1 ) instanceof Title && |
| 24 | + !$wgUser->isAllowed( 'upload' ) ) { |
| 25 | + if ( $wgUser->isAnon() ) { |
| 26 | + return self::AS_IMAGE_REDIRECT_ANON; |
| 27 | + } else { |
| 28 | + return self::AS_IMAGE_REDIRECT_LOGGED; |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + # Check for spam |
| 33 | + $match = self::matchSummarySpamRegex( $this->summary ); |
| 34 | + if ( $match === false ) { |
| 35 | + $match = self::matchSpamRegex( $this->textbox1 ); |
| 36 | + } |
| 37 | + if ( $match !== false ) { |
| 38 | + $result['spam'] = $match; |
| 39 | + $ip = wfGetIP(); |
| 40 | + $pdbk = $this->mTitle->getPrefixedDBkey(); |
| 41 | + $match = str_replace( "\n", '', $match ); |
| 42 | + wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); |
| 43 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 44 | + wfProfileOut( __METHOD__ ); |
| 45 | + return self::AS_SPAM_ERROR; |
| 46 | + } |
| 47 | + if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) { |
| 48 | + # Error messages or other handling should be performed by the filter function |
| 49 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 50 | + wfProfileOut( __METHOD__ ); |
| 51 | + return self::AS_FILTERING; |
| 52 | + } |
| 53 | + if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) { |
| 54 | + # Error messages etc. could be handled within the hook... |
| 55 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 56 | + wfProfileOut( __METHOD__ ); |
| 57 | + return self::AS_HOOK_ERROR; |
| 58 | + } elseif ( $this->hookError != '' ) { |
| 59 | + # ...or the hook could be expecting us to produce an error |
| 60 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 61 | + wfProfileOut( __METHOD__ ); |
| 62 | + return self::AS_HOOK_ERROR_EXPECTED; |
| 63 | + } |
| 64 | + if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { |
| 65 | + # Check block state against master, thus 'false'. |
| 66 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 67 | + wfProfileOut( __METHOD__ ); |
| 68 | + return self::AS_BLOCKED_PAGE_FOR_USER; |
| 69 | + } |
| 70 | + $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); |
| 71 | + if ( $this->kblength > $wgMaxArticleSize ) { |
| 72 | + // Error will be displayed by showEditForm() |
| 73 | + $this->tooBig = true; |
| 74 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 75 | + wfProfileOut( __METHOD__ ); |
| 76 | + return self::AS_CONTENT_TOO_BIG; |
| 77 | + } |
| 78 | + |
| 79 | + if ( !$wgUser->isAllowed( 'edit' ) ) { |
| 80 | + if ( $wgUser->isAnon() ) { |
| 81 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 82 | + wfProfileOut( __METHOD__ ); |
| 83 | + return self::AS_READ_ONLY_PAGE_ANON; |
| 84 | + } else { |
| 85 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 86 | + wfProfileOut( __METHOD__ ); |
| 87 | + return self::AS_READ_ONLY_PAGE_LOGGED; |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + if ( wfReadOnly() ) { |
| 92 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 93 | + wfProfileOut( __METHOD__ ); |
| 94 | + return self::AS_READ_ONLY_PAGE; |
| 95 | + } |
| 96 | + if ( $wgUser->pingLimiter() ) { |
| 97 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 98 | + wfProfileOut( __METHOD__ ); |
| 99 | + return self::AS_RATE_LIMITED; |
| 100 | + } |
| 101 | + |
| 102 | + # If the article has been deleted while editing, don't save it without |
| 103 | + # confirmation |
| 104 | + if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) { |
| 105 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 106 | + wfProfileOut( __METHOD__ ); |
| 107 | + return self::AS_ARTICLE_WAS_DELETED; |
| 108 | + } |
| 109 | + |
| 110 | + wfProfileOut( __METHOD__ . '-checks' ); |
| 111 | + |
| 112 | + # If article is new, insert it. |
| 113 | + $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); |
| 114 | + if ( 0 == $aid ) { |
| 115 | + // Late check for create permission, just in case *PARANOIA* |
| 116 | + if ( !$this->mTitle->userCan( 'create' ) ) { |
| 117 | + wfDebug( __METHOD__ . ": no create permission\n" ); |
| 118 | + wfProfileOut( __METHOD__ ); |
| 119 | + return self::AS_NO_CREATE_PERMISSION; |
| 120 | + } |
| 121 | + |
| 122 | + # Don't save a new article if it's blank. |
| 123 | + if ( $this->textbox1 == '' ) { |
| 124 | + wfProfileOut( __METHOD__ ); |
| 125 | + return self::AS_BLANK_ARTICLE; |
| 126 | + } |
| 127 | + |
| 128 | + // Run post-section-merge edit filter |
| 129 | + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { |
| 130 | + # Error messages etc. could be handled within the hook... |
| 131 | + wfProfileOut( __METHOD__ ); |
| 132 | + return self::AS_HOOK_ERROR; |
| 133 | + } elseif ( $this->hookError != '' ) { |
| 134 | + # ...or the hook could be expecting us to produce an error |
| 135 | + wfProfileOut( __METHOD__ ); |
| 136 | + return self::AS_HOOK_ERROR_EXPECTED; |
| 137 | + } |
| 138 | + |
| 139 | + # Handle the user preference to force summaries here. Check if it's not a redirect. |
| 140 | + if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) { |
| 141 | + if ( md5( $this->summary ) == $this->autoSumm ) { |
| 142 | + $this->missingSummary = true; |
| 143 | + wfProfileOut( __METHOD__ ); |
| 144 | + return self::AS_SUMMARY_NEEDED; |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + $isComment = ( $this->section == 'new' ); |
| 149 | + |
| 150 | + # FIXME: paste contents from Article::insertNewArticle here and |
| 151 | + # actually handle errors it may return |
| 152 | + $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | |
| 153 | + ( $isminor ? EDIT_MINOR : 0 ) | |
| 154 | + ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) | |
| 155 | + ( $bot ? EDIT_FORCE_BOT : 0 ); |
| 156 | + |
| 157 | + $this->mArticle->doEdit( $this->textbox1, $this->summary, $flags, false, $user, $this->watchthis, $isComment, '', true ); |
| 158 | + wfProfileOut( __METHOD__ ); |
| 159 | + return self::AS_SUCCESS_NEW_ARTICLE; |
| 160 | + } |
| 161 | + |
| 162 | + # Article exists. Check for edit conflict. |
| 163 | + |
| 164 | + $this->mArticle->clear(); # Force reload of dates, etc. |
| 165 | + $this->mArticle->forUpdate( true ); # Lock the article |
| 166 | + |
| 167 | + wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" ); |
| 168 | + |
| 169 | + if ( $this->mArticle->getTimestamp() != $this->edittime ) { |
| 170 | + $this->isConflict = true; |
| 171 | + if ( $this->section == 'new' ) { |
| 172 | + if ( $this->mArticle->getUserText() == $wgUser->getName() && |
| 173 | + $this->mArticle->getComment() == $this->summary ) { |
| 174 | + // Probably a duplicate submission of a new comment. |
| 175 | + // This can happen when squid resends a request after |
| 176 | + // a timeout but the first one actually went through. |
| 177 | + wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); |
| 178 | + } else { |
| 179 | + // New comment; suppress conflict. |
| 180 | + $this->isConflict = false; |
| 181 | + wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); |
| 182 | + } |
| 183 | + } |
| 184 | + } |
| 185 | + $userid = $wgUser->getId(); |
| 186 | + |
| 187 | + # Suppress edit conflict with self, except for section edits where merging is required. |
| 188 | + if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) { |
| 189 | + wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); |
| 190 | + $this->isConflict = false; |
| 191 | + } |
| 192 | + |
| 193 | + if ( $this->isConflict ) { |
| 194 | + wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" . |
| 195 | + $this->mArticle->getTimestamp() . "')\n" ); |
| 196 | + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); |
| 197 | + } else { |
| 198 | + wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); |
| 199 | + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); |
| 200 | + } |
| 201 | + if ( is_null( $text ) ) { |
| 202 | + wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); |
| 203 | + $this->isConflict = true; |
| 204 | + $text = $this->textbox1; // do not try to merge here! |
| 205 | + } else if ( $this->isConflict ) { |
| 206 | + # Attempt merge |
| 207 | + if ( $this->mergeChangesInto( $text ) ) { |
| 208 | + // Successful merge! Maybe we should tell the user the good news? |
| 209 | + $this->isConflict = false; |
| 210 | + wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); |
| 211 | + } else { |
| 212 | + $this->section = ''; |
| 213 | + $this->textbox1 = $text; |
| 214 | + wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + if ( $this->isConflict ) { |
| 219 | + wfProfileOut( __METHOD__ ); |
| 220 | + return self::AS_CONFLICT_DETECTED; |
| 221 | + } |
| 222 | + |
| 223 | + $oldtext = $this->mArticle->getContent(); |
| 224 | + |
| 225 | + // Run post-section-merge edit filter |
| 226 | + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { |
| 227 | + # Error messages etc. could be handled within the hook... |
| 228 | + wfProfileOut( __METHOD__ ); |
| 229 | + return self::AS_HOOK_ERROR; |
| 230 | + } elseif ( $this->hookError != '' ) { |
| 231 | + # ...or the hook could be expecting us to produce an error |
| 232 | + wfProfileOut( __METHOD__ ); |
| 233 | + return self::AS_HOOK_ERROR_EXPECTED; |
| 234 | + } |
| 235 | + |
| 236 | + # Handle the user preference to force summaries here, but not for null edits |
| 237 | + if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp( $oldtext, $text ) |
| 238 | + && !Title::newFromRedirect( $text ) ) # check if it's not a redirect |
| 239 | + { |
| 240 | + if ( md5( $this->summary ) == $this->autoSumm ) { |
| 241 | + $this->missingSummary = true; |
| 242 | + wfProfileOut( __METHOD__ ); |
| 243 | + return self::AS_SUMMARY_NEEDED; |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + # And a similar thing for new sections |
| 248 | + if ( $this->section == 'new' && !$this->allowBlankSummary ) { |
| 249 | + if ( trim( $this->summary ) == '' ) { |
| 250 | + $this->missingSummary = true; |
| 251 | + wfProfileOut( __METHOD__ ); |
| 252 | + return self::AS_SUMMARY_NEEDED; |
| 253 | + } |
| 254 | + } |
| 255 | + |
| 256 | + # All's well |
| 257 | + wfProfileIn( __METHOD__ . '-sectionanchor' ); |
| 258 | + $sectionanchor = ''; |
| 259 | + if ( $this->section == 'new' ) { |
| 260 | + if ( $this->textbox1 == '' ) { |
| 261 | + $this->missingComment = true; |
| 262 | + wfProfileOut( __METHOD__ . '-sectionanchor' ); |
| 263 | + wfProfileOut( __METHOD__ ); |
| 264 | + return self::AS_TEXTBOX_EMPTY; |
| 265 | + } |
| 266 | + if ( $this->summary != '' ) { |
| 267 | + $sectionanchor = $wgParser->guessSectionNameFromWikiText( $this->summary ); |
| 268 | + # This is a new section, so create a link to the new section |
| 269 | + # in the revision summary. |
| 270 | + $cleanSummary = $wgParser->stripSectionName( $this->summary ); |
| 271 | + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); |
| 272 | + } |
| 273 | + } elseif ( $this->section != '' ) { |
| 274 | + # Try to get a section anchor from the section source, redirect to edited section if header found |
| 275 | + # XXX: might be better to integrate this into Article::replaceSection |
| 276 | + # for duplicate heading checking and maybe parsing |
| 277 | + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); |
| 278 | + # we can't deal with anchors, includes, html etc in the header for now, |
| 279 | + # headline would need to be parsed to improve this |
| 280 | + if ( $hasmatch and strlen( $matches[2] ) > 0 ) { |
| 281 | + $sectionanchor = $wgParser->guessSectionNameFromWikiText( $matches[2] ); |
| 282 | + } |
| 283 | + } |
| 284 | + wfProfileOut( __METHOD__ . '-sectionanchor' ); |
| 285 | + |
| 286 | + // Save errors may fall down to the edit form, but we've now |
| 287 | + // merged the section into full text. Clear the section field |
| 288 | + // so that later submission of conflict forms won't try to |
| 289 | + // replace that into a duplicated mess. |
| 290 | + $this->textbox1 = $text; |
| 291 | + $this->section = ''; |
| 292 | + |
| 293 | + // Check for length errors again now that the section is merged in |
| 294 | + $this->kblength = (int)( strlen( $text ) / 1024 ); |
| 295 | + if ( $this->kblength > $wgMaxArticleSize ) { |
| 296 | + $this->tooBig = true; |
| 297 | + wfProfileOut( __METHOD__ ); |
| 298 | + return self::AS_MAX_ARTICLE_SIZE_EXCEEDED; |
| 299 | + } |
| 300 | + |
| 301 | + // Update the article here |
| 302 | + $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | |
| 303 | + ( $this->minoredit ? EDIT_MINOR : 0 ) | |
| 304 | + ( $bot ? EDIT_FORCE_BOT : 0 ); |
| 305 | + $status = $this->mArticle->doEdit( $text, $this->summary, $flags, |
| 306 | + false, $userObj, $this->watchthis, false, $sectionanchor, true ); |
| 307 | + |
| 308 | + if ( $status->isOK() ) |
| 309 | + { |
| 310 | + wfProfileOut( __METHOD__ ); |
| 311 | + return self::AS_SUCCESS_UPDATE; |
| 312 | + } else { |
| 313 | + $this->isConflict = true; |
| 314 | + $result = $status->getErrorsArray(); |
| 315 | + } |
| 316 | + wfProfileOut( __METHOD__ ); |
| 317 | + return self::AS_END; |
| 318 | + } |
| 319 | +} |
\ No newline at end of file |