Merge "Pass change tags to NewRevisionFromEditComplete hook"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 19 Feb 2018 22:55:53 +0000 (22:55 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 19 Feb 2018 22:55:54 +0000 (22:55 +0000)
1  2 
docs/hooks.txt
includes/page/WikiPage.php

diff --combined docs/hooks.txt
@@@ -74,7 -74,9 +74,7 @@@ Using a hook-running strategy, we can a
  stuff in our mainline code. Using hooks, the function becomes:
  
        function showAnArticle( $article ) {
 -
                if ( Hooks::run( 'ArticleShow', array( &$article ) ) ) {
 -
                        # code to actually show the article goes here
  
                        Hooks::run( 'ArticleShowComplete', array( &$article ) );
@@@ -689,8 -691,6 +689,8 @@@ $destTitle: destination title (object
  'ArticlePageDataBefore': Before loading data of an article from the database.
  &$wikiPage: WikiPage (object) that data will be loaded
  &$fields: fields (array) to load from the database
 +&$tables: tables (array) to load from the database
 +&$joinConds: join conditions (array) to load from the database
  
  'ArticlePrepareTextForEdit': Called when preparing text to be saved.
  $wikiPage: the WikiPage being saved
@@@ -736,10 -736,7 +736,10 @@@ $current: the reverted revisio
  $create: Whether or not the restoration caused the page to be created (i.e. it
    didn't exist before).
  $comment: The comment associated with the undeletion.
 -$oldPageId: ID of page previously deleted (from archive table)
 +$oldPageId: ID of page previously deleted (from archive table). This ID will be used
 +  for the restored page.
 +$restoredPages: Set of page IDs that have revisions restored for this undelete,
 +  with keys being page IDs and values are 'true'.
  
  'ArticleUndeleteLogEntry': When a log entry is generated but not yet saved.
  $pageArchive: the PageArchive object
@@@ -951,7 -948,7 +951,7 @@@ $id: the page ID (original ID in case o
  in a Category page. Gives extensions the opportunity to batch load any
  related data about the pages.
  $type: The category type. Either 'page', 'file' or 'subcat'
 -$res: Query result from DatabaseBase::select()
 +$res: Query result from Wikimedia\Rdbms\IDatabase::select()
  
  'CategoryViewer::generateLink': Before generating an output link allow
  extensions opportunity to generate a more specific or relevant link.
@@@ -985,9 -982,7 +985,9 @@@ $rows: The data that will be rendered. 
  $unpatrolled: Whether or not we are showing unpatrolled changes.
  $watched: Whether or not the change is watched by the user.
  
 -'ChangesListSpecialPageFilters': Called after building form options on pages
 +'ChangesListSpecialPageFilters': DEPRECATED! Use 'ChangesListSpecialPageStructuredFilters'
 +instead.
 +Called after building form options on pages
  inheriting from ChangesListSpecialPage (in core: RecentChanges,
  RecentChangesLinked and Watchlist).
  $special: ChangesListSpecialPage instance
  'ChangesListSpecialPageQuery': Called when building SQL query on pages
  inheriting from ChangesListSpecialPage (in core: RecentChanges,
  RecentChangesLinked and Watchlist).
 +Do not use this to implement individual filters if they are compatible with the
 +ChangesListFilter and ChangesListFilterGroup structure.
 +Instead, use sub-classes of those classes, in conjunction with the
 +ChangesListSpecialPageStructuredFilters hook.
 +This hook can be used to implement filters that do not implement that structure,
 +or custom behavior that is not an individual filter.
  $name: name of the special page, e.g. 'Watchlist'
  &$tables: array of tables to be queried
  &$fields: array of columns to select
  &$join_conds: join conditions for the tables
  $opts: FormOptions for this request
  
 +'ChangesListSpecialPageStructuredFilters': Called to allow extensions to register
 +filters for pages inheriting from ChangesListSpecialPage (in core: RecentChanges,
 +RecentChangesLinked, and Watchlist).  Generally, you will want to construct
 +new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects.
 +When constructing them, you specify which group they belong to.  You can reuse
 +existing groups (accessed through $special->getFilterGroup), or create your own
 +(ChangesListBooleanFilterGroup or ChangesListStringOptionsFilterGroup).
 +If you create new groups, you must register them with $special->registerFilterGroup.
 +Note that this is called regardless of whether the user is currently using
 +the new (structured) or old (unstructured) filter UI.  If you want your boolean
 +filter to show on both the new and old UI, specify all the supported fields.
 +These include showHide, label, and description.
 +See the constructor of each ChangesList* class for documentation of supported
 +fields.
 +$special: ChangesListSpecialPage instance
 +
  'ChangeTagAfterDelete': Called after a change tag has been deleted (that is,
  removed from all revisions and log entries to which it was applied). This gives
  extensions a chance to take it off their books.
@@@ -1072,13 -1045,6 +1072,13 @@@ $params: tag param
  $rc: RecentChange being tagged when the tagging accompanies the action or null
  $user: User who performed the tagging when the tagging is subsequent to the action or null
  
 +'ChangeTagsAllowedAdd': Called when checking if a user can add tags to a change.
 +&$allowedTags: List of all the tags the user is allowed to add. Any tags the
 +  user wants to add ($addTags) that are not in this array will cause it to fail.
 +  You may add or remove tags to this array as required.
 +$addTags: List of tags user intends to add.
 +$user: User who is adding the tags.
 +
  'ChangeUserGroups': Called before user groups are changed.
  $performer: The User who will perform the change
  $user: The User whose groups will be changed
@@@ -1127,7 -1093,7 +1127,7 @@@ $title: the Title in questio
  a given content model name, but no entry for that model exists in
  $wgContentHandlers.
  Note: if your extension implements additional models via this hook, please
 -use GetContentModels hook to make them known to core. 
 +use GetContentModels hook to make them known to core.
  $modeName: the requested content model name
  &$handler: set this to a ContentHandler object, if desired.
  
@@@ -1157,9 -1123,6 +1157,9 @@@ $page: SpecialPage object for contribut
  &$ret: the HTML line
  $row: the DB row for this line
  &$classes: the classes to add to the surrounding <li>
 +&$attribs: associative array of other HTML attributes for the <li> element.
 +  Currently only data attributes reserved to MediaWiki are allowed
 +  (see Sanitizer::isReservedDataAttribute).
  
  'ContributionsToolLinks': Change tool links above Special:Contributions
  $id: User identifier
@@@ -1205,9 -1168,6 +1205,9 @@@ $page: SpecialPage object for DeletedCo
  &$ret: the HTML line
  $row: the DB row for this line
  &$classes: the classes to add to the surrounding <li>
 +&$attribs: associative array of other HTML attributes for the <li> element.
 +  Currently only data attributes reserved to MediaWiki are allowed
 +  (see Sanitizer::isReservedDataAttribute).
  
  'DifferenceEngineAfterLoadNewText': called in DifferenceEngine::loadNewText()
  after the new revision's content has been loaded into the class member variable
@@@ -1422,18 -1382,15 +1422,18 @@@ textarea in the edit form
  &$buttons: Array of edit buttons "Save", "Preview", "Live", and "Diff"
  &$tabindex: HTML tabindex of the last edit check/button
  
 -'EditPageBeforeEditChecks': Allows modifying the edit checks below the textarea
 -in the edit form.
 +'EditPageBeforeEditChecks': DEPRECATED! Use 'EditPageGetCheckboxesDefinition' instead,
 +or 'EditPage::showStandardInputs:options' if you don't actually care about checkboxes
 +and just want to add some HTML to the page.
 +Allows modifying the edit checks below the textarea in the edit form.
  &$editpage: The current EditPage object
 -&$checks: Array of edit checks like "watch this page"/"minor edit"
 +&$checks: Array of the HTML for edit checks like "watch this page"/"minor edit"
  &$tabindex: HTML tabindex of the last edit check/button
  
  'EditPageBeforeEditToolbar': Allows modifying the edit toolbar above the
  textarea in the edit form.
 -&$toolbar: The toolbar HTMl
 +&$toolbar: The toolbar HTML
 +Hook subscribers can return false to avoid the default toolbar code being loaded.
  
  'EditPageCopyrightWarning': Allow for site and per-namespace customization of
  contribution/copyright notice.
@@@ -1441,12 -1398,6 +1441,12 @@@ $title: title of page being edite
  &$msg: localization message name, overridable. Default is either
    'copyrightwarning' or 'copyrightwarning2'.
  
 +'EditPageGetCheckboxesDefinition': Allows modifying the edit checkboxes
 +below the textarea in the edit form.
 +$editpage: The current EditPage object
 +&$checkboxes: Array of checkbox definitions. See EditPage::getCheckboxesDefinition()
 +for the format.
 +
  'EditPageGetDiffContent': Allow modifying the wikitext that will be used in
  "Show changes". Note that it is preferable to implement diff handling for
  different data types using the ContentHandler facility.
@@@ -1520,9 -1471,6 +1520,9 @@@ $changesList: EnhancedChangesList objec
  $block: An array of RecentChange objects in that block
  $rc: The RecentChange object for this line
  &$classes: An array of classes to change
 +&$attribs: associative array of other HTML attributes for the <tr> element.
 +  Currently only data attributes reserved to MediaWiki are allowed
 +  (see Sanitizer::isReservedDataAttribute).
  
  'EnhancedChangesListModifyBlockLineData': to alter data used to build
  a non-grouped recent change line in EnhancedChangesList.
@@@ -1538,6 -1486,13 +1538,6 @@@ $ip: The ip address of the use
  change the tables headers.
  &$extTypes: associative array of extensions types
  
 -'ExtractThumbParameters': DEPRECATED! Media handler should override
 -MediaHandler::parseParamString instead.
 -Called when extracting thumbnail parameters from a thumbnail file name.
 -$thumbname: the base name of the thumbnail file
 -&$params: the currently extracted params (has source name, temp or archived
 -zone)
 -
  'FetchChangesList': When fetching the ChangesList derivative for a particular
  user.
  $user: User the list is being fetched for
@@@ -1666,13 -1621,6 +1666,13 @@@ $query: query options passed to Title::
  'GetIP': modify the ip of the current user (called only once).
  &$ip: string holding the ip as determined so far
  
 +'GetLangPreferredVariant': Called in LanguageConverter#getPreferredVariant() to
 +  allow fetching the language variant code from cookies or other such
 +  alternative storage.
 +&$req: language variant from the URL (string) or boolean false if no variant
 +  was specified in the URL; the value of this variable comes from
 +  LanguageConverter#getURLVariant()
 +
  'GetLinkColours': modify the CSS class of an array of page links.
  $linkcolour_ids: array of prefixed DB keys of the pages linked to,
    indexed by page_id.
@@@ -1847,11 -1795,6 +1847,11 @@@ $revisionInfo: Array of revision inform
  Return false to stop further processing of the tag
  $reader: XMLReader object
  
 +'ImportHandleUnknownUser': When a user doesn't exist locally, this hook is called
 +to give extensions an opportunity to auto-create it. If the auto-creation is
 +successful, return false.
 +$name: User name
 +
  'ImportHandleUploadXMLTag': When parsing a XML tag in a file upload.
  Return false to stop further processing of the tag
  $reader: XMLReader object
@@@ -2022,16 -1965,6 +2022,16 @@@ $file: the File object or false if brok
  &$attribs: the attributes to be applied
  &$ret: the value to return if your hook returns false
  
 +'LogEventsListLineEnding': Called before a Special:Log line is finished
 +$page: the LogEventsList object
 +&$ret: the HTML line
 +$entry: the DatabaseLogEntry object for this row
 +&$classes: the classes to add to the surrounding <li>
 +&$attribs: associative array of other HTML attributes for the <li> element.
 +  Currently only data attributes reserved to MediaWiki are allowed
 +  (see Sanitizer::isReservedDataAttribute).
 +
 +
  'HtmlPageLinkRendererBegin':
  Used when generating internal and interwiki links in
  LinkRenderer, before processing starts.  Return false to skip default
@@@ -2317,32 -2250,20 +2317,33 @@@ $title: the diff page title (nullable
  $old: the ?old= param value from the url
  $new: the ?new= param value from the url
  
 +'NewPagesLineEnding': Called before a NewPages line is finished.
 +$page: the SpecialNewPages object
 +&$ret: the HTML line
 +$row: the database row for this page (the recentchanges record and a few extras - see
 +  NewPagesPager::getQueryInfo)
 +&$classes: the classes to add to the surrounding <li>
 +&$attribs: associative array of other HTML attributes for the <li> element.
 +  Currently only data attributes reserved to MediaWiki are allowed
 +  (see Sanitizer::isReservedDataAttribute).
 +
  'NewRevisionFromEditComplete': Called when a revision was inserted due to an
  edit.
  $wikiPage: the WikiPage edited
  $rev: the new revision
  $baseID: the revision ID this was based off, if any
  $user: the editing user
+ &$tags: tags to apply to the edit and recent change
  
  'OldChangesListRecentChangesLine': Customize entire recent changes line, or
  return false to omit the line from RecentChanges and Watchlist special pages.
  &$changeslist: The OldChangesList instance.
  &$s: HTML of the form "<li>...</li>" containing one RC entry.
  $rc: The RecentChange object.
 -&$classes: array of css classes for the <li> element
 +&$classes: array of css classes for the <li> element.
 +&$attribs: associative array of other HTML attributes for the <li> element.
 +  Currently only data attributes reserved to MediaWiki are allowed
 +  (see Sanitizer::isReservedDataAttribute).
  
  'OpenSearchUrls': Called when constructing the OpenSearch description XML. Hooks
  can alter or append to the array of URLs for search & suggestion formats.
@@@ -2359,10 -2280,6 +2360,10 @@@ $page: the Page that was rendered
  $title: the Title of the rendered page.
  $parserOutput: ParserOutput resulting from rendering the page.
  
 +'OtherAutoblockLogLink': Get links to the autoblock log from extensions which
 +autoblocks users and/or IP addresses too.
 +&$otherBlockLink: An array with links to other autoblock logs
 +
  'OtherBlockLogLink': Get links to the block log from extensions which blocks
  users and/or IP addresses too.
  &$otherBlockLink: An array with links to other block logs
@@@ -2450,9 -2367,6 +2451,9 @@@ $historyAction: the action objec
  &$row: the revision row for this line
  &$s: the string representing this parsed line
  &$classes: array containing the <li> element classes
 +&$attribs: associative array of other HTML attributes for the <li> element.
 +  Currently only data attributes reserved to MediaWiki are allowed
 +  (see Sanitizer::isReservedDataAttribute).
  
  'PageHistoryPager::doBatchLookups': Called after the pager query was run, before
  any output is generated, to allow batch lookups for prefetching information
@@@ -2466,8 -2380,7 +2467,8 @@@ constructed
  &$pager: the pager
  &$queryInfo: the query parameters
  
 -'PageRenderingHash': Alter the parser cache option hash key. A parser extension
 +'PageRenderingHash': NOTE: Consider using ParserOptionsRegister instead.
 +Alter the parser cache option hash key. A parser extension
  which depends on user options should install this hook and append its values to
  the key.
  &$confstr: reference to a hash key string which can be modified
@@@ -2591,22 -2504,6 +2592,22 @@@ $file: file object that will be used t
  &$params: 2-D array of parameters
  $parser: Parser object that called the hook
  
 +'ParserOptionsRegister': Register additional parser options. Note that if you
 +change the default value for an option, all existing parser cache entries will
 +be invalid. To avoid bugs, you'll need to handle that somehow (e.g. with the
 +RejectParserCacheValue hook) because MediaWiki won't do it for you.
 +&$defaults: Set the default value for your option here.
 +&$inCacheKey: To fragment the parser cache on your option, set a truthy value here.
 +&$lazyLoad: To lazy-initialize your option, set it null in $defaults and set a
 +  callable here. The callable is passed the ParserOptions object and the option
 +  name.
 +
 +'ParserOutputPostCacheTransform': Called from ParserOutput::getText() to do
 +post-cache transforms.
 +$parserOutput: The ParserOutput object.
 +&$text: The text being transformed, before core transformations are done.
 +&$options: The options array being used for the transformation.
 +
  'ParserSectionCreate': Called each time the parser creates a document section
  from wikitext. Use this to apply per-section modifications to HTML (like
  wrapping the section in a DIV).  Caveat: DIVs are valid wikitext, and a DIV
@@@ -2679,7 -2576,6 +2680,7 @@@ $formData: array of user submitted dat
  $form: PreferencesForm object, also a ContextSource
  $user: User object with preferences to be saved set
  &$result: boolean indicating success
 +$oldUserOptions: array with user old options (before save)
  
  'PreferencesGetLegend': Override the text used for the <legend> of a
  preferences section.
@@@ -2738,11 -2634,6 +2739,11 @@@ random pages
  'RecentChange_save': Called at the end of RecentChange::save().
  &$recentChange: RecentChange object
  
 +'RecentChangesPurgeRows': Called when old recentchanges rows are purged, after
 +deleting those rows but within the same transaction.
 +$rows: The deleted rows as an array of recentchanges row objects (with up to
 +  $wgUpdateRowsPerQuery items).
 +
  '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.
@@@ -2786,19 -2677,11 +2787,19 @@@ configuration variables to JavaScript. 
  or request state must be added through MakeGlobalVariablesScript instead.
  &$vars: array( variable name => value )
  
 -'ResourceLoaderGetLessVars': Called in ResourceLoader::getLessVars after
 -variables from $wgResourceLoaderLESSVars are added. Can be used to add
 -context-based variables.
 +'ResourceLoaderGetLessVars': DEPRECATED! Called in ResourceLoader::getLessVars
 +to add global LESS variables. Loaded after $wgResourceLoaderLESSVars is added.
 +Global LESS variables are deprecated. Use ResourceLoaderModule::getLessVars()
 +instead to expose variables only in modules that need them.
  &$lessVars: array of variables already added
  
 +'ResourceLoaderJqueryMsgModuleMagicWords': Called in
 +ResourceLoaderJqueryMsgModule to allow adding magic words for jQueryMsg.
 +The value should be a string, and they can depend only on the
 +ResourceLoaderContext.
 +$context: ResourceLoaderContext
 +&$magicWords: Associative array mapping all-caps magic word to a string value
 +
  'ResourceLoaderRegisterModules': Right before modules information is required,
  such as when responding to a resource
  loader request or generating HTML output.
@@@ -2817,14 -2700,14 +2818,14 @@@ called after the addition of 'qunit' an
    added to any module.
  &$ResourceLoader: object
  
 -'RevisionInsertComplete': Called after a revision is inserted into the database.
 -&$revision: the Revision
 -$data: the data stored in old_text.  The meaning depends on $flags: if external
 -  is set, it's the URL of the revision text in external storage; otherwise,
 -  it's the revision text itself.  In either case, if gzip is set, the revision
 -  text is gzipped.
 -$flags: a comma-delimited list of strings representing the options used.  May
 -  include: utf8 (this will always be set for new revisions); gzip; external.
 +'RevisionRecordInserted': Called after a revision is inserted into the database.
 +$revisionRecord: the RevisionRecord that has just been inserted.
 +
 +'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
 +Called after a revision is inserted into the database.
 +$revision: the Revision
 +$data: DEPRECATED! Always null!
 +$flags: DEPRECATED! Always null!
  
  'SearchableNamespaces': An option to modify which namespaces are searchable.
  &$arr: Array of namespaces ($nsId => $name) which will be used.
@@@ -2960,7 -2843,6 +2961,7 @@@ $result: The SearchResult objec
  $terms: String of the search terms entered
  $specialSearch: The SpecialSearch object
  &$query: Array of query string parameters for the link representing the search result.
 +&$attributes: Array of title link attributes, can be modified by extension.
  
  'SidebarBeforeOutput': Allows to edit sidebar just before it is output by skins.
  Warning: This hook is run on each display. You should consider to use
@@@ -3140,7 -3022,7 +3141,7 @@@ UsersPager::formatRow()
  &$item: HTML to be returned. Will be wrapped in <li></li> after the hook finishes
  $row: Database row object
  
 -'SpecialListusersHeader': Called before closing the <fieldset> in
 +'SpecialListusersHeader': Called after adding the submit button in
  UsersPager::getPageHeader().
  $pager: The UsersPager instance
  &$out: The header HTML
@@@ -3214,7 -3096,7 +3215,7 @@@ use this to change some selection crite
  &$title: If the hook returns false, a Title object to use instead of the
    result from the normal query
  
 -'SpecialRecentChangesFilters': DEPRECATED! Use ChangesListSpecialPageFilters
 +'SpecialRecentChangesFilters': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
  instead.
  Called after building form options at RecentChanges.
  $special: the special page object
@@@ -3227,8 -3109,8 +3228,8 @@@ SpecialRecentChanges
  &$extraOpts: array of added items, to which can be added
  $opts: FormOptions for this request
  
 -'SpecialRecentChangesQuery': DEPRECATED! Use ChangesListSpecialPageQuery
 -instead.
 +'SpecialRecentChangesQuery': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
 +or ChangesListSpecialPageQuery instead.
  Called when building SQL query for SpecialRecentChanges and
  SpecialRecentChangesLinked.
  &$conds: array of WHERE conditionals for query
@@@ -3259,10 -3141,8 +3260,10 @@@ $term: The string the user searched fo
  $title: The title the 'go' feature has decided to forward the user to
  &$url: Initially null, hook subscribers can set this to specify the final url to redirect to
  
 -'SpecialSearchNogomatch': Called when user clicked the "Go" button but the
 -target doesn't exist.
 +'SpecialSearchNogomatch': Called when the 'Go' feature is triggered (generally
 +from autocomplete search other than the main bar on Special:Search) and the
 +target doesn't exist. Full text search results are generated after this hook is
 +called.
  &$title: title object generated from the text entered by the user
  
  'SpecialSearchPowerBox': The equivalent of SpecialSearchProfileForm for
@@@ -3332,7 -3212,7 +3333,7 @@@ Special:Upload
  $wgVersion: Current $wgVersion for you to use
  &$versionUrl: Raw url to link to (eg: release notes)
  
 -'SpecialWatchlistFilters': DEPRECATED! Use ChangesListSpecialPageFilters
 +'SpecialWatchlistFilters': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
  instead.
  Called after building form options at Watchlist.
  $special: the special page object
@@@ -3345,8 -3225,7 +3346,8 @@@ SpecialWatchlist. Allows extensions to 
  inserted to rc_type so they can be returned as part of the watchlist.
  &$nonRevisionTypes: array of values in the rc_type field of recentchanges table
  
 -'SpecialWatchlistQuery': DEPRECATED! Use ChangesListSpecialPageQuery instead.
 +'SpecialWatchlistQuery': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
 +or ChangesListSpecialPageQuery instead.
  Called when building sql query for SpecialWatchlist.
  &$conds: array of WHERE conditionals for query
  &$tables: array of tables to be queried
@@@ -3482,14 -3361,6 +3483,14 @@@ $title: Title object of the page that w
  $title: title object related to the revision
  $rev: revision (object) that will be viewed
  
 +'UnitTestsAfterDatabaseSetup': Called right after MediaWiki's test infrastructure
 +has finished creating/duplicating core tables for unit tests.
 +$database: Database in question
 +$prefix: Table prefix to be used in unit tests
 +
 +'UnitTestsBeforeDatabaseTeardown': Called right before MediaWiki tears down its
 +database infrastructure used for unit tests.
 +
  'UnitTestsList': Called when building a list of paths containing PHPUnit tests.
  Since 1.24: Paths pointing to a directory will be recursively scanned for
  test case files matching the suffix "Test.php".
@@@ -3528,12 -3399,6 +3529,12 @@@ blank form with no error message; use U
  instead.
  &$form: UploadForm object
  
 +'UploadForm:getInitialPageText': After the initial page text for file uploads
 +is generated, to allow it to be altered.
 +&$pageText: the page text
 +$msg: array of header messages
 +$config: Config object
 +
  'UploadForm:initial': Before the upload form is generated. You might set the
  member-variables $uploadFormTextTop and $uploadFormTextAfterSummary to inject
  text (HTML) either before or after the editform.
@@@ -3682,10 -3547,6 +3683,10 @@@ $removed: Groups remove
  $performer: User who performed the change, false if via autopromotion
  $reason: The reason, if any, given by the user performing the change,
  false if via autopromotion.
 +$oldUGMs: An associative array (group name => UserGroupMembership object) of
 +the user's group memberships before the change.
 +$newUGMs: An associative array (group name => UserGroupMembership object) of
 +the user's current group memberships.
  
  'UserIsBlockedFrom': Check if a user is blocked from a specific page (for
  specific block exemptions).
@@@ -3816,16 -3677,12 +3817,16 @@@ After a user's group memberships are ch
  $add: Array of strings corresponding to groups added
  $remove: Array of strings corresponding to groups removed
  
 -'UserSaveOptions': Called just before saving user preferences/options.
 -$user: User object
 -&$options: Options, modifiable
 +'UserSaveOptions': Called just before saving user preferences. Hook handlers can either add or
 +manipulate options, or reset one back to it's default to block changing it. Hook handlers are also
 +allowed to abort the process by returning false, e.g. to save to a global profile instead. Compare
 +to the UserSaveSettings hook, which is called after the preferences have been saved.
 +$user: The User for which the options are going to be saved
 +&$options: The users options as an associative array, modifiable
  
 -'UserSaveSettings': Called when saving user settings.
 -$user: User object
 +'UserSaveSettings': Called directly after user preferences (user_properties in the database) have
 +been saved. Compare to the UserSaveOptions hook, which is called before.
 +$user: The User for which the options have been saved
  
  'UserSetCookies': DEPRECATED! If you're trying to replace core session cookie
  handling, you want to create a subclass of MediaWiki\Session\CookieSessionProvider
@@@ -3934,15 -3791,14 +3935,15 @@@ dumps. One, and only one hook should se
  &$opts: Options to use for the query
  &$join: Join conditions
  
 -'WikiPageDeletionUpdates': manipulate the list of DataUpdates to be applied when
 +'WikiPageDeletionUpdates': manipulate the list of DeferrableUpdates 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 Content's
  getDeletionUpdates() method.
  $page: the WikiPage
 -$content: the Content to generate updates for (or null, if the Content could not be loaded
 -due to an error)
 -&$updates: the array of DataUpdate objects. Hook function may want to add to it.
 +$content: the Content to generate updates for, or null in case the page revision could not be
 +  loaded. The delete will succeed despite this.
 +&$updates: the array of objects that implement DeferrableUpdate. Hook function may want to add to
 +  it.
  
  'WikiPageFactory': Override WikiPage class used for a title
  $title: Title of the page
   * @file
   */
  
 -use \MediaWiki\Logger\LoggerFactory;
 -use \MediaWiki\MediaWikiServices;
 +use MediaWiki\Edit\PreparedEdit;
 +use MediaWiki\Logger\LoggerFactory;
 +use MediaWiki\MediaWikiServices;
 +use Wikimedia\Assert\Assert;
 +use Wikimedia\Rdbms\FakeResultWrapper;
 +use Wikimedia\Rdbms\IDatabase;
 +use Wikimedia\Rdbms\DBError;
 +use Wikimedia\Rdbms\DBUnexpectedError;
  
  /**
   * Class representing a MediaWiki article and history.
@@@ -51,7 -45,7 +51,7 @@@ class WikiPage implements Page, IDBAcce
        public $mLatest = false;             // !< Integer (false means "not loaded")
        /**@}}*/
  
 -      /** @var stdClass Map of cache fields (text, parser output, ect) for a proposed/new edit */
 +      /** @var PreparedEdit Map of cache fields (text, parser output, ect) for a proposed/new edit */
        public $mPreparedEdit = false;
  
        /**
         */
        protected $mLinksUpdated = '19700101000000';
  
 -      const PURGE_CDN_CACHE = 1; // purge CDN cache for page variant URLs
 -      const PURGE_CLUSTER_PCACHE = 2; // purge parser cache in the local datacenter
 -      const PURGE_GLOBAL_PCACHE = 4; // set page_touched to clear parser cache in all datacenters
 -      const PURGE_ALL = 7;
 -
        /**
         * Constructor and clear the article
         * @param Title $title Reference to a Title object.
         * @return WikiPage|null
         */
        public static function newFromID( $id, $from = 'fromdb' ) {
 -              // page id's are never 0 or negative, see bug 61166
 +              // page ids are never 0 or negative, see T63166
                if ( $id < 1 ) {
                        return null;
                }
  
                $from = self::convertSelectType( $from );
                $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
 +              $pageQuery = self::getQueryInfo();
                $row = $db->selectRow(
 -                      'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
 +                      $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__,
 +                      [], $pageQuery['joins']
 +              );
                if ( !$row ) {
                        return null;
                }
         */
        private static function convertSelectType( $type ) {
                switch ( $type ) {
 -              case 'fromdb':
 -                      return self::READ_NORMAL;
 -              case 'fromdbmaster':
 -                      return self::READ_LATEST;
 -              case 'forupdate':
 -                      return self::READ_LOCKING;
 -              default:
 -                      // It may already be an integer or whatever else
 -                      return $type;
 +                      case 'fromdb':
 +                              return self::READ_NORMAL;
 +                      case 'fromdbmaster':
 +                              return self::READ_LATEST;
 +                      case 'forupdate':
 +                              return self::READ_LOCKING;
 +                      default:
 +                              // It may already be an integer or whatever else
 +                              return $type;
                }
        }
  
         * @todo Move this UI stuff somewhere else
         *
         * @see ContentHandler::getActionOverrides
 +       * @return array
         */
        public function getActionOverrides() {
                return $this->getContentHandler()->getActionOverrides();
                $this->mTimestamp = '';
                $this->mIsRedirect = false;
                $this->mLatest = false;
 -              // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
 +              // T59026: do not clear mPreparedEdit since prepareTextForEdit() already checks
                // the requested rev ID and content against the cached one for equality. For most
                // content types, the output should not change during the lifetime of this cache.
                // Clearing it can cause extra parses on edit for no reason.
         * Return the list of revision fields that should be selected to create
         * a new page.
         *
 +       * @deprecated since 1.31, use self::getQueryInfo() instead.
         * @return array
         */
        public static function selectFields() {
                global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
  
 +              wfDeprecated( __METHOD__, '1.31' );
 +
                $fields = [
                        'page_id',
                        'page_namespace',
                return $fields;
        }
  
 +      /**
 +       * Return the tables, fields, and join conditions to be selected to create
 +       * a new page object.
 +       * @since 1.31
 +       * @return array With three keys:
 +       *   - tables: (string[]) to include in the `$table` to `IDatabase->select()`
 +       *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
 +       *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
 +       */
 +      public static function getQueryInfo() {
 +              global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
 +
 +              $ret = [
 +                      'tables' => [ 'page' ],
 +                      'fields' => [
 +                              'page_id',
 +                              'page_namespace',
 +                              'page_title',
 +                              'page_restrictions',
 +                              'page_is_redirect',
 +                              'page_is_new',
 +                              'page_random',
 +                              'page_touched',
 +                              'page_links_updated',
 +                              'page_latest',
 +                              'page_len',
 +                      ],
 +                      'joins' => [],
 +              ];
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $ret['fields'][] = 'page_content_model';
 +              }
 +
 +              if ( $wgPageLanguageUseDB ) {
 +                      $ret['fields'][] = 'page_lang';
 +              }
 +
 +              return $ret;
 +      }
 +
        /**
         * Fetch a page record with the given conditions
         * @param IDatabase $dbr
         * @return object|bool Database result resource, or false on failure
         */
        protected function pageData( $dbr, $conditions, $options = [] ) {
 -              $fields = self::selectFields();
 +              $pageQuery = self::getQueryInfo();
  
                // Avoid PHP 7.1 warning of passing $this by reference
                $wikiPage = $this;
  
 -              Hooks::run( 'ArticlePageDataBefore', [ &$wikiPage, &$fields ] );
 +              Hooks::run( 'ArticlePageDataBefore', [
 +                      &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins']
 +              ] );
  
 -              $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
 +              $row = $dbr->selectRow(
 +                      $pageQuery['tables'],
 +                      $pageQuery['fields'],
 +                      $conditions,
 +                      __METHOD__,
 +                      $options,
 +                      $pageQuery['joins']
 +              );
  
                Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
  
  
                if ( is_int( $from ) ) {
                        list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
 -                      $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
 +                      $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
 +                      $db = $loadBalancer->getConnection( $index );
 +                      $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
  
                        if ( !$data
                                && $index == DB_REPLICA
 -                              && wfGetLB()->getServerCount() > 1
 -                              && wfGetLB()->hasOrMadeRecentMasterChanges()
 +                              && $loadBalancer->getServerCount() > 1
 +                              && $loadBalancer->hasOrMadeRecentMasterChanges()
                        ) {
                                $from = self::READ_LATEST;
                                list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
 -                              $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
 +                              $db = $loadBalancer->getConnection( $index );
 +                              $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
                        }
                } else {
                        // No idea from where the caller got this data, assume replica DB.
                        $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
                        $this->mIsRedirect = intval( $data->page_is_redirect );
                        $this->mLatest = intval( $data->page_latest );
 -                      // Bug 37225: $latest may no longer match the cached latest Revision object.
 +                      // T39225: $latest may no longer match the cached latest Revision object.
                        // Double-check the ID of any cached latest Revision object for consistency.
                        if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
                                $this->mLastRevision = null;
                        $cache = ObjectCache::getMainWANInstance();
  
                        return $cache->getWithSetCallback(
 -                              $cache->makeKey( 'page', 'content-model', $this->getLatest() ),
 +                              $cache->makeKey( 'page-content-model', $this->getLatest() ),
                                $cache::TTL_MONTH,
                                function () {
                                        $rev = $this->getRevision();
         * @return Revision|null
         */
        public function getOldestRevision() {
 -
                // Try using the replica DB first, then try the master
 -              $continue = 2;
 -              $db = wfGetDB( DB_REPLICA );
 -              $revSelectFields = Revision::selectFields();
 -
 -              $row = null;
 -              while ( $continue ) {
 -                      $row = $db->selectRow(
 -                              [ 'revision' ],
 -                              $revSelectFields,
 -                              [
 -                                      'rev_page' => $this->getId()
 -                              ],
 -                              __METHOD__,
 -                              [
 -                                      'ORDER BY' => 'rev_timestamp ASC'
 -                              ]
 -                      );
 -
 -                      if ( $row ) {
 -                              $continue = 0;
 -                      } else {
 -                              $db = wfGetDB( DB_MASTER );
 -                              $continue--;
 -                      }
 +              $rev = $this->mTitle->getFirstRevision();
 +              if ( !$rev ) {
 +                      $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE );
                }
 -
 -              return $row ? Revision::newFromRow( $row ) : null;
 +              return $rev;
        }
  
        /**
                }
  
                if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
 -                      // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always
 +                      // T39225: 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
                        $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
                } else {
                        $dbr = wfGetDB( DB_REPLICA );
 -                      $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
 +                      $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
                }
  
                if ( $revision ) { // sanity
         * Determine whether a page would be suitable for being counted as an
         * article in the site_stats table based on the title & its content
         *
 -       * @param object|bool $editInfo (false): object returned by prepareTextForEdit(),
 +       * @param PreparedEdit|bool $editInfo (false): object returned by prepareTextForEdit(),
         *   if false, the current database state will be used
         * @return bool
         */
                }
  
                // Update the DB post-send if the page has not cached since now
 -              $that = $this;
                $latest = $this->getLatest();
                DeferredUpdates::addCallableUpdate(
 -                      function () use ( $that, $retval, $latest ) {
 -                              $that->insertRedirectEntry( $retval, $latest );
 +                      function () use ( $retval, $latest ) {
 +                              $this->insertRedirectEntry( $retval, $latest );
                        },
                        DeferredUpdates::POSTSEND,
                        wfGetDB( DB_MASTER )
  
                $dbr = wfGetDB( DB_REPLICA );
  
 -              if ( $dbr->implicitGroupby() ) {
 -                      $realNameField = 'user_real_name';
 -              } else {
 -                      $realNameField = 'MIN(user_real_name) AS user_real_name';
 -              }
 -
                $tables = [ 'revision', 'user' ];
  
                $fields = [
                        'user_id' => 'rev_user',
                        'user_name' => 'rev_user_text',
 -                      $realNameField,
 +                      'user_real_name' => 'MIN(user_real_name)',
                        'timestamp' => 'MAX(rev_timestamp)',
                ];
  
         *
         * @since 1.19
         * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
 -       * @param null|int      $oldid Revision ID to get the text from, passing null or 0 will
 -       *                             get the current revision (default value)
 -       * @param bool          $forceParse Force reindexing, regardless of cache settings
 +       * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
 +       *   get the current revision (default value)
 +       * @param bool $forceParse Force reindexing, regardless of cache settings
         * @return bool|ParserOutput ParserOutput or false if the revision was not found
         */
        public function getParserOutput(
        ) {
                $useParserCache =
                        ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
 +
 +              if ( $useParserCache && !$parserOptions->isSafeToCache() ) {
 +                      throw new InvalidArgumentException(
 +                              'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
 +                      );
 +              }
 +
                wfDebug( __METHOD__ .
                        ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
                if ( $parserOptions->getStubThreshold() ) {
                }
  
                if ( $useParserCache ) {
 -                      $parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
 +                      $parserOutput = MediaWikiServices::getInstance()->getParserCache()
 +                              ->get( $this, $parserOptions );
                        if ( $parserOutput !== false ) {
                                return $parserOutput;
                        }
  
        /**
         * Perform the actions of a page purging
 -       * @param integer $flags Bitfield of WikiPage::PURGE_* constants
         * @return bool
 +       * @note In 1.28 (and only 1.28), this took a $flags parameter that
 +       *  controlled how much purging was done.
         */
 -      public function doPurge( $flags = self::PURGE_ALL ) {
 +      public function doPurge() {
                // Avoid PHP 7.1 warning of passing $this by reference
                $wikiPage = $this;
  
                        return false;
                }
  
 -              if ( ( $flags & self::PURGE_GLOBAL_PCACHE ) == self::PURGE_GLOBAL_PCACHE ) {
 -                      // Set page_touched in the database to invalidate all DC caches
 -                      $this->mTitle->invalidateCache();
 -              } elseif ( ( $flags & self::PURGE_CLUSTER_PCACHE ) == self::PURGE_CLUSTER_PCACHE ) {
 -                      // Delete the parser options key in the local cluster to invalidate the DC cache
 -                      ParserCache::singleton()->deleteOptionsKey( $this );
 -                      // Avoid sending HTTP 304s in ViewAction to the client who just issued the purge
 -                      $cache = ObjectCache::getLocalClusterInstance();
 -                      $cache->set(
 -                              $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ),
 -                              wfTimestamp( TS_MW ),
 -                              $cache::TTL_HOUR
 -                      );
 -              }
 +              $this->mTitle->invalidateCache();
  
 -              if ( ( $flags & self::PURGE_CDN_CACHE ) == self::PURGE_CDN_CACHE ) {
 -                      // Clear any HTML file cache
 -                      HTMLFileCache::clearFileCache( $this->getTitle() );
 -                      // Send purge after any page_touched above update was committed
 -                      DeferredUpdates::addUpdate(
 -                              new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
 -                              DeferredUpdates::PRESEND
 -                      );
 -              }
 +              // Clear file cache
 +              HTMLFileCache::clearFileCache( $this->getTitle() );
 +              // Send purge after above page_touched update was committed
 +              DeferredUpdates::addUpdate(
 +                      new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
 +                      DeferredUpdates::PRESEND
 +              );
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        $messageCache = MessageCache::singleton();
                return true;
        }
  
 -      /**
 -       * Get the last time a user explicitly purged the page via action=purge
 -       *
 -       * @return string|bool TS_MW timestamp or false
 -       * @since 1.28
 -       */
 -      public function getLastPurgeTimestamp() {
 -              $cache = ObjectCache::getLocalClusterInstance();
 -
 -              return $cache->get( $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ) );
 -      }
 -
        /**
         * Insert a new empty page record for this article.
         * This *must* be followed up by creating a revision
         *   page ID is already in use.
         */
        public function insertOn( $dbw, $pageId = null ) {
 -              $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
 +              $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : [];
                $dbw->insert(
                        'page',
                        [
 -                              'page_id'           => $pageIdForInsert,
                                'page_namespace'    => $this->mTitle->getNamespace(),
                                'page_title'        => $this->mTitle->getDBkey(),
                                'page_restrictions' => '',
                                'page_touched'      => $dbw->timestamp(),
                                'page_latest'       => 0, // Fill this in shortly...
                                'page_len'          => 0, // Fill this in shortly...
 -                      ],
 +                      ] + $pageIdForInsert,
                        __METHOD__,
                        'IGNORE'
                );
  
                if ( $dbw->affectedRows() > 0 ) {
 -                      $newid = $pageId ?: $dbw->insertId();
 +                      $newid = $pageId ? (int)$pageId : $dbw->insertId();
                        $this->mId = $newid;
                        $this->mTitle->resetArticleID( $newid );
  
                        $conditions['page_latest'] = $lastRevision;
                }
  
 +              $revId = $revision->getId();
 +              Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
 +
                $row = [ /* SET */
 -                      'page_latest'      => $revision->getId(),
 +                      'page_latest'      => $revId,
                        'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
                        'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
                        'page_is_redirect' => $rt !== null ? 1 : 0,
         * Add row to the redirect table if this is a redirect, remove otherwise.
         *
         * @param IDatabase $dbw
 -       * @param Title $redirectTitle Title object pointing to the redirect target,
 +       * @param Title|null $redirectTitle Title object pointing to the redirect target,
         *   or NULL if this is not a redirect
         * @param null|bool $lastRevIsRedirect If given, will optimize adding and
         *   removing rows in redirect table.
         * @return bool
         */
        public function updateIfNewerOn( $dbw, $revision ) {
 -
                $row = $dbw->selectRow(
                        [ 'revision', 'page' ],
                        [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
        public function replaceSectionContent(
                $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
        ) {
 -
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
 -                      $dbr = wfGetDB( DB_REPLICA );
 +                      $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
 +                      $dbr = $lb->getConnection( DB_REPLICA );
                        $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
                        // Try the master if this thread may have just added it.
                        // This could be abstracted into a Revision method, but we don't want
                        // to encourage loading of revisions by timestamp.
                        if ( !$rev
 -                              && wfGetLB()->getServerCount() > 1
 -                              && wfGetLB()->hasOrMadeRecentMasterChanges()
 +                              && $lb->getServerCount() > 1
 +                              && $lb->hasOrMadeRecentMasterChanges()
                        ) {
 -                              $dbw = wfGetDB( DB_MASTER );
 +                              $dbw = $lb->getConnection( DB_MASTER );
                                $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
                        }
                        if ( $rev ) {
        public function replaceSectionAtRev( $sectionId, Content $sectionContent,
                $sectionTitle = '', $baseRevId = null
        ) {
 -
                if ( strval( $sectionId ) === '' ) {
                        // Whole-page edit; let the whole text through
                        $newContent = $sectionContent;
                                        $this->getContentHandler()->getModelID() );
                        }
  
 -                      // Bug 30711: always use current version when adding a new section
 +                      // T32711: always use current version when adding a new section
                        if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
                                $oldContent = $this->getContent();
                        } else {
                $old_revision = $this->getRevision(); // current revision
                $old_content = $this->getContent( Revision::RAW ); // current revision's content
  
 -              if ( $old_content && $old_content->getModel() !== $content->getModel() ) {
 -                      $tags[] = 'mw-contentmodelchange';
 +              $handler = $content->getContentHandler();
 +              $tag = $handler->getChangeTag( $old_content, $content, $flags );
 +              // If there is no applicable tag, null is returned, so we need to check
 +              if ( $tag ) {
 +                      $tags[] = $tag;
 +              }
 +
 +              // Check for undo tag
 +              if ( $undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
 +                      $tags[] = 'mw-undo';
                }
  
 -              // Provide autosummaries if one is not provided and autosummaries are enabled
 +              // Provide autosummaries if summary is not provided and autosummaries are enabled
                if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
 -                      $handler = $content->getContentHandler();
                        $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
  
                $meta = [
                        'bot' => ( $flags & EDIT_FORCE_BOT ),
                        'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
 -                      'serialized' => $editInfo->pst,
 +                      'serialized' => $pstContent->serialize( $serialFormat ),
                        'serialFormat' => $serialFormat,
                        'baseRevId' => $baseRevId,
                        'oldRevision' => $old_revision,
  
        /**
         * @param Content $content Pre-save transform content
 -       * @param integer $flags
 +       * @param int $flags
         * @param User $user
         * @param string $summary
         * @param array $meta
                // Convenience variables
                $now = wfTimestampNow();
                $oldid = $meta['oldId'];
 -              /** @var $oldContent Content|null */
 +              /** @var Content|null $oldContent */
                $oldContent = $meta['oldContent'];
                $newsize = $content->getSize();
  
  
                        return $status;
                } elseif ( !$oldContent ) {
 -                      // Sanity check for bug 37225
 +                      // Sanity check for T39225
                        throw new MWException( "Could not find text for current revision {$oldid}." );
                }
  
 -              // @TODO: pass content object?!
 -              $revision = new Revision( [
 -                      'page'       => $this->getId(),
 -                      'title'      => $this->mTitle, // for determining the default content model
 -                      'comment'    => $summary,
 -                      'minor_edit' => $meta['minor'],
 -                      'text'       => $meta['serialized'],
 -                      'len'        => $newsize,
 -                      'parent_id'  => $oldid,
 -                      'user'       => $user->getId(),
 -                      'user_text'  => $user->getName(),
 -                      'timestamp'  => $now,
 -                      'content_model' => $content->getModel(),
 -                      'content_format' => $meta['serialFormat'],
 -              ] );
 -
                $changed = !$content->equals( $oldContent );
  
                $dbw = wfGetDB( DB_MASTER );
  
                if ( $changed ) {
 +                      // @TODO: pass content object?!
 +                      $revision = new Revision( [
 +                              'page'       => $this->getId(),
 +                              'title'      => $this->mTitle, // for determining the default content model
 +                              'comment'    => $summary,
 +                              'minor_edit' => $meta['minor'],
 +                              'text'       => $meta['serialized'],
 +                              'len'        => $newsize,
 +                              'parent_id'  => $oldid,
 +                              'user'       => $user->getId(),
 +                              'user_text'  => $user->getName(),
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $meta['serialFormat'],
 +                      ] );
 +
                        $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
                        $status->merge( $prepStatus );
                        if ( !$status->isOK() ) {
                                throw new MWException( "Failed to update page row to use new revision." );
                        }
  
+                       $tags = $meta['tags'];
                        Hooks::run( 'NewRevisionFromEditComplete',
-                               [ $this, $revision, $meta['baseRevId'], $user ] );
+                               [ $this, $revision, $meta['baseRevId'], $user, &$tags ] );
  
                        // Update recentchanges
                        if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
                                        $newsize,
                                        $revisionId,
                                        $patrolled,
-                                       $meta['tags']
+                                       $tags
                                );
                        }
  
                        $dbw->endAtomic( __METHOD__ );
                        $this->mTimestamp = $now;
                } else {
 -                      // Bug 32948: revision ID must be set to page {{REVISIONID}} and
 +                      // T34948: revision ID must be set to page {{REVISIONID}} and
                        // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
 -                      $revision->setId( $this->getLatest() );
 -                      $revision->setUserIdAndName(
 -                              $this->getUser( Revision::RAW ),
 -                              $this->getUserText( Revision::RAW )
 -                      );
 +                      // Since we don't insert a new revision into the database, the least
 +                      // error-prone way is to reuse given old revision.
 +                      $revision = $meta['oldRevision'];
                }
  
                if ( $changed ) {
  
        /**
         * @param Content $content Pre-save transform content
 -       * @param integer $flags
 +       * @param int $flags
         * @param User $user
         * @param string $summary
         * @param array $meta
                                        $wikiPage = $this;
                                        // Trigger post-create hook
                                        $params = [ &$wikiPage, &$user, $content, $summary,
 -                                              $flags & EDIT_MINOR, null, null, &$flags, $revision ];
 +                                                              $flags & EDIT_MINOR, null, null, &$flags, $revision ];
                                        Hooks::run( 'PageContentInsertComplete', $params );
                                        // Trigger post-save hook
 -                                      $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
 +                                      $params = array_merge( $params, [ &$status, $meta['baseRevId'], 0 ] );
                                        Hooks::run( 'PageContentSaveComplete', $params );
                                }
                        ),
  
        /**
         * Prepare content which is about to be saved.
 -       * Returns a stdClass with source, pst and output members
 +       *
 +       * Prior to 1.30, this returned a stdClass object with the same class
 +       * members.
         *
         * @param Content $content
         * @param Revision|int|null $revision Revision object. For backwards compatibility, a
         * @param string|null $serialFormat
         * @param bool $useCache Check shared prepared edit cache
         *
 -       * @return object
 +       * @return PreparedEdit
         *
         * @since 1.21
         */
                        // This code path is deprecated, and nothing is known to
                        // use it, so performance here shouldn't be a worry.
                        if ( $revid !== null ) {
 +                              wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
                                $revision = Revision::newFromId( $revid, Revision::READ_LATEST );
                        } else {
                                $revision = null;
                $user = is_null( $user ) ? $wgUser : $user;
                // XXX: check $user->getId() here???
  
 -              // Use a sane default for $serialFormat, see bug 57026
 +              // Use a sane default for $serialFormat, see T59026
                if ( $serialFormat === null ) {
                        $serialFormat = $content->getContentHandler()->getDefaultFormat();
                }
                $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
                Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
  
 -              $edit = (object)[];
 +              $edit = new PreparedEdit();
                if ( $cachedEdit ) {
                        $edit->timestamp = $cachedEdit->timestamp;
                } else {
                $edit->newContent = $content;
                $edit->oldContent = $this->getContent( Revision::RAW );
  
 -              // NOTE: B/C for hooks! don't use these fields!
 -              $edit->newText = $edit->newContent
 -                      ? ContentHandler::getContentText( $edit->newContent )
 -                      : '';
 -              $edit->oldText = $edit->oldContent
 -                      ? ContentHandler::getContentText( $edit->oldContent )
 -                      : '';
 -              $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
 -
                if ( $edit->output ) {
                        $edit->output->setCacheTime( wfTimestampNow() );
                }
         * @param Revision $revision
         * @param User $user User object that did the revision
         * @param array $options Array of options, following indexes are used:
 -       * - changed: boolean, whether the revision changed the content (default true)
 -       * - created: boolean, whether the revision created the page (default false)
 -       * - moved: boolean, whether the page was moved (default false)
 -       * - restored: boolean, whether the page was undeleted (default false)
 +       * - changed: bool, whether the revision changed the content (default true)
 +       * - created: bool, whether the revision created the page (default false)
 +       * - moved: bool, whether the page was moved (default false)
 +       * - restored: bool, whether the page was undeleted (default false)
         * - oldrevision: Revision object for the pre-update revision (default null)
 -       * - oldcountable: boolean, null, or string 'no-change' (default null):
 -       *   - boolean: whether the page was counted as an article before that
 +       * - oldcountable: bool, null, or string 'no-change' (default null):
 +       *   - bool: whether the page was counted as an article before that
         *     revision, only used in changed is true and created is false
         *   - null: if created is false, don't update the article count; if created
         *     is true, do update the article count
  
                // Save it to the parser cache.
                // Make sure the cache time matches page_touched to avoid double parsing.
 -              ParserCache::singleton()->save(
 +              MediaWikiServices::getInstance()->getParserCache()->save(
                        $editInfo->output, $this, $editInfo->popts,
                        $revision->getTimestamp(), $editInfo->revid
                );
  
                // Update the links tables and other secondary data
                if ( $content ) {
 -                      $recursive = $options['changed']; // bug 50785
 +                      $recursive = $options['changed']; // T52785
                        $updates = $content->getSecondaryDataUpdates(
                                $this->getTitle(), null, $recursive, $editInfo->output
                        );
                        foreach ( $updates as $update ) {
 +                              $update->setCause( 'edit-page', $user->getName() );
                                if ( $update instanceof LinksUpdate ) {
                                        $update->setRevision( $revision );
                                        $update->setTriggeringUser( $user );
                        $good = 0;
                }
                $edits = $options['changed'] ? 1 : 0;
 -              $total = $options['created'] ? 1 : 0;
 +              $pages = $options['created'] ? 1 : 0;
  
 -              DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
 +              DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
 +                      [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
 +              ) );
                DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
  
                // If this is another user's talk page, update newtalk.
  
                if ( $options['created'] ) {
                        self::onArticleCreate( $this->mTitle );
 -              } elseif ( $options['changed'] ) { // bug 50785
 +              } elseif ( $options['changed'] ) { // T52785
                        self::onArticleEdit( $this->mTitle, $revision );
                }
  
        public function doUpdateRestrictions( array $limit, array $expiry,
                &$cascade, $reason, User $user, $tags = null
        ) {
 -              global $wgCascadingRestrictionLevels, $wgContLang;
 +              global $wgCascadingRestrictionLevels;
  
                if ( wfReadOnly() ) {
 -                      return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
 +                      return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
                }
  
                $this->loadPageData( 'fromdbmaster' );
                        $logAction = 'protect';
                }
  
 -              // Truncate for whole multibyte characters
 -              $reason = $wgContLang->truncate( $reason, 255 );
 -
                $logRelationsValues = [];
                $logRelationsField = null;
                $logParamsDetails = [];
                                        $dbw->insert(
                                                'page_restrictions',
                                                [
 -                                                      'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
                                                        'pr_page' => $id,
                                                        'pr_type' => $action,
                                                        'pr_level' => $restrictions,
                        $cascade = false;
  
                        if ( $limit['create'] != '' ) {
 +                              $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason );
                                $dbw->replace( 'protected_titles',
                                        [ [ 'pt_namespace', 'pt_title' ] ],
                                        [
                                                'pt_timestamp' => $dbw->timestamp(),
                                                'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
                                                'pt_user' => $user->getId(),
 -                                              'pt_reason' => $reason,
 -                                      ], __METHOD__
 +                                      ] + $commentFields, __METHOD__
                                );
                                $logParamsDetails[] = [
                                        'type' => 'create',
         * @param array|string &$error Array of errors to append to
         * @param User $user The deleting user
         * @param array $tags Tags to apply to the deletion action
 +       * @param string $logsubtype
         * @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
                $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
                $tags = [], $logsubtype = 'delete'
        ) {
 -              global $wgUser, $wgContentHandlerUseDB;
 +              global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage;
  
                wfDebug( __METHOD__ . "\n" );
  
                        $content = null;
                }
  
 -              $fields = Revision::selectFields();
 +              $commentStore = CommentStore::getStore();
 +
 +              $revQuery = Revision::getQueryInfo();
                $bitfield = false;
  
                // Bitfields to further suppress the content
                if ( $suppress ) {
                        $bitfield = Revision::SUPPRESSED_ALL;
 -                      $fields = array_diff( $fields, [ 'rev_deleted' ] );
 +                      $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
                }
  
                // For now, shunt the revision data into the archive table.
  
                // Get all of the page revisions
                $res = $dbw->select(
 -                      'revision',
 -                      $fields,
 +                      $revQuery['tables'],
 +                      $revQuery['fields'],
                        [ 'rev_page' => $id ],
                        __METHOD__,
 -                      'FOR UPDATE'
 +                      'FOR UPDATE',
 +                      $revQuery['joins']
                );
 +
                // Build their equivalent archive rows
                $rowsInsert = [];
 +              $revids = [];
 +
 +              /** @var int[] Revision IDs of edits that were made by IPs */
 +              $ipRevIds = [];
 +
                foreach ( $res as $row ) {
 +                      $comment = $commentStore->getComment( 'rev_comment', $row );
                        $rowInsert = [
                                'ar_namespace'  => $namespace,
                                'ar_title'      => $dbKey,
 -                              'ar_comment'    => $row->rev_comment,
                                'ar_user'       => $row->rev_user,
                                'ar_user_text'  => $row->rev_user_text,
                                'ar_timestamp'  => $row->rev_timestamp,
                                'ar_page_id'    => $id,
                                'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
                                'ar_sha1'       => $row->rev_sha1,
 -                      ];
 +                      ] + $commentStore->insert( $dbw, 'ar_comment', $comment );
                        if ( $wgContentHandlerUseDB ) {
                                $rowInsert['ar_content_model'] = $row->rev_content_model;
                                $rowInsert['ar_content_format'] = $row->rev_content_format;
                        }
                        $rowsInsert[] = $rowInsert;
 +                      $revids[] = $row->rev_id;
 +
 +                      // Keep track of IP edits, so that the corresponding rows can
 +                      // be deleted in the ip_changes table.
 +                      if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
 +                              $ipRevIds[] = $row->rev_id;
 +                      }
                }
                // Copy them into the archive table
                $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
                // Now that it's safely backed up, delete it
                $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
                $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
 +              if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
 +                      $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
 +              }
 +
 +              // Also delete records from ip_changes as applicable.
 +              if ( count( $ipRevIds ) > 0 ) {
 +                      $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
 +              }
  
                // Log the deletion, if the page was suppressed, put it in the suppression log instead
                $logtype = $suppress ? 'suppress' : 'delete';
  
                $dbw->onTransactionPreCommitOrIdle(
                        function () use ( $dbw, $logEntry, $logid ) {
 -                              // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
 +                              // T58776: avoid deadlocks (especially from FileDeleteForm)
                                $logEntry->publish( $logid );
                        },
                        __METHOD__
  
                $dbw->endAtomic( __METHOD__ );
  
 -              $this->doDeleteUpdates( $id, $content, $revision );
 +              $this->doDeleteUpdates( $id, $content, $revision, $user );
  
                Hooks::run( 'ArticleDeleteComplete', [
                        &$wikiPageBeforeDelete,
                $status->value = $logid;
  
                // Show log excerpt on 404 pages rather than just a link
 -              $cache = ObjectCache::getMainStashInstance();
 -              $key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
 +              $cache = MediaWikiServices::getInstance()->getMainObjectStash();
 +              $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
                $cache->set( $key, 1, $cache::TTL_DAY );
  
                return $status;
        /**
         * Lock the page row for this title+id and return page_latest (or 0)
         *
 -       * @return integer Returns 0 if no row was found with this title+id
 +       * @return int Returns 0 if no row was found with this title+id
         * @since 1.27
         */
        public function lockAndGetLatest() {
         *   the required updates. This may be needed because $this->getContent()
         *   may already return null when the page proper was deleted.
         * @param Revision|null $revision The latest page revision
 +       * @param User|null $user The user that caused the deletion
         */
 -      public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
 +      public function doDeleteUpdates(
 +              $id, Content $content = null, Revision $revision = null, User $user = null
 +      ) {
                try {
                        $countable = $this->isCountable();
                } catch ( Exception $ex ) {
                }
  
                // Update site status
 -              DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
 +              DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
 +                      [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ]
 +              ) );
  
                // Delete pagelinks, update secondary indexes, etc
                $updates = $this->getDeletionUpdates( $content );
                        DeferredUpdates::addUpdate( $update );
                }
  
 +              $causeAgent = $user ? $user->getName() : 'unknown';
                // Reparse any pages transcluding this page
 -              LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
 -
 +              LinksUpdate::queueRecursiveJobsForTable(
 +                      $this->mTitle, 'templatelinks', 'delete-page', $causeAgent );
                // Reparse any pages including this image
                if ( $this->mTitle->getNamespace() == NS_FILE ) {
 -                      LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
 +                      LinksUpdate::queueRecursiveJobsForTable(
 +                              $this->mTitle, 'imagelinks', 'delete-page', $causeAgent );
                }
  
                // Clear caches
 -              WikiPage::onArticleDelete( $this->mTitle );
 +              self::onArticleDelete( $this->mTitle );
                ResourceLoaderWikiModule::invalidateModuleCache(
                        $this->mTitle, $revision, null, wfWikiID()
                );
         * @param string $token Rollback token.
         * @param bool $bot If true, mark all reverted edits as bot.
         *
 -       * @param array $resultDetails Array contains result-specific array of additional values
 +       * @param array &$resultDetails Array contains result-specific array of additional values
         *    'alreadyrolled' : 'current' (rev)
         *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
         *
         * @param string $summary Custom summary. Set to default summary if empty.
         * @param bool $bot If true, mark all reverted edits as bot.
         *
 -       * @param array $resultDetails Contains result-specific array of additional values
 +       * @param array &$resultDetails Contains result-specific array of additional values
         * @param User $guser The user performing the rollback
         * @param array|null $tags Change tags to apply to the rollback
         * Callers are responsible for permission checks
                // Trim spaces on user supplied text
                $summary = trim( $summary );
  
 -              // Truncate for whole multibyte characters.
 -              $summary = $wgContLang->truncate( $summary, 255 );
 -
                // Save
                $flags = EDIT_UPDATE | EDIT_INTERNAL;
  
                $targetContent = $target->getContent();
                $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
  
 +              if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
 +                      $tags[] = 'mw-rollback';
 +              }
 +
                // Actually store the edit
                $status = $this->doEditContent(
                        $targetContent,
                );
  
                // Set patrolling and bot flag on the edits, which gets rollbacked.
 -              // This is done even on edit failure to have patrolling in that case (bug 62157).
 +              // This is done even on edit failure to have patrolling in that case (T64157).
                $set = [];
                if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
                        // Mark all reverted edits as bot
                        'summary' => $summary,
                        'current' => $current,
                        'target' => $target,
 -                      'newid' => $revId
 +                      'newid' => $revId,
 +                      'tags' => $tags
                ];
  
                return [];
  
                MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
  
 +              // Invalidate caches of articles which include this page
 +              DeferredUpdates::addUpdate(
 +                      new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' )
 +              );
 +
                if ( $title->getNamespace() == NS_CATEGORY ) {
                        // Load the Category object, which will schedule a job to create
                        // the category table row if necessary. Checking a replica DB is ok
         */
        public static function onArticleDelete( Title $title ) {
                // Update existence markers on article/talk tabs...
 +              // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
 +              BacklinkCache::get( $title )->clear();
                $other = $title->getOtherPage();
  
                $other->purgeSquid();
  
                // Images
                if ( $title->getNamespace() == NS_FILE ) {
 -                      DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
 +                      DeferredUpdates::addUpdate(
 +                              new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' )
 +                      );
                }
  
                // User talk pages
         */
        public static function onArticleEdit( Title $title, Revision $revision = null ) {
                // Invalidate caches of articles which include this page
 -              DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) );
 +              DeferredUpdates::addUpdate(
 +                      new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
 +              );
  
                // Invalidate the caches of all pages which redirect here
 -              DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
 +              DeferredUpdates::addUpdate(
 +                      new HTMLCacheUpdate( $title, 'redirect', 'page-edit' )
 +              );
  
                MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
  
                HTMLFileCache::clearFileCache( $title );
  
                $revid = $revision ? $revision->getId() : null;
 -              DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) {
 +              DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
                        InfoAction::invalidateCache( $title, $revid );
                } );
        }
         *
         * @param array $added The names of categories that were added
         * @param array $deleted The names of categories that were deleted
 -       * @param integer $id Page ID (this should be the original deleted page ID)
 +       * @param int $id Page ID (this should be the original deleted page ID)
         */
        public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
                $id = $id ?: $this->getId();
                        );
                        foreach ( $rows as $row ) {
                                $cat = Category::newFromRow( $row );
 -                              $cat->refreshCounts();
 +                              // T166757: do the update after this DB commit
 +                              DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
 +                                      $cat->refreshCounts();
 +                              } );
                        }
                }
        }
                return $this->getTitle()->getCanonicalURL();
        }
  
 -      /*
 +      /**
         * @param WANObjectCache $cache
         * @return string[]
         * @since 1.28