123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- <?php
- /**
- * Copyright © 2007 Iker Labarga "<Firstname><Lastname>@gmail.com"
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
- use MediaWiki\MediaWikiServices;
- use MediaWiki\Revision\RevisionRecord;
- /**
- * A module that allows for editing and creating pages.
- *
- * Currently, this wraps around the EditPage class in an ugly way,
- * EditPage.php should be rewritten to provide a cleaner interface,
- * see T20654 if you're inspired to fix this.
- *
- * @ingroup API
- */
- class ApiEditPage extends ApiBase {
- public function execute() {
- $this->useTransactionalTimeLimit();
- $user = $this->getUser();
- $params = $this->extractRequestParams();
- $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
- $pageObj = $this->getTitleOrPageId( $params );
- $titleObj = $pageObj->getTitle();
- $apiResult = $this->getResult();
- if ( $params['redirect'] ) {
- if ( $params['prependtext'] === null && $params['appendtext'] === null
- && $params['section'] !== 'new'
- ) {
- $this->dieWithError( 'apierror-redirect-appendonly' );
- }
- if ( $titleObj->isRedirect() ) {
- $oldTitle = $titleObj;
- $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
- ->getContent( RevisionRecord::FOR_THIS_USER, $user )
- ->getRedirectChain();
- // array_shift( $titles );
- $redirValues = [];
- /** @var Title $newTitle */
- foreach ( $titles as $id => $newTitle ) {
- $titles[$id - 1] = $titles[$id - 1] ?? $oldTitle;
- $redirValues[] = [
- 'from' => $titles[$id - 1]->getPrefixedText(),
- 'to' => $newTitle->getPrefixedText()
- ];
- $titleObj = $newTitle;
- // T239428: Check whether the new title is valid
- if ( $titleObj->isExternal() || !$titleObj->canExist() ) {
- $redirValues[count( $redirValues ) - 1]['to'] = $titleObj->getFullText();
- $this->dieWithError(
- [
- 'apierror-edit-invalidredirect',
- Message::plaintextParam( $oldTitle->getPrefixedText() ),
- Message::plaintextParam( $titleObj->getFullText() ),
- ],
- 'edit-invalidredirect',
- [ 'redirects' => $redirValues ]
- );
- }
- }
- ApiResult::setIndexedTagName( $redirValues, 'r' );
- $apiResult->addValue( null, 'redirects', $redirValues );
- // Since the page changed, update $pageObj
- $pageObj = WikiPage::factory( $titleObj );
- }
- }
- if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
- $contentHandler = $pageObj->getContentHandler();
- } else {
- $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
- }
- $contentModel = $contentHandler->getModelID();
- $name = $titleObj->getPrefixedDBkey();
- $model = $contentHandler->getModelID();
- if ( $params['undo'] > 0 ) {
- // allow undo via api
- } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
- $this->dieWithError( [ 'apierror-no-direct-editing', $model, $name ] );
- }
- if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
- $contentFormat = $contentHandler->getDefaultFormat();
- } else {
- $contentFormat = $params['contentformat'];
- }
- if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
- $this->dieWithError( [ 'apierror-badformat', $contentFormat, $model, $name ] );
- }
- if ( $params['createonly'] && $titleObj->exists() ) {
- $this->dieWithError( 'apierror-articleexists' );
- }
- if ( $params['nocreate'] && !$titleObj->exists() ) {
- $this->dieWithError( 'apierror-missingtitle' );
- }
- // Now let's check whether we're even allowed to do this
- $this->checkTitleUserPermissions(
- $titleObj,
- $titleObj->exists() ? 'edit' : [ 'edit', 'create' ],
- [ 'autoblock' => true ]
- );
- $toMD5 = $params['text'];
- if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) {
- $content = $pageObj->getContent();
- if ( !$content ) {
- if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
- # If this is a MediaWiki:x message, then load the messages
- # and return the message value for x.
- $text = $titleObj->getDefaultMessageText();
- if ( $text === false ) {
- $text = '';
- }
- try {
- $content = ContentHandler::makeContent( $text, $titleObj );
- } catch ( MWContentSerializationException $ex ) {
- $this->dieWithException( $ex, [
- 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
- ] );
- return;
- }
- } else {
- # Otherwise, make a new empty content.
- $content = $contentHandler->makeEmptyContent();
- }
- }
- // @todo Add support for appending/prepending to the Content interface
- if ( !( $content instanceof TextContent ) ) {
- $modelName = $contentHandler->getModelID();
- $this->dieWithError( [ 'apierror-appendnotsupported', $modelName ] );
- }
- if ( !is_null( $params['section'] ) ) {
- if ( !$contentHandler->supportsSections() ) {
- $modelName = $contentHandler->getModelID();
- $this->dieWithError( [ 'apierror-sectionsnotsupported', $modelName ] );
- }
- if ( $params['section'] == 'new' ) {
- // DWIM if they're trying to prepend/append to a new section.
- $content = null;
- } else {
- // Process the content for section edits
- $section = $params['section'];
- $content = $content->getSection( $section );
- if ( !$content ) {
- $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
- }
- }
- }
- if ( !$content ) {
- $text = '';
- } else {
- $text = $content->serialize( $contentFormat );
- }
- $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
- $toMD5 = $params['prependtext'] . $params['appendtext'];
- }
- if ( $params['undo'] > 0 ) {
- if ( $params['undoafter'] > 0 ) {
- if ( $params['undo'] < $params['undoafter'] ) {
- list( $params['undo'], $params['undoafter'] ) =
- [ $params['undoafter'], $params['undo'] ];
- }
- $undoafterRev = Revision::newFromId( $params['undoafter'] );
- }
- $undoRev = Revision::newFromId( $params['undo'] );
- if ( is_null( $undoRev ) || $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
- $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
- }
- if ( $params['undoafter'] == 0 ) {
- $undoafterRev = $undoRev->getPrevious();
- }
- if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
- $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
- }
- if ( $undoRev->getPage() != $pageObj->getId() ) {
- $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
- $titleObj->getPrefixedText() ] );
- }
- if ( $undoafterRev->getPage() != $pageObj->getId() ) {
- $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
- $titleObj->getPrefixedText() ] );
- }
- $newContent = $contentHandler->getUndoContent(
- $pageObj->getRevision(),
- $undoRev,
- $undoafterRev
- );
- if ( !$newContent ) {
- $this->dieWithError( 'undo-failure', 'undofailure' );
- }
- if ( empty( $params['contentmodel'] )
- && empty( $params['contentformat'] )
- ) {
- // If we are reverting content model, the new content model
- // might not support the current serialization format, in
- // which case go back to the old serialization format,
- // but only if the user hasn't specified a format/model
- // parameter.
- if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
- $contentFormat = $undoafterRev->getContentFormat();
- }
- // Override content model with model of undid revision.
- $contentModel = $newContent->getModel();
- }
- $params['text'] = $newContent->serialize( $contentFormat );
- // If no summary was given and we only undid one rev,
- // use an autosummary
- if ( is_null( $params['summary'] ) ) {
- $nextRev = MediaWikiServices::getInstance()->getRevisionLookup()
- ->getNextRevision( $undoafterRev->getRevisionRecord() );
- if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
- $params['summary'] = wfMessage( 'undo-summary' )
- ->params( $params['undo'], $undoRev->getUserText() )
- ->inContentLanguage()->text();
- }
- }
- }
- // See if the MD5 hash checks out
- if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] ) {
- $this->dieWithError( 'apierror-badmd5' );
- }
- // EditPage wants to parse its stuff from a WebRequest
- // That interface kind of sucks, but it's workable
- $requestArray = [
- 'wpTextbox1' => $params['text'],
- 'format' => $contentFormat,
- 'model' => $contentModel,
- 'wpEditToken' => $params['token'],
- 'wpIgnoreBlankSummary' => true,
- 'wpIgnoreBlankArticle' => true,
- 'wpIgnoreSelfRedirect' => true,
- 'bot' => $params['bot'],
- 'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
- ];
- if ( !is_null( $params['summary'] ) ) {
- $requestArray['wpSummary'] = $params['summary'];
- }
- if ( !is_null( $params['sectiontitle'] ) ) {
- $requestArray['wpSectionTitle'] = $params['sectiontitle'];
- }
- // TODO: Pass along information from 'undoafter' as well
- if ( $params['undo'] > 0 ) {
- $requestArray['wpUndidRevision'] = $params['undo'];
- }
- // Watch out for basetimestamp == '' or '0'
- // It gets treated as NOW, almost certainly causing an edit conflict
- if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
- $requestArray['wpEdittime'] = $params['basetimestamp'];
- } else {
- $requestArray['wpEdittime'] = $pageObj->getTimestamp();
- }
- if ( $params['starttimestamp'] !== null ) {
- $requestArray['wpStarttime'] = $params['starttimestamp'];
- } else {
- $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
- }
- if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) {
- $requestArray['wpMinoredit'] = '';
- }
- if ( $params['recreate'] ) {
- $requestArray['wpRecreate'] = '';
- }
- if ( !is_null( $params['section'] ) ) {
- $section = $params['section'];
- if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
- $this->dieWithError( 'apierror-invalidsection' );
- }
- $content = $pageObj->getContent();
- if ( $section !== '0' && $section != 'new'
- && ( !$content || !$content->getSection( $section ) )
- ) {
- $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
- }
- $requestArray['wpSection'] = $params['section'];
- } else {
- $requestArray['wpSection'] = '';
- }
- $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj );
- // Deprecated parameters
- if ( $params['watch'] ) {
- $watch = true;
- } elseif ( $params['unwatch'] ) {
- $watch = false;
- }
- if ( $watch ) {
- $requestArray['wpWatchthis'] = '';
- }
- // Apply change tags
- if ( $params['tags'] ) {
- $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
- if ( $tagStatus->isOK() ) {
- $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
- } else {
- $this->dieStatus( $tagStatus );
- }
- }
- // Pass through anything else we might have been given, to support extensions
- // This is kind of a hack but it's the best we can do to make extensions work
- $requestArray += $this->getRequest()->getValues();
- global $wgTitle, $wgRequest;
- $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
- // Some functions depend on $wgTitle == $ep->mTitle
- // TODO: Make them not or check if they still do
- $wgTitle = $titleObj;
- $articleContext = new RequestContext;
- $articleContext->setRequest( $req );
- $articleContext->setWikiPage( $pageObj );
- $articleContext->setUser( $this->getUser() );
- /** @var Article $articleObject */
- $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
- $ep = new EditPage( $articleObject );
- $ep->setApiEditOverride( true );
- $ep->setContextTitle( $titleObj );
- $ep->importFormData( $req );
- $content = $ep->textbox1;
- // Do the actual save
- $oldRevId = $articleObject->getRevIdFetched();
- $result = null;
- // Fake $wgRequest for some hooks inside EditPage
- // @todo FIXME: This interface SUCKS
- $oldRequest = $wgRequest;
- $wgRequest = $req;
- $status = $ep->attemptSave( $result );
- $wgRequest = $oldRequest;
- $r = [];
- switch ( $status->value ) {
- case EditPage::AS_HOOK_ERROR:
- case EditPage::AS_HOOK_ERROR_EXPECTED:
- if ( isset( $status->apiHookResult ) ) {
- $r = $status->apiHookResult;
- $r['result'] = 'Failure';
- $apiResult->addValue( null, $this->getModuleName(), $r );
- return;
- }
- if ( !$status->getErrors() ) {
- // This appears to be unreachable right now, because all
- // code paths will set an error. Could change, though.
- $status->fatal( 'hookaborted' ); //@codeCoverageIgnore
- }
- $this->dieStatus( $status );
- // These two cases will normally have been caught earlier, and will
- // only occur if something blocks the user between the earlier
- // check and the check in EditPage (presumably a hook). It's not
- // obvious that this is even possible.
- // @codeCoverageIgnoreStart
- case EditPage::AS_BLOCKED_PAGE_FOR_USER:
- $this->dieBlocked( $user->getBlock() );
- case EditPage::AS_READ_ONLY_PAGE:
- $this->dieReadOnly();
- // @codeCoverageIgnoreEnd
- case EditPage::AS_SUCCESS_NEW_ARTICLE:
- $r['new'] = true;
- // fall-through
- case EditPage::AS_SUCCESS_UPDATE:
- $r['result'] = 'Success';
- $r['pageid'] = (int)$titleObj->getArticleID();
- $r['title'] = $titleObj->getPrefixedText();
- $r['contentmodel'] = $articleObject->getContentModel();
- $newRevId = $articleObject->getLatest();
- if ( $newRevId == $oldRevId ) {
- $r['nochange'] = true;
- } else {
- $r['oldrevid'] = (int)$oldRevId;
- $r['newrevid'] = (int)$newRevId;
- $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
- $pageObj->getTimestamp() );
- }
- break;
- default:
- if ( !$status->getErrors() ) {
- // EditPage sometimes only sets the status code without setting
- // any actual error messages. Supply defaults for those cases.
- switch ( $status->value ) {
- // Currently needed
- case EditPage::AS_IMAGE_REDIRECT_ANON:
- $status->fatal( 'apierror-noimageredirect-anon' );
- break;
- case EditPage::AS_IMAGE_REDIRECT_LOGGED:
- $status->fatal( 'apierror-noimageredirect' );
- break;
- case EditPage::AS_CONTENT_TOO_BIG:
- case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
- $status->fatal( 'apierror-contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) );
- break;
- case EditPage::AS_READ_ONLY_PAGE_ANON:
- $status->fatal( 'apierror-noedit-anon' );
- break;
- case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
- $status->fatal( 'apierror-cantchangecontentmodel' );
- break;
- case EditPage::AS_ARTICLE_WAS_DELETED:
- $status->fatal( 'apierror-pagedeleted' );
- break;
- case EditPage::AS_CONFLICT_DETECTED:
- $status->fatal( 'editconflict' );
- break;
- // Currently shouldn't be needed, but here in case
- // hooks use them without setting appropriate
- // errors on the status.
- // @codeCoverageIgnoreStart
- case EditPage::AS_SPAM_ERROR:
- $status->fatal( 'apierror-spamdetected', $result['spam'] );
- break;
- case EditPage::AS_READ_ONLY_PAGE_LOGGED:
- $status->fatal( 'apierror-noedit' );
- break;
- case EditPage::AS_RATE_LIMITED:
- $status->fatal( 'apierror-ratelimited' );
- break;
- case EditPage::AS_NO_CREATE_PERMISSION:
- $status->fatal( 'nocreate-loggedin' );
- break;
- case EditPage::AS_BLANK_ARTICLE:
- $status->fatal( 'apierror-emptypage' );
- break;
- case EditPage::AS_TEXTBOX_EMPTY:
- $status->fatal( 'apierror-emptynewsection' );
- break;
- case EditPage::AS_SUMMARY_NEEDED:
- $status->fatal( 'apierror-summaryrequired' );
- break;
- default:
- wfWarn( __METHOD__ . ": Unknown EditPage code {$status->value} with no message" );
- $status->fatal( 'apierror-unknownerror-editpage', $status->value );
- break;
- // @codeCoverageIgnoreEnd
- }
- }
- $this->dieStatus( $status );
- }
- $apiResult->addValue( null, $this->getModuleName(), $r );
- }
- public function mustBePosted() {
- return true;
- }
- public function isWriteMode() {
- return true;
- }
- public function getAllowedParams() {
- return [
- 'title' => [
- ApiBase::PARAM_TYPE => 'string',
- ],
- 'pageid' => [
- ApiBase::PARAM_TYPE => 'integer',
- ],
- 'section' => null,
- 'sectiontitle' => [
- ApiBase::PARAM_TYPE => 'string',
- ],
- 'text' => [
- ApiBase::PARAM_TYPE => 'text',
- ],
- 'summary' => null,
- 'tags' => [
- ApiBase::PARAM_TYPE => 'tags',
- ApiBase::PARAM_ISMULTI => true,
- ],
- 'minor' => false,
- 'notminor' => false,
- 'bot' => false,
- 'basetimestamp' => [
- ApiBase::PARAM_TYPE => 'timestamp',
- ],
- 'starttimestamp' => [
- ApiBase::PARAM_TYPE => 'timestamp',
- ],
- 'recreate' => false,
- 'createonly' => false,
- 'nocreate' => false,
- 'watch' => [
- ApiBase::PARAM_DFLT => false,
- ApiBase::PARAM_DEPRECATED => true,
- ],
- 'unwatch' => [
- ApiBase::PARAM_DFLT => false,
- ApiBase::PARAM_DEPRECATED => true,
- ],
- 'watchlist' => [
- ApiBase::PARAM_DFLT => 'preferences',
- ApiBase::PARAM_TYPE => [
- 'watch',
- 'unwatch',
- 'preferences',
- 'nochange'
- ],
- ],
- 'md5' => null,
- 'prependtext' => [
- ApiBase::PARAM_TYPE => 'text',
- ],
- 'appendtext' => [
- ApiBase::PARAM_TYPE => 'text',
- ],
- 'undo' => [
- ApiBase::PARAM_TYPE => 'integer',
- ApiBase::PARAM_MIN => 0,
- ApiBase::PARAM_RANGE_ENFORCE => true,
- ],
- 'undoafter' => [
- ApiBase::PARAM_TYPE => 'integer',
- ApiBase::PARAM_MIN => 0,
- ApiBase::PARAM_RANGE_ENFORCE => true,
- ],
- 'redirect' => [
- ApiBase::PARAM_TYPE => 'boolean',
- ApiBase::PARAM_DFLT => false,
- ],
- 'contentformat' => [
- ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
- ],
- 'contentmodel' => [
- ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
- ],
- 'token' => [
- // Standard definition automatically inserted
- ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
- ],
- ];
- }
- public function needsToken() {
- return 'csrf';
- }
- protected function getExamplesMessages() {
- return [
- 'action=edit&title=Test&summary=test%20summary&' .
- 'text=article%20content&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
- => 'apihelp-edit-example-edit',
- 'action=edit&title=Test&summary=NOTOC&minor=&' .
- 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
- => 'apihelp-edit-example-prepend',
- 'action=edit&title=Test&undo=13585&undoafter=13579&' .
- 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
- => 'apihelp-edit-example-undo',
- ];
- }
- public function getHelpUrls() {
- return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
- }
- }
|