protected function getMessageParameters() { $lang = $this->context->getLanguage(); $params = parent::getMessageParameters(); $params[3] = ContentHandler::getLocalizedName($params[3], $lang); $params[4] = ContentHandler::getLocalizedName($params[4], $lang); return $params; }
/** * @dataProvider dataGetLocalizedName */ public function testGetLocalizedName($id, $expected) { $name = ContentHandler::getLocalizedName($id); if ($expected) { $this->assertNotNull($name, "no name found for content model {$id}"); $this->assertTrue(preg_match($expected, $name) > 0, "content model name for #{$id} did not match pattern {$expected}"); } else { $this->assertEquals($id, $name, "localization of unknown model {$id} should have " . "fallen back to use the model id directly."); } }
/** * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * * @param $content Content: new content * @param string $summary edit summary * @param $flags Integer bitfield: * EDIT_NEW * Article is known or assumed to be non-existent, create a new one * EDIT_UPDATE * Article is known or assumed to be pre-existing, update it * EDIT_MINOR * Mark this edit minor, if the user is allowed to do so * EDIT_SUPPRESS_RC * Do not log the change in recentchanges * EDIT_FORCE_BOT * Mark the edit a "bot" edit regardless of user rights * EDIT_DEFER_UPDATES * Defer some of the updates until the end of index.php * EDIT_AUTOSUMMARY * Fill in blank summaries with generated text where possible * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an * edit-already-exists error will be returned. These two conditions are also possible with * auto-detection due to MediaWiki's performance-optimised locking strategy. * * @param bool|int $baseRevId the revision ID this edit was based off, if any * @param $user User the user doing the edit * @param $serialisation_format String: format for storing the content in the database * * @throws MWException * @return Status object. Possible errors: * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status * edit-gone-missing: In update mode, but the article didn't exist * edit-conflict: In update mode, the article changed unexpectedly * edit-no-change: Warning that the text was the same as before * edit-already-exists: In creation mode, but the article already exists * * Extensions may define additional errors. * * $return->value will contain an associative array with members as follows: * new: Boolean indicating if the function attempted to create a new article * revision: The revision object for the inserted revision, or null * * @since 1.21 */ public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, User $user = null, $serialisation_format = null ) { global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; // Low-level sanity check if ( $this->mTitle->getText() === '' ) { throw new MWException( 'Something is trying to edit an article with an empty title' ); } wfProfileIn( __METHOD__ ); if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { wfProfileOut( __METHOD__ ); return Status::newFatal( 'content-not-allowed-here', ContentHandler::getLocalizedName( $content->getModel() ), $this->getTitle()->getPrefixedText() ); } $user = is_null( $user ) ? $wgUser : $user; $status = Status::newGood( array() ); // Load the data from the master database if needed. // The caller may already loaded it from the master or even loaded it using // SELECT FOR UPDATE, so do not override that using clear(). $this->loadPageData( 'fromdbmaster' ); $flags = $this->checkFlags( $flags ); // handle hook $hook_args = array( &$this, &$user, &$content, &$summary, $flags & EDIT_MINOR, null, null, &$flags, &$status ); if ( !wfRunHooks( 'PageContentSave', $hook_args ) || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); if ( $status->isOK() ) { $status->fatal( 'edit-hook-aborted' ); } wfProfileOut( __METHOD__ ); return $status; } // Silently ignore EDIT_MINOR if not allowed $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); $bot = $flags & EDIT_FORCE_BOT; $old_content = $this->getContent( Revision::RAW ); // current revision's content $oldsize = $old_content ? $old_content->getSize() : 0; $oldid = $this->getLatest(); $oldIsRedirect = $this->isRedirect(); $oldcountable = $this->isCountable(); $handler = $content->getContentHandler(); // Provide autosummaries if one is not provided and autosummaries are enabled. if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { if ( !$old_content ) { $old_content = null; } $summary = $handler->getAutosummary( $old_content, $content, $flags ); } $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); $serialized = $editInfo->pst; /** * @var Content $content */ $content = $editInfo->pstContent; $newsize = $content->getSize(); $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); $this->mTimestamp = $now; if ( $flags & EDIT_UPDATE ) { // Update article, but only if changed. $status->value['new'] = false; if ( !$oldid ) { // Article gone missing wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); $status->fatal( 'edit-gone-missing' ); wfProfileOut( __METHOD__ ); return $status; } elseif ( !$old_content ) { // Sanity check for bug 37225 wfProfileOut( __METHOD__ ); throw new MWException( "Could not find text for current revision {$oldid}." ); } $revision = new Revision( array( 'page' => $this->getId(), 'title' => $this->getTitle(), // for determining the default content model 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'parent_id' => $oldid, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialisation_format, ) ); // XXX: pass content object?! $changed = !$content->equals( $old_content ); if ( $changed ) { if ( !$content->isValid() ) { wfProfileOut( __METHOD__ ); throw new MWException( "New content failed validity check!" ); } $dbw->begin( __METHOD__ ); $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); $status->merge( $prepStatus ); if ( !$status->isOK() ) { $dbw->rollback( __METHOD__ ); wfProfileOut( __METHOD__ ); return $status; } $revisionId = $revision->insertOn( $dbw ); // Update page // // Note that we use $this->mLatest instead of fetching a value from the master DB // during the course of this function. This makes sure that EditPage can detect // edit conflicts reliably, either by $ok here, or by $article->getTimestamp() // before this function is called. A previous function used a separate query, this // creates a window where concurrent edits can cause an ignored edit conflict. $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); if ( !$ok ) { // Belated edit conflict! Run away!! $status->fatal( 'edit-conflict' ); $dbw->rollback( __METHOD__ ); wfProfileOut( __METHOD__ ); return $status; } wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); // Update recentchanges if ( !( $flags & EDIT_SUPPRESS_RC ) ) { // Mark as patrolled if the user can do so $patrolled = $wgUseRCPatrol && !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); // Add RC row to the DB $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId, $patrolled ); // Log auto-patrolled edits if ( $patrolled ) { PatrolLog::record( $rc, true, $user ); } } $user->incEditCount(); $dbw->commit( __METHOD__ ); } else { // Bug 32948: revision ID must be set to page {{REVISIONID}} and // related variables correctly $revision->setId( $this->getLatest() ); } // Update links tables, site stats, etc. $this->doEditUpdates( $revision, $user, array( 'changed' => $changed, 'oldcountable' => $oldcountable ) ); if ( !$changed ) { $status->warning( 'edit-no-change' ); $revision = null; // Update page_touched, this is usually implicit in the page update // Other cache updates are done in onArticleEdit() $this->mTitle->invalidateCache(); } } else { // Create new article $status->value['new'] = true; $dbw->begin( __METHOD__ ); $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); $status->merge( $prepStatus ); if ( !$status->isOK() ) { $dbw->rollback( __METHOD__ ); wfProfileOut( __METHOD__ ); return $status; } $status->merge( $prepStatus ); // Add the page record; stake our claim on this title! // This will return false if the article already exists $newid = $this->insertOn( $dbw ); if ( $newid === false ) { $dbw->rollback( __METHOD__ ); $status->fatal( 'edit-already-exists' ); wfProfileOut( __METHOD__ ); return $status; } // Save the revision text... $revision = new Revision( array( 'page' => $newid, 'title' => $this->getTitle(), // for determining the default content model 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialisation_format, ) ); $revisionId = $revision->insertOn( $dbw ); // Bug 37225: use accessor to get the text as Revision may trim it $content = $revision->getContent(); // sanity; get normalized version if ( $content ) { $newsize = $content->getSize(); } // Update the page record with revision data $this->updateRevisionOn( $dbw, $revision, 0 ); wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); // Update recentchanges if ( !( $flags & EDIT_SUPPRESS_RC ) ) { // Mark as patrolled if the user can do so $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); // Add RC row to the DB $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, '', $newsize, $revisionId, $patrolled ); // Log auto-patrolled edits if ( $patrolled ) { PatrolLog::record( $rc, true, $user ); } } $user->incEditCount(); $dbw->commit( __METHOD__ ); // Update links, etc. $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); $hook_args = array( &$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision ); ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); wfRunHooks( 'PageContentInsertComplete', $hook_args ); } // Do updates right now unless deferral was requested if ( !( $flags & EDIT_DEFER_UPDATES ) ) { DeferredUpdates::doUpdates(); } // Return the new revision (or null) to the caller $status->value['revision'] = $revision; $hook_args = array( &$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); wfRunHooks( 'PageContentSaveComplete', $hook_args ); // Promote user to any groups they meet the criteria for $user->addAutopromoteOnceGroups( 'onEdit' ); wfProfileOut( __METHOD__ ); return $status; }
/** * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * * @param Content $content New content * @param string $summary Edit summary * @param int $flags Bitfield: * EDIT_NEW * Article is known or assumed to be non-existent, create a new one * EDIT_UPDATE * Article is known or assumed to be pre-existing, update it * EDIT_MINOR * Mark this edit minor, if the user is allowed to do so * EDIT_SUPPRESS_RC * Do not log the change in recentchanges * EDIT_FORCE_BOT * Mark the edit a "bot" edit regardless of user rights * EDIT_AUTOSUMMARY * Fill in blank summaries with generated text where possible * EDIT_INTERNAL * Signal that the page retrieve/save cycle happened entirely in this request. * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the * article will be detected. If EDIT_UPDATE is specified and the article * doesn't exist, the function will return an edit-gone-missing error. If * EDIT_NEW is specified and the article does exist, an edit-already-exists * error will be returned. These two conditions are also possible with * auto-detection due to MediaWiki's performance-optimised locking strategy. * * @param bool|int $baseRevId The revision ID this edit was based off, if any. * This is not the parent revision ID, rather the revision ID for older * content used as the source for a rollback, for example. * @param User $user The user doing the edit * @param string $serialFormat Format for storing the content in the * database. * @param array|null $tags Change tags to apply to this edit * Callers are responsible for permission checks * (with ChangeTags::canAddTagsAccompanyingChange) * * @throws MWException * @return Status Possible errors: * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't * set the fatal flag of $status. * edit-gone-missing: In update mode, but the article didn't exist. * edit-conflict: In update mode, the article changed unexpectedly. * edit-no-change: Warning that the text was the same as before. * edit-already-exists: In creation mode, but the article already exists. * * Extensions may define additional errors. * * $return->value will contain an associative array with members as follows: * new: Boolean indicating if the function attempted to create a new article. * revision: The revision object for the inserted revision, or null. * * @since 1.21 * @throws MWException */ public function doEditContent(Content $content, $summary, $flags = 0, $baseRevId = false, User $user = null, $serialFormat = null, $tags = []) { global $wgUser, $wgUseAutomaticEditSummaries; // Old default parameter for $tags was null if ($tags === null) { $tags = []; } // Low-level sanity check if ($this->mTitle->getText() === '') { throw new MWException('Something is trying to edit an article with an empty title'); } // Make sure the given content type is allowed for this page if (!$content->getContentHandler()->canBeUsedOn($this->mTitle)) { return Status::newFatal('content-not-allowed-here', ContentHandler::getLocalizedName($content->getModel()), $this->mTitle->getPrefixedText()); } // Load the data from the master database if needed. // The caller may already loaded it from the master or even loaded it using // SELECT FOR UPDATE, so do not override that using clear(). $this->loadPageData('fromdbmaster'); $user = $user ?: $wgUser; $flags = $this->checkFlags($flags); // Trigger pre-save hook (using provided edit summary) $hookStatus = Status::newGood([]); $hook_args = [&$this, &$user, &$content, &$summary, $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus]; // Check if the hook rejected the attempted save if (!Hooks::run('PageContentSave', $hook_args) || !ContentHandler::runLegacyHooks('ArticleSave', $hook_args, '1.21')) { if ($hookStatus->isOK()) { // Hook returned false but didn't call fatal(); use generic message $hookStatus->fatal('edit-hook-aborted'); } return $hookStatus; } $old_revision = $this->getRevision(); // current revision $old_content = $this->getContent(Revision::RAW); // current revision's content if ($old_content && $old_content->getModel() !== $content->getModel()) { $tags[] = 'mw-contentmodelchange'; } // Provide autosummaries if one is not provided and autosummaries are enabled if ($wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '') { $handler = $content->getContentHandler(); $summary = $handler->getAutosummary($old_content, $content, $flags); } // Avoid statsd noise and wasted cycles check the edit stash (T136678) if ($flags & EDIT_INTERNAL || $flags & EDIT_FORCE_BOT) { $useCache = false; } else { $useCache = true; } // Get the pre-save transform content and final parser output $editInfo = $this->prepareContentForEdit($content, null, $user, $serialFormat, $useCache); $pstContent = $editInfo->pstContent; // Content object $meta = ['bot' => $flags & EDIT_FORCE_BOT, 'minor' => $flags & EDIT_MINOR && $user->isAllowed('minoredit'), 'serialized' => $editInfo->pst, 'serialFormat' => $serialFormat, 'baseRevId' => $baseRevId, 'oldRevision' => $old_revision, 'oldContent' => $old_content, 'oldId' => $this->getLatest(), 'oldIsRedirect' => $this->isRedirect(), 'oldCountable' => $this->isCountable(), 'tags' => $tags !== null ? (array) $tags : []]; // Actually create the revision and create/update the page if ($flags & EDIT_UPDATE) { $status = $this->doModify($pstContent, $flags, $user, $summary, $meta); } else { $status = $this->doCreate($pstContent, $flags, $user, $summary, $meta); } // Promote user to any groups they meet the criteria for DeferredUpdates::addCallableUpdate(function () use($user) { $user->addAutopromoteOnceGroups('onEdit'); $user->addAutopromoteOnceGroups('onView'); // b/c }); return $status; }
public function onSubmit(array $data) { global $wgContLang; if ($data['pagetitle'] === '') { // Initial form view of special page, pass return false; } // At this point, it has to be a POST request. This is enforced by HTMLForm, // but lets be safe verify that. if (!$this->getRequest()->wasPosted()) { throw new RuntimeException("Form submission was not POSTed"); } $this->title = Title::newFromText($data['pagetitle']); $user = $this->getUser(); // Check permissions and make sure the user has permission to edit the specific page $errors = $this->title->getUserPermissionsErrors('editcontentmodel', $user); $errors = wfMergeErrorArrays($errors, $this->title->getUserPermissionsErrors('edit', $user)); if ($errors) { $out = $this->getOutput(); $wikitext = $out->formatPermissionsErrorMessage($errors); // Hack to get our wikitext parsed return Status::newFatal(new RawMessage('$1', array($wikitext))); } $page = WikiPage::factory($this->title); if ($this->oldRevision === null) { $this->oldRevision = $page->getRevision() ?: false; } $oldModel = $this->title->getContentModel(); if ($this->oldRevision) { $oldContent = $this->oldRevision->getContent(); try { $newContent = ContentHandler::makeContent($oldContent->getNativeData(), $this->title, $data['model']); } catch (MWException $e) { return Status::newFatal($this->msg('changecontentmodel-cannot-convert')->params($this->title->getPrefixedText(), ContentHandler::getLocalizedName($data['model']))); } } else { // Page doesn't exist, create an empty content object $newContent = ContentHandler::getForModelID($data['model'])->makeEmptyContent(); } $flags = $this->oldRevision ? EDIT_UPDATE : EDIT_NEW; if ($user->isAllowed('bot')) { $flags |= EDIT_FORCE_BOT; } $log = new ManualLogEntry('contentmodel', 'change'); $log->setPerformer($user); $log->setTarget($this->title); $log->setComment($data['reason']); $log->setParameters(array('4::oldmodel' => $oldModel, '5::newmodel' => $data['model'])); $formatter = LogFormatter::newFromEntry($log); $formatter->setContext(RequestContext::newExtraneousContext($this->title)); $reason = $formatter->getPlainActionText(); if ($data['reason'] !== '') { $reason .= $this->msg('colon-separator')->inContentLanguage()->text() . $data['reason']; } # Truncate for whole multibyte characters. $reason = $wgContLang->truncate($reason, 255); $status = $page->doEditContent($newContent, $reason, $flags, $this->oldRevision ? $this->oldRevision->getId() : false, $user); if (!$status->isOK()) { return $status; } $logid = $log->insert(); $log->publish($logid); return $status; }
/** * Returns page information in an easily-manipulated format. Array keys are used so extensions * may add additional information in arbitrary positions. Array values are arrays with one * element to be rendered as a header, arrays with two elements to be rendered as a table row. * * @return array */ protected function pageInfo() { global $wgContLang; $user = $this->getUser(); $lang = $this->getLanguage(); $title = $this->getTitle(); $id = $title->getArticleID(); $config = $this->context->getConfig(); $cache = ObjectCache::getMainWANInstance(); $memcKey = wfMemcKey('infoaction', sha1($title->getPrefixedText()), $this->page->getLatest()); $pageCounts = $cache->get($memcKey); $version = isset($pageCounts['cacheversion']) ? $pageCounts['cacheversion'] : false; if ($pageCounts === false || $version !== self::CACHE_VERSION) { // Get page information that would be too "expensive" to retrieve by normal means $pageCounts = $this->pageCounts($title); $pageCounts['cacheversion'] = self::CACHE_VERSION; $cache->set($memcKey, $pageCounts); } // Get page properties $dbr = wfGetDB(DB_SLAVE); $result = $dbr->select('page_props', array('pp_propname', 'pp_value'), array('pp_page' => $id), __METHOD__); $pageProperties = array(); foreach ($result as $row) { $pageProperties[$row->pp_propname] = $row->pp_value; } // Basic information $pageInfo = array(); $pageInfo['header-basic'] = array(); // Display title $displayTitle = $title->getPrefixedText(); if (isset($pageProperties['displaytitle'])) { $displayTitle = $pageProperties['displaytitle']; } $pageInfo['header-basic'][] = array($this->msg('pageinfo-display-title'), $displayTitle); // Is it a redirect? If so, where to? if ($title->isRedirect()) { $pageInfo['header-basic'][] = array($this->msg('pageinfo-redirectsto'), Linker::link($this->page->getRedirectTarget()) . $this->msg('word-separator')->escaped() . $this->msg('parentheses')->rawParams(Linker::link($this->page->getRedirectTarget(), $this->msg('pageinfo-redirectsto-info')->escaped(), array(), array('action' => 'info')))->escaped()); } // Default sort key $sortKey = $title->getCategorySortkey(); if (isset($pageProperties['defaultsort'])) { $sortKey = $pageProperties['defaultsort']; } $sortKey = htmlspecialchars($sortKey); $pageInfo['header-basic'][] = array($this->msg('pageinfo-default-sort'), $sortKey); // Page length (in bytes) $pageInfo['header-basic'][] = array($this->msg('pageinfo-length'), $lang->formatNum($title->getLength())); // Page ID (number not localised, as it's a database ID) $pageInfo['header-basic'][] = array($this->msg('pageinfo-article-id'), $id); // Language in which the page content is (supposed to be) written $pageLang = $title->getPageLanguage()->getCode(); if ($config->get('PageLanguageUseDB') && $this->getTitle()->userCan('pagelang', $this->getUser())) { // Link to Special:PageLanguage with pre-filled page title if user has permissions $titleObj = SpecialPage::getTitleFor('PageLanguage', $title->getPrefixedText()); $langDisp = Linker::link($titleObj, $this->msg('pageinfo-language')->escaped()); } else { // Display just the message $langDisp = $this->msg('pageinfo-language')->escaped(); } $pageInfo['header-basic'][] = array($langDisp, Language::fetchLanguageName($pageLang, $lang->getCode()) . ' ' . $this->msg('parentheses', $pageLang)->escaped()); // Content model of the page $pageInfo['header-basic'][] = array($this->msg('pageinfo-content-model'), htmlspecialchars(ContentHandler::getLocalizedName($title->getContentModel()))); // Search engine status $pOutput = new ParserOutput(); if (isset($pageProperties['noindex'])) { $pOutput->setIndexPolicy('noindex'); } if (isset($pageProperties['index'])) { $pOutput->setIndexPolicy('index'); } // Use robot policy logic $policy = $this->page->getRobotPolicy('view', $pOutput); $pageInfo['header-basic'][] = array($this->msg('pageinfo-robot-policy'), $this->msg("pageinfo-robot-{$policy['index']}")); $unwatchedPageThreshold = $config->get('UnwatchedPageThreshold'); if ($user->isAllowed('unwatchedpages') || $unwatchedPageThreshold !== false && $pageCounts['watchers'] >= $unwatchedPageThreshold) { // Number of page watchers $pageInfo['header-basic'][] = array($this->msg('pageinfo-watchers'), $lang->formatNum($pageCounts['watchers'])); if ($config->get('ShowUpdatedMarker') && isset($pageCounts['visitingWatchers'])) { $minToDisclose = $config->get('UnwatchedPageSecret'); if ($pageCounts['visitingWatchers'] > $minToDisclose || $user->isAllowed('unwatchedpages')) { $pageInfo['header-basic'][] = array($this->msg('pageinfo-visiting-watchers'), $lang->formatNum($pageCounts['visitingWatchers'])); } else { $pageInfo['header-basic'][] = array($this->msg('pageinfo-visiting-watchers'), $this->msg('pageinfo-few-visiting-watchers')); } } } elseif ($unwatchedPageThreshold !== false) { $pageInfo['header-basic'][] = array($this->msg('pageinfo-watchers'), $this->msg('pageinfo-few-watchers')->numParams($unwatchedPageThreshold)); } // Redirects to this page $whatLinksHere = SpecialPage::getTitleFor('Whatlinkshere', $title->getPrefixedText()); $pageInfo['header-basic'][] = array(Linker::link($whatLinksHere, $this->msg('pageinfo-redirects-name')->escaped(), array(), array('hidelinks' => 1, 'hidetrans' => 1, 'hideimages' => $title->getNamespace() == NS_FILE)), $this->msg('pageinfo-redirects-value')->numParams(count($title->getRedirectsHere()))); // Is it counted as a content page? if ($this->page->isCountable()) { $pageInfo['header-basic'][] = array($this->msg('pageinfo-contentpage'), $this->msg('pageinfo-contentpage-yes')); } // Subpages of this page, if subpages are enabled for the current NS if (MWNamespace::hasSubpages($title->getNamespace())) { $prefixIndex = SpecialPage::getTitleFor('Prefixindex', $title->getPrefixedText() . '/'); $pageInfo['header-basic'][] = array(Linker::link($prefixIndex, $this->msg('pageinfo-subpages-name')->escaped()), $this->msg('pageinfo-subpages-value')->numParams($pageCounts['subpages']['total'], $pageCounts['subpages']['redirects'], $pageCounts['subpages']['nonredirects'])); } if ($title->inNamespace(NS_CATEGORY)) { $category = Category::newFromTitle($title); // $allCount is the total number of cat members, // not the count of how many members are normal pages. $allCount = (int) $category->getPageCount(); $subcatCount = (int) $category->getSubcatCount(); $fileCount = (int) $category->getFileCount(); $pagesCount = $allCount - $subcatCount - $fileCount; $pageInfo['category-info'] = array(array($this->msg('pageinfo-category-total'), $lang->formatNum($allCount)), array($this->msg('pageinfo-category-pages'), $lang->formatNum($pagesCount)), array($this->msg('pageinfo-category-subcats'), $lang->formatNum($subcatCount)), array($this->msg('pageinfo-category-files'), $lang->formatNum($fileCount))); } // Page protection $pageInfo['header-restrictions'] = array(); // Is this page affected by the cascading protection of something which includes it? if ($title->isCascadeProtected()) { $cascadingFrom = ''; $sources = $title->getCascadeProtectionSources(); // Array deferencing is in PHP 5.4 :( foreach ($sources[0] as $sourceTitle) { $cascadingFrom .= Html::rawElement('li', array(), Linker::linkKnown($sourceTitle)); } $cascadingFrom = Html::rawElement('ul', array(), $cascadingFrom); $pageInfo['header-restrictions'][] = array($this->msg('pageinfo-protect-cascading-from'), $cascadingFrom); } // Is out protection set to cascade to other pages? if ($title->areRestrictionsCascading()) { $pageInfo['header-restrictions'][] = array($this->msg('pageinfo-protect-cascading'), $this->msg('pageinfo-protect-cascading-yes')); } // Page protection foreach ($title->getRestrictionTypes() as $restrictionType) { $protectionLevel = implode(', ', $title->getRestrictions($restrictionType)); if ($protectionLevel == '') { // Allow all users $message = $this->msg('protect-default')->escaped(); } else { // Administrators only // Messages: protect-level-autoconfirmed, protect-level-sysop $message = $this->msg("protect-level-{$protectionLevel}"); if ($message->isDisabled()) { // Require "$1" permission $message = $this->msg("protect-fallback", $protectionLevel)->parse(); } else { $message = $message->escaped(); } } $expiry = $title->getRestrictionExpiry($restrictionType); $formattedexpiry = $this->msg('parentheses', $this->getLanguage()->formatExpiry($expiry))->escaped(); $message .= $this->msg('word-separator')->escaped() . $formattedexpiry; // Messages: restriction-edit, restriction-move, restriction-create, // restriction-upload $pageInfo['header-restrictions'][] = array($this->msg("restriction-{$restrictionType}"), $message); } if (!$this->page->exists()) { return $pageInfo; } // Edit history $pageInfo['header-edits'] = array(); $firstRev = $this->page->getOldestRevision(); $lastRev = $this->page->getRevision(); $batch = new LinkBatch(); if ($firstRev) { $firstRevUser = $firstRev->getUserText(Revision::FOR_THIS_USER); if ($firstRevUser !== '') { $batch->add(NS_USER, $firstRevUser); $batch->add(NS_USER_TALK, $firstRevUser); } } if ($lastRev) { $lastRevUser = $lastRev->getUserText(Revision::FOR_THIS_USER); if ($lastRevUser !== '') { $batch->add(NS_USER, $lastRevUser); $batch->add(NS_USER_TALK, $lastRevUser); } } $batch->execute(); if ($firstRev) { // Page creator $pageInfo['header-edits'][] = array($this->msg('pageinfo-firstuser'), Linker::revUserTools($firstRev)); // Date of page creation $pageInfo['header-edits'][] = array($this->msg('pageinfo-firsttime'), Linker::linkKnown($title, htmlspecialchars($lang->userTimeAndDate($firstRev->getTimestamp(), $user)), array(), array('oldid' => $firstRev->getId()))); } if ($lastRev) { // Latest editor $pageInfo['header-edits'][] = array($this->msg('pageinfo-lastuser'), Linker::revUserTools($lastRev)); // Date of latest edit $pageInfo['header-edits'][] = array($this->msg('pageinfo-lasttime'), Linker::linkKnown($title, htmlspecialchars($lang->userTimeAndDate($this->page->getTimestamp(), $user)), array(), array('oldid' => $this->page->getLatest()))); } // Total number of edits $pageInfo['header-edits'][] = array($this->msg('pageinfo-edits'), $lang->formatNum($pageCounts['edits'])); // Total number of distinct authors if ($pageCounts['authors'] > 0) { $pageInfo['header-edits'][] = array($this->msg('pageinfo-authors'), $lang->formatNum($pageCounts['authors'])); } // Recent number of edits (within past 30 days) $pageInfo['header-edits'][] = array($this->msg('pageinfo-recent-edits', $lang->formatDuration($config->get('RCMaxAge'))), $lang->formatNum($pageCounts['recent_edits'])); // Recent number of distinct authors $pageInfo['header-edits'][] = array($this->msg('pageinfo-recent-authors'), $lang->formatNum($pageCounts['recent_authors'])); // Array of MagicWord objects $magicWords = MagicWord::getDoubleUnderscoreArray(); // Array of magic word IDs $wordIDs = $magicWords->names; // Array of IDs => localized magic words $localizedWords = $wgContLang->getMagicWords(); $listItems = array(); foreach ($pageProperties as $property => $value) { if (in_array($property, $wordIDs)) { $listItems[] = Html::element('li', array(), $localizedWords[$property][1]); } } $localizedList = Html::rawElement('ul', array(), implode('', $listItems)); $hiddenCategories = $this->page->getHiddenCategories(); if (count($listItems) > 0 || count($hiddenCategories) > 0 || $pageCounts['transclusion']['from'] > 0 || $pageCounts['transclusion']['to'] > 0) { $options = array('LIMIT' => $config->get('PageInfoTransclusionLimit')); $transcludedTemplates = $title->getTemplateLinksFrom($options); if ($config->get('MiserMode')) { $transcludedTargets = array(); } else { $transcludedTargets = $title->getTemplateLinksTo($options); } // Page properties $pageInfo['header-properties'] = array(); // Magic words if (count($listItems) > 0) { $pageInfo['header-properties'][] = array($this->msg('pageinfo-magic-words')->numParams(count($listItems)), $localizedList); } // Hidden categories if (count($hiddenCategories) > 0) { $pageInfo['header-properties'][] = array($this->msg('pageinfo-hidden-categories')->numParams(count($hiddenCategories)), Linker::formatHiddenCategories($hiddenCategories)); } // Transcluded templates if ($pageCounts['transclusion']['from'] > 0) { if ($pageCounts['transclusion']['from'] > count($transcludedTemplates)) { $more = $this->msg('morenotlisted')->escaped(); } else { $more = null; } $pageInfo['header-properties'][] = array($this->msg('pageinfo-templates')->numParams($pageCounts['transclusion']['from']), Linker::formatTemplates($transcludedTemplates, false, false, $more)); } if (!$config->get('MiserMode') && $pageCounts['transclusion']['to'] > 0) { if ($pageCounts['transclusion']['to'] > count($transcludedTargets)) { $more = Linker::link($whatLinksHere, $this->msg('moredotdotdot')->escaped(), array(), array('hidelinks' => 1, 'hideredirs' => 1)); } else { $more = null; } $pageInfo['header-properties'][] = array($this->msg('pageinfo-transclusions')->numParams($pageCounts['transclusion']['to']), Linker::formatTemplates($transcludedTargets, false, false, $more)); } } return $pageInfo; }
/** * Check whether a given move operation would be valid. * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise * * @param Title $nt The new title * @param bool $auth Indicates whether $wgUser's permissions * should be checked * @param string $reason Is the log summary of the move, used for spam checking * @return array|bool True on success, getUserPermissionsErrors()-like array on failure */ public function isValidMoveOperation(&$nt, $auth = true, $reason = '') { global $wgUser, $wgContentHandlerUseDB; $errors = array(); if (!$nt) { // Normally we'd add this to $errors, but we'll get // lots of syntax errors if $nt is not an object return array(array('badtitletext')); } if ($this->equals($nt)) { $errors[] = array('selfmove'); } if (!$this->isMovable()) { $errors[] = array('immobile-source-namespace', $this->getNsText()); } if ($nt->isExternal()) { $errors[] = array('immobile-target-namespace-iw'); } if (!$nt->isMovable()) { $errors[] = array('immobile-target-namespace', $nt->getNsText()); } $oldid = $this->getArticleID(); $newid = $nt->getArticleID(); if (strlen($nt->getDBkey()) < 1) { $errors[] = array('articleexists'); } if ($this->getDBkey() == '' || !$oldid || $nt->getDBkey() == '') { $errors[] = array('badarticleerror'); } // Content model checks if (!$wgContentHandlerUseDB && $this->getContentModel() !== $nt->getContentModel()) { // can't move a page if that would change the page's content model $errors[] = array('bad-target-model', ContentHandler::getLocalizedName($this->getContentModel()), ContentHandler::getLocalizedName($nt->getContentModel())); } // Image-specific checks if ($this->getNamespace() == NS_FILE) { $errors = array_merge($errors, $this->validateFileMoveOperation($nt)); } if ($nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE) { $errors[] = array('nonfile-cannot-move-to-file'); } if ($auth) { $errors = wfMergeErrorArrays($errors, $this->getUserPermissionsErrors('move', $wgUser), $this->getUserPermissionsErrors('edit', $wgUser), $nt->getUserPermissionsErrors('move-target', $wgUser), $nt->getUserPermissionsErrors('edit', $wgUser)); } $match = EditPage::matchSummarySpamRegex($reason); if ($match !== false) { // This is kind of lame, won't display nice $errors[] = array('spamprotectiontext'); } $err = null; if (!wfRunHooks('AbortMove', array($this, $nt, $wgUser, &$err, $reason))) { $errors[] = array('hookaborted', $err); } # The move is allowed only if (1) the target doesn't exist, or # (2) the target is a redirect to the source, and has no history # (so we can undo bad moves right after they're done). if (0 != $newid) { # Target exists; check for validity if (!$this->isValidMoveTarget($nt)) { $errors[] = array('articleexists'); } } else { $tp = $nt->getTitleProtection(); $right = $tp['pt_create_perm']; if ($right == 'sysop') { $right = 'editprotected'; // B/C } if ($right == 'autoconfirmed') { $right = 'editsemiprotected'; // B/C } if ($tp and !$wgUser->isAllowed($right)) { $errors[] = array('cantmove-titleprotected'); } } if (empty($errors)) { return true; } return $errors; }
/** * Turns the given text into a Content object by unserializing it. * * If the resulting Content object is not of a type that can be edited using * the text base EditPage, an exception will be raised. Set * $this->allowNonTextContent to true to allow editing of non-textual * content. * * @param string|null|bool $text Text to unserialize * @return Content The content object created from $text. If $text was false * or null, false resp. null will be returned instead. * * @throws MWException If unserializing the text results in a Content * object that is not an instance of TextContent and * $this->allowNonTextContent is not true. */ protected function toEditContent($text) { if ($text === false || $text === null) { return $text; } $content = ContentHandler::makeContent($text, $this->getTitle(), $this->contentModel, $this->contentFormat); if (!$this->isSupportedContentModel($content->getModel())) { throw new MWException('This content model is not supported: ' . ContentHandler::getLocalizedName($content->getModel())); } return $content; }
/** * Does various sanity checks that the move is * valid. Only things based on the two titles * should be checked here. * * @return Status */ public function isValidMove() { global $wgContentHandlerUseDB; $status = new Status(); if ($this->oldTitle->equals($this->newTitle)) { $status->fatal('selfmove'); } if (!$this->oldTitle->isMovable()) { $status->fatal('immobile-source-namespace', $this->oldTitle->getNsText()); } if ($this->newTitle->isExternal()) { $status->fatal('immobile-target-namespace-iw'); } if (!$this->newTitle->isMovable()) { $status->fatal('immobile-target-namespace', $this->newTitle->getNsText()); } $oldid = $this->oldTitle->getArticleID(); if (strlen($this->newTitle->getDBkey()) < 1) { $status->fatal('articleexists'); } if ($this->oldTitle->getDBkey() == '' || !$oldid || $this->newTitle->getDBkey() == '') { $status->fatal('badarticleerror'); } # The move is allowed only if (1) the target doesn't exist, or # (2) the target is a redirect to the source, and has no history # (so we can undo bad moves right after they're done). if ($this->newTitle->getArticleID() && !$this->isValidMoveTarget()) { $status->fatal('articleexists'); } // Content model checks if (!$wgContentHandlerUseDB && $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel()) { // can't move a page if that would change the page's content model $status->fatal('bad-target-model', ContentHandler::getLocalizedName($this->oldTitle->getContentModel()), ContentHandler::getLocalizedName($this->newTitle->getContentModel())); } // Image-specific checks if ($this->oldTitle->inNamespace(NS_FILE)) { $status->merge($this->isValidFileMove()); } if ($this->newTitle->inNamespace(NS_FILE) && !$this->oldTitle->inNamespace(NS_FILE)) { $status->fatal('nonfile-cannot-move-to-file'); } // Hook for extensions to say a title can't be moved for technical reasons Hooks::run('MovePageIsValidMove', array($this->oldTitle, $this->newTitle, $status)); return $status; }
/** * Turns the given text into a Content object by unserializing it. * * If the resulting Content object is not of a type that can be edited using the text base EditPage, * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual * content. * * @param string|null|bool $text Text to unserialize * @return Content The content object created from $text. If $text was false or null, false resp. null will be * returned instead. * * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent * and $this->allowNonTextContent is not true. */ protected function toEditContent($text) { if ($text === false || $text === null) { return $text; } $content = ContentHandler::makeContent($text, $this->getTitle(), $this->contentModel, $this->contentFormat); if (!$this->allowNonTextContent && !$content instanceof TextContent) { throw new MWException("This content model can not be edited as text: " . ContentHandler::getLocalizedName($content->getModel())); } return $content; }
/** * Change an existing article or create a new article. Updates RC and all necessary caches, * optionally via the deferred update array. * * @param Content $content New content * @param string $summary Edit summary * @param int $flags Bitfield: * EDIT_NEW * Article is known or assumed to be non-existent, create a new one * EDIT_UPDATE * Article is known or assumed to be pre-existing, update it * EDIT_MINOR * Mark this edit minor, if the user is allowed to do so * EDIT_SUPPRESS_RC * Do not log the change in recentchanges * EDIT_FORCE_BOT * Mark the edit a "bot" edit regardless of user rights * EDIT_AUTOSUMMARY * Fill in blank summaries with generated text where possible * * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the * article will be detected. If EDIT_UPDATE is specified and the article * doesn't exist, the function will return an edit-gone-missing error. If * EDIT_NEW is specified and the article does exist, an edit-already-exists * error will be returned. These two conditions are also possible with * auto-detection due to MediaWiki's performance-optimised locking strategy. * * @param bool|int $baseRevId The revision ID this edit was based off, if any. * This is not the parent revision ID, rather the revision ID for older * content used as the source for a rollback, for example. * @param User $user The user doing the edit * @param string $serialFormat Format for storing the content in the * database. * * @throws MWException * @return Status Possible errors: * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't * set the fatal flag of $status. * edit-gone-missing: In update mode, but the article didn't exist. * edit-conflict: In update mode, the article changed unexpectedly. * edit-no-change: Warning that the text was the same as before. * edit-already-exists: In creation mode, but the article already exists. * * Extensions may define additional errors. * * $return->value will contain an associative array with members as follows: * new: Boolean indicating if the function attempted to create a new article. * revision: The revision object for the inserted revision, or null. * * @since 1.21 * @throws MWException */ public function doEditContent(Content $content, $summary, $flags = 0, $baseRevId = false, User $user = null, $serialFormat = null) { global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; // Low-level sanity check if ($this->mTitle->getText() === '') { throw new MWException('Something is trying to edit an article with an empty title'); } if (!$content->getContentHandler()->canBeUsedOn($this->getTitle())) { return Status::newFatal('content-not-allowed-here', ContentHandler::getLocalizedName($content->getModel()), $this->getTitle()->getPrefixedText()); } $user = is_null($user) ? $wgUser : $user; $status = Status::newGood(array()); // Load the data from the master database if needed. // The caller may already loaded it from the master or even loaded it using // SELECT FOR UPDATE, so do not override that using clear(). $this->loadPageData('fromdbmaster'); $flags = $this->checkFlags($flags); // handle hook $hook_args = array(&$this, &$user, &$content, &$summary, $flags & EDIT_MINOR, null, null, &$flags, &$status); if (!Hooks::run('PageContentSave', $hook_args) || !ContentHandler::runLegacyHooks('ArticleSave', $hook_args)) { wfDebug(__METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n"); if ($status->isOK()) { $status->fatal('edit-hook-aborted'); } return $status; } // Silently ignore EDIT_MINOR if not allowed $isminor = $flags & EDIT_MINOR && $user->isAllowed('minoredit'); $bot = $flags & EDIT_FORCE_BOT; $old_revision = $this->getRevision(); // current revision $old_content = $this->getContent(Revision::RAW); // current revision's content $oldsize = $old_content ? $old_content->getSize() : 0; $oldid = $this->getLatest(); $oldIsRedirect = $this->isRedirect(); $oldcountable = $this->isCountable(); $handler = $content->getContentHandler(); // Provide autosummaries if one is not provided and autosummaries are enabled. if ($wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '') { if (!$old_content) { $old_content = null; } $summary = $handler->getAutosummary($old_content, $content, $flags); } $editInfo = $this->prepareContentForEdit($content, null, $user, $serialFormat); $serialized = $editInfo->pst; /** * @var Content $content */ $content = $editInfo->pstContent; $newsize = $content->getSize(); $dbw = wfGetDB(DB_MASTER); $now = wfTimestampNow(); if ($flags & EDIT_UPDATE) { // Update article, but only if changed. $status->value['new'] = false; if (!$oldid) { // Article gone missing wfDebug(__METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n"); $status->fatal('edit-gone-missing'); return $status; } elseif (!$old_content) { // Sanity check for bug 37225 throw new MWException("Could not find text for current revision {$oldid}."); } $revision = new Revision(array('page' => $this->getId(), 'title' => $this->getTitle(), 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'parent_id' => $oldid, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialFormat)); // XXX: pass content object?! $changed = !$content->equals($old_content); if ($changed) { $prepStatus = $content->prepareSave($this, $flags, $oldid, $user); $status->merge($prepStatus); if (!$status->isOK()) { return $status; } $dbw->begin(__METHOD__); // Get the latest page_latest value while locking it. // Do a CAS style check to see if it's the same as when this method // started. If it changed then bail out before touching the DB. $latestNow = $this->lockAndGetLatest(); if ($latestNow != $oldid) { $dbw->commit(__METHOD__); // Page updated or deleted in the mean time $status->fatal('edit-conflict'); return $status; } // At this point we are now comitted to returning an OK // status unless some DB query error or other exception comes up. // This way callers don't have to call rollback() if $status is bad // unless they actually try to catch exceptions (which is rare). $revisionId = $revision->insertOn($dbw); // Update page_latest and friends to reflect the new revision if (!$this->updateRevisionOn($dbw, $revision, null, $oldIsRedirect)) { $dbw->rollback(__METHOD__); throw new MWException("Failed to update page row to use new revision."); } Hooks::run('NewRevisionFromEditComplete', array($this, $revision, $baseRevId, $user)); // Update recentchanges if (!($flags & EDIT_SUPPRESS_RC)) { // Mark as patrolled if the user can do so $patrolled = $wgUseRCPatrol && !count($this->mTitle->getUserPermissionsErrors('autopatrol', $user)); // Add RC row to the DB RecentChange::notifyEdit($now, $this->mTitle, $isminor, $user, $summary, $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, $revisionId, $patrolled); } $user->incEditCount(); $dbw->commit(__METHOD__); $this->mTimestamp = $now; } else { // Bug 32948: revision ID must be set to page {{REVISIONID}} and // related variables correctly $revision->setId($this->getLatest()); } // Update links tables, site stats, etc. $this->doEditUpdates($revision, $user, array('changed' => $changed, 'oldcountable' => $oldcountable, 'oldrevision' => $old_revision)); if (!$changed) { $status->warning('edit-no-change'); $revision = null; // Update page_touched, this is usually implicit in the page update // Other cache updates are done in onArticleEdit() $this->mTitle->invalidateCache($now); } } else { // Create new article $status->value['new'] = true; $prepStatus = $content->prepareSave($this, $flags, $oldid, $user); $status->merge($prepStatus); if (!$status->isOK()) { return $status; } $dbw->begin(__METHOD__); // Add the page record unless one already exists for the title $newid = $this->insertOn($dbw); if ($newid === false) { $dbw->commit(__METHOD__); // nothing inserted $status->fatal('edit-already-exists'); return $status; // nothing done } // At this point we are now comitted to returning an OK // status unless some DB query error or other exception comes up. // This way callers don't have to call rollback() if $status is bad // unless they actually try to catch exceptions (which is rare). // Save the revision text... $revision = new Revision(array('page' => $newid, 'title' => $this->getTitle(), 'comment' => $summary, 'minor_edit' => $isminor, 'text' => $serialized, 'len' => $newsize, 'user' => $user->getId(), 'user_text' => $user->getName(), 'timestamp' => $now, 'content_model' => $content->getModel(), 'content_format' => $serialFormat)); $revisionId = $revision->insertOn($dbw); // Bug 37225: use accessor to get the text as Revision may trim it $content = $revision->getContent(); // sanity; get normalized version if ($content) { $newsize = $content->getSize(); } // Update the page record with revision data if (!$this->updateRevisionOn($dbw, $revision, 0)) { $dbw->rollback(__METHOD__); throw new MWException("Failed to update page row to use new revision."); } Hooks::run('NewRevisionFromEditComplete', array($this, $revision, false, $user)); // Update recentchanges if (!($flags & EDIT_SUPPRESS_RC)) { // Mark as patrolled if the user can do so $patrolled = ($wgUseRCPatrol || $wgUseNPPatrol) && !count($this->mTitle->getUserPermissionsErrors('autopatrol', $user)); // Add RC row to the DB RecentChange::notifyNew($now, $this->mTitle, $isminor, $user, $summary, $bot, '', $newsize, $revisionId, $patrolled); } $user->incEditCount(); $dbw->commit(__METHOD__); $this->mTimestamp = $now; // Update links, etc. $this->doEditUpdates($revision, $user, array('created' => true, 'oldrevision' => $old_revision)); $hook_args = array(&$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision); ContentHandler::runLegacyHooks('ArticleInsertComplete', $hook_args); Hooks::run('PageContentInsertComplete', $hook_args); } // Return the new revision (or null) to the caller $status->value['revision'] = $revision; $hook_args = array(&$this, &$user, $content, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId); ContentHandler::runLegacyHooks('ArticleSaveComplete', $hook_args); Hooks::run('PageContentSaveComplete', $hook_args); // Promote user to any groups they meet the criteria for DeferredUpdates::addCallableUpdate(function () use($user) { $user->addAutopromoteOnceGroups('onEdit'); $user->addAutopromoteOnceGroups('onView'); // b/c }); return $status; }
/** * This function collects the form data and uses it to populate various member variables. * @param WebRequest $request * @throws ErrorPageError */ function importFormData(&$request) { global $wgContLang, $wgUser; # Section edit can come from either the form or a link $this->section = $request->getVal('wpSection', $request->getVal('section')); if ($this->section !== null && $this->section !== '' && !$this->isSectionEditSupported()) { throw new ErrorPageError('sectioneditnotsupported-title', 'sectioneditnotsupported-text'); } $this->isNew = !$this->mTitle->exists() || $this->section == 'new'; if ($request->wasPosted()) { # These fields need to be checked for encoding. # Also remove trailing whitespace, but don't remove _initial_ # whitespace from the text boxes. This may be significant formatting. $this->textbox1 = $this->safeUnicodeInput($request, 'wpTextbox1'); if (!$request->getCheck('wpTextbox2')) { // Skip this if wpTextbox2 has input, it indicates that we came // from a conflict page with raw page text, not a custom form // modified by subclasses $textbox1 = $this->importContentFormData($request); if ($textbox1 !== null) { $this->textbox1 = $textbox1; } } # Truncate for whole multibyte characters $this->summary = $wgContLang->truncate($request->getText('wpSummary'), 255); # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for # section titles. $this->summary = preg_replace('/^\\s*=+\\s*(.*?)\\s*=+\\s*$/', '$1', $this->summary); # Treat sectiontitle the same way as summary. # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is # currently doing double duty as both edit summary and section title. Right now this # is just to allow API edits to work around this limitation, but this should be # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312). $this->sectiontitle = $wgContLang->truncate($request->getText('wpSectionTitle'), 255); $this->sectiontitle = preg_replace('/^\\s*=+\\s*(.*?)\\s*=+\\s*$/', '$1', $this->sectiontitle); $this->edittime = $request->getVal('wpEdittime'); $this->editRevId = $request->getIntOrNull('editRevId'); $this->starttime = $request->getVal('wpStarttime'); $undidRev = $request->getInt('wpUndidRevision'); if ($undidRev) { $this->undidRev = $undidRev; } $this->scrolltop = $request->getIntOrNull('wpScrolltop'); if ($this->textbox1 === '' && $request->getVal('wpTextbox1') === null) { // wpTextbox1 field is missing, possibly due to being "too big" // according to some filter rules such as Suhosin's setting for // suhosin.request.max_value_length (d'oh) $this->incompleteForm = true; } else { // If we receive the last parameter of the request, we can fairly // claim the POST request has not been truncated. // TODO: softened the check for cutover. Once we determine // that it is safe, we should complete the transition by // removing the "edittime" clause. $this->incompleteForm = !$request->getVal('wpUltimateParam') && is_null($this->edittime); } if ($this->incompleteForm) { # If the form is incomplete, force to preview. wfDebug(__METHOD__ . ": Form data appears to be incomplete\n"); wfDebug("POST DATA: " . var_export($_POST, true) . "\n"); $this->preview = true; } else { $this->preview = $request->getCheck('wpPreview'); $this->diff = $request->getCheck('wpDiff'); // Remember whether a save was requested, so we can indicate // if we forced preview due to session failure. $this->mTriedSave = !$this->preview; if ($this->tokenOk($request)) { # Some browsers will not report any submit button # if the user hits enter in the comment box. # The unmarked state will be assumed to be a save, # if the form seems otherwise complete. wfDebug(__METHOD__ . ": Passed token check.\n"); } elseif ($this->diff) { # Failed token check, but only requested "Show Changes". wfDebug(__METHOD__ . ": Failed token check; Show Changes requested.\n"); } else { # Page might be a hack attempt posted from # an external site. Preview instead of saving. wfDebug(__METHOD__ . ": Failed token check; forcing preview\n"); $this->preview = true; } } $this->save = !$this->preview && !$this->diff; if (!preg_match('/^\\d{14}$/', $this->edittime)) { $this->edittime = null; } if (!preg_match('/^\\d{14}$/', $this->starttime)) { $this->starttime = null; } $this->recreate = $request->getCheck('wpRecreate'); $this->minoredit = $request->getCheck('wpMinoredit'); $this->watchthis = $request->getCheck('wpWatchthis'); # Don't force edit summaries when a user is editing their own user or talk page if (($this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK) && $this->mTitle->getText() == $wgUser->getName()) { $this->allowBlankSummary = true; } else { $this->allowBlankSummary = $request->getBool('wpIgnoreBlankSummary') || !$wgUser->getOption('forceeditsummary'); } $this->autoSumm = $request->getText('wpAutoSummary'); $this->allowBlankArticle = $request->getBool('wpIgnoreBlankArticle'); $this->allowSelfRedirect = $request->getBool('wpIgnoreSelfRedirect'); $changeTags = $request->getVal('wpChangeTags'); if (is_null($changeTags) || $changeTags === '') { $this->changeTags = []; } else { $this->changeTags = array_filter(array_map('trim', explode(',', $changeTags))); } } else { # Not a posted form? Start with nothing. wfDebug(__METHOD__ . ": Not a posted form.\n"); $this->textbox1 = ''; $this->summary = ''; $this->sectiontitle = ''; $this->edittime = ''; $this->editRevId = null; $this->starttime = wfTimestampNow(); $this->edit = false; $this->preview = false; $this->save = false; $this->diff = false; $this->minoredit = false; // Watch may be overridden by request parameters $this->watchthis = $request->getBool('watchthis', false); $this->recreate = false; // When creating a new section, we can preload a section title by passing it as the // preloadtitle parameter in the URL (Bug 13100) if ($this->section == 'new' && $request->getVal('preloadtitle')) { $this->sectiontitle = $request->getVal('preloadtitle'); // Once wpSummary isn't being use for setting section titles, we should delete this. $this->summary = $request->getVal('preloadtitle'); } elseif ($this->section != 'new' && $request->getVal('summary')) { $this->summary = $request->getText('summary'); if ($this->summary !== '') { $this->hasPresetSummary = true; } } if ($request->getVal('minor')) { $this->minoredit = true; } } $this->oldid = $request->getInt('oldid'); $this->parentRevId = $request->getInt('parentRevId'); $this->bot = $request->getBool('bot', true); $this->nosummary = $request->getBool('nosummary'); // May be overridden by revision. $this->contentModel = $request->getText('model', $this->contentModel); // May be overridden by revision. $this->contentFormat = $request->getText('format', $this->contentFormat); try { $handler = ContentHandler::getForModelID($this->contentModel); } catch (MWUnknownContentModelException $e) { throw new ErrorPageError('editpage-invalidcontentmodel-title', 'editpage-invalidcontentmodel-text', [$this->contentModel]); } if (!$handler->isSupportedFormat($this->contentFormat)) { throw new ErrorPageError('editpage-notsupportedcontentformat-title', 'editpage-notsupportedcontentformat-text', [$this->contentFormat, ContentHandler::getLocalizedName($this->contentModel)]); } /** * @todo Check if the desired model is allowed in this namespace, and if * a transition from the page's current model to the new model is * allowed. */ $this->editintro = $request->getText('editintro', $this->section === 'new' ? 'MediaWiki:addsection-editintro' : ''); // Allow extensions to modify form data Hooks::run('EditPage::importFormData', [$this, $request]); }