merged master
authordaniel <daniel.kinzler@wikimedia.de>
Mon, 23 Jul 2012 20:07:18 +0000 (22:07 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Mon, 23 Jul 2012 20:07:18 +0000 (22:07 +0200)
Change-Id: Iad12ee382d6aeb1fab6fefb611d290b74865ea4b

40 files changed:
1  2 
RELEASE-NOTES-1.20
docs/hooks.txt
includes/Article.php
includes/AutoLoader.php
includes/DefaultSettings.php
includes/Defines.php
includes/EditPage.php
includes/Export.php
includes/ImagePage.php
includes/Import.php
includes/LinksUpdate.php
includes/Namespace.php
includes/OutputPage.php
includes/Revision.php
includes/SqlDataUpdate.php
includes/Title.php
includes/WikiPage.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiMain.php
includes/api/ApiParse.php
includes/api/ApiQueryRevisions.php
includes/diff/DifferenceEngine.php
includes/filerepo/file/LocalFile.php
includes/job/DoubleRedirectJob.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/SearchEngine.php
includes/specials/SpecialBooksources.php
includes/specials/SpecialUndelete.php
languages/Language.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/cleanupSpam.php
tests/phpunit/includes/WikiPageTest.php
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/maintenance/DumpTestCase.php
tests/phpunit/maintenance/backupPrefetchTest.php
tests/phpunit/maintenance/backupTextPassTest.php

diff --combined RELEASE-NOTES-1.20
@@@ -20,11 -20,18 +20,18 @@@ upgrade PHP if you have not done so pri
  * `$wgUsePathInfo = true;` is no longer needed to make $wgArticlePath work on servers
    using like nginx, lighttpd, and apache over fastcgi. MediaWiki now always extracts
    path info from REQUEST_URI if it's available.
+ * The user right 'upload_by_url' is no longer given to sysops by default.
+   This only affects installations which have $wgAllowCopyUploads set to true.
+ * Removed f-prot support from $wgAntivirusSetup.
+ * $wgDBerrorLogInUTC to log error in $wgDBerrorLog using an UTC date instead
+   of the wiki timezone set by $wgLocalTimezone.
  
  === New features in 1.20 ===
  * Added TitleIsAlwaysKnown hook which gets called when determining if a page exists.
  * Added NamespaceIsMovable hook which gets called when determining if pages in a
    certain namespace can be moved.
+ * Added SpecialPageBeforeExecute hook which gets called before SpecialPage::execute.
+ * Added SpecialPageAfterExecute hook which gets called after SpecialPage::execute.
  * (bug 32341) Add upload by URL domain limitation.
  * &useskin=default will now always display the default skin. Useful for users with a
    preference for the non-default skin to look at something using the default skin.
@@@ -55,6 -62,8 +62,8 @@@
  * Edit notices can now be translated.
  * (bug 35680) jQuery upgraded to 1.7.2.
  * jQuery UI upgraded to 1.8.21.
+ * (bug 35705) QUnit upgraded from v1.2.0 to v1.8.0.
+ * (bug 37604) jquery.cookie upgraded to 2011 version.
  * (bug 22887) Add warning and tracking category for preprocessor errors
  * (bug 31704) Allow selection of associated namespace on the watchlist
  * (bug 5445) Now remove autoblocks when a user is unblocked.
  * New getCreator and getOldestRevision methods added to WikiPage class
  * (bug 4220) the XML dump format schema now have unique identity constraints
    for page and revision identifiers. Patch by Elvis Stansvik.
- * (bug 35705) QUnit upgraded from v1.2.0 to v1.8.0.
  * cleanupSpam.php now can delete spam pages if --delete was specified instead of blanking
    them.
  * Added new hook ChangePasswordForm to allow adding of additional fields in Special:ChangePassword
  * Added new function getDomain to AuthPlugin for getting a user's domain
  * (bug 23427) New magic word {{PAGEID}} which gives the current page ID.
    Will be null on previewing a page being created.
 +* Added new hook AfterFinalPageOutput to allow modifications to buffered page output before sent
 +  to the client.
+ * (bug 37627) UserNotLoggedIn() exception to show a generic error page whenever
+   a user is not logged in.
+ * Watched status in changes lists are no longer indicated by <strong></strong>
+   tags with class "mw-watched". Instead, each line now has a class
+   "mw-changeslist-line-watched" or "mw-changeslist-line-not-watched", and the
+   title itself is surrounded by <span></span> tags with class "mw-title".
+ * Added ContribsPager::reallyDoQuery hook allowing extensions to data to MyContribs
+ * Added new hook ParserAfterParse to allow extensions to affect parsed output
+   after the parse is complete but before block level processing, link holder
+   replacement, and so on.
+ * (bug 34678) Added InternalParseBeforeSanitize hook which gets called during Parser's
+   internalParse method just before the parser removes unwanted/dangerous HTML tags.
+ * (bug 36783) Implement jQuery Promise interface in mediawiki.api module.
+ * Make dates in sortable tables sort according to the page content language
+   instead of the site content language
+ * (bug 37926) Deleterevision will no longer allow users to delete log entries,
+   the new deletelogentry permission is required for this.
  
  === Bug fixes in 1.20 ===
  * (bug 30245) Use the correct way to construct a log page title.
  * (bug 35572) Blocks appear to succeed even if query fails due to wrong DB structure
  * (bug 31757) Add a word-separator between help-messages in HTMLForm
  * (bug 30410) Removed deprecated $wgFilterCallback and the 'filtered' API error.
- * (bug 32604) Some messages needs escaping of wikitext inside username
+ * (bug 32604) Some messages needs escaping of wikitext inside username.
  * (bug 36537) Rename wfArrayToCGI to wfArrayToCgi for consistency with wfCgiToArray.
- * (bug 25946) The message on the top of Special:RecentChanges is now displayed
+ * (bug 25946) The message on the top of Special:RecentChanges is now displayed.
    in user language instead of content language.
  * (bug 35264) Wrong type used for <ns> in export.xsd
  * (bug 24985) Use $wgTmpDirectory as the default temp directory so that people
    who don't have access to /tmp can specify an alternative.
  * (bug 27283) SqlBagOStuff breaks PostgreSQL transactions.
  * (bug 35727) mw.Api ajax() should put token parameter last.
+ * (bug 260) Handle <pre> overflow automatically with a scroll bar.
  * (bug 37708) mw.Uri.clone() should make a deep copy.
+ * (bug 38024) ResourceLoader should not create empty stylesheets for modules
+   that don't have stylesheets.
+ * (bug 36812) Special:ActiveUsers "Hide bots" should hide users from any group
+   having the "bot" user right, instead of just the default "bot" user group.
+ * (bug 35082) mw.util.addPortletLink incorrectly adds link to mutiple <ul> tags.
+ * (bug 36495) Sanitizer::fixDeprecatedAttributes should convert "align"
+   attribute to margin or float instead of text-align (for non-table-cells).
+ * (bug 36991) jquery.tablesorter should extract date sort format from date
+   string instead of global config. Dates like "April 1 2012" and "1 April 2012"
+   now sort correctly regardless of the content language's DefaultDateFormat.
+ * (bug 31895) mw.loader mode now correct when triggered from a $.fn.ready
+   handler that is bound before mediawiki.js's handler (e.g. browser-userscripts
+   like greasemonkey).
+ * (bug 38152) jquery.tablesorter: Use .data() instead of .attr(), so that live
+   values are used instead of just the fixed values from when the tablesorter
+   was initialized.
+ * (bug 38093) Gender of changed user groups missing in Special:Log/rights
  
  === API changes in 1.20 ===
  * (bug 34316) Add ability to retrieve maximum upload size from MediaWiki API.
  * (bug 32497) API now allows changing of protection level using pageid.
  * (bug 32498) API now allows comparing pages using pageids.
  * (bug 30975) API import of pages with invalid characters in this wiki leads to Fatal Error.
- * (bug 30488) API now allows listing of backlinks/embeddedin/imageusage per pageid
- * (bug 34927) Output media_type for list=filearchive
- * (bug 28814) add properties to output of action=parse
- * (bug 33224) add variants of content language to meta=siteinfo
- * (bug 36761) "Mark pages as visited" now submits previously established filter options
- * (bug 32643) action=purge with forcelinkupdate no longer crashes when ratelimit is reached
- * The paraminfo module now also contains result properties for most modules
+ * (bug 30488) API now allows listing of backlinks/embeddedin/imageusage per pageid.
+ * (bug 34927) Output media_type for list=filearchive.
+ * (bug 28814) add properties to output of action=parse.
+ * (bug 33224) add variants of content language to meta=siteinfo.
+ * (bug 36761) "Mark pages as visited" now submits previously established filter options.
+ * (bug 32643) action=purge with forcelinkupdate no longer crashes when ratelimit is reached.
+ * The paraminfo module now also contains result properties for most modules.
+ * (bug 32348) Allow descending order for list=alllinks.
+ * (bug 31777) Upload unknown error ``fileexists-forbidden''.
+ * (bug 32382) Allow descending order for list=iwbacklinks.
+ * (bug 32381) Allow descending order for list=backlinks, list=embeddedin and list=imageusage.
+ * (bug 32383) Allow descending order for list=langbacklinks.
+ * API meta=siteinfo can now return the list of known variable IDs.
+ * (bug 35980) list=deletedrevs now honors drdir correctly in "all" mode (mode #3).
+ * (bug 29290) API avoids mangling fields in continuation parameters
+ * (bug 36987) API avoids mangling fields in continuation parameters
+ * (bug 30836) siteinfo prop=specialpagealiases will no longer return nonexistent special pages
+ * (bug 38190) Add "required" flag to some token params for hint in api docs.
  
  === Languages updated in 1.20 ===
  
@@@ -153,6 -205,7 +207,7 @@@ MediaWiki supports over 350 languages. 
  regularly. Below only new and removed languages are listed, as well as
  changes to languages because of Bugzilla reports.
  
+ * Emilian (egl) added.
  * Tornedalen Finnish (fit) added.
  * Mizo (lus) added.
  * Santali (sat) added.
diff --combined docs/hooks.txt
@@@ -245,6 -245,10 +245,10 @@@ $block: The block from which the autobl
  'AbortDiffCache': Can be used to cancel the caching of a diff
  &$diffEngine: DifferenceEngine object
  
+ 'AbortEmailNotification': Can be used to cancel email notifications for an edit.
+ $editor: The User who made the change.
+ $title: The Title of the page that was edited.
  'AbortLogin': Return false to cancel account login.
  $user: the User object being authenticated against
  $password: the password being submitted, not yet checked for validity
@@@ -279,11 -283,6 +283,11 @@@ $article: Article objec
  $user: the User object that was created. (Parameter added in 1.7)
  $byEmail: true when account was created "by email" (added in 1.12)
  
 +'AfterFinalPageOutput': At the end of OutputPage::output() but before
 +final ob_end_flush() which will send the buffered output to the client.
 +This allows for last-minute modification of the output within the buffer
 +by using ob_get_clean().
 +
  'AfterImportPage': When a page import is completed
  $title: Title under which the revisions were imported
  $origTitle: Title provided by the XML file
@@@ -411,14 -410,9 +415,14 @@@ token types
  used to retrieve this type of tokens.
  
  'ArticleAfterFetchContent': after fetching content of an article from
 +the database. DEPRECATED, use ArticleAfterFetchContentObject instead.
 +$article: the article (object) being loaded from the database
 +&$content: the content (string) of the article
 +
 +'ArticleAfterFetchContentObject': after fetching content of an article from
  the database
  $article: the article (object) being loaded from the database
 -$content: the content (string) of the article
 +&$content: the content of the article, as a Content object
  
  'ArticleConfirmDelete': before writing the confirmation form for article
        deletion
@@@ -437,6 -431,8 +441,8 @@@ $user: the user (object) deleting the a
  $reason: the reason (string) the article is being deleted
  $error: if the deletion was prohibited, the (raw HTML) error message to display
    (added in 1.13)
+ $status: Status object, modify this to throw an error. Overridden by $error
+   (added in 1.20)
  
  'ArticleDeleteComplete': after an article is deleted
  $article: the WikiPage that was deleted
@@@ -464,7 -460,7 +470,7 @@@ Wiki::articleFromTitle(
  $title: title (object) used to create the article object
  $article: article (object) that will be returned
  
 -'ArticleInsertComplete': After a new article is created
 +'ArticleInsertComplete': After a new article is created. DEPRECATED, use ArticleContentInsertComplete
  $article: WikiPage created
  $user: User creating the article
  $text: New content
@@@ -475,17 -471,6 +481,17 @@@ $section: (No longer used
  $flags: Flags passed to Article::doEdit()
  $revision: New Revision of the article
  
 +'ArticleContentInsertComplete': After a new article is created
 +$article: WikiPage created
 +$user: User creating the article
 +$content: New content as a Content object
 +$summary: Edit summary/comment
 +$isMinor: Whether or not the edit was marked as minor
 +$isWatch: (No longer used)
 +$section: (No longer used)
 +$flags: Flags passed to Article::doEdit()
 +$revision: New Revision of the article
 +
  'ArticleMergeComplete': after merging to article using Special:Mergehistory
  $targetTitle: target title (object)
  $destTitle: destination title (object)
@@@ -534,7 -519,7 +540,7 @@@ $user: the user who did the rollbac
  $revision: the revision the page was reverted back to
  $current: the reverted revision
  
 -'ArticleSave': before an article is saved
 +'ArticleSave': before an article is saved. DEPRECATED, use ArticleContentSave instead
  $article: the WikiPage (object) being saved
  $user: the user (object) saving the article
  $text: the new article text
@@@ -543,16 -528,7 +549,16 @@@ $isminor: minor fla
  $iswatch: watch flag
  $section: section #
  
 -'ArticleSaveComplete': After an article has been updated
 +'ArticleContentSave': before an article is saved.
 +$article: the WikiPage (object) being saved
 +$user: the user (object) saving the article
 +$content: the new article content, as a Content object
 +$summary: the article summary (comment)
 +$isminor: minor flag
 +$iswatch: watch flag
 +$section: section #
 +
 +'ArticleSaveComplete': After an article has been updated. DEPRECATED, use ArticleContentSaveComplete instead.
  $article: WikiPage modified
  $user: User performing the modification
  $text: New content
@@@ -565,19 -541,6 +571,19 @@@ $revision: New Revision of the articl
  $status: Status object about to be returned by doEdit()
  $baseRevId: the rev ID (or false) this edit was based on
  
 +'ArticleContentSaveComplete': After an article has been updated
 +$article: WikiPage modified
 +$user: User performing the modification
 +$content: New content, as a Content object
 +$summary: Edit summary/comment
 +$isMinor: Whether or not the edit was marked as minor
 +$isWatch: (No longer used)
 +$section: (No longer used)
 +$flags: Flags passed to Article::doEdit()
 +$revision: New Revision of the article
 +$status: Status object about to be returned by doEdit()
 +$baseRevId: the rev ID (or false) this edit was based on
 +
  'ArticleUndelete': When one or more revisions of an article are restored
  $title: Title corresponding to the article restored
  $create: Whether or not the restoration caused the page to be created
@@@ -604,19 -567,11 +610,19 @@@ object to both indicate that the outpu
  follwed an redirect
  $article: target article (object)
  
 -'ArticleViewCustom': allows to output the text of the article in a different format than wikitext
 +'ArticleViewCustom': allows to output the text of the article in a different format than wikitext.
 +DEPRECATED, use ArticleContentViewCustom instead.
 +Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility.
  $text: text of the page
  $title: title of the page
  $output: reference to $wgOut
  
 +'ArticleContentViewCustom': allows to output the text of the article in a different format than wikitext.
 +Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility.
 +$content: content of the page, as a Content object
 +$title: title of the page
 +$output: reference to $wgOut
 +
  'AuthPluginAutoCreate': Called when creating a local account for an user logged
  in from an external authentication method
  $user: User object created locally
@@@ -748,24 -703,22 +754,32 @@@ the collation given in $collationName
  'ConfirmEmailComplete': Called after a user's email has been confirmed successfully
  $user: user (object) whose email is being confirmed
  
 +'ContentHandlerDefaultModelFor': Called when the default content model is determiend
 +for a given title. May be used to assign a different model for that title.
 +$title: the Title in question
 +&$model: the model name. Use with CONTENT_MODEL_XXX constants.
 +
 +'ContentHandlerForModelID': Called when a ContentHandler is requested for a given
 +cointent model name, but no entry for that model exists in $wgContentHandlers.
 +$modeName: the requested content model name
 +&$handler: set this to a ContentHandler object, if desired.
 +
  'ContribsPager::getQueryInfo': Before the contributions query is about to run
  &$pager: Pager object for contributions
  &$queryInfo: The query for the contribs Pager
  
+ 'ContribsPager::reallyDoQuery': Called before really executing the query for My Contributions
+ &$data: an array of results of all contribs queries
+ $pager: The ContribsPager object hooked into
+ $offset: Index offset, inclusive
+ $limit: Exact query limit
+ $descending: Query direction, false for ascending, true for descending
  'ContributionsLineEnding': Called before a contributions HTML line is finished
  $page: SpecialPage object for contributions
- $ret: the HTML line
&$ret: the HTML line
  $row: the DB row for this line
+ &$classes: the classes to add to the surrounding <li>
  
  'ContributionsToolLinks': Change tool links above Special:Contributions
  $id: User identifier
@@@ -823,19 -776,12 +837,19 @@@ $section: Section being edite
  &$error: Error message to return
  $summary: Edit summary for page
  
 -'EditFilterMerged': Post-section-merge edit filter
 +'EditFilterMerged': Post-section-merge edit filter.
 +DEPRECATED, use EditFilterMergedContent instead.
  $editor: EditPage instance (object)
  $text: content of the edit box
  &$error: error message to return
  $summary: Edit summary for page
  
 +'EditFilterMergedContent': Post-section-merge edit filter
 +$editor: EditPage instance (object)
 +$content: content of the edit box, as a Content object
 +&$error: error message to return
 +$summary: Edit summary for page
 +
  'EditFormPreloadText': Allows population of the edit form when creating
  new pages
  &$text: Text to preload with
@@@ -898,28 -844,14 +912,28 @@@ $title: title of page being edite
  &$msg: localization message name, overridable. Default is either 'copyrightwarning' or 'copyrightwarning2'
  
  'EditPageGetDiffText': Allow modifying the wikitext that will be used in
 -"Show changes"
 +"Show changes". DEPRECATED. Use EditPageGetDiffContent instead.
 +Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility.
  $editPage: EditPage object
  &$newtext: wikitext that will be used as "your version"
  
 -'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed
 +'EditPageGetDiffContent': Allow modifying the wikitext that will be used in
 +"Show changes".
 +Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility.
 +$editPage: EditPage object
 +&$newtext: wikitext that will be used as "your version"
 +
 +'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed.
 +DEPRECATED. Use EditPageGetPreviewContent instead.
 +Note that it is preferrable to implement previews for different data types using the COntentHandler facility.
  $editPage: EditPage object
  &$toparse: wikitext that will be parsed
  
 +'EditPageGetPreviewContent': Allow modifying the wikitext that will be previewed.
 +Note that it is preferrable to implement previews for different data types using the COntentHandler facility.
 +$editPage: EditPage object
 +&$content: Content object to be previewed (may be replaced by hook function)
 +
  'EditPageNoSuchSection': When a section edit request is given for an non-existent section
  &$editpage: The current EditPage object
  &$res: the HTML of the error text
@@@ -1201,8 -1133,16 +1215,16 @@@ $prefix: interwiki prefix we are lookin
  &$iwData: output array describing the interwiki with keys iw_url, iw_local,
    iw_trans and optionally iw_api and iw_wikiid.
  
+ 'InternalParseBeforeSanitize': during Parser's internalParse method just before the
+ parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/
+ onlyinclude and other processings. Ideal for syntax-extensions after template/parser
+ function execution which respect nowiki and HTML-comments.
+ &$parser: Parser object
+ &$text: string containing partially parsed text
+ &$stripState: Parser's internal StripState object
  'InternalParseBeforeLinks': during Parser's internalParse method before links
- but after noinclude/includeonly/onlyinclude and other processing.
+ but after nowiki/noinclude/includeonly/onlyinclude and other processings.
  &$parser: Parser object
  &$text: string containing partially parsed text
  &$stripState: Parser's internal StripState object
@@@ -1542,6 -1482,12 +1564,12 @@@ A parser extension which depends on use
  this hook and append its values to the key.
  $hash: reference to a hash key string which can be modified
  
+ 'ParserAfterParse': Called from Parser::parse() just after the call to
+ Parser::internalParse() returns
+ $parser: parser object
+ $text: text being parsed
+ $stripState: stripState used (object)
  'ParserAfterStrip': Same as ParserBeforeStrip
  
  'ParserAfterTidy': Called after Parser::tidy() in Parser::parse()
@@@ -1680,6 -1626,11 +1708,11 @@@ $out: OutputPage objec
  'RecentChange_save': called at the end of RecentChange::save()
  $recentChange: RecentChange object
  
+ 'RedirectSpecialArticleRedirectParams': lets you alter the set of
+ parameter names such as "oldid" that are preserved when using
+ redirecting special pages such as Special:MyPage and Special:MyTalk.
+ &$redirectParams: An array of parameters preserved by redirecting special pages.
  'RequestContextCreateSkin': Called when RequestContext::getSkin creates a skin instance.
  Can be used by an extension override what skin is used in certain contexts.
  IContextSource $context: The RequestContext the skin is being created for.
@@@ -1751,8 -1702,7 +1784,8 @@@ $query : Original query
  'ShowMissingArticle': Called when generating the output for a non-existent page
  $article: The article object corresponding to the page
  
 -'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views
 +'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views.
 +DEPRECATED, use the ContentHandler facility to handle CSS and JavaScript!
  $text: Text being shown
  $title: Title of the custom script/stylesheet page
  $output: Current OutputPage object
@@@ -1911,6 -1861,14 +1944,14 @@@ Each key maps to an associative array w
  hook to remove a core special page
  $list: list (array) of core special pages
  
+ 'SpecialPageAfterExecute': called after SpecialPage::execute
+ $special: the SpecialPage object
+ $subPage: the subpage string or null if no subpage was specified
+ 'SpecialPageBeforeExecute': called before SpecialPage::execute
+ $special: the SpecialPage object
+ $subPage: the subpage string or null if no subpage was specified
  'SpecialPasswordResetOnSubmit': when executing a form submission on Special:PasswordReset
  $users: array of User objects
  $data: array of data submitted by the user
@@@ -2363,13 -2321,6 +2404,13 @@@ One, and only one hook should set this
  &$opts: Options to use for the query
  &$join: Join conditions
  
 +'WikiPageDeletionUpdates': manipulate the list of DataUpdates to be applied when
 +      a page is deleted. Called in WikiPage::getDeletionUpdates().
 +      Note that updates specific to a content model should be provided by the
 +      respective ContentHandler's getDeletionUpdates() method.
 +$page: the WikiPage
 +&$updates: the array of DataUpdate objects. Hook function may want to add to it.
 +
  'wfShellWikiCmd': Called when generating a shell-escaped command line
        string to run a MediaWiki cli script.
  &$script: MediaWiki cli script path
diff --combined includes/Article.php
@@@ -57,17 -57,10 +57,17 @@@ class Article extends Page 
        public $mParserOptions;
  
        /**
 -       * Content of the revision we are working on
 +       * Text of the revision we are working on
         * @var string $mContent
         */
 -      var $mContent;                    // !<
 +      var $mContent;                    // !< #BC cruft
 +
 +      /**
 +       * Content of the revision we are working on
 +       * @var Content
 +       * @since 1.WD
 +       */
 +      var $mContentObject;              // !<
  
        /**
         * Is the content ($mContent) already loaded?
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
 +       * @deprecated in 1.WD; use getContentObject() instead
 +       *
         * @return string Return the text of this revision
         */
        public function getContent() {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = $this->getContentObject();
 +              return ContentHandler::getContentText( $content );
 +      }
 +
 +      /**
 +       * Returns a Content object representing the pages effective display content,
 +       * not necessarily the revision's content!
 +       *
 +       * Note that getContent/loadContent do not follow redirects anymore.
 +       * If you need to fetch redirectable content easily, try
 +       * the shortcut in WikiPage::getRedirectTarget()
 +       *
 +       * This function has side effects! Do not use this function if you
 +       * only want the real revision text if any.
 +       *
 +       * @return Content Return the content of this revision
 +       *
 +       * @since 1.WD
 +       *
 +       * @todo: FIXME: this should really be protected, all callers should be changed to use WikiPage::getContent() instead.
 +       */
 +      public function getContentObject() {
 +              global $wgUser;
                wfProfileIn( __METHOD__ );
  
                if ( $this->mPage->getID() === 0 ) {
                                if ( $text === false ) {
                                        $text = '';
                                }
 +
 +                              $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
 -                              $text = wfMsgExt( $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
 +                              $content = new MessageContent( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
  
 -                      return $text;
 +                      return $content;
                } else {
 -                      $this->fetchContent();
 +                      $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
  
 -                      return $this->mContent;
 +                      return $this->mContentObject;
                }
        }
  
         * Get text of an article from database
         * Does *NOT* follow redirects.
         *
 +       * @protected
 +       * @note this is really internal functionality that should really NOT be used by other functions. For accessing
 +       *       article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code
 +       *       uses this method to retrieve page text from the database, so the function has to remain public for now.
 +       *
         * @return mixed string containing article contents, or false if null
 +       * @deprecated in 1.WD, use WikiPage::getContent() instead
         */
 -      function fetchContent() {
 -              if ( $this->mContentLoaded ) {
 +      function fetchContent() { #BC cruft!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
  
                wfProfileIn( __METHOD__ );
  
 +              $content = $this->fetchContentObject();
 +
 +              $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere!
 +              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft, deprecated!
 +
 +              wfProfileOut( __METHOD__ );
 +
 +              return $this->mContent;
 +      }
 +
 +
 +      /**
 +       * Get text content object
 +       * Does *NOT* follow redirects.
 +       * TODO: when is this null?
 +       *
 +       * @note code that wants to retrieve page content from the database should use WikiPage::getContent().
 +       *
 +       * @return Content|null
 +       *
 +       * @since 1.WD
 +       */
 +      protected function fetchContentObject() {
 +              if ( $this->mContentLoaded ) {
 +                      return $this->mContentObject;
 +              }
 +
 +              wfProfileIn( __METHOD__ );
 +
                $this->mContentLoaded = true;
 +              $this->mContent = null;
  
                $oldid = $this->getOldID();
  
                # fails we'll have something telling us what we intended.
                $t = $this->getTitle()->getPrefixedText();
                $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
 -              $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
 +              $this->mContentObject = new MessageContent( 'missing-article', array($t, $d), array() ) ; // @todo: this isn't page content but a UI message. horrible.
  
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
                        }
  
                        $this->mRevision = $this->mPage->getRevision();
 +
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
  
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
 -              $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
 +              $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
  
 -              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +              wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
  
                wfProfileOut( __METHOD__ );
  
 -              return $this->mContent;
 +              return $this->mContentObject;
        }
  
        /**
         * @return Revision|null
         */
        public function getRevisionFetched() {
 -              $this->fetchContent();
 +              $this->fetchContentObject();
  
                return $this->mRevision;
        }
                                        break;
                                case 3:
                                        # This will set $this->mRevision if needed
 -                                      $this->fetchContent();
 +                                      $this->fetchContentObject();
  
                                        # Are we looking at an old revision
                                        if ( $oldid && $this->mRevision ) {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
 -                                      } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 +                                      } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) {
 +                                              # Allow extensions do their own custom view for certain pages
 +                                              $outputDone = true;
 +                                      } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $outputPage ) ) ) { #FIXME: fetchContent() is deprecated!
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
 -                                              $text = $this->getContent();
 -                                              $rt = Title::newFromRedirectArray( $text );
 +                                              $content = $this->getContentObject();
 +                                              $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
 -                                                      $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
 +                                                      $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false );
                                                        $outputPage->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
                                        # Run the parse, protected by a pool counter
                                        wfDebug( __METHOD__ . ": doing uncached parse\n" );
  
 +                                      // @todo: shouldn't we be passing $this->getPage() to PoolWorkArticleView instead of plain $this?
                                        $poolArticleView = new PoolWorkArticleView( $this, $parserOptions,
 -                                              $this->getRevIdFetched(), $useParserCache, $this->getContent() );
 +                                              $this->getRevIdFetched(), $useParserCache, $this->getContentObject(), $this->getContext() );
  
                                        if ( !$poolArticleView->execute() ) {
                                                $error = $poolArticleView->getError();
                $unhide = $request->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
  
 -              $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +              $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
 +              $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
 -      protected function showCssOrJsPage() {
 -              $dir = $this->getContext()->getLanguage()->getDir();
 -              $lang = $this->getContext()->getLanguage()->getCode();
 +      protected function showCssOrJsPage( $showCacheHint = true ) {
 +              global $wgOut;
  
 -              $outputPage = $this->getContext()->getOutput();
 -              $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 -                      'clearyourcache' );
 +              if ( $showCacheHint ) {
 +                      $dir = $this->getContext()->getLanguage()->getDir();
 +                      $lang = $this->getContext()->getLanguage()->getCode();
 +
 +                      $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 +                              'clearyourcache' );
 +              }
  
                // Give hooks a chance to customise the output
 -              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 -                      // Wrap the whole lot in a <pre> and don't parse
 -                      $m = array();
 -                      preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
 -                      $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                      $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
 -                      $outputPage->addHTML( "\n</pre>\n" );
 +              if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated
 +                      $po = $this->mContentObject->getParserOutput( $this->getTitle() );
 +                      $wgOut->addHTML( $po->getText() );
                }
        }
  
  
                        $this->doDelete( $reason, $suppress );
  
-                       if ( $request->getCheck( 'wpWatch' ) && $user->isLoggedIn() ) {
-                               WatchAction::doWatch( $title, $user );
-                       } elseif ( $title->userIsWatching() ) {
-                               WatchAction::doUnwatch( $title, $user );
+                       if ( $user->isLoggedIn() && $request->getCheck( 'wpWatch' ) != $user->isWatched( $title ) ) {
+                               if ( $request->getCheck( 'wpWatch' ) ) {
+                                       WatchAction::doWatch( $title, $user );
+                               } else {
+                                       WatchAction::doUnwatch( $title, $user );
+                               }
                        }
  
                        return;
                // Generate deletion reason
                $hasHistory = false;
                if ( !$reason ) {
 -                      $reason = $this->generateReason( $hasHistory );
 +                      try {
 +                              $reason = $this->generateReason( $hasHistory );
 +                      } catch (MWException $e) {
 +                              # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here.
 +                              wfDebug("Error while building auto delete summary: $e");
 +                              $reason = '';
 +                      }
                }
  
                // If the page has a history, insert a warning
                } else {
                        $suppress = '';
                }
-               $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $this->getTitle()->userIsWatching();
+               $checkWatch = $user->getBoolOption( 'watchdeletion' ) || $user->isWatched( $this->getTitle() );
  
                $form = Xml::openElement( 'form', array( 'method' => 'post',
                        'action' => $this->getTitle()->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) .
        public function doDelete( $reason, $suppress = false ) {
                $error = '';
                $outputPage = $this->getContext()->getOutput();
-               if ( $this->mPage->doDeleteArticle( $reason, $suppress, 0, true, $error ) ) {
+               $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error );
+               if ( $status->isGood() ) {
                        $deleted = $this->getTitle()->getPrefixedText();
  
                        $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
                } else {
                        $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) );
                        if ( $error == '' ) {
+                               $errors = $status->getErrorsArray();
                                $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
-                                       array( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) )
+                                       $errors[0]
                                );
                                $outputPage->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
  
         * @return mixed
         */
        public function generateReason( &$hasHistory ) {
 -              return $this->mPage->getAutoDeleteReason( $hasHistory );
 +              $title = $this->mPage->getTitle();
 +              $handler = ContentHandler::getForTitle( $title );
 +              return $handler->getAutoDeleteReason( $title, $hasHistory );
        }
  
        // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** //
         * @param $newtext
         * @param $flags
         * @return string
 +       * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
         */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
                return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
diff --combined includes/AutoLoader.php
@@@ -285,19 -285,6 +285,19 @@@ $wgAutoloadLocalClasses = array
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
        'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php',
  
 +      # content handler
 +      'Content' => 'includes/Content.php',
 +      'AbstractContent' => 'includes/Content.php',
 +      'ContentHandler' => 'includes/ContentHandler.php',
 +      'CssContent' => 'includes/Content.php',
 +      'CssContentHandler' => 'includes/ContentHandler.php',
 +      'JavaScriptContent' => 'includes/Content.php',
 +      'JavaScriptContentHandler' => 'includes/ContentHandler.php',
 +      'MessageContent' => 'includes/Content.php',
 +      'TextContent' => 'includes/Content.php',
 +      'WikitextContent' => 'includes/Content.php',
 +      'WikitextContentHandler' => 'includes/ContentHandler.php',
 +
        # includes/actions
        'CachedAction' => 'includes/actions/CachedAction.php',
        'CreditsAction' => 'includes/actions/CreditsAction.php',
        'ApiFormatDump' => 'includes/api/ApiFormatDump.php',
        'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
        'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
 +      'ApiFormatNone' => 'includes/api/ApiFormatNone.php',
        'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
        'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php',
        'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
        'LinkCache' => 'includes/cache/LinkCache.php',
        'MessageCache' => 'includes/cache/MessageCache.php',
        'ObjectFileCache' => 'includes/cache/ObjectFileCache.php',
+       'ProcessCacheLRU' => 'includes/cache/ProcessCacheLRU.php',
        'ResourceFileCache' => 'includes/cache/ResourceFileCache.php',
        'SquidUpdate' => 'includes/cache/SquidUpdate.php',
        'TitleDependency' => 'includes/cache/CacheDependency.php',
        'IContextSource' => 'includes/context/IContextSource.php',
        'RequestContext' => 'includes/context/RequestContext.php',
  
+       # includes/dao
+       'IDBAccessObject' => 'includes/dao/IDBAccessObject.php',
        # includes/db
        'Blob' => 'includes/db/DatabaseUtility.php',
        'ChronologyProtector' => 'includes/db/LBFactory.php',
        'TestFileIterator' => 'tests/testHelpers.inc',
        'TestRecorder' => 'tests/testHelpers.inc',
  
 +      # tests/phpunit
 +      'RevisionStorageTest' => 'tests/phpunit/includes/RevisionStorageTest.php',
 +      'WikiPageTest' => 'tests/phpunit/includes/WikiPageTest.php',
 +      'WikitextContentTest' => 'tests/phpunit/includes/WikitextContentTest.php',
 +      'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php',
 +      'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +      'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +
        # tests/phpunit/includes/db
        'ORMRowTest' => 'tests/phpunit/includes/db/ORMRowTest.php',
  
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /**
-  * Default values for configuration settings.
+  * Default values for MediaWiki configuration settings.
   *
   *
   *                 NEVER EDIT THIS FILE
@@@ -17,6 -17,9 +17,9 @@@
   * Documentation is in the source and on:
   * http://www.mediawiki.org/wiki/Manual:Configuration_settings
   *
+  * @warning  Note: this (and other things) will break if the autoloader is not
+  * enabled. Please include includes/AutoLoader.php before including this file.
+  *
   * 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
   * @file
   */
  
+ /**
+  * @defgroup Globalsettings Global settings
+  */
  /**
   * @cond file_level_code
-  * This is not a valid entry point, perform no further processing unless MEDIAWIKI is defined
+  * This is not a valid entry point, perform no further processing unless
+  * MEDIAWIKI is defined
   */
  if( !defined( 'MEDIAWIKI' ) ) {
        echo "This file is part of MediaWiki and is not a valid entry point\n";
        die( 1 );
  }
  
- # Create a site configuration object. Not used for much in a default install.
- # Note: this (and other things) will break if the autoloader is not enabled.
- # Please include includes/AutoLoader.php before including this file.
+ /**
+  * wgConf hold the site configuration.
+  * Not used for much in a default install.
+  */
  $wgConf = new SiteConfiguration;
- /** @endcond */
  
  /** MediaWiki version number */
  $wgVersion = '1.20alpha';
@@@ -59,10 -67,10 +67,10 @@@ $wgSitename = 'MediaWiki'
  /**
   * URL of the server.
   *
-  * Example:
-  * <code>
+  * @par Example:
+  * @code
   * $wgServer = 'http://example.com';
-  * </code>
+  * @endcode
   *
   * This is usually detected correctly by MediaWiki. If MediaWiki detects the
   * wrong server, it will redirect incorrectly after you save a page. In that
@@@ -128,28 -136,6 +136,6 @@@ $wgUsePathInfo 
   */
  $wgScriptExtension  = '.php';
  
- /**
-  * The URL path to index.php.
-  *
-  * Will default to "{$wgScriptPath}/index{$wgScriptExtension}" in Setup.php
-  */
- $wgScript = false;
- /**
-  * The URL path to redirect.php. This is a script that is used by the Nostalgia
-  * skin.
-  *
-  * Will default to "{$wgScriptPath}/redirect{$wgScriptExtension}" in Setup.php
-  */
- $wgRedirectScript = false;
- /**
-  * The URL path to load.php.
-  *
-  * Defaults to "{$wgScriptPath}/load{$wgScriptExtension}".
-  */
- $wgLoadScript = false;
  
  /**@}*/
  
   */
  
  /**
-  * The URL path of the skins directory. Will default to "{$wgScriptPath}/skins" in Setup.php
+  * The URL path to index.php.
+  *
+  * Defaults to "{$wgScriptPath}/index{$wgScriptExtension}".
+  */
+ $wgScript = false;
+ /**
+  * The URL path to redirect.php. This is a script that is used by the Nostalgia
+  * skin.
+  *
+  * Defaults to "{$wgScriptPath}/redirect{$wgScriptExtension}".
+  */
+ $wgRedirectScript = false;
+ /**
+  * The URL path to load.php.
+  *
+  * Defaults to "{$wgScriptPath}/load{$wgScriptExtension}".
+  */
+ $wgLoadScript = false;
+ /**
+  * The URL path of the skins directory.
+  * Defaults to "{$wgScriptPath}/skins".
   */
  $wgStylePath = false;
  $wgStyleSheetPath = &$wgStylePath;
@@@ -191,7 -200,8 +200,8 @@@ $wgLocalStylePath = false
  $wgExtensionAssetsPath = false;
  
  /**
-  * Filesystem stylesheets directory. Will default to "{$IP}/skins" in Setup.php
+  * Filesystem stylesheets directory.
+  * Defaults to "{$IP}/skins".
   */
  $wgStyleDirectory = false;
  
   * The URL path for primary article page views. This path should contain $1,
   * which is replaced by the article title.
   *
-  * Will default to "{$wgScript}/$1" or "{$wgScript}?title=$1" in Setup.php,
+  * Defaults to "{$wgScript}/$1" or "{$wgScript}?title=$1",
   * depending on $wgUsePathInfo.
   */
  $wgArticlePath = false;
  
  /**
-  * The URL path for the images directory. Will default to "{$wgScriptPath}/images" in Setup.php
+  * The URL path for the images directory.
+  * Defaults to "{$wgScriptPath}/images".
   */
  $wgUploadPath = false;
  
  /**
-  * The maximum age of temporary (incomplete) uploaded files
+  * The filesystem path of the images directory. Defaults to "{$IP}/images".
   */
- $wgUploadStashMaxAge = 6 * 3600; // 6 hours
+ $wgUploadDirectory = false;
  
  /**
-  * The filesystem path of the images directory. Defaults to "{$IP}/images".
+  * Directory where the cached page will be saved.
+  * Defaults to "{$wgUploadDirectory}/cache".
   */
- $wgUploadDirectory = false;
+ $wgFileCacheDirectory = false;
  
  /**
   * The URL path of the wiki logo. The logo size should be 135x135 pixels.
-  * Will default to "{$wgStylePath}/common/images/wiki.png" in Setup.php
+  * Defaults to "{$wgStylePath}/common/images/wiki.png".
   */
  $wgLogo = false;
  
@@@ -241,14 -253,14 +253,14 @@@ $wgAppleTouchIcon = false
   * be web accessible.
   *
   * When this setting is set to false, its value will be set through a call
-  * to wfTempDir(). See that methods implementation for the actul detection
+  * to wfTempDir(). See that methods implementation for the actual detection
   * logic.
   *
   * Developers should use the global function wfTempDir() instead of this
   * variable.
   *
   * @see wfTempDir()
-  * @note Default modified to false in v1.20
+  * @note Default changed to false in MediaWiki 1.20.
   *
   */
  $wgTmpDirectory = false;
@@@ -269,11 -281,16 +281,16 @@@ $wgUploadStashScalerBaseUrl = false
  
  /**
   * To set 'pretty' URL paths for actions other than
-  * plain page views, add to this array. For instance:
+  * plain page views, add to this array.
+  *
+  * @par Example:
+  * Set pretty URL for the edit action:
+  * @code
   *   'edit' => "$wgScriptPath/edit/$1"
+  * @endcode
   *
-  * There must be an appropriate script or rewrite rule
-  * in place to handle these URLs.
+  * There must be an appropriate script or rewrite rule in place to handle these
+  * URLs.
   */
  $wgActionPaths = array();
  
  /** Uploads have to be specially set up to be secure */
  $wgEnableUploads = false;
  
+ /**
+  * The maximum age of temporary (incomplete) uploaded files
+  */
+ $wgUploadStashMaxAge = 6 * 3600; // 6 hours
  /** Allows to move images and other media files */
  $wgAllowImageMoving = true;
  
  /**
-  * These are additional characters that should be replaced with '-' in file names
+  * These are additional characters that should be replaced with '-' in filenames
   */
  $wgIllegalFileChars = ":";
  
  $wgFileStore = array();
  
  /**
-  * What directory to place deleted uploads in
+  * What directory to place deleted uploads in.
+  * Defaults to "{$wgUploadDirectory}/deleted".
   */
- $wgDeletedDirectory = false; //  Defaults to $wgUploadDirectory/deleted
+ $wgDeletedDirectory = false;
  
  /**
   * Set this to true if you use img_auth and want the user to see details on why access failed.
@@@ -338,7 -361,7 +361,7 @@@ $wgImgAuthPublicTest = true
   *                          container : backend container name the zone is in
   *                          directory : root path within container for the zone
   *                          url       : base URL to the root of the zone
-  *                      Zones default to using <repo name>-<zone name> as the container name
+  *                      Zones default to using "<repo name>-<zone name>" as the container name
   *                      and default to using the container root as the zone's root directory.
   *                      Nesting of zone locations within other zones should be avoided.
   *   - url              Public zone URL. The 'zones' settings take precedence.
   *                      is 0644.
   *   - directory        The local filesystem directory where public files are stored. Not used for
   *                      some remote repos.
-  *   - thumbDir         The base thumbnail directory. Defaults to <directory>/thumb.
-  *   - thumbUrl         The base thumbnail URL. Defaults to <url>/thumb.
+  *   - thumbDir         The base thumbnail directory. Defaults to "<directory>/thumb".
+  *   - thumbUrl         The base thumbnail URL. Defaults to "<url>/thumb".
   *
   *
   * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored
@@@ -409,10 -432,11 +432,11 @@@ $wgUseInstantCommons = false
   * File backend structure configuration.
   * This is an array of file backend configuration arrays.
   * Each backend configuration has the following parameters:
-  *     'name'        : A unique name for the backend
-  *     'class'       : The file backend class to use
-  *     'wikiId'      : A unique string that identifies the wiki (container prefix)
-  *     'lockManager' : The name of a lock manager (see $wgLockManagers)
+  *  - 'name'        : A unique name for the backend
+  *  - 'class'       : The file backend class to use
+  *  - 'wikiId'      : A unique string that identifies the wiki (container prefix)
+  *  - 'lockManager' : The name of a lock manager (see $wgLockManagers)
+  *
   * Additional parameters are specific to the class used.
   */
  $wgFileBackends = array();
  /**
   * Array of configuration arrays for each lock manager.
   * Each backend configuration has the following parameters:
-  *     'name'        : A unique name for the lock manger
-  *     'class'       : The lock manger class to use
+  *  - 'name'        : A unique name for the lock manager
+  *  - 'class'       : The lock manger class to use
   * Additional parameters are specific to the class used.
   */
  $wgLockManagers = array();
   * Show EXIF data, on by default if available.
   * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php
   *
-  * NOTE FOR WINDOWS USERS:
-  * To enable EXIF functions, add the following lines to the
-  * "Windows extensions" section of php.ini:
-  *
+  * @note FOR WINDOWS USERS:
+  * To enable EXIF functions, add the following lines to the "Windows
+  * extensions" section of php.ini:
+  * @code{.ini}
   * extension=extensions/php_mbstring.dll
   * extension=extensions/php_exif.dll
+  * @endcode
   */
  $wgShowEXIF = function_exists( 'exif_read_data' );
  
@@@ -460,28 -485,38 +485,38 @@@ $wgUpdateCompatibleMetadata = false
   * $wgForeignFileRepos variable.
   */
  $wgUseSharedUploads = false;
  /** Full path on the web server where shared uploads can be found */
  $wgSharedUploadPath = "http://commons.wikimedia.org/shared/images";
  /** Fetch commons image description pages and display them on the local wiki? */
  $wgFetchCommonsDescriptions = false;
  /** Path on the file system where shared uploads can be found. */
  $wgSharedUploadDirectory = "/var/www/wiki3/images";
  /** DB name with metadata about shared directory. Set this to false if the uploads do not come from a wiki. */
  $wgSharedUploadDBname = false;
  /** Optional table prefix used in database. */
  $wgSharedUploadDBprefix = '';
  /** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */
  $wgCacheSharedUploads = true;
  /**
- * Allow for upload to be copied from an URL. Requires Special:Upload?source=web
- * The timeout for copy uploads is set by $wgHTTPTimeout.
- */
+  * Allow for upload to be copied from an URL. Requires Special:Upload?source=web
+  * The timeout for copy uploads is set by $wgHTTPTimeout.
+  * You have to assign the user right 'upload_by_url' to a user group, to use this.
+  */
  $wgAllowCopyUploads = false;
  /**
   * Allow asynchronous copy uploads.
   * This feature is experimental and broken as of r81612.
   */
  $wgAllowAsyncCopyUploads = false;
  /**
   * A list of domains copy uploads can come from
   */
@@@ -493,11 -528,13 +528,13 @@@ $wgCopyUploadsDomains = array()
   * file and url keys. If the * key is set this value will be used as maximum
   * for non-specified types.
   *
-  * For example:
+  * @par Example:
+  * @code
   * $wgMaxUploadSize = array(
   *     '*' => 250 * 1024,
   *     'url' => 500 * 1024,
   * );
+  * @endcode
   * Sets the maximum for all uploads to 250 kB except for upload-by-url, which
   * will have a maximum of 500 kB.
   *
@@@ -507,27 -544,37 +544,37 @@@ $wgMaxUploadSize = 1024*1024*100; # 100
  /**
   * Point the upload navigation link to an external URL
   * Useful if you want to use a shared repository by default
-  * without disabling local uploads (use $wgEnableUploads = false for that)
-  * e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload';
+  * without disabling local uploads (use $wgEnableUploads = false for that).
+  *
+  * @par Example:
+  * @code
+  * $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload';
+  * @endcode
   */
  $wgUploadNavigationUrl = false;
  
  /**
   * Point the upload link for missing files to an external URL, as with
-  * $wgUploadNavigationUrl. The URL will get (?|&)wpDestFile=<filename>
+  * $wgUploadNavigationUrl. The URL will get "(?|&)wpDestFile=<filename>"
   * appended to it as appropriate.
   */
  $wgUploadMissingFileUrl = false;
  
  /**
-  * Give a path here to use thumb.php for thumbnail generation on client request, instead of
-  * generating them on render and outputting a static URL. This is necessary if some of your
-  * apache servers don't have read/write access to the thumbnail path.
+  * Give a path here to use thumb.php for thumbnail generation on client
+  * request, instead of generating them on render and outputting a static URL.
+  * This is necessary if some of your apache servers don't have read/write
+  * access to the thumbnail path.
   *
-  * Example:
+  * @par Example:
+  * @code
   *   $wgThumbnailScriptPath = "{$wgScriptPath}/thumb{$wgScriptExtension}";
+  * @endcode
   */
  $wgThumbnailScriptPath = false;
+ /**
+  * @see $wgThumbnailScriptPath
+  */
  $wgSharedThumbnailScriptPath = false;
  
  /**
   * maintenance/rebuildImages.php to register them in the database. This is no
   * longer recommended, use maintenance/importImages.php instead.
   *
-  * Note that this variable may be ignored if $wgLocalFileRepo is set.
+  * @note That this variable may be ignored if $wgLocalFileRepo is set.
+  * @todo Deprecate the setting and ultimately remove it from Core.
   */
  $wgHashedUploadDirectory = true;
  
@@@ -565,13 -613,17 +613,17 @@@ $wgRepositoryBaseUrl = "http://commons.
   * This is the list of preferred extensions for uploading files. Uploading files
   * with extensions not in this list will trigger a warning.
   *
-  * WARNING: If you add any OpenOffice or Microsoft Office file formats here,
+  * @warning If you add any OpenOffice or Microsoft Office file formats here,
   * such as odt or doc, and untrusted users are allowed to upload files, then
   * your wiki will be vulnerable to cross-site request forgery (CSRF).
   */
  $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' );
  
- /** Files with these extensions will never be allowed as uploads. */
+ /**
+  * Files with these extensions will never be allowed as uploads.
+  * An array of file extensions to blacklist. You should append to this array
+  * if you want to blacklist additional files.
+  * */
  $wgFileBlacklist = array(
        # HTML may contain cookie-stealing JavaScript and web bugs
        'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht',
@@@ -609,7 -661,7 +661,7 @@@ $wgAllowJavaUploads = false
  /**
   * This is a flag to determine whether or not to check file extensions on upload.
   *
-  * WARNING: setting this to false is insecure for public wikis.
+  * @warning Setting this to false is insecure for public wikis.
   */
  $wgCheckFileExtensions = true;
  
   * If this is turned off, users may override the warning for files not covered
   * by $wgFileExtensions.
   *
-  * WARNING: setting this to false is insecure for public wikis.
+  * @warning Setting this to false is insecure for public wikis.
   */
  $wgStrictFileExtensions = true;
  
  /**
   * Setting this to true will disable the upload system's checks for HTML/JavaScript.
-  * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions
-  * TO RESTRICT UPLOADING to only those that you trust
+  *
+  * @warning THIS IS VERY DANGEROUS on a publicly editable site, so USE
+  * $wgGroupPermissions TO RESTRICT UPLOADING to only those that you trust
   */
  $wgDisableUploadScriptChecks = false;
  
- /** Warn if uploaded files are larger than this (in bytes), or false to disable*/
+ /**
+  * Warn if uploaded files are larger than this (in bytes), or false to disable
+  */
  $wgUploadSizeWarning = false;
  
  /**
@@@ -655,30 -710,20 +710,30 @@@ $wgTrustedMediaFormats = array
   * Each entry in the array maps a MIME type to a class name
   */
  $wgMediaHandlers = array(
-       'image/jpeg' => 'JpegHandler',
-       'image/png' => 'PNGHandler',
-       'image/gif' => 'GIFHandler',
-       'image/tiff' => 'TiffHandler',
+       'image/jpeg'     => 'JpegHandler',
+       'image/png'      => 'PNGHandler',
+       'image/gif'      => 'GIFHandler',
+       'image/tiff'     => 'TiffHandler',
        'image/x-ms-bmp' => 'BmpHandler',
-       'image/x-bmp' => 'BmpHandler',
-       'image/x-xcf' => 'XCFHandler',
-       'image/svg+xml' => 'SvgHandler', // official
-       'image/svg' => 'SvgHandler', // compat
+       'image/x-bmp'    => 'BmpHandler',
+       'image/x-xcf'    => 'XCFHandler',
+       'image/svg+xml'  => 'SvgHandler', // official
+       'image/svg'      => 'SvgHandler', // compat
        'image/vnd.djvu' => 'DjVuHandler', // official
-       'image/x.djvu' => 'DjVuHandler', // compat
-       'image/x-djvu' => 'DjVuHandler', // compat
+       'image/x.djvu'   => 'DjVuHandler', // compat
+       'image/x-djvu'   => 'DjVuHandler', // compat
  );
  
 +/**
 + * Plugins for page content model handling.
 + * Each entry in the array maps a model id to a class name
 + */
 +$wgContentHandlers = array(
 +      CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
 +      CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
 +      CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
 +);
 +
  /**
   * Resizing can be done using PHP's internal image libraries or using
   * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@@ -710,17 -755,18 +765,18 @@@ $wgImageMagickTempDir = false
   * %s will be replaced with the source path, %d with the destination
   * %w and %h will be replaced with the width and height.
   *
-  * Example for GraphicMagick:
-  * <code>
+  * @par Example for GraphicMagick:
+  * @code
   * $wgCustomConvertCommand = "gm convert %s -resize %wx%h %d"
-  * </code>
+  * @endcode
   *
   * Leave as false to skip this.
   */
  $wgCustomConvertCommand = false;
  
  /**
-  * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some image formats.
+  * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some
+  * image formats.
   */
  $wgExiv2Command = '/usr/bin/exiv2';
  
@@@ -742,22 -788,31 +798,31 @@@ $wgSVGConverters = array
        'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output',
        'ImagickExt' => array( 'SvgHandler::rasterizeImagickExt' ),
        );
  /** Pick a converter defined in $wgSVGConverters */
  $wgSVGConverter = 'ImageMagick';
  /** If not in the executable PATH, specify the SVG converter path. */
  $wgSVGConverterPath = '';
  /** Don't scale a SVG larger than this */
  $wgSVGMaxSize = 2048;
  /** Don't read SVG metadata beyond this point.
-  * Default is 1024*256 bytes */
+  * Default is 1024*256 bytes
+  */
  $wgSVGMetadataCutoff = 262144;
  
  /**
-  * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't
-  * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading
-  * crap files as images. When this directive is on, <title> will be allowed in files with
-  * an "image/svg+xml" MIME type. You should leave this disabled if your web server is misconfigured
-  * and doesn't send appropriate MIME types for SVG images.
+  * Disallow <title> element in SVG files.
+  *
+  * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic
+  * browsers which can not perform basic stuff like MIME detection and which are
+  * vulnerable to further idiots uploading crap files as images.
+  *
+  * When this directive is on, "<title>" will be allowed in files with an
+  * "image/svg+xml" MIME type. You should leave this disabled if your web server
+  * is misconfigured and doesn't send appropriate MIME types for SVG images.
   */
  $wgAllowTitlesInSVG = false;
  
@@@ -787,13 -842,13 +852,13 @@@ $wgMaxAnimatedGifArea = 1.25e7
   * For inline display, we need to convert to PNG or JPEG.
   * Note scaling should work with ImageMagick, but may not with GD scaling.
   *
-  * Example:
-  * <code>
+  * @par Example:
+  * @code
   *  // PNG is lossless, but inefficient for photos
   *  $wgTiffThumbnailType = array( 'png', 'image/png' );
   *  // JPEG is good for photos, but has no transparency support. Bad for diagrams.
   *  $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' );
-  * </code>
+  * @endcode
   */
   $wgTiffThumbnailType = false;
  
  $wgThumbnailEpoch = '20030516000000';
  
  /**
-  * If set, inline scaled images will still produce <img> tags ready for
+  * If set, inline scaled images will still produce "<img>" tags ready for
   * output instead of showing an error message.
   *
   * This may be useful if errors are transitory, especially if the site
@@@ -898,20 -953,6 +963,6 @@@ $wgAntivirusSetup = array
  
                'messagepattern' => '/.*?:(.*)/sim',
        ),
-       #setup for f-prot
-       'f-prot' => array (
-               'command' => "f-prot ",
-               'codemap' => array (
-                       "0" => AV_NO_VIRUS, # no virus
-                       "3" => AV_VIRUS_FOUND, # virus found
-                       "6" => AV_VIRUS_FOUND, # virus found
-                       "*" => AV_SCAN_FAILED, # else scan failed
-               ),
-               'messagepattern' => '/.*?Infection:(.*)$/m',
-       ),
  );
  
  
@@@ -941,10 -982,11 +992,11 @@@ $wgLoadFileinfoExtension = false
   * the mime type to standard output.
   * The name of the file to process will be appended to the command given here.
   * If not set or NULL, mime_content_type will be used if available.
-  * Example:
-  * <code>
+  *
+  * @par Example:
+  * @code
   * #$wgMimeDetectorCommand = "file -bi"; # use external mime detector (Linux)
-  * </code>
+  * @endcode
   */
  $wgMimeDetectorCommand = null;
  
@@@ -998,7 -1040,7 +1050,7 @@@ $wgThumbLimits = array
  );
  
  /**
-  * Default parameters for the <gallery> tag
+  * Default parameters for the "<gallery>" tag
   */
  $wgGalleryOptions = array (
        'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize
@@@ -1021,7 -1063,10 +1073,10 @@@ $wgThumbUpright = 0.75
  $wgDirectoryMode = 0777;
  
  /**
-  * DJVU settings
+  * @name DJVU settings
+  * @{
+  */
+ /**
   * Path of the djvudump executable
   * Enable this and $wgDjvuRenderer to enable djvu rendering
   */
@@@ -1046,15 -1091,18 +1101,18 @@@ $wgDjvuTxt = null
   * Path of the djvutoxml executable
   * This works like djvudump except much, much slower as of version 3.5.
   *
-  * For now I recommend you use djvudump instead. The djvuxml output is
+  * For now we  recommend you use djvudump instead. The djvuxml output is
   * probably more stable, so we'll switch back to it as soon as they fix
   * the efficiency problem.
   * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583
+  *
+  * @par Example:
+  * @code
+  * $wgDjvuToXML = 'djvutoxml';
+  * @endcode
   */
- # $wgDjvuToXML = 'djvutoxml';
  $wgDjvuToXML = null;
  
  /**
   * Shell command for the DJVU post processor
   * Default: pnmtopng, since ddjvu generates ppm output
@@@ -1065,6 -1113,7 +1123,7 @@@ $wgDjvuPostProcessor = 'pnmtojpeg'
   * File extension for the DJVU post processor output
   */
  $wgDjvuOutputExtension = 'jpg';
+ /** @} */ # end of DJvu }
  
  /** @} */ # end of file uploads }
  
@@@ -1141,17 -1190,21 +1200,21 @@@ $wgNewPasswordExpiry = 3600 * 24 * 7
  $wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60;
  
  /**
-  * SMTP Mode
+  * SMTP Mode.
+  *
   * For using a direct (authenticated) SMTP server connection.
   * Default to false or fill an array :
-  * <code>
-  * "host" => 'SMTP domain',
-  * "IDHost" => 'domain for MessageID',
-  * "port" => "25",
-  * "auth" => true/false,
-  * "username" => user,
-  * "password" => password
-  * </code>
+  *
+  * @code
+  * $wgSMTP = array(
+  *    'host'     => 'SMTP domain',
+  *    'IDHost'   => 'domain for MessageID',
+  *    'port'     => '25',
+  *    'auth'     => [true|false],
+  *    'username' => [SMTP username],
+  *    'password' => [SMTP password],
+  * );
+  * @endcode
   */
  $wgSMTP = false;
  
@@@ -1173,9 -1226,9 +1236,9 @@@ $wgEnotifFromEditor = false
  # It call this to be a "user-preferences-option (UPO)"
  
  /**
-  * Require email authentication before sending mail to an email addres. This is
-  * highly recommended. It prevents MediaWiki from being used as an open spam
-  * relay.
+  * Require email authentication before sending mail to an email address.
+  * This is highly recommended. It prevents MediaWiki from being used as an open
+  * spam relay.
   */
  $wgEmailAuthentication = true;
  
@@@ -1354,9 -1407,9 +1417,9 @@@ $wgSharedTables = array( 'user', 'user_
   * accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your
   * slaves in my.cnf. You can set read_only mode at runtime using:
   *
-  * <code>
+  * @code
   *     SET @@read_only=1;
-  * </code>
+  * @endcode
   *
   * Since the effect of writing to a slave is so damaging and difficult to clean
   * up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even
@@@ -1381,13 -1434,19 +1444,19 @@@ $wgMasterWaitTimeout = 10
  
  /** File to log database errors to */
  $wgDBerrorLog = false;
+ /**
+  * Override wiki timezone to UTC for wgDBerrorLog
+  * @since 1.20
+  */
+ $wgDBerrorLogInUTC = false;
  
  /** When to give an error message */
  $wgDBClusterTimeout = 10;
  
  /**
-  * Scale load balancer polling time so that under overload conditions, the database server
-  * receives a SHOW STATUS query at an average interval of this many microseconds
+  * Scale load balancer polling time so that under overload conditions, the
+  * database server receives a SHOW STATUS query at an average interval of this
+  * many microseconds
   */
  $wgDBAvgStatusPoll = 2000;
  
   * Set to true to engage MySQL 4.1/5.0 charset-related features;
   * for now will just cause sending of 'SET NAMES=utf8' on connect.
   *
-  * WARNING: THIS IS EXPERIMENTAL!
+  * @warning THIS IS EXPERIMENTAL!
   *
   * May break if you're not using the table defs from mysql5/tables.sql.
   * May break if you're upgrading an existing wiki if set differently.
@@@ -1448,19 -1507,30 +1517,30 @@@ $wgCompressRevisions = false
  
  /**
   * External stores allow including content
-  * from non database sources following URL links
+  * from non database sources following URL links.
   *
   * Short names of ExternalStore classes may be specified in an array here:
+  * @code
   * $wgExternalStores = array("http","file","custom")...
+  * @endcode
   *
   * CAUTION: Access to database might lead to code execution
   */
  $wgExternalStores = false;
  
  /**
-  * An array of external mysql servers, e.g.
-  * $wgExternalServers = array( 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) );
-  * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to another class.
+  * An array of external MySQL servers.
+  *
+  * @par Example:
+  * Create a cluster named 'cluster1' containing three servers:
+  * @code
+  * $wgExternalServers = array(
+  *    'cluster1' => array( 'srv28', 'srv29', 'srv30' )
+  * );
+  * @endcode
+  *
+  * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to
+  * another class.
   */
  $wgExternalServers = array();
  
   * Part of a URL, e.g. DB://cluster1
   *
   * Can be an array instead of a single string, to enable data distribution. Keys
-  * must be consecutive integers, starting at zero. Example:
+  * must be consecutive integers, starting at zero.
   *
+  * @par Example:
+  * @code
   * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' );
+  * @endcode
   *
   * @var array
   */
@@@ -1634,13 -1707,14 +1717,14 @@@ $wgObjectCaches = array
  );
  
  /**
-  * The expiry time for the parser cache, in seconds. The default is 86.4k
-  * seconds, otherwise known as a day.
+  * The expiry time for the parser cache, in seconds.
+  * The default is 86400 (one day).
   */
  $wgParserCacheExpireTime = 86400;
  
  /**
-  * Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php> to use as CACHE_DBA backend
+  * Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php>
+  * to use as CACHE_DBA backend.
   */
  $wgDBAhandler = 'db3';
  
@@@ -1683,9 -1757,9 +1767,9 @@@ $wgMemCachedTimeout = 100000
  $wgUseLocalMessageCache = false;
  
  /**
-  * Defines format of local cache
-  * true - Serialized object
-  * false - PHP source file (Warning - security risk)
+  * Defines format of local cache.
+  *  - true: Serialized object
+  *  - false: PHP source file (Warning - security risk)
   */
  $wgLocalMessageCacheSerialized = true;
  
@@@ -1698,23 -1772,23 +1782,23 @@@ $wgAdaptiveMessageCache = false
  
  /**
   * Localisation cache configuration. Associative array with keys:
-  *     class:       The class to use. May be overridden by extensions.
+  * class:       The class to use. May be overridden by extensions.
   *
-  *     store:       The location to store cache data. May be 'files', 'db' or
-  *                  'detect'. If set to "files", data will be in CDB files. If set
-  *                  to "db", data will be stored to the database. If set to
-  *                  "detect", files will be used if $wgCacheDirectory is set,
-  *                  otherwise the database will be used.
+  * store:       The location to store cache data. May be 'files', 'db' or
+  *              'detect'. If set to "files", data will be in CDB files. If set
+  *              to "db", data will be stored to the database. If set to
+  *              "detect", files will be used if $wgCacheDirectory is set,
+  *              otherwise the database will be used.
   *
-  *     storeClass:  The class name for the underlying storage. If set to a class
-  *                  name, it overrides the "store" setting.
+  * storeClass:  The class name for the underlying storage. If set to a class
+  *              name, it overrides the "store" setting.
   *
-  *     storeDirectory:  If the store class puts its data in files, this is the
-  *                      directory it will use. If this is false, $wgCacheDirectory
-  *                      will be used.
+  * storeDirectory:  If the store class puts its data in files, this is the
+  *                  directory it will use. If this is false, $wgCacheDirectory
+  *                  will be used.
   *
-  *     manualRecache:   Set this to true to disable cache updates on web requests.
-  *                      Use maintenance/rebuildLocalisationCache.php instead.
+  * manualRecache:   Set this to true to disable cache updates on web requests.
+  *                  Use maintenance/rebuildLocalisationCache.php instead.
   */
  $wgLocalisationCacheConf = array(
        'class' => 'LocalisationCache',
@@@ -1729,14 -1803,17 +1813,17 @@@ $wgCachePages = true
  
  /**
   * Set this to current time to invalidate all prior cached pages. Affects both
-  * client- and server-side caching.
+  * client-side and server-side caching.
   * You can get the current date on your server by using the command:
+  * @verbatim
   *   date +%Y%m%d%H%M%S
+  * @endverbatim
   */
  $wgCacheEpoch = '20030516000000';
  
  /**
   * Bump this number when changing the global style sheets and JavaScript.
+  *
   * It should be appended in the query string of static CSS and JS includes,
   * to ensure that client-side caches do not keep obsolete copies of global
   * styles.
@@@ -1752,12 -1829,6 +1839,6 @@@ $wgStyleVersion = '303'
   */
  $wgUseFileCache = false;
  
- /**
-  * Directory where the cached page will be saved.
-  * Will default to "{$wgUploadDirectory}/cache" in Setup.php
-  */
- $wgFileCacheDirectory = false;
  /**
   * Depth of the subdirectory hierarchy to be created under
   * $wgFileCacheDirectory.  The subdirectories will be named based on
@@@ -1868,10 -1939,12 +1949,12 @@@ $wgUseXVO = false
  $wgVaryOnXFP = false;
  
  /**
-  * Internal server name as known to Squid, if different. Example:
-  * <code>
+  * Internal server name as known to Squid, if different.
+  *
+  * @par Example:
+  * @code
   * $wgInternalServer = 'http://yourinternal.tld:8000';
-  * </code>
+  * @endcode
   */
  $wgInternalServer = false;
  
@@@ -1919,6 -1992,7 +2002,7 @@@ $wgMaxSquidPurgeTitles = 400
   *
   * Example configuration to send purges for upload.wikimedia.org to one
   * multicast group and all other purges to another:
+  * @code
   * $wgHTCPMulticastRouting = array(
   *         '|^https?://upload\.wikimedia\.org|' => array(
   *                 'host' => '239.128.0.113',
   *                 'port' => 4827,
   *         ),
   * );
+  * @endcode
   *
   * @see $wgHTCPMulticastTTL
   */
@@@ -1977,11 -2052,12 +2062,12 @@@ $wgLanguageCode = 'en'
  
  /**
   * Some languages need different word forms, usually for different cases.
-  * Used in Language::convertGrammar(). Example:
+  * Used in Language::convertGrammar().
   *
-  * <code>
+  * @par Example:
+  * @code
   * $wgGrammarForms['en']['genitive']['car'] = 'car\'s';
-  * </code>
+  * @endcode
   */
  $wgGrammarForms = array();
  
@@@ -2064,7 -2140,7 +2150,7 @@@ $wgAllUnicodeFixes = false
   * converting a wiki from MediaWiki 1.4 or earlier to UTF-8 without the
   * burdensome mass conversion of old text data.
   *
-  * NOTE! This DOES NOT touch any fields other than old_text.Titles, comments,
+  * @note This DOES NOT touch any fields other than old_text. Titles, comments,
   * user names, etc still must be converted en masse in the database before
   * continuing as a UTF-8 wiki.
   */
@@@ -2173,28 -2249,27 +2259,27 @@@ $wgCanonicalLanguageLinks = true
  $wgDefaultLanguageVariant = false;
  
  /**
-  * Disabled variants array of language variant conversion. Example:
-  * <code>
+  * Disabled variants array of language variant conversion.
+  *
+  * @par Example:
+  * @code
   *  $wgDisabledVariants[] = 'zh-mo';
   *  $wgDisabledVariants[] = 'zh-my';
-  * </code>
-  *
-  * or:
-  *
-  * <code>
-  *  $wgDisabledVariants = array('zh-mo', 'zh-my');
-  * </code>
+  * @endcode
   */
  $wgDisabledVariants = array();
  
  /**
   * Like $wgArticlePath, but on multi-variant wikis, this provides a
   * path format that describes which parts of the URL contain the
-  * language variant.  For Example:
+  * language variant.
   *
-  *   $wgLanguageCode = 'sr';
-  *   $wgVariantArticlePath = '/$2/$1';
-  *   $wgArticlePath = '/wiki/$1';
+  * @par Example:
+  * @code
+  *    $wgLanguageCode = 'sr';
+  *    $wgVariantArticlePath = '/$2/$1';
+  *    $wgArticlePath = '/wiki/$1';
+  * @endcode
   *
   * A link to /wiki/ would be redirected to /sr/Главна_страна
   *
@@@ -2220,10 -2295,14 +2305,14 @@@ $wgLoginLanguageSelector = false
   * wfMsg(). The code behaves this way by default. However, sites like the
   * Wikimedia Commons do offer different versions of 'mainpage' and the like for
   * different languages. This array provides a way to override the default
-  * behavior. For example, to allow language-specific main page and community
-  * portal, set
+  * behavior.
   *
-  * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
+  * @par Example:
+  * To allow language-specific main page and community
+  * portal:
+  * @code
+  *    $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
+  * @endcode
   */
  $wgForceUIMsgAsContentMsg = array();
  
   * Timezones can be translated by editing MediaWiki messages of type
   * timezone-nameinlowercase like timezone-utc.
   *
-  * Examples:
-  * <code>
+  * @par Examples:
+  * @code
   * $wgLocaltimezone = 'GMT';
   * $wgLocaltimezone = 'PST8PDT';
   * $wgLocaltimezone = 'Europe/Sweden';
   * $wgLocaltimezone = 'CET';
-  * </code>
+  * @endcode
   */
  $wgLocaltimezone = null;
  
@@@ -2265,7 -2344,7 +2354,7 @@@ $wgLocalTZoffset = null
   * language variant conversion is disabled in interface messages. Setting this
   * to true re-enables it.
   *
-  * This variable should be removed (implicitly false) in 1.20 or earlier.
+  * @todo This variable should be removed (implicitly false) in 1.20 or earlier.
   */
  $wgBug34832TransitionalRollback = true;
  
@@@ -2362,10 -2441,14 +2451,14 @@@ $wgWellFormedXml = true
  
  /**
   * Permit other namespaces in addition to the w3.org default.
-  * Use the prefix for the key and the namespace for the value. For
-  * example:
+  *
+  * Use the prefix for the key and the namespace for the value.
+  *
+  * @par Example:
+  * @code
   * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg';
-  * Normally we wouldn't have to define this in the root <html>
+  * @endCode
+  * Normally we wouldn't have to define this in the root "<html>"
   * element, but IE needs it there in some circumstances.
   *
   * This is ignored if $wgHtml5 is true, for the same reason as
@@@ -2376,7 -2459,7 +2469,7 @@@ $wgXhtmlNamespaces = array()
  /**
   * Show IP address, for non-logged in users. It's necessary to switch this off
   * for some forms of caching.
-  * Will disable file cache.
+  * @warning Will disable file cache.
   */
  $wgShowIPinHeader = true;
  
@@@ -2535,15 -2618,16 +2628,16 @@@ $wgExperimentalHtmlIds = false
   * The value should be either a string or an array. If it is a string it will be output
   * directly as html, however some skins may choose to ignore it. An array is the preferred format
   * for the icon, the following keys are used:
-  *   src: An absolute url to the image to use for the icon, this is recommended
+  *  - src: An absolute url to the image to use for the icon, this is recommended
   *        but not required, however some skins will ignore icons without an image
-  *   url: The url to use in the <a> arround the text or icon, if not set an <a> will not be outputted
-  *   alt: This is the text form of the icon, it will be displayed without an image in
+  * - url: The url to use in the a element arround the text or icon, if not set an a element will not be outputted
+  * - alt: This is the text form of the icon, it will be displayed without an image in
   *        skins like Modern or if src is not set, and will otherwise be used as
   *        the alt="" for the image. This key is required.
-  *   width and height: If the icon specified by src is not of the standard size
+  * - width and height: If the icon specified by src is not of the standard size
   *                     you can specify the size of image to use with these keys.
   *                     Otherwise they will default to the standard 88x31.
+  * @todo Reformat documentation.
   */
  $wgFooterIcons = array(
        "copyright" => array(
  );
  
  /**
-  * Login / create account link behavior when it's possible for anonymous users to create an account
-  * true = use a combined login / create account link
-  * false = split login and create account into two separate links
+  * Login / create account link behavior when it's possible for anonymous users
+  * to create an account.
+  *  - true = use a combined login / create account link
+  *  - false = split login and create account into two separate links
   */
  $wgUseCombinedLoginLink = true;
  
  /**
-  * Search form behavior for Vector skin only
-  * true = use an icon search button
-  * false = use Go & Search buttons
+  * Search form behavior for Vector skin only.
+  *  - true = use an icon search button
+  *  - false = use Go & Search buttons
   */
  $wgVectorUseSimpleSearch = false;
  
  /**
-  * Watch and unwatch as an icon rather than a link for Vector skin only
-  * true = use an icon watch/unwatch button
-  * false = use watch/unwatch text link
+  * Watch and unwatch as an icon rather than a link for Vector skin only.
+  *  - true = use an icon watch/unwatch button
+  *  - false = use watch/unwatch text link
   */
  $wgVectorUseIconWatch = false;
  
@@@ -2613,10 -2698,13 +2708,13 @@@ $wgSend404Code = true
   */
  
  /**
-  * Client-side resource modules. Extensions should add their module definitions
-  * here.
+  * Client-side resource modules.
   *
-  * Example:
+  * Extensions should add their resource loader module definitions
+  * to the $wgResourceModules variable.
+  *
+  * @par Example:
+  * @code
   *   $wgResourceModules['ext.myExtension'] = array(
   *      'scripts' => 'myExtension.js',
   *      'styles' => 'myExtension.css',
   *      'localBasePath' => dirname( __FILE__ ),
   *      'remoteExtPath' => 'MyExtension',
   *   );
+  * @endcode
   */
  $wgResourceModules = array();
  
   * built-in source that is not in this array, but defined by
   * ResourceLoader::__construct() so that it cannot be unset.
   *
-  * Example:
+  * @par Example:
+  * @code
   *   $wgResourceLoaderSources['foo'] = array(
   *       'loadScript' => 'http://example.org/w/load.php',
   *       'apiScript' => 'http://example.org/w/api.php'
   *   );
+  * @endcode
   */
  $wgResourceLoaderSources = array();
  
  /**
-  * Default 'remoteBasePath' value for resource loader modules.
+  * Default 'remoteBasePath' value for instances of ResourceLoaderFileModule.
   * If not set, then $wgScriptPath will be used as a fallback.
   */
  $wgResourceBasePath = null;
  
  /**
-  * Maximum time in seconds to cache resources served by the resource loader
+  * Maximum time in seconds to cache resources served by the resource loader.
+  *
+  * @todo Document array structure
   */
  $wgResourceLoaderMaxage = array(
        'versioned' => array(
  );
  
  /**
-  * The default debug mode (on/off) for of ResourceLoader requests. This will still
-  * be overridden when the debug URL parameter is used.
+  * The default debug mode (on/off) for of ResourceLoader requests.
+  *
+  * This will still be overridden when the debug URL parameter is used.
   */
  $wgResourceLoaderDebug = false;
  
@@@ -2690,33 -2784,54 +2794,54 @@@ $wgResourceLoaderMinifierMaxLineLength 
  
  /**
   * Whether to include the mediawiki.legacy JS library (old wikibits.js), and its
-  * dependencies
+  * dependencies.
   */
  $wgIncludeLegacyJavaScript = true;
  
  /**
-  * Whether to preload the mediawiki.util module as blocking module in the top queue.
-  * Before MediaWiki 1.19, modules used to load slower/less asynchronous which allowed
-  * modules to lack dependencies on 'popular' modules that were likely loaded already.
+  * Whether to preload the mediawiki.util module as blocking module in the top
+  * queue.
+  *
+  * Before MediaWiki 1.19, modules used to load slower/less asynchronous which
+  * allowed modules to lack dependencies on 'popular' modules that were likely
+  * loaded already.
+  *
   * This setting is to aid scripts during migration by providing mediawiki.util
   * unconditionally (which was the most commonly missed dependency).
-  * It doesn't cover all missing dependencies obviously but should fix most of them.
+  * It doesn't cover all missing dependencies obviously but should fix most of
+  * them.
+  *
   * This should be removed at some point after site/user scripts have been fixed.
-  * Enable this if your wiki has a large amount of user/site scripts that are lacking
-  * dependencies.
+  * Enable this if your wiki has a large amount of user/site scripts that are
+  * lacking dependencies.
+  * @todo Deprecate
   */
  $wgPreloadJavaScriptMwUtil = false;
  
  /**
-  * Whether or not to assing configuration variables to the global window object.
-  * If this is set to false, old code using deprecated variables like:
-  * " if ( window.wgRestrictionEdit ) ..."
+  * Whether or not to assign configuration variables to the global window object.
+  *
+  * If this is set to false, old code using deprecated variables will no longer
+  * work.
+  *
+  * @par Example of legacy code:
+  * @code{,js}
+  *    if ( window.wgRestrictionEdit ) { ... }
+  * @endcode
   * or:
-  * " if ( wgIsArticle ) ..."
-  * will no longer work and needs to use mw.config instead. For example:
-  * " if ( mw.config.exists('wgRestrictionEdit') )"
-  * or
-  * " if ( mw.config.get('wgIsArticle') )".
+  * @code{,js}
+  *    if ( wgIsArticle ) { ... }
+  * @endcode
+  *
+  * Instead, one needs to use mw.config.
+  * @par Example using mw.config global configuration:
+  * @code{,js}
+  *    if ( mw.config.exists('wgRestrictionEdit') ) { ... }
+  * @endcode
+  * or:
+  * @code{,js}
+  *    if ( mw.config.get('wgIsArticle') ) { ... }
+  * @endcode
   */
  $wgLegacyJavaScriptGlobals = true;
  
  $wgResourceLoaderMaxQueryLength = -1;
  
  /**
-  * If set to true, JavaScript modules loaded from wiki pages will be parsed prior
-  * to minification to validate it.
+  * If set to true, JavaScript modules loaded from wiki pages will be parsed
+  * prior to minification to validate it.
   *
   * Parse errors will result in a JS exception being thrown during module load,
   * which avoids breaking other modules loaded in the same request.
@@@ -2753,7 -2868,7 +2878,7 @@@ $wgResourceLoaderValidateJS = true
  $wgResourceLoaderValidateStaticJS = false;
  
  /**
-  * If set to true, asynchronous loading of bottom-queue scripts in the <head>
+  * If set to true, asynchronous loading of bottom-queue scripts in the "<head>"
   * will be enabled. This is an experimental feature that's supposed to make
   * JavaScript load faster.
   */
@@@ -2789,19 -2904,25 +2914,25 @@@ $wgMetaNamespaceTalk = false
   * names of existing namespaces. Extensions developers should use
   * $wgCanonicalNamespaceNames.
   *
-  * PLEASE  NOTE: Once you delete a namespace, the pages in that namespace will
+  * @warning Once you delete a namespace, the pages in that namespace will
   * no longer be accessible. If you rename it, then you can access them through
   * the new namespace name.
   *
   * Custom namespaces should start at 100 to avoid conflicting with standard
   * namespaces, and should always follow the even/odd main/talk pattern.
+  *
+  * @par Example:
+  * @code
+  * $wgExtraNamespaces = array(
+  *    100 => "Hilfe",
+  *    101 => "Hilfe_Diskussion",
+  *    102 => "Aide",
+  *    103 => "Discussion_Aide"
+  * );
+  * @endcode
+  *
+  * @todo Add a note about maintenance/namespaceDupes.php
   */
- # $wgExtraNamespaces = array(
- #     100 => "Hilfe",
- #     101 => "Hilfe_Diskussion",
- #     102 => "Aide",
- #     103 => "Discussion_Aide"
- # );
  $wgExtraNamespaces = array();
  
  /**
  $wgExtraGenderNamespaces = array();
  
  /**
-  * Namespace aliases
+  * Namespace aliases.
+  *
   * These are alternate names for the primary localised namespace names, which
   * are defined by $wgExtraNamespaces and the language file. If a page is
   * requested with such a prefix, the request will be redirected to the primary
   * name.
   *
   * Set this to a map from namespace names to IDs.
-  * Example:
+  *
+  * @par Example:
+  * @code
   *    $wgNamespaceAliases = array(
   *        'Wikipedian' => NS_USER,
   *        'Help' => 100,
   *    );
+  * @endcode
   */
  $wgNamespaceAliases = array();
  
   *   -  +         Enabled by default, but doesn't work with path to query rewrite rules, corrupted by apache
   *   -  ?         Enabled by default, but doesn't work with path to PATH_INFO rewrites
   *
-  * All three of these punctuation problems can be avoided by using an alias, instead of a
-  * rewrite rule of either variety.
+  * All three of these punctuation problems can be avoided by using an alias,
+  * instead of a rewrite rule of either variety.
   *
   * The problem with % is that when using a path to query rewrite rule, URLs are
   * double-unescaped: once by Apache's path conversion code, and again by PHP. So
@@@ -2866,33 -2991,47 +3001,47 @@@ $wgLocalInterwiki = false
   */
  $wgInterwikiExpiry = 10800;
  
- /** Interwiki caching settings.
-       $wgInterwikiCache specifies path to constant database file
-               This cdb database is generated by dumpInterwiki from maintenance
-               and has such key formats:
-                       dbname:key - a simple key (e.g. enwiki:meta)
-                       _sitename:key - site-scope key (e.g. wiktionary:meta)
-                       __global:key - global-scope key (e.g. __global:meta)
-                       __sites:dbname - site mapping (e.g. __sites:enwiki)
-               Sites mapping just specifies site name, other keys provide
-                       "local url" data layout.
-       $wgInterwikiScopes specify number of domains to check for messages:
-               1 - Just wiki(db)-level
-               2 - wiki and global levels
-               3 - site levels
-       $wgInterwikiFallbackSite - if unable to resolve from cache
+ /**
+  * @name Interwiki caching settings.
+  * @{
+  */
+ /**
+  *$wgInterwikiCache specifies path to constant database file.
+  *
+  * This cdb database is generated by dumpInterwiki from maintenance and has
+  * such key formats:
+  *  - dbname:key - a simple key (e.g. enwiki:meta)
+  *  - _sitename:key - site-scope key (e.g. wiktionary:meta)
+  *  - __global:key - global-scope key (e.g. __global:meta)
+  *  - __sites:dbname - site mapping (e.g. __sites:enwiki)
+  *
+  * Sites mapping just specifies site name, other keys provide "local url"
+  * data layout.
   */
  $wgInterwikiCache = false;
+ /**
+  * Specify number of domains to check for messages.
+  *    - 1: Just wiki(db)-level
+  *    - 2: wiki and global levels
+  *    - 3: site levels
+  */
  $wgInterwikiScopes = 3;
+ /**
+  *    $wgInterwikiFallbackSite - if unable to resolve from cache
+  */
  $wgInterwikiFallbackSite = 'wiki';
+ /** @} */ # end of Interwiki caching settings.
  
  /**
   * If local interwikis are set up which allow redirects,
   * set this regexp to restrict URLs which will be displayed
   * as 'redirected from' links.
   *
+  * @par Example:
   * It might look something like this:
+  * @code
   * $wgRedirectSources = '!^https?://[a-z-]+\.wikipedia\.org/!';
+  * @endcode
   *
   * Leave at false to avoid displaying any incoming redirect markers.
   * This does not affect intra-wiki redirects, which don't change
@@@ -2902,7 -3041,8 +3051,8 @@@ $wgRedirectSources = false
  
  /**
   * Set this to false to avoid forcing the first letter of links to capitals.
-  * WARNING: may break links! This makes links COMPLETELY case-sensitive. Links
+  *
+  * @warning may break links! This makes links COMPLETELY case-sensitive. Links
   * appearing with a capital at the beginning of a sentence will *not* go to the
   * same place as links in the middle of a sentence using a lowercase initial.
   */
@@@ -2916,7 -3056,11 +3066,11 @@@ $wgCapitalLinks = true
   * associated content namespaces, the values for those are ignored in favor of the
   * subject namespace's setting. Setting for NS_MEDIA is taken automatically from
   * NS_FILE.
-  * EX: $wgCapitalLinkOverrides[ NS_FILE ] = false;
+  *
+  * @par Example:
+  * @code
+  *    $wgCapitalLinkOverrides[ NS_FILE ] = false;
+  * @endcode
   */
  $wgCapitalLinkOverrides = array();
  
@@@ -3049,11 -3193,11 +3203,11 @@@ $wgAllowExternalImages = false
   * You can use this to set up a trusted, simple repository of images.
   * You may also specify an array of strings to allow multiple sites
   *
-  * Examples:
-  * <code>
+  * @par Examples:
+  * @code
   * $wgAllowExternalImagesFrom = 'http://127.0.0.1/';
   * $wgAllowExternalImagesFrom = array( 'http://127.0.0.1/', 'http://example.com' );
-  * </code>
+  * @endcode
   */
  $wgAllowExternalImagesFrom = '';
  
  $wgEnableImageWhitelist = true;
  
  /**
-  * A different approach to the above: simply allow the <img> tag to be used.
+  * A different approach to the above: simply allow the "<img>" tag to be used.
   * This allows you to specify alt text and other attributes, copy-paste HTML to
   * your wiki more easily, etc.  However, allowing external images in any manner
   * will allow anyone with editing rights to snoop on your visitors' IP
@@@ -3110,7 -3254,7 +3264,7 @@@ $wgTidyInternal = extension_loaded( 'ti
   */
  $wgDebugTidy = false;
  
- /** Allow raw, unchecked HTML in <html>...</html> sections.
+ /** Allow raw, unchecked HTML in "<html>...</html>" sections.
   * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions
   * TO RESTRICT EDITING to only those that you trust
   */
@@@ -3378,7 -3522,7 +3532,7 @@@ $wgInvalidUsernameCharacters = '@'
  /**
   * Character used as a delimiter when testing for interwiki userrights
   * (In Special:UserRights, it is possible to modify users on different
-  * databases if the delimiter is used, e.g. Someuser@enwiki).
+  * databases if the delimiter is used, e.g. "Someuser@enwiki").
   *
   * It is recommended that you have this delimiter in
   * $wgInvalidUsernameCharacters above, or you will not be able to
@@@ -3500,18 -3644,19 +3654,19 @@@ $wgBlockCIDRLimit = array
  $wgBlockDisablesLogin = false;
  
  /**
-  * Pages anonymous user may see as an array, e.g.
+  * Pages anonymous user may see, set as an array of pages titles.
   *
-  * <code>
+  * @par Example:
+  * @code
   * $wgWhitelistRead = array ( "Main Page", "Wikipedia:Help");
-  * </code>
+  * @endcode
   *
   * Special:Userlogin and Special:ChangePassword are always whitelisted.
   *
-  * NOTE: This will only work if $wgGroupPermissions['*']['read'] is false --
+  * @note This will only work if $wgGroupPermissions['*']['read'] is false --
   * see below. Otherwise, ALL pages are accessible, regardless of this setting.
   *
-  * Also note that this will only protect _pages in the wiki_. Uploaded files
+  * @note Also that this will only protect _pages in the wiki_. Uploaded files
   * will remain readable. You can use img_auth.php to protect uploaded files,
   * see http://www.mediawiki.org/wiki/Manual:Image_Authorization
   */
@@@ -3525,6 -3670,7 +3680,7 @@@ $wgEmailConfirmToEdit = false
  
  /**
   * Permission keys given to users in each group.
+  *
   * This is an array where the keys are all groups and each value is an
   * array of the format (right => boolean).
   *
@@@ -3615,7 -3761,6 +3771,6 @@@ $wgGroupPermissions['sysop']['reupload'
  $wgGroupPermissions['sysop']['reupload-shared']  = true;
  $wgGroupPermissions['sysop']['unwatchedpages']   = true;
  $wgGroupPermissions['sysop']['autoconfirmed']    = true;
- $wgGroupPermissions['sysop']['upload_by_url']    = true;
  $wgGroupPermissions['sysop']['ipblock-exempt']   = true;
  $wgGroupPermissions['sysop']['blockemail']       = true;
  $wgGroupPermissions['sysop']['markbotedits']     = true;
@@@ -3625,6 -3770,7 +3780,7 @@@ $wgGroupPermissions['sysop']['noratelim
  $wgGroupPermissions['sysop']['movefile']         = true;
  $wgGroupPermissions['sysop']['unblockself']      = true;
  $wgGroupPermissions['sysop']['suppressredirect'] = true;
+ #$wgGroupPermissions['sysop']['upload_by_url']    = true;
  #$wgGroupPermissions['sysop']['mergehistory']     = true;
  
  // Permission to change users' group assignments
@@@ -3635,6 -3781,7 +3791,7 @@@ $wgGroupPermissions['bureaucrat']['nora
  // Permission to export pages including linked pages regardless of $wgExportMaxLinkDepth
  #$wgGroupPermissions['bureaucrat']['override-export-depth'] = true;
  
+ #$wgGroupPermissions['sysop']['deletelogentry']  = true;
  #$wgGroupPermissions['sysop']['deleterevision']  = true;
  // To hide usernames from users and Sysops
  #$wgGroupPermissions['suppress']['hideuser'] = true;
  
  /**
   * Permission keys revoked from users in each group.
+  *
   * This acts the same way as wgGroupPermissions above, except that
   * if the user is in a group here, the permission will be removed from them.
   *
@@@ -3672,16 -3820,20 +3830,20 @@@ $wgImplicitGroups = array( '*', 'user'
   * A map of group names that the user is in, to group names that those users
   * are allowed to add or revoke.
   *
-  * Setting the list of groups to add or revoke to true is equivalent to "any group".
-  *
-  * For example, to allow sysops to add themselves to the "bot" group:
+  * Setting the list of groups to add or revoke to true is equivalent to "any
+  * group".
   *
+  * @par Example:
+  * To allow sysops to add themselves to the "bot" group:
+  * @code
   *    $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) );
+  * @endcode
   *
+  * @par Example:
   * Implicit groups may be used for the source group, for instance:
-  *
+  * @code
   *    $wgGroupsRemoveFromSelf = array( '*' => true );
-  *
+  * @endcode
   * This allows users in the '*' group (i.e. any user) to remove themselves from
   * any group that they happen to be in.
   *
@@@ -3717,13 -3869,16 +3879,16 @@@ $wgRestrictionLevels = array( '', 'auto
   * namespace.  If you list more than one permission, a user must
   * have all of them to edit pages in that namespace.
   *
-  * Note: NS_MEDIAWIKI is implicitly restricted to editinterface.
+  * @note NS_MEDIAWIKI is implicitly restricted to 'editinterface'.
   */
  $wgNamespaceProtection = array();
  
  /**
   * Pages in namespaces in this array can not be used as templates.
-  * Elements must be numeric namespace ids.
+  *
+  * Elements MUST be numeric namespace ids, you can safely use the MediaWiki
+  * namespaces constants (NS_USER, NS_MAIN...).
+  *
   * Among other things, this may be useful to enforce read-restrictions
   * which may otherwise be bypassed by using the template machanism.
   */
@@@ -3739,11 -3894,15 +3904,15 @@@ $wgNonincludableNamespaces = array()
   *
   * When left at 0, all registered accounts will pass.
   *
-  * Example:
-  * <code>
+  * @par Example:
+  * Set automatic confirmation to 10 minutes (which is 600 seconds):
+  * @code
   *  $wgAutoConfirmAge = 600;     // ten minutes
+  * @endcode
+  * Set age to one day:
+  * @code
   *  $wgAutoConfirmAge = 3600*24; // one day
-  * </code>
+  * @endcode
   */
  $wgAutoConfirmAge = 0;
  
   * Number of edits an account requires before it is autoconfirmed.
   * Passing both this AND the time requirement is needed. Example:
   *
-  * <code>
+  * @par Example:
+  * @code
   * $wgAutoConfirmCount = 50;
-  * </code>
+  * @endcode
   */
  $wgAutoConfirmCount = 0;
  
  /**
   * Automatically add a usergroup to any user who matches certain conditions.
+  *
+  * @todo Redocument $wgAutopromote
+  *
   * The format is
   *   array( '&' or '|' or '^' or '!', cond1, cond2, ... )
   * where cond1, cond2, ... are themselves conditions; *OR*
@@@ -3786,14 -3949,19 +3959,19 @@@ $wgAutopromote = array
  
  /**
   * Automatically add a usergroup to any user who matches certain conditions.
+  *
   * Does not add the user to the group again if it has been removed.
   * Also, does not remove the group if the user no longer meets the criteria.
   *
-  * The format is
+  * The format is:
+  * @code
   *    array( event => criteria, ... )
-  * where event is
-  *    'onEdit' (when user edits) or 'onView' (when user views the wiki)
-  * and criteria has the same format as $wgAutopromote
+  * @endcode
+  * Where event is either:
+  *    - 'onEdit' (when user edits)
+  *    - 'onView' (when user views the wiki)
+  *
+  * Criteria has the same format as $wgAutopromote
   *
   * @see $wgAutopromote
   * @since 1.18
@@@ -3811,16 -3979,23 +3989,23 @@@ $wgAutopromoteOnceLogInRC = true
  
  /**
   * $wgAddGroups and $wgRemoveGroups can be used to give finer control over who
-  * can assign which groups at Special:Userrights.  Example configuration:
+  * can assign which groups at Special:Userrights.
   *
+  * @par Example:
+  * Bureaucrats can add any group:
   * @code
-  * // Bureaucrat can add any group
   * $wgAddGroups['bureaucrat'] = true;
-  * // Bureaucrats can only remove bots and sysops
+  * @endcode
+  * Bureaucrats can only remove bots and sysops:
+  * @code
   * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' );
-  * // Sysops can make bots
+  * @endcode
+  * Sysops can make bots:
+  * @code
   * $wgAddGroups['sysop'] = array( 'bot' );
-  * // Sysops can disable other sysops in an emergency, and disable bots
+  * @endcode
+  * Sysops can disable other sysops in an emergency, and disable bots:
+  * @code
   * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' );
   * @endcode
   */
@@@ -3840,8 -4015,10 +4025,10 @@@ $wgAvailableRights = array()
   */
  $wgDeleteRevisionsLimit = 0;
  
- /** Number of accounts each IP address may create, 0 to disable.
-  * Requires memcached */
+ /**
+  * Number of accounts each IP address may create, 0 to disable.
+  *
+  * @warning Requires memcached */
  $wgAccountCreationThrottle = 0;
  
  /**
   * There's no administrator override on-wiki, so be careful what you set. :)
   * May be an array of regexes or a single string for backwards compatibility.
   *
-  * See http://en.wikipedia.org/wiki/Regular_expression
-  * Note that each regex needs a beginning/end delimiter, eg: # or /
+  * @see http://en.wikipedia.org/wiki/Regular_expression
+  *
+  * @note Each regex needs a beginning/end delimiter, eg: # or /
   */
  $wgSpamRegex = array();
  
  $wgSummarySpamRegex = array();
  
  /**
-  * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies
+  * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open
+  * proxies
   * @since 1.16
   */
  $wgEnableDnsBlacklist = false;
  
  /**
-  * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for backward
-  *  compatibility
+  * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for
+  * backward compatibility.
   */
  $wgEnableSorbs = false;
  
  /**
-  * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. This is an
-  * array of either a URL or an array with the URL and a key (should the blacklist
-  * require a key). For example:
+  * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true.
+  *
+  * This is an array of either a URL or an array with the URL and a key (should
+  * the blacklist require a key).
+  *
+  * @par Example:
   * @code
   * $wgDnsBlacklistUrls = array(
   *   // String containing URL
-  *   'http.dnsbl.sorbs.net',
+  *   'http.dnsbl.sorbs.net.',
   *   // Array with URL and key, for services that require a key
-  *   array( 'dnsbl.httpbl.net', 'mykey' ),
+  *   array( 'dnsbl.httpbl.net.', 'mykey' ),
   *   // Array with just the URL. While this works, it is recommended that you
   *   // just use a string as shown above
-  *   array( 'opm.tornevall.org' )
+  *   array( 'opm.tornevall.org.' )
   * );
   * @endcode
+  *
+  * @note You should end the domain name with a . to avoid searching your
+  * eventual domain search suffixes.
   * @since 1.16
   */
  $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' );
  
  /**
-  * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for backward
-  *  compatibility
+  * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for
+  * backward compatibility.
   */
  $wgSorbsUrl = array();
  
  $wgProxyWhitelist = array();
  
  /**
-  * Simple rate limiter options to brake edit floods.  Maximum number actions
-  * allowed in the given number of seconds; after that the violating client re-
-  * ceives HTTP 500 error pages until the period elapses.
+  * Simple rate limiter options to brake edit floods.
+  *
+  * Maximum number actions allowed in the given number of seconds; after that
+  * the violating client receives HTTP 500 error pages until the period
+  * elapses.
+  *
+  * @par Example:
+  * To set a generic maximum of 4 hits in 60 seconds:
+  * @code
+  * $wgRateLimits = array( 4, 60 );
+  * @endcode
   *
-  * array( 4, 60 ) for a maximum of 4 hits in 60 seconds.
+  * You could also limit per action and then type of users. See the inline
+  * code for a template to use.
   *
-  * This option set is experimental and likely to change. Requires memcached.
+  * This option set is experimental and likely to change.
+  *
+  * @warning Requires memcached.
   */
  $wgRateLimits = array(
        'edit' => array(
@@@ -3958,7 -4154,8 +4164,8 @@@ $wgQueryPageDefaultLimit = 50
  
  /**
   * Limit password attempts to X attempts per Y seconds per IP per account.
-  * Requires memcached.
+  *
+  * @warning Requires memcached.
   */
  $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
  
   * If you enable this, every editor's IP address will be scanned for open HTTP
   * proxies.
   *
-  * Don't enable this. Many sysops will report "hostile TCP port scans" to your
-  * ISP and ask for your server to be shut down.
-  *
+  * @warning Don't enable this. Many sysops will report "hostile TCP port scans"
+  * to your ISP and ask for your server to be shut down.
   * You have been warned.
+  *
   */
  $wgBlockOpenProxies = false;
  /** Port we want to scan for a proxy */
@@@ -4110,18 -4307,18 +4317,18 @@@ $wgDebugRedirects = false
  
  /**
   * If true, log debugging data from action=raw and load.php.
-  * This is normally false to avoid overlapping debug entries due to gen=css and
-  * gen=js requests.
+  * This is normally false to avoid overlapping debug entries due to gen=css
+  * and gen=js requests.
   */
  $wgDebugRawPage = false;
  
  /**
   * Send debug data to an HTML comment in the output.
   *
-  * This may occasionally be useful when supporting a non-technical end-user. It's
-  * more secure than exposing the debug log file to the web, since the output only
-  * contains private data for the current user. But it's not ideal for development
-  * use since data is lost on fatal errors and redirects.
+  * This may occasionally be useful when supporting a non-technical end-user.
+  * It's more secure than exposing the debug log file to the web, since the
+  * output only contains private data for the current user. But it's not ideal
+  * for development use since data is lost on fatal errors and redirects.
   */
  $wgDebugComments = false;
  
@@@ -4196,6 -4393,13 +4403,13 @@@ $wgLogExceptionBacktrace = true
   */
  $wgShowHostnames = false;
  
+ /**
+  * Override server hostname detection with a hardcoded value.
+  * Should be a string, default false.
+  * @since 1.20
+  */
+ $wgOverrideHostname = false;
  /**
   * If set to true MediaWiki will throw notices for some possible error
   * conditions and for deprecated functions.
@@@ -4430,12 -4634,13 +4644,13 @@@ $wgMWSuggestTemplate = false
  $wgDisableSearchUpdate = false;
  
  /**
-  * List of namespaces which are searched by default. Example:
+  * List of namespaces which are searched by default.
   *
-  * <code>
+  * @par Example:
+  * @code
   * $wgNamespacesToBeSearchedDefault[NS_MAIN] = true;
   * $wgNamespacesToBeSearchedDefault[NS_PROJECT] = true;
-  * </code>
+  * @endcode
   */
  $wgNamespacesToBeSearchedDefault = array(
        NS_MAIN => true,
  
  /**
   * Namespaces to be searched when user clicks the "Help" tab
-  * on Special:Search
+  * on Special:Search.
   *
-  * Same format as $wgNamespacesToBeSearchedDefault
+  * Same format as $wgNamespacesToBeSearchedDefault.
   */
  $wgNamespacesToBeSearchedHelp = array(
        NS_PROJECT => true,
  );
  
  /**
-  * If set to true the 'searcheverything' preference will be effective only for logged-in users.
-  * Useful for big wikis to maintain different search profiles for anonymous and logged-in users.
+  * If set to true the 'searcheverything' preference will be effective only for
+  * logged-in users.
+  * Useful for big wikis to maintain different search profiles for anonymous and
+  * logged-in users.
   *
   */
  $wgSearchEverythingOnlyLoggedIn = false;
@@@ -4470,18 -4677,22 +4687,22 @@@ $wgDisableInternalSearch = false
   * If the URL includes '$1', this will be replaced with the URL-encoded
   * search term.
   *
-  * For example, to forward to Google you'd have something like:
-  * $wgSearchForwardUrl = 'http://www.google.com/search?q=$1' .
-  *                       '&domains=http://example.com' .
-  *                       '&sitesearch=http://example.com' .
-  *                       '&ie=utf-8&oe=utf-8';
+  * @par Example:
+  * To forward to Google you'd have something like:
+  * @code
+  * $wgSearchForwardUrl =
+  *    'http://www.google.com/search?q=$1' .
+  *    '&domains=http://example.com' .
+  *    '&sitesearch=http://example.com' .
+  *    '&ie=utf-8&oe=utf-8';
+  * @endcode
   */
  $wgSearchForwardUrl = null;
  
  /**
-  * Search form behavior
-  * true = use Go & Search buttons
-  * false = use Go button & Advanced search link
+  * Search form behavior.
+  * true = use Go & Search buttons
+  * false = use Go button & Advanced search link
   */
  $wgUseTwoButtonsSearchForm = true;
  
@@@ -4498,11 -4709,13 +4719,13 @@@ $wgSitemapNamespaces = false
   * maintenance/generateSitemap.php script.
   *
   * This should be a map of namespace IDs to priority
-  * Example:
+  * @par Example:
+  * @code
   *  $wgSitemapNamespacesPriorities = array(
   *      NS_USER => '0.9',
   *      NS_HELP => '0.0',
   *  );
+  * @endcode
   */
  $wgSitemapNamespacesPriorities = false;
  
@@@ -4731,18 -4944,23 +4954,23 @@@ $wgFeedDiffCutoff = 32768
  /** Override the site's default RSS/ATOM feed for recentchanges that appears on
   * every page. Some sites might have a different feed they'd like to promote
   * instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one).
-  * Ex: $wgSiteFeed['format'] = "http://example.com/somefeed.xml"; Format can be one
-  * of either 'rss' or 'atom'.
+  * Should be a format as key (either 'rss' or 'atom') and an URL to the feed
+  * as value.
+  * @par Example:
+  * Configure the 'atom' feed to http://example.com/somefeed.xml
+  * @code
+  * $wgSiteFeed['atom'] = "http://example.com/somefeed.xml";
+  * @endcode
   */
  $wgOverrideSiteFeed = array();
  
  /**
-  * Available feeds objects
+  * Available feeds objects.
   * Should probably only be defined when a page is syndicated ie when
-  * $wgOut->isSyndicated() is true
+  * $wgOut->isSyndicated() is true.
   */
  $wgFeedClasses = array(
-       'rss' => 'RSSFeed',
+       'rss'  => 'RSSFeed',
        'atom' => 'AtomFeed',
  );
  
@@@ -4901,9 -5119,9 +5129,9 @@@ $wgExportAllowListContributors = false
   * can become *insanely large* and could easily break your wiki,
   * it's disabled by default for now.
   *
-  * There's a HARD CODED limit of 5 levels of recursion to prevent a
-  * crazy-big export from being done by someone setting the depth
-  * number too high. In other words, last resort safety net.
+  * @warning There's a HARD CODED limit of 5 levels of recursion to prevent a
+  * crazy-big export from being done by someone setting the depth number too
+  * high. In other words, last resort safety net.
   */
  $wgExportMaxLinkDepth = 0;
  
@@@ -4925,7 -5143,8 +5153,8 @@@ $wgExportAllowAll = false
   */
  
  /**
-  * A list of callback functions which are called once MediaWiki is fully initialised
+  * A list of callback functions which are called once MediaWiki is fully
+  * initialised
   */
  $wgExtensionFunctions = array();
  
   * Variables defined in extensions will override conflicting variables defined
   * in the core.
   *
-  * Example:
+  * @par Example:
+  * @code
   *    $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php';
-  *
+  * @endcode
   */
  $wgExtensionMessagesFiles = array();
  
   * Registration is done with $pout->addOutputHook( $tag, $data ).
   *
   * The callback has the form:
+  * @code
   *    function outputHook( $outputPage, $parserOutput, $data ) { ... }
+  * @endcode
   */
  $wgParserOutputHooks = array();
  
@@@ -4987,7 -5209,7 +5219,7 @@@ $wgAutoloadClasses = array()
   * urls, descriptions and pointers to localized description msgs. Note that
   * the version, url, description and descriptionmsg key can be omitted.
   *
-  * <code>
+  * @code
   * $wgExtensionCredits[$type][] = array(
   *     'name' => 'Example extension',
   *     'version' => 1.9,
   *     'description' => 'An example extension',
   *     'descriptionmsg' => 'exampleextension-desc',
   * );
-  * </code>
+  * @endcode
   *
   * Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'.
   * Where 'descriptionmsg' can be an array with message key and parameters:
@@@ -5013,12 -5235,30 +5245,30 @@@ $wgAuth = null
  
  /**
   * Global list of hooks.
-  * Add a hook by doing:
+  *
+  * The key is one of the events made available by MediaWiki, you can find
+  * a description for most of them in docs/hooks.txt. The array is used
+  * internally by Hook:run().
+  *
+  * The value can be one of:
+  *
+  * - A function name:
+  * @code
   *     $wgHooks['event_name'][] = $function;
-  * or:
+  * @endcode
+  * - A function with some data:
+  * @code
   *     $wgHooks['event_name'][] = array($function, $data);
-  * or:
+  * @endcode
+  * - A an object method:
+  * @code
   *     $wgHooks['event_name'][] = array($object, 'method');
+  * @endcode
+  *
+  * @warning You should always append to an event array or you will end up
+  * deleting a previous registered hook.
+  *
+  * @todo Does it support PHP closures?
   */
  $wgHooks = array();
  
@@@ -5172,17 -5412,19 +5422,19 @@@ $wgLogRestrictions = array
   *
   * See $wgLogTypes for a list of available log types.
   *
-  * For example:
+  * @par Example:
+  * @code
   *   $wgFilterLogTypes => array(
   *      'move' => true,
   *      'import' => false,
   *   );
+  * @endcode
   *
   * Will display show/hide links for the move and import logs. Move logs will be
   * hidden by default unless the link is clicked. Import logs will be shown by
   * default, and hidden when the link is clicked.
   *
-  * A message of the form log-show-hide-<type> should be added, and will be used
+  * A message of the form log-show-hide-[type] should be added, and will be used
   * for the link text.
   */
  $wgFilterLogTypes = array(
   *
   * Extensions with custom log types may add to this array.
   *
-  * Since 1.19, if you follow the naming convention log-name-TYPE,
+  * @since 1.19, if you follow the naming convention log-name-TYPE,
   * where TYPE is your log type, yoy don't need to use this array.
   */
  $wgLogNames = array(
   *
   * Extensions with custom log types may add to this array.
   *
-  * Since 1.19, if you follow the naming convention log-description-TYPE,
+  * @since 1.19, if you follow the naming convention log-description-TYPE,
   * where TYPE is your log type, yoy don't need to use this array.
   */
  $wgLogHeaders = array(
@@@ -5432,7 -5674,7 +5684,7 @@@ $wgMaxRedirectLinksRetrieved = 500
   */
  
  /**
-  * Array of allowed values for the title=foo&action=<action> parameter. Syntax is:
+  * Array of allowed values for the "title=foo&action=<action>" parameter. Syntax is:
   *     'foo' => 'ClassName'    Load the specified class which subclasses Action
   *     'foo' => true           Load the class FooAction which subclasses Action
   *                             If something is specified in the getActionOverrides()
@@@ -5497,8 -5739,10 +5749,10 @@@ $wgDefaultRobotPolicy = 'index,follow'
   * URLs, so search engine spiders risk getting lost in a maze of twisty special
   * pages, all alike, and never reaching your actual content.
   *
-  * Example:
+  * @par Example:
+  * @code
   *   $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' );
+  * @endcode
   */
  $wgNamespaceRobotPolicies = array();
  
   * Robot policies per article. These override the per-namespace robot policies.
   * Must be in the form of an array where the key part is a properly canonical-
   * ised text form title and the value is a robot policy.
-  * Example:
-  *   $wgArticleRobotPolicies = array( 'Main Page' => 'noindex,follow',
-  *     'User:Bob' => 'index,follow' );
-  * Example that DOES NOT WORK because the names are not canonical text forms:
+  *
+  * @par Example:
+  * @code
+  * $wgArticleRobotPolicies = array(
+  *            'Main Page' => 'noindex,follow',
+  *            'User:Bob' => 'index,follow',
+  * );
+  * @endcode
+  *
+  * @par Example that DOES NOT WORK because the names are not canonical text
+  * forms:
+  * @code
   *   $wgArticleRobotPolicies = array(
   *     # Underscore, not space!
   *     'Main_Page' => 'noindex,follow',
   *     # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false for that namespace)!
   *     'abc' => 'noindex,nofollow'
   *   );
+  * @endcode
   */
  $wgArticleRobotPolicies = array();
  
   * An array of namespace keys in which the __INDEX__/__NOINDEX__ magic words
   * will not function, so users can't decide whether pages in that namespace are
   * indexed by search engines.  If set to null, default to $wgContentNamespaces.
-  * Example:
+  *
+  * @par Example:
+  * @code
   *   $wgExemptFromUserRobotsControl = array( NS_MAIN, NS_TALK, NS_PROJECT );
+  * @endcode
   */
  $wgExemptFromUserRobotsControl = null;
  
@@@ -5556,9 -5812,10 +5822,10 @@@ $wgEnableAPI = true
  $wgEnableWriteAPI = true;
  
  /**
-  * API module extensions
+  * API module extensions.
   * Associative array mapping module name to class name.
   * Extension modules may override the core modules.
+  * @todo Describe each of the variables, group them and add examples
   */
  $wgAPIModules = array();
  $wgAPIMetaModules = array();
@@@ -5573,7 -5830,7 +5840,7 @@@ $wgAPIMaxDBRows = 5000
  
  /**
   * The maximum size (in bytes) of an API result.
-  * Don't set this lower than $wgMaxArticleSize*1024
+  * @warning Do not set this lower than $wgMaxArticleSize*1024
   */
  $wgAPIMaxResultSize = 8388608;
  
@@@ -5628,17 -5885,18 +5895,18 @@@ $wgAjaxLicensePreview = true
   * This is currently only used by the API (requests to api.php)
   * $wgCrossSiteAJAXdomains can be set using a wildcard syntax:
   *
-  * '*' matches any number of characters
-  * '?' matches any 1 character
-  *
-  * Example:
-  $wgCrossSiteAJAXdomains = array(
-   'www.mediawiki.org',
-   '*.wikipedia.org',
-   '*.wikimedia.org',
-   '*.wiktionary.org',
-  );
+  * - '*' matches any number of characters
+  * - '?' matches any 1 character
   *
+  * @par Example:
+  * @code
+  * $wgCrossSiteAJAXdomains = array(
+  *    'www.mediawiki.org',
+  *    '*.wikipedia.org',
+  *    '*.wikimedia.org',
+  *    '*.wiktionary.org',
+  * );
+  * @endcode
   */
  $wgCrossSiteAJAXdomains = array();
  
@@@ -5742,7 -6000,7 +6010,7 @@@ $wgUpdateRowsPerQuery = 100
  
  /**
   * The build directory for HipHop compilation.
-  * Defaults to $IP/maintenance/hiphop/build.
+  * Defaults to '$IP/maintenance/hiphop/build'.
   */
  $wgHipHopBuildDirectory = false;
  
@@@ -5762,8 -6020,9 +6030,9 @@@ $wgHipHopCompilerProcs = 'detect'
   *
   * To compile extensions with HipHop, set $wgExtensionsDirectory correctly,
   * and use code like:
-  *
+  * @code
   *    require( MWInit::extensionSetupPath( 'Extension/Extension.php' ) );
+  * @endcode
   *
   * to include the extension setup file from LocalSettings.php. It is not
   * necessary to set this variable unless you use MWInit::extensionSetupPath().
@@@ -5808,9 -6067,11 +6077,11 @@@ $wgExternalDiffEngine = false
  
  /**
   * Disable redirects to special pages and interwiki redirects, which use a 302
-  * and have no "redirected from" link. Note this is only for articles with #Redirect
-  * in them. URL's containing a local interwiki prefix (or a non-canonical special
-  * page name) are still hard redirected regardless of this setting.
+  * and have no "redirected from" link.
+  *
+  * @note This is only for articles with #REDIRECT in them. URL's containing a
+  * local interwiki prefix (or a non-canonical special page name) are still hard
+  * redirected regardless of this setting.
   */
  $wgDisableHardRedirects = false;
  
  $wgLinkHolderBatchSize = 1000;
  
  /**
-  * By default MediaWiki does not register links pointing to same server in externallinks dataset,
-  * use this value to override:
+  * By default MediaWiki does not register links pointing to same server in
+  * externallinks dataset, use this value to override:
   */
  $wgRegisterInternalExternals = false;
  
@@@ -5850,8 -6111,10 +6121,10 @@@ $wgRedirectOnLogin = null
   * This configuration array maps pool types to an associative array. The only
   * defined key in the associative array is "class", which gives the class name.
   * The remaining elements are passed through to the class as constructor
-  * parameters. Example:
+  * parameters.
   *
+  * @par Example:
+  * @code
   *   $wgPoolCounterConf = array( 'ArticleView' => array(
   *     'class' => 'PoolCounter_Client',
   *     'timeout' => 15, // wait timeout in seconds
   *     'maxqueue' => 50, // maximum number of total threads in each pool
   *     ... any extension-specific options...
   *   );
+  * @endcode
   */
  $wgPoolCounterConf = null;
  
@@@ -5876,31 -6140,6 +6150,31 @@@ $wgSeleniumConfigFile = null
  $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
  $wgDBtestpassword = '';
  
 +/**
 + * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
 + * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
 + * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
 + */
 +$wgNamespaceContentModels = array();
 +
 +/**
 + * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
 + *
 + * * 'ignore': return null
 + * * 'fail': throw an MWException
 + * * 'serializeContent': serializeContent to default format
 + */
 +$wgContentHandlerTextFallback = 'ignore';
 +
 +/**
 + * Compatibility switch for running ContentHandler code withoput a schema update.
 + * Set to false to disable use of the database fields introduced by the ContentHandler facility.
 + *
 + * @deprecated this is only here to allow code deployment without a database schema update on large sites.
 + *             get rid of it in the next version.
 + */
 +$wgContentHandlerUseDB = true;
 +
  /**
   * For really cool vim folding this needs to be at the end:
   * vim: foldmarker=@{,@} foldmethod=marker
diff --combined includes/Defines.php
@@@ -25,7 -25,7 +25,7 @@@
   */
  
  /**
-  * @defgroup Constants
+  * @defgroup Constants MediaWiki constants
   */
  
  /**
@@@ -259,7 -259,7 +259,7 @@@ define( 'APCOND_BLOCKED', 8 )
  define( 'APCOND_ISBOT', 9 );
  /**@}*/
  
 -/**
 +/** @{
   * Protocol constants for wfExpandUrl()
   */
  define( 'PROTO_HTTP', 'http://' );
@@@ -268,34 -268,3 +268,34 @@@ define( 'PROTO_RELATIVE', '//' )
  define( 'PROTO_CURRENT', null );
  define( 'PROTO_CANONICAL', 1 );
  define( 'PROTO_INTERNAL', 2 );
 +/**@}*/
 +
 +/**@{
 + * Content model ids, used by Content and ContentHandler.
 + * These IDs will be exposed in the API and XML dumps.
 + *
 + * Extensions that define their own content model IDs should take
 + * care to avoid conflicts. Using the extension name as a prefix is recommended.
 + */
 +define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' );
 +define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
 +define( 'CONTENT_MODEL_CSS', 'css' );
 +define( 'CONTENT_MODEL_TEXT', 'text' );
 +/**@}*/
 +
 +/**@{
 + * Content formats, used by Content and ContentHandler.
 + * These should be MIME types, and will be exposed in the API and XML dumps.
 + *
 + * Extensions are free to use the below formats, or define their own.
 + * It is recommended to stick with the conventions for MIME types.
 + */
 +define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); // wikitext
 +define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); // for js pages
 +define( 'CONTENT_FORMAT_CSS', 'text/css' );  // for css pages
 +define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); // for future use, e.g. with some plain-html messages.
 +define( 'CONTENT_FORMAT_HTML', 'text/html' ); // for future use, e.g. with some plain-html messages.
 +define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); // for future use with the api, and for use by extensions
 +define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions
 +define( 'CONTENT_FORMAT_XML', 'application/xml' ); // for future use with the api, and for use by extensions
 +/**@}*/
diff --combined includes/EditPage.php
@@@ -155,11 -155,6 +155,11 @@@ class EditPage 
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
  
 +      /**
 +       * Status: can't parse content
 +       */
 +      const AS_PARSE_ERROR                = 240;
 +
        /**
         * HTML id and name for the beginning of the edit form.
         */
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
 +      var $content_model = null, $content_format = null;
  
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
        public $editFormTextBottom = '';
        public $editFormTextAfterContent = '';
        public $previewTextAfterContent = '';
 -      public $mPreloadText = '';
 +      public $mPreloadContent = null;
  
        /* $didSave should be set to true whenever an article was succesfully altered. */
        public $didSave = false;
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
 +
 +              $this->content_model = $this->mTitle->getContentModel();
 +
 +              $handler = ContentHandler::getForModelID( $this->content_model );
 +              $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
  
        /**
                        return;
                }
  
 -              $content = $this->getContent();
 +              $content = $this->getContentObject();
  
                # Use the normal message if there's nothing to display
 -              if ( $this->firsttime && $content === '' ) {
 +              if ( $this->firsttime && $content->isEmpty() ) {
                        $action = $this->mTitle->exists() ? 'edit' :
                                ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
                        throw new PermissionsError( $action, $permErrors );
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
 -                      $content = $this->textbox1;
 +                      $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
 +                      $text = $content->serialize( $this->content_format );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
  
 -              $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
 +              $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
  
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
                                wfProfileOut( get_class( $this ) . "::importContentFormData" );
                        }
  
-                       # Truncate for whole multibyte characters. +5 bytes for ellipsis
-                       $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 );
+                       # Truncate for whole multibyte characters
+                       $this->summary = $wgLang->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
                        # 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 = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 250 );
+                       $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
                        $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
  
                        $this->edittime = $request->getVal( 'wpEdittime' );
                } else {
                        # Not a posted form? Start with nothing.
                        wfDebug( __METHOD__ . ": Not a posted form.\n" );
 -                      $this->textbox1     = '';
 +                      $this->textbox1     = ''; #FIXME: track content object
                        $this->summary      = '';
                        $this->sectiontitle = '';
                        $this->edittime     = '';
                        }
                }
  
 +              $this->oldid = $request->getInt( 'oldid' );
 +
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
  
 -              $this->oldid = $request->getInt( 'oldid' );
 +              $content_handler = ContentHandler::getForTitle( $this->mTitle );
 +              $this->content_model = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision
 +              $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
 +
 +              #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
 +              #TODO: check if the desired content model supports the given content format!
  
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
 -              $this->textbox1 = $this->getContent( false );
 +
 +              $content = $this->getContentObject( false ); #TODO: track content object?!
 +              $this->textbox1 = $content->serialize( $this->content_format );
 +
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
                } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
                        # Watch creations
                        $this->watchthis = true;
-               } elseif ( $this->mTitle->userIsWatching() ) {
+               } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
                        # Already watched
                        $this->watchthis = true;
                }
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
 +       * @deprecated since 1.WD
         */
 -      function getContent( $def_text = '' ) {
 -              global $wgOut, $wgRequest, $wgParser;
 +      function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
 +                      $def_content = ContentHandler::makeContent( $def_text, $this->getTitle() );
 +              } else {
 +                      $def_content = false;
 +              }
 +
 +              $content = $this->getContentObject( $def_content );
 +
 +              return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?
 +      }
 +
 +      private function getContentObject( $def_content = null ) { #FIXME: use this!
 +              global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
 -              $text = false;
 +              $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
 -                              $text = $this->mTitle->getDefaultMessageText();
 +                              $msg = $this->mTitle->getDefaultMessageText();
 +
 +                              $content = ContentHandler::makeContent( $msg, $this->mTitle );
                        }
 -                      if ( $text === false ) {
 +                      if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
 -                              $text = $this->getPreloadedText( $preload );
 +
 +                              $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
 -                              $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
 +                              $orig = $this->getOriginalContent();
 +                              $content = $orig ? $orig->getSection( $this->section ) : null;
 +
 +                              if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
  
                                        # Sanity check, make sure it's the right page,
                                        # the revisions exist and they were not deleted.
 -                                      # Otherwise, $text will be left as-is.
 +                                      # Otherwise, $content will be left as-is.
                                        if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
                                                $undorev->getPage() == $oldrev->getPage() &&
                                                $undorev->getPage() == $this->mTitle->getArticleID() &&
                                                !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
                                                !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
  
 -                                              $text = $this->mArticle->getUndoText( $undorev, $oldrev );
 -                                              if ( $text === false ) {
 +                                              $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
 +
 +                                              if ( $content === false ) {
                                                        # Warn the user that something went wrong
                                                        $undoMsg = 'failure';
                                                } else {
                                                wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true );
                                }
  
 -                              if ( $text === false ) {
 -                                      $text = $this->getOriginalContent();
 +                              if ( $content === false ) {
 +                                      $content = $this->getOriginalContent();
                                }
                        }
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $content;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
 -                      return $this->getCurrentText();
 +                      return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
 -                      return '';
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                }
 -              return $this->mArticle->getContent();
 +              $content = $revision->getContent();
 +              return $content;
        }
  
        /**
 -       * Get the actual text of the page. This is basically similar to
 -       * WikiPage::getRawText() except that when the page doesn't exist an empty
 -       * string is returned instead of false.
 +       * Get the current content of the page. This is basically similar to
 +       * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
 +       * content object is returned instead of null.
         *
 -       * @since 1.19
 +       * @since 1.WD
         * @return string
         */
 -      private function getCurrentText() {
 -              $text = $this->mArticle->getRawText();
 -              if ( $text === false ) {
 -                      return '';
 +      private function getCurrentContent() {
 +              $rev = $this->mArticle->getRevision();
 +              $content = $rev ? $rev->getContent( Revision::RAW ) : null;
 +
 +              if ( $content  === false || $content === null ) {
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                } else {
 -                      return $text;
 +                      #FIXME: nasty side-effect!
 +                      $this->content_model = $rev->getContentModel();
 +                      $this->content_format = $rev->getContentFormat();
 +
 +                      return $content;
                }
        }
  
 +
        /**
         * Use this method before edit() to preload some text into the edit box
         *
         * @param $text string
 +       * @deprecated since 1.WD
         */
        public function setPreloadedText( $text ) {
 -              $this->mPreloadText = $text;
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              $this->setPreloadedContent( $content );
 +      }
 +
 +      /**
 +       * Use this method before edit() to preload some content into the edit box
 +       *
 +       * @param $content Content
 +       *
 +       * @since 1.WD
 +       */
 +      public function setPreloadedContent( Content $content ) {
 +              $this->mPreloadedContent = $content;
        }
  
        /**
         * an earlier setPreloadText() or by loading the given page.
         *
         * @param $preload String: representing the title to preload from.
 +       *
         * @return String
 +       *
 +       * @deprecated since 1.WD, use getPreloadedContent() instead
         */
 -      protected function getPreloadedText( $preload ) {
 -              global $wgUser, $wgParser;
 +      protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage!
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = $this->getPreloadedContent( $preload );
 +              $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
  
 -              if ( !empty( $this->mPreloadText ) ) {
 -                      return $this->mPreloadText;
 +              return $text;
 +      }
 +
 +      /**
 +       * Get the contents to be preloaded into the box, either set by
 +       * an earlier setPreloadText() or by loading the given page.
 +       *
 +       * @param $preload String: representing the title to preload from.
 +       *
 +       * @return Content
 +       *
 +       * @since 1.WD
 +       */
 +      protected function getPreloadedContent( $preload ) { #@todo: use this!
 +              global $wgUser;
 +
 +              if ( !empty( $this->mPreloadContent ) ) {
 +                      return $this->mPreloadContent;
                }
  
 +              $handler = ContentHandler::getForTitle( $this->getTitle() );
 +
                if ( $preload === '' ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
                if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $page = WikiPage::factory( $title );
                        $title = $page->getRedirectTarget();
                        # Same as before
                        if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                              return '';
 +                              return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
  
                $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
 +              $content = $page->getContent( Revision::RAW );
 +
 +              return $content->preloadTransform( $title, $parserOptions );
        }
  
        /**
                        case self::AS_HOOK_ERROR:
                                return false;
  
 +                      case self::AS_PARSE_ERROR:
 +                              $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
 +                              #FIXME: cause editform to be shown again, not just an error!
 +                              return false;
 +
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
  
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
 -                      Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
 +                      Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
  
                wfProfileOut( __METHOD__ . '-checks' );
  
-               // Use SELECT FOR UPDATE here to avoid transaction collision in
-               // WikiPage::updateRevisionOn() and ending in the self::AS_END case.
-               $this->mArticle->loadPageData( 'forupdate' );
+               # Load the page data from the master. If anything changes in the meantime,
+               # we detect it by using page_latest like a token in a 1 try compare-and-swap.
+               $this->mArticle->loadPageData( 'fromdbmaster' );
                $new = !$this->mArticle->exists();
  
 -              if ( $new ) {
 -                      // Late check for create permission, just in case *PARANOIA*
 -                      if ( !$this->mTitle->userCan( 'create' ) ) {
 -                              $status->fatal( 'nocreatetext' );
 -                              $status->value = self::AS_NO_CREATE_PERMISSION;
 -                              wfDebug( __METHOD__ . ": no create permission\n" );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +              try {
 +                      if ( $new ) {
 +                              // Late check for create permission, just in case *PARANOIA*
 +                              if ( !$this->mTitle->userCan( 'create' ) ) {
 +                                      $status->fatal( 'nocreatetext' );
 +                                      $status->value = self::AS_NO_CREATE_PERMISSION;
 +                                      wfDebug( __METHOD__ . ": no create permission\n" );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      # Don't save a new article if it's blank.
 -                      if ( $this->textbox1 == '' ) {
 -                              $status->setResult( false, self::AS_BLANK_ARTICLE );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              # Don't save a new article if it's blank.
 +                              if ( $this->textbox1 == '' ) {
 +                                      $status->setResult( false, self::AS_BLANK_ARTICLE );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      $text = $this->textbox1;
 -                      $result['sectionanchor'] = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text;
 -
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 -                                      }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text;
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                              $result['sectionanchor'] = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
 +
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
  
 -                                      // Create a link to the new section from the edit summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +
 +                                              // Create a link to the new section from the edit summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
                                }
 -                      }
  
 -                      $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 +                              $status->value = self::AS_SUCCESS_NEW_ARTICLE;
  
 -              } else {
 +                      } else { # not $new
  
 -                      # Article exists. Check for edit conflict.
 -                      $timestamp = $this->mArticle->getTimestamp();
 -                      wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +                              # Article exists. Check for edit conflict.
  
 -                      if ( $timestamp != $this->edittime ) {
 -                              $this->isConflict = true;
 -                              if ( $this->section == 'new' ) {
 -                                      if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 -                                              $this->mArticle->getComment() == $this->summary ) {
 -                                              // Probably a duplicate submission of a new comment.
 -                                              // This can happen when squid resends a request after
 -                                              // a timeout but the first one actually went through.
 -                                              wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 -                                      } else {
 -                                              // New comment; suppress conflict.
 +                              $this->mArticle->clear(); # Force reload of dates, etc.
 +                              $timestamp = $this->mArticle->getTimestamp();
 +
 +                              wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +
 +                              if ( $timestamp != $this->edittime ) {
 +                                      $this->isConflict = true;
 +                                      if ( $this->section == 'new' ) {
 +                                              if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 +                                                      $this->mArticle->getComment() == $this->summary ) {
 +                                                      // Probably a duplicate submission of a new comment.
 +                                                      // This can happen when squid resends a request after
 +                                                      // a timeout but the first one actually went through.
 +                                                      wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 +                                              } else {
 +                                                      // New comment; suppress conflict.
 +                                                      $this->isConflict = false;
 +                                                      wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
 +                                              }
 +                                      } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 +                                              # Suppress edit conflict with self, except for section edits where merging is required.
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
                                                $this->isConflict = false;
 -                                              wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
                                        }
 -                              } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 -                                      # Suppress edit conflict with self, except for section edits where merging is required.
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
 -                                      $this->isConflict = false;
                                }
 -                      }
 -
 -                      // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 -                      // backwards compatibility with old forms/bots).
 -                      if ( $this->sectiontitle !== '' ) {
 -                              $sectionTitle = $this->sectiontitle;
 -                      } else {
 -                              $sectionTitle = $this->summary;
 -                      }
  
 -                      if ( $this->isConflict ) {
 -                              wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
 -                      } else {
 -                              wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
 -                      }
 -                      if ( is_null( $text ) ) {
 -                              wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 -                              $this->isConflict = true;
 -                              $text = $this->textbox1; // do not try to merge here!
 -                      } elseif ( $this->isConflict ) {
 -                              # Attempt merge
 -                              if ( $this->mergeChangesInto( $text ) ) {
 -                                      // Successful merge! Maybe we should tell the user the good news?
 -                                      $this->isConflict = false;
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                              // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 +                              // backwards compatibility with old forms/bots).
 +                              if ( $this->sectiontitle !== '' ) {
 +                                      $sectionTitle = $this->sectiontitle;
                                } else {
 -                                      $this->section = '';
 -                                      $this->textbox1 = $text;
 -                                      wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      $sectionTitle = $this->summary;
                                }
 -                      }
  
 -                      if ( $this->isConflict ) {
 -                              $status->setResult( false, self::AS_CONFLICT_DETECTED );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +                              $content = null;
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              if ( $this->isConflict ) {
 +                                      wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
 +                              } else {
 +                                      wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
 +                              }
  
 -                      # Handle the user preference to force summaries here, but not for null edits
 -                      if ( $this->section != 'new' && !$this->allowBlankSummary
 -                              && $this->getOriginalContent() != $text
 -                              && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
 -                      {
 -                              if ( md5( $this->summary ) == $this->autoSumm ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' );
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 -                                      wfProfileOut( __METHOD__ );
 -                                      return $status;
 +                              if ( is_null( $content ) ) {
 +                                      wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 +                                      $this->isConflict = true;
 +                                      $content = $textbox_content; // do not try to merge here!
 +                              } elseif ( $this->isConflict ) {
 +                                      # Attempt merge
 +                                      if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
 +                                              // Successful merge! Maybe we should tell the user the good news?
 +                                              $this->isConflict = false;
 +                                              $content = $textbox_content;
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                                      } else {
 +                                              $this->section = '';
 +                                              #$this->textbox1 = $text; #redundant, nothing to do here?
 +                                              wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      }
                                }
 -                      }
  
 -                      # And a similar thing for new sections
 -                      if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 -                              if ( trim( $this->summary ) == '' ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 +                              if ( $this->isConflict ) {
 +                                      $status->setResult( false, self::AS_CONFLICT_DETECTED );
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                      }
  
 -                      # All's well
 -                      wfProfileIn( __METHOD__ . '-sectionanchor' );
 -                      $sectionanchor = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->textbox1 == '' ) {
 -                                      $this->missingComment = true;
 -                                      $status->fatal( 'missingcommenttext' );
 -                                      $status->value = self::AS_TEXTBOX_EMPTY;
 -                                      wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
 +                                              || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +                              # Handle the user preference to force summaries here, but not for null edits
 +                              if ( $this->section != 'new' && !$this->allowBlankSummary
 +                                      && !$content->equals( $this->getOriginalContent() )
 +                                      && !$content->isRedirect() ) # check if it's not a redirect
 +                              {
 +                                      if ( md5( $this->summary ) == $this->autoSumm ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' );
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
                                        }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 -                                      # This is a new section, so create a link to the new section
 -                                      # in the revision summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
                                }
 -                      } elseif ( $this->section != '' ) {
 -                              # Try to get a section anchor from the section source, redirect to edited section if header found
 -                              # XXX: might be better to integrate this into Article::replaceSection
 -                              # for duplicate heading checking and maybe parsing
 -                              $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 -                              # we can't deal with anchors, includes, html etc in the header for now,
 -                              # headline would need to be parsed to improve this
 -                              if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +
 +                              # And a similar thing for new sections
 +                              if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 +                                      if ( trim( $this->summary ) == '' ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
                                }
 -                      }
 -                      $result['sectionanchor'] = $sectionanchor;
 -                      wfProfileOut( __METHOD__ . '-sectionanchor' );
  
 -                      // Save errors may fall down to the edit form, but we've now
 -                      // merged the section into full text. Clear the section field
 -                      // so that later submission of conflict forms won't try to
 -                      // replace that into a duplicated mess.
 -                      $this->textbox1 = $text;
 -                      $this->section = '';
 +                              # All's well
 +                              wfProfileIn( __METHOD__ . '-sectionanchor' );
 +                              $sectionanchor = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->textbox1 == '' ) {
 +                                              $this->missingComment = true;
 +                                              $status->fatal( 'missingcommenttext' );
 +                                              $status->value = self::AS_TEXTBOX_EMPTY;
 +                                              wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                                              # This is a new section, so create a link to the new section
 +                                              # in the revision summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
 +                              } elseif ( $this->section != '' ) {
 +                                      # Try to get a section anchor from the section source, redirect to edited section if header found
 +                                      # XXX: might be better to integrate this into Article::replaceSection
 +                                      # for duplicate heading checking and maybe parsing
 +                                      $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 +                                      # we can't deal with anchors, includes, html etc in the header for now,
 +                                      # headline would need to be parsed to improve this
 +                                      if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +                                      }
 +                              }
 +                              $result['sectionanchor'] = $sectionanchor;
 +                              wfProfileOut( __METHOD__ . '-sectionanchor' );
 +
 +                              // Save errors may fall down to the edit form, but we've now
 +                              // merged the section into full text. Clear the section field
 +                              // so that later submission of conflict forms won't try to
 +                              // replace that into a duplicated mess.
 +                                      $this->textbox1 = $content->serialize( $this->content_format );
 +                              $this->section = '';
  
 -                      $status->value = self::AS_SUCCESS_UPDATE;
 -              }
 +                              $status->value = self::AS_SUCCESS_UPDATE;
 +                      }
  
 -              // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 -              if ( $this->kblength > $wgMaxArticleSize ) {
 -                      $this->tooBig = true;
 -                      $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              }
 +                      // Check for length errors again now that the section is merged in
 +                              $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
 +                      if ( $this->kblength > $wgMaxArticleSize ) {
 +                              $this->tooBig = true;
 +                              $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      }
  
 -              $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 -                      ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 -                      ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 -                      ( $bot ? EDIT_FORCE_BOT : 0 );
 +                      $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 +                              ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 +                              ( ( $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'] = Title::newFromRedirect( $text ) !== null;
 -                      $this->commitWatch();
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              } else {
 -                      // Failure from doEdit()
 -                      // Show the edit conflict page for certain recognized errors from doEdit(),
 -                      // but don't show it for errors from extension hooks
 -                      $errors = $doEditStatus->getErrorsArray();
 -                      if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
 -                              'edit-already-exists' ) ) )
 -                      {
 -                              $this->isConflict = true;
 -                              // Destroys data doEdit() put in $status->value but who cares
 -                              $doEditStatus->value = self::AS_END;
 +                      if ( $doEditStatus->isOK() ) {
 +                                      $result['redirect'] = $content->isRedirect();
 +                              $this->commitWatch();
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      } else {
 +                              // Failure from doEdit()
 +                              // Show the edit conflict page for certain recognized errors from doEdit(),
 +                              // but don't show it for errors from extension hooks
 +                              $errors = $doEditStatus->getErrorsArray();
 +                              if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
 +                                      'edit-already-exists' ) ) )
 +                              {
 +                                      $this->isConflict = true;
 +                                      // Destroys data doEdit() put in $status->value but who cares
 +                                      $doEditStatus->value = self::AS_END;
 +                              }
 +                              wfProfileOut( __METHOD__ );
 +                              return $doEditStatus;
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
                        wfProfileOut( __METHOD__ );
 -                      return $doEditStatus;
 +                      return $status;
                }
        }
  
         */
        protected function commitWatch() {
                global $wgUser;
-               if ( $this->watchthis xor $this->mTitle->userIsWatching() ) {
+               if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle ) ) {
                        $dbw = wfGetDB( DB_MASTER );
                        $dbw->begin( __METHOD__ );
                        if ( $this->watchthis ) {
         * @private
         * @todo document
         *
-        * @parma $editText string
+        * @param $editText string
         *
         * @return bool
 +       * @deprecated since 1.WD, use mergeChangesIntoContent() instead
 +       */
 +      function mergeChangesInto( &$editText ){
 +              wfDebug( __METHOD__, "1.WD" );
 +
 +              $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +              $ok = $this->mergeChangesIntoContent( $editContent );
 +
 +              if ( $ok ) {
 +                      $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?!
 +                      return true;
 +              } else {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * @private
 +       * @todo document
 +       *
 +       * @parma $editText string
 +       *
 +       * @return bool
 +       * @since since 1.WD
         */
 -      function mergeChangesInto( &$editText ) {
 +      private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
  
                $db = wfGetDB( DB_MASTER );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $baseText = $baseRevision->getText();
 +              $baseContent = $baseRevision->getContent();
  
                // The current state, we want to merge updates into it
                $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $currentText = $currentRevision->getText();
 +              $currentContent = $currentRevision->getContent();
 +
 +              $handler = ContentHandler::getForModelID( $baseContent->getModel() );
  
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 +              $result = $handler->merge3( $baseContent, $editContent, $currentContent );
 +
 +              if ( $result ) {
 +                      $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
        /**
         * Check given input text against $wgSpamRegex, and return the text of the first match.
         *
-        * @parma $text string
+        * @param $text string
         *
         * @return string|bool  matching string or false
         */
                        }
                }
  
 +              #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
                        $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
                }
  
+               if ( $this->undidRev ) {
+                       $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
+               }
                if ( $this->hasPresetSummary ) {
                        // If a summary has been preset using &summary= we dont want to prompt for
                        // a different summary. Only prompt for a summary if the summary is blanked.
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
 +              $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
 +              $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
 +
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
 -                      $this->textbox1 = $this->getCurrentText();
 +
 +                      $content = $this->getCurrentContent();
 +                      $this->textbox1 = $content->serialize( $this->content_format );
  
                        $this->showTextbox1();
                } else {
         * @return array An array in the format array( $label, $input )
         */
        function getSummaryInput( $summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null ) {
-               // Note: the maxlength is overriden in JS to 250 and to make it use UTF-8 bytes, not characters.
+               // Note: the maxlength is overriden in JS to 255 and to make it use UTF-8 bytes, not characters.
                $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array(
                        'id' => 'wpSummary',
                        'maxlength' => '200',
                $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
        }
  
 -      protected function showTextbox( $content, $name, $customAttribs = array() ) {
 +      protected function showTextbox( $text, $name, $customAttribs = array() ) {
                global $wgOut, $wgUser;
  
 -              $wikitext = $this->safeUnicodeOutput( $content );
 +              $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
                        // is awkward.
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
 +                              $oldContent = ContentHandler::makeContent( $oldtext, $this->mTitle );
 +                      } else {
 +                              $oldContent = null;
                        }
                } else {
 -                      $oldtext = $this->mArticle->getRawText();
 +                      $oldContent = $this->getOriginalContent();
                }
 -              $newtext = $this->mArticle->replaceSection(
 -                      $this->section, $this->textbox1, $this->summary, $this->edittime );
  
 +              $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                              $this->content_model, $this->content_format ); #XXX: handle parse errors ?
 +
 +              $newContent = $this->mArticle->replaceSectionContent(
 +                                                                                      $this->section, $textboxContent,
 +                                                                                      $this->summary, $this->edittime );
 +
 +              # hanlde legacy text-based hook
 +              $newtext_orig = $newContent->serialize( $this->content_format );
 +              $newtext = $newtext_orig; #clone
                wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
  
 +              if ( $newtext != $newtext_orig ) {
 +                                              #if the hook changed the text, create a new Content object accordingly.
 +                                              $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModel() ); #XXX: handle parse errors ?
 +              }
 +
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
 +
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +              if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) );
                        $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
 +                      $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                        '</div>' );
        }
  
+       /**
+        * Get the copyright warning
+        *
+        * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
+        */
        protected function getCopywarn() {
+               return self::getCopyrightWarning( $this->mTitle );
+       }
+       public static function getCopyrightWarning( $title ) {
                global $wgRightsText;
                if ( $wgRightsText ) {
                        $copywarnMsg = array( 'copyrightwarning',
                                '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
                }
                // Allow for site and per-namespace customization of contribution/copyright notice.
-               wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) );
+               wfRunHooks( 'EditPageCopyrightWarning', array( $title, &$copywarnMsg ) );
  
                return "<div id=\"editpage-copywarn\">\n" .
                        call_user_func_array( "wfMsgNoTrans", $copywarnMsg ) . "\n</div>";
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 +                      $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +                      $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
                        $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
  
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMsg( 'token_suffix_mismatch' );
 -                      } else {
 -                              $note = wfMsg( 'session_fail_preview' );
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMsg( 'edit_form_incomplete' );
 -              } else {
 -                      $note = wfMsg( 'previewnote' ) .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 -              }
 +              $note = '';
  
 -              $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              $parserOptions->setEditSection( false );
 -              $parserOptions->setTidy( true );
 -              $parserOptions->setIsPreview( true );
 -              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 -
 -              # don't parse non-wikitext pages, show message about preview
 -              if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
 -                      if ( $this->mTitle->isCssJsSubpage() ) {
 -                              $level = 'user';
 -                      } elseif ( $this->mTitle->isCssOrJsPage() ) {
 -                              $level = 'site';
 -                      } else {
 -                              $level = false;
 -                      }
 +              try {
 +                      $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
 -                                      $class .= " mw-js";
 +                      if ( $this->mTriedSave && !$this->mTokenOk ) {
 +                              if ( $this->mTokenOkExceptSuffix ) {
 +                                      $note = wfMsg( 'token_suffix_mismatch' );
                                } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
 +                                      $note = wfMsg( 'session_fail_preview' );
                                }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
 +                      } elseif ( $this->incompleteForm ) {
 +                              $note = wfMsg( 'edit_form_incomplete' );
                        } else {
 -                              $previewHTML = '';
 -                      }
 +                              $note = wfMsg( 'previewnote' ) .
 +                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 +                      }
 +
 +                      $parserOptions = ParserOptions::newFromUser( $wgUser );
 +                      $parserOptions->setEditSection( false );
 +                      $parserOptions->setTidy( true );
 +                      $parserOptions->setIsPreview( true );
 +                      $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +
 +                      if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
 +                              # don't parse non-wikitext pages, show message about preview
 +                              if( $this->mTitle->isCssJsSubpage() ) {
 +                                      $level = 'user';
 +                              } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                                      $level = 'site';
 +                              } else {
 +                                      $level = false;
 +                              }
  
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 +                              if ( $content->getModel() == CONTENT_MODEL_CSS ) {
 +                                      $format = 'css';
 +                              } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
 +                                      $format = 'js';
 +                              } else {
 +                                      $format = false;
 +                              }
  
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse;
 +                              # Used messages to make sure grep find them:
 +                              # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                              if( $level && $format ) {
 +                                      $note = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
 +                              } else {
 +                                      $note = wfMsg( 'previewnote' );
 +                              }
 +                      } else {
 +                              $note = wfMsg( 'previewnote' );
                        }
  
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 -
 -                      $parserOptions->enableLimitReport();
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 +                      $rt = $content->getRedirectChain();
  
 -                      $rt = Title::newFromRedirectArray( $this->textbox1 );
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
 -                              $previewHTML = $parserOutput->getText();
 -                      }
  
 -                      $this->mParserOutput = $parserOutput;
 -                      $wgOut->addParserOutputNoText( $parserOutput );
 +                              # If we're adding a comment, we need to show the
 +                              # summary as the headline
 +                              if ( $this->section == "new" && $this->summary != "" ) {
 +                                      $content = $content->addSectionHeader( $this->summary );
 +                              }
 +
 +                              $toparse_orig = $content->serialize( $this->content_format );
 +                              $toparse = $toparse_orig;
 +                              wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 +
 +                              if ( $toparse !== $toparse_orig ) {
 +                                      #hook changed the text, create new Content object
 +                                      $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format );
 +                              }
 +
 +                              wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) );
  
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              $parserOptions->enableLimitReport();
 +
 +                              #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind
 +                              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +
 +                              // TODO: might be a saner way to get a meaningfull context here?
 +                              $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
 +
 +                              $previewHTML = $parserOutput->getText();
 +                              $this->mParserOutput = $parserOutput;
 +                              $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                              if ( count( $parserOutput->getWarnings() ) ) {
 +                                      $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              }
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
         * failure, etc).
         *
         * @todo This doesn't include category or interlanguage links.
-        *       Would need to enhance it a bit, <s>maybe wrap them in XML
-        *       or something...</s> that might also require more skin
+        *       Would need to enhance it a bit, "<s>maybe wrap them in XML
+        *       or something...</s>" that might also require more skin
         *       initialization, so check whether that's a problem.
         */
        function livePreview() {
         * @private
         */
        function checkUnicodeCompliantBrowser() {
-               global $wgBrowserBlackList;
-               if ( empty( $_SERVER["HTTP_USER_AGENT"] ) ) {
+               global $wgBrowserBlackList, $wgRequest;
+               $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
+               if ( $currentbrowser === false ) {
                        // No User-Agent header sent? Trust it by default...
                        return true;
                }
-               $currentbrowser = $_SERVER["HTTP_USER_AGENT"];
                foreach ( $wgBrowserBlackList as $browser ) {
                        if ( preg_match( $browser, $currentbrowser ) ) {
                                return false;
diff --combined includes/Export.php
@@@ -58,6 -58,14 +58,14 @@@ class WikiExporter 
         */
        var $sink;
  
 -              return "0.7";
+       /**
+        * Returns the export schema version.
+        * @return string
+        */
+       public static function schemaVersion() {
++              return "0.7"; #FIXME: bump this when pushing ContentHandler additions.
+       }
        /**
         * If using WikiExporter::STREAM to stream a large amount of data,
         * provide a database connection which is not managed by
  class XmlDumpWriter {
        /**
         * Returns the export schema version.
+        * @deprecated in 1.20; use WikiExporter::schemaVersion() instead
         * @return string
         */
        function schemaVersion() {
-               return "0.8"; #FIXME: Make sure to bump this to > 0.7 when merging Wikidata branch!
+               wfDeprecated( __METHOD__, '1.20' );
+               return WikiExporter::schemaVersion();
        }
  
        /**
-        * Opens the XML output stream's root <mediawiki> element.
+        * Opens the XML output stream's root "<mediawiki>" element.
         * This does not include an xml directive, so is safe to include
         * as a subelement in a larger XML stream. Namespace and XML Schema
         * references are included.
         */
        function openStream() {
                global $wgLanguageCode;
-               $ver = $this->schemaVersion();
+               $ver = WikiExporter::schemaVersion();
                return Xml::element( 'mediawiki', array(
                        'xmlns'              => "http://www.mediawiki.org/xml/export-$ver/",
                        'xmlns:xsi'          => "http://www.w3.org/2001/XMLSchema-instance",
                        'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
 -                                              "http://www.mediawiki.org/xml/export-$ver.xsd",
 +                                              "http://www.mediawiki.org/xml/export-$ver.xsd", #TODO: how do we get a new version up there?
                        'version'            => $ver,
                        'xml:lang'           => $wgLanguageCode ),
                        null ) .
        }
  
        /**
-        * Opens a <page> section on the output stream, with data
+        * Opens a "<page>" section on the output stream, with data
         * from the given database row.
         *
         * @param $row object
        }
  
        /**
-        * Closes a <page> section on the output stream.
+        * Closes a "<page>" section on the output stream.
         *
         * @access private
         * @return string
        }
  
        /**
-        * Dumps a <revision> section on the output stream, with
+        * Dumps a "<revision>" section on the output stream, with
         * data filled in from the given database row.
         *
         * @param $row object
                        $out .= "      " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n";
                }
  
 -              if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
 -                      $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
 -              } else {
 -                      $out .= "      <sha1/>\n";
 -              }
 -
                $text = '';
                if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
                        $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
                                "" ) . "\n";
                }
  
 +              if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
 +                      $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
 +              } else {
 +                      $out .= "      <sha1/>\n";
 +              }
 +
 +              if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model )  ) {
 +                      $content_model = strval( $row->rev_content_model );
 +              } else {
 +                      // probably using $wgContentHandlerUseDB = false;
 +                      // @todo: test!
 +                      $title = Title::makeTitle( $row->page_namespace, $row->page_title );
 +                      $content_model = ContentHandler::getDefaultModelFor( $title );
 +              }
 +
 +              $out .= "      " . Xml::element('model', null, strval( $content_model ) ) . "\n";
 +
 +              if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
 +                      $content_format = strval( $row->rev_content_format );
 +              } else {
 +                      // probably using $wgContentHandlerUseDB = false;
 +                      // @todo: test!
 +                      $content_handler = ContentHandler::getForModelID( $content_model );
 +                      $content_format = $content_handler->getDefaultFormat();
 +              }
 +
 +              $out .= "      " . Xml::element('format', null, strval( $content_format ) ) . "\n";
 +
                wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
  
                $out .= "    </revision>\n";
        }
  
        /**
-        * Dumps a <logitem> section on the output stream, with
+        * Dumps a "<logitem>" section on the output stream, with
         * data filled in from the given database row.
         *
         * @param $row object
  
        /**
         * @param $timestamp string
+        * @param $indent string Default to six spaces
         * @return string
         */
        function writeTimestamp( $timestamp, $indent = "      " ) {
        /**
         * @param $id
         * @param $text string
+        * @param $indent string Default to six spaces
         * @return string
         */
        function writeContributor( $id, $text, $indent = "      " ) {
         * @return string
         */
        function writeUploads( $row, $dumpContents = false ) {
-               if ( $row->page_namespace == NS_IMAGE ) {
+               if ( $row->page_namespace == NS_FILE ) {
                        $img = wfLocalFile( $row->page_title );
                        if ( $img && $img->exists() ) {
                                $out = '';
         * Return prefixed text form of title, but using the content language's
         * canonical namespace. This skips any special-casing such as gendered
         * user namespaces -- which while useful, are not yet listed in the
-        * XML <siteinfo> data so are unsafe in export.
+        * XML "<siteinfo>" data so are unsafe in export.
         *
         * @param Title $title
         * @return string
diff --combined includes/ImagePage.php
@@@ -155,11 -155,9 +155,11 @@@ class ImagePage extends Article 
                        # should be in page content language
                        $pageLang = $this->getTitle()->getPageLanguage();
                        $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
-                               'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
+                               'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
 +
                        parent::view();
 +
                        $out->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
        }
  
        /**
 -       * Overloading Article's getContent method.
 +       * Overloading Article's getContentObject method.
         *
         * Omit noarticletext if sharedupload; text will be fetched from the
         * shared upload server if possible.
         * @return string
         */
 -      public function getContent() {
 +      public function getContentObject() {
                $this->loadFile();
                if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
 -                      return '';
 +                      return null;
                }
 -              return parent::getContent();
 +              return parent::getContentObject();
        }
  
        protected function openShowImage() {
  
                                        if ( $page > 1 ) {
                                                $label = $out->parse( wfMsg( 'imgmultipageprev' ), false );
-                                               $link = Linker::link(
+                                               $link = Linker::linkKnown(
                                                        $this->getTitle(),
                                                        $label,
                                                        array(),
-                                                       array( 'page' => $page - 1 ),
-                                                       array( 'known', 'noclasses' )
+                                                       array( 'page' => $page - 1 )
                                                );
                                                $thumb1 = Linker::makeThumbLinkObj( $this->getTitle(), $this->displayImg, $link, $label, 'none',
                                                        array( 'page' => $page - 1 ) );
  
                                        if ( $page < $count ) {
                                                $label = wfMsg( 'imgmultipagenext' );
-                                               $link = Linker::link(
+                                               $link = Linker::linkKnown(
                                                        $this->getTitle(),
                                                        $label,
                                                        array(),
-                                                       array( 'page' => $page + 1 ),
-                                                       array( 'known', 'noclasses' )
+                                                       array( 'page' => $page + 1 )
                                                );
                                                $thumb2 = Linker::makeThumbLinkObj( $this->getTitle(), $this->displayImg, $link, $label, 'none',
                                                        array( 'page' => $page + 1 ) );
@@@ -646,7 -642,7 +644,7 @@@ EO
  
                # External editing link
                if ( $wgUseExternalEditor ) {
-                       $elink = Linker::link(
+                       $elink = Linker::linkKnown(
                                $this->getTitle(),
                                wfMsgHtml( 'edit-externally' ),
                                array(),
                                        'action' => 'edit',
                                        'externaledit' => 'true',
                                        'mode' => 'file'
-                               ),
-                               array( 'known', 'noclasses' )
+                               )
                        );
                        $out->addHTML(
                                '<li id="mw-imagepage-edit-external">' . $elink . ' <small>' .
                foreach ( $dupes as $file ) {
                        $fromSrc = '';
                        if ( $file->isLocal() ) {
-                               $link = Linker::link(
-                                       $file->getTitle(),
-                                       null,
-                                       array(),
-                                       array(),
-                                       array( 'known', 'noclasses' )
-                               );
+                               $link = Linker::linkKnown( $file->getTitle() );
                        } else {
                                $link = Linker::makeExternalLink( $file->getDescriptionUrl(),
                                        $file->getTitle()->getPrefixedText() );
@@@ -1006,10 -995,10 +997,10 @@@ class ImageHistoryList extends ContextS
                                if ( !$iscur ) {
                                        $q['oldimage'] = $img;
                                }
-                               $row .= Linker::link(
+                               $row .= Linker::linkKnown(
                                        $this->title,
                                        wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
-                                       array(), $q, array( 'known' )
+                                       array(), $q
                                );
                        }
                        # Link to hide content. Don't show useless link to people who cannot hide revisions.
                        if ( $file->isDeleted( File::DELETED_FILE ) ) {
                                $row .= wfMsgHtml( 'filehist-revert' );
                        } else {
-                               $row .= Linker::link(
+                               $row .= Linker::linkKnown(
                                        $this->title,
                                        wfMsgHtml( 'filehist-revert' ),
                                        array(),
                                                'action' => 'revert',
                                                'oldimage' => $img,
                                                'wpEditToken' => $user->getEditToken( $img )
-                                       ),
-                                       array( 'known', 'noclasses' )
+                                       )
                                );
                        }
                }
                                $this->preventClickjacking();
                                $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
                                # Make a link to review the image
-                               $url = Linker::link(
+                               $url = Linker::linkKnown(
                                        $revdel,
                                        $lang->timeanddate( $timestamp, true ),
                                        array(),
                                                'target' => $this->title->getPrefixedText(),
                                                'file' => $img,
                                                'token' => $user->getEditToken( $img )
-                                       ),
-                                       array( 'known', 'noclasses' )
+                                       )
                                );
                        } else {
                                $url = $lang->timeanddate( $timestamp, true );
diff --combined includes/Import.php
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /**
-  * MediaWiki page data importer
+  * MediaWiki page data importer.
   *
   * Copyright © 2003,2005 Brion Vibber <brion@pobox.com>
   * http://www.mediawiki.org/
@@@ -275,7 -275,7 +275,7 @@@ class WikiImporter 
        }
  
        /**
-        * Notify the callback function when a new <page> is reached.
+        * Notify the callback function when a new "<page>" is reached.
         * @param $title Title
         */
        function pageCallback( $title ) {
        }
  
        /**
-        * Notify the callback function when a </page> is closed.
+        * Notify the callback function when a "</page>" is closed.
         * @param $title Title
         * @param $origTitle Title
         * @param $revCount Integer
                $this->debug( "Enter revision handler" );
                $revisionInfo = array();
  
 -              $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'text' );
 +              $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'model', 'format', 'text' );
  
                $skip = false;
  
                if ( isset( $revisionInfo['text'] ) ) {
                        $revision->setText( $revisionInfo['text'] );
                }
 +              if ( isset( $revisionInfo['model'] ) ) {
 +                      $revision->setModel( $revisionInfo['model'] );
 +              }
 +              if ( isset( $revisionInfo['text'] ) ) {
 +                      $revision->setFormat( $revisionInfo['format'] );
 +              }
                $revision->setTitle( $pageInfo['_title'] );
  
                if ( isset( $revisionInfo['timestamp'] ) ) {
@@@ -978,8 -972,6 +978,8 @@@ class WikiRevision 
        var $timestamp = "20010115000000";
        var $user = 0;
        var $user_text = "";
 +      var $model = null;
 +      var $format = null;
        var $text = "";
        var $comment = "";
        var $minor = false;
                $this->user_text = $ip;
        }
  
 +      /**
 +       * @param $model
 +       */
 +      function setModel( $model ) {
 +              $this->model = $model;
 +      }
 +
 +      /**
 +       * @param $format
 +       */
 +      function setFormat( $format ) {
 +              $this->format = $format;
 +      }
 +
        /**
         * @param $text
         */
                return $this->text;
        }
  
 +      /**
 +       * @return String
 +       */
 +      function getModel() {
 +              if ( is_null( $this->model ) ) {
 +                      $this->model = $this->getTitle()->getContentModel();
 +              }
 +
 +              return $this->model;
 +      }
 +
 +      /**
 +       * @return String
 +       */
 +      function getFormat() {
 +              if ( is_null( $this->model ) ) {
 +                      $this->format = ContentHandler::getForTitle( $this->getTitle() )->getDefaultFormat();
 +              }
 +
 +              return $this->format;
 +      }
 +
        /**
         * @return string
         */
                # Insert the row
                $revision = new Revision( array(
                        'page'       => $pageId,
 +                      'content_model'  => $this->getModel(),
 +                      'content_format' => $this->getFormat(),
                        'text'       => $this->getText(),
                        'comment'    => $this->getComment(),
                        'user'       => $userId,
diff --combined includes/LinksUpdate.php
@@@ -39,6 -39,8 +39,6 @@@ class LinksUpdate extends SqlDataUpdat
                $mCategories,    //!< Map of category names to sort keys
                $mInterlangs,    //!< Map of language codes to titles
                $mProperties,    //!< Map of arbitrary name to value
 -              $mDb,            //!< Database connection reference
 -              $mOptions,       //!< SELECT options to be used (array)
                $mRecursive;     //!< Whether to queue jobs for recursive updates
  
        /**
@@@ -69,7 -71,6 +69,7 @@@
                }
  
                $this->mParserOutput = $parserOutput;
 +
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
   **/
  class LinksDeletionUpdate extends SqlDataUpdate {
  
 -      protected $mPage;     //!< WikiPage the wikipage that was deleted
 +      protected $mTitle;     //!< Title the title of page that was deleted
  
        /**
         * Constructor
         *
-        * @param $title Title of the page we're updating
-        * @param $parserOutput ParserOutput: output from a full parse of this page
-        * @param $recursive Boolean: queue jobs for recursive updates?
+        * @param $page WikiPage Page we are updating
         */
 -      function __construct( WikiPage $page ) {
 +      function __construct( Title $title ) {
                parent::__construct( );
  
 -              $this->mPage = $page;
 +              $this->mTitle = $title;
 +
 +              if ( !$title->getArticleID() ) {
 +                      throw new MWException( "The Title object did not provide an article ID. Perhaps the page doesn't exist?" );
 +              }
        }
  
        /**
         * Do some database updates after deletion
         */
        public function doUpdate() {
 -              $title = $this->mPage->getTitle();
 -              $id = $this->mPage->getId();
 +              $title = $this->mTitle;
 +              $id = $title->getArticleID();
  
                # Delete restrictions for it
                $this->mDb->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
                        $cats [] = $row->cl_to;
                }
  
 -              $this->mPage->updateCategoryCounts( array(), $cats );
 +              $this->updateCategoryCounts( array(), $cats );
  
                # If using cascading deletes, we can skip some explicit deletes
                if ( !$this->mDb->cascadingDeletes() ) {
                                __METHOD__ );
                }
        }
 +
 +      /**
 +       * Update all the appropriate counts in the category table.
 +       * @param $added array associative array of category name => sort key
 +       * @param $deleted array associative array of category name => sort key
 +       */
 +      function updateCategoryCounts( $added, $deleted ) {
 +              $a = WikiPage::factory( $this->mTitle );
 +              $a->updateCategoryCounts(
 +                      array_keys( $added ), array_keys( $deleted )
 +              );
 +      }
  }
diff --combined includes/Namespace.php
@@@ -209,14 -209,12 +209,14 @@@ class MWNamespace 
         * Returns array of all defined namespaces with their canonical
         * (English) names.
         *
 +       * @param bool $rebuild rebuild namespace list (default = false). Used for testing.
 +       *
         * @return array
         * @since 1.17
         */
 -      public static function getCanonicalNamespaces() {
 +      public static function getCanonicalNamespaces( $rebuild = false ) {
                static $namespaces = null;
 -              if ( $namespaces === null ) {
 +              if ( $namespaces === null || $rebuild ) {
                        global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
                        $namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames;
                        if ( is_array( $wgExtraNamespaces ) ) {
                        return $wgContentNamespaces;
                }
        }
+       /**
+        * List all namespace indices which are considered subject, aka not a talk
+        * or special namespace. See also MWNamespace::isSubject
+        *
+        * @return array of namespace indices
+        */
+       public static function getSubjectNamespaces() {
+               return array_filter(
+                       MWNamespace::getValidNamespaces(),
+                       'MWNamespace::isSubject'
+               );
+       }
+       /**
+        * List all namespace indices which are considered talks, aka not a subject
+        * or special namespace. See also MWNamespace::isTalk
+        *
+        * @return array of namespace indices
+        */
+       public static function getTalkNamespaces() {
+               return array_filter(
+                       MWNamespace::getValidNamespaces(),
+                       'MWNamespace::isTalk'
+               );
+       }
        /**
         * Is the namespace first-letter capitalized?
         *
diff --combined includes/OutputPage.php
   * @todo document
   */
  class OutputPage extends ContextSource {
-       /// Should be private. Used with addMeta() which adds <meta>
+       /// Should be private. Used with addMeta() which adds "<meta>"
        var $mMetatags = array();
  
-       /// <meta keywords="stuff"> most of the time the first 10 links to an article
+       /// "<meta keywords='stuff'>" most of the time the first 10 links to an article
        var $mKeywords = array();
  
        var $mLinktags = array();
@@@ -50,7 -50,7 +50,7 @@@
        /// Should be private - has getter and setter. Contains the HTML title
        var $mPagetitle = '';
  
-       /// Contains all of the <body> content. Should be private we got set/get accessors and the append() method.
+       /// Contains all of the "<body>" content. Should be private we got set/get accessors and the append() method.
        var $mBodytext = '';
  
        /**
@@@ -60,7 -60,7 +60,7 @@@
         */
        public $mDebugtext = ''; // TODO: we might want to replace it by wfDebug() wfDebugLog()
  
-       /// Should be private. Stores contents of <title> tag
+       /// Should be private. Stores contents of "<title>" tag
        var $mHTMLtitle = '';
  
        /// Should be private. Is the displayed content related to the source of the corresponding wiki article.
        /**
         * Should be private. Used for JavaScript (pre resource loader)
         * We should split js / css.
-        * mScripts content is inserted as is in <head> by Skin. This might contains
-        * either a link to a stylesheet or inline css.
+        * mScripts content is inserted as is in "<head>" by Skin. This might
+        * contains either a link to a stylesheet or inline css.
         */
        var $mScripts = '';
  
         */
        var $mPageLinkTitle = '';
  
-       /// Array of elements in <head>. Parser might add its own headers!
+       /// Array of elements in "<head>". Parser might add its own headers!
        var $mHeadItems = array();
  
        // @todo FIXME: Next variables probably comes from the resource loader
  
        /**
         * Comes from the parser. This was probably made to load CSS/JS only
-        * if we had <gallery>. Used directly in CategoryPage.php
+        * if we had "<gallery>". Used directly in CategoryPage.php
         * Looks like resource loader can replace this.
         */
        var $mNoGallery = false;
        private $mFollowPolicy = 'follow';
        private $mVaryHeader = array(
                'Accept-Encoding' => array( 'list-contains=gzip' ),
-               'Cookie' => null
        );
  
        /**
        }
  
        /**
-        * Add a new <meta> tag
+        * Add a new "<meta>" tag
         * To add an http-equiv meta tag, precede the name with "http:"
         *
         * @param $name String tag name
        /**
         * Add a self-contained script tag with the given contents
         *
-        * @param $script String: JavaScript text, no <script> tags
+        * @param $script String: JavaScript text, no "<script>" tags
         */
        public function addInlineScript( $script ) {
                $this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n";
        }
  
        /**
-        * "HTML title" means the contents of <title>.
+        * "HTML title" means the contents of "<title>".
         * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
         *
         * @param $name string
        }
  
        /**
-        * Return the "HTML title", i.e. the content of the <title> tag.
+        * Return the "HTML title", i.e. the content of the "<title>" tag.
         *
         * @return String
         */
        /**
         * Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
         *
-        * param @t Title
+        * @param $t Title
         */
        public function setRedirectedFrom( $t ) {
                $this->mRedirectedFrom = $t;
         * Set the timestamp of the revision which will be displayed. This is used
         * to avoid a extra DB call in Skin::lastModified().
         *
-        * @param $revid Mixed: string, or null
+        * @param $timestamp Mixed: string, or null
         * @return Mixed: previous value
         */
-       public function setRevisionTimestamp( $timestmap ) {
-               return wfSetVar( $this->mRevisionTimestamp, $timestmap );
+       public function setRevisionTimestamp( $timestamp) {
+               return wfSetVar( $this->mRevisionTimestamp, $timestamp );
        }
  
        /**
                $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] );
        }
  
+       /**
+        * Return a Vary: header on which to vary caches. Based on the keys of $mVaryHeader,
+        * such as Accept-Encoding or Cookie
+        * 
+        * @return String
+        */
+       public function getVaryHeader() {
+               return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) );
+       }
        /**
         * Get a complete X-Vary-Options header
         *
                        $response->header( "ETag: $this->mETag" );
                }
  
+               $this->addVaryHeader( 'Cookie' );
                $this->addAcceptLanguage();
  
                # don't serve compressed data to clients who can't handle it
                # maintain different caches for logged-in users and non-logged in ones
-               $response->header( 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ) );
+               $response->header( $this->getVaryHeader() );
  
                if ( $wgUseXVO ) {
                        # Add an X-Vary-Options header for Squid with Wikimedia patches
                }
  
                $this->sendCacheControl();
 +
 +              wfRunHooks( 'AfterFinalPageOutput', array( &$this ) );
 +
                ob_end_flush();
 +
                wfProfileOut( __METHOD__ );
        }
  
        /**
         * Prepare this object to display an error page; disable caching and
         * indexing, clear the current text and redirect, set the page's title
-        * and optionally an custom HTML title (content of the <title> tag).
+        * and optionally an custom HTML title (content of the "<title>" tag).
         *
         * @param $pageTitle String|Message will be passed directly to setPageTitle()
         * @param $htmlTitle String|Message will be passed directly to setHTMLTitle();
-        *                   optional, if not passed the <title> attribute will be
+        *                   optional, if not passed the "<title>" attribute will be
         *                   based on $pageTitle
         */
        public function prepareErrorPage( $pageTitle, $htmlTitle = false ) {
@@@ -2385,7 -2391,7 +2395,7 @@@ $template
        /**
         * @param $sk Skin The given Skin
         * @param $includeStyle Boolean: unused
-        * @return String: The doctype, opening <html>, and head element.
+        * @return String: The doctype, opening "<html>", and head element.
         */
        public function headElement( Skin $sk, $includeStyle = true ) {
                global $wgContLang;
         * @param $only String ResourceLoaderModule TYPE_ class constant
         * @param $useESI boolean
         * @param $extraQuery Array with extra query parameters to add to each request. array( param => value )
-        * @param $loadCall boolean If true, output an (asynchronous) mw.loader.load() call rather than a <script src="..."> tag
-        * @return string html <script> and <style> tags
+        * @param $loadCall boolean If true, output an (asynchronous) mw.loader.load() call rather than a "<script src='...'>" tag
+        * @return string html "<script>" and "<style>" tags
         */
        protected function makeResourceLoaderLink( $modules, $only, $useESI = false, array $extraQuery = array(), $loadCall = false ) {
                global $wgResourceLoaderUseESI;
  
+               $modules = (array) $modules;
                if ( !count( $modules ) ) {
                        return '';
                }
  
                if ( count( $modules ) > 1 ) {
                        // Remove duplicate module requests
-                       $modules = array_unique( (array) $modules );
+                       $modules = array_unique( $modules );
                        // Sort module names so requests are more uniform
                        sort( $modules );
  
                // Create keyed-by-group list of module objects from modules list
                $groups = array();
                $resourceLoader = $this->getResourceLoader();
-               foreach ( (array) $modules as $name ) {
+               foreach ( $modules as $name ) {
                        $module = $resourceLoader->getModule( $name );
                        # Check that we're allowed to include this module on this page
                        if ( !$module
                }
  
                $links = '';
-               foreach ( $groups as $group => $modules ) {
+               foreach ( $groups as $group => $grpModules ) {
                        // Special handling for user-specific groups
                        $user = null;
                        if ( ( $group === 'user' || $group === 'private' ) && $this->getUser()->isLoggedIn() ) {
                        $context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
                        // Extract modules that know they're empty
                        $emptyModules = array ();
-                       foreach ( $modules as $key => $module ) {
+                       foreach ( $grpModules as $key => $module ) {
                                if ( $module->isKnownEmpty( $context ) ) {
                                        $emptyModules[$key] = 'ready';
-                                       unset( $modules[$key] );
+                                       unset( $grpModules[$key] );
                                }
                        }
                        // Inline empty modules: since they're empty, just mark them as 'ready'
                        }
  
                        // If there are no modules left, skip this group
-                       if ( $modules === array() ) {
+                       if ( count( $grpModules ) === 0 ) {
                                continue;
                        }
  
                        if ( $group === 'private' ) {
                                if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
                                        $links .= Html::inlineStyle(
-                                               $resourceLoader->makeModuleResponse( $context, $modules )
+                                               $resourceLoader->makeModuleResponse( $context, $grpModules )
                                        );
                                } else {
                                        $links .= Html::inlineScript(
                                                ResourceLoader::makeLoaderConditionalScript(
-                                                       $resourceLoader->makeModuleResponse( $context, $modules )
+                                                       $resourceLoader->makeModuleResponse( $context, $grpModules )
                                                )
                                        );
                                }
                        if ( $group === 'user' ) {
                                // Get the maximum timestamp
                                $timestamp = 1;
-                               foreach ( $modules as $module ) {
+                               foreach ( $grpModules as $module ) {
                                        $timestamp = max( $timestamp, $module->getModifiedTime( $context ) );
                                }
                                // Add a version parameter so cache will break when things change
                        }
  
                        $url = ResourceLoader::makeLoaderURL(
-                               array_keys( $modules ),
+                               array_keys( $grpModules ),
                                $this->getLanguage()->getCode(),
                                $this->getSkin()->getSkinName(),
                                $user,
        }
  
        /**
-        * JS stuff to put in the <head>. This is the startup module, config
+        * JS stuff to put in the "<head>". This is the startup module, config
         * vars and modules marked with position 'top'
         *
         * @return String: HTML fragment
        }
  
        /**
-        * JS stuff to put at the 'bottom', which can either be the bottom of the <body>
-        * or the bottom of the <head> depending on $wgResourceLoaderExperimentalAsyncLoading:
+        * JS stuff to put at the 'bottom', which can either be the bottom of the "<body>"
+        * or the bottom of the "<head>" depending on $wgResourceLoaderExperimentalAsyncLoading:
         * modules marked with position 'bottom', legacy scripts ($this->mScripts),
         * user preferences, site JS and user JS
         *
-        * @param $inHead boolean If true, this HTML goes into the <head>, if false it goes into the <body>
+        * @param $inHead boolean If true, this HTML goes into the "<head>", if false it goes into the "<body>"
         * @return string
         */
        function getScriptsForBottomQueue( $inHead ) {
        }
  
        /**
-        * JS stuff to put at the bottom of the <body>
+        * JS stuff to put at the bottom of the "<body>"
         * @return string
         */
        function getBottomScripts() {
        /**
         * Add one or more variables to be set in mw.config in JavaScript.
         *
-        * @param $key {String|Array} Key or array of key/value pars.
+        * @param $keys {String|Array} Key or array of key/value pairs.
         * @param $value {Mixed} [optional] Value of the configuration variable.
         */
        public function addJsConfigVars( $keys, $value = null ) {
                        'wgPageContentLanguage' => $lang->getCode(),
                        'wgSeparatorTransformTable' => $compactSeparatorTransTable,
                        'wgDigitTransformTable' => $compactDigitTransTable,
+                       'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
+                       'wgMonthNames' => $lang->getMonthNamesArray(),
+                       'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
                        'wgRelevantPageName' => $relevantTitle->getPrefixedDBKey(),
                );
                if ( $wgContLang->hasVariants() ) {
        }
  
        /**
-        * @param $addContentType bool: Whether <meta> specifying content type should be returned
+        * @param $addContentType bool: Whether "<meta>" specifying content type should be returned
         *
         * @return array in format "link name or number => 'link html'".
         */
  
        /**
         * @param $unused
-        * @param $addContentType bool: Whether <meta> specifying content type should be returned
+        * @param $addContentType bool: Whether "<meta>" specifying content type should be returned
         *
         * @return string HTML tag links to be put in the header.
         */
        }
  
        /**
-        * Generate a <link rel/> for a feed.
+        * Generate a "<link rel/>" for a feed.
         *
         * @param $type String: feed type
         * @param $url String: URL to the feed
        }
  
        /**
-        * Build a set of <link>s for the stylesheets specified in the $this->styles array.
+        * Build a set of "<link>" elements for the stylesheets specified in the $this->styles array.
         * These will be applied to various media & IE conditionals.
         *
         * @return string
diff --combined includes/Revision.php
@@@ -23,7 -23,7 +23,7 @@@
  /**
   * @todo document
   */
- class Revision {
+ class Revision implements IDBAccessObject {
        protected $mId;
        protected $mPage;
        protected $mUserText;
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
 +      protected $mContentModel;
 +      protected $mContentFormat;
 +      protected $mContent;
 +      protected $mContentHandler;
  
        const DELETED_TEXT = 1;
        const DELETED_COMMENT = 2;
@@@ -51,7 -47,7 +51,7 @@@
        const DELETED_RESTRICTED = 8;
        // Convenience field
        const SUPPRESSED_USER = 12;
-       // Audience options for Revision::getText()
+       // Audience options for accessors
        const FOR_PUBLIC = 1;
        const FOR_THIS_USER = 2;
        const RAW = 3;
         * Load a page revision from a given revision ID number.
         * Returns null if no such revision can be found.
         *
+        * $flags include:
+        *      IDBAccessObject::LATEST_READ  : Select the data from the master
+        *      IDBAccessObject::LOCKING_READ : Select & lock the data from the master
+        *      IDBAccessObject::AVOID_MASTER : Avoid master queries; data may be stale
+        *
         * @param $id Integer
+        * @param $flags Integer (optional)
         * @return Revision or null
         */
-       public static function newFromId( $id ) {
-               return Revision::newFromConds( array( 'rev_id' => intval( $id ) ) );
+       public static function newFromId( $id, $flags = 0 ) {
+               return self::newFromConds( array( 'rev_id' => intval( $id ) ), $flags );
        }
  
        /**
         * that's attached to a given title. If not attached
         * to that title, will return null.
         *
+        * $flags include:
+        *      IDBAccessObject::LATEST_READ  : Select the data from the master
+        *      IDBAccessObject::LOCKING_READ : Select & lock the data from the master
+        *      IDBAccessObject::AVOID_MASTER : Avoid master queries; data may be stale
+        *
         * @param $title Title
         * @param $id Integer (optional)
+        * @param $flags Integer Bitfield (optional)
         * @return Revision or null
         */
-       public static function newFromTitle( $title, $id = 0 ) {
+       public static function newFromTitle( $title, $id = 0, $flags = 0 ) {
                $conds = array(
                        'page_namespace' => $title->getNamespace(),
                        'page_title'     => $title->getDBkey()
@@@ -84,7 -92,7 +96,7 @@@
                if ( $id ) {
                        // Use the specified ID
                        $conds['rev_id'] = $id;
-               } elseif ( wfGetLB()->getServerCount() > 1 ) {
+               } elseif ( !( $flags & self::AVOID_MASTER ) && wfGetLB()->getServerCount() > 1 ) {
                        // Get the latest revision ID from the master
                        $dbw = wfGetDB( DB_MASTER );
                        $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
                        // Use a join to get the latest revision
                        $conds[] = 'rev_id=page_latest';
                }
-               return Revision::newFromConds( $conds );
+               return self::newFromConds( $conds, $flags );
        }
  
        /**
         * that's attached to a given page ID.
         * Returns null if no such revision can be found.
         *
+        * $flags include:
+        *      IDBAccessObject::LATEST_READ  : Select the data from the master
+        *      IDBAccessObject::LOCKING_READ : Select & lock the data from the master
+        *      IDBAccessObject::AVOID_MASTER : Avoid master queries; data may be stale
+        *
         * @param $revId Integer
         * @param $pageId Integer (optional)
+        * @param $flags Integer Bitfield (optional)
         * @return Revision or null
         */
-       public static function newFromPageId( $pageId, $revId = 0 ) {
+       public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
                $conds = array( 'page_id' => $pageId );
                if ( $revId ) {
                        $conds['rev_id'] = $revId;
-               } elseif ( wfGetLB()->getServerCount() > 1 ) {
+               } elseif ( !( $flags & self::AVOID_MASTER ) && wfGetLB()->getServerCount() > 1 ) {
                        // Get the latest revision ID from the master
                        $dbw = wfGetDB( DB_MASTER );
                        $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
                } else {
                        $conds[] = 'rev_id = page_latest';
                }
-               return Revision::newFromConds( $conds );
+               return self::newFromConds( $conds, $flags );
        }
  
        /**
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = array() ) {
 +              global $wgContentHandlerUseDB;
 +
                $attribs = $overrides + array(
                        'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
                        'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
                        'deleted'    => $row->ar_deleted,
                        'len'        => $row->ar_len,
                        'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
 +                      'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
 +                      'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
                );
 +
 +              if ( !$wgContentHandlerUseDB ) {
 +                      unset( $attribs['content_model'] );
 +                      unset( $attribs['content_format'] );
 +              }
 +
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
                        $attribs['text'] = self::getRevisionText( $row, 'ar_' );
         * @return Revision or null
         */
        public static function loadFromId( $db, $id ) {
-               return Revision::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
+               return self::loadFromConds( $db, array( 'rev_id' => intval( $id ) ) );
        }
  
        /**
                } else {
                        $conds[] = 'rev_id=page_latest';
                }
-               return Revision::loadFromConds( $db, $conds );
+               return self::loadFromConds( $db, $conds );
        }
  
        /**
                } else {
                        $matchId = 'page_latest';
                }
-               return Revision::loadFromConds( $db,
+               return self::loadFromConds( $db,
                        array( "rev_id=$matchId",
                                   'page_namespace' => $title->getNamespace(),
                                   'page_title'     => $title->getDBkey() )
         * @return Revision or null
         */
        public static function loadFromTimestamp( $db, $title, $timestamp ) {
-               return Revision::loadFromConds( $db,
+               return self::loadFromConds( $db,
                        array( 'rev_timestamp'  => $db->timestamp( $timestamp ),
                                   'page_namespace' => $title->getNamespace(),
                                   'page_title'     => $title->getDBkey() )
         * Given a set of conditions, fetch a revision.
         *
         * @param $conditions Array
+        * @param $flags integer (optional)
         * @return Revision or null
         */
-       public static function newFromConds( $conditions ) {
-               $db = wfGetDB( DB_SLAVE );
-               $rev = Revision::loadFromConds( $db, $conditions );
-               if( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
-                       $dbw = wfGetDB( DB_MASTER );
-                       $rev = Revision::loadFromConds( $dbw, $conditions );
+       private static function newFromConds( $conditions, $flags = 0 ) {
+               $db = wfGetDB( ( $flags & self::LATEST_READ ) ? DB_MASTER : DB_SLAVE );
+               $rev = self::loadFromConds( $db, $conditions, $flags );
+               if ( is_null( $rev ) && wfGetLB()->getServerCount() > 1 ) {
+                       if ( !( $flags & self::LATEST_READ ) && !( $flags & self::AVOID_MASTER ) ) {
+                               $dbw = wfGetDB( DB_MASTER );
+                               $rev = self::loadFromConds( $dbw, $conditions, $flags );
+                       }
                }
                return $rev;
        }
         *
         * @param $db DatabaseBase
         * @param $conditions Array
+        * @param $flags integer (optional)
         * @return Revision or null
         */
-       private static function loadFromConds( $db, $conditions ) {
-               $res = Revision::fetchFromConds( $db, $conditions );
+       private static function loadFromConds( $db, $conditions, $flags = 0 ) {
+               $res = self::fetchFromConds( $db, $conditions, $flags );
                if( $res ) {
                        $row = $res->fetchObject();
                        if( $row ) {
         * @return ResultWrapper
         */
        public static function fetchRevision( $title ) {
-               return Revision::fetchFromConds(
+               return self::fetchFromConds(
                        wfGetDB( DB_SLAVE ),
                        array( 'rev_id=page_latest',
                                   'page_namespace' => $title->getNamespace(),
         *
         * @param $db DatabaseBase
         * @param $conditions Array
+        * @param $flags integer (optional)
         * @return ResultWrapper
         */
-       private static function fetchFromConds( $db, $conditions ) {
+       private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
                $fields = array_merge(
                        self::selectFields(),
                        self::selectPageFields(),
                        self::selectUserFields()
                );
+               $options = array( 'LIMIT' => 1 );
+               if ( $flags & self::FOR_UPDATE ) {
+                       $options[] = 'FOR UPDATE';
+               }
                return $db->select(
                        array( 'revision', 'page', 'user' ),
                        $fields,
                        $conditions,
                        __METHOD__,
-                       array( 'LIMIT' => 1 ),
+                       $options,
                        array( 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() )
                );
        }
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'rev_id',
                        'rev_page',
                        'rev_text_id',
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
 -                      'rev_sha1'
 +                      'rev_sha1',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_format';
 +                      $fields[] = 'rev_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
                                $this->mTitle = null;
                        }
  
 +                      if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
 +                              $this->mContentModel = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentModel = strval( $row->rev_content_model );
 +                      }
 +
 +                      if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
 +                              $this->mContentFormat = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentFormat = strval( $row->rev_content_format );
 +                      }
 +
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
                        // Build a new revision to be saved...
                        global $wgUser; // ugh
  
 +
 +                      # if we have a content object, use it to set the model and type
 +                      if ( !empty( $row['content'] ) ) {
 +                              if ( !empty( $row['text_id'] ) ) { //@todo: when is that set? test with external store setup! check out insertOn() [dk]
 +                                      throw new MWException( "Text already stored in external store (id {$row['text_id']}), can't serialize content object" );
 +                              }
 +
 +                              $row['content_model'] = $row['content']->getModel();
 +                              # note: mContentFormat is initializes later accordingly
 +                              # note: content is serialized later in this method!
 +                              # also set text to null?
 +                      }
 +
                        $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
                        $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
                        $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
  
 +                      $this->mContentModel = isset( $row['content_model']  )  ? strval( $row['content_model'] )  : null;
 +                      $this->mContentFormat    = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
 +
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
                        $this->mTextRow   = null;
  
 -                      $this->mTitle     = null; # Load on demand if needed
 -                      $this->mCurrent   = false;
 -                      # If we still have no length, see it we have the text to figure it out
 +                      $this->mTitle     = isset( $row['title']      ) ? $row['title'] : null;
 +
 +                      // if we have a Content object, override mText and mContentModel
 +                      if ( !empty( $row['content'] ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $row['content'];
 +
 +                              $this->mContentModel = $this->mContent->getModel();
 +                              $this->mContentHandler = null;
 +
 +                              $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
 +                      } elseif ( !is_null( $this->mText ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $handler->unserializeContent( $this->mText );
 +                      }
 +
 +                      // if we have a Title object, override mPage. Useful for testing and convenience.
 +                      if ( isset( $row['title'] ) ) {
 +                              $this->mTitle     = $row['title'];
 +                              $this->mPage      = $this->mTitle->getArticleID();
 +                      } else {
 +                              $this->mTitle     = null; // Load on demand if needed
 +                      }
 +
 +                      $this->mCurrent   = false; // @todo: XXX: really? we are about to create a revision. it will usually then be the current one.
 +
 +                      // If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
 -                              $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
 +                              if ( !is_null( $this->mContent ) ) {
 +                                      $this->mSize = $this->mContent->getSize();
 +                              } else {
 +                                      #NOTE: this should never happen if we have either text or content object!
 +                                      $this->mSize = null;
 +                              }
                        }
 -                      # Same for sha1
 +
 +                      // Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
 +
 +                      // force lazy init
 +                      $this->getContentModel();
 +                      $this->getContentFormat();
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
                if( isset( $this->mTitle ) ) {
                        return $this->mTitle;
                }
 -              if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL
 +              if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
                        $dbr = wfGetDB( DB_SLAVE );
                        $row = $dbr->selectRow(
                                array( 'page', 'revision' ),
                                $this->mTitle = Title::newFromRow( $row );
                        }
                }
 +
 +              if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
 +                      $this->mTitle = Title::newFromID( $this->mPage );
 +              }
 +
                return $this->mTitle;
        }
  
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
         * @return String
 +       * @deprecated in 1.WD, use getContent() instead
 +       * @todo: replace usage in core
         */
        public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = $this->getContent( $audience, $user );
 +              return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
 +      }
 +
 +      /**
 +       * Fetch revision content if it's available to the specified audience.
 +       * If the specified audience does not have the ability to view this
 +       * revision, null will be returned.
 +       *
 +       * @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
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
                if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
 -                      return '';
 +                      return null;
                } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
 -                      return '';
 +                      return null;
                } else {
 -                      return $this->getRawText();
 +                      return $this->getContentInternal();
                }
        }
  
         * Fetch revision text without regard for view restrictions
         *
         * @return String
 +       *
 +       * @deprecated since 1.WD. Instead, use Revision::getContent( Revision::RAW ) or Revision::getSerializedData() as appropriate.
         */
        public function getRawText() {
 -              if( is_null( $this->mText ) ) {
 -                      // Revision text is immutable. Load on demand:
 -                      $this->mText = $this->loadText();
 -              }
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              return $this->getText( self::RAW );
 +      }
 +
 +      /**
 +       * Fetch original serialized data without regard for view restrictions
 +       *
 +       * @return String
 +       *
 +       * @since 1.WD
 +       */
 +      public function getSerializedData() {
                return $this->mText;
        }
  
 +      protected function getContentInternal() {
 +              if( is_null( $this->mContent ) ) {
 +                      // Revision is immutable. Load on demand:
 +
 +                      $handler = $this->getContentHandler();
 +                      $format = $this->getContentFormat();
 +                      $title = $this->getTitle();
 +
 +                      if( is_null( $this->mText ) ) {
 +                              // Load text on demand:
 +                              $this->mText = $this->loadText();
 +                      }
 +
 +                      $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format );
 +              }
 +
 +              return $this->mContent;
 +      }
 +
 +      /**
 +       * Returns the content model for this revision.
 +       *
 +       * If no content model was stored in the database, $this->getTitle()->getContentModel() is
 +       * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
 +       * is used as a last resort.
 +       *
 +       * @return String the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
 +       **/
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $title = $this->getTitle();
 +                      $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
 +
 +                      assert( !empty( $this->mContentModel ) );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Returns the content format for this revision.
 +       *
 +       * If no content format was stored in the database, the default format for this
 +       * revision's content model is returned.
 +       *
 +       * @return String the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
 +       **/
 +      public function getContentFormat() {
 +              if ( !$this->mContentFormat ) {
 +                      $handler = $this->getContentHandler();
 +                      $this->mContentFormat = $handler->getDefaultFormat();
 +
 +                      assert( !empty( $this->mContentFormat ) );
 +              }
 +
 +              return $this->mContentFormat;
 +      }
 +
 +      /**
 +       * Returns the content handler appropriate for this revision's content model.
 +       *
 +       * @return ContentHandler
 +       */
 +      public function getContentHandler() {
 +              if ( !$this->mContentHandler ) {
 +                      $model = $this->getContentModel();
 +                      $this->mContentHandler = ContentHandler::getForModelID( $model );
 +
 +                      $format = $this->getContentFormat();
 +
 +                      if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
 +                              throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
 +                      }
 +              }
 +
 +              return $this->mContentHandler;
 +      }
 +
        /**
         * @return String
         */
                if( $this->getTitle() ) {
                        $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
                        if( $prev ) {
-                               return Revision::newFromTitle( $this->getTitle(), $prev );
+                               return self::newFromTitle( $this->getTitle(), $prev );
                        }
                }
                return null;
                if( $this->getTitle() ) {
                        $next = $this->getTitle()->getNextRevisionID( $this->getId() );
                        if ( $next ) {
-                               return Revision::newFromTitle( $this->getTitle(), $next );
+                               return self::newFromTitle( $this->getTitle(), $next );
                        }
                }
                return null;
                                $text = gzdeflate( $text );
                                $flags[] = 'gzip';
                        } else {
-                               wfDebug( "Revision::compressRevisionText() -- no zlib support, not compressing\n" );
+                               wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
                        }
                }
                return implode( ',', $flags );
         * @return Integer
         */
        public function insertOn( $dbw ) {
 -              global $wgDefaultExternalStore;
 +              global $wgDefaultExternalStore, $wgContentHandlerUseDB;
  
                wfProfileIn( __METHOD__ );
  
 +              $this->checkContentModel();
 +
                $data = $this->mText;
-               $flags = Revision::compressRevisionText( $data );
+               $flags = self::compressRevisionText( $data );
  
                # Write to external storage if required
                if( $wgDefaultExternalStore ) {
                $rev_id = isset( $this->mId )
                        ? $this->mId
                        : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
 -              $dbw->insert( 'revision',
 -                      array(
 -                              'rev_id'         => $rev_id,
 -                              'rev_page'       => $this->mPage,
 -                              'rev_text_id'    => $this->mTextId,
 -                              'rev_comment'    => $this->mComment,
 -                              'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 -                              'rev_user'       => $this->mUser,
 -                              'rev_user_text'  => $this->mUserText,
 -                              'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 -                              'rev_deleted'    => $this->mDeleted,
 -                              'rev_len'        => $this->mSize,
 -                              'rev_parent_id'  => is_null( $this->mParentId )
 -                                      ? $this->getPreviousRevisionId( $dbw )
 -                                      : $this->mParentId,
 -                              'rev_sha1'       => is_null( $this->mSha1 )
 -                                      ? self::base36Sha1( $this->mText )
 -                                      : $this->mSha1
 -                      ), __METHOD__
 +              $row = array(
 +                      'rev_id'         => $rev_id,
 +                      'rev_page'       => $this->mPage,
 +                      'rev_text_id'    => $this->mTextId,
 +                      'rev_comment'    => $this->mComment,
 +                      'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 +                      'rev_user'       => $this->mUser,
 +                      'rev_user_text'  => $this->mUserText,
 +                      'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 +                      'rev_deleted'    => $this->mDeleted,
 +                      'rev_len'        => $this->mSize,
 +                      'rev_parent_id'  => is_null( $this->mParentId )
 +                              ? $this->getPreviousRevisionId( $dbw )
 +                              : $this->mParentId,
 +                      'rev_sha1'       => is_null( $this->mSha1 )
 +                              ? Revision::base36Sha1( $this->mText )
 +                              : $this->mSha1,
                );
  
 +              if ( $wgContentHandlerUseDB ) {
 +                      //NOTE: Store null for the default model and format, to save space.
 +                      //XXX: Makes the DB sensitive to changed defaults. Make this behaviour optional? Only in miser mode?
 +
 +                      $model = $this->getContentModel();
 +                      $format = $this->getContentFormat();
 +
 +                      $defaultModel = ContentHandler::getDefaultModelFor( $this->getTitle() );
 +                      $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
 +
 +                      $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model;
 +                      $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format;
 +              }
 +
 +              $dbw->insert( 'revision', $row, __METHOD__ );
 +
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
  
                wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
                return $this->mId;
        }
  
 +      protected function checkContentModel() {
 +              global $wgContentHandlerUseDB;
 +
 +              $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
 +
 +              $model = $this->getContentModel();
 +              $format = $this->getContentFormat();
 +              $handler = $this->getContentHandler();
 +
 +              if ( !$handler->isSupportedFormat( $format ) ) {
 +                      $t = $title->getPrefixedDBkey();
 +
 +                      throw new MWException( "Can't use format $format with content model $model on $t" );
 +              }
 +
 +              if ( !$wgContentHandlerUseDB && $title ) {
 +                      // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
 +
 +                      $defaultModel = ContentHandler::getDefaultModelFor( $title );
 +                      $defaultHandler = ContentHandler::getForModelID( $defaultModel );
 +                      $defaultFormat = $defaultHandler->getDefaultFormat();
 +
 +                      if ( $this->getContentModel() != $defaultModel ) {
 +                              $t = $title->getPrefixedDBkey();
 +
 +                              throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: model is $model , default for $t is $defaultModel" );
 +                      }
 +
 +                      if ( $this->getContentFormat() != $defaultFormat ) {
 +                              $t = $title->getPrefixedDBkey();
 +
 +                              throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: format is $format, default for $t is $defaultFormat" );
 +                      }
 +              }
 +
 +              $content = $this->getContent( Revision::RAW );
 +
 +              if ( !$content->isValid() ) {
 +                      $t = $title->getPrefixedDBkey();
 +
 +                      throw new MWException( "Content of $t is not valid! Content model is $model" );
 +              }
 +      }
 +
        /**
         * Get the base 36 SHA-1 value for a string of text
         * @param $text String
         * @return Revision|null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 +              $fields = array( 'page_latest', 'page_namespace', 'page_title',
 +                                              'rev_text_id', 'rev_len', 'rev_sha1' );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_model';
 +                      $fields[] = 'rev_content_format';
 +              }
 +
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
 -                      array( 'page_latest', 'page_namespace', 'page_title',
 -                              'rev_text_id', 'rev_len', 'rev_sha1' ),
 +                      $fields,
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
                        __METHOD__ );
  
                if( $current ) {
 -                      $revision = new Revision( array(
 +                      $row = array(
                                'page'       => $pageId,
                                'comment'    => $summary,
                                'minor_edit' => $minor,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
                                'sha1'       => $current->rev_sha1
 -                              ) );
 +                      );
 +
 +                      if ( $wgContentHandlerUseDB ) {
 +                              $row[ 'content_model' ] = $current->rev_content_model;
 +                              $row[ 'content_format' ] = $current->rev_content_format;
 +                      }
 +
 +                      $revision = new Revision( $row );
                        $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
                } else {
                        $revision = null;
        static function countByTitle( $db, $title ) {
                $id = $title->getArticleID();
                if( $id ) {
-                       return Revision::countByPageId( $db, $id );
+                       return self::countByPageId( $db, $id );
                }
                return 0;
        }
  /**
   * Abstract base class for update jobs that put some secondary data extracted
   * from article content into the database.
+  *
+  * @note: subclasses should NOT start or commit transactions in their doUpdate() method,
+  *        a transaction will automatically be wrapped around the update. Starting another
+  *        one would break the outer transaction bracket. If need be, subclasses can override
+  *        the beginTransaction() and commitTransaction() methods.
   */
  abstract class SqlDataUpdate extends DataUpdate {
  
@@@ -46,6 -51,8 +51,6 @@@
                        $this->mOptions = array( 'FOR UPDATE' );
                }
  
 -              // @todo: get connection only when it's needed? make sure that doesn't break anything, especially transactions!
 -              $this->mDb = wfGetDB( DB_MASTER );
                $this->mHasTransaction = false;
        }
  
@@@ -56,8 -63,6 +61,8 @@@
         * checkes Database::trxLevel() and only opens a transaction if none is yet active.
         */
        public function beginTransaction() {
 +              $this->mDb = wfGetDB( DB_MASTER );
 +
                // NOTE: nested transactions are not supported, only start a transaction if none is open
                if ( $this->mDb->trxLevel() === 0 ) {
                        $this->mDb->begin( get_class( $this ) . '::beginTransaction'  );
@@@ -78,7 -83,7 +83,7 @@@
         * Abort the database transaction started via beginTransaction (if any).
         */
        public function abortTransaction() {
 -              if ( $this->mHasTransaction ) {
 +              if ( $this->mHasTransaction ) { //XXX: actually... maybe always?
                        $this->mDb->rollback( get_class( $this ) . '::abortTransaction' );
                }
        }
diff --combined includes/Title.php
@@@ -65,7 -65,6 +65,7 @@@ class Title 
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
 +      var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
                }
        }
  
 +      /**
 +       * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
 +       * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
 +       *
 +       * @return array
 +       */
 +      protected static function getSelectFields() {
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
 +                      'page_namespace', 'page_title', 'page_id',
 +                      'page_len', 'page_is_redirect', 'page_latest',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
 +      }
 +
        /**
         * Create a new Title from an article ID
         *
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
  
                $res = $dbr->select(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
 +                      if ( isset( $row->page_content_model ) )
 +                              $this->mContentModel = strval( $row->page_content_model );
 +                      else
 +                              $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +                      $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
  
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
 +       * @deprecated since 1.WD, use Content::getRedirectTarget instead.
         */
        public static function newFromRedirect( $text ) {
 -              return self::newFromRedirectInternal( $text );
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Title
 +       * @deprecated since 1.WD, use Content::getUltimateRedirectTarget instead.
         */
        public static function newFromRedirectRecurse( $text ) {
 -              $titles = self::newFromRedirectArray( $text );
 -              return $titles ? array_pop( $titles ) : null;
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getUltimateRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Array of Titles, with the destination last
 +       * @deprecated since 1.WD, use Content::getRedirectChain instead.
         */
        public static function newFromRedirectArray( $text ) {
 -              global $wgMaxRedirects;
 -              $title = self::newFromRedirectInternal( $text );
 -              if ( is_null( $title ) ) {
 -                      return null;
 -              }
 -              // recursive check to follow double redirects
 -              $recurse = $wgMaxRedirects;
 -              $titles = array( $title );
 -              while ( --$recurse > 0 ) {
 -                      if ( $title->isRedirect() ) {
 -                              $page = WikiPage::factory( $title );
 -                              $newtitle = $page->getRedirectTarget();
 -                      } else {
 -                              break;
 -                      }
 -                      // Redirects to some special pages are not permitted
 -                      if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
 -                              // the new title passes the checks, so make that our current title so that further recursion can be checked
 -                              $title = $newtitle;
 -                              $titles[] = $newtitle;
 -                      } else {
 -                              break;
 -                      }
 -              }
 -              return $titles;
 -      }
 -
 -      /**
 -       * Really extract the redirect destination
 -       * Do not call this function directly, use one of the newFromRedirect* functions above
 -       *
 -       * @param $text String Text with possible redirect
 -       * @return Title
 -       */
 -      protected static function newFromRedirectInternal( $text ) {
 -              global $wgMaxRedirects;
 -              if ( $wgMaxRedirects < 1 ) {
 -                      //redirects are disabled, so quit early
 -                      return null;
 -              }
 -              $redir = MagicWord::get( 'redirect' );
 -              $text = trim( $text );
 -              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;
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getRedirectChain();
        }
  
        /**
                return $this->mNamespace;
        }
  
 +      /**
 +       * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
 +       *
 +       * @return String: Content model id
 +       */
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $linkCache = LinkCache::singleton();
 +                      $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
 +              }
 +
 +              if ( !$this->mContentModel ) {
 +                      $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 +              }
 +
 +              if( !$this->mContentModel ) {
 +                      throw new MWException( "failed to determin content model!" );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Convenience method for checking a title's content model name
 +       *
 +       * @param int $id
 +       * @return Boolean true if $this->getContentModel() == $id
 +       */
 +      public function hasContentModel( $id ) {
 +              return $this->getContentModel() == $id;
 +      }
 +
        /**
         * Get the namespace text
         *
         * @return Bool
         */
        public function isWikitextPage() {
 -              $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
 -              wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
 -              return $retval;
 +              return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
  
        /**
 -       * Could this page contain custom CSS or JavaScript, based
 -       * on the title?
 +       * Could this page contain custom CSS or JavaScript for the global UI.
 +       * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
 +       * or CONTENT_MODEL_JAVASCRIPT.
 +       *
 +       * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
 +       *
 +       * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
 -              $retval = $this->mNamespace == NS_MEDIAWIKI
 -                      && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
 -              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
 -              return $retval;
 +              $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
 +                      && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                              || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
 +
 +              #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
 +              #      hook funktions can force this method to return true even outside the mediawiki namespace.
 +
 +              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
 +
 +              return $isCssOrJsPage;
        }
  
        /**
         * @return Bool
         */
        public function isCssJsSubpage() {
 -              return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                              && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                                      || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
  
        /**
         * @return Bool
         */
        public function isCssSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_CSS ) );
        }
  
        /**
         * @return Bool
         */
        public function isJsSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
  
        /**
        /**
         * Is $wgUser watching this page?
         *
+        * @deprecated in 1.20; use User::isWatched() instead.
         * @return Bool
         */
        public function userIsWatching() {
                if ( !$this->getArticleID( $flags ) ) {
                        return $this->mRedirect = false;
                }
 +
                $linkCache = LinkCache::singleton();
 -              $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mRedirect = (bool)$cached;
  
                return $this->mRedirect;
        }
                        return $this->mLength = 0;
                }
                $linkCache = LinkCache::singleton();
 -              $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mLength = intval( $cached );
  
                return $this->mLength;
        }
         * @return Int or 0 if the page doesn't exist
         */
        public function getLatestRevID( $flags = 0 ) {
-               if ( $this->mLatestID !== false ) {
+               if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
                        return intval( $this->mLatestID );
                }
                # Calling getArticleID() loads the field from cache as needed
                        return $this->mLatestID = 0;
                }
                $linkCache = LinkCache::singleton();
 -              $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mLatestID = intval( $cached );
  
                return $this->mLatestID;
        }
         *
         * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
         * loading of the new page_id. It's also called from
-        * WikiPage::doDeleteArticle()
+        * WikiPage::doDeleteArticleReal()
         *
         * @param $newid Int the new Article ID
         */
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
 +              $this->mContentModel = false;
                $this->mEstimateRevisions = null;
        }
  
  
                $res = $db->select(
                        array( 'page', $table ),
 -                      array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
 +                      self::getSelectFields(),
                        array(
                                "{$prefix}_from=page_id",
                                "{$prefix}_namespace" => $this->getNamespace(),
         * @return Array of Title objects linking here
         */
        public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
 +              global $wgContentHandlerUseDB;
 +
                $id = $this->getArticleID();
  
                # If the page doesn't exist; there can't be any link from this page
                $namespaceFiled = "{$prefix}_namespace";
                $titleField = "{$prefix}_title";
  
 +              $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
 +              if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
 +
                $res = $db->select(
                        array( $table, 'page' ),
 -                      array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
 +                      $fields,
                        array( "{$prefix}_from" => $id ),
                        __METHOD__,
                        $options,
         * @return Array of String the URLs
         */
        public function getSquidURLs() {
-               global $wgContLang;
                $urls = array(
                        $this->getInternalURL(),
                        $this->getInternalURL( 'action=history' )
                );
  
-               // purge variant urls as well
-               if ( $wgContLang->hasVariants() ) {
-                       $variants = $wgContLang->getVariants();
+               $pageLang = $this->getPageLanguage();
+               if ( $pageLang->hasVariants() ) {
+                       $variants = $pageLang->getVariants();
                        foreach ( $variants as $vCode ) {
                                $urls[] = $this->getInternalURL( '', $vCode );
                        }
                        $wgUser->spreadAnyEditBlock();
                        return $err;
                }
+               // Check suppressredirect permission
+               if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
+                       $createRedirect = true;
+               }
  
                // If it is a file, move it first.
                // It is done before all other moving stuff is done because it's hard to revert.
         *
         * @param $nt Title the page to move to, which should be a redirect or nonexistent
         * @param $reason String The reason for the move
-        * @param $createRedirect Bool Whether to leave a redirect at the old title.  Ignored
-        *   if the user doesn't have the suppressredirect right
+        * @param $createRedirect Bool Whether to leave a redirect at the old title. Does not check
+        *   if the user has the suppressredirect right
         * @throws MWException
         */
        private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
                        $logType = 'move';
                }
  
-               $redirectSuppressed = !$createRedirect && $wgUser->isAllowed( 'suppressredirect' );
+               $redirectSuppressed = !$createRedirect;
  
                $logEntry = new ManualLogEntry( 'move', $logType );
                $logEntry->setPerformer( $wgUser );
         * @return Bool
         */
        public function isSingleRevRedirect() {
 +              global $wgContentHandlerUseDB;
 +
                $dbw = wfGetDB( DB_MASTER );
 +
                # Is it a redirect?
 +              $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
 +              if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
 +
                $row = $dbw->selectRow( 'page',
 -                      array( 'page_is_redirect', 'page_latest', 'page_id' ),
 +                      $fields,
                        $this->pageCond(),
                        __METHOD__,
                        array( 'FOR UPDATE' )
                $this->mArticleID = $row ? intval( $row->page_id ) : 0;
                $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
                $this->mLatestID = $row ? intval( $row->page_latest ) : false;
 +              $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
                if( !is_object( $rev ) ){
                        return false;
                }
 -              $text = $rev->getText();
 +              $content = $rev->getContent();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
 -              $m = array();
 -              if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
 -                      $redirTitle = Title::newFromText( $m[1] );
 -                      if ( !is_object( $redirTitle ) ||
 -                              ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 -                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
 +              $redirTitle = $content->getRedirectTarget();
 +
 +              if ( $redirTitle ) {
 +                      if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 +                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
                                wfDebug( __METHOD__ . ": redirect points to other page\n" );
                                return false;
 +                      } else {
 +                              return true;
                        }
                } else {
 -                      # Fail safe
 -                      wfDebug( __METHOD__ . ": failsafe\n" );
 +                      # Fail safe (not a redirect after all. strange.)
 +                      wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . " is a redirect, but it doesn't contain a valid redirect.\n" );
                        return false;
                }
 -              return true;
        }
  
        /**
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
 -              } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
 -                      // css/js should always be LTR and is, in fact, English
 -                      return wfGetLangObj( 'en' );
                } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
                        // Parse mediawiki messages with correct target language
                        list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
                        return wfGetLangObj( $lang );
                }
 -              global $wgContLang;
 -              // If nothing special, it should be in the wiki content language
 -              $pageLang = $wgContLang;
 +
 +              //TODO: use the LinkCache to cache this!
 +              //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
 +              $contentHandler = ContentHandler::getForTitle( $this );
 +              $pageLang = $contentHandler->getPageLanguage( $this );
 +
                // Hook at the end because we don't want to override the above stuff
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );
diff --combined includes/WikiPage.php
@@@ -34,30 -34,6 +34,6 @@@ abstract class Page {
   * @internal documentation reviewed 15 Mar 2010
   */
  class WikiPage extends Page {
-       // doDeleteArticleReal() return values. Values less than zero indicate fatal errors,
-       // values greater than zero indicate that there were problems not resulting in page
-       // not being deleted
-       /**
-        * Delete operation aborted by hook
-        */
-       const DELETE_HOOK_ABORTED = -1;
-       /**
-        * Deletion successful
-        */
-       const DELETE_SUCCESS = 0;
-       /**
-        * Page not found
-        */
-       const DELETE_NO_PAGE = 1;
-       /**
-        * No revisions found to delete
-        */
-       const DELETE_NO_REVISIONS = 2;
        // Constants for $mDataLoadedFrom and related
  
        /**
         * @return Array
         */
        public function getActionOverrides() {
 -              return array();
 +              $content_handler = $this->getContentHandler();
 +              return $content_handler->getActionOverrides();
 +      }
 +
 +      /**
 +       * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
 +       *
 +       * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
 +       *
 +       * @return ContentHandler
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContentHandler() {
 +              return ContentHandler::getForModelID( $this->getContentModel() );
        }
  
        /**
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'page_id',
                        'page_namespace',
                        'page_title',
                        'page_latest',
                        'page_len',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
        }
  
        /**
 -       * Tests if the article text represents a redirect
 +       * Tests if the article content represents a redirect
         *
 -       * @param $text mixed string containing article contents, or boolean
         * @return bool
         */
 -      public function isRedirect( $text = false ) {
 -              if ( $text === false ) {
 -                      if ( !$this->mDataLoaded ) {
 -                              $this->loadPageData();
 -                      }
 +      public function isRedirect( ) {
 +              $content = $this->getContent();
 +              if ( !$content ) return false;
  
 -                      return (bool)$this->mIsRedirect;
 -              } else {
 -                      return Title::newFromRedirect( $text ) !== null;
 +              return $content->isRedirect();
 +      }
 +
 +      /**
 +       * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
 +       *
 +       * Will use the revisions actual content model if the page exists,
 +       * and the page's default if the page doesn't exist yet.
 +       *
 +       * @return String
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContentModel() {
 +              if ( $this->exists() ) {
 +                      # look at the revision's actual content model
 +                      $rev = $this->getRevision();
 +
 +                      if ( $rev !== null ) {
 +                              return $rev->getContentModel();
 +                      } else {
 +                              wfWarn( "Page exists but has no revision!" );
 +                      }
                }
 +
 +              # use the default model for this page
 +              return $this->mTitle->getContentModel();
        }
  
        /**
                        return; // page doesn't exist or is missing page_latest info
                }
  
-               $revision = Revision::newFromPageId( $this->getId(), $latest );
+               // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the
+               // latest changes committed. This is true even within REPEATABLE-READ transactions, where
+               // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to
+               // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row
+               // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT.
+               // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read.
+               $flags = ( $this->mDataLoadedFrom == self::DATA_FOR_UPDATE ) ? Revision::LOCKING_READ : 0;
+               $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
                if ( $revision ) { // sanity
                        $this->setLastEdit( $revision );
                }
                return null;
        }
  
 +      /**
 +       * 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
 +       * @return Content|null The content of the current revision
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContent( $audience = Revision::FOR_PUBLIC ) {
 +              $this->loadLastEdit();
 +              if ( $this->mLastRevision ) {
 +                      return $this->mLastRevision->getContent( $audience );
 +              }
 +              return null;
 +      }
 +
        /**
         * Get the text of the current revision. No side-effects...
         *
         *      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
 -       * @return String|bool The text of the current revision. False on failure
 +       * @return String|false The text of the current revision
 +       * @deprecated as of 1.WD, getContent() should be used instead.
         */
 -      public function getText( $audience = Revision::FOR_PUBLIC ) {
 +      public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
                        return $this->mLastRevision->getText( $audience );
         * Get the text of the current revision. No side-effects...
         *
         * @return String|bool The text of the current revision. False on failure
 +       * @deprecated as of 1.WD, getContent() should be used instead.
         */
        public function getRawText() {
 -              $this->loadLastEdit();
 -              if ( $this->mLastRevision ) {
 -                      return $this->mLastRevision->getRawText();
 -              }
 -              return false;
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              return $this->getText( Revision::RAW );
        }
  
        /**
                        return false;
                }
  
 -              $text = $editInfo ? $editInfo->pst : false;
 +              if ( $editInfo ) {
 +                      $content = $editInfo->pstContent;
 +              } else {
 +                      $content = $this->getContent();
 +              }
  
 -              if ( $this->isRedirect( $text ) ) {
 +              if ( !$content || $content->isRedirect( ) ) {
                        return false;
                }
  
 -              switch ( $wgArticleCountMethod ) {
 -              case 'any':
 -                      return true;
 -              case 'comma':
 -                      if ( $text === false ) {
 -                              $text = $this->getRawText();
 -                      }
 -                      return strpos( $text,  ',' ) !== false;
 -              case 'link':
 +              $hasLinks = null;
 +
 +              if ( $wgArticleCountMethod === 'link' ) {
 +                      # nasty special case to avoid re-parsing to detect links
 +
                        if ( $editInfo ) {
                                // ParserOutput::getLinks() is a 2D array of page links, so
                                // to be really correct we would need to recurse in the array
                                // but the main array should only have items in it if there are
                                // links.
 -                              return (bool)count( $editInfo->output->getLinks() );
 +                              $hasLinks = (bool)count( $editInfo->output->getLinks() );
                        } else {
 -                              return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
 +                              $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
                                        array( 'pl_from' => $this->getId() ), __METHOD__ );
                        }
                }
 +
 +              return $content->isCountable( $hasLinks );
        }
  
        /**
         */
        public function insertRedirect() {
                // recurse through to only get the final target
 -              $retval = Title::newFromRedirectRecurse( $this->getRawText() );
 +              $content = $this->getContent();
 +              $retval = $content ? $content->getUltimateRedirectTarget() : null;
                if ( !$retval ) {
                        return null;
                }
                        && $parserOptions->getStubThreshold() == 0
                        && $this->mTitle->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
 -                      && $this->mTitle->isWikitextPage();
 +                      && $this->getContentHandler()->isParserCacheSupported();
        }
  
        /**
         * @param $parserOptions ParserOptions to use for the parse operation
         * @param $oldid Revision ID to get the text from, passing null or 0 will
         *               get the current revision (default value)
 +       *
         * @return ParserOutput or false if the revision was not found
         */
        public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 +                      //@todo: move this logic to MessageCache
 +
                        if ( $this->mTitle->exists() ) {
 -                              $text = $this->getRawText();
 +                              // NOTE: use transclusion text for messages.
 +                              //       This is consistent with  MessageCache::getMsgFromNamespace()
 +
 +                              $content = $this->getContent();
 +                              $text = $content === null ? null : $content->getWikitextForTransclusion();
 +
 +                              if ( $text === null ) $text = false;
                        } else {
                                $text = false;
                        }
         * @private
         */
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 -              $text = $revision->getText();
 -              $len = strlen( $text );
 -              $rt = Title::newFromRedirectRecurse( $text );
 +              $content = $revision->getContent();
 +              $len = $content->getSize();
 +              $rt = $content->getUltimateRedirectTarget();
  
                $conditions = array( 'page_id' => $this->getId() );
  
                }
  
                $now = wfTimestampNow();
 +              $row = array( /* SET */
 +                      'page_latest'      => $revision->getId(),
 +                      'page_touched'     => $dbw->timestamp( $now ),
 +                      'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 +                      'page_is_redirect' => $rt !== null ? 1 : 0,
 +                      'page_len'         => $len,
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'page_content_model' ] = $revision->getContentModel();
 +              }
 +
                $dbw->update( 'page',
 -                      array( /* SET */
 -                              'page_latest'      => $revision->getId(),
 -                              'page_touched'     => $dbw->timestamp( $now ),
 -                              'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 -                              'page_is_redirect' => $rt !== null ? 1 : 0,
 -                              'page_len'         => $len,
 -                      ),
 +                      $row,
                        $conditions,
                        __METHOD__ );
  
-               $result = $dbw->affectedRows() != 0;
+               $result = $dbw->affectedRows() > 0;
                if ( $result ) {
                        $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
                        $this->setLastEdit( $revision );
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        # Update the LinkCache.
 -                      LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest );
 +                      LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest, $revision->getContentModel() );
                }
  
                wfProfileOut( __METHOD__ );
                return $ret;
        }
  
 +    /**
 +     * Get the content that needs to be saved in order to undo all revisions
 +     * between $undo and $undoafter. Revisions must belong to the same page,
 +     * must exist and must not be deleted
 +     * @param $undo Revision
 +     * @param $undoafter Revision Must be an earlier revision than $undo
 +     * @return mixed string on success, false on failure
 +     * @since 1.WD
 +     * Before we had the Content object, this was done in getUndoText
 +     */
 +    public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
 +        $handler = $undo->getContentHandler();
 +        return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
 +    }
 +
        /**
         * Get the text that needs to be saved in order to undo all revisions
         * between $undo and $undoafter. Revisions must belong to the same page,
         * @param $undo Revision
         * @param $undoafter Revision Must be an earlier revision than $undo
         * @return mixed string on success, false on failure
 +       * @deprecated since 1.WD: use ContentHandler::getUndoContent() instead.
         */
        public function getUndoText( Revision $undo, Revision $undoafter = null ) {
 -              $cur_text = $this->getRawText();
 -              if ( $cur_text === false ) {
 -                      return false; // no page
 -              }
 -              $undo_text = $undo->getText();
 -              $undoafter_text = $undoafter->getText();
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -              if ( $cur_text == $undo_text ) {
 -                      # No use doing a merge if it's just a straight revert.
 -                      return $undoafter_text;
 -              }
 +              $this->loadLastEdit();
  
 -              $undone_text = '';
 +              if ( $this->mLastRevision ) {
 +                      if ( is_null( $undoafter ) ) {
 +                              $undoafter = $undo->getPrevious();
 +                      }
  
 -              if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
 -                      return false;
 +                      $handler = $this->getContentHandler();
 +                      $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
 +
 +                      if ( !$undone ) {
 +                              return false;
 +                      } else {
 +                              return ContentHandler::getContentText( $undone );
 +                      }
                }
  
 -              return $undone_text;
 +              return false;
        }
  
        /**
         * @param $text String: new text of the section
         * @param $sectionTitle String: new section's subject, only if $section is 'new'
         * @param $edittime String: revision timestamp or null to use the current revision
 -       * @return string Complete article text, or null if error
 +       * @return String new complete article text, or null if error
 +       *
 +       * @deprecated since 1.WD, use replaceSectionContent() instead
         */
        public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent!
 +                      // Whole-page edit; let the whole text through
 +                      return $text;
 +              }
 +
 +              if ( !$this->supportsSections() ) {
 +                      throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
 +              }
 +
 +              $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); # could even make section title, but that's not required.
 +
 +              $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 +
 +              return ContentHandler::getContentText( $newContent );
 +      }
 +
 +      /**
 +       * Returns true iff this page's content model supports sections.
 +       *
 +       * @return boolean whether sections are supported.
 +       *
 +       * @todo: the skin should check this and not offer section functionality if sections are not supported.
 +       * @todo: the EditPage should check this and not offer section functionality if sections are not supported.
 +       */
 +      public function supportsSections() {
 +              return $this->getContentHandler()->supportsSections();
 +      }
 +
 +      /**
 +       * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
 +       * @param $content Content: new content of the section
 +       * @param $sectionTitle String: new section's subject, only if $section is 'new'
 +       * @param $edittime String: revision timestamp or null to use the current revision
 +       *
 +       * @return Content new complete article content, or null if error
 +       *
 +       * @since 1.WD
 +       */
 +      public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
                wfProfileIn( __METHOD__ );
  
                if ( strval( $section ) == '' ) {
                        // Whole-page edit; let the whole text through
 +                      $newContent = $sectionContent;
                } else {
 +                      if ( !$this->supportsSections() ) {
 +                              throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
 +                      }
 +
                        // Bug 30711: always use current version when adding a new section
                        if ( is_null( $edittime ) || $section == 'new' ) {
 -                              $oldtext = $this->getRawText();
 -                              if ( $oldtext === false ) {
 +                              $oldContent = $this->getContent();
 +                              if ( ! $oldContent ) {
                                        wfDebug( __METHOD__ . ": no page text\n" );
                                        wfProfileOut( __METHOD__ );
                                        return null;
                                        return null;
                                }
  
 -                              $oldtext = $rev->getText();
 +                              $oldContent = $rev->getContent();
                        }
  
 -                      if ( $section == 'new' ) {
 -                              # Inserting a new section
 -                              $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\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 = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $newContent;
        }
  
        /**
         *     revision:                The revision object for the inserted revision, or null
         *
         *  Compatibility note: this function previously returned a boolean value indicating success/failure
 +       *
 +       * @deprecated since 1.WD: use doEditContent() instead.
         */
 -      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
 -              global $wgUser, $wgUseAutomaticEditSummaries;
 +      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #@todo: use doEditContent() instead
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
 +      }
 +
 +      /**
 +       * 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 $summary String: 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 $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
 +       *
 +       * @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.WD
 +       */
 +      public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
 +                                                                 User $user = null, $serialisation_format = null ) {
 +              global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
  
                # Low-level sanity check
                if ( $this->mTitle->getText() === '' ) {
  
                $flags = $this->checkFlags( $flags );
  
 -              if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
 -              {
 -                      wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
 +              # call legacy hook
 +              $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
 +
 +              if ( $hook_ok && Hooks::isRegistered( 'ArticleSave' ) ) { # avoid serialization overhead if the hook isn't present
 +                      $content_text = $content->serialize();
 +                      $txt = $content_text; # clone
 +
 +                      $hook_ok = wfRunHooks( 'ArticleSave', array( &$this, &$user, &$txt, &$summary,
 +                              $flags & EDIT_MINOR, null, null, &$flags, &$status ) ); #TODO: survey extensions using this hook
 +
 +                      if ( $txt !== $content_text ) {
 +                              # if the text changed, unserialize the new version to create an updated Content object.
 +                              $content = $content->getContentHandler()->unserializeContent( $txt );
 +                      }
 +              }
 +
 +              if ( !$hook_ok ) {
 +                      wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
  
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
                $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
                $bot = $flags & EDIT_FORCE_BOT;
  
 -              $oldtext = $this->getRawText(); // current revision
 -              $oldsize = strlen( $oldtext );
 +              $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 == '' ) {
 -                      $summary = self::getAutosummary( $oldtext, $text, $flags );
 +                      if ( !$old_content ) $old_content = null;
 +                      $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
  
 -              $editInfo = $this->prepareTextForEdit( $text, null, $user );
 -              $text = $editInfo->pst;
 -              $newsize = strlen( $text );
 +              $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
 +              $serialized = $editInfo->pst;
 +              $content = $editInfo->pstContent;
 +              $newsize =  $content->getSize();
  
                $dbw = wfGetDB( DB_MASTER );
                $now = wfTimestampNow();
  
                                wfProfileOut( __METHOD__ );
                                return $status;
 -                      } elseif ( $oldtext === false ) {
++                      } 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(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 -                      ) );
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
 +                      ) ); #XXX: pass content object?!
 +
+                       # Bug 37225: use accessor to get the text as Revision may trim it.
+                       # After trimming, the text may be a duplicate of the current text.
 -                      $text = $revision->getText(); // sanity; EditPage should trim already
++                      $content = $revision->getContent(); // sanity; EditPage should trim already
 -                      $changed = ( strcmp( $text, $oldtext ) != 0 );
 +                      $changed = !$content->equals( $old_content );
  
                        if ( $changed ) {
 +                              if ( !$content->isValid() ) {
 +                                      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();
 +
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
 +
                                $revisionId = $revision->insertOn( $dbw );
  
                                # Update page
                        }
  
                        # Update links tables, site stats, etc.
 -                      $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
 -                              'oldcountable' => $oldcountable ) );
 +                      $this->doEditUpdates(
 +                              $revision,
 +                              $user,
 +                              array(
 +                                      'changed' => $changed,
 +                                      'oldcountable' => $oldcountable
 +                              )
 +                      );
  
                        if ( !$changed ) {
                                $status->warning( 'edit-no-change' );
  
                        $dbw->begin( __METHOD__ );
  
 +                      $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                      $status->merge( $prepStatus );
 +
 +                      if ( !$status->isOK() ) {
 +                              $dbw->rollback();
 +
 +                              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 );
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              '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
+                       $text = $revision->getText(); // sanity; EditPage should trim already
                        # Update the page record with revision data
                        $this->updateRevisionOn( $dbw, $revision, 0 );
  
                                        $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
                                # Add RC row to the DB
                                $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
 -                                      '', strlen( $text ), $revisionId, $patrolled );
 +                                      '', $content->getSize(), $revisionId, $patrolled );
  
                                # Log auto-patrolled edits
                                if ( $patrolled ) {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
  
 -                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
 +                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary,
 +                              $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 +
 +                      wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary,
                                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
                }
  
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
  
 -              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
 +              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 +
 +              wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary,
                        $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
  
                # Promote user to any groups they meet the criteria for
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
 -       * @return bool|object
 +       *
 +       * @deprecated in 1.WD: use prepareContentForEdit instead.
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->prepareContentForEdit( $content, $revid , $user );
 +      }
 +
 +      /**
 +       * Prepare content which is about to be saved.
 +       * Returns a stdclass with source, pst and output members
 +       *
 +       * @param \Content $content
 +       * @param null $revid
 +       * @param null|\User $user
 +       * @param null $serialization_format
 +       *
 +       * @return bool|object
 +       *
 +       * @since 1.WD
 +       */
 +      public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) {
                global $wgParser, $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
                // @TODO fixme: check $user->getId() here???
 +
                if ( $this->mPreparedEdit
 -                      && $this->mPreparedEdit->newText == $text
 +                      && $this->mPreparedEdit->newContent
 +                      && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
 +                      && $this->mPreparedEdit->format == $serialization_format
 +                      #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
  
                $edit = (object)array();
                $edit->revid = $revid;
 -              $edit->newText = $text;
 -              $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
 +
 +              $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
 +              $edit->pst = $edit->pstContent->serialize( $serialization_format ); #XXX: do we need this??
 +              $edit->format = $serialization_format;
 +
                $edit->popts = $this->makeParserOptions( 'canonical' );
 -              $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
 -              $edit->oldText = $this->getRawText();
 +
 +              $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts );
 +
 +              $edit->newContent = $content;
 +              $edit->oldContent = $this->getContent( Revision::RAW );
 +
 +              #NOTE: B/C for hooks! don't use these fields!
 +              $edit->newText = ContentHandler::getContentText( $edit->newContent );
 +              $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
  
                $this->mPreparedEdit = $edit;
  
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
 -       * @private
         * @param $revision Revision object
         * @param $user User object that did the revision
         * @param $options Array of options, following indexes are used:
                wfProfileIn( __METHOD__ );
  
                $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
 -              $text = $revision->getText();
 +              $content = $revision->getContent();
  
                # Parse the text
                # Be careful not to double-PST: $text is usually already PST-ed once
                if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
                        wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
 -                      $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
 +                      $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
                } else {
                        wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
                }
  
                # Update the links tables and other secondary data
 -              $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
 +              $contentHandler = $revision->getContentHandler();
 +              $updates = $contentHandler->getSecondaryDataUpdates( $content, $this->getTitle(), null, true, $editInfo->output );
                DataUpdate::runUpdates( $updates );
  
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
                }
  
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
 -              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
 +              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) ); #TODO: let the search engine decide what to do with the content object
  
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 -                      MessageCache::singleton()->replace( $shortTitle, $text );
 +                      $msgtext = $content->getWikitextForTransclusion(); #XXX: could skip pseudo-messages like js/css here, based on content model.
 +                      if ( $msgtext === false || $msgtext === null ) $msgtext = '';
 +
 +                      MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
  
                if( $options['created'] ) {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
 +       *
 +       * @deprecated since 1.WD, use doEditContent() instead.
         */
        public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->doQuickEditContent( $content, $user, $comment , $minor );
 +      }
 +
 +      /**
 +       * Edit an article without doing all that other stuff
 +       * The article must already exist; link tables etc
 +       * are not updated, caches are not flushed.
 +       *
 +       * @param $content Content: content submitted
 +       * @param $user User The relevant user
 +       * @param $comment String: comment submitted
 +       * @param $serialisation_format String: format for storing the content in the database
 +       * @param $minor Boolean: whereas it's a minor modification
 +       */
 +      public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) {
                wfProfileIn( __METHOD__ );
  
 +              $serialized = $content->serialize( $serialisation_format );
 +
                $dbw = wfGetDB( DB_MASTER );
                $revision = new Revision( array(
                        'page'       => $this->getId(),
 -                      'text'       => $text,
 +                      'text'       => $serialized,
 +                      'length'     => $content->getSize(),
                        'comment'    => $comment,
                        'minor_edit' => $minor ? 1 : 0,
 -              ) );
 +              ) ); #XXX: set the content object?
                $revision->insertOn( $dbw );
                $this->updateRevisionOn( $dbw, $revision );
  
        }
  
        /**
-        * Same as doDeleteArticleReal(), but returns more detailed success/failure status
+        * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
+        * backwards compatibility, if you care about error reporting you should use
+        * doDeleteArticleReal() instead.
+        *
         * Deletes the article with database consistency, writes logs, purges caches
         *
         * @param $reason string delete reason for deletion log
        public function doDeleteArticle(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
-               return $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user )
-                       == WikiPage::DELETE_SUCCESS;
+               $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user );
+               return $status->isGood();
        }
  
        /**
         * @param $commit boolean defaults to true, triggers transaction end
         * @param &$error Array of errors to append to
         * @param $user User The deleting user
-        * @return int: One of WikiPage::DELETE_* constants
+        * @return Status: Status object; if successful, $status->value is the log_id of the
+        *                 deletion log entry. If the page couldn't be deleted because it wasn't
+        *                 found, $status is a non-fatal 'cannotdelete' error
         */
        public function doDeleteArticleReal(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
 -              global $wgUser;
 +              global $wgUser, $wgContentHandlerUseDB;
  
                wfDebug( __METHOD__ . "\n" );
  
+               $status = Status::newGood();
                if ( $this->mTitle->getDBkey() === '' ) {
-                       return WikiPage::DELETE_NO_PAGE;
+                       $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+                       return $status;
                }
  
                $user = is_null( $user ) ? $wgUser : $user;
-               if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error ) ) ) {
-                       return WikiPage::DELETE_HOOK_ABORTED;
+               if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) {
+                       if ( $status->isOK() ) {
+                               // Hook aborted but didn't set a fatal status
+                               $status->fatal( 'delete-hook-aborted' );
+                       }
+                       return $status;
                }
  
                if ( $id == 0 ) {
                        $this->loadPageData( 'forupdate' );
                        $id = $this->getID();
                        if ( $id == 0 ) {
-                               return WikiPage::DELETE_NO_PAGE;
+                               $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+                               return $status;
                        }
                }
  
                        $bitfield = 'rev_deleted';
                }
  
 +              // we need to remember the old content so we can use it to generate all deletion updates.
 +              $content = $this->getContent( Revision::RAW );
 +
                $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
                // For now, shunt the revision data into the archive table.
                //
                // In the future, we may keep revisions and mark them with
                // the rev_deleted field, which is reserved for this purpose.
 +
 +              $row = array(
 +                      'ar_namespace'  => 'page_namespace',
 +                      'ar_title'      => 'page_title',
 +                      'ar_comment'    => 'rev_comment',
 +                      'ar_user'       => 'rev_user',
 +                      'ar_user_text'  => 'rev_user_text',
 +                      'ar_timestamp'  => 'rev_timestamp',
 +                      'ar_minor_edit' => 'rev_minor_edit',
 +                      'ar_rev_id'     => 'rev_id',
 +                      'ar_parent_id'  => 'rev_parent_id',
 +                      'ar_text_id'    => 'rev_text_id',
 +                      'ar_text'       => '\'\'', // Be explicit to appease
 +                      'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 +                      'ar_len'        => 'rev_len',
 +                      'ar_page_id'    => 'page_id',
 +                      'ar_deleted'    => $bitfield,
 +                      'ar_sha1'       => 'rev_sha1',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'ar_content_model' ] = 'rev_content_model';
 +                      $row[ 'ar_content_format' ] = 'rev_content_format';
 +              }
 +
                $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
 +                      $row,
                        array(
 -                              'ar_namespace'  => 'page_namespace',
 -                              'ar_title'      => 'page_title',
 -                              'ar_comment'    => 'rev_comment',
 -                              'ar_user'       => 'rev_user',
 -                              'ar_user_text'  => 'rev_user_text',
 -                              'ar_timestamp'  => 'rev_timestamp',
 -                              'ar_minor_edit' => 'rev_minor_edit',
 -                              'ar_rev_id'     => 'rev_id',
 -                              'ar_parent_id'  => 'rev_parent_id',
 -                              'ar_text_id'    => 'rev_text_id',
 -                              'ar_text'       => '\'\'', // Be explicit to appease
 -                              'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 -                              'ar_len'        => 'rev_len',
 -                              'ar_page_id'    => 'page_id',
 -                              'ar_deleted'    => $bitfield,
 -                              'ar_sha1'       => 'rev_sha1'
 -                      ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
                        ), __METHOD__
  
                if ( !$ok ) {
                        $dbw->rollback( __METHOD__ );
-                       return WikiPage::DELETE_NO_REVISIONS;
+                       $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+                       return $status;
                }
  
 -              $this->doDeleteUpdates( $id );
 +              $this->doDeleteUpdates( $id, $content );
  
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
                }
  
                wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) );
-               return WikiPage::DELETE_SUCCESS;
+               $status->value = $logid;
+               return $status;
        }
  
        /**
         * Do some database updates after deletion
         *
         * @param $id Int: page_id value of the page being deleted (B/C, currently unused)
 +       * @param $content Content: optional page content to be used when determining the required updates.
 +       *        This may be needed because $this->getContent() may already return null when the page proper was deleted.
         */
 -      public function doDeleteUpdates( $id ) {
 +      public function doDeleteUpdates( $id, Content $content = null ) {
                # update site status
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
  
                # remove secondary indexes, etc
 -              $updates = $this->getDeletionUpdates( );
 +              $updates = $this->getDeletionUpdates( $content );
                DataUpdate::runUpdates( $updates );
  
                # Clear caches
                $this->mTitle->resetArticleID( 0 );
        }
  
 -      public function getDeletionUpdates() {
 -              $updates = array(
 -                      new LinksDeletionUpdate( $this ),
 -              );
 -
 -              //@todo: make a hook to add update objects
 -              //NOTE: deletion updates will be determined by the ContentHandler in the future
 -              return $updates;
 -      }
 -
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
                }
  
                # Actually store the edit
 -              $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
 +              $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
                if ( !empty( $status->value['revision'] ) ) {
                        $revId = $status->value['revision']->getId();
                } else {
  
        /**
        * Return an applicable autosummary if one exists for the given edit.
 -      * @param $oldtext String: the previous text of the page.
 -      * @param $newtext String: The submitted text of the page.
 +      * @param $oldtext String|null: the previous text of the page.
 +      * @param $newtext String|null: The submitted text of the page.
        * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
        * @return string An appropriate autosummary, or an empty string.
 +      *
 +      * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
        */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
 -              global $wgContLang;
 -
 -              # Decide what kind of autosummary is needed.
 -
 -              # Redirect autosummaries
 -              $ot = Title::newFromRedirect( $oldtext );
 -              $rt = Title::newFromRedirect( $newtext );
 -
 -              if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 255
 -                                      - strlen( wfMsgForContent( 'autoredircomment' ) )
 -                                      - strlen( $rt->getFullText() )
 -                              ) );
 -                      return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
 -              }
 +              # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
  
 -              # New page autosummaries
 -              if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
 -                      # If they're making a new article, give its text, truncated, in the summary.
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
 +              $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
 +              $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
 +              $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
  
 -                      return wfMsgForContent( 'autosumm-new', $truncatedtext );
 -              }
 -
 -              # Blanking autosummaries
 -              if ( $oldtext != '' && $newtext == '' ) {
 -                      return wfMsgForContent( 'autosumm-blank' );
 -              } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
 -                      # Removing more than 90% of the article
 -
 -                      $truncatedtext = $wgContLang->truncate(
 -                              $newtext,
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
 -
 -                      return wfMsgForContent( 'autosumm-replace', $truncatedtext );
 -              }
 -
 -              # If we reach this point, there's no applicable autosummary for our case, so our
 -              # autosummary is empty.
 -              return '';
 +              return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
  
        /**
         *    if no revision occurred
         */
        public function getAutoDeleteReason( &$hasHistory ) {
 -              global $wgContLang;
 -
 -              // Get the last revision
 -              $rev = $this->getRevision();
 -
 -              if ( is_null( $rev ) ) {
 -                      return false;
 -              }
 -
 -              // Get the article's contents
 -              $contents = $rev->getText();
 -              $blank = false;
 -
 -              // If the page is blank, use the text from the previous revision,
 -              // which can only be blank if there's a move/import/protect dummy revision involved
 -              if ( $contents == '' ) {
 -                      $prev = $rev->getPrevious();
 -
 -                      if ( $prev )    {
 -                              $contents = $prev->getText();
 -                              $blank = true;
 -                      }
 -              }
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -
 -              // Find out if there was only one contributor
 -              // Only scan the last 20 revisions
 -              $res = $dbw->select( 'revision', 'rev_user_text',
 -                      array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
 -                      __METHOD__,
 -                      array( 'LIMIT' => 20 )
 -              );
 -
 -              if ( $res === false ) {
 -                      // This page has no revisions, which is very weird
 -                      return false;
 -              }
 -
 -              $hasHistory = ( $res->numRows() > 1 );
 -              $row = $dbw->fetchObject( $res );
 -
 -              if ( $row ) { // $row is false if the only contributor is hidden
 -                      $onlyAuthor = $row->rev_user_text;
 -                      // Try to find a second contributor
 -                      foreach ( $res as $row ) {
 -                              if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
 -                                      $onlyAuthor = false;
 -                                      break;
 -                              }
 -                      }
 -              } else {
 -                      $onlyAuthor = false;
 -              }
 -
 -              // Generate the summary with a '$1' placeholder
 -              if ( $blank ) {
 -                      // The current revision is blank and the one before is also
 -                      // blank. It's just not our lucky day
 -                      $reason = wfMsgForContent( 'exbeforeblank', '$1' );
 -              } else {
 -                      if ( $onlyAuthor ) {
 -                              $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
 -                      } else {
 -                              $reason = wfMsgForContent( 'excontent', '$1' );
 -                      }
 -              }
 -
 -              if ( $reason == '-' ) {
 -                      // Allow these UI messages to be blanked out cleanly
 -                      return '';
 -              }
 -
 -              // Replace newlines with spaces to prevent uglyness
 -              $contents = preg_replace( "/[\n\r]/", ' ', $contents );
 -              // Calculate the maximum amount of chars to get
 -              // Max content length = max comment length - length of the comment (excl. $1)
 -              $maxLength = 255 - ( strlen( $reason ) - 2 );
 -              $contents = $wgContLang->truncate( $contents, $maxLength );
 -              // Remove possible unfinished links
 -              $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
 -              // Now replace the '$1' placeholder
 -              $reason = str_replace( '$1', $contents, $reason );
 -
 -              return $reason;
 +              return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
        }
  
        /**
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
 +
 +      /**
 +       * Returns a list of updates to be performed when this page is deleted. The updates should remove any infomration
 +       * about this page from secondary data stores such as links tables.
 +       *
 +       * @param Content|null $content optional Content object for determining the necessary updates
 +       * @return Array an array of DataUpdates objects
 +       */
 +      public function getDeletionUpdates( Content $content = null ) {
 +              if ( !$content ) {
 +                      // load content object, which may be used to determine the necessary updates
 +                      // XXX: the content may not be needed to determine the updates, then this would be overhead.
 +                      $content = $this->getContent( Revision::RAW );
 +              }
 +
 +              $updates = $this->getContentHandler()->getDeletionUpdates( $content, $this->mTitle );
 +
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, &$updates ) );
 +              return $updates;
 +      }
 +
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
        private $parserOptions;
  
        /**
 -       * @var string|null
 +       * @var Content|null
         */
 -      private $text;
 +      private $content = null;
  
        /**
         * @var ParserOutput|bool
         * @param $revid Integer: ID of the revision being parsed
         * @param $useParserCache Boolean: whether to use the parser cache
         * @param $parserOptions parserOptions to use for the parse operation
 -       * @param $text String: text to parse or null to load it
 +       * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC
         */
 -      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
 +      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
 +              if ( is_string($content) ) { #BC: old style call
 +                      $modelId = $page->getRevision()->getContentModel();
 +                      $format = $page->getRevision()->getContentFormat();
 +                      $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
 +              }
 +
                $this->page = $page;
                $this->revid = $revid;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
 -              $this->text = $text;
 +              $this->content = $content;
                $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
                parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
        }
         * @return bool
         */
        function doWork() {
 -              global $wgParser, $wgUseFileCache;
 +              global $wgUseFileCache;
 +
 +              // @todo: several of the methods called on $this->page are not declared in Page, but present in WikiPage and delegated by Article.
  
                $isCurrent = $this->revid === $this->page->getLatest();
  
 -              if ( $this->text !== null ) {
 -                      $text = $this->text;
 +              if ( $this->content !== null ) {
 +                      $content = $this->content;
                } elseif ( $isCurrent ) {
 -                      $text = $this->page->getRawText();
 +                      $content = $this->page->getContent( Revision::RAW ); #XXX: why use RAW audience here, and PUBLIC (default) below?
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
 -                      $text = $rev->getText();
 +                      $content = $rev->getContent(); #XXX: why use PUBLIC audience here (default), and RAW above?
                }
  
                $time = - microtime( true );
 -              $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
 -                      $this->parserOptions, true, true, $this->revid );
 +              $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
  
                # Timing hack
                return false;
        }
  }
 +
@@@ -4,7 -4,7 +4,7 @@@
   *
   * Created on Jun 30, 2007
   *
-  * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com
+  * Copyright © 2007 Roan Kattouw "<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
@@@ -52,17 -52,18 +52,18 @@@ class ApiDelete extends ApiBase 
                }
  
                $titleObj = $pageObj->getTitle();
-               $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
+               $reason = $params['reason'];
                $user = $this->getUser();
  
                if ( $titleObj->getNamespace() == NS_FILE ) {
-                       $retval = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false );
+                       $status = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false );
                } else {
-                       $retval = self::delete( $pageObj, $user, $params['token'], $reason );
+                       $status = self::delete( $pageObj, $user, $params['token'], $reason );
                }
  
-               if ( count( $retval ) ) {
-                       $this->dieUsageMsg( reset( $retval ) ); // We don't care about multiple errors, just report one of them
+               if ( !$status->isGood() ) {
+                       $errors = $status->getErrorsArray();
+                       $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them
                }
  
                // Deprecated parameters
                }
                $this->setWatch( $watch, $titleObj, 'watchdeletion' );
  
-               $r = array( 'title' => $titleObj->getPrefixedText(), 'reason' => $reason );
+               $r = array(
+                       'title' => $titleObj->getPrefixedText(),
+                       'reason' => $reason,
+                       'logid' => $status->value
+               );
                $this->getResult()->addValue( null, $this->getModuleName(), $r );
        }
  
         * @param $user User doing the action
         * @param $token String: delete token (same as edit token)
         * @param $reason String: reason for the deletion. Autogenerated if NULL
-        * @return Title::getUserPermissionsErrors()-like array
+        * @return Status
         */
        public static function delete( Page $page, User $user, $token, &$reason = null ) {
                $title = $page->getTitle();
                        // Need to pass a throwaway variable because generateReason expects
                        // a reference
                        $hasHistory = false;
 -                      $reason = $page->getAutoDeleteReason( $hasHistory );
 +                      $reason = $page->getAutoDeleteReason( $hasHistory ); 
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete', $title->getPrefixedText() ) );
                        }
  
                $error = '';
                // Luckily, Article.php provides a reusable delete function that does the hard work for us
-               if ( $page->doDeleteArticle( $reason, false, 0, true, $error ) ) {
-                       return array();
-               } else {
-                       return array( array( 'cannotdelete', $title->getPrefixedText() ) );
-               }
+               return $page->doDeleteArticleReal( $reason, false, 0, true, $error );
        }
  
        /**
         * @param $oldimage
         * @param $reason
         * @param $suppress bool
-        * @return array|Title
+        * @return Status
         */
        public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) {
                $title = $page->getTitle();
                if ( is_null( $reason ) ) { // Log and RC don't like null reasons
                        $reason = '';
                }
-               $status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
-               if ( !$status->isGood() ) {
-                       return array( array( 'cannotdelete', $title->getPrefixedText() ) );
-               }
-               return array();
+               return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
        }
  
        public function mustBePosted() {
                        'pageid' => array(
                                ApiBase::PARAM_TYPE => 'integer'
                        ),
-                       'token' => null,
+                       'token' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true
+                       ),
                        'reason' => null,
                        'watch' => array(
                                ApiBase::PARAM_DFLT => false,
                return array(
                        '' => array(
                                'title' => 'string',
-                               'reason' => 'string'
+                               'reason' => 'string',
+                               'logid' => 'integer'
                        )
                );
        }
@@@ -4,7 -4,7 +4,7 @@@
   *
   * Created on August 16, 2007
   *
-  * Copyright © 2007 Iker Labarga <Firstname><Lastname>@gmail.com
+  * 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
@@@ -54,30 -54,13 +54,30 @@@ class ApiEditPage extends ApiBase 
                        $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                }
  
 +              $contentHandler = $pageObj->getContentHandler();
 +
 +              // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such
 +
 +              if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
 +                      $params['contentformat'] = $contentHandler->getDefaultFormat();
 +              }
 +
 +              $contentFormat = $params['contentformat'];
 +
 +              if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
 +                      $name = $titleObj->getPrefixedDBkey();
 +                      $model = $contentHandler->getModelID();
 +
 +                      $this->dieUsage( "The requested format $contentFormat is not supported for content model $model used by $name", 'badformat' );
 +              }
 +
                $apiResult = $this->getResult();
  
                if ( $params['redirect'] ) {
                        if ( $titleObj->isRedirect() ) {
                                $oldTitle = $titleObj;
  
 -                              $titles = Title::newFromRedirectArray( Revision::newFromTitle( $oldTitle )->getText( Revision::FOR_THIS_USER ) );
 +                              $titles = Revision::newFromTitle( $oldTitle )->getContent( Revision::FOR_THIS_USER )->getRedirectChain();
                                // array_shift( $titles );
  
                                $redirValues = array();
                        $this->dieUsageMsg( $errors[0] );
                }
  
 -              $articleObj = Article::newFromTitle( $titleObj, $this->getContext() );
 -
                $toMD5 = $params['text'];
                if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) )
                {
 -                      // For non-existent pages, Article::getContent()
 -                      // returns an interface message rather than ''
 -                      // We do want getContent()'s behavior for non-existent
 -                      // MediaWiki: pages, though
 -                      if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) {
 -                              $content = '';
 -                      } else {
 -                              $content = $articleObj->getContent();
 +                      $content = $pageObj->getContent();
 +
 +                      if ( !( $content instanceof TextContent ) ) {
 +                              // @todo: ContentHandler should have an isFlat() method or some such
 +                              // @todo: XXX: or perhaps there should be Content::append(), Content::prepend() and Content::supportsConcatenation()
 +                              $mode = $contentHandler->getModelID();
 +                              $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' );
 +                      }
 +
 +                      if ( !$content ) {
 +                              # If this is a MediaWiki:x message, then load the messages
 +                              # and return the message value for x.
 +                              if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
 +                                      $text = $titleObj->getDefaultMessageText();
 +                                      if ( $text === false ) {
 +                                              $text = '';
 +                                      }
 +
 +                                      $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +                              }
                        }
  
                        if ( !is_null( $params['section'] ) ) {
 +                              if ( !$contentHandler->supportsSections() ) {
 +                                      $modelName = $contentHandler->getModelID();
 +                                      $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' );
 +                              }
 +
                                // Process the content for section edits
 -                              global $wgParser;
                                $section = intval( $params['section'] );
 -                              $content = $wgParser->getSection( $content, $section, false );
 -                              if ( $content === false ) {
 +                              $content = $content->getSection( $section );
 +
 +                              if ( !$content ) {
                                        $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
                                }
                        }
 -                      $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
 +
 +                      if ( !$content ) {
 +                              $text = '';
 +                      } else {
 +                              $text = $content->serialize( $contentFormat );
 +                      }
 +
 +                      $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
                        $toMD5 = $params['prependtext'] . $params['appendtext'];
                }
  
                                $this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) );
                        }
  
 -                      if ( $undoRev->getPage() != $articleObj->getID() ) {
 +                      if ( $undoRev->getPage() != $pageObj->getID() ) {
                                $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) );
                        }
 -                      if ( $undoafterRev->getPage() != $articleObj->getID() ) {
 +                      if ( $undoafterRev->getPage() != $pageObj->getID() ) {
                                $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) );
                        }
  
 -                      $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev );
 -                      if ( $newtext === false ) {
 +                      $newContent = $contentHandler->getUndoContent( $undoRev, $undoafterRev );
 +                      if ( !$newContent ) {
                                $this->dieUsageMsg( 'undo-failure' );
                        }
 -                      $params['text'] = $newtext;
 +
 +                      $params['contentformat'] = $contentHandler->getDefaultFormat();
 +                      $params['text'] = $newContent->serialize( $params['contentformat'] );
 +
                        // If no summary was given and we only undid one rev,
                        // use an autosummary
                        if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] ) {
                // That interface kind of sucks, but it's workable
                $requestArray = array(
                        'wpTextbox1' => $params['text'],
 +                      'format' => $contentFormat,
 +                      'model' => $contentHandler->getModelID(),
                        'wpEditToken' => $params['token'],
                        'wpIgnoreBlankSummary' => ''
                );
                if ( !is_null( $params['basetimestamp'] ) && $params['basetimestamp'] != '' ) {
                        $requestArray['wpEdittime'] = wfTimestamp( TS_MW, $params['basetimestamp'] );
                } else {
 -                      $requestArray['wpEdittime'] = $articleObj->getTimestamp();
 +                      $requestArray['wpEdittime'] = $pageObj->getTimestamp();
                }
  
                if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' ) {
                // TODO: Make them not or check if they still do
                $wgTitle = $titleObj;
  
 -              $ep = new EditPage( $articleObj );
 +              $articleObject = new Article( $titleObj );
 +              $ep = new EditPage( $articleObject );
 +
                $ep->setContextTitle( $titleObj );
                $ep->importFormData( $req );
  
                }
  
                // Do the actual save
 -              $oldRevId = $articleObj->getRevIdFetched();
 +              $oldRevId = $articleObject->getRevIdFetched();
                $result = null;
                // Fake $wgRequest for some hooks inside EditPage
                // @todo FIXME: This interface SUCKS
                                $r['result'] = 'Success';
                                $r['pageid'] = intval( $titleObj->getArticleID() );
                                $r['title'] = $titleObj->getPrefixedText();
 -                              $newRevId = $articleObj->getLatest();
 +                              $r['contentmodel'] = $titleObj->getContentModel();
 +                              $newRevId = $articleObject->getLatest();
                                if ( $newRevId == $oldRevId ) {
                                        $r['nochange'] = '';
                                } else {
                                        $r['oldrevid'] = intval( $oldRevId );
                                        $r['newrevid'] = intval( $newRevId );
                                        $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
 -                                              $articleObj->getTimestamp() );
 +                                              $pageObj->getTimestamp() );
                                }
                                break;
  
                                array( 'unknownerror', 'retval' ),
                                array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ),
                                array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ),
 +                              array( 'code' => 'sectionsnotsupported', 'info' => 'Sections are not supported for this type of page.' ),
 +                              array( 'code' => 'editnotsupported', 'info' => 'Editing of this type of page is not supported using the text based edit API.' ),
 +                              array( 'code' => 'appendnotsupported', 'info' => 'This type of page can not be edited by appending or prepending text.' ),
 +                              array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to the page\'s content model' ),
                                array( 'customcssprotected' ),
                                array( 'customjsprotected' ),
                        )
                                ApiBase::PARAM_REQUIRED => false,
                        ),
                        'text' => null,
-                       'token' => null,
+                       'token' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true
+                       ),
                        'summary' => null,
                        'minor' => false,
                        'notminor' => false,
diff --combined includes/api/ApiMain.php
@@@ -4,7 -4,7 +4,7 @@@
   *
   * Created on Sep 4, 2006
   *
-  * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+  * Copyright © 2006 Yuri Astrakhan "<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
@@@ -104,7 -104,6 +104,7 @@@ class ApiMain extends ApiBase 
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
 +              'none' => 'ApiFormatNone',
        );
  
        /**
         * have been accumulated, and replace it with an error message and a help screen.
         */
        protected function executeActionWithErrorHandling() {
+               // Verify the CORS header before executing the action
+               if ( !$this->handleCORS() ) {
+                       // handleCORS() has sent a 403, abort
+                       return;
+               }
                // In case an error occurs during data output,
                // clear the output buffer and print just the error information
                ob_start();
                ob_end_flush();
        }
  
+       /**
+        * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
+        *
+        * If no origin parameter is present, nothing happens.
+        * If an origin parameter is present but doesn't match the Origin header, a 403 status code
+        * is set and false is returned.
+        * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
+        * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
+        * headers are set.
+        *
+        * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
+        */
+       protected function handleCORS() {
+               global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions;
+               $response = $this->getRequest()->response();
+               $originParam = $this->getParameter( 'origin' ); // defaults to null
+               if ( $originParam === null ) {
+                       // No origin parameter, nothing to do
+                       return true;
+               }
+               // Origin: header is a space-separated list of origins, check all of them
+               $originHeader = isset( $_SERVER['HTTP_ORIGIN'] ) ? $_SERVER['HTTP_ORIGIN'] : '';
+               $origins = explode( ' ', $originHeader );
+               if ( !in_array( $originParam, $origins ) ) {
+                       // origin parameter set but incorrect
+                       // Send a 403 response
+                       $message = HttpStatus::getMessage( 403 );
+                       $response->header( "HTTP/1.1 403 $message", true, 403 );
+                       $response->header( 'Cache-Control: no-cache' );
+                       echo "'origin' parameter does not match Origin header\n";
+                       return false;
+               }
+               if ( self::matchOrigin( $originParam, $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions ) ) {
+                       $response->header( "Access-Control-Allow-Origin: $originParam" );
+                       $response->header( 'Access-Control-Allow-Credentials: true' );
+                       $this->getOutput()->addVaryHeader( 'Origin' );
+               }
+               return true;
+       }
+       /**
+        * Attempt to match an Origin header against a set of rules and a set of exceptions
+        * @param $value string Origin header
+        * @param $rules array Set of wildcard rules
+        * @param $exceptions array Set of wildcard rules
+        * @return bool True if $value matches a rule in $rules and doesn't match any rules in $exceptions, false otherwise
+        */
+       protected static function matchOrigin( $value, $rules, $exceptions ) {
+               foreach ( $rules as $rule ) {
+                       if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
+                               // Rule matches, check exceptions
+                               foreach ( $exceptions as $exc ) {
+                                       if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
+                                               return false;
+                                       }
+                               }
+                               return true;
+                       }
+               }
+               return false;
+       }
+       /**
+        * Helper function to convert wildcard string into a regex
+        * '*' => '.*?'
+        * '?' => '.'
+        *
+        * @param $wildcard string String with wildcards
+        * @return string Regular expression
+        */
+       protected static function wildcardToRegex( $wildcard ) {
+               $wildcard = preg_quote( $wildcard, '/' );
+               $wildcard = str_replace(
+                       array( '\*', '\?' ),
+                       array( '.*?', '.' ),
+                       $wildcard
+               );
+               return "/https?:\/\/$wildcard/";
+       }
        protected function sendCacheHeaders() {
                global $wgUseXVO, $wgVaryOnXFP;
                $response = $this->getRequest()->response();
+               $out = $this->getOutput();
+               if ( $wgVaryOnXFP ) {
+                       $out->addVaryHeader( 'X-Forwarded-Proto' );
+               }
  
                if ( $this->mCacheMode == 'private' ) {
                        $response->header( 'Cache-Control: private' );
                }
  
                if ( $this->mCacheMode == 'anon-public-user-private' ) {
-                       $xfp = $wgVaryOnXFP ? ', X-Forwarded-Proto' : '';
-                       $response->header( 'Vary: Accept-Encoding, Cookie' . $xfp );
+                       $out->addVaryHeader( 'Cookie' );
+                       $response->header( $out->getVaryHeader() );
                        if ( $wgUseXVO ) {
-                               $out = $this->getOutput();
-                               if ( $wgVaryOnXFP ) {
-                                       $out->addVaryHeader( 'X-Forwarded-Proto' );
-                               }
                                $response->header( $out->getXVO() );
                                if ( $out->haveCacheVaryCookies() ) {
                                        // Logged in, mark this request private
                }
  
                // Send public headers
-               if ( $wgVaryOnXFP ) {
-                       $response->header( 'Vary: Accept-Encoding, X-Forwarded-Proto' );
-                       if ( $wgUseXVO ) {
-                               // Bleeeeegh. Our header setting system sucks
-                               $response->header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip, X-Forwarded-Proto' );
-                       }
+               $response->header( $out->getVaryHeader() );
+               if ( $wgUseXVO ) {
+                       $response->header( $out->getXVO() );
                }
  
                // If nobody called setCacheMaxAge(), use the (s)maxage parameters
                        ),
                        'requestid' => null,
                        'servedby'  => false,
+                       'origin' => null,
                );
        }
  
                        'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
                        'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
                        'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error',
+                       'origin' => array(
+                               'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.',
+                               'This must match one of the origins in the Origin: header exactly, so it has to be set to something like http://en.wikipedia.org or https://meta.wikimedia.org .',
+                               'If this parameter does not match the Origin: header, a 403 response will be returned.',
+                               'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.',
+                       ),
                );
        }
  
        protected function getCredits() {
                return array(
                        'API developers:',
-                       '    Roan Kattouw <Firstname>.<Lastname>@gmail.com (lead developer Sep 2007-present)',
+                       '    Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-present)',
                        '    Victor Vasiliev - vasilvv at gee mail dot com',
                        '    Bryan Tong Minh - bryan . tongminh @ gmail . com',
                        '    Sam Reed - sam @ reedyboy . net',
-                       '    Yuri Astrakhan <Firstname><Lastname>@gmail.com (creator, lead developer Sep 2006-Sep 2007)',
+                       '    Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007)',
                        '',
                        'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org',
                        'or file a bug report at https://bugzilla.wikimedia.org/'
  class UsageException extends MWException {
  
        private $mCodestr;
+       /**
+        * @var null|array
+        */
        private $mExtraData;
  
+       /**
+        * @param $message string
+        * @param $codestr string
+        * @param $code int
+        * @param $extradata array|null
+        */
        public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
                parent::__construct( $message, $code );
                $this->mCodestr = $codestr;
@@@ -2,7 -2,7 +2,7 @@@
  /**
   * Created on Dec 01, 2007
   *
-  * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+  * Copyright © 2007 Yuri Astrakhan "<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
@@@ -189,7 -189,7 +189,7 @@@ class ApiParse extends ApiBase 
                                return;
                        }
                        // Not cached (save or load)
 -                      $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts );
 +                      $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts ); #FIXME: use Content object¡
                }
  
                $result_array = array();
  
                $page = WikiPage::factory( $titleObj );
  
 -              if ( $this->section !== false ) {
 +              if ( $this->section !== false ) { #FIXME: get section Content, get parser output, ...
                        $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
 -                                      ? 'page id ' . $pageId : $titleObj->getText() );
 +                                      ? 'page id ' . $pageId : $titleObj->getText() ); #FIXME: get section...
  
                        // Not cached (save or load)
                        return $wgParser->parse( $this->text, $titleObj, $popts );
                                $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
                        }
                        if ( $getWikitext ) {
 -                              $this->text = $page->getRawText();
 +                              $this->content = $page->getContent( Revision::RAW ); #FIXME: use $this->content everywhere
 +                              $this->text = ContentHandler::getContentText( $this->content ); #FIXME: serialize, get format from params; or use object structure in result?
                        }
                        return $pout;
                }
        }
  
 -      private function getSectionText( $text, $what ) {
 +      private function getSectionText( $text, $what ) { #FIXME: replace with Content::getSection
                global $wgParser;
                // Not cached (save or load)
                $text = $wgParser->getSection( $text, $this->section, false );
@@@ -4,7 -4,7 +4,7 @@@
   *
   * Created on Sep 7, 2006
   *
-  * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+  * Copyright © 2006 Yuri Astrakhan "<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
  class ApiQueryRevisions extends ApiQueryBase {
  
        private $diffto, $difftotext, $expandTemplates, $generateXML, $section,
 -              $token, $parseContent;
 +              $token, $parseContent, $contentFormat;
  
        public function __construct( $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'rv' );
        }
  
 -      private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
 +      private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, $fld_sha1 = false,
                        $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
 -                      $fld_content = false, $fld_tags = false;
 +                      $fld_content = false, $fld_tags = false, $fld_contentmodel = false;
  
        private $tokenFunctions;
  
                $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
                $this->fld_size = isset ( $prop['size'] );
                $this->fld_sha1 = isset ( $prop['sha1'] );
 +              $this->fld_contentmodel = isset ( $prop['contentmodel'] );
                $this->fld_userid = isset( $prop['userid'] );
                $this->fld_user = isset ( $prop['user'] );
                $this->token = $params['token'];
  
 +              if ( !empty( $params['contentformat'] ) ) {
 +                      $this->contentFormat = $params['contentformat'];
 +              }
 +
                // Possible indexes used
                $index = array();
  
                        }
                }
  
 +              if ( $this->fld_contentmodel ) {
 +                      $vals['contentmodel'] = $revision->getContentModel();
 +              }
 +
                if ( $this->fld_comment || $this->fld_parsedcomment ) {
                        if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = '';
                        }
                }
  
 -              $text = null;
 +              $content = null;
                global $wgParser;
                if ( $this->fld_content || !is_null( $this->difftotext ) ) {
 -                      $text = $revision->getText();
 +                      $content = $revision->getContent();
                        // Expand templates after getting section content because
                        // template-added sections don't count and Parser::preprocess()
                        // will have less input
                        if ( $this->section !== false ) {
 -                              $text = $wgParser->getSection( $text, $this->section, false );
 -                              if ( $text === false ) {
 +                              $content = $content->getSection( $this->section, false );
 +                              if ( !$content ) {
                                        $this->dieUsage( "There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection' );
                                }
                        }
                }
                if ( $this->fld_content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
 +                      $text = null;
 +
                        if ( $this->generateXML ) {
 -                              $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
 -                              $dom = $wgParser->preprocessToDom( $text );
 -                              if ( is_callable( array( $dom, 'saveXML' ) ) ) {
 -                                      $xml = $dom->saveXML();
 +                              if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
 +                                      $t = $content->getNativeData(); # note: don't set $text
 +
 +                                      $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
 +                                      $dom = $wgParser->preprocessToDom( $t );
 +                                      if ( is_callable( array( $dom, 'saveXML' ) ) ) {
 +                                              $xml = $dom->saveXML();
 +                                      } else {
 +                                              $xml = $dom->__toString();
 +                                      }
 +                                      $vals['parsetree'] = $xml;
                                } else {
 -                                      $xml = $dom->__toString();
 +                                      $this->setWarning( "Conversion to XML is supported for wikitext only, " .
 +                                                                              $title->getPrefixedDBkey() .
 +                                                                              " uses content model " . $content->getModel() . ")" );
                                }
 -                              $vals['parsetree'] = $xml;
 -
                        }
 +
                        if ( $this->expandTemplates && !$this->parseContent ) {
 -                              $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
 +                              #XXX: implement template expansion for all content types in ContentHandler?
 +                              if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
 +                                      $text = $content->getNativeData();
 +
 +                                      $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
 +                              } else {
 +                                      $this->setWarning( "Template expansion is supported for wikitext only, " .
 +                                              $title->getPrefixedDBkey() .
 +                                              " uses content model " . $content->getModel() . ")" );
 +
 +                                      $text = false;
 +                              }
                        }
                        if ( $this->parseContent ) {
 -                              $text = $wgParser->parse( $text, $title, ParserOptions::newFromContext( $this->getContext() ) )->getText();
 +                              $po = $content->getParserOutput( $title, ParserOptions::newFromContext( $this->getContext() ) );
 +                              $text = $po->getText();
 +                      }
 +
 +                      if ( $text === null ) {
 +                              $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat();
 +
 +                              if ( !$content->isSupportedFormat( $format ) ) {
 +                                      $model = $content->getModel();
 +                                      $name = $title->getPrefixedDBkey();
 +
 +                                      $this->dieUsage( "The requested format {$this->contentFormat} is not supported for content model $model used by $name", 'badformat' );
 +                              }
 +
 +                              $text = $content->serialize( $format );
 +                              $vals['contentformat'] = $format;
 +                      }
 +
 +                      if ( $text !== false ) {
 +                              ApiResult::setContent( $vals, $text );
                        }
 -                      ApiResult::setContent( $vals, $text );
                } elseif ( $this->fld_content ) {
                        $vals['texthidden'] = '';
                }
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
 +                              $handler = ContentHandler::getForTitle( $title );
 +
                                if ( !is_null( $this->difftotext ) ) {
 -                                      $engine = new DifferenceEngine( $context );
 -                                      $engine->setText( $text, $this->difftotext );
 +                                      $model = $title->getContentModel();
 +
 +                                      if ( $this->contentFormat && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) {
 +                                              $name = $title->getPrefixedDBkey();
 +
 +                                              $this->dieUsage( "The requested format {$this->contentFormat} is not supported for content model $model used by $name", 'badformat' );
 +                                      }
 +
 +                                      $difftocontent = ContentHandler::makeContent( $this->difftotext, $title, $model, $this->contentFormat );
 +
 +                                      $engine = $handler->createDifferenceEngine( $context );
 +                                      $engine->setContent( $content, $difftocontent );
                                } else {
 -                                      $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto );
 +                                      $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
                                        $vals['diff']['from'] = $engine->getOldid();
                                        $vals['diff']['to'] = $engine->getNewid();
                                }
                                        'userid',
                                        'size',
                                        'sha1',
 +                                      'contentmodel',
                                        'comment',
                                        'parsedcomment',
                                        'content',
                        'continue' => null,
                        'diffto' => null,
                        'difftotext' => null,
 +                      'contentformat' => array(
 +                              ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
 +                              ApiBase::PARAM_DFLT => null
 +                      ),
                );
        }
  
                                ' userid         - User id of revision creator',
                                ' size           - Length (bytes) of the revision',
                                ' sha1           - SHA-1 (base 16) of the revision',
 +                              ' contentmodel   - Content model id',
                                ' comment        - Comment by the user for revision',
                                ' parsedcomment  - Parsed comment by the user for the revision',
                                ' content        - Text of the revision',
                        'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.',
                                "Overrides {$p}diffto. If {$p}section is set, only that section will be diffed against this text" ),
                        'tag' => 'Only list revisions tagged with this tag',
 +                      'contentformat' => 'Serialization format used for difftotext and expected for output of content',
                );
        }
  
                        array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ),
                        array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ),
                        array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ),
 +                      array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to the page\'s content model' ),
                ) );
        }
  
@@@ -38,7 -38,7 +38,7 @@@ class DifferenceEngine extends ContextS
         * @private
         */
        var $mOldid, $mNewid;
 -      var $mOldtext, $mNewtext;
 +      var $mOldContent, $mNewContent;
        protected $mDiffLang;
  
        /**
                # If external diffs are enabled both globally and for the user,
                # we'll use the application/x-external-editor interface to call
                # an external diff tool like kompare, kdiff3, etc.
 -              if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
 +              if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) { #FIXME: how to handle this for non-text content?
                        $urls = array(
                                'File' => array( 'Extension' => 'wiki', 'URL' =>
                                        # This should be mOldPage, but it may not be set, see below.
                        if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
                                if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
                                        $out->preventClickjacking();
-                                       $rollback = '&#160;&#160;&#160;' . Linker::generateRollback( $this->mNewRev );
+                                       $rollback = '&#160;&#160;&#160;' . Linker::generateRollback( $this->mNewRev, $this->getContext() );
                                }
                                if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
                                        $undoLink = ' ' . $this->msg( 'parentheses' )->rawParams(
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
  
 -                      if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
 +                      if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { #NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                                // Stolen from Article::view --AG 2007-10-11
                                // Give hooks a chance to customise the output
                                // @TODO: standardize this crap into one function
 -                              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 -                                      // Wrap the whole lot in a <pre> and don't parse
 -                                      $m = array();
 -                                      preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
 -                                      $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                                      $out->addHTML( htmlspecialchars( $this->mNewtext ) );
 -                                      $out->addHTML( "\n</pre>\n" );
 +                              if ( !Hook::isRegistered( 'ShowRawCssJs' )
 +                                      || wfRunHooks( 'ShowRawCssJs', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
 +                                      // use the content object's own rendering
 +                                      $po = $this->mContentObject->getParserOutput();
 +                                      $out->addHTML( $po->getText() );
                                }
 -                      } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 +                      } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                              // Handled by extension
 +                      } elseif( Hooks::isRegistered( 'ArticleViewCustom' )
 +                                      && !wfRunHooks( 'ArticleViewCustom', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
                                // Handled by extension
                        } else {
                                // Normal page
                                        $wikiPage = WikiPage::factory( $this->mNewPage );
                                }
  
 -                              $parserOptions = ParserOptions::newFromContext( $this->getContext() );
 -                              $parserOptions->enableLimitReport();
 -                              $parserOptions->setTidy( true );
 -
 -                              if ( !$this->mNewRev->isCurrent() ) {
 -                                      $parserOptions->setEditSection( false );
 -                              }
 -
 -                              $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid );
 +                              $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
  
                                # WikiPage::getParserOutput() should not return false, but just in case
                                if( $parserOutput ) {
                wfProfileOut( __METHOD__ );
        }
  
 +      protected function getParserOutput( WikiPage $page, Revision $rev ) {
 +              $parserOptions = ParserOptions::newFromContext( $this->getContext() );
 +              $parserOptions->enableLimitReport();
 +              $parserOptions->setTidy( true );
 +
 +              if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) {
 +                      $parserOptions->setEditSection( false );
 +              }
 +
 +              $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
 +              return $parserOutput;
 +      }
 +
        /**
         * Get the diff text, send it to the OutputPage object
         * Returns false if the diff could not be generated, otherwise returns true
                        return false;
                }
                // Short-circuit
-               // If mOldRev is false, it means that the 
+               // If mOldRev is false, it means that the
                if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
                        && $this->mOldRev->getID() == $this->mNewRev->getID() ) )
                {
                        return false;
                }
  
 -              $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
 +              $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
  
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
                }
        }
  
 +      /**
 +       * Generate a diff, no caching.
 +       *
 +       * Subclasses may override this to provide a
 +       *
 +       * @param $old Content: old content
 +       * @param $new Content: new content
 +       *
 +       * @since 1.WD
 +       */
 +      function generateContentDiffBody( Content $old, Content $new ) {
 +              #XXX: generate a warning if $old or $new are not instances of TextContent?
 +              #XXX: fail if $old and $new don't have the same content model? or what?
 +
 +              $otext = $old->serialize();
 +              $ntext = $new->serialize();
 +
 +              #XXX: text should be "already segmented". what does that mean?
 +              return $this->generateTextDiffBody( $otext, $ntext );
 +      }
 +
        /**
         * Generate a diff, no caching
         *
         * @param $otext String: old text, must be already segmented
         * @param $ntext String: new text, must be already segmented
 -       * @return bool|string
 +       * @deprecated since 1.WD, use generateContentDiffBody() instead!
         */
        function generateDiffBody( $otext, $ntext ) {
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              return $this->generateTextDiffBody( $otext, $ntext );
 +      }
 +
 +      /**
 +       * Generate a diff, no caching
 +       *
 +       * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
 +       *
 +       * @param $otext String: old text, must be already segmented
 +       * @param $ntext String: new text, must be already segmented
 +       * @return bool|string
 +       */
 +      function generateTextDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
  
                wfProfileIn( __METHOD__ );
         *        the visibility of the revision and a link to edit the page.
         * @return String HTML fragment
         */
 -      private function getRevisionHeader( Revision $rev, $complete = '' ) {
 +      protected function getRevisionHeader( Revision $rev, $complete = '' ) {
                $lang = $this->getLanguage();
                $user = $this->getUser();
                $revtimestamp = $rev->getTimestamp();
  
        /**
         * Use specified text instead of loading from the database
 +       * @deprecated since 1.WD
         */
 -      function setText( $oldText, $newText ) {
 -              $this->mOldtext = $oldText;
 -              $this->mNewtext = $newText;
 +      function setText( $oldText, $newText ) { #FIXME: no longer use this, use setContent()!
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
 +              $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
 +
 +              $this->setContent( $oldContent, $newContent );
 +      }
 +
 +      /**
 +       * Use specified text instead of loading from the database
 +       * @since 1.WD
 +       */
 +      function setContent( Content $oldContent, Content $newContent ) {
 +              $this->mOldContent = $oldContent;
 +              $this->mNewContent = $newContent;
 +
                $this->mTextLoaded = 2;
                $this->mRevisionsLoaded = true;
        }
                // Load the new revision object
                $this->mNewRev = $this->mNewid
                        ? Revision::newFromId( $this->mNewid )
-                       : Revision::newFromTitle( $this->getTitle() );
+                       : Revision::newFromTitle( $this->getTitle(), false, Revision::AVOID_MASTER );
  
                if ( !$this->mNewRev instanceof Revision ) {
                        return false;
                        return false;
                }
                if ( $this->mOldRev ) {
 -                      $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
 -                      if ( $this->mOldtext === false ) {
 +                      $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
 +                      if ( $this->mOldContent === false ) {
                                return false;
                        }
                }
                if ( $this->mNewRev ) {
 -                      $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
 -                      if ( $this->mNewtext === false ) {
 +                      $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
 +                      if ( $this->mNewContent === false ) {
                                return false;
                        }
                }
                if ( !$this->loadRevisionData() ) {
                        return false;
                }
 -              $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
 +              $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
                return true;
        }
  }
@@@ -633,7 -633,7 +633,7 @@@ class LocalFile extends File 
  
        /**
         * Fix thumbnail files from 1.4 or before, with extreme prejudice
-        * @TODO: do we still care about this? Perhaps a maintenance script
+        * @todo : do we still care about this? Perhaps a maintenance script
         *        can be made instead. Enabling this code results in a serious
         *        RTT regression for wikis without 404 handling.
         */
         */
        function getDescriptionText() {
                global $wgParser;
-               $revision = Revision::newFromTitle( $this->title );
+               $revision = Revision::newFromTitle( $this->title, false, Revision::AVOID_MASTER );
                if ( !$revision ) return false;
 -              $text = $revision->getText();
 -              if ( !$text ) return false;
 -              $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
 +              $content = $revision->getContent();
 +              if ( !$content ) return false;
 +              $pout = $content->getParserOutput( $this->title, null, new ParserOptions() );
                return $pout->getText();
        }
  
@@@ -36,7 -36,7 +36,7 @@@ class DoubleRedirectJob extends Job 
  
        /**
         * Insert jobs into the job queue to fix redirects to the given title
-        * @param $reason String: the reason for the fix, see message double-redirect-fixed-<reason>
+        * @param $reason String: the reason for the fix, see message "double-redirect-fixed-<reason>"
         * @param $redirTitle Title: the title which has changed, redirects pointing to this title are fixed
         * @param $destTitle bool Not used
         */
                        wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" );
                        return true;
                }
 -              $text = $targetRev->getText();
 -              $currentDest = Title::newFromRedirect( $text );
 +              $content = $targetRev->getContent();
 +              $currentDest = $content->getRedirectTarget();
                if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
                        wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" );
                        return true;
                }
  
                # Check for a suppression tag (used e.g. in periodically archived discussions)
 +              $text = ContentHandler::getContentText( $content );
                $mw = MagicWord::get( 'staticredirect' );
 -              if ( $mw->match( $text ) ) {
 +              if ( $mw->match( $text ) ) { #FIXME: add support for this to ContentHandler/Content
                        wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" );
                        return true;
                }
                # so the regex has to be fairly general
                $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
                        '[[' . $newTitle->getFullText() . ']]',
 -                      $text, 1 );
 +                      $text, 1 ); #FIXME: need a way to do this via ContentHandler!
  
                if ( $newText === $text ) {
                        $this->setLastError( 'Text unchanged???' );
   * transformation of that wiki markup it into XHTML output / markup
   * (which in turn the browser understands, and can display).
   *
-  * <pre>
-  * There are five main entry points into the Parser class:
-  * parse()
+  * There are seven main entry points into the Parser class:
+  *
+  * - Parser::parse()
   *     produces HTML output
-  * preSaveTransform().
+  * - Parser::preSaveTransform().
   *     produces altered wiki markup.
-  * preprocess()
+  * - Parser::preprocess()
   *     removes HTML comments and expands templates
-  * cleanSig() / cleanSigInSig()
+  * - Parser::cleanSig() and Parser::cleanSigInSig()
   *     Cleans a signature before saving it to preferences
-  * getSection()
+  * - Parser::getSection()
   *     Return the content of a section from an article for section editing
-  * replaceSection()
+  * - Parser::replaceSection()
   *     Replaces a section by number inside an article
-  * getPreloadText()
+  * - Parser::getPreloadText()
   *     Removes <noinclude> sections, and <includeonly> tags.
   *
   * Globals used:
   *    object: $wgContLang
   *
-  * NOT $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
+  * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
   *
-  * settings:
-  *  $wgUseDynamicDates*, $wgInterwikiMagic*,
-  *  $wgNamespacesWithSubpages, $wgAllowExternalImages*,
-  *  $wgLocaltimezone, $wgAllowSpecialInclusion*,
-  *  $wgMaxArticleSize*
+  * @par Settings:
+  * $wgLocaltimezone
+  * $wgNamespacesWithSubpages
   *
-  *  * only within ParserOptions
-  * </pre>
+  * @par Settings only within ParserOptions:
+  * $wgAllowExternalImages
+  * $wgAllowSpecialInclusion
+  * $wgInterwikiMagic
+  * $wgMaxArticleSize
+  * $wgUseDynamicDates
   *
   * @ingroup Parser
   */
@@@ -364,6 -366,7 +366,7 @@@ class Parser 
                # No more strip!
                wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
                $text = $this->internalParse( $text );
+               wfRunHooks( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) );
  
                $text = $this->mStripState->unstripGeneral( $text );
  
        }
  
        /**
-        * Process the wikitext for the ?preload= feature. (bug 5210)
+        * Process the wikitext for the "?preload=" feature. (bug 5210)
         *
-        * <noinclude>, <includeonly> etc. are parsed as for template transclusion,
-        * comments, templates, arguments, tags hooks and parser functions are untouched.
+        * "<noinclude>", "<includeonly>" etc. are parsed as for template
+        * transclusion, comments, templates, arguments, tags hooks and parser
+        * functions are untouched.
         *
         * @param $text String
         * @param $title Title
         * in the text with a random marker and returns the next text. The output
         * parameter $matches will be an associative array filled with data in
         * the form:
+        *
+        * @code
         *   'UNIQ-xxxxx' => array(
         *     'element',
         *     'tag content',
         *     array( 'param' => 'x' ),
         *     '<element param="x">tag content</element>' ) )
+        * @endcode
         *
         * @param $elements array list of element names. Comments are always extracted.
         * @param $text string Source text string.
                        $text = $this->replaceVariables( $text );
                }
  
+               wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
                $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
                wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
  
         *
         * @param $text String: The text to parse
         * @param $flags Integer: bitwise combination of:
-        *          self::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
+        *          self::PTD_FOR_INCLUSION    Handle "<noinclude>" and "<includeonly>" as if the text is being
         *                                     included. Default is to assume a direct page view.
         *
         * The generated DOM tree must depend only on the input text and the flags.
         * Static function to get a template
         * Can be overridden via ParserOptions::setTemplateCallback().
         *
-        * @parma $title Title
+        * @param $title  Title
         * @param $parser Parser
         *
         * @return array
                        }
  
                        if ( $rev ) {
 -                              $text = $rev->getText();
 +                              $content = $rev->getContent();
 +                              $text = $content->getWikitextForTransclusion();
 +
 +                              if ( $text === false || $text === null ) {
 +                                      $text = false;
 +                                      break;
 +                              }
                        } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
                                global $wgContLang;
                                $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
                                        $text = false;
                                        break;
                                }
 +                              $content = $message->content();
                                $text = $message->plain();
                        } else {
                                break;
                        }
 -                      if ( $text === false ) {
 +                      if ( !$content ) {
                                break;
                        }
                        # Redirect?
                        $finalTitle = $title;
 -                      $title = Title::newFromRedirect( $text );
 +                      $title = $content->getRedirectTarget();
                }
                return array(
                        'text' => $text,
         * Triple brace replacement -- used for template arguments
         * @private
         *
-        * @param $peice array
+        * @param $piece array
         * @param $frame PPFrame
         *
         * @return array
        }
  
        /**
-        * Transform wiki markup when saving a page by doing \r\n -> \n
+        * Transform wiki markup when saving a page by doing "\r\n" -> "\n"
         * conversion, substitting signatures, {{subst:}} templates, etc.
         *
         * @param $text String: the text to transform
        }
  
        /**
-        * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
+        * Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>"
         * The callback should have the following form:
         *    function myParserHook( $text, $params, $parser, $frame ) { ... }
         *
         * this interface, as it is not documented and injudicious use could smash
         * private variables.**
         *
-        * @param $tag Mixed: the tag to use, e.g. 'hook' for <hook>
+        * @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>"
         * @param $callback Mixed: the callback function (and object) to use for the tag
         * @return Mixed|null The old value of the mTagHooks array associated with the hook
         */
         * @since 1.10
         * @todo better document or deprecate this
         *
-        * @param $tag Mixed: the tag to use, e.g. 'hook' for <hook>
+        * @param $tag Mixed: the tag to use, e.g. 'hook' for "<hook>"
         * @param $callback Mixed: the callback function (and object) to use for the tag
         * @return Mixed|null The old value of the mTagHooks array associated with the hook
         */
        }
  
        /**
-        * Create a tag function, e.g. <test>some stuff</test>.
+        * Create a tag function, e.g. "<test>some stuff</test>".
         * Unlike tag hooks, tag functions are parsed at preprocessor level.
         * Unlike parser functions, their content is not preprocessed.
         * @return null
  
        /**
         * @todo FIXME: Update documentation. makeLinkObj() is deprecated.
-        * Replace <!--LINK--> link placeholders with actual links, in the buffer
+        * Replace "<!--LINK-->" link placeholders with actual links, in the buffer
         * Placeholders created in Skin::makeLinkObj()
         *
         * @param $text string
        }
  
        /**
-        * Replace <!--LINK--> link placeholders with plain text of links
+        * Replace "<!--LINK-->" link placeholders with plain text of links
         * (not HTML-formatted).
         *
         * @param $text String
         *
         * @param $text String: Page wikitext
         * @param $section String: a section identifier string of the form:
-        *   <flag1> - <flag2> - ... - <section number>
+        *   "<flag1> - <flag2> - ... - <section number>"
         *
         * Currently the only recognised flag is "T", which means the target section number
         * was derived during a template inclusion parse, in other words this is a template
         * section edit link. If no flags are given, it was an ordinary section edit link.
         * This flag is required to avoid a section numbering mismatch when a section is
-        * enclosed by <includeonly> (bug 6563).
+        * enclosed by "<includeonly>" (bug 6563).
         *
         * The section number 0 pulls the text before the first heading; other numbers will
         * pull the given section along with its lower-level subsections. If the section is
@@@ -157,7 -157,7 +157,7 @@@ class ParserOutput extends CacheTime 
                $mTimestamp;                  # Timestamp of the revision
                private $mIndexPolicy = '';       # 'index' or 'noindex'?  Any other value will result in no change.
                private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
 -              private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place.
 +              private $mSecondaryDataUpdates = array(); # List of instances of DataUpdate, used to cause some information extracted from the page in a custom place.
  
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
  
        }
  
        /**
-        * Add some text to the <head>.
+        * Add some text to the "<head>".
         * If $tag is set, the section with that tag will only be included once
         * in a given page.
         */
         * extracted from the page's content, including a LinksUpdate object for all links stored in
         * this ParserOutput object.
         *
 +       * @note: Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() instead! The content
 +       *        handler may provide additional update objects.
 +       *
         * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText()
         * @param $recursive Boolean: queue jobs for recursive updates?
         *
@@@ -42,7 -42,6 +42,6 @@@ abstract class ResourceLoaderWikiModul
        /* Abstract Protected Methods */
  
        /**
-        * @abstract
         * @param $context ResourceLoaderContext
         */
        abstract protected function getPages( ResourceLoaderContext $context );
                if ( !$revision ) {
                        return null;
                }
 -              return $revision->getRawText();
 +
 +              $content = $revision->getContent( Revision::RAW );
 +              $model = $content->getModel();
 +
 +              if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) {
 +                      wfDebug( __METHOD__ . "bad content model #$model for JS/CSS page!\n" );
 +                      return null;
 +              }
 +
 +              return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS
        }
  
        /* Methods */
@@@ -756,7 -756,8 +756,8 @@@ class SearchResult 
        protected function initFromTitle( $title ) {
                $this->mTitle = $title;
                if ( !is_null( $this->mTitle ) ) {
-                       $this->mRevision = Revision::newFromTitle( $this->mTitle );
+                       $this->mRevision = Revision::newFromTitle(
+                               $this->mTitle, false, Revision::AVOID_MASTER );
                        if ( $this->mTitle->getNamespace() === NS_FILE )
                                $this->mImage = wfFindFile( $this->mTitle );
                }
         */
        protected function initText() {
                if ( !isset( $this->mText ) ) {
 -                      if ( $this->mRevision != null )
 -                              $this->mText = $this->mRevision->getText();
 -                      else // TODO: can we fetch raw wikitext for commons images?
 +                      if ( $this->mRevision != null ) {
 +                              $content = $this->mRevision->getContent();
 +                              $this->mText = $content->getTextForSearchIndex(); //XXX: maybe we don't even need the text, but the content object?
 +                      } else { // TODO: can we fetch raw wikitext for commons images?
                                $this->mText = '';
 -
 +                      }
                }
        }
  
                global $wgUser, $wgAdvancedSearchHighlighting;
                $this->initText();
                list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs( $wgUser );
 -              $h = new SearchHighlighter();
 +              $h = new SearchHighlighter(); // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
                if ( $wgAdvancedSearchHighlighting )
                        return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars );
                else
@@@ -143,8 -143,8 +143,8 @@@ class SpecialBookSources extends Specia
                $page = $this->msg( 'booksources' )->inContentLanguage()->text();
                $title = Title::makeTitleSafe( NS_PROJECT, $page ); # Show list in content language
                if( is_object( $title ) && $title->exists() ) {
-                       $rev = Revision::newFromTitle( $title );
+                       $rev = Revision::newFromTitle( $title, false, Revision::AVOID_MASTER );
 -                      $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
 +                      $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) ); #FIXME: need a way to do this via ContentHandler (or enforce flat text-based content)
                        return true;
                }
  
@@@ -112,22 -112,12 +112,22 @@@ class PageArchive 
         * @return ResultWrapper
         */
        function listRevisions() {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 +                      'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $res = $dbr->select( 'archive',
 -                      array(
 -                              'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 -                              'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
                        'PageArchive::listRevisions',
         * @return Revision
         */
        function getRevision( $timestamp ) {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_len',
 +                      'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $row = $dbr->selectRow( 'archive',
 -                      array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_len',
 -                              'ar_sha1',
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
                                        'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
         * @param $comment String
         * @param $fileVersions Array
         * @param $unsuppress Boolean
+        * @param $user User doing the action, or null to use $wgUser
         *
         * @return array(number of file revisions restored, number of image revisions restored, log message)
         * on success, false on failure
         */
-       function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false ) {
+       function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false, User $user = null ) {
+               global $wgContLang, $wgUser;
                // If both the set of text revisions and file revisions are empty,
                // restore everything. Otherwise, just restore the requested items.
                $restoreAll = empty( $timestamps ) && empty( $fileVersions );
                }
  
                // Touch the log!
-               global $wgContLang;
-               $log = new LogPage( 'delete' );
  
                if( $textRestored && $filesRestored ) {
                        $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
                if( trim( $comment ) != '' ) {
                        $reason .= wfMsgForContent( 'colon-separator' ) . $comment;
                }
-               $log->addEntry( 'restore', $this->title, $reason );
+               if ( $user === null ) {
+                       $user = $wgUser;
+               }
+               $logEntry = new ManualLogEntry( 'delete', 'restore' );
+               $logEntry->setPerformer( $user );
+               $logEntry->setTarget( $this->title );
+               $logEntry->setComment( $reason );
+               $logid = $logEntry->insert();
+               $logEntry->publish( $logid );
  
                return array( $textRestored, $filesRestored, $reason );
        }
         * @return Mixed: number of revisions restored or false on failure
         */
        private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
 +              global $wgContentHandlerNoDB;
 +
                if ( wfReadOnly() ) {
                        return false;
                }
                        $oldones = "ar_timestamp IN ( {$oldts} )";
                }
  
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_page_id',
 +                      'ar_len',
 +                      'ar_sha1');
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                /**
                 * Select each archived revision...
                 */
                $result = $dbw->select( 'archive',
 -                      /* fields */ array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_page_id',
 -                              'ar_len',
 -                              'ar_sha1' ),
 +                      $fields,
                        /* WHERE */ array(
                                'ar_namespace' => $this->title->getNamespace(),
                                'ar_title'     => $this->title->getDBkey(),
@@@ -783,8 -765,8 +794,8 @@@ class SpecialUndelete extends SpecialPa
                                );
                        } else {
                                // The title is no longer valid, show as text
-                               $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
-                               $item = htmlspecialchars( $title->getPrefixedText() );
+                               $item = Html::element( 'span', array( 'class' => 'mw-invalidtitle' ),
+                                       Linker::getInvalidTitleDescription( $this->getContext(), $row->ar_namespace, $row->ar_title ) );
                        }
                        $revs = $this->msg( 'undeleterevisions' )->numParams( $row->count )->parse();
                        $out->addHTML( "<li>{$item} ({$revs})</li>\n" );
  
                if( $this->mPreview ) {
                        // Hide [edit]s
 +                      //FIXME: ContentHandler will have to provide some specialized magic to do this
                        $popts = $out->parserOptions();
                        $popts->setEditSection( false );
                        $out->parserOptions( $popts );
                                        'readonly' => 'readonly',
                                        'cols' => intval( $user->getOption( 'cols' ) ),
                                        'rows' => intval( $user->getOption( 'rows' ) ) ),
 -                              $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) .
 +                              $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) . //FIXME: ContentHandler will have to provide some specialized magic to do this
                        Xml::openElement( 'div' ) .
                        Xml::openElement( 'form', array(
                                'method' => 'post',
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
 -              $diffEngine = new DifferenceEngine( $this->getContext() );
 +              $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
 +              $diffEngine = $contentHandler->createDifferenceEngine( $this->getContext() );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
 -                      $diffEngine->generateDiffBody(
 -                              $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ),
 -                              $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) .
 +                      $diffEngine->generateContentDiffBody(
-                               $previousRev->getContent(), $currentRev->getContent() ) .
++                              $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
++                              $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ) .
                        "</table>" .
                        "</div>\n"
                );
                        $this->mTargetTimestamp,
                        $this->mComment,
                        $this->mFileVersions,
-                       $this->mUnsuppress );
+                       $this->mUnsuppress,
+                       $this->getUser()
+               );
  
                if( is_array( $ok ) ) {
                        if ( $ok[1] ) { // Undeleted file count
diff --combined languages/Language.php
@@@ -246,7 -246,11 +246,11 @@@ class Language 
         */
        public static function isValidCode( $code ) {
                return
-                       strcspn( $code, ":/\\\000" ) === strlen( $code )
+                       // People think language codes are html safe, so enforce it.
+                       // Ideally we should only allow a-zA-Z0-9-
+                       // but, .+ and other chars are often used for {{int:}} hacks
+                       // see bugs 37564, 37587, 36938
+                       strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
                        && !preg_match( Title::getTitleInvalidRegex(), $code );
        }
  
         */
        public function setNamespaces( array $namespaces ) {
                $this->namespaceNames = $namespaces;
 +              $this->mNamespaceIds = null;
 +      }
 +
 +      /**
 +       * Resets all of the namespace caches. Mainly used for testing
 +       */
 +      public function resetNamespaces( ) {
 +              $this->namespaceNames = null;
 +              $this->mNamespaceIds = null;
 +              $this->namespaceAliases = null;
        }
  
        /**
                                        $s .= $num;
                                        $raw = false;
                                } elseif ( $roman ) {
-                                       $s .= self::romanNumeral( $num );
+                                       $s .= Language::romanNumeral( $num );
                                        $roman = false;
                                } elseif ( $hebrewNum ) {
                                        $s .= self::hebrewNumeral( $num );
        }
  
        /**
-        * Roman number formatting up to 3000
+        * Roman number formatting up to 10000
         *
         * @param $num int
         *
                        array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
                        array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
                        array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
-                       array( '', 'M', 'MM', 'MMM' )
+                       array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM', 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
                );
  
                $num = intval( $num );
-               if ( $num > 3000 || $num <= 0 ) {
+               if ( $num > 10000 || $num <= 0 ) {
                        return $num;
                }
  
        /**
         * Decode an expiry (block, protection, etc) which has come from the DB
         *
-        * @FIXME: why are we returnings DBMS-dependent strings???
+        * @todo FIXME: why are we returnings DBMS-dependent strings???
         *
         * @param $expiry String: Database expiry String
         * @param $format Bool|Int true to process using language functions, or TS_ constant
@@@ -645,17 -645,17 +645,17 @@@ XHTML id names
  'tog-editsectiononrightclick' => 'Enable section editing by right clicking on section titles (requires JavaScript)',
  'tog-showtoc'                 => 'Show table of contents (for pages with more than 3 headings)',
  'tog-rememberpassword'        => 'Remember my login on this browser (for a maximum of $1 {{PLURAL:$1|day|days}})',
- 'tog-watchcreations'          => 'Add pages I create to my watchlist',
- 'tog-watchdefault'            => 'Add pages I edit to my watchlist',
- 'tog-watchmoves'              => 'Add pages I move to my watchlist',
- 'tog-watchdeletion'           => 'Add pages I delete to my watchlist',
+ 'tog-watchcreations'          => 'Add pages I create and files I upload to my watchlist',
+ 'tog-watchdefault'            => 'Add pages and files I edit to my watchlist',
+ 'tog-watchmoves'              => 'Add pages and files I move to my watchlist',
+ 'tog-watchdeletion'           => 'Add pages and files I delete to my watchlist',
  'tog-minordefault'            => 'Mark all edits minor by default',
  'tog-previewontop'            => 'Show preview before edit box',
  'tog-previewonfirst'          => 'Show preview on first edit',
  'tog-nocache'                 => 'Disable browser page caching',
- 'tog-enotifwatchlistpages'    => 'E-mail me when a page on my watchlist is changed',
+ 'tog-enotifwatchlistpages'    => 'E-mail me when a page or file on my watchlist is changed',
  'tog-enotifusertalkpages'     => 'E-mail me when my user talk page is changed',
- 'tog-enotifminoredits'        => 'E-mail me also for minor edits of pages',
+ 'tog-enotifminoredits'        => 'E-mail me also for minor edits of pages and files',
  'tog-enotifrevealaddr'        => 'Reveal my e-mail address in notification e-mails',
  'tog-shownumberswatching'     => 'Show the number of watching users',
  'tog-oldsig'                  => 'Existing signature:',
  'index-category'                 => 'Indexed pages',
  'noindex-category'               => 'Noindexed pages',
  'broken-file-category'           => 'Pages with broken file links',
- 'categoryviewer-pagedlinks'      => '($1) ($2)',
+ 'categoryviewer-pagedlinks'      => '($1) ($2)', # only translate this message to other languages if you have to change it
  
  'linkprefix' => '/^(.*?)([a-zA-Z\\x80-\\xff]+)$/sD', # only translate this message to other languages if you have to change it
  
@@@ -890,7 -890,6 +890,7 @@@ $1'
  'portal-url'           => 'Project:Community portal',
  'privacy'              => 'Privacy policy',
  'privacypage'          => 'Project:Privacy policy',
 +'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3",
  
  'badaccess'        => 'Permission error',
  'badaccess-group0' => 'You are not allowed to execute the action you have requested.',
@@@ -965,86 -964,90 +965,90 @@@ This might also indicate a bug in the s
  A list of valid special pages can be found at [[Special:SpecialPages|{{int:specialpages}}]].',
  
  # General errors
- 'error'                => 'Error',
- 'databaseerror'        => 'Database error',
- 'dberrortext'          => 'A database query syntax error has occurred.
+ 'error'                         => 'Error',
+ 'databaseerror'                 => 'Database error',
+ 'dberrortext'                   => 'A database query syntax error has occurred.
  This may indicate a bug in the software.
  The last attempted database query was:
  <blockquote><tt>$1</tt></blockquote>
  from within function "<tt>$2</tt>".
  Database returned error "<tt>$3: $4</tt>".',
- 'dberrortextcl'        => 'A database query syntax error has occurred.
+ 'dberrortextcl'                 => 'A database query syntax error has occurred.
  The last attempted database query was:
  "$1"
  from within function "$2".
  Database returned error "$3: $4"',
- 'laggedslavemode'      => "'''Warning:''' Page may not contain recent updates.",
- 'readonly'             => 'Database locked',
- 'enterlockreason'      => 'Enter a reason for the lock, including an estimate of when the lock will be released',
- 'readonlytext'         => 'The database is currently locked to new entries and other modifications, probably for routine database maintenance, after which it will be back to normal.
+ 'laggedslavemode'               => "'''Warning:''' Page may not contain recent updates.",
+ 'readonly'                      => 'Database locked',
+ 'enterlockreason'               => 'Enter a reason for the lock, including an estimate of when the lock will be released',
+ 'readonlytext'                  => 'The database is currently locked to new entries and other modifications, probably for routine database maintenance, after which it will be back to normal.
  
  The administrator who locked it offered this explanation: $1',
- 'missing-article'      => 'The database did not find the text of a page that it should have found, named "$1" $2.
+ 'missing-article'               => 'The database did not find the text of a page that it should have found, named "$1" $2.
  
  This is usually caused by following an outdated diff or history link to a page that has been deleted.
  
  If this is not the case, you may have found a bug in the software.
  Please report this to an [[Special:ListUsers/sysop|administrator]], making note of the URL.',
- 'missingarticle-rev'   => '(revision#: $1)',
- 'missingarticle-diff'  => '(Diff: $1, $2)',
- 'readonly_lag'         => 'The database has been automatically locked while the slave database servers catch up to the master',
- 'internalerror'        => 'Internal error',
- 'internalerror_info'   => 'Internal error: $1',
- 'fileappenderrorread'  => 'Could not read "$1" during append.',
- 'fileappenderror'      => 'Could not append "$1" to "$2".',
- 'filecopyerror'        => 'Could not copy file "$1" to "$2".',
- 'filerenameerror'      => 'Could not rename file "$1" to "$2".',
- 'filedeleteerror'      => 'Could not delete file "$1".',
- 'directorycreateerror' => 'Could not create directory "$1".',
- 'filenotfound'         => 'Could not find file "$1".',
- 'fileexistserror'      => 'Unable to write to file "$1": File exists.',
- 'unexpected'           => 'Unexpected value: "$1"="$2".',
- 'formerror'            => 'Error: Could not submit form.',
- 'badarticleerror'      => 'This action cannot be performed on this page.',
- 'cannotdelete'         => 'The page or file "$1" could not be deleted.
+ 'missingarticle-rev'            => '(revision#: $1)',
+ 'missingarticle-diff'           => '(Diff: $1, $2)',
+ 'readonly_lag'                  => 'The database has been automatically locked while the slave database servers catch up to the master',
+ 'internalerror'                 => 'Internal error',
+ 'internalerror_info'            => 'Internal error: $1',
+ 'fileappenderrorread'           => 'Could not read "$1" during append.',
+ 'fileappenderror'               => 'Could not append "$1" to "$2".',
+ 'filecopyerror'                 => 'Could not copy file "$1" to "$2".',
+ 'filerenameerror'               => 'Could not rename file "$1" to "$2".',
+ 'filedeleteerror'               => 'Could not delete file "$1".',
+ 'directorycreateerror'          => 'Could not create directory "$1".',
+ 'filenotfound'                  => 'Could not find file "$1".',
+ 'fileexistserror'               => 'Unable to write to file "$1": File exists.',
+ 'unexpected'                    => 'Unexpected value: "$1"="$2".',
+ 'formerror'                     => 'Error: Could not submit form.',
+ 'badarticleerror'               => 'This action cannot be performed on this page.',
+ 'cannotdelete'                  => 'The page or file "$1" could not be deleted.
  It may have already been deleted by someone else.',
- 'cannotdelete-title'   => 'Cannot delete page "$1"',
- 'badtitle'             => 'Bad title',
- 'badtitletext'         => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title.
+ 'cannotdelete-title'            => 'Cannot delete page "$1"',
+ 'delete-hook-aborted'           => 'Deletion aborted by hook.
+ It gave no explanation.',
+ 'badtitle'                      => 'Bad title',
+ 'badtitletext'                  => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title.
  It may contain one or more characters which cannot be used in titles.',
- 'perfcached'           => 'The following data is cached and may not be up to date. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.',
- 'perfcachedts'         => 'The following data is cached, and was last updated $1. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.',
- 'querypage-no-updates' => 'Updates for this page are currently disabled.
+ 'perfcached'                    => 'The following data is cached and may not be up to date. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.',
+ 'perfcachedts'                  => 'The following data is cached, and was last updated $1. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.',
+ 'querypage-no-updates'          => 'Updates for this page are currently disabled.
  Data here will not presently be refreshed.',
- 'wrong_wfQuery_params' => 'Incorrect parameters to wfQuery()<br />
+ 'wrong_wfQuery_params'          => 'Incorrect parameters to wfQuery()<br />
  Function: $1<br />
  Query: $2',
- 'viewsource'           => 'View source',
- 'viewsource-title'     => 'View source for $1',
- 'actionthrottled'      => 'Action throttled',
- 'actionthrottledtext'  => 'As an anti-spam measure, you are limited from performing this action too many times in a short space of time, and you have exceeded this limit.
+ 'viewsource'                    => 'View source',
+ 'viewsource-title'              => 'View source for $1',
+ 'actionthrottled'               => 'Action throttled',
+ 'actionthrottledtext'           => 'As an anti-spam measure, you are limited from performing this action too many times in a short space of time, and you have exceeded this limit.
  Please try again in a few minutes.',
- 'protectedpagetext'    => 'This page has been protected to prevent editing.',
- 'viewsourcetext'       => 'You can view and copy the source of this page:',
- 'viewyourtext'         => "You can view and copy the source of '''your edits''' to this page:",
- 'protectedinterface'   => 'This page provides interface text for the software, and is protected to prevent abuse.',
- 'editinginterface'     => "'''Warning:''' You are editing a page which is used to provide interface text for the software.
+ 'protectedpagetext'             => 'This page has been protected to prevent editing.',
+ 'viewsourcetext'                => 'You can view and copy the source of this page:',
+ 'viewyourtext'                  => "You can view and copy the source of '''your edits''' to this page:",
+ 'protectedinterface'            => 'This page provides interface text for the software, and is protected to prevent abuse.',
+ 'editinginterface'              => "'''Warning:''' You are editing a page which is used to provide interface text for the software.
  Changes to this page will affect the appearance of the user interface for other users.
  For translations, please consider using [//translatewiki.net/wiki/Main_Page?setlang=en translatewiki.net], the MediaWiki localisation project.",
- 'sqlhidden'            => '(SQL query hidden)',
- 'cascadeprotected'     => 'This page has been protected from editing, because it is included in the following {{PLURAL:$1|page, which is|pages, which are}} protected with the "cascading" option turned on:
+ 'sqlhidden'                     => '(SQL query hidden)',
+ 'cascadeprotected'              => 'This page has been protected from editing, because it is included in the following {{PLURAL:$1|page, which is|pages, which are}} protected with the "cascading" option turned on:
  $2',
- 'namespaceprotected'   => "You do not have permission to edit pages in the '''$1''' namespace.",
- 'customcssprotected'   => "You do not have permission to edit this CSS page, because it contains another user's personal settings.",
- 'customjsprotected'    => "You do not have permission to edit this JavaScript page, because it contains another user's personal settings.",
- 'ns-specialprotected'  => 'Special pages cannot be edited.',
- 'titleprotected'       => 'This title has been protected from creation by [[User:$1|$1]].
+ 'namespaceprotected'            => "You do not have permission to edit pages in the '''$1''' namespace.",
+ 'customcssprotected'            => "You do not have permission to edit this CSS page, because it contains another user's personal settings.",
+ 'customjsprotected'             => "You do not have permission to edit this JavaScript page, because it contains another user's personal settings.",
+ 'ns-specialprotected'           => 'Special pages cannot be edited.',
+ 'titleprotected'                => 'This title has been protected from creation by [[User:$1|$1]].
  The reason given is "\'\'$2\'\'".',
- 'filereadonlyerror'    => 'Unable to modify the file "$1" because the file repository "$2" is in read-only mode.
+ 'filereadonlyerror'             => 'Unable to modify the file "$1" because the file repository "$2" is in read-only mode.
  
  The administrator who locked it offered this explanation: "$3".',
  'invalidtitle-knownnamespace'   => 'Invalid title with namespace "$2" and text "$3"',
  'invalidtitle-unknownnamespace' => 'Invalid title with unknown namespace number $1 and text "$2"',
+ 'exception-nologin'             => 'Not logged in',
+ 'exception-nologin-text'        => 'This page or action requires you to be logged in on this wiki.',
  
  # Virus scanner
  'virus-badscanner'     => "Bad configuration: Unknown virus scanner: ''$1''",
@@@ -1379,7 -1382,7 +1383,7 @@@ Custom .css and .js pages use a lowerca
  'note'                             => "'''Note:'''",
  'previewnote'                      => "'''Remember that this is only a preview.'''
  Your changes have not yet been saved!",
- 'continue-editing'                 => "Continue editing",
+ 'continue-editing'                 => 'Continue editing',
  'previewconflict'                  => 'This preview reflects the text in the upper text editing area as it will appear if you choose to save.',
  'session_fail_preview'             => "'''Sorry! We could not process your edit due to a loss of session data.'''
  Please try again.
@@@ -1413,7 -1416,7 +1417,7 @@@ If you save it, any changes made since 
  'yourdiff'                         => 'Differences',
  'copyrightwarning'                 => "Please note that all contributions to {{SITENAME}} are considered to be released under the $2 (see $1 for details).
  If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.<br />
 -You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
 +You are also promising us that you wrote this yourself, or copied editpageit from a public domain or similar free resource.
  '''Do not submit copyrighted work without permission!'''",
  'copyrightwarning2'                => "Please note that all contributions to {{SITENAME}} may be edited, altered, or removed by other contributors.
  If you do not want your writing to be edited mercilessly, then do not submit it here.<br />
@@@ -1469,7 -1472,6 +1473,7 @@@ It already exists.'
  'addsection-preload'               => '', # do not translate or duplicate this message to other languages
  'addsection-editintro'             => '', # do not translate or duplicate this message to other languages
  'defaultmessagetext'               => 'Default message text',
 +'invalid-content-data'             => 'Invalid content data',
  
  # Parser/template warnings
  'expensive-parserfunction-warning'        => "'''Warning:''' This page contains too many expensive parser function calls.
@@@ -1666,7 -1668,7 +1670,7 @@@ Note that using the navigation links wi
  'mergehistory-comment'             => 'Merged [[:$1]] into [[:$2]]: $3',
  'mergehistory-same-destination'    => 'Source and destination pages cannot be the same',
  'mergehistory-reason'              => 'Reason:',
- 'mergehistory-revisionrow'         => '$1 ($2) $3 . . $4 $5 $6',
+ 'mergehistory-revisionrow'         => '$1 ($2) $3 . . $4 $5 $6', # only translate this message to other languages if you have to change it
  
  # Merge log
  'mergelog'           => 'Merge log',
@@@ -1983,6 -1985,7 +1987,7 @@@ Your e-mail address is not revealed whe
  'right-writeapi'              => 'Use of the write API',
  'right-delete'                => 'Delete pages',
  'right-bigdelete'             => 'Delete pages with large histories',
+ 'right-deletelogentry'        => 'Delete and undelete specific log entries',
  'right-deleterevision'        => 'Delete and undelete specific revisions of pages',
  'right-deletedhistory'        => 'View deleted history entries, without their associated text',
  'right-deletedtext'           => 'View deleted text and changes between deleted revisions',
@@@ -2293,14 -2296,15 +2298,15 @@@ If the problem persists, contact an [[S
  'backend-fail-read'          => 'Could not read file $1.',
  'backend-fail-create'        => 'Could not write file $1.',
  'backend-fail-maxsize'       => 'Could not write file $1 because it is larger than {{PLURAL:$2|one byte|$2 bytes}}.',
- 'backend-fail-usable'        => 'Could not write file $1 due to insufficient permissions or missing directories/containers.',
  'backend-fail-readonly'      => 'The storage backend "$1" is currently read-only. The reason given is: "\'\'$2\'\'"',
  'backend-fail-synced'        => 'The file "$1" is in an inconsistent state within the internal storage backends',
  'backend-fail-connect'       => 'Could not connect to storage backend "$1".',
  'backend-fail-internal'      => 'An unknown error occurred in storage backend "$1".',
  'backend-fail-contenttype'   => 'Could not determine the content type of the file to store at "$1".',
  'backend-fail-batchsize'     => 'Storage backend given a batch of $1 file {{PLURAL:$1|operation|operations}}; the limit is $2 {{PLURAL:$2|operation|operations}}.',
+ 'backend-fail-usable'        => 'Could not write file $1 due to insufficient permissions or missing directories/containers.',
  
+ # File journal errors
  'filejournal-fail-dbconnect' => 'Could not connect to the journal database for storage backend "$1".',
  'filejournal-fail-dbquery'   => 'Could not update the journal database for storage backend "$1".',
  
@@@ -2694,8 -2698,8 +2700,8 @@@ It may contain one or more characters w
  
  # SpecialCachedPage
  'cachedspecial-viewing-cached-ttl' => 'You are viewing a cached version of this page, which can be up to $1 old.',
- 'cachedspecial-viewing-cached-ts' => 'You are viewing a cached version of this page, which might not be completely actual.',
- 'cachedspecial-refresh-now' => 'View latest.',
+ 'cachedspecial-viewing-cached-ts'  => 'You are viewing a cached version of this page, which might not be completely actual.',
+ 'cachedspecial-refresh-now'        => 'View latest.',
  
  # Special:Categories
  'categories'                    => 'Categories',
@@@ -3060,7 -3064,7 +3066,7 @@@ It may have already been undeleted.'
  $1',
  'undelete-show-file-confirm'   => 'Are you sure you want to view the deleted revision of the file "<nowiki>$1</nowiki>" from $2 at $3?',
  'undelete-show-file-submit'    => 'Yes',
- 'undelete-revisionrow'        => "$1 $2 $3 $4 . . $5 $6 $7",
+ 'undelete-revisionrow'         => '$1 $2 ($3) $4 . . $5 $6 $7', # only translate this message to other languages if you have to change it
  
  # Namespace form on various pages
  'namespace'                     => 'Namespace:',
@@@ -3198,8 -3202,8 +3204,8 @@@ See the [[Special:BlockList|block list]
  'expiringblock'                   => 'expires on $1 at $2',
  'anononlyblock'                   => 'anon. only',
  'noautoblockblock'                => 'autoblock disabled',
- 'createaccountblock'              => 'account creation blocked',
- 'emailblock'                      => 'e-mail blocked',
+ 'createaccountblock'              => 'account creation disabled',
+ 'emailblock'                      => 'e-mail disabled',
  'blocklist-nousertalk'            => 'cannot edit own talk page',
  'ipblocklist-empty'               => 'The block list is empty.',
  'ipblocklist-no-results'          => 'The requested IP address or username is not blocked.',
@@@ -3224,7 -3228,7 +3230,7 @@@ See the [[Special:BlockList|block list]
  'block-log-flags-anononly'        => 'anonymous users only',
  'block-log-flags-nocreate'        => 'account creation disabled',
  'block-log-flags-noautoblock'     => 'autoblock disabled',
- 'block-log-flags-noemail'         => 'e-mail blocked',
+ 'block-log-flags-noemail'         => 'e-mail disabled',
  'block-log-flags-nousertalk'      => 'cannot edit own talk page',
  'block-log-flags-angry-autoblock' => 'enhanced autoblock enabled',
  'block-log-flags-hiddenname'      => 'username hidden',
@@@ -4616,44 -4620,44 +4622,44 @@@ You can also [[Special:EditWatchlist|us
  'duplicate-defaultsort' => '\'\'\'Warning:\'\'\' Default sort key "$2" overrides earlier default sort key "$1".',
  
  # Special:Version
- 'version'                       => 'Version',
- 'version-summary'               => '', # do not translate or duplicate this message to other languages
- 'version-extensions'            => 'Installed extensions',
- 'version-specialpages'          => 'Special pages',
- 'version-parserhooks'           => 'Parser hooks',
- 'version-variables'             => 'Variables',
- 'version-antispam'              => 'Spam prevention',
- 'version-skins'                 => 'Skins',
- 'version-api'                   => 'API', # only translate this message to other languages if you have to change it
- 'version-other'                 => 'Other',
- 'version-mediahandlers'         => 'Media handlers',
- 'version-hooks'                 => 'Hooks',
- 'version-extension-functions'   => 'Extension functions',
- 'version-parser-extensiontags'  => 'Parser extension tags',
- 'version-parser-function-hooks' => 'Parser function hooks',
- 'version-hook-name'             => 'Hook name',
- 'version-hook-subscribedby'     => 'Subscribed by',
- 'version-version'               => '(Version $1)',
- 'version-svn-revision'          => '(r$2)', # only translate this message to other languages if you have to change it
- 'version-license'               => 'License',
- 'version-poweredby-credits'     => "This wiki is powered by '''[//www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
- 'version-poweredby-others'      => 'others',
- 'version-license-info'          => 'MediaWiki 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.
+ 'version'                               => 'Version',
+ 'version-summary'                       => '', # do not translate or duplicate this message to other languages
+ 'version-extensions'                    => 'Installed extensions',
+ 'version-specialpages'                  => 'Special pages',
+ 'version-parserhooks'                   => 'Parser hooks',
+ 'version-variables'                     => 'Variables',
+ 'version-antispam'                      => 'Spam prevention',
+ 'version-skins'                         => 'Skins',
+ 'version-api'                           => 'API', # only translate this message to other languages if you have to change it
+ 'version-other'                         => 'Other',
+ 'version-mediahandlers'                 => 'Media handlers',
+ 'version-hooks'                         => 'Hooks',
+ 'version-extension-functions'           => 'Extension functions',
+ 'version-parser-extensiontags'          => 'Parser extension tags',
+ 'version-parser-function-hooks'         => 'Parser function hooks',
+ 'version-hook-name'                     => 'Hook name',
+ 'version-hook-subscribedby'             => 'Subscribed by',
+ 'version-version'                       => '(Version $1)',
+ 'version-svn-revision'                  => '(r$2)', # only translate this message to other languages if you have to change it
+ 'version-license'                       => 'License',
+ 'version-poweredby-credits'             => "This wiki is powered by '''[//www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
+ 'version-poweredby-others'              => '[{{SERVER}}{{SCRIPTPATH}}/CREDITS others]',
+ 'version-license-info'                  => 'MediaWiki 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.
  
  MediaWiki 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 [{{SERVER}}{{SCRIPTPATH}}/COPYING 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 or [//www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].',
- 'version-software'              => 'Installed software',
- 'version-software-product'      => 'Product',
- 'version-software-version'      => 'Version',
- 'version-entrypoints'           => 'Entry point URLs',
+ 'version-software'                      => 'Installed software',
+ 'version-software-product'              => 'Product',
+ 'version-software-version'              => 'Version',
+ 'version-entrypoints'                   => 'Entry point URLs',
  'version-entrypoints-header-entrypoint' => 'Entry point',
- 'version-entrypoints-header-url' => 'URL',
- 'version-entrypoints-articlepath' => '[https://www.mediawiki.org/wiki/Manual:$wgArticlePath Article path]',
- 'version-entrypoints-scriptpath' => '[https://www.mediawiki.org/wiki/Manual:$wgScriptPath Script path]',
- 'version-entrypoints-index-php' => '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
- 'version-entrypoints-api-php'   => '[https://www.mediawiki.org/wiki/Manual:api.php api.php]',
- 'version-entrypoints-load-php'  => '[https://www.mediawiki.org/wiki/Manual:load.php load.php]',
+ 'version-entrypoints-header-url'        => 'URL',
+ 'version-entrypoints-articlepath'       => '[https://www.mediawiki.org/wiki/Manual:$wgArticlePath Article path]', # only translate this message to other languages if you have to change it
+ 'version-entrypoints-scriptpath'        => '[https://www.mediawiki.org/wiki/Manual:$wgScriptPath Script path]', # only translate this message to other languages if you have to change it
+ 'version-entrypoints-index-php'         => '[https://www.mediawiki.org/wiki/Manual:index.php index.php]', # do not translate or duplicate this message to other languages
+ 'version-entrypoints-api-php'           => '[https://www.mediawiki.org/wiki/Manual:api.php api.php]', # do not translate or duplicate this message to other languages
+ 'version-entrypoints-load-php'          => '[https://www.mediawiki.org/wiki/Manual:load.php load.php]', # do not translate or duplicate this message to other languages
  
  # Special:FilePath
  'filepath'         => 'File path',
@@@ -4841,6 -4845,8 +4847,8 @@@ Otherwise, you can use the easy form be
  'api-error-empty-file'                    => 'The file you submitted was empty.',
  'api-error-emptypage'                     => 'Creating new, empty pages is not allowed.',
  'api-error-fetchfileerror'                => 'Internal error: Something went wrong while fetching the file.',
+ 'api-error-fileexists-forbidden'          => 'A file with name "$1" already exists, and cannot be overwritten.',
+ 'api-error-fileexists-shared-forbidden'   => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.',
  'api-error-file-too-large'                => 'The file you submitted was too large.',
  'api-error-filename-tooshort'             => 'The filename is too short.',
  'api-error-filetype-banned'               => 'This type of file is banned.',
  'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}',
  'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel()
 +'content-model-wikitext' => 'wikitext',
 +'content-model-javascript' => 'JavaScript',
 +'content-model-css' => 'CSS',
 +'content-model-text' => 'plain text',
 +
  );
@@@ -74,6 -74,7 +74,7 @@@
   * @author Mihai
   * @author Mormegil
   * @author Mpradeep
+  * @author Murma174
   * @author Najami
   * @author Nemo bis
   * @author Niels
@@@ -138,7 -139,7 +139,7 @@@ $messages = array
  'tog-hidepatrolled' => 'Option in Recent changes tab of [[Special:Preferences]] (if [[mw:Manual:$wgUseRCPatrol|$wgUseRCPatrol]] is enabled). {{Gender}}',
  'tog-newpageshidepatrolled' => 'Toggle in [[Special:Preferences]], section "Recent changes" (if [[mw:Manual:$wgUseRCPatrol|$wgUseRCPatrol]] is enabled). {{Gender}}',
  'tog-extendwatchlist' => "[[Special:Preferences]], tab 'Watchlist'. Offers user to show all applicable changes in watchlist (by default only the last change to a page on the watchlist is shown). {{Gender}}",
- 'tog-usenewrc' => "[[Special:Preferences]], tab 'Recent changes'. Offers user to use alternative reprsentation of [[Special:RecentChanges]] and watchlist. {{Gender}}",
+ 'tog-usenewrc' => "[[Special:Preferences]], tab 'Recent changes'. Offers user to use alternative representation of [[Special:RecentChanges]] and watchlist. {{Gender}}",
  'tog-numberheadings' => "[[Special:Preferences]], tab 'Misc'. Offers numbered headings on content pages to user. {{Gender}}",
  'tog-showtoolbar' => "[[Special:Preferences]], tab 'Edit'. Offers user to show edit toolbar in page edit screen. {{Gender}}
  
@@@ -168,7 -169,7 +169,7 @@@ Is only shown if {{msg-mw|tog-enotifuse
  'tog-fancysig' => 'In user preferences under the signature box.  {{Gender}}',
  'tog-externaleditor' => "[[Special:Preferences]], tab 'Edit'. Offers user to use an external editor by default. {{Gender}}",
  'tog-externaldiff' => "[[Special:Preferences]], tab 'Edit'. Offers user to use an external diff program by default. {{Gender}}",
- 'tog-showjumplinks' => 'Toggle option used in [[Special:Preferences]]. The "jump to" part should be the same with {{msg-mw|jumpto}} (or you can use <nowiki>{{int:jumpto}}</nowiki>). Thess links are shown in some of the older skins as "jump to: navigation, search" but they are hidden by default (you can enable them with this option). {{Gender}}',
+ 'tog-showjumplinks' => 'Toggle option used in [[Special:Preferences]]. The "jump to" part should be the same with {{msg-mw|jumpto}} (or you can use <nowiki>{{int:jumpto}}</nowiki>). These links are shown in some of the older skins as "jump to: navigation, search" but they are hidden by default (you can enable them with this option). {{Gender}}',
  'tog-uselivepreview' => 'Toggle option used in [[Special:Preferences]]. Live preview is an experimental feature (unavailable by default) to use edit preview without loading the page again. {{Gender}}',
  'tog-forceeditsummary' => "Toggle option used in [[Special:Preferences]] to force an edit ''{{msg-mw|summary}}''. {{Gender}}",
  'tog-watchlisthideown' => "[[Special:Preferences]], tab 'Watchlist'. Offers user to hide own edits from watchlist. {{Gender}}",
@@@ -664,6 -665,7 +665,7 @@@ HTML markup cannot be used
  $1 is a filename, I think.',
  'cannotdelete-title' => 'Title of error page when the user cannot delete a page
  * $1 is the page name',
+ 'delete-hook-aborted' => 'Error message shown when an extension hook prevents a page deletion, but does not provide an error message.',
  'badtitle' => 'The page title when a user requested a page with invalid page name. The content will be {{msg-mw|badtitletext}}.',
  'badtitletext' => 'The message shown when a user requested a page with invalid page name. The page title will be {{msg-mw|badtitle}}.',
  'perfcached' => 'Like {{msg-mw|perfcachedts}} but used when we do not know how long ago page was cached (unlikely to happen). Parameters:
  'invalidtitle-unknownnamespace' => 'Displayed when an invalid title was encountered (generally in a list) and the namespace number is unknown.
  * $1 is the namespace number
  * $2 is the part of the title after the namespace (e.g. SomeName for the page User:SomeName)',
+ 'exception-nologin' => 'Generic page title used on error page when a user is not logged in. Message used by the UserNotLoggedIn exception.',
+ 'exception-nologin-text' => 'Generic reason displayed on error page when a user is not logged in. Message used by the UserNotLoggedIn exception.',
  
  # Login and logout pages
  'logouttext' => 'Log out message',
@@@ -1016,14 -1020,13 +1020,14 @@@ This text will be shown below upload fo
  'sectioneditnotsupported-title' => 'Page title of special page, which presumably appears when someone tries to edit a section, and section editing is disabled. Explanation of section editing on [http://meta.wikimedia.org/wiki/Help:Section_editing#Section_editing meta].',
  'sectioneditnotsupported-text' => 'I think this is the text of an error message, which presumably appears when someone tries to edit a section, and section editing is disabled. Explanation of section editing on [http://meta.wikimedia.org/wiki/Help:Section_editing#Section_editing meta].',
  'permissionserrorstext-withaction' => '* $1 is the number of reasons that were found why the action cannot be performed.
- * $2 is one of the action-* messages (for example {{msg|action-edit}}).
+ * $2 is one of the action-* messages (for example {{msg|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation.
  
  Please report at [[Support]] if you are unable to properly translate this message. Also see [[bugzilla:14246]]',
  'recreate-moveddeleted-warn' => 'Warning shown when creating a page which has already been deleted. See for example [[Test]].',
  'moveddeleted-notice' => 'Shown on top of a deleted page in normal view modus ([http://translatewiki.net/wiki/Test example]).',
  'edit-conflict' => "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.",
  'defaultmessagetext' => 'Caption above the default message text shown on the left-hand side of a diff displayed after clicking “Show changes” when creating a new page in the MediaWiki: namespace',
 +'invalid-content-data'             => 'Error message indicating that the page\'s content can not be saved because it is invalid. This may occurr for some non-text content types.',
  
  # Parser/template warnings
  'expensive-parserfunction-warning' => 'On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g. <code><nowiki>{{#ifexist:}}</nowiki></code>) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded.
@@@ -1051,11 -1054,16 +1055,16 @@@ When templates are expanded, there is 
  * <tt>$2</tt> is the value of the max depth limit',
  'parser-unstrip-loop-warning' => 'This error is shown when a parser extension tag such as &lt;pre> includes a reference to itself in its own output.
  The reference must be to the exact same invocation of the tag at the same location in the source, merely writing &lt;pre>&lt;pre>&lt;/pre>&lt;/pre> will not do it.
- This is usually impossible and unlikely to happen by accident, so translation is not essential.',
+ This is usually impossible and unlikely to happen by accident, so translation is not essential.
+ "Unstrip" refers to the internal function of the parser, called \'unstrip\', which recursively puts the output of parser functions in the place of the parser function call and which would enter an infinite loop in the situation above. See also:
+ *{{msg-mw|Parser-unstrip-recursion-limit}}',
  'parser-unstrip-recursion-limit' => 'This message is shown when the recursion limit for nested parser extension tags is exceeded.
  This warning may be encountered due to input text like &lt;ref>&lt;ref>&lt;ref>...&lt;/ref>&lt;/ref>&lt;/ref>.
  
- * <tt>$1</tt> is the depth limit',
+ * <tt>$1</tt> is the depth limit
+ "Unstrip" refers to the internal function of the parser, called \'unstrip\', which recursively puts the output of parser functions in the place of the parser function call and which would enter an infinite loop in the situation above. See also:
+ *{{msg-mw|Parser-unstrip-loop-warning}}',
  
  # "Undo" feature
  'undo-success' => 'Text on special page to confirm edit revert. You arrive on this page by clicking on the "undo" link on a revision history special page.
@@@ -1120,9 -1128,12 +1129,12 @@@ Used in History and [[Special:Contribut
  * '''$4''' - time.",
  
  # Revision deletion
+ 'rev-deleted-comment' => 'Apparently this can also be about the reason of a log action, not only an edit summary. See also:
+ *{{msg-mw|revdelete-hide-comment}}',
  'rev-deleted-user-contribs' => 'Part of revision deletion.',
  'rev-deleted-text-unhide' => 'This message is very similar to {{msg-mw|rev-suppressed-unhide-diff}}. Parameters:
  * $1 is a HTML link to the diff',
+ 'rev-deleted-text-view' => 'I believe this is an error message which appears if a user tries to view a past revision of a page, where the revision has been hidden from view, although later revisions of the page still exist.',
  'rev-suppressed-unhide-diff' => 'This message is very similar to {{msg-mw|rev-deleted-unhide-diff}} and to {{msg-mw|rev-suppressed-text-unhide}}. Parameters:
  * $1 is a HTML link to the diff',
  'rev-delundel' => 'Link in page history for oversight (see also {{msg-mw|rev-showdeleted}})',
@@@ -1651,6 -1662,15 +1663,15 @@@ If someone with this right (bots by def
  'right-writeapi' => '{{doc-right|writeapi}}',
  'right-delete' => '{{doc-right|delete}}',
  'right-bigdelete' => '{{doc-right|bigdelete}}',
+ 'right-deletelogentry' => '{{doc-right|deletelogentry}}
+ This user right is part of the [[mw:RevisionDelete|RevisionDelete]] feature.
+ It can be given to the group {{msg|group-sysop|pl=yes}}, although this right is disabled by default.
+ See also
+ * {{msg|right-suppressionlog|pl=yes}}
+ * {{msg|right-hideuser|pl=yes}}
+ * {{msg|right-suppressrevision|pl=yes}}
+ * {{msg|right-deleterevision|pl=yes}}',
  'right-deleterevision' => '{{doc-right|deleterevision}}
  This user right is part of the [[mw:RevisionDelete|RevisionDelete]] feature.
  It can be given to the group {{msg|group-sysop|pl=yes}}, although this right is disabled by default.
  See also
  * {{msg|right-suppressionlog|pl=yes}}
  * {{msg|right-hideuser|pl=yes}}
- * {{msg|right-suppressrevision|pl=yes}}',
+ * {{msg|right-suppressrevision|pl=yes}}
+ * {{msg|right-deletelogentry|pl=yes}}',
  'right-deletedhistory' => '{{doc-right|deletedhistory}}',
  'right-deletedtext' => '{{doc-right|deletedtext}}',
  'right-browsearchive' => '{{doc-right|browsearchive}}',
@@@ -1670,6 -1691,7 +1692,7 @@@ It can be given to the group {{msg|grou
  See also
  * {{msg|right-suppressionlog|pl=yes}}
  * {{msg|right-hideuser|pl=yes}}
+ * {{msg|right-deletelogentry|pl=yes}}
  * {{msg|right-deleterevision|pl=yes}}',
  'right-suppressionlog' => '{{doc-right|suppressionlog}}
  This user right is part of the [[mw:RevisionDelete|RevisionDelete]] feature.
@@@ -1678,6 -1700,7 +1701,7 @@@ It can be given to the group {{msg|grou
  See also
  * {{msg|right-suppressrevision|pl=yes}}
  * {{msg|right-hideuser|pl=yes}}
+ * {{msg|right-deletelogentry|pl=yes}}
  * {{msg|right-deleterevision|pl=yes}}',
  'right-block' => '{{doc-right|block}}',
  'right-blockemail' => '{{doc-right|blockemail}}',
@@@ -1688,6 -1711,7 +1712,7 @@@ It can be given to the group {{msg|grou
  See also
  * {{msg|right-suppressionlog|pl=yes}}
  * {{msg|right-suppressrevision|pl=yes}}
+ * {{msg|right-deletelogentry|pl=yes}}
  * {{msg|right-deleterevision|pl=yes}}',
  'right-ipblock-exempt' => '{{doc-right|ipblock-exempt}}
  This user automatically bypasses IP blocks, auto-blocks and range blocks - so I presume - but I am uncertain',
@@@ -1803,10 -1827,10 +1828,10 @@@ Similar to {{msg-mw|wlnote}} which is u
   Example: "\'\'{{int:rcnote/en|50|7||24 January 2008|14:48}}\'\'"',
  'rcnotefrom' => 'This message is displayed at [[Special:RecentChanges]] when viewing recentchanges from some specific time. The corrosponding message is {{msg-mw|Rclistfrom}} (without split of date and time, [[bugzilla:19104|Bug 19104]]).
  
- Parameter $1 is the maximum number of changes that are displayed.
- Parameter $2 is a date and time.
- Parameter $3 is a date.
Parameter $4 is a time.',
Parameter $1 is the maximum number of changes that are displayed.
+ * Parameter $2 is a date and time. (alternative to $3 and $4)
+ * Parameter $3 is a date. (alternative to $1)
* Parameter $4 is a time. (alternative to $1)',
  'rclistfrom' => 'Used on [[Special:RecentChanges]]. Parameter $1 is a link to the revision of a specific date and time. The date and the time are the link description (without split of date and time, [[bugzilla:19104|Bug 19104]]). The corrosponding message is {{msg-mw|Rcnotefrom}}.',
  'rcshowhideminor' => "Option text in [[Special:RecentChanges]]. Parameters:
  * $1 is the 'show/hide' command, with the text taken from either {{msg-mw|show}} or {{msg-mw|hide}}.",
@@@ -3360,7 -3384,12 +3385,12 @@@ See also {{msg-mw|Anonuser}} and {{msg-
  'nocredits' => 'This message is shown when viewing the credits of a page (example: {{fullurl:Main Page|action=credits}}) but when there are no credits available. Note that the credits action is disabled by default (currently enabled on translatewiki.net).',
  
  # Spam protection
- 'spam_reverting' => '{{Identical|Revert}}',
+ 'spam_reverting' => 'Edit summary for spam cleanup script. Used when a page is reverted because all later revisions contained a particular link. Parameters:
+ * $1 is a spammed domain name.',
+ 'spam_blanking' => 'Edit summary for spam cleanup script. Used when a page is blanked (made to have no content, but still exist) because the script could not find an appropriate revision to set the page to. Parameters:
+ * $1 is a spammed domain name.',
+ 'spam_deleting' => 'Edit summary for spam cleanup script. Used when a page is deleted because all revisions contained a particular link. Parameters:
+ * $1 is a spammed domain name.',
  
  # Info page
  'pageinfo-title' => 'Page title for action=info.
@@@ -4711,6 -4740,8 +4741,8 @@@ $4 is the gender of the target user.'
  'api-error-empty-file' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-emptypage' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-fetchfileerror' => 'API error message that can be used for client side localisation of API errors.',
+ 'api-error-fileexists-forbidden' => 'API error message that can be used for client side localisation of API errors.',
+ 'api-error-fileexists-shared-forbidden' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-file-too-large' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-filename-tooshort' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-filetype-banned' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel()
 +'content-model-wikitext' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
 +'content-model-javascript' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
 +'content-model-css' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
 +'content-model-text' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
 +
  );
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /**
-  * Cleanup all spam from a given hostname
+  * Cleanup all spam from a given hostname.
   *
   * 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
  
  require_once( dirname( __FILE__ ) . '/Maintenance.php' );
  
+ /**
+  * Maintenance script to cleanup all spam from a given hostname.
+  *
+  * @ingroup Maintenance
+  */
  class CleanupSpam extends Maintenance {
  
        public function __construct() {
                $rev = Revision::newFromTitle( $title );
                $currentRevId = $rev->getId();
  
 +              //FIXME: LinkFilter needs to handle Content objects! Or rather, ContentHandler needs to provide the appropriate LinkFilter.
                while ( $rev && ( $rev->isDeleted( Revision::DELETED_TEXT ) || LinkFilter::matchEntry( $rev->getText() , $domain ) ) ) {
                        $rev = $rev->getPrevious();
                }
@@@ -1,6 -1,5 +1,6 @@@
  <?php
  /**
 +* @group ContentHandler
  * @group Database
  * ^--- important, causes temporary tables to be used instead of the real database
  **/
@@@ -29,12 -28,10 +29,12 @@@ class WikiPageTest extends MediaWikiLan
                                                       'templatelinks',
                                                       'iwlinks' ) );
        }
 -
 +      
        public function setUp() {
                parent::setUp();
                $this->pages_to_delete = array();
 +
 +              LinkCache::singleton()->clear(); # avoid cached redirect status, etc
        }
  
        public function tearDown() {
                parent::tearDown();
        }
  
 +      /**
 +       * @param Title $title
 +       * @return WikiPage
 +       */
        protected function newPage( $title ) {
                if ( is_string( $title ) ) $title = Title::newFromText( $title );
  
                return $p;
        }
  
 +
 +      /**
 +       * @param String|Title|WikiPage $page
 +       * @param String $text
 +       * @param int $model
 +       *
 +       * @return WikiPage
 +       */
        protected function createPage( $page, $text, $model = null ) {
                if ( is_string( $page ) ) $page = Title::newFromText( $page );
 -              if ( $page instanceof Title ) $page = $this->newPage( $page );
  
 -              $page->doEdit( $text, "testing", EDIT_NEW );
 +              if ( $page instanceof Title ) {
 +                      $title = $page;
 +                      $page = $this->newPage( $page );
 +              } else {
 +                      $title = null;
 +              }
 +
 +              $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
 +              $page->doEditContent( $content, "testing", EDIT_NEW );
  
                return $page;
        }
  
 +      public function testDoEditContent() {
 +              $title = Title::newFromText( "WikiPageTest_testDoEditContent" );
 +
 +              $page = $this->newPage( $title );
 +
 +              $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
 +                                                      . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
 +                                                      $title );
 +
 +              $page->doEditContent( $content, "[[testing]] 1" );
 +
 +              $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
 +              $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
 +              $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
 +              $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
 +
 +              $id = $page->getId();
 +
 +              # ------------------------
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
 +              $n = $res->numRows();
 +              $res->free();
 +
 +              $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
 +
 +              # ------------------------
 +              $page = new WikiPage( $title );
 +
 +              $retrieved = $page->getContent();
 +              $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
 +
 +              # ------------------------
 +              $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
 +                                                      . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
 +                                                      $title );
 +
 +              $page->doEditContent( $content, "testing 2" );
 +
 +              # ------------------------
 +              $page = new WikiPage( $title );
 +
 +              $retrieved = $page->getContent();
 +              $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
 +
 +              # ------------------------
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
 +              $n = $res->numRows();
 +              $res->free();
 +
 +              $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
 +      }
 +      
        public function testDoEdit() {
                $title = Title::newFromText( "WikiPageTest_testDoEdit" );
  
                $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
                       . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.";
  
 -              $page->doEdit( $text, "testing 1" );
 +              $page->doEdit( $text, "[[testing]] 1" );
  
 +              $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
 +              $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
                $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
                $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
  
                $id = $page->getId();
  
 +              # ------------------------
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
 +              $n = $res->numRows();
 +              $res->free();
 +
 +              $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
 +
                # ------------------------
                $page = new WikiPage( $title );
  
                $this->assertEquals( $text, $page->getText() );
        }
  
 +      public function testDoQuickEditContent() {
 +              global $wgUser;
 +
 +              $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text" );
 +
 +              $content = ContentHandler::makeContent( "quick text", $page->getTitle() );
 +              $page->doQuickEditContent( $content, $wgUser, "testing q" );
 +
 +              # ---------------------
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertTrue( $content->equals( $page->getContent() ) );
 +      }
 +      
        public function testDoDeleteArticle() {
                $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" );
                $id = $page->getId();
  
                $page->doDeleteArticle( "testing deletion" );
  
 +              $this->assertFalse( $page->getTitle()->getArticleID() > 0, "Title object should now have page id 0" );
 +              $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
                $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" );
 +              $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" );
                $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" );
  
                $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
                $rev = $page->getRevision();
  
                $this->assertEquals( $page->getLatest(), $rev->getId() );
 -              $this->assertEquals( "some text", $rev->getText() );
 +              $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
 +      }
 +
 +      public function testGetContent() {
 +              $page = $this->newPage( "WikiPageTest_testGetContent" );
 +
 +              $content = $page->getContent();
 +              $this->assertNull( $content );
 +
 +              # -----------------
 +              $this->createPage( $page, "some text" );
 +
 +              $content = $page->getContent();
 +              $this->assertEquals( "some text", $content->getNativeData() );
        }
  
        public function testGetText() {
                $this->assertEquals( "some text", $text );
        }
  
 -      
 +      public function testGetContentModel() {
 +              $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
 +
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
 +      }
 +
 +      public function testGetContentHandler() {
 +              $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
 +
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) );
 +      }
 +
        public function testExists() {
                $page = $this->newPage( "WikiPageTest_testExists" );
                $this->assertFalse( $page->exists() );
        public function testGetRedirectTarget( $title, $text, $target ) {
                $page = $this->createPage( $title, $text );
  
 +              # sanity check, because this test seems to fail for no reason for some people.
 +              $c = $page->getContent();
 +              $this->assertEquals( 'WikitextContent', get_class( $c ) );
 +              
                # now, test the actual redirect
                $t = $page->getRedirectTarget();
                $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
        public function testIsCountable( $title, $text, $mode, $expected ) {
                global $wgArticleCountMethod;
  
 -              $old = $wgArticleCountMethod;
 +              $oldArticleCountMethod = $wgArticleCountMethod;
                $wgArticleCountMethod = $mode;
  
                $page = $this->createPage( $title, $text );
 -              $editInfo = $page->prepareTextForEdit( $page->getText() );
 +              $hasLinks = wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
 +                                      array( 'pl_from' => $page->getId() ), __METHOD__ );
 +
 +              $editInfo = $page->prepareContentForEdit( $page->getContent() );
  
                $v = $page->isCountable();
                $w = $page->isCountable( $editInfo );
 -              $wgArticleCountMethod = $old;
 +
 +              $wgArticleCountMethod = $oldArticleCountMethod;
  
                $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true )
                                                    . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
@@@ -614,19 -478,7 +614,19 @@@ more stuf
                $this->assertEquals( $expected, $text );
        }
  
-       /* @FIXME: fix this!
 +      /**
 +       * @dataProvider dataReplaceSection
 +       */
 +      public function testReplaceSectionContent( $title, $text, $section, $with, $sectionTitle, $expected ) {
 +              $page = $this->createPage( $title, $text );
 +
 +              $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
 +              $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
 +
 +              $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
 +      }
 +      
+       /* @todo FIXME: fix this!
        public function testGetUndoText() {
                global $wgDiff3;
  
  
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
 -              $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
  
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section two", 0, false, $user1 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
  
                $user2 = new User();
                $user2->setName( "127.0.2.13" );
                $text .= "\n\nthree";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section three", 0, false, $user2 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three", 0, false, $user2 );
  
                # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing
                # or not committed under some circumstances. so, make sure the last revision has the right user name.
  
                $page = new WikiPage( $page->getTitle() );
                $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
 -              $this->assertEquals( "one\n\ntwo", $page->getText() );
 +              $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
        }
  
        /**
  
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
 -              $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
                $rev1 = $page->getRevision();
  
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section two", 0, false, $user1 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
  
                # now, try the rollback
                $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
  
                $page = new WikiPage( $page->getTitle() );
                $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
 -              $this->assertEquals( "one", $page->getText() );
 +              $this->assertEquals( "one", $page->getContent()->getNativeData() );
        }
  
        public function dataGetAutosummary( ) {
                        if ( !empty( $edit[1] ) ) $user->setName( $edit[1] );
                        else $user = $wgUser;
  
 -                      $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
 +                      $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
 +
 +                      $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
  
                        $c += 1;
                }
@@@ -1,9 -1,6 +1,9 @@@
  <?php
  
  /**
 + * @group medium
 + * ^---- causes phpunit to use a higher timeout threshold
 + * 
   * @group FileRepo
   * @group FileBackend
   */
@@@ -29,6 -26,9 +29,9 @@@ class FileBackendTest extends MediaWiki
                                        }
                                }
                                $useConfig['name'] = 'localtesting'; // swap name
+                               $useConfig['shardViaHashLevels'] = array( // test sharding
+                                       'unittest-cont1' => array( 'levels' => 1, 'base' => 16, 'repeat' => 1 )
+                               );
                                $class = $useConfig['class'];
                                self::$backendToUse = new $class( $useConfig );
                                $this->singleBackend = self::$backendToUse;
                $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
+               $this->assertBackendPathsConsistent( array( $dest ) );
        }
  
        public function provider_testStore() {
                $cases = array();
  
                $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath();
-               $toPath = $this->baseStorePath() . '/unittest-cont1/fun/obj1.txt';
+               $toPath = $this->baseStorePath() . '/unittest-cont1/e/fun/obj1.txt';
                $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath );
                $cases[] = array(
                        $op, // operation
                $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
+               $this->assertBackendPathsConsistent( array( $source, $dest ) );
        }
  
        public function provider_testCopy() {
                $cases = array();
  
-               $source = $this->baseStorePath() . '/unittest-cont1/file.txt';
-               $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt';
+               $source = $this->baseStorePath() . '/unittest-cont1/e/file.txt';
+               $dest = $this->baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
  
                $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest );
                $cases[] = array(
                        "Source file does not exist accourding to props ($backendName)." );
                $this->assertEquals( true, $props2['fileExists'],
                        "Destination file exists accourding to props ($backendName)." );
+               $this->assertBackendPathsConsistent( array( $source, $dest ) );
        }
  
        public function provider_testMove() {
                $cases = array();
  
-               $source = $this->baseStorePath() . '/unittest-cont1/file.txt';
-               $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt';
+               $source = $this->baseStorePath() . '/unittest-cont1/e/file.txt';
+               $dest = $this->baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
  
                $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest );
                $cases[] = array(
                $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
                $this->assertFalse( $props1['fileExists'],
                        "Source file $source does not exist according to props ($backendName)." );
+               $this->assertBackendPathsConsistent( array( $source ) );
        }
  
        public function provider_testDelete() {
                $cases = array();
  
-               $source = $this->baseStorePath() . '/unittest-cont1/myfacefile.txt';
+               $source = $this->baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
  
                $op = array( 'op' => 'delete', 'src' => $source );
                $cases[] = array(
                                $this->backend->getFileSize( array( 'src' => $dest ) ),
                                "Destination file $dest has original size according to props ($backendName)." );
                }
+               $this->assertBackendPathsConsistent( array( $dest ) );
        }
  
        /**
        public function provider_testCreate() {
                $cases = array();
  
-               $dest = $this->baseStorePath() . '/unittest-cont2/myspacefile.txt';
+               $dest = $this->baseStorePath() . '/unittest-cont2/a/myspacefile.txt';
  
                $op = array( 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest );
                $cases[] = array(
  
                $base = $this->baseStorePath();
                $files = array(
-                       "$base/unittest-cont1/fileA.a",
-                       "$base/unittest-cont1/fileB.a",
-                       "$base/unittest-cont1/fileC.a"
+                       "$base/unittest-cont1/e/fileA.a",
+                       "$base/unittest-cont1/e/fileB.a",
+                       "$base/unittest-cont1/e/fileC.a"
                );
                $ops = array();
                $purgeOps = array();
                $rand = mt_rand( 0, 2000000000 ) . time();
                $dest = wfTempDir() . "/randomfile!$rand.txt";
                $srcs = array(
-                       $this->baseStorePath() . '/unittest-cont1/file1.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file2.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file3.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file4.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file5.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file6.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file7.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file8.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file9.txt',
-                       $this->baseStorePath() . '/unittest-cont1/file10.txt'
+                       $this->baseStorePath() . '/unittest-cont1/e/file1.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file2.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file3.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file4.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file5.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file6.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file7.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file8.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file9.txt',
+                       $this->baseStorePath() . '/unittest-cont1/e/file10.txt'
                );
                $content = array(
                        'egfage',
                $cases = array();
  
                $base = $this->baseStorePath();
-               $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents", true );
-               $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "", true );
-               $cases[] = array( "$base/unittest-cont1/b/some-diff_file.txt", null, false );
+               $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true );
+               $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "", true );
+               $cases[] = array( "$base/unittest-cont1/e/b/some-diff_file.txt", null, false );
  
                return $cases;
        }
                $cases = array();
  
                $base = $this->baseStorePath();
-               $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents" );
-               $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" );
  
                return $cases;
        }
                $cases = array();
  
                $base = $this->baseStorePath();
-               $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" );
-               $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
  
                return $cases;
        }
                $cases = array();
  
                $base = $this->baseStorePath();
-               $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" );
-               $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
  
                return $cases;
        }
        function provider_testPrepareAndClean() {
                $base = $this->baseStorePath();
                return array(
-                       array( "$base/unittest-cont1/a/z/some_file1.txt", true ),
+                       array( "$base/unittest-cont1/e/a/z/some_file1.txt", true ),
                        array( "$base/unittest-cont2/a/z/some_file2.txt", true ),
                        # Specific to FS backend with no basePath field set
                        #array( "$base/unittest-cont3/a/z/some_file3.txt", false ),
  
                $base = $this->baseStorePath();
                $dirs = array(
-                       "$base/unittest-cont1/a",
-                       "$base/unittest-cont1/a/b",
-                       "$base/unittest-cont1/a/b/c",
-                       "$base/unittest-cont1/a/b/c/d0",
-                       "$base/unittest-cont1/a/b/c/d1",
-                       "$base/unittest-cont1/a/b/c/d2",
-                       "$base/unittest-cont1/a/b/c/d0/1",
-                       "$base/unittest-cont1/a/b/c/d0/2",
-                       "$base/unittest-cont1/a/b/c/d1/3",
-                       "$base/unittest-cont1/a/b/c/d1/4",
-                       "$base/unittest-cont1/a/b/c/d2/5",
-                       "$base/unittest-cont1/a/b/c/d2/6"
+                       "$base/unittest-cont1/e/a",
+                       "$base/unittest-cont1/e/a/b",
+                       "$base/unittest-cont1/e/a/b/c",
+                       "$base/unittest-cont1/e/a/b/c/d0",
+                       "$base/unittest-cont1/e/a/b/c/d1",
+                       "$base/unittest-cont1/e/a/b/c/d2",
+                       "$base/unittest-cont1/e/a/b/c/d0/1",
+                       "$base/unittest-cont1/e/a/b/c/d0/2",
+                       "$base/unittest-cont1/e/a/b/c/d1/3",
+                       "$base/unittest-cont1/e/a/b/c/d1/4",
+                       "$base/unittest-cont1/e/a/b/c/d2/5",
+                       "$base/unittest-cont1/e/a/b/c/d2/6"
                );
                foreach ( $dirs as $dir ) {
                        $status = $this->prepare( array( 'dir' => $dir ) );
        private function doTestDoOperations() {
                $base = $this->baseStorePath();
  
-               $fileA = "$base/unittest-cont1/a/b/fileA.txt";
+               $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
                $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
-               $fileB = "$base/unittest-cont1/a/b/fileB.txt";
+               $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
                $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
-               $fileC = "$base/unittest-cont1/a/b/fileC.txt";
+               $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
                $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
-               $fileD = "$base/unittest-cont1/a/b/fileD.txt";
+               $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
  
                $this->prepare( array( 'dir' => dirname( $fileA ) ) );
                $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
                $this->filesToPrune[] = $tmpNameB; # avoid file leaking
                $this->filesToPrune[] = $tmpNameC; # avoid file leaking
  
-               $fileA = "$base/unittest-cont1/a/b/fileA.txt";
-               $fileB = "$base/unittest-cont1/a/b/fileB.txt";
-               $fileC = "$base/unittest-cont1/a/b/fileC.txt";
-               $fileD = "$base/unittest-cont1/a/b/fileD.txt";
+               $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
+               $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
+               $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
+               $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
  
                $this->prepare( array( 'dir' => dirname( $fileA ) ) );
                $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
                $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont-notexists" ) );
  
                $files = array(
-                       "$base/unittest-cont1/test1.txt",
-                       "$base/unittest-cont1/test2.txt",
-                       "$base/unittest-cont1/test3.txt",
-                       "$base/unittest-cont1/subdir1/test1.txt",
-                       "$base/unittest-cont1/subdir1/test2.txt",
-                       "$base/unittest-cont1/subdir2/test3.txt",
-                       "$base/unittest-cont1/subdir2/test4.txt",
-                       "$base/unittest-cont1/subdir2/subdir/test1.txt",
-                       "$base/unittest-cont1/subdir2/subdir/test2.txt",
-                       "$base/unittest-cont1/subdir2/subdir/test3.txt",
-                       "$base/unittest-cont1/subdir2/subdir/test4.txt",
-                       "$base/unittest-cont1/subdir2/subdir/test5.txt",
-                       "$base/unittest-cont1/subdir2/subdir/sub/test0.txt",
-                       "$base/unittest-cont1/subdir2/subdir/sub/120-px-file.txt",
+                       "$base/unittest-cont1/e/test1.txt",
+                       "$base/unittest-cont1/e/test2.txt",
+                       "$base/unittest-cont1/e/test3.txt",
+                       "$base/unittest-cont1/e/subdir1/test1.txt",
+                       "$base/unittest-cont1/e/subdir1/test2.txt",
+                       "$base/unittest-cont1/e/subdir2/test3.txt",
+                       "$base/unittest-cont1/e/subdir2/test4.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/test2.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/test3.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/test4.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/test5.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/sub/test0.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/sub/120-px-file.txt",
                );
  
                // Add the files
  
                // Expected listing
                $expected = array(
-                       "test1.txt",
-                       "test2.txt",
-                       "test3.txt",
-                       "subdir1/test1.txt",
-                       "subdir1/test2.txt",
-                       "subdir2/test3.txt",
-                       "subdir2/test4.txt",
-                       "subdir2/subdir/test1.txt",
-                       "subdir2/subdir/test2.txt",
-                       "subdir2/subdir/test3.txt",
-                       "subdir2/subdir/test4.txt",
-                       "subdir2/subdir/test5.txt",
-                       "subdir2/subdir/sub/test0.txt",
-                       "subdir2/subdir/sub/120-px-file.txt",
+                       "e/test1.txt",
+                       "e/test2.txt",
+                       "e/test3.txt",
+                       "e/subdir1/test1.txt",
+                       "e/subdir1/test2.txt",
+                       "e/subdir2/test3.txt",
+                       "e/subdir2/test4.txt",
+                       "e/subdir2/subdir/test1.txt",
+                       "e/subdir2/subdir/test2.txt",
+                       "e/subdir2/subdir/test3.txt",
+                       "e/subdir2/subdir/test4.txt",
+                       "e/subdir2/subdir/test5.txt",
+                       "e/subdir2/subdir/sub/test0.txt",
+                       "e/subdir2/subdir/sub/120-px-file.txt",
                );
                sort( $expected );
  
  
                // Actual listing (no trailing slash)
                $list = array();
-               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) );
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
  
                // Actual listing (with trailing slash)
                $list = array();
-               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir/" ) );
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir/" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
  
                // Actual listing (top files only)
                $list = array();
-               $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) );
+               $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
  
                $base = $this->baseStorePath();
                $files = array(
-                       "$base/unittest-cont1/test1.txt",
-                       "$base/unittest-cont1/test2.txt",
-                       "$base/unittest-cont1/test3.txt",
-                       "$base/unittest-cont1/subdir1/test1.txt",
-                       "$base/unittest-cont1/subdir1/test2.txt",
-                       "$base/unittest-cont1/subdir2/test3.txt",
-                       "$base/unittest-cont1/subdir2/test4.txt",
-                       "$base/unittest-cont1/subdir2/subdir/test1.txt",
-                       "$base/unittest-cont1/subdir3/subdir/test2.txt",
-                       "$base/unittest-cont1/subdir4/subdir/test3.txt",
-                       "$base/unittest-cont1/subdir4/subdir/test4.txt",
-                       "$base/unittest-cont1/subdir4/subdir/test5.txt",
-                       "$base/unittest-cont1/subdir4/subdir/sub/test0.txt",
-                       "$base/unittest-cont1/subdir4/subdir/sub/120-px-file.txt",
+                       "$base/unittest-cont1/e/test1.txt",
+                       "$base/unittest-cont1/e/test2.txt",
+                       "$base/unittest-cont1/e/test3.txt",
+                       "$base/unittest-cont1/e/subdir1/test1.txt",
+                       "$base/unittest-cont1/e/subdir1/test2.txt",
+                       "$base/unittest-cont1/e/subdir2/test3.txt",
+                       "$base/unittest-cont1/e/subdir2/test4.txt",
+                       "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
+                       "$base/unittest-cont1/e/subdir3/subdir/test2.txt",
+                       "$base/unittest-cont1/e/subdir4/subdir/test3.txt",
+                       "$base/unittest-cont1/e/subdir4/subdir/test4.txt",
+                       "$base/unittest-cont1/e/subdir4/subdir/test5.txt",
+                       "$base/unittest-cont1/e/subdir4/subdir/sub/test0.txt",
+                       "$base/unittest-cont1/e/subdir4/subdir/sub/120-px-file.txt",
                );
  
                // Add the files
                $this->assertEquals( true, $status->isOK(),
                        "Creation of files succeeded with OK status ($backendName)." );
  
+               $this->assertEquals( true,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir1" ) ),
+                       "Directory exists in ($backendName)." );
+               $this->assertEquals( true,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ) ),
+                       "Directory exists in ($backendName)." );
+               $this->assertEquals( false,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/e/subdir2/test1.txt" ) ),
+                       "Directory does not exists in ($backendName)." );
+               // Expected listing
+               $expected = array(
+                       "e",
+               );
+               sort( $expected );
+               // Actual listing (no trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
                // Expected listing
                $expected = array(
                        "subdir1",
                );
                sort( $expected );
  
-               $this->assertEquals( true,
-                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir1" ) ),
-                       "Directory exists in ($backendName)." );
-               $this->assertEquals( true,
-                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) ),
-                       "Directory exists in ($backendName)." );
-               $this->assertEquals( false,
-                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/test1.txt" ) ),
-                       "Directory does not exists in ($backendName)." );
                // Actual listing (no trailing slash)
                $list = array();
-               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) );
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
  
                // Actual listing (with trailing slash)
                $list = array();
-               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
  
                // Actual listing (no trailing slash)
                $list = array();
-               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2" ) );
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
  
                // Actual listing (with trailing slash)
                $list = array();
-               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2/" ) );
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir2/" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
  
                // Expected listing (recursive)
                $expected = array(
-                       "subdir1",
-                       "subdir2",
-                       "subdir3",
-                       "subdir4",
-                       "subdir2/subdir",
-                       "subdir3/subdir",
-                       "subdir4/subdir",
-                       "subdir4/subdir/sub",
+                       "e",
+                       "e/subdir1",
+                       "e/subdir2",
+                       "e/subdir3",
+                       "e/subdir4",
+                       "e/subdir2/subdir",
+                       "e/subdir3/subdir",
+                       "e/subdir4/subdir",
+                       "e/subdir4/subdir/sub",
                );
                sort( $expected );
  
  
                // Actual listing (recursive)
                $list = array();
-               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir4" ) );
+               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/e/subdir4" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
                $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) );
        }
  
+       function assertBackendPathsConsistent( array $paths ) {
+               if ( $this->backend instanceof FileBackendMultiWrite ) {
+                       $status = $this->backend->consistencyCheck( $paths );
+                       $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
+               }
+       }
        function assertGoodStatus( $status, $msg ) {
                $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg );
        }
@@@ -295,18 -295,15 +295,18 @@@ abstract class DumpTestCase extends Med
         * @param $text_sha1 string: the base36 SHA-1 of the revision's text
         * @param $text string|false: (optional) The revision's string, or false to check for a
         *            revision stub
 +       * @param $model String: the expected content model id (default: CONTENT_MODEL_WIKITEXT)
 +       * @param $format String: the expected format model id (default: CONTENT_FORMAT_WIKITEXT)
         * @param $parentid int|false: (optional) id of the parent revision
         */
 -      protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false ) {
 +      protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false,
 +                                              $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT ) {
  
                $this->assertNodeStart( "revision" );
                $this->skipWhitespace();
  
                $this->assertTextNode( "id", $id );
-               if( $parentid ) {
+               if ( $parentid !== false ) {
                        $this->assertTextNode( "parentid", $parentid );
                }
                $this->assertTextNode( "timestamp", false );
                $this->skipWhitespace();
  
                $this->assertTextNode( "comment", $summary );
 +              $this->skipWhitespace();
 +
 +              if ( $this->xml->name == "text" ) {
 +                      // note: <text> tag may occur here or at the very end.
 +                      $text_found = true;
 +                      $this->assertText( $id, $text_id, $text_bytes, $text );
 +              } else {
 +                      $text_found = false;
 +              }
  
                $this->assertTextNode( "sha1", $text_sha1 );
  
 +              $this->assertTextNode( "model", $model );
 +              $this->skipWhitespace();
 +
 +              $this->assertTextNode( "format", $format );
 +              $this->skipWhitespace();
 +
 +              if ( !$text_found ) {
 +                      $this->assertText( $id, $text_id, $text_bytes, $text );
 +              }
 +
 +              $this->assertNodeEnd( "revision" );
 +              $this->skipWhitespace();
 +      }
 +
 +      protected function assertText( $id, $text_id, $text_bytes, $text ) {
                $this->assertNodeStart( "text", false );
                if ( $text_bytes !== false ) {
                        $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
                        $this->assertNodeEnd( "text" );
                        $this->skipWhitespace();
                }
 -
 -              $this->assertNodeEnd( "revision" );
 -              $this->skipWhitespace();
        }
 -
  }
@@@ -199,8 -199,6 +199,8 @@@ class BaseDumpTest extends MediaWikiTes
        <comment>BackupDumperTestP1Summary1</comment>
        <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
        <text xml:space="preserve">BackupDumperTestP1Text1</text>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
      </revision>
    </page>
  ';
      <id>2</id>
      <revision>
        <id>2</id>
-       <parentid>5</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        <comment>BackupDumperTestP2Summary1</comment>
        <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
        <text xml:space="preserve">BackupDumperTestP2Text1</text>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
      </revision>
      <revision>
        <id>5</id>
+       <parentid>2</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        <comment>BackupDumperTestP2Summary4 extra</comment>
        <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
        <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
      </revision>
    </page>
  ';
        </contributor>
        <comment>Talk BackupDumperTestP1 Summary1</comment>
        <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
        <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text>
      </revision>
    </page>
@@@ -114,16 -114,16 +114,16 @@@ class TextPassDumperTest extends DumpTe
                $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
                $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
-                       "BackupDumperTestP2Text1", $this->revId2_2 );
+                       "BackupDumperTestP2Text1" );
                $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                       "BackupDumperTestP2Text2", $this->revId2_3 );
+                       "BackupDumperTestP2Text2", $this->revId2_1 );
                $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                       "BackupDumperTestP2Text3", $this->revId2_4 );
+                       "BackupDumperTestP2Text3", $this->revId2_2 );
                $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
-                       "BackupDumperTestP2Text4 some additional Text" );
+                       "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
                $this->assertPageEnd();
  
                // Page 3
                $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
                $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
-                       "BackupDumperTestP2Text1", $this->revId2_2 );
+                       "BackupDumperTestP2Text1" );
                $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                       "BackupDumperTestP2Text2", $this->revId2_3 );
+                       "BackupDumperTestP2Text2", $this->revId2_1 );
                // Prefetch kicks in. This is still the SHA-1 of the original text,
                // But the actual text (with different SHA-1) comes from prefetch.
                $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                       "Prefetch_________2Text3", $this->revId2_4 );
+                       "Prefetch_________2Text3", $this->revId2_2 );
                $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
-                       "BackupDumperTestP2Text4 some additional Text" );
+                       "BackupDumperTestP2Text4 some additional Text", $this->revId2_3 );
                $this->assertPageEnd();
  
                // Page 3
                                        "BackupDumperTestP2" );
                                $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1",
                                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
-                                       "BackupDumperTestP2Text1", $this->revId2_2 + $i * self::$numOfRevs );
+                                       "BackupDumperTestP2Text1" );
                                $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2",
                                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                                       "BackupDumperTestP2Text2", $this->revId2_3 + $i * self::$numOfRevs );
+                                       "BackupDumperTestP2Text2", $this->revId2_1 + $i * self::$numOfRevs );
                                $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3",
                                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                                       "BackupDumperTestP2Text3", $this->revId2_4 + $i * self::$numOfRevs );
+                                       "BackupDumperTestP2Text3", $this->revId2_2 + $i * self::$numOfRevs );
                                $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs,
                                        "BackupDumperTestP2Summary4 extra",
                                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
-                                       "BackupDumperTestP2Text4 some additional Text" );
+                                       "BackupDumperTestP2Text4 some additional Text",
+                                       $this->revId2_3 + $i * self::$numOfRevs );
                                $this->assertPageEnd();
  
                                $lookingForPage = 4;
                $this->assertEmpty( $files, "Remaining unchecked files" );
  
                // ... and have dealt with more than one checkpoint file
 -              $this->assertGreaterThan( 1, $checkpointFiles, "# of checkpoint files" );
 +              $this->assertGreaterThan( 1, $checkpointFiles, "expected more than 1 checkpoint to have been created. Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" );
  
                $this->expectETAOutput();
        }
         *           file is generated that is automatically removed upon
         *           tearDown.
         * @param $iterations integer: (Optional) specifies how often the block
-        *           of 3 pages should go into the stub file. The page id
-        *           increase further and further, while the revision and text
-        *           ids of the first iteration are reused. The pages of
-        *           iteration > 1 have no corresponding representation in the
+        *           of 3 pages should go into the stub file. The page and
+        *           revision id increase further and further, while the text
+        *           id of the first iteration is reused. The pages and revision
+        *           of iteration > 1 have no corresponding representation in the
         *           database.
         * @return string absolute filename of the stub
         */
        </contributor>
        <comment>BackupDumperTestP1Summary1</comment>
        <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId1_1 . '" bytes="23" />
      </revision>
    </page>
      <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id>
      <revision>
        <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id>
-       <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary1</comment>
        <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_1 . '" bytes="23" />
      </revision>
      <revision>
        <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id>
-       <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid>
+       <parentid>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary2</comment>
        <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_2 . '" bytes="23" />
      </revision>
      <revision>
        <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id>
-       <parentid>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</parentid>
+       <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary3</comment>
        <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_3 . '" bytes="23" />
      </revision>
      <revision>
        <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id>
+       <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary4 extra</comment>
        <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_4 . '" bytes="44" />
      </revision>
    </page>
        </contributor>
        <comment>Talk BackupDumperTestP1 Summary1</comment>
        <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId4_1 . '" bytes="35" />
      </revision>
    </page>