production.
=== Configuration changes in 1.21 ===
+ * (bug 29374) $wgVectorUseSimpleSearch is now enabled by default.
+ * Deprecated $wgAllowRealName is removed. Use $wgHiddenPrefs[] = 'realname' instead
=== New features in 1.21 ===
+* Added ContentHandler facility to allow extensions to support other content than wikitext.
+ See docs/contenthandler.txt for details.
=== Bug fixes in 1.21 ===
+ * (bug 40353) SpecialDoubleRedirect should support interwiki redirects.
+ * (bug 40352) fixDoubleRedirects.php should support interwiki redirects.
+ * (bug 9237) SpecialBrokenRedirect should not list interwiki redirects.
+ * (bug 34960) Drop unused fields rc_moved_to_ns and rc_moved_to_title from recentchanges table.
+ * (bug 32951) Do not register internal externals with absolute protocol,
+ when server has relative protocol.
=== API changes in 1.21 ===
+* prop=revisions can now report the contentmodel and contentformat, see docs/contenthandler.txt
+* action=edit and action=parse now support contentmodel and contentformat parameters to control the interpretation of
+ page content; See docs/contenthandler.txt for details.
+ * (bug 35693) ApiQueryImageInfo now suppresses errors when unserializing metadata.
=== Languages updated in 1.21 ===
-
+ MediaWiki supports over 350 languages. Many localisations are updated
+ regularly. Below only new and removed languages are listed, as well as
+ changes to languages because of Bugzilla reports.
=== Other changes in 1.21 ===
== Compatibility ==
// @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
// We should instead work with the Revision object when we need it...
- $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
- $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed
++ $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed
$this->mRevIdFetched = $this->mRevision->getId();
- wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
+ wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
wfProfileOut( __METHOD__ );
$title = Title::newFromText( $preload );
# Check for existence to avoid getting MediaWiki:Noarticletext
- if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+ if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
- return '';
+ return $handler->makeEmptyContent();
}
$page = WikiPage::factory( $title );
if ( $page->isRedirect() ) {
$title = $page->getRedirectTarget();
# Same as before
- if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+ if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
- return '';
+ return $handler->makeEmptyContent();
}
$page = WikiPage::factory( $title );
}
// passed.
if ( $this->summary === '' ) {
$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
- $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
- ->inContentLanguage()->text() ;
+ $this->summary = wfMessage( 'newsectionsummary' )
- ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
++ ->rawParams( $cleanSectionTitle )->inContentLanguage()->text() ;
}
} elseif ( $this->summary !== '' ) {
// Insert the section title above the content.
( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
( $bot ? EDIT_FORCE_BOT : 0 );
- $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
+ $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
+ false, null, $this->content_format );
if ( $doEditStatus->isOK() ) {
- $result['redirect'] = $content->isRedirect();
- $this->commitWatch();
- $result['redirect'] = Title::newFromRedirect( $text ) !== null;
++ $result['redirect'] = $content->isRedirect();
+ $this->updateWatchlist();
wfProfileOut( __METHOD__ );
return $status;
} else {
return null;
}
- public function getContent( $audience = Revision::FOR_PUBLIC ) {
+ /**
+ * Get the content of the current revision. No side-effects...
+ *
+ * @param $audience Integer: one of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::RAW get the text regardless of permissions
++ * @param $user User object to check for, only if FOR_THIS_USER is passed
++ * to the $audience parameter
+ * @return Content|null The content of the current revision
+ *
+ * @since 1.21
+ */
++ public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
+ $this->loadLastEdit();
+ if ( $this->mLastRevision ) {
+ return $this->mLastRevision->getContent( $audience );
+ }
+ return null;
+ }
+
/**
* Get the text of the current revision. No side-effects...
*
* @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
- * @return String|bool The text of the current revision. False on failure
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @return String|false The text of the current revision
+ * @deprecated as of 1.21, getContent() should be used instead.
*/
- public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
- public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
++ public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { #@todo: deprecated, replace usage!
+ wfDeprecated( __METHOD__, '1.21' );
-
$this->loadLastEdit();
if ( $this->mLastRevision ) {
- return $this->mLastRevision->getText( $audience );
+ return $this->mLastRevision->getText( $audience, $user );
}
return false;
}
if ( $titleObj->isRedirect() ) {
$oldTitle = $titleObj;
- $titles = Title::newFromRedirectArray(
- Revision::newFromTitle(
- $oldTitle, false, Revision::READ_LATEST
- )->getText( Revision::FOR_THIS_USER, $user )
- );
+ $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
- ->getContent( Revision::FOR_THIS_USER )
++ ->getContent( Revision::FOR_THIS_USER, $user )
+ ->getRedirectChain();
// array_shift( $titles );
$redirValues = array();
--- /dev/null
- ->inContentLanguage()->params( $header )->text();
+<?php
+/**
+ * @since 1.21
+ */
+class WikitextContent extends TextContent {
+
+ public function __construct( $text ) {
+ parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
+ }
+
+ /**
+ * @see Content::getSection()
+ */
+ public function getSection( $section ) {
+ global $wgParser;
+
+ $text = $this->getNativeData();
+ $sect = $wgParser->getSection( $text, $section, false );
+
+ return new WikitextContent( $sect );
+ }
+
+ /**
+ * @see Content::replaceSection()
+ */
+ public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ $myModelId = $this->getModel();
+ $sectionModelId = $with->getModel();
+
+ if ( $sectionModelId != $myModelId ) {
+ throw new MWException( "Incompatible content model for section: " .
+ "document uses $myModelId but " .
+ "section uses $sectionModelId." );
+ }
+
+ $oldtext = $this->getNativeData();
+ $text = $with->getNativeData();
+
+ if ( $section === '' ) {
+ return $with; # XXX: copy first?
+ } if ( $section == 'new' ) {
+ # Inserting a new section
+ $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
+ ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
+ if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
+ $text = strlen( trim( $oldtext ) ) > 0
+ ? "{$oldtext}\n\n{$subject}{$text}"
+ : "{$subject}{$text}";
+ }
+ } else {
+ # Replacing an existing section; roll out the big guns
+ global $wgParser;
+
+ $text = $wgParser->replaceSection( $oldtext, $section, $text );
+ }
+
+ $newContent = new WikitextContent( $text );
+
+ wfProfileOut( __METHOD__ );
+ return $newContent;
+ }
+
+ /**
+ * Returns a new WikitextContent object with the given section heading
+ * prepended.
+ *
+ * @param $header string
+ * @return Content
+ */
+ public function addSectionHeader( $header ) {
+ $text = wfMessage( 'newsectionheaderdefaultlevel' )
++ ->rawParams( $header )->inContentLanguage()->text();
+ $text .= "\n\n";
+ $text .= $this->getNativeData();
+
+ return new WikitextContent( $text );
+ }
+
+ /**
+ * Returns a Content object with pre-save transformations applied using
+ * Parser::preSaveTransform().
+ *
+ * @param $title Title
+ * @param $user User
+ * @param $popts ParserOptions
+ * @return Content
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ global $wgParser;
+
+ $text = $this->getNativeData();
+ $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+ return new WikitextContent( $pst );
+ }
+
+ /**
+ * Returns a Content object with preload transformations applied (or this
+ * object if no transformations apply).
+ *
+ * @param $title Title
+ * @param $popts ParserOptions
+ * @return Content
+ */
+ public function preloadTransform( Title $title, ParserOptions $popts ) {
+ global $wgParser;
+
+ $text = $this->getNativeData();
+ $plt = $wgParser->getPreloadText( $text, $title, $popts );
+
+ return new WikitextContent( $plt );
+ }
+
+ /**
+ * Implement redirect extraction for wikitext.
+ *
+ * @return null|Title
+ *
+ * @note: migrated here from Title::newFromRedirectInternal()
+ *
+ * @see Content::getRedirectTarget
+ * @see AbstractContent::getRedirectTarget
+ */
+ public function getRedirectTarget() {
+ global $wgMaxRedirects;
+ if ( $wgMaxRedirects < 1 ) {
+ // redirects are disabled, so quit early
+ return null;
+ }
+ $redir = MagicWord::get( 'redirect' );
+ $text = trim( $this->getNativeData() );
+ if ( $redir->matchStartAndRemove( $text ) ) {
+ // Extract the first link and see if it's usable
+ // Ensure that it really does come directly after #REDIRECT
+ // Some older redirects included a colon, so don't freak about that!
+ $m = array();
+ if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
+ // Strip preceding colon used to "escape" categories, etc.
+ // and URL-decode links
+ if ( strpos( $m[1], '%' ) !== false ) {
+ // Match behavior of inline link parsing here;
+ $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
+ }
+ $title = Title::newFromText( $m[1] );
+ // If the title is a redirect to bad special pages or is invalid, return null
+ if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
+ return null;
+ }
+ return $title;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @see Content::updateRedirect()
+ *
+ * This implementation replaces the first link on the page with the given new target
+ * if this Content object is a redirect. Otherwise, this method returns $this.
+ *
+ * @since 1.21
+ *
+ * @param Title $target
+ *
+ * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
+ */
+ public function updateRedirect( Title $target ) {
+ if ( !$this->isRedirect() ) {
+ return $this;
+ }
+
+ # Fix the text
+ # Remember that redirect pages can have categories, templates, etc.,
+ # so the regex has to be fairly general
+ $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x',
+ '[[' . $target->getFullText() . ']]',
+ $this->getNativeData(), 1 );
+
+ return new WikitextContent( $newText );
+ }
+
+ /**
+ * Returns true if this content is not a redirect, and this content's text
+ * is countable according to the criteria defined by $wgArticleCountMethod.
+ *
+ * @param $hasLinks Bool if it is known whether this content contains
+ * links, provide this information here, to avoid redundant parsing to
+ * find out.
+ * @param $title null|\Title
+ *
+ * @internal param \IContextSource $context context for parsing if necessary
+ *
+ * @return bool True if the content is countable
+ */
+ public function isCountable( $hasLinks = null, Title $title = null ) {
+ global $wgArticleCountMethod;
+
+ if ( $this->isRedirect( ) ) {
+ return false;
+ }
+
+ $text = $this->getNativeData();
+
+ switch ( $wgArticleCountMethod ) {
+ case 'any':
+ return true;
+ case 'comma':
+ return strpos( $text, ',' ) !== false;
+ case 'link':
+ if ( $hasLinks === null ) { # not known, find out
+ if ( !$title ) {
+ $context = RequestContext::getMain();
+ $title = $context->getTitle();
+ }
+
+ $po = $this->getParserOutput( $title, null, null, false );
+ $links = $po->getLinks();
+ $hasLinks = !empty( $links );
+ }
+
+ return $hasLinks;
+ }
+
+ return false;
+ }
+
+ public function getTextForSummary( $maxlength = 250 ) {
+ $truncatedtext = parent::getTextForSummary( $maxlength );
+
+ # clean up unfinished links
+ # XXX: make this optional? wasn't there in autosummary, but required for
+ # deletion summary.
+ $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
+
+ return $truncatedtext;
+ }
+
+ /**
+ * Returns a ParserOutput object resulting from parsing the content's text
+ * using $wgParser.
+ *
+ * @since 1.21
+ *
+ * @param $content Content the content to render
+ * @param $title \Title
+ * @param $revId null
+ * @param $options null|ParserOptions
+ * @param $generateHtml bool
+ *
+ * @internal param \IContextSource|null $context
+ * @return ParserOutput representing the HTML form of the text
+ */
+ public function getParserOutput( Title $title,
+ $revId = null,
+ ParserOptions $options = null, $generateHtml = true
+ ) {
+ global $wgParser;
+
+ if ( !$options ) {
+ //NOTE: use canonical options per default to produce cacheable output
+ $options = $this->getContentHandler()->makeParserOptions( 'canonical' );
+ }
+
+ $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
+ return $po;
+ }
+
+ protected function getHtml() {
+ throw new MWException(
+ "getHtml() not implemented for wikitext. "
+ . "Use getParserOutput()->getText()."
+ );
+ }
+
+ /**
+ * @see Content::matchMagicWord()
+ *
+ * This implementation calls $word->match() on the this TextContent object's text.
+ *
+ * @param MagicWord $word
+ *
+ * @return bool whether this Content object matches the given magic word.
+ */
+ public function matchMagicWord( MagicWord $word ) {
+ return $word->match( $this->getNativeData() );
+ }
+}
return false;
}
if ( $this->mOldRev ) {
- $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
- $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
- if ( $this->mOldtext === false ) {
++ $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( $this->mOldContent === false ) {
return false;
}
}
if ( $this->mNewRev ) {
- $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
- if ( $this->mNewtext === false ) {
++ $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+ if ( $this->mNewContent === false ) {
return false;
}
}
if ( !$this->loadRevisionData() ) {
return false;
}
- $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
++ $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
return true;
}
}
array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ),
// 1.21
+ array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ),
+ array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ),
+ array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ),
+ array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ),
+ array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ),
+ array( 'dropField', 'site_stats', 'ss_admins', 'patch-drop-ss_admins.sql' ),
+ array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ),
);
}
array( 'dropField', 'category', 'cat_hidden', 'patch-cat_hidden.sql' ),
// 1.21
+ array( 'addField', 'revision', 'rev_content_format', 'patch-revision-rev_content_format.sql' ),
+ array( 'addField', 'revision', 'rev_content_model', 'patch-revision-rev_content_model.sql' ),
+ array( 'addField', 'archive', 'ar_content_format', 'patch-archive-ar_content_format.sql' ),
+ array( 'addField', 'archive', 'ar_content_model', 'patch-archive-ar_content_model.sql' ),
+ array( 'addField', 'page', 'page_content_model', 'patch-page-page_content_model.sql' ),
+ array( 'dropField', 'site_stats', 'ss_admins', 'patch-drop-ss_admins.sql' ),
+ array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ),
);
}
# Preserve fragment (bug 14904)
$newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
- $currentDest->getFragment() );
+ $currentDest->getFragment(), $newTitle->getInterwiki() );
# Fix the text
- # Remember that redirect pages can have categories, templates, etc.,
- # so the regex has to be fairly general
- $newText = preg_replace( '/ \[ \[ [^\]]* \] \] /x',
- '[[' . $newTitle->getFullText() . ']]',
- $text, 1 );
-
- if ( $newText === $text ) {
- $this->setLastError( 'Text unchanged???' );
+ $newContent = $content->updateRedirect( $newTitle );
+
+ if ( $newContent->equals( $content ) ) {
+ $this->setLastError( 'Content unchanged???' );
return false;
}
# Interwikis
wfProfileIn( __METHOD__."-interwiki" );
if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
- // FIXME: the above check prevents links to sites with identifiers that are not language codes
- $this->mOutput->addLanguageLink( $nt->getFullText() );
++ // XXX: the above check prevents links to sites with identifiers that are not language codes
++
+ # Bug 24502: filter duplicates
+ if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
+ $this->mLangLinkLanguages[$iw] = true;
+ $this->mOutput->addLanguageLink( $nt->getFullText() );
+ }
++
$s = rtrim( $s . $prefix );
$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
wfProfileOut( __METHOD__."-interwiki" );
#@todo: test recursive, too!
- protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) {
+ protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) {
$update = new LinksUpdate( $title, $parserOutput );
+ $update->beginTransaction();
$update->doUpdate();
+ $update->commitTransaction();
$this->assertSelect( $table, $fields, $condition, $expectedRows );
}