r69198 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r69197‎ | r69198 | r69199 >
Date:22:11, 8 July 2010
Author:tisane
Status:deferred (Comments)
Tags:
Comment:
Add MirrorTools extension
Modified paths:
  • /trunk/extensions/MirrorTools (added) (history)
  • /trunk/extensions/MirrorTools/APIMirrorTools.php (added) (history)
  • /trunk/extensions/MirrorTools/MirrorTools.classes.php (added) (history)
  • /trunk/extensions/MirrorTools/MirrorTools.i18n.php (added) (history)
  • /trunk/extensions/MirrorTools/MirrorTools.php (added) (history)

Diff [purge]

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

Follow-up revisions

RevisionCommit summaryAuthorDate
r69561Followup r69198 for Tisane...reedy19:08, 19 July 2010

Comments

#Comment by Reedy (talk | contribs)   05:57, 9 July 2010

You look to be duplicating a LOT of code verbatim...

#Comment by Tisane (talk | contribs)   13:54, 16 July 2010

This is true. I'm not sure if there's a better way to do this. Only a few aspects of the code are changing; e.g. a $mirrorUser parameter is added to internalAttemptSave().

#Comment by Tisane (talk | contribs)   18:35, 19 July 2010

I mean, we could just add such a parameter to the core and have it default to null. Do you think there would be any support for doing that?

#Comment by Reedy (talk | contribs)   19:10, 19 July 2010

See r69561 for example - It removes a lot of the duplicated code that isn't in execute()

Status & tagging log