Merge "title: Allow passing MessageLocalizer to newMainPage()"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sun, 14 Apr 2019 02:52:22 +0000 (02:52 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sun, 14 Apr 2019 02:52:22 +0000 (02:52 +0000)
105 files changed:
.phpcs.xml
.stylelintrc.json
RELEASE-NOTES-1.34
autoload.php
docs/extension.schema.v1.json
docs/extension.schema.v2.json
includes/DefaultSettings.php
includes/Feed.php [deleted file]
includes/OutputPage.php
includes/Revision/RenderedRevision.php
includes/Storage/DerivedPageDataUpdater.php
includes/actions/HistoryAction.php
includes/api/ApiFeedRecentChanges.php
includes/api/ApiFormatXmlRsd.php [new file with mode: 0644]
includes/api/ApiImport.php
includes/api/ApiImportReporter.php [new file with mode: 0644]
includes/api/ApiOpenSearch.php
includes/api/ApiOpenSearchFormatJson.php [new file with mode: 0644]
includes/api/ApiRsd.php
includes/changes/AtomFeed.php [new file with mode: 0644]
includes/changes/ChangesFeed.php
includes/changes/ChannelFeed.php [new file with mode: 0644]
includes/changes/FeedItem.php [new file with mode: 0644]
includes/changes/RSSFeed.php [new file with mode: 0644]
includes/edit/PreparedEdit.php
includes/installer/DatabaseInstaller.php
includes/libs/mime/MimeAnalyzer.php
includes/logging/DatabaseLogEntry.php [new file with mode: 0644]
includes/logging/LegacyLogFormatter.php [new file with mode: 0644]
includes/logging/LogEntry.php
includes/logging/LogEntryBase.php [new file with mode: 0644]
includes/logging/LogFormatter.php
includes/logging/ManualLogEntry.php [new file with mode: 0644]
includes/logging/RCDatabaseLogEntry.php [new file with mode: 0644]
includes/media/SVGMetadataExtractor.php
includes/media/SVGReader.php [new file with mode: 0644]
includes/parser/ParserOptions.php
includes/preferences/DefaultPreferencesFactory.php
includes/preferences/PreferencesFactory.php
includes/registration/ExtensionDependencyError.php
includes/registration/ExtensionRegistry.php
includes/registration/VersionChecker.php
includes/resourceloader/ResourceLoader.php
includes/specials/SpecialPreferences.php
includes/specials/forms/PreferencesFormLegacy.php [deleted file]
mw-config/config-cc.css
mw-config/config.css
resources/src/jquery.tablesorter.styles/jquery.tablesorter.styles.less
resources/src/jquery.tipsy/jquery.tipsy.css
resources/src/jquery/jquery.confirmable.css
resources/src/jquery/jquery.suggestions.css
resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css
resources/src/mediawiki.action/mediawiki.action.edit.styles.less
resources/src/mediawiki.action/mediawiki.action.history.css
resources/src/mediawiki.action/mediawiki.action.history.styles.less
resources/src/mediawiki.action/mediawiki.action.view.categoryPage.less
resources/src/mediawiki.action/mediawiki.action.view.filepage.css
resources/src/mediawiki.action/mediawiki.action.view.metadata.css
resources/src/mediawiki.action/mediawiki.action.view.postEdit.less
resources/src/mediawiki.action/mediawiki.action.view.postEdit.monobook.css
resources/src/mediawiki.action/mediawiki.action.view.redirectPage.css
resources/src/mediawiki.apihelp.css
resources/src/mediawiki.apipretty.css
resources/src/mediawiki.content.json.less
resources/src/mediawiki.debug/debug.less
resources/src/mediawiki.diff.styles/diff.css
resources/src/mediawiki.diff.styles/print.css
resources/src/mediawiki.feedlink/feedlink.css
resources/src/mediawiki.filewarning/filewarning.less
resources/src/mediawiki.hlist/default.css
resources/src/mediawiki.hlist/hlist.less
resources/src/mediawiki.htmlform.ooui.styles.less
resources/src/mediawiki.interface.helpers.styles.less
resources/src/mediawiki.legacy/commonPrint.css
resources/src/mediawiki.legacy/oldshared.css
resources/src/mediawiki.legacy/shared.css
resources/src/mediawiki.page.gallery.styles/gallery.css
resources/src/mediawiki.page.gallery.styles/print.css
resources/src/mediawiki.pager.tablePager/TablePager.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.searchSuggest/searchSuggest.css
resources/src/mediawiki.skinning/content.css
resources/src/mediawiki.skinning/content.externallinks.less
resources/src/mediawiki.skinning/content.parsoid.less
resources/src/mediawiki.skinning/elements.css
resources/src/mediawiki.skinning/interface.css
resources/src/mediawiki.special.apisandbox/apisandbox.css
resources/src/mediawiki.special.changeslist.enhanced.less
resources/src/mediawiki.special.search.interwikiwidget.styles.less
resources/src/mediawiki.special.search.styles.css
resources/src/mediawiki.special.userlogin.common.styles/userlogin.css
resources/src/mediawiki.special.userlogin.signup.styles/signup.css
resources/src/mediawiki.special/movePage.css
resources/src/mediawiki.special/pagesWithProp.css
resources/src/mediawiki.special/special.less
resources/src/mediawiki.toc.styles/common.css
resources/src/mediawiki.toc.styles/print.css
resources/src/mediawiki.toc.styles/screen.less
resources/src/mediawiki.ui/components/buttons.less
resources/src/mediawiki.ui/components/forms.less
tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
tests/phpunit/includes/Storage/PreparedEditTest.php [new file with mode: 0644]
tests/phpunit/includes/parser/ParserOptionsTest.php
tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php
tests/phpunit/includes/registration/VersionCheckerTest.php

index 170e16d..0fb1587 100644 (file)
@@ -70,7 +70,6 @@
                        Whitelist existing violations, but enable the sniff to prevent
                        any new occurrences.
                -->
-               <exclude-pattern>*/includes/Feed\.php</exclude-pattern>
                <exclude-pattern>*/includes/installer/PhpBugTests\.php</exclude-pattern>
                <exclude-pattern>*/includes/specials/SpecialMostinterwikis\.php</exclude-pattern>
                <exclude-pattern>*/includes/compat/XMPReader\.php</exclude-pattern>
                        any new occurrences.
                -->
                <exclude-pattern>*/includes/api/ApiErrorFormatter\.php</exclude-pattern>
-               <exclude-pattern>*/includes/api/ApiImport\.php</exclude-pattern>
-               <exclude-pattern>*/includes/api/ApiMessage\.php</exclude-pattern>
-               <exclude-pattern>*/includes/api/ApiOpenSearch\.php</exclude-pattern>
-               <exclude-pattern>*/includes/api/ApiRsd\.php</exclude-pattern>
                <exclude-pattern>*/includes/compat/XMPReader\.php</exclude-pattern>
                <exclude-pattern>*/includes/diff/DairikiDiff\.php</exclude-pattern>
-               <exclude-pattern>*/includes/Feed\.php</exclude-pattern>
                <exclude-pattern>*/includes/filerepo/file/LocalFile\.php</exclude-pattern>
                <exclude-pattern>*/includes/htmlform/HTMLFormElement\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/FileBackendStore\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/FSFileBackend\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/SwiftFileBackend\.php</exclude-pattern>
-               <exclude-pattern>*/includes/logging/LogEntry\.php</exclude-pattern>
-               <exclude-pattern>*/includes/logging/LogFormatter\.php</exclude-pattern>
-               <exclude-pattern>*/includes/media/SVGMetadataExtractor\.php</exclude-pattern>
                <exclude-pattern>*/includes/parser/Preprocessor_DOM\.php</exclude-pattern>
                <exclude-pattern>*/includes/parser/Preprocessor_Hash\.php</exclude-pattern>
                <exclude-pattern>*/includes/parser/Preprocessor\.php</exclude-pattern>
-               <exclude-pattern>*/includes/PathRouter\.php</exclude-pattern>
                <exclude-pattern>*/includes/profiler/SectionProfiler\.php</exclude-pattern>
-               <exclude-pattern>*/includes/specialpage/LoginSignupSpecialPage\.php</exclude-pattern>
-               <exclude-pattern>*/includes/specials/forms/PreferencesFormLegacy\.php</exclude-pattern>
                <exclude-pattern>*/includes/StubObject\.php</exclude-pattern>
                <exclude-pattern>*/includes/upload/UploadStash\.php</exclude-pattern>
                <exclude-pattern>*/includes/utils/AutoloadGenerator\.php</exclude-pattern>
index 60c8f36..43f499b 100644 (file)
@@ -1,8 +1,8 @@
 {
        "extends": "stylelint-config-wikimedia",
        "rules": {
+               "selector-class-pattern": "^((mw|oo-ui)-|(wikitable|(toc(|toggle|hidden))|client-(no)?js)$)",
                "no-descending-specificity": null,
-
                "selector-max-id": null
        }
 }
index ba4ac51..da765c3 100644 (file)
@@ -84,6 +84,8 @@ because of Phabricator reports.
   services after any configuration change. Even if your code works now, it is
   likely to break in future versions as more code is moved to services.
 * The ill-defined "DatabaseOraclePostInit" hook has been removed.
+* PreferencesFormLegacy and PreferencesForm classes, deprecated in 1.32, have
+  been removed.
 
 === Deprecations in 1.34 ===
 * The MWNamespace class is deprecated. Use MediaWikiServices::getNamespaceInfo.
index a74a0b8..727c21f 100644 (file)
@@ -52,12 +52,12 @@ $wgAutoloadLocalClasses = [
        'ApiFormatPhp' => __DIR__ . '/includes/api/ApiFormatPhp.php',
        'ApiFormatRaw' => __DIR__ . '/includes/api/ApiFormatRaw.php',
        'ApiFormatXml' => __DIR__ . '/includes/api/ApiFormatXml.php',
-       'ApiFormatXmlRsd' => __DIR__ . '/includes/api/ApiRsd.php',
+       'ApiFormatXmlRsd' => __DIR__ . '/includes/api/ApiFormatXmlRsd.php',
        'ApiHelp' => __DIR__ . '/includes/api/ApiHelp.php',
        'ApiHelpParamValueMessage' => __DIR__ . '/includes/api/ApiHelpParamValueMessage.php',
        'ApiImageRotate' => __DIR__ . '/includes/api/ApiImageRotate.php',
        'ApiImport' => __DIR__ . '/includes/api/ApiImport.php',
-       'ApiImportReporter' => __DIR__ . '/includes/api/ApiImport.php',
+       'ApiImportReporter' => __DIR__ . '/includes/api/ApiImportReporter.php',
        'ApiLinkAccount' => __DIR__ . '/includes/api/ApiLinkAccount.php',
        'ApiLogin' => __DIR__ . '/includes/api/ApiLogin.php',
        'ApiLogout' => __DIR__ . '/includes/api/ApiLogout.php',
@@ -69,7 +69,7 @@ $wgAutoloadLocalClasses = [
        'ApiModuleManager' => __DIR__ . '/includes/api/ApiModuleManager.php',
        'ApiMove' => __DIR__ . '/includes/api/ApiMove.php',
        'ApiOpenSearch' => __DIR__ . '/includes/api/ApiOpenSearch.php',
-       'ApiOpenSearchFormatJson' => __DIR__ . '/includes/api/ApiOpenSearch.php',
+       'ApiOpenSearchFormatJson' => __DIR__ . '/includes/api/ApiOpenSearchFormatJson.php',
        'ApiOptions' => __DIR__ . '/includes/api/ApiOptions.php',
        'ApiPageSet' => __DIR__ . '/includes/api/ApiPageSet.php',
        'ApiParamInfo' => __DIR__ . '/includes/api/ApiParamInfo.php',
@@ -161,7 +161,7 @@ $wgAutoloadLocalClasses = [
        'ArrayUtils' => __DIR__ . '/includes/libs/ArrayUtils.php',
        'Article' => __DIR__ . '/includes/page/Article.php',
        'AssembleUploadChunksJob' => __DIR__ . '/includes/jobqueue/jobs/AssembleUploadChunksJob.php',
-       'AtomFeed' => __DIR__ . '/includes/Feed.php',
+       'AtomFeed' => __DIR__ . '/includes/changes/AtomFeed.php',
        'AtomicSectionUpdate' => __DIR__ . '/includes/deferred/AtomicSectionUpdate.php',
        'AttachLatest' => __DIR__ . '/maintenance/attachLatest.php',
        'AugmentPageProps' => __DIR__ . '/includes/search/AugmentPageProps.php',
@@ -253,7 +253,7 @@ $wgAutoloadLocalClasses = [
        'ChangesListSpecialPage' => __DIR__ . '/includes/specialpage/ChangesListSpecialPage.php',
        'ChangesListStringOptionsFilter' => __DIR__ . '/includes/changes/ChangesListStringOptionsFilter.php',
        'ChangesListStringOptionsFilterGroup' => __DIR__ . '/includes/changes/ChangesListStringOptionsFilterGroup.php',
-       'ChannelFeed' => __DIR__ . '/includes/Feed.php',
+       'ChannelFeed' => __DIR__ . '/includes/changes/ChannelFeed.php',
        'CheckBadRedirects' => __DIR__ . '/maintenance/checkBadRedirects.php',
        'CheckComposerLockUpToDate' => __DIR__ . '/maintenance/checkComposerLockUpToDate.php',
        'CheckExtensionsCLI' => __DIR__ . '/maintenance/language/checkLanguage.inc',
@@ -353,7 +353,7 @@ $wgAutoloadLocalClasses = [
        'DatabaseBase' => __DIR__ . '/includes/libs/rdbms/database/Database.php',
        'DatabaseInstaller' => __DIR__ . '/includes/installer/DatabaseInstaller.php',
        'DatabaseLag' => __DIR__ . '/maintenance/lag.php',
-       'DatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
+       'DatabaseLogEntry' => __DIR__ . '/includes/logging/DatabaseLogEntry.php',
        'DatabaseMssql' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMssql.php',
        'DatabaseMysqlBase' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqlBase.php',
        'DatabaseMysqli' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqli.php',
@@ -498,7 +498,7 @@ $wgAutoloadLocalClasses = [
        'FatalError' => __DIR__ . '/includes/exception/FatalError.php',
        'FauxRequest' => __DIR__ . '/includes/FauxRequest.php',
        'FauxResponse' => __DIR__ . '/includes/FauxResponse.php',
-       'FeedItem' => __DIR__ . '/includes/Feed.php',
+       'FeedItem' => __DIR__ . '/includes/changes/FeedItem.php',
        'FeedUtils' => __DIR__ . '/includes/FeedUtils.php',
        'FetchText' => __DIR__ . '/maintenance/fetchText.php',
        'FewestrevisionsPage' => __DIR__ . '/includes/specials/SpecialFewestrevisions.php',
@@ -772,7 +772,7 @@ $wgAutoloadLocalClasses = [
        'LanguageZh_hans' => __DIR__ . '/languages/classes/LanguageZh_hans.php',
        'Languages' => __DIR__ . '/maintenance/language/languages.inc',
        'LayeredParameterizedPassword' => __DIR__ . '/includes/password/LayeredParameterizedPassword.php',
-       'LegacyLogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php',
+       'LegacyLogFormatter' => __DIR__ . '/includes/logging/LegacyLogFormatter.php',
        'License' => __DIR__ . '/includes/specials/helpers/License.php',
        'Licenses' => __DIR__ . '/includes/specials/formfields/Licenses.php',
        'LinkBatch' => __DIR__ . '/includes/cache/LinkBatch.php',
@@ -803,7 +803,7 @@ $wgAutoloadLocalClasses = [
        'LockManager' => __DIR__ . '/includes/libs/lockmanager/LockManager.php',
        'LockManagerGroup' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroup.php',
        'LogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
-       'LogEntryBase' => __DIR__ . '/includes/logging/LogEntry.php',
+       'LogEntryBase' => __DIR__ . '/includes/logging/LogEntryBase.php',
        'LogEventsList' => __DIR__ . '/includes/logging/LogEventsList.php',
        'LogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php',
        'LogPage' => __DIR__ . '/includes/logging/LogPage.php',
@@ -851,7 +851,7 @@ $wgAutoloadLocalClasses = [
        'MalformedTitleException' => __DIR__ . '/includes/title/MalformedTitleException.php',
        'ManageForeignResources' => __DIR__ . '/maintenance/manageForeignResources.php',
        'ManageJobs' => __DIR__ . '/maintenance/manageJobs.php',
-       'ManualLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
+       'ManualLogEntry' => __DIR__ . '/includes/logging/ManualLogEntry.php',
        'MapCacheLRU' => __DIR__ . '/includes/libs/MapCacheLRU.php',
        'MappedIterator' => __DIR__ . '/includes/libs/MappedIterator.php',
        'MarkpatrolledAction' => __DIR__ . '/includes/actions/MarkpatrolledAction.php',
@@ -1134,8 +1134,6 @@ $wgAutoloadLocalClasses = [
        'PostgreSqlLockManager' => __DIR__ . '/includes/libs/lockmanager/PostgreSqlLockManager.php',
        'PostgresInstaller' => __DIR__ . '/includes/installer/PostgresInstaller.php',
        'PostgresUpdater' => __DIR__ . '/includes/installer/PostgresUpdater.php',
-       'PreferencesForm' => __DIR__ . '/includes/specials/forms/PreferencesFormLegacy.php',
-       'PreferencesFormLegacy' => __DIR__ . '/includes/specials/forms/PreferencesFormLegacy.php',
        'PreferencesFormOOUI' => __DIR__ . '/includes/specials/forms/PreferencesFormOOUI.php',
        'PrefixSearch' => __DIR__ . '/includes/search/PrefixSearch.php',
        'PrefixingStatsdDataFactoryProxy' => __DIR__ . '/includes/libs/stats/PrefixingStatsdDataFactoryProxy.php',
@@ -1179,12 +1177,12 @@ $wgAutoloadLocalClasses = [
        'QuorumLockManager' => __DIR__ . '/includes/libs/lockmanager/QuorumLockManager.php',
        'RCCacheEntry' => __DIR__ . '/includes/changes/RCCacheEntry.php',
        'RCCacheEntryFactory' => __DIR__ . '/includes/changes/RCCacheEntryFactory.php',
-       'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
+       'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/RCDatabaseLogEntry.php',
        'RCFeed' => __DIR__ . '/includes/rcfeed/RCFeed.php',
        'RCFeedEngine' => __DIR__ . '/includes/rcfeed/RCFeedEngine.php',
        'RCFeedFormatter' => __DIR__ . '/includes/rcfeed/RCFeedFormatter.php',
        'RESTBagOStuff' => __DIR__ . '/includes/libs/objectcache/RESTBagOStuff.php',
-       'RSSFeed' => __DIR__ . '/includes/Feed.php',
+       'RSSFeed' => __DIR__ . '/includes/changes/RSSFeed.php',
        'RandomPage' => __DIR__ . '/includes/specials/SpecialRandompage.php',
        'RangeChronologicalPager' => __DIR__ . '/includes/pager/RangeChronologicalPager.php',
        'RangeDifference' => __DIR__ . '/includes/diff/RangeDifference.php',
@@ -1291,7 +1289,7 @@ $wgAutoloadLocalClasses = [
        'RunJobs' => __DIR__ . '/maintenance/runJobs.php',
        'RunnableJob' => __DIR__ . '/includes/jobqueue/RunnableJob.php',
        'SVGMetadataExtractor' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
-       'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
+       'SVGReader' => __DIR__ . '/includes/media/SVGReader.php',
        'SamplingStatsdClient' => __DIR__ . '/includes/libs/stats/SamplingStatsdClient.php',
        'Sanitizer' => __DIR__ . '/includes/parser/Sanitizer.php',
        'ScopedLock' => __DIR__ . '/includes/libs/lockmanager/ScopedLock.php',
index 8cd4e71..36e2fe2 100644 (file)
                                                "php": {
                                                        "type": "string",
                                                        "description": "Version constraint string against PHP."
+                                               },
+                                               "ability-shell": {
+                                                       "type": "boolean",
+                                                       "default": false,
+                                                       "description": "Whether this extension requires shell access."
                                                }
                                        },
                                        "patternProperties": {
index 1d64095..ed903f8 100644 (file)
                                                "php": {
                                                        "type": "string",
                                                        "description": "Version constraint string against PHP."
+                                               },
+                                               "ability-shell": {
+                                                       "type": "boolean",
+                                                       "default": false,
+                                                       "description": "Whether this extension requires shell access."
                                                }
                                        },
                                        "patternProperties": {
index 828af49..4547009 100644 (file)
@@ -8996,7 +8996,7 @@ $wgEnableBlockNoticeStats = false;
 /**
  * Origin Trials tokens.
  *
- * @since 1.34
+ * @since 1.33
  * @var array
  */
 $wgOriginTrials = [];
@@ -9006,7 +9006,7 @@ $wgOriginTrials = [];
  *
  * @warning EXPERIMENTAL!
  *
- * @since 1.34
+ * @since 1.33
  * @var bool
  */
 $wgPriorityHints = false;
@@ -9016,11 +9016,42 @@ $wgPriorityHints = false;
  *
  * @warning EXPERIMENTAL!
  *
- * @since 1.34
+ * @since 1.33
  * @var bool
  */
 $wgElementTiming = false;
 
+/**
+ * Expiry of the endpoint definition for the Reporting API.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var int
+ */
+$wgReportToExpiry = 86400;
+
+/**
+ * List of endpoints for the Reporting API.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var array
+ */
+$wgReportToEndpoints = [];
+
+/**
+ * List of Feature Policy Reporting types to enable.
+ * Each entry is turned into a Feature-Policy-Report-Only header.
+ *
+ * @warning EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var array
+ */
+$wgFeaturePolicyReportOnly = [];
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
diff --git a/includes/Feed.php b/includes/Feed.php
deleted file mode 100644 (file)
index 86e9bee..0000000
+++ /dev/null
@@ -1,494 +0,0 @@
-<?php
-/**
- * Basic support for outputting syndication feeds in RSS, other formats.
- *
- * Contain a feed class as well as classes to build rss / atom ... feeds
- * Available feeds are defined in Defines.php
- *
- * Copyright Â© 2004 Brion Vibber <brion@pobox.com>
- * https://www.mediawiki.org/
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * @defgroup Feed Feed
- */
-
-/**
- * A base class for basic support for outputting syndication feeds in RSS and other formats.
- *
- * @ingroup Feed
- */
-class FeedItem {
-       /** @var Title */
-       public $title;
-
-       public $description;
-
-       public $url;
-
-       public $date;
-
-       public $author;
-
-       public $uniqueId;
-
-       public $comments;
-
-       public $rssIsPermalink = false;
-
-       /**
-        * @param string|Title $title Item's title
-        * @param string $description
-        * @param string $url URL uniquely designating the item.
-        * @param string $date Item's date
-        * @param string $author Author's user name
-        * @param string $comments
-        */
-       function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
-               $this->title = $title;
-               $this->description = $description;
-               $this->url = $url;
-               $this->uniqueId = $url;
-               $this->date = $date;
-               $this->author = $author;
-               $this->comments = $comments;
-       }
-
-       /**
-        * Encode $string so that it can be safely embedded in a XML document
-        *
-        * @param string $string String to encode
-        * @return string
-        */
-       public function xmlEncode( $string ) {
-               $string = str_replace( "\r\n", "\n", $string );
-               $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string );
-               return htmlspecialchars( $string );
-       }
-
-       /**
-        * Get the unique id of this item; already xml-encoded
-        * @return string
-        */
-       public function getUniqueID() {
-               $id = $this->getUniqueIdUnescaped();
-               if ( $id ) {
-                       return $this->xmlEncode( $id );
-               }
-       }
-
-       /**
-        * Get the unique id of this item, without any escaping
-        * @return string
-        */
-       public function getUniqueIdUnescaped() {
-               if ( $this->uniqueId ) {
-                       return wfExpandUrl( $this->uniqueId, PROTO_CURRENT );
-               }
-       }
-
-       /**
-        * Set the unique id of an item
-        *
-        * @param string $uniqueId Unique id for the item
-        * @param bool $rssIsPermalink Set to true if the guid (unique id) is a permalink (RSS feeds only)
-        */
-       public function setUniqueId( $uniqueId, $rssIsPermalink = false ) {
-               $this->uniqueId = $uniqueId;
-               $this->rssIsPermalink = $rssIsPermalink;
-       }
-
-       /**
-        * Get the title of this item; already xml-encoded
-        *
-        * @return string
-        */
-       public function getTitle() {
-               return $this->xmlEncode( $this->title );
-       }
-
-       /**
-        * Get the URL of this item; already xml-encoded
-        *
-        * @return string
-        */
-       public function getUrl() {
-               return $this->xmlEncode( $this->url );
-       }
-
-       /** Get the URL of this item without any escaping
-        *
-        * @return string
-        */
-       public function getUrlUnescaped() {
-               return $this->url;
-       }
-
-       /**
-        * Get the description of this item; already xml-encoded
-        *
-        * @return string
-        */
-       public function getDescription() {
-               return $this->xmlEncode( $this->description );
-       }
-
-       /**
-        * Get the description of this item without any escaping
-        *
-        * @return string
-        */
-       public function getDescriptionUnescaped() {
-               return $this->description;
-       }
-
-       /**
-        * Get the language of this item
-        *
-        * @return string
-        */
-       public function getLanguage() {
-               global $wgLanguageCode;
-               return LanguageCode::bcp47( $wgLanguageCode );
-       }
-
-       /**
-        * Get the date of this item
-        *
-        * @return string
-        */
-       public function getDate() {
-               return $this->date;
-       }
-
-       /**
-        * Get the author of this item; already xml-encoded
-        *
-        * @return string
-        */
-       public function getAuthor() {
-               return $this->xmlEncode( $this->author );
-       }
-
-       /**
-        * Get the author of this item without any escaping
-        *
-        * @return string
-        */
-       public function getAuthorUnescaped() {
-               return $this->author;
-       }
-
-       /**
-        * Get the comment of this item; already xml-encoded
-        *
-        * @return string
-        */
-       public function getComments() {
-               return $this->xmlEncode( $this->comments );
-       }
-
-       /**
-        * Get the comment of this item without any escaping
-        *
-        * @return string
-        */
-       public function getCommentsUnescaped() {
-               return $this->comments;
-       }
-
-       /**
-        * Quickie hack... strip out wikilinks to more legible form from the comment.
-        *
-        * @param string $text Wikitext
-        * @return string
-        */
-       public static function stripComment( $text ) {
-               return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
-       }
-       /**#@-*/
-}
-
-/**
- * Class to support the outputting of syndication feeds in Atom and RSS format.
- *
- * @ingroup Feed
- */
-abstract class ChannelFeed extends FeedItem {
-
-       /** @var TemplateParser */
-       protected $templateParser;
-
-       /**
-        * @param string|Title $title Feed's title
-        * @param string $description
-        * @param string $url URL uniquely designating the feed.
-        * @param string $date Feed's date
-        * @param string $author Author's user name
-        * @param string $comments
-        */
-       function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
-               parent::__construct( $title, $description, $url, $date, $author, $comments );
-               $this->templateParser = new TemplateParser();
-       }
-
-       /**
-        * Generate Header of the feed
-        * @par Example:
-        * @code
-        * print "<feed>";
-        * @endcode
-        */
-       abstract public function outHeader();
-
-       /**
-        * Generate an item
-        * @par Example:
-        * @code
-        * print "<item>...</item>";
-        * @endcode
-        * @param FeedItem $item
-        */
-       abstract public function outItem( $item );
-
-       /**
-        * Generate Footer of the feed
-        * @par Example:
-        * @code
-        * print "</feed>";
-        * @endcode
-        */
-       abstract public function outFooter();
-
-       /**
-        * Setup and send HTTP headers. Don't send any content;
-        * content might end up being cached and re-sent with
-        * these same headers later.
-        *
-        * This should be called from the outHeader() method,
-        * but can also be called separately.
-        */
-       public function httpHeaders() {
-               global $wgOut, $wgVaryOnXFP;
-
-               # We take over from $wgOut, excepting its cache header info
-               $wgOut->disable();
-               $mimetype = $this->contentType();
-               header( "Content-type: $mimetype; charset=UTF-8" );
-
-               // Set a sane filename
-               $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
-                       ->getExtensionsForType( $mimetype );
-               $ext = $exts ? strtok( $exts, ' ' ) : 'xml';
-               header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" );
-
-               if ( $wgVaryOnXFP ) {
-                       $wgOut->addVaryHeader( 'X-Forwarded-Proto' );
-               }
-               $wgOut->sendCacheControl();
-       }
-
-       /**
-        * Return an internet media type to be sent in the headers.
-        *
-        * @return string
-        */
-       private function contentType() {
-               global $wgRequest;
-
-               $ctype = $wgRequest->getVal( 'ctype', 'application/xml' );
-               $allowedctypes = [
-                       'application/xml',
-                       'text/xml',
-                       'application/rss+xml',
-                       'application/atom+xml'
-               ];
-
-               return ( in_array( $ctype, $allowedctypes ) ? $ctype : 'application/xml' );
-       }
-
-       /**
-        * Output the initial XML headers.
-        */
-       protected function outXmlHeader() {
-               $this->httpHeaders();
-               echo '<?xml version="1.0"?>' . "\n";
-       }
-}
-
-/**
- * Generate a RSS feed
- *
- * @ingroup Feed
- */
-class RSSFeed extends ChannelFeed {
-
-       /**
-        * Format a date given a timestamp. If a timestamp is not given, nothing is returned
-        *
-        * @param int|null $ts Timestamp
-        * @return string|null Date string
-        */
-       function formatTime( $ts ) {
-               if ( $ts ) {
-                       return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
-               }
-       }
-
-       /**
-        * Output an RSS 2.0 header
-        */
-       function outHeader() {
-               global $wgVersion;
-
-               $this->outXmlHeader();
-               // Manually escaping rather than letting Mustache do it because Mustache
-               // uses htmlentities, which does not work with XML
-               $templateParams = [
-                       'title' => $this->getTitle(),
-                       'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
-                       'description' => $this->getDescription(),
-                       'language' => $this->xmlEncode( $this->getLanguage() ),
-                       'version' => $this->xmlEncode( $wgVersion ),
-                       'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) )
-               ];
-               print $this->templateParser->processTemplate( 'RSSHeader', $templateParams );
-       }
-
-       /**
-        * Output an RSS 2.0 item
-        * @param FeedItem $item Item to be output
-        */
-       function outItem( $item ) {
-               // Manually escaping rather than letting Mustache do it because Mustache
-               // uses htmlentities, which does not work with XML
-               $templateParams = [
-                       "title" => $item->getTitle(),
-                       "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
-                       "permalink" => $item->rssIsPermalink,
-                       "uniqueID" => $item->getUniqueID(),
-                       "description" => $item->getDescription(),
-                       "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
-                       "author" => $item->getAuthor()
-               ];
-               $comments = $item->getCommentsUnescaped();
-               if ( $comments ) {
-                       $commentsEscaped = $this->xmlEncode( wfExpandUrl( $comments, PROTO_CURRENT ) );
-                       $templateParams["comments"] = $commentsEscaped;
-               }
-               print $this->templateParser->processTemplate( 'RSSItem', $templateParams );
-       }
-
-       /**
-        * Output an RSS 2.0 footer
-        */
-       function outFooter() {
-               print "</channel></rss>";
-       }
-}
-
-/**
- * Generate an Atom feed
- *
- * @ingroup Feed
- */
-class AtomFeed extends ChannelFeed {
-       /**
-        * Format a date given timestamp, if one is given.
-        *
-        * @param string|int|null $timestamp
-        * @return string|null
-        */
-       function formatTime( $timestamp ) {
-               if ( $timestamp ) {
-                       // need to use RFC 822 time format at least for rss2.0
-                       return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
-               }
-       }
-
-       /**
-        * Outputs a basic header for Atom 1.0 feeds.
-        */
-       function outHeader() {
-               global $wgVersion;
-               $this->outXmlHeader();
-               // Manually escaping rather than letting Mustache do it because Mustache
-               // uses htmlentities, which does not work with XML
-               $templateParams = [
-                       'language' => $this->xmlEncode( $this->getLanguage() ),
-                       'feedID' => $this->getFeedId(),
-                       'title' => $this->getTitle(),
-                       'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
-                       'selfUrl' => $this->getSelfUrl(),
-                       'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ),
-                       'description' => $this->getDescription(),
-                       'version' => $this->xmlEncode( $wgVersion ),
-               ];
-               print $this->templateParser->processTemplate( 'AtomHeader', $templateParams );
-       }
-
-       /**
-        * Atom 1.0 requires a unique, opaque IRI as a unique identifier
-        * for every feed we create. For now just use the URL, but who
-        * can tell if that's right? If we put options on the feed, do we
-        * have to change the id? Maybe? Maybe not.
-        *
-        * @return string
-        */
-       private function getFeedId() {
-               return $this->getSelfUrl();
-       }
-
-       /**
-        * Atom 1.0 requests a self-reference to the feed.
-        * @return string
-        */
-       private function getSelfUrl() {
-               global $wgRequest;
-               return htmlspecialchars( $wgRequest->getFullRequestURL() );
-       }
-
-       /**
-        * Output a given item.
-        * @param FeedItem $item
-        */
-       function outItem( $item ) {
-               global $wgMimeType;
-               // Manually escaping rather than letting Mustache do it because Mustache
-               // uses htmlentities, which does not work with XML
-               $templateParams = [
-                       "uniqueID" => $item->getUniqueID(),
-                       "title" => $item->getTitle(),
-                       "mimeType" => $this->xmlEncode( $wgMimeType ),
-                       "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
-                       "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
-                       "description" => $item->getDescription(),
-                       "author" => $item->getAuthor()
-               ];
-               print $this->templateParser->processTemplate( 'AtomItem', $templateParams );
-       }
-
-       /**
-        * Outputs the footer for Atom 1.0 feed (basically '\</feed\>').
-        */
-       function outFooter() {
-               print "</feed>";
-       }
-}
index 1da8ac8..859593b 100644 (file)
@@ -2527,6 +2527,37 @@ class OutputPage extends ContextSource {
                return $config->get( 'OriginTrials' );
        }
 
+       private function getReportTo() {
+               $config = $this->getConfig();
+
+               $expiry = $config->get( 'ReportToExpiry' );
+
+               if ( !$expiry ) {
+                       return false;
+               }
+
+               $endpoints = $config->get( 'ReportToEndpoints' );
+
+               if ( !$endpoints ) {
+                       return false;
+               }
+
+               $output = [ 'max_age' => $expiry, 'endpoints' => [] ];
+
+               foreach ( $endpoints as $endpoint ) {
+                       $output['endpoints'][] = [ 'url' => $endpoint ];
+               }
+
+               return json_encode( $output, JSON_UNESCAPED_SLASHES );
+       }
+
+       private function getFeaturePolicyReportOnly() {
+               $config = $this->getConfig();
+
+               $features = $config->get( 'FeaturePolicyReportOnly' );
+               return implode( ';', $features );
+       }
+
        /**
         * Send cache control HTTP headers
         */
@@ -2694,6 +2725,16 @@ class OutputPage extends ContextSource {
                        $response->header( "Origin-Trial: $originTrial", false );
                }
 
+               $reportTo = $this->getReportTo();
+               if ( $reportTo ) {
+                       $response->header( "Report-To: $reportTo" );
+               }
+
+               $featurePolicyReportOnly = $this->getFeaturePolicyReportOnly();
+               if ( $featurePolicyReportOnly ) {
+                       $response->header( "Feature-Policy-Report-Only: $featurePolicyReportOnly" );
+               }
+
                ContentSecurityPolicy::sendHeaders( $this );
 
                if ( $this->mArticleBodyOnly ) {
index d3e5938..c4a0054 100644 (file)
@@ -380,9 +380,9 @@ class RenderedRevision implements SlotRenderingProvider {
                $method = __METHOD__;
 
                if ( $out->getFlag( 'vary-revision' ) ) {
-                       // XXX: Would be just keep the output if the speculative revision ID was correct,
-                       // but that can go wrong for some edge cases, like {{PAGEID}} during page creation.
-                       // For that specific case, it would perhaps nice to have a vary-page flag.
+                       // If {{PAGEID}} resolved to 0 or {{REVISIONTIMESTAMP}} used the current
+                       // timestamp rather than that of an actual revision, then those words need
+                       // to resolve to the actual page ID or revision timestamp, respectively.
                        $this->saveParseLogger->info(
                                "$method: Prepared output has vary-revision...\n"
                        );
index c4aec13..3dbe0a8 100644 (file)
@@ -1239,7 +1239,7 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                $preparedEdit = new PreparedEdit();
 
                $preparedEdit->popts = $this->getCanonicalParserOptions();
-               $preparedEdit->output = $this->getCanonicalParserOutput();
+               $preparedEdit->parserOutputCallback = [ $this, 'getCanonicalParserOutput' ];
                $preparedEdit->pstContent = $this->revision->getContent( SlotRecord::MAIN );
                $preparedEdit->newContent =
                        $slotsUpdate->isModifiedSlot( SlotRecord::MAIN )
@@ -1401,13 +1401,31 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                $legacyUser = User::newFromIdentity( $this->user );
                $legacyRevision = new Revision( $this->revision );
 
-               $this->doParserCacheUpdate();
+               $userParserOptions = ParserOptions::newFromUser( $legacyUser );
+               // Decide whether to save the final canonical parser ouput based on the fact that
+               // users are typically redirected to viewing pages right after they edit those pages.
+               // Due to vary-revision-id, getting/saving that output here might require a reparse.
+               if ( $userParserOptions->matchesForCacheKey( $this->getCanonicalParserOptions() ) ) {
+                       // Whether getting the final output requires a reparse or not, the user will
+                       // need canonical output anyway, since that is what their parser options use.
+                       // A reparse now at least has the benefit of various warm process caches.
+                       $this->doParserCacheUpdate();
+               } else {
+                       // If the user does not have canonical parse options, then don't risk another parse
+                       // to make output they cannot use on the page refresh that typically occurs after
+                       // editing. Doing the parser output save post-send will still benefit *other* users.
+                       DeferredUpdates::addCallableUpdate( function () {
+                               $this->doParserCacheUpdate();
+                       } );
+               }
 
-               $this->doSecondaryDataUpdates( [
-                       // T52785 do not update any other pages on a null edit
-                       'recursive' => $this->options['changed'],
-                       'defer' => DeferredUpdates::POSTSEND,
-               ] );
+               // Defer the getCannonicalParserOutput() call triggered by getSecondaryDataUpdates()
+               DeferredUpdates::addCallableUpdate( function () {
+                       $this->doSecondaryDataUpdates( [
+                               // T52785 do not update any other pages on a null edit
+                               'recursive' => $this->options['changed']
+                       ] );
+               } );
 
                // TODO: MCR: check if *any* changed slot supports categories!
                if ( $this->rcWatchCategoryMembership
@@ -1427,8 +1445,11 @@ class DerivedPageDataUpdater implements IDBAccessObject {
                }
 
                // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
+               // @note: Extensions should *avoid* calling getCannonicalParserOutput() when using
+               // this hook whenever possible in order to avoid unnecessary additional parses.
                $editInfo = $this->getPreparedEdit();
-               Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $this->options['changed'] ] );
+               Hooks::run( 'ArticleEditUpdates',
+                       [ &$wikiPage, &$editInfo, $this->options['changed'] ] );
 
                // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
                if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
index bc1d351..fc42be4 100644 (file)
@@ -181,10 +181,9 @@ class HistoryAction extends FormlessAction {
                }
 
                // Handle atom/RSS feeds.
-               $feedType = $request->getVal( 'feed' );
-               if ( $feedType ) {
+               $feedType = $request->getRawVal( 'feed' );
+               if ( $feedType !== null ) {
                        $this->feed( $feedType );
-
                        return;
                }
 
index e5dba8f..678b97b 100644 (file)
@@ -101,7 +101,7 @@ class ApiFeedRecentChanges extends ApiBase {
                                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $this->params['target'] ) ] );
                        }
 
-                       $feed = new ChangesFeed( $feedFormat, false );
+                       $feed = new ChangesFeed( $feedFormat );
                        $feedObj = $feed->getFeedObject(
                                $this->msg( 'recentchangeslinked-title', $title->getPrefixedText() )
                                        ->inContentLanguage()->text(),
@@ -109,7 +109,7 @@ class ApiFeedRecentChanges extends ApiBase {
                                SpecialPage::getTitleFor( 'Recentchangeslinked' )->getFullURL()
                        );
                } else {
-                       $feed = new ChangesFeed( $feedFormat, 'rcfeed' );
+                       $feed = new ChangesFeed( $feedFormat );
                        $feedObj = $feed->getFeedObject(
                                $this->msg( 'recentchanges' )->inContentLanguage()->text(),
                                $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(),
diff --git a/includes/api/ApiFormatXmlRsd.php b/includes/api/ApiFormatXmlRsd.php
new file mode 100644 (file)
index 0000000..6b892fa
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * Copyright Â© 2010 Bryan Tong Minh and Brion Vibber
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class ApiFormatXmlRsd extends ApiFormatXml {
+       public function __construct( ApiMain $main, $format ) {
+               parent::__construct( $main, $format );
+               $this->setRootElement( 'rsd' );
+       }
+
+       public function getMimeType() {
+               return 'application/rsd+xml';
+       }
+
+       public static function recXmlPrint( $name, $value, $indent, $attributes = [] ) {
+               unset( $attributes['_idx'] );
+               return parent::recXmlPrint( $name, $value, $indent, $attributes );
+       }
+}
index 596ab75..b36045e 100644 (file)
@@ -181,42 +181,3 @@ class ApiImport extends ApiBase {
                return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Import';
        }
 }
-
-/**
- * Import reporter for the API
- * @ingroup API
- */
-class ApiImportReporter extends ImportReporter {
-       private $mResultArr = [];
-
-       /**
-        * @param Title $title
-        * @param Title $origTitle
-        * @param int $revisionCount
-        * @param int $successCount
-        * @param array $pageInfo
-        * @return void
-        */
-       public function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) {
-               // Add a result entry
-               $r = [];
-
-               if ( $title === null ) {
-                       # Invalid or non-importable title
-                       $r['title'] = $pageInfo['title'];
-                       $r['invalid'] = true;
-               } else {
-                       ApiQueryBase::addTitleInfo( $r, $title );
-                       $r['revisions'] = (int)$successCount;
-               }
-
-               $this->mResultArr[] = $r;
-
-               // Piggyback on the parent to do the logging
-               parent::reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo );
-       }
-
-       public function getData() {
-               return $this->mResultArr;
-       }
-}
diff --git a/includes/api/ApiImportReporter.php b/includes/api/ApiImportReporter.php
new file mode 100644 (file)
index 0000000..21d9d23
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Copyright Â© 2009 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Import reporter for the API
+ * @ingroup API
+ */
+class ApiImportReporter extends ImportReporter {
+       private $mResultArr = [];
+
+       /**
+        * @param Title $title
+        * @param Title $origTitle
+        * @param int $revisionCount
+        * @param int $successCount
+        * @param array $pageInfo
+        * @return void
+        */
+       public function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) {
+               // Add a result entry
+               $r = [];
+
+               if ( $title === null ) {
+                       # Invalid or non-importable title
+                       $r['title'] = $pageInfo['title'];
+                       $r['invalid'] = true;
+               } else {
+                       ApiQueryBase::addTitleInfo( $r, $title );
+                       $r['revisions'] = (int)$successCount;
+               }
+
+               $this->mResultArr[] = $r;
+
+               // Piggyback on the parent to do the logging
+               parent::reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo );
+       }
+
+       public function getData() {
+               return $this->mResultArr;
+       }
+}
index 416fc7f..8e2837b 100644 (file)
@@ -379,41 +379,3 @@ class ApiOpenSearch extends ApiBase {
                }
        }
 }
-
-/**
- * @ingroup API
- */
-class ApiOpenSearchFormatJson extends ApiFormatJson {
-       private $warningsAsError = false;
-
-       public function __construct( ApiMain $main, $fm, $warningsAsError ) {
-               parent::__construct( $main, "json$fm" );
-               $this->warningsAsError = $warningsAsError;
-       }
-
-       public function execute() {
-               $result = $this->getResult();
-               if ( !$result->getResultData( 'error' ) && !$result->getResultData( 'errors' ) ) {
-                       // Ignore warnings or treat as errors, as requested
-                       $warnings = $result->removeValue( 'warnings', null );
-                       if ( $this->warningsAsError && $warnings ) {
-                               $this->dieWithError(
-                                       'apierror-opensearch-json-warnings',
-                                       'warnings',
-                                       [ 'warnings' => $warnings ]
-                               );
-                       }
-
-                       // Ignore any other unexpected keys (e.g. from $wgDebugToolbar)
-                       $remove = array_keys( array_diff_key(
-                               $result->getResultData(),
-                               [ 0 => 'search', 1 => 'terms', 2 => 'descriptions', 3 => 'urls' ]
-                       ) );
-                       foreach ( $remove as $key ) {
-                               $result->removeValue( $key, null );
-                       }
-               }
-
-               parent::execute();
-       }
-}
diff --git a/includes/api/ApiOpenSearchFormatJson.php b/includes/api/ApiOpenSearchFormatJson.php
new file mode 100644 (file)
index 0000000..b1903f2
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Copyright Â© 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ * Copyright Â© 2008 Brion Vibber <brion@wikimedia.org>
+ * Copyright Â© 2014 Wikimedia Foundation and contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup API
+ */
+class ApiOpenSearchFormatJson extends ApiFormatJson {
+       private $warningsAsError = false;
+
+       public function __construct( ApiMain $main, $fm, $warningsAsError ) {
+               parent::__construct( $main, "json$fm" );
+               $this->warningsAsError = $warningsAsError;
+       }
+
+       public function execute() {
+               $result = $this->getResult();
+               if ( !$result->getResultData( 'error' ) && !$result->getResultData( 'errors' ) ) {
+                       // Ignore warnings or treat as errors, as requested
+                       $warnings = $result->removeValue( 'warnings', null );
+                       if ( $this->warningsAsError && $warnings ) {
+                               $this->dieWithError(
+                                       'apierror-opensearch-json-warnings',
+                                       'warnings',
+                                       [ 'warnings' => $warnings ]
+                               );
+                       }
+
+                       // Ignore any other unexpected keys (e.g. from $wgDebugToolbar)
+                       $remove = array_keys( array_diff_key(
+                               $result->getResultData(),
+                               [ 0 => 'search', 1 => 'terms', 2 => 'descriptions', 3 => 'urls' ]
+                       ) );
+                       foreach ( $remove as $key ) {
+                               $result->removeValue( $key, null );
+                       }
+               }
+
+               parent::execute();
+       }
+}
index 71bab36..00ab77b 100644 (file)
@@ -149,19 +149,3 @@ class ApiRsd extends ApiBase {
                return $outputData;
        }
 }
-
-class ApiFormatXmlRsd extends ApiFormatXml {
-       public function __construct( ApiMain $main, $format ) {
-               parent::__construct( $main, $format );
-               $this->setRootElement( 'rsd' );
-       }
-
-       public function getMimeType() {
-               return 'application/rsd+xml';
-       }
-
-       public static function recXmlPrint( $name, $value, $indent, $attributes = [] ) {
-               unset( $attributes['_idx'] );
-               return parent::recXmlPrint( $name, $value, $indent, $attributes );
-       }
-}
diff --git a/includes/changes/AtomFeed.php b/includes/changes/AtomFeed.php
new file mode 100644 (file)
index 0000000..a4ce0c1
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Copyright Â© 2004 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Generate an Atom feed.
+ *
+ * @ingroup Feed
+ */
+class AtomFeed extends ChannelFeed {
+       /**
+        * Format a date given timestamp, if one is given.
+        *
+        * @param string|int|null $timestamp
+        * @return string|null
+        */
+       function formatTime( $timestamp ) {
+               if ( $timestamp ) {
+                       // need to use RFC 822 time format at least for rss2.0
+                       return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
+               }
+       }
+
+       /**
+        * Outputs a basic header for Atom 1.0 feeds.
+        */
+       function outHeader() {
+               global $wgVersion;
+               $this->outXmlHeader();
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       'language' => $this->xmlEncode( $this->getLanguage() ),
+                       'feedID' => $this->getFeedId(),
+                       'title' => $this->getTitle(),
+                       'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       'selfUrl' => $this->getSelfUrl(),
+                       'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ),
+                       'description' => $this->getDescription(),
+                       'version' => $this->xmlEncode( $wgVersion ),
+               ];
+               print $this->templateParser->processTemplate( 'AtomHeader', $templateParams );
+       }
+
+       /**
+        * Atom 1.0 requires a unique, opaque IRI as a unique identifier
+        * for every feed we create. For now just use the URL, but who
+        * can tell if that's right? If we put options on the feed, do we
+        * have to change the id? Maybe? Maybe not.
+        *
+        * @return string
+        */
+       private function getFeedId() {
+               return $this->getSelfUrl();
+       }
+
+       /**
+        * Atom 1.0 requests a self-reference to the feed.
+        * @return string
+        */
+       private function getSelfUrl() {
+               global $wgRequest;
+               return htmlspecialchars( $wgRequest->getFullRequestURL() );
+       }
+
+       /**
+        * Output a given item.
+        * @param FeedItem $item
+        */
+       function outItem( $item ) {
+               global $wgMimeType;
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       "uniqueID" => $item->getUniqueID(),
+                       "title" => $item->getTitle(),
+                       "mimeType" => $this->xmlEncode( $wgMimeType ),
+                       "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+                       "description" => $item->getDescription(),
+                       "author" => $item->getAuthor()
+               ];
+               print $this->templateParser->processTemplate( 'AtomItem', $templateParams );
+       }
+
+       /**
+        * Outputs the footer for Atom 1.0 feed (basically '\</feed\>').
+        */
+       function outFooter() {
+               print "</feed>";
+       }
+}
index 50c6826..4d00fbc 100644 (file)
  * @file
  */
 
-use Wikimedia\Rdbms\ResultWrapper;
-use MediaWiki\MediaWikiServices;
-
 /**
- * Feed to Special:RecentChanges and Special:RecentChangesLiked
+ * Feed to Special:RecentChanges and Special:RecentChangesLinked.
  *
  * @ingroup Feed
  */
 class ChangesFeed {
-       public $format, $type, $titleMsg, $descMsg;
+       private $format;
 
        /**
         * @param string $format Feed's format (either 'rss' or 'atom')
-        * @param string $type Type of feed (for cache keys)
         */
-       public function __construct( $format, $type ) {
+       public function __construct( $format ) {
                $this->format = $format;
-               $this->type = $type;
        }
 
        /**
@@ -65,119 +60,6 @@ class ChangesFeed {
                        $feedTitle, htmlspecialchars( $description ), $url );
        }
 
-       /**
-        * Generates feed's content
-        *
-        * @param ChannelFeed $feed ChannelFeed subclass object (generally the one returned
-        *   by getFeedObject())
-        * @param ResultWrapper $rows ResultWrapper object with rows in recentchanges table
-        * @param int $lastmod Timestamp of the last item in the recentchanges table (only
-        *   used for the cache key)
-        * @param FormOptions $opts As in SpecialRecentChanges::getDefaultOptions()
-        * @return null|bool True or null
-        */
-       public function execute( $feed, $rows, $lastmod, $opts ) {
-               global $wgLang, $wgRenderHashAppend;
-
-               if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
-                       return null;
-               }
-
-               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-               $optionsHash = md5( serialize( $opts->getAllValues() ) ) . $wgRenderHashAppend;
-               $timekey = $cache->makeKey(
-                       $this->type, $this->format, $wgLang->getCode(), $optionsHash, 'timestamp' );
-               $key = $cache->makeKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash );
-
-               FeedUtils::checkPurge( $timekey, $key );
-
-               /**
-                * Bumping around loading up diffs can be pretty slow, so where
-                * possible we want to cache the feed output so the next visitor
-                * gets it quick too.
-                */
-               $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key );
-               if ( is_string( $cachedFeed ) ) {
-                       wfDebug( "RC: Outputting cached feed\n" );
-                       $feed->httpHeaders();
-                       echo $cachedFeed;
-               } else {
-                       wfDebug( "RC: rendering new feed and caching it\n" );
-                       ob_start();
-                       self::generateFeed( $rows, $feed );
-                       $cachedFeed = ob_get_contents();
-                       ob_end_flush();
-                       $this->saveToCache( $cachedFeed, $timekey, $key );
-               }
-               return true;
-       }
-
-       /**
-        * Save to feed result to cache
-        *
-        * @param string $feed Feed's content
-        * @param string $timekey Memcached key of the last modification
-        * @param string $key Memcached key of the content
-        */
-       public function saveToCache( $feed, $timekey, $key ) {
-               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-               $cache->set( $key, $feed, $cache::TTL_DAY );
-               $cache->set( $timekey, wfTimestamp( TS_MW ), $cache::TTL_DAY );
-       }
-
-       /**
-        * Try to load the feed result from cache
-        *
-        * @param int $lastmod Timestamp of the last item in the recentchanges table
-        * @param string $timekey Memcached key of the last modification
-        * @param string $key Memcached key of the content
-        * @return string|bool Feed's content on cache hit or false on cache miss
-        */
-       public function loadFromCache( $lastmod, $timekey, $key ) {
-               global $wgFeedCacheTimeout, $wgOut;
-
-               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-               $feedLastmod = $cache->get( $timekey );
-
-               if ( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) {
-                       /**
-                        * If the cached feed was rendered very recently, we may
-                        * go ahead and use it even if there have been edits made
-                        * since it was rendered. This keeps a swarm of requests
-                        * from being too bad on a super-frequently edited wiki.
-                        */
-
-                       $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod );
-                       $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod );
-                       $lastmodUnix = wfTimestamp( TS_UNIX, $lastmod );
-
-                       if ( $feedAge < $wgFeedCacheTimeout || $feedLastmodUnix > $lastmodUnix ) {
-                               wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
-                               if ( $feedLastmodUnix < $lastmodUnix ) {
-                                       $wgOut->setLastModified( $feedLastmod ); // T23916
-                               }
-                               return $cache->get( $key );
-                       } else {
-                               wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Generate the feed items given a row from the database, printing the feed.
-        * @param object $rows IDatabase resource with recentchanges rows
-        * @param ChannelFeed &$feed
-        */
-       public static function generateFeed( $rows, &$feed ) {
-               $items = self::buildItems( $rows );
-               $feed->outHeader();
-               foreach ( $items as $item ) {
-                       $feed->outItem( $item );
-               }
-               $feed->outFooter();
-       }
-
        /**
         * Generate the feed items given a row from the database.
         * @param object $rows IDatabase resource with recentchanges rows
diff --git a/includes/changes/ChannelFeed.php b/includes/changes/ChannelFeed.php
new file mode 100644 (file)
index 0000000..a1b832e
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+/**
+ * Copyright Â© 2004 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Class to support the outputting of syndication feeds in Atom and RSS format.
+ *
+ * @ingroup Feed
+ */
+abstract class ChannelFeed extends FeedItem {
+
+       /** @var TemplateParser */
+       protected $templateParser;
+
+       /**
+        * @param string|Title $title Feed's title
+        * @param string $description
+        * @param string $url URL uniquely designating the feed.
+        * @param string $date Feed's date
+        * @param string $author Author's user name
+        * @param string $comments
+        */
+       function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
+               parent::__construct( $title, $description, $url, $date, $author, $comments );
+               $this->templateParser = new TemplateParser();
+       }
+
+       /**
+        * Generate Header of the feed
+        * @par Example:
+        * @code
+        * print "<feed>";
+        * @endcode
+        */
+       abstract public function outHeader();
+
+       /**
+        * Generate an item
+        * @par Example:
+        * @code
+        * print "<item>...</item>";
+        * @endcode
+        * @param FeedItem $item
+        */
+       abstract public function outItem( $item );
+
+       /**
+        * Generate Footer of the feed
+        * @par Example:
+        * @code
+        * print "</feed>";
+        * @endcode
+        */
+       abstract public function outFooter();
+
+       /**
+        * Setup and send HTTP headers. Don't send any content;
+        * content might end up being cached and re-sent with
+        * these same headers later.
+        *
+        * This should be called from the outHeader() method,
+        * but can also be called separately.
+        */
+       public function httpHeaders() {
+               global $wgOut, $wgVaryOnXFP;
+
+               # We take over from $wgOut, excepting its cache header info
+               $wgOut->disable();
+               $mimetype = $this->contentType();
+               header( "Content-type: $mimetype; charset=UTF-8" );
+
+               // Set a sane filename
+               $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
+                       ->getExtensionsForType( $mimetype );
+               $ext = $exts ? strtok( $exts, ' ' ) : 'xml';
+               header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" );
+
+               if ( $wgVaryOnXFP ) {
+                       $wgOut->addVaryHeader( 'X-Forwarded-Proto' );
+               }
+               $wgOut->sendCacheControl();
+       }
+
+       /**
+        * Return an internet media type to be sent in the headers.
+        *
+        * @return string
+        */
+       private function contentType() {
+               global $wgRequest;
+
+               $ctype = $wgRequest->getVal( 'ctype', 'application/xml' );
+               $allowedctypes = [
+                       'application/xml',
+                       'text/xml',
+                       'application/rss+xml',
+                       'application/atom+xml'
+               ];
+
+               return ( in_array( $ctype, $allowedctypes ) ? $ctype : 'application/xml' );
+       }
+
+       /**
+        * Output the initial XML headers.
+        */
+       protected function outXmlHeader() {
+               $this->httpHeaders();
+               echo '<?xml version="1.0"?>' . "\n";
+       }
+}
diff --git a/includes/changes/FeedItem.php b/includes/changes/FeedItem.php
new file mode 100644 (file)
index 0000000..a6a2615
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Copyright Â© 2004 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @defgroup Feed Feed
+ */
+
+/**
+ * A base class for outputting syndication feeds (e.g. RSS and other formats).
+ *
+ * @ingroup Feed
+ */
+class FeedItem {
+       /** @var Title */
+       public $title;
+
+       public $description;
+
+       public $url;
+
+       public $date;
+
+       public $author;
+
+       public $uniqueId;
+
+       public $comments;
+
+       public $rssIsPermalink = false;
+
+       /**
+        * @param string|Title $title Item's title
+        * @param string $description
+        * @param string $url URL uniquely designating the item.
+        * @param string $date Item's date
+        * @param string $author Author's user name
+        * @param string $comments
+        */
+       function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
+               $this->title = $title;
+               $this->description = $description;
+               $this->url = $url;
+               $this->uniqueId = $url;
+               $this->date = $date;
+               $this->author = $author;
+               $this->comments = $comments;
+       }
+
+       /**
+        * Encode $string so that it can be safely embedded in a XML document
+        *
+        * @param string $string String to encode
+        * @return string
+        */
+       public function xmlEncode( $string ) {
+               $string = str_replace( "\r\n", "\n", $string );
+               $string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string );
+               return htmlspecialchars( $string );
+       }
+
+       /**
+        * Get the unique id of this item; already xml-encoded
+        * @return string
+        */
+       public function getUniqueID() {
+               $id = $this->getUniqueIdUnescaped();
+               if ( $id ) {
+                       return $this->xmlEncode( $id );
+               }
+       }
+
+       /**
+        * Get the unique id of this item, without any escaping
+        * @return string
+        */
+       public function getUniqueIdUnescaped() {
+               if ( $this->uniqueId ) {
+                       return wfExpandUrl( $this->uniqueId, PROTO_CURRENT );
+               }
+       }
+
+       /**
+        * Set the unique id of an item
+        *
+        * @param string $uniqueId Unique id for the item
+        * @param bool $rssIsPermalink Set to true if the guid (unique id) is a permalink (RSS feeds only)
+        */
+       public function setUniqueId( $uniqueId, $rssIsPermalink = false ) {
+               $this->uniqueId = $uniqueId;
+               $this->rssIsPermalink = $rssIsPermalink;
+       }
+
+       /**
+        * Get the title of this item; already xml-encoded
+        *
+        * @return string
+        */
+       public function getTitle() {
+               return $this->xmlEncode( $this->title );
+       }
+
+       /**
+        * Get the URL of this item; already xml-encoded
+        *
+        * @return string
+        */
+       public function getUrl() {
+               return $this->xmlEncode( $this->url );
+       }
+
+       /** Get the URL of this item without any escaping
+        *
+        * @return string
+        */
+       public function getUrlUnescaped() {
+               return $this->url;
+       }
+
+       /**
+        * Get the description of this item; already xml-encoded
+        *
+        * @return string
+        */
+       public function getDescription() {
+               return $this->xmlEncode( $this->description );
+       }
+
+       /**
+        * Get the description of this item without any escaping
+        *
+        * @return string
+        */
+       public function getDescriptionUnescaped() {
+               return $this->description;
+       }
+
+       /**
+        * Get the language of this item
+        *
+        * @return string
+        */
+       public function getLanguage() {
+               global $wgLanguageCode;
+               return LanguageCode::bcp47( $wgLanguageCode );
+       }
+
+       /**
+        * Get the date of this item
+        *
+        * @return string
+        */
+       public function getDate() {
+               return $this->date;
+       }
+
+       /**
+        * Get the author of this item; already xml-encoded
+        *
+        * @return string
+        */
+       public function getAuthor() {
+               return $this->xmlEncode( $this->author );
+       }
+
+       /**
+        * Get the author of this item without any escaping
+        *
+        * @return string
+        */
+       public function getAuthorUnescaped() {
+               return $this->author;
+       }
+
+       /**
+        * Get the comment of this item; already xml-encoded
+        *
+        * @return string
+        */
+       public function getComments() {
+               return $this->xmlEncode( $this->comments );
+       }
+
+       /**
+        * Get the comment of this item without any escaping
+        *
+        * @return string
+        */
+       public function getCommentsUnescaped() {
+               return $this->comments;
+       }
+
+       /**
+        * Quickie hack... strip out wikilinks to more legible form from the comment.
+        *
+        * @param string $text Wikitext
+        * @return string
+        */
+       public static function stripComment( $text ) {
+               return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
+       }
+       /**#@-*/
+}
diff --git a/includes/changes/RSSFeed.php b/includes/changes/RSSFeed.php
new file mode 100644 (file)
index 0000000..3b34500
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Copyright Â© 2004 Brion Vibber <brion@pobox.com>
+ * https://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Generate an RSS feed.
+ *
+ * @ingroup Feed
+ */
+class RSSFeed extends ChannelFeed {
+
+       /**
+        * Format a date given a timestamp. If a timestamp is not given, nothing is returned
+        *
+        * @param int|null $ts Timestamp
+        * @return string|null Date string
+        */
+       function formatTime( $ts ) {
+               if ( $ts ) {
+                       return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+               }
+       }
+
+       /**
+        * Output an RSS 2.0 header
+        */
+       function outHeader() {
+               global $wgVersion;
+
+               $this->outXmlHeader();
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       'title' => $this->getTitle(),
+                       'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       'description' => $this->getDescription(),
+                       'language' => $this->xmlEncode( $this->getLanguage() ),
+                       'version' => $this->xmlEncode( $wgVersion ),
+                       'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) )
+               ];
+               print $this->templateParser->processTemplate( 'RSSHeader', $templateParams );
+       }
+
+       /**
+        * Output an RSS 2.0 item
+        * @param FeedItem $item Item to be output
+        */
+       function outItem( $item ) {
+               // Manually escaping rather than letting Mustache do it because Mustache
+               // uses htmlentities, which does not work with XML
+               $templateParams = [
+                       "title" => $item->getTitle(),
+                       "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+                       "permalink" => $item->rssIsPermalink,
+                       "uniqueID" => $item->getUniqueID(),
+                       "description" => $item->getDescription(),
+                       "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+                       "author" => $item->getAuthor()
+               ];
+               $comments = $item->getCommentsUnescaped();
+               if ( $comments ) {
+                       $commentsEscaped = $this->xmlEncode( wfExpandUrl( $comments, PROTO_CURRENT ) );
+                       $templateParams["comments"] = $commentsEscaped;
+               }
+               print $this->templateParser->processTemplate( 'RSSItem', $templateParams );
+       }
+
+       /**
+        * Output an RSS 2.0 footer
+        */
+       function outFooter() {
+               print "</channel></rss>";
+       }
+}
index 7007316..88eae36 100644 (file)
@@ -22,6 +22,7 @@ namespace MediaWiki\Edit;
 
 use Content;
 use ParserOptions;
+use RuntimeException;
 use ParserOutput;
 
 /**
@@ -32,7 +33,6 @@ use ParserOutput;
  * @since 1.30
  */
 class PreparedEdit {
-
        /**
         * Time this prepared edit was made
         *
@@ -73,7 +73,7 @@ class PreparedEdit {
         *
         * @var ParserOutput|null
         */
-       public $output;
+       private $canonicalOutput;
 
        /**
         * Content that is being saved (before PST)
@@ -89,4 +89,36 @@ class PreparedEdit {
         */
        public $oldContent;
 
+       /**
+        * Lazy-loading callback to get canonical ParserOutput object
+        *
+        * @var callable
+        */
+       public $parserOutputCallback;
+
+       /**
+        * @return ParserOutput Canonical parser output
+        */
+       public function getOutput() {
+               if ( !$this->canonicalOutput ) {
+                       $this->canonicalOutput = call_user_func( $this->parserOutputCallback );
+               }
+
+               return $this->canonicalOutput;
+       }
+
+       /**
+        * Fetch the ParserOutput via a lazy-loaded callback (for backwards compatibility).
+        *
+        * @deprecated since 1.33
+        * @param string $name
+        * @return mixed
+        */
+       function __get( $name ) {
+               if ( $name === 'output' ) {
+                       return $this->getOutput();
+               }
+
+               throw new RuntimeException( "Undefined field $name." );
+       }
 }
index 6315de4..a219452 100644 (file)
@@ -37,8 +37,6 @@ abstract class DatabaseInstaller {
        /**
         * The Installer object.
         *
-        * @todo Naming this parent is confusing, 'installer' would be clearer.
-        *
         * @var WebInstaller
         */
        public $parent;
index 413fb2a..a326df2 100644 (file)
@@ -806,9 +806,7 @@ EOT;
                if ( $eocdrPos !== false ) {
                        $this->logger->info( __METHOD__ . ": ZIP signature present in $file\n" );
                        // Check if it really is a ZIP file, make sure the EOCDR is at the end (T40432)
-                       // FIXME: unpack()'s third argument was added in PHP 7.1
-                       // @phan-suppress-next-line PhanParamTooManyInternal
-                       $commentLength = unpack( "n", $tail, $eocdrPos + 20 )[0];
+                       $commentLength = unpack( "n", substr( $tail, $eocdrPos + 20 ) )[0];
                        if ( $eocdrPos + 22 + $commentLength !== strlen( $tail ) ) {
                                $this->logger->info( __METHOD__ . ": ZIP EOCDR not at end. Not a ZIP file." );
                        } else {
diff --git a/includes/logging/DatabaseLogEntry.php b/includes/logging/DatabaseLogEntry.php
new file mode 100644 (file)
index 0000000..db0f599
--- /dev/null
@@ -0,0 +1,231 @@
+<?php
+/**
+ * Contains a class for dealing with database log entries
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ * @since 1.19
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * A value class to process existing log entries. In other words, this class caches a log
+ * entry from the database and provides an immutable object-oriented representation of it.
+ *
+ * @since 1.19
+ */
+class DatabaseLogEntry extends LogEntryBase {
+
+       /**
+        * Returns array of information that is needed for querying
+        * log entries. Array contains the following keys:
+        * tables, fields, conds, options and join_conds
+        *
+        * @return array
+        */
+       public static function getSelectQueryData() {
+               $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
+               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
+
+               $tables = array_merge(
+                       [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ]
+               );
+               $fields = [
+                       'log_id', 'log_type', 'log_action', 'log_timestamp',
+                       'log_namespace', 'log_title', // unused log_page
+                       'log_params', 'log_deleted',
+                       'user_id', 'user_name', 'user_editcount',
+               ] + $commentQuery['fields'] + $actorQuery['fields'];
+
+               $joins = [
+                       // IPs don't have an entry in user table
+                       'user' => [ 'LEFT JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
+               ] + $commentQuery['joins'] + $actorQuery['joins'];
+
+               return [
+                       'tables' => $tables,
+                       'fields' => $fields,
+                       'conds' => [],
+                       'options' => [],
+                       'join_conds' => $joins,
+               ];
+       }
+
+       /**
+        * Constructs new LogEntry from database result row.
+        * Supports rows from both logging and recentchanges table.
+        *
+        * @param stdClass|array $row
+        * @return DatabaseLogEntry
+        */
+       public static function newFromRow( $row ) {
+               $row = (object)$row;
+               if ( isset( $row->rc_logid ) ) {
+                       return new RCDatabaseLogEntry( $row );
+               } else {
+                       return new self( $row );
+               }
+       }
+
+       /**
+        * Loads a LogEntry with the given id from database
+        *
+        * @param int $id
+        * @param IDatabase $db
+        * @return DatabaseLogEntry|null
+        */
+       public static function newFromId( $id, IDatabase $db ) {
+               $queryInfo = self::getSelectQueryData();
+               $queryInfo['conds'] += [ 'log_id' => $id ];
+               $row = $db->selectRow(
+                       $queryInfo['tables'],
+                       $queryInfo['fields'],
+                       $queryInfo['conds'],
+                       __METHOD__,
+                       $queryInfo['options'],
+                       $queryInfo['join_conds']
+               );
+               if ( !$row ) {
+                       return null;
+               }
+               return self::newFromRow( $row );
+       }
+
+       /** @var stdClass Database result row. */
+       protected $row;
+
+       /** @var User */
+       protected $performer;
+
+       /** @var array Parameters for log entry */
+       protected $params;
+
+       /** @var int A rev id associated to the log entry */
+       protected $revId = null;
+
+       /** @var bool Whether the parameters for this log entry are stored in new or old format. */
+       protected $legacy;
+
+       protected function __construct( $row ) {
+               $this->row = $row;
+       }
+
+       /**
+        * Returns the unique database id.
+        *
+        * @return int
+        */
+       public function getId() {
+               return (int)$this->row->log_id;
+       }
+
+       /**
+        * Returns whatever is stored in the database field.
+        *
+        * @return string
+        */
+       protected function getRawParameters() {
+               return $this->row->log_params;
+       }
+
+       public function isLegacy() {
+               // This extracts the property
+               $this->getParameters();
+               return $this->legacy;
+       }
+
+       public function getType() {
+               return $this->row->log_type;
+       }
+
+       public function getSubtype() {
+               return $this->row->log_action;
+       }
+
+       public function getParameters() {
+               if ( !isset( $this->params ) ) {
+                       $blob = $this->getRawParameters();
+                       Wikimedia\suppressWarnings();
+                       $params = LogEntryBase::extractParams( $blob );
+                       Wikimedia\restoreWarnings();
+                       if ( $params !== false ) {
+                               $this->params = $params;
+                               $this->legacy = false;
+                       } else {
+                               $this->params = LogPage::extractParams( $blob );
+                               $this->legacy = true;
+                       }
+
+                       if ( isset( $this->params['associated_rev_id'] ) ) {
+                               $this->revId = $this->params['associated_rev_id'];
+                               unset( $this->params['associated_rev_id'] );
+                       }
+               }
+
+               return $this->params;
+       }
+
+       public function getAssociatedRevId() {
+               // This extracts the property
+               $this->getParameters();
+               return $this->revId;
+       }
+
+       public function getPerformer() {
+               if ( !$this->performer ) {
+                       $actorId = isset( $this->row->log_actor ) ? (int)$this->row->log_actor : 0;
+                       $userId = (int)$this->row->log_user;
+                       if ( $userId !== 0 || $actorId !== 0 ) {
+                               // logged-in users
+                               if ( isset( $this->row->user_name ) ) {
+                                       $this->performer = User::newFromRow( $this->row );
+                               } elseif ( $actorId !== 0 ) {
+                                       $this->performer = User::newFromActorId( $actorId );
+                               } else {
+                                       $this->performer = User::newFromId( $userId );
+                               }
+                       } else {
+                               // IP users
+                               $userText = $this->row->log_user_text;
+                               $this->performer = User::newFromName( $userText, false );
+                       }
+               }
+
+               return $this->performer;
+       }
+
+       public function getTarget() {
+               $namespace = $this->row->log_namespace;
+               $page = $this->row->log_title;
+               return Title::makeTitle( $namespace, $page );
+       }
+
+       public function getTimestamp() {
+               return wfTimestamp( TS_MW, $this->row->log_timestamp );
+       }
+
+       public function getComment() {
+               return CommentStore::getStore()->getComment( 'log_comment', $this->row )->text;
+       }
+
+       public function getDeleted() {
+               return $this->row->log_deleted;
+       }
+}
diff --git a/includes/logging/LegacyLogFormatter.php b/includes/logging/LegacyLogFormatter.php
new file mode 100644 (file)
index 0000000..61104db
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+/**
+ * Contains a class for formatting log legacy entries
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ * @since 1.19
+ */
+
+/**
+ * This class formats all log entries for log types
+ * which have not been converted to the new system.
+ * This is not about old log entries which store
+ * parameters in a different format - the new
+ * LogFormatter classes have code to support formatting
+ * those too.
+ * @since 1.19
+ */
+class LegacyLogFormatter extends LogFormatter {
+       /**
+        * Backward compatibility for extension changing the comment from
+        * the LogLine hook. This will be set by the first call on getComment(),
+        * then it might be modified by the hook when calling getActionLinks(),
+        * so that the modified value will be returned when calling getComment()
+        * a second time.
+        *
+        * @var string|null
+        */
+       private $comment = null;
+
+       /**
+        * Cache for the result of getActionLinks() so that it does not need to
+        * run multiple times depending on the order that getComment() and
+        * getActionLinks() are called.
+        *
+        * @var string|null
+        */
+       private $revert = null;
+
+       public function getComment() {
+               if ( $this->comment === null ) {
+                       $this->comment = parent::getComment();
+               }
+
+               // Make sure we execute the LogLine hook so that we immediately return
+               // the correct value.
+               if ( $this->revert === null ) {
+                       $this->getActionLinks();
+               }
+
+               return $this->comment;
+       }
+
+       /**
+        * @return string
+        * @return-taint onlysafefor_html
+        */
+       protected function getActionMessage() {
+               $entry = $this->entry;
+               $action = LogPage::actionText(
+                       $entry->getType(),
+                       $entry->getSubtype(),
+                       $entry->getTarget(),
+                       $this->plaintext ? null : $this->context->getSkin(),
+                       (array)$entry->getParameters(),
+                       !$this->plaintext // whether to filter [[]] links
+               );
+
+               $performer = $this->getPerformerElement();
+               if ( !$this->irctext ) {
+                       $sep = $this->msg( 'word-separator' );
+                       $sep = $this->plaintext ? $sep->text() : $sep->escaped();
+                       $action = $performer . $sep . $action;
+               }
+
+               return $action;
+       }
+
+       public function getActionLinks() {
+               if ( $this->revert !== null ) {
+                       return $this->revert;
+               }
+
+               if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
+                       $this->revert = '';
+                       return $this->revert;
+               }
+
+               $title = $this->entry->getTarget();
+               $type = $this->entry->getType();
+               $subtype = $this->entry->getSubtype();
+
+               // Do nothing. The implementation is handled by the hook modifiying the
+               // passed-by-ref parameters. This also changes the default value so that
+               // getComment() and getActionLinks() do not call them indefinitely.
+               $this->revert = '';
+
+               // This is to populate the $comment member of this instance so that it
+               // can be modified when calling the hook just below.
+               if ( $this->comment === null ) {
+                       $this->getComment();
+               }
+
+               $params = $this->entry->getParameters();
+
+               Hooks::run( 'LogLine', [ $type, $subtype, $title, $params,
+                       &$this->comment, &$this->revert, $this->entry->getTimestamp() ] );
+
+               return $this->revert;
+       }
+}
index c5e4a92..17f72bd 100644 (file)
@@ -1,11 +1,6 @@
 <?php
 /**
- * Contain classes for dealing with individual log entries
- *
- * This is how I see the log system history:
- * - appending to plain wiki pages
- * - formatting log entries based on database fields
- * - user is now part of the action message
+ * Contains a class for dealing with individual log entries
  *
  * 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
  * @since 1.19
  */
 
-use MediaWiki\ChangeTags\Taggable;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\User\UserIdentity;
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Assert\Assert;
-
 /**
  * Interface for log entries. Every log entry has these methods.
  *
@@ -110,810 +99,3 @@ interface LogEntry {
         */
        public function isDeleted( $field );
 }
-
-/**
- * Extends the LogEntryInterface with some basic functionality
- *
- * @since 1.19
- */
-abstract class LogEntryBase implements LogEntry {
-
-       public function getFullType() {
-               return $this->getType() . '/' . $this->getSubtype();
-       }
-
-       public function isDeleted( $field ) {
-               return ( $this->getDeleted() & $field ) === $field;
-       }
-
-       /**
-        * Whether the parameters for this log are stored in new or
-        * old format.
-        *
-        * @return bool
-        */
-       public function isLegacy() {
-               return false;
-       }
-
-       /**
-        * Create a blob from a parameter array
-        *
-        * @since 1.26
-        * @param array $params
-        * @return string
-        */
-       public static function makeParamBlob( $params ) {
-               return serialize( (array)$params );
-       }
-
-       /**
-        * Extract a parameter array from a blob
-        *
-        * @since 1.26
-        * @param string $blob
-        * @return array
-        */
-       public static function extractParams( $blob ) {
-               return unserialize( $blob );
-       }
-}
-
-/**
- * A value class to process existing log entries. In other words, this class caches a log
- * entry from the database and provides an immutable object-oriented representation of it.
- *
- * @since 1.19
- */
-class DatabaseLogEntry extends LogEntryBase {
-
-       /**
-        * Returns array of information that is needed for querying
-        * log entries. Array contains the following keys:
-        * tables, fields, conds, options and join_conds
-        *
-        * @return array
-        */
-       public static function getSelectQueryData() {
-               $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
-               $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
-
-               $tables = array_merge(
-                       [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ]
-               );
-               $fields = [
-                       'log_id', 'log_type', 'log_action', 'log_timestamp',
-                       'log_namespace', 'log_title', // unused log_page
-                       'log_params', 'log_deleted',
-                       'user_id', 'user_name', 'user_editcount',
-               ] + $commentQuery['fields'] + $actorQuery['fields'];
-
-               $joins = [
-                       // IPs don't have an entry in user table
-                       'user' => [ 'LEFT JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
-               ] + $commentQuery['joins'] + $actorQuery['joins'];
-
-               return [
-                       'tables' => $tables,
-                       'fields' => $fields,
-                       'conds' => [],
-                       'options' => [],
-                       'join_conds' => $joins,
-               ];
-       }
-
-       /**
-        * Constructs new LogEntry from database result row.
-        * Supports rows from both logging and recentchanges table.
-        *
-        * @param stdClass|array $row
-        * @return DatabaseLogEntry
-        */
-       public static function newFromRow( $row ) {
-               $row = (object)$row;
-               if ( isset( $row->rc_logid ) ) {
-                       return new RCDatabaseLogEntry( $row );
-               } else {
-                       return new self( $row );
-               }
-       }
-
-       /**
-        * Loads a LogEntry with the given id from database
-        *
-        * @param int $id
-        * @param IDatabase $db
-        * @return DatabaseLogEntry|null
-        */
-       public static function newFromId( $id, IDatabase $db ) {
-               $queryInfo = self::getSelectQueryData();
-               $queryInfo['conds'] += [ 'log_id' => $id ];
-               $row = $db->selectRow(
-                       $queryInfo['tables'],
-                       $queryInfo['fields'],
-                       $queryInfo['conds'],
-                       __METHOD__,
-                       $queryInfo['options'],
-                       $queryInfo['join_conds']
-               );
-               if ( !$row ) {
-                       return null;
-               }
-               return self::newFromRow( $row );
-       }
-
-       /** @var stdClass Database result row. */
-       protected $row;
-
-       /** @var User */
-       protected $performer;
-
-       /** @var array Parameters for log entry */
-       protected $params;
-
-       /** @var int A rev id associated to the log entry */
-       protected $revId = null;
-
-       /** @var bool Whether the parameters for this log entry are stored in new or old format. */
-       protected $legacy;
-
-       protected function __construct( $row ) {
-               $this->row = $row;
-       }
-
-       /**
-        * Returns the unique database id.
-        *
-        * @return int
-        */
-       public function getId() {
-               return (int)$this->row->log_id;
-       }
-
-       /**
-        * Returns whatever is stored in the database field.
-        *
-        * @return string
-        */
-       protected function getRawParameters() {
-               return $this->row->log_params;
-       }
-
-       public function isLegacy() {
-               // This extracts the property
-               $this->getParameters();
-               return $this->legacy;
-       }
-
-       public function getType() {
-               return $this->row->log_type;
-       }
-
-       public function getSubtype() {
-               return $this->row->log_action;
-       }
-
-       public function getParameters() {
-               if ( !isset( $this->params ) ) {
-                       $blob = $this->getRawParameters();
-                       Wikimedia\suppressWarnings();
-                       $params = LogEntryBase::extractParams( $blob );
-                       Wikimedia\restoreWarnings();
-                       if ( $params !== false ) {
-                               $this->params = $params;
-                               $this->legacy = false;
-                       } else {
-                               $this->params = LogPage::extractParams( $blob );
-                               $this->legacy = true;
-                       }
-
-                       if ( isset( $this->params['associated_rev_id'] ) ) {
-                               $this->revId = $this->params['associated_rev_id'];
-                               unset( $this->params['associated_rev_id'] );
-                       }
-               }
-
-               return $this->params;
-       }
-
-       public function getAssociatedRevId() {
-               // This extracts the property
-               $this->getParameters();
-               return $this->revId;
-       }
-
-       public function getPerformer() {
-               if ( !$this->performer ) {
-                       $actorId = isset( $this->row->log_actor ) ? (int)$this->row->log_actor : 0;
-                       $userId = (int)$this->row->log_user;
-                       if ( $userId !== 0 || $actorId !== 0 ) {
-                               // logged-in users
-                               if ( isset( $this->row->user_name ) ) {
-                                       $this->performer = User::newFromRow( $this->row );
-                               } elseif ( $actorId !== 0 ) {
-                                       $this->performer = User::newFromActorId( $actorId );
-                               } else {
-                                       $this->performer = User::newFromId( $userId );
-                               }
-                       } else {
-                               // IP users
-                               $userText = $this->row->log_user_text;
-                               $this->performer = User::newFromName( $userText, false );
-                       }
-               }
-
-               return $this->performer;
-       }
-
-       public function getTarget() {
-               $namespace = $this->row->log_namespace;
-               $page = $this->row->log_title;
-               $title = Title::makeTitle( $namespace, $page );
-
-               return $title;
-       }
-
-       public function getTimestamp() {
-               return wfTimestamp( TS_MW, $this->row->log_timestamp );
-       }
-
-       public function getComment() {
-               return CommentStore::getStore()->getComment( 'log_comment', $this->row )->text;
-       }
-
-       public function getDeleted() {
-               return $this->row->log_deleted;
-       }
-}
-
-/**
- * A subclass of DatabaseLogEntry for objects constructed from entries in the
- * recentchanges table (rather than the logging table).
- */
-class RCDatabaseLogEntry extends DatabaseLogEntry {
-
-       public function getId() {
-               return $this->row->rc_logid;
-       }
-
-       protected function getRawParameters() {
-               return $this->row->rc_params;
-       }
-
-       public function getAssociatedRevId() {
-               return $this->row->rc_this_oldid;
-       }
-
-       public function getType() {
-               return $this->row->rc_log_type;
-       }
-
-       public function getSubtype() {
-               return $this->row->rc_log_action;
-       }
-
-       public function getPerformer() {
-               if ( !$this->performer ) {
-                       $actorId = isset( $this->row->rc_actor ) ? (int)$this->row->rc_actor : 0;
-                       $userId = (int)$this->row->rc_user;
-                       if ( $actorId !== 0 ) {
-                               $this->performer = User::newFromActorId( $actorId );
-                       } elseif ( $userId !== 0 ) {
-                               $this->performer = User::newFromId( $userId );
-                       } else {
-                               $userText = $this->row->rc_user_text;
-                               // Might be an IP, don't validate the username
-                               $this->performer = User::newFromName( $userText, false );
-                       }
-               }
-
-               return $this->performer;
-       }
-
-       public function getTarget() {
-               $namespace = $this->row->rc_namespace;
-               $page = $this->row->rc_title;
-               $title = Title::makeTitle( $namespace, $page );
-
-               return $title;
-       }
-
-       public function getTimestamp() {
-               return wfTimestamp( TS_MW, $this->row->rc_timestamp );
-       }
-
-       public function getComment() {
-               return CommentStore::getStore()
-                       // Legacy because the row may have used RecentChange::selectFields()
-                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $this->row )->text;
-       }
-
-       public function getDeleted() {
-               return $this->row->rc_deleted;
-       }
-}
-
-/**
- * Class for creating new log entries and inserting them into the database.
- *
- * @since 1.19
- */
-class ManualLogEntry extends LogEntryBase implements Taggable {
-       /** @var string Type of log entry */
-       protected $type;
-
-       /** @var string Sub type of log entry */
-       protected $subtype;
-
-       /** @var array Parameters for log entry */
-       protected $parameters = [];
-
-       /** @var array */
-       protected $relations = [];
-
-       /** @var User Performer of the action for the log entry */
-       protected $performer;
-
-       /** @var Title Target title for the log entry */
-       protected $target;
-
-       /** @var string Timestamp of creation of the log entry */
-       protected $timestamp;
-
-       /** @var string Comment for the log entry */
-       protected $comment = '';
-
-       /** @var int A rev id associated to the log entry */
-       protected $revId = 0;
-
-       /** @var string[] Change tags add to the log entry */
-       protected $tags = [];
-
-       /** @var int Deletion state of the log entry */
-       protected $deleted;
-
-       /** @var int ID of the log entry */
-       protected $id;
-
-       /** @var bool Can this log entry be patrolled? */
-       protected $isPatrollable = false;
-
-       /** @var bool Whether this is a legacy log entry */
-       protected $legacy = false;
-
-       /**
-        * @since 1.19
-        * @param string $type
-        * @param string $subtype
-        */
-       public function __construct( $type, $subtype ) {
-               $this->type = $type;
-               $this->subtype = $subtype;
-       }
-
-       /**
-        * Set extra log parameters.
-        *
-        * You can pass params to the log action message by prefixing the keys with
-        * a number and optional type, using colons to separate the fields. The
-        * numbering should start with number 4, the first three parameters are
-        * hardcoded for every message.
-        *
-        * If you want to store stuff that should not be available in messages, don't
-        * prefix the array key with a number and don't use the colons.
-        *
-        * Example:
-        *   $entry->setParameters(
-        *     '4::color' => 'blue',
-        *     '5:number:count' => 3000,
-        *     'animal' => 'dog'
-        *   );
-        *
-        * @since 1.19
-        * @param array $parameters Associative array
-        */
-       public function setParameters( $parameters ) {
-               $this->parameters = $parameters;
-       }
-
-       /**
-        * Declare arbitrary tag/value relations to this log entry.
-        * These can be used to filter log entries later on.
-        *
-        * @param array $relations Map of (tag => (list of values|value))
-        * @since 1.22
-        */
-       public function setRelations( array $relations ) {
-               $this->relations = $relations;
-       }
-
-       /**
-        * Set the user that performed the action being logged.
-        *
-        * @since 1.19
-        * @param UserIdentity $performer
-        */
-       public function setPerformer( UserIdentity $performer ) {
-               $this->performer = User::newFromIdentity( $performer );
-       }
-
-       /**
-        * Set the title of the object changed.
-        *
-        * @since 1.19
-        * @param LinkTarget $target
-        */
-       public function setTarget( LinkTarget $target ) {
-               $this->target = Title::newFromLinkTarget( $target );
-       }
-
-       /**
-        * Set the timestamp of when the logged action took place.
-        *
-        * @since 1.19
-        * @param string $timestamp
-        */
-       public function setTimestamp( $timestamp ) {
-               $this->timestamp = $timestamp;
-       }
-
-       /**
-        * Set a comment associated with the action being logged.
-        *
-        * @since 1.19
-        * @param string $comment
-        */
-       public function setComment( $comment ) {
-               $this->comment = $comment;
-       }
-
-       /**
-        * Set an associated revision id.
-        *
-        * For example, the ID of the revision that was inserted to mark a page move
-        * or protection, file upload, etc.
-        *
-        * @since 1.27
-        * @param int $revId
-        */
-       public function setAssociatedRevId( $revId ) {
-               $this->revId = $revId;
-       }
-
-       /**
-        * Set change tags for the log entry.
-        *
-        * Passing `null` means the same as empty array,
-        * for compatibility with WikiPage::doUpdateRestrictions().
-        *
-        * @since 1.27
-        * @param string|string[]|null $tags
-        * @deprecated since 1.33 Please use addTags() instead
-        */
-       public function setTags( $tags ) {
-               if ( $this->tags ) {
-                       wfDebug( 'Overwriting existing ManualLogEntry tags' );
-               }
-               $this->tags = [];
-               if ( $tags !== null ) {
-                       $this->addTags( $tags );
-               }
-       }
-
-       /**
-        * Add change tags for the log entry
-        *
-        * @since 1.33
-        * @param string|string[] $tags Tags to apply
-        */
-       public function addTags( $tags ) {
-               if ( is_string( $tags ) ) {
-                       $tags = [ $tags ];
-               }
-               Assert::parameterElementType( 'string', $tags, 'tags' );
-               $this->tags = array_unique( array_merge( $this->tags, $tags ) );
-       }
-
-       /**
-        * Set whether this log entry should be made patrollable
-        * This shouldn't depend on config, only on whether there is full support
-        * in the software for patrolling this log entry.
-        * False by default
-        *
-        * @since 1.27
-        * @param bool $patrollable
-        */
-       public function setIsPatrollable( $patrollable ) {
-               $this->isPatrollable = (bool)$patrollable;
-       }
-
-       /**
-        * Set the 'legacy' flag
-        *
-        * @since 1.25
-        * @param bool $legacy
-        */
-       public function setLegacy( $legacy ) {
-               $this->legacy = $legacy;
-       }
-
-       /**
-        * Set the 'deleted' flag.
-        *
-        * @since 1.19
-        * @param int $deleted One of LogPage::DELETED_* bitfield constants
-        */
-       public function setDeleted( $deleted ) {
-               $this->deleted = $deleted;
-       }
-
-       /**
-        * Insert the entry into the `logging` table.
-        *
-        * @param IDatabase|null $dbw
-        * @return int ID of the log entry
-        * @throws MWException
-        */
-       public function insert( IDatabase $dbw = null ) {
-               global $wgActorTableSchemaMigrationStage;
-
-               $dbw = $dbw ?: wfGetDB( DB_MASTER );
-
-               if ( $this->timestamp === null ) {
-                       $this->timestamp = wfTimestampNow();
-               }
-
-               // Trim spaces on user supplied text
-               $comment = trim( $this->getComment() );
-
-               $params = $this->getParameters();
-               $relations = $this->relations;
-
-               // Ensure actor relations are set
-               if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) &&
-                       empty( $relations['target_author_actor'] )
-               ) {
-                       $actorIds = [];
-                       if ( !empty( $relations['target_author_id'] ) ) {
-                               foreach ( $relations['target_author_id'] as $id ) {
-                                       $actorIds[] = User::newFromId( $id )->getActorId( $dbw );
-                               }
-                       }
-                       if ( !empty( $relations['target_author_ip'] ) ) {
-                               foreach ( $relations['target_author_ip'] as $ip ) {
-                                       $actorIds[] = User::newFromName( $ip, false )->getActorId( $dbw );
-                               }
-                       }
-                       if ( $actorIds ) {
-                               $relations['target_author_actor'] = $actorIds;
-                               $params['authorActors'] = $actorIds;
-                       }
-               }
-               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
-                       unset( $relations['target_author_id'], $relations['target_author_ip'] );
-                       unset( $params['authorIds'], $params['authorIPs'] );
-               }
-
-               // Additional fields for which there's no space in the database table schema
-               $revId = $this->getAssociatedRevId();
-               if ( $revId ) {
-                       $params['associated_rev_id'] = $revId;
-                       $relations['associated_rev_id'] = $revId;
-               }
-
-               $data = [
-                       'log_type' => $this->getType(),
-                       'log_action' => $this->getSubtype(),
-                       'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
-                       'log_namespace' => $this->getTarget()->getNamespace(),
-                       'log_title' => $this->getTarget()->getDBkey(),
-                       'log_page' => $this->getTarget()->getArticleID(),
-                       'log_params' => LogEntryBase::makeParamBlob( $params ),
-               ];
-               if ( isset( $this->deleted ) ) {
-                       $data['log_deleted'] = $this->deleted;
-               }
-               $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $comment );
-               $data += ActorMigration::newMigration()
-                       ->getInsertValues( $dbw, 'log_user', $this->getPerformer() );
-
-               $dbw->insert( 'logging', $data, __METHOD__ );
-               $this->id = $dbw->insertId();
-
-               $rows = [];
-               foreach ( $relations as $tag => $values ) {
-                       if ( !strlen( $tag ) ) {
-                               throw new MWException( "Got empty log search tag." );
-                       }
-
-                       if ( !is_array( $values ) ) {
-                               $values = [ $values ];
-                       }
-
-                       foreach ( $values as $value ) {
-                               $rows[] = [
-                                       'ls_field' => $tag,
-                                       'ls_value' => $value,
-                                       'ls_log_id' => $this->id
-                               ];
-                       }
-               }
-               if ( count( $rows ) ) {
-                       $dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' );
-               }
-
-               return $this->id;
-       }
-
-       /**
-        * Get a RecentChanges object for the log entry
-        *
-        * @param int $newId
-        * @return RecentChange
-        * @since 1.23
-        */
-       public function getRecentChange( $newId = 0 ) {
-               $formatter = LogFormatter::newFromEntry( $this );
-               $context = RequestContext::newExtraneousContext( $this->getTarget() );
-               $formatter->setContext( $context );
-
-               $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() );
-               $user = $this->getPerformer();
-               $ip = "";
-               if ( $user->isAnon() ) {
-                       // "MediaWiki default" and friends may have
-                       // no IP address in their name
-                       if ( IP::isIPAddress( $user->getName() ) ) {
-                               $ip = $user->getName();
-                       }
-               }
-
-               return RecentChange::newLogEntry(
-                       $this->getTimestamp(),
-                       $logpage,
-                       $user,
-                       $formatter->getPlainActionText(),
-                       $ip,
-                       $this->getType(),
-                       $this->getSubtype(),
-                       $this->getTarget(),
-                       $this->getComment(),
-                       LogEntryBase::makeParamBlob( $this->getParameters() ),
-                       $newId,
-                       $formatter->getIRCActionComment(), // Used for IRC feeds
-                       $this->getAssociatedRevId(), // Used for e.g. moves and uploads
-                       $this->getIsPatrollable()
-               );
-       }
-
-       /**
-        * Publish the log entry.
-        *
-        * @param int $newId Id of the log entry.
-        * @param string $to One of: rcandudp (default), rc, udp
-        */
-       public function publish( $newId, $to = 'rcandudp' ) {
-               $canAddTags = true;
-               // FIXME: this code should be removed once all callers properly call publish()
-               if ( $to === 'udp' && !$newId && !$this->getAssociatedRevId() ) {
-                       \MediaWiki\Logger\LoggerFactory::getInstance( 'logging' )->warning(
-                               'newId and/or revId must be set when calling ManualLogEntry::publish()',
-                               [
-                                       'newId' => $newId,
-                                       'to' => $to,
-                                       'revId' => $this->getAssociatedRevId(),
-                                       // pass a new exception to register the stack trace
-                                       'exception' => new RuntimeException()
-                               ]
-                       );
-                       $canAddTags = false;
-               }
-
-               DeferredUpdates::addCallableUpdate(
-                       function () use ( $newId, $to, $canAddTags ) {
-                               $log = new LogPage( $this->getType() );
-                               if ( !$log->isRestricted() ) {
-                                       Hooks::runWithoutAbort( 'ManualLogEntryBeforePublish', [ $this ] );
-                                       $rc = $this->getRecentChange( $newId );
-
-                                       if ( $to === 'rc' || $to === 'rcandudp' ) {
-                                               // save RC, passing tags so they are applied there
-                                               $rc->addTags( $this->getTags() );
-                                               $rc->save( $rc::SEND_NONE );
-                                       } else {
-                                               $tags = $this->getTags();
-                                               if ( $tags && $canAddTags ) {
-                                                       $revId = $this->getAssociatedRevId();
-                                                       ChangeTags::addTags(
-                                                               $tags,
-                                                               null,
-                                                               $revId > 0 ? $revId : null,
-                                                               $newId > 0 ? $newId : null
-                                                       );
-                                               }
-                                       }
-
-                                       if ( $to === 'udp' || $to === 'rcandudp' ) {
-                                               $rc->notifyRCFeeds();
-                                       }
-                               }
-                       },
-                       DeferredUpdates::POSTSEND,
-                       wfGetDB( DB_MASTER )
-               );
-       }
-
-       public function getType() {
-               return $this->type;
-       }
-
-       public function getSubtype() {
-               return $this->subtype;
-       }
-
-       public function getParameters() {
-               return $this->parameters;
-       }
-
-       /**
-        * @return User
-        */
-       public function getPerformer() {
-               return $this->performer;
-       }
-
-       /**
-        * @return Title
-        */
-       public function getTarget() {
-               return $this->target;
-       }
-
-       public function getTimestamp() {
-               $ts = $this->timestamp ?? wfTimestampNow();
-
-               return wfTimestamp( TS_MW, $ts );
-       }
-
-       public function getComment() {
-               return $this->comment;
-       }
-
-       /**
-        * @since 1.27
-        * @return int
-        */
-       public function getAssociatedRevId() {
-               return $this->revId;
-       }
-
-       /**
-        * @since 1.27
-        * @return string[]
-        */
-       public function getTags() {
-               return $this->tags;
-       }
-
-       /**
-        * Whether this log entry is patrollable
-        *
-        * @since 1.27
-        * @return bool
-        */
-       public function getIsPatrollable() {
-               return $this->isPatrollable;
-       }
-
-       /**
-        * @since 1.25
-        * @return bool
-        */
-       public function isLegacy() {
-               return $this->legacy;
-       }
-
-       public function getDeleted() {
-               return (int)$this->deleted;
-       }
-}
diff --git a/includes/logging/LogEntryBase.php b/includes/logging/LogEntryBase.php
new file mode 100644 (file)
index 0000000..170fc29
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Contains a class for dealing with individual log entries
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ * @since 1.19
+ */
+
+/**
+ * Extends the LogEntry Interface with some basic functionality
+ *
+ * @since 1.19
+ */
+abstract class LogEntryBase implements LogEntry {
+
+       public function getFullType() {
+               return $this->getType() . '/' . $this->getSubtype();
+       }
+
+       public function isDeleted( $field ) {
+               return ( $this->getDeleted() & $field ) === $field;
+       }
+
+       /**
+        * Whether the parameters for this log are stored in new or
+        * old format.
+        *
+        * @return bool
+        */
+       public function isLegacy() {
+               return false;
+       }
+
+       /**
+        * Create a blob from a parameter array
+        *
+        * @since 1.26
+        * @param array $params
+        * @return string
+        */
+       public static function makeParamBlob( $params ) {
+               return serialize( (array)$params );
+       }
+
+       /**
+        * Extract a parameter array from a blob
+        *
+        * @since 1.26
+        * @param string $blob
+        * @return array
+        */
+       public static function extractParams( $blob ) {
+               return unserialize( $blob );
+       }
+}
index b9bb70c..3e942ae 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * Contains classes for formatting log entries
+ * Contains a class for formatting log entries
  *
  * 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
@@ -910,106 +910,3 @@ class LogFormatter {
                return [ $name => $value ];
        }
 }
-
-/**
- * This class formats all log entries for log types
- * which have not been converted to the new system.
- * This is not about old log entries which store
- * parameters in a different format - the new
- * LogFormatter classes have code to support formatting
- * those too.
- * @since 1.19
- */
-class LegacyLogFormatter extends LogFormatter {
-       /**
-        * Backward compatibility for extension changing the comment from
-        * the LogLine hook. This will be set by the first call on getComment(),
-        * then it might be modified by the hook when calling getActionLinks(),
-        * so that the modified value will be returned when calling getComment()
-        * a second time.
-        *
-        * @var string|null
-        */
-       private $comment = null;
-
-       /**
-        * Cache for the result of getActionLinks() so that it does not need to
-        * run multiple times depending on the order that getComment() and
-        * getActionLinks() are called.
-        *
-        * @var string|null
-        */
-       private $revert = null;
-
-       public function getComment() {
-               if ( $this->comment === null ) {
-                       $this->comment = parent::getComment();
-               }
-
-               // Make sure we execute the LogLine hook so that we immediately return
-               // the correct value.
-               if ( $this->revert === null ) {
-                       $this->getActionLinks();
-               }
-
-               return $this->comment;
-       }
-
-       /**
-        * @return string
-        * @return-taint onlysafefor_html
-        */
-       protected function getActionMessage() {
-               $entry = $this->entry;
-               $action = LogPage::actionText(
-                       $entry->getType(),
-                       $entry->getSubtype(),
-                       $entry->getTarget(),
-                       $this->plaintext ? null : $this->context->getSkin(),
-                       (array)$entry->getParameters(),
-                       !$this->plaintext // whether to filter [[]] links
-               );
-
-               $performer = $this->getPerformerElement();
-               if ( !$this->irctext ) {
-                       $sep = $this->msg( 'word-separator' );
-                       $sep = $this->plaintext ? $sep->text() : $sep->escaped();
-                       $action = $performer . $sep . $action;
-               }
-
-               return $action;
-       }
-
-       public function getActionLinks() {
-               if ( $this->revert !== null ) {
-                       return $this->revert;
-               }
-
-               if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) {
-                       $this->revert = '';
-                       return $this->revert;
-               }
-
-               $title = $this->entry->getTarget();
-               $type = $this->entry->getType();
-               $subtype = $this->entry->getSubtype();
-
-               // Do nothing. The implementation is handled by the hook modifiying the
-               // passed-by-ref parameters. This also changes the default value so that
-               // getComment() and getActionLinks() do not call them indefinitely.
-               $this->revert = '';
-
-               // This is to populate the $comment member of this instance so that it
-               // can be modified when calling the hook just below.
-               if ( $this->comment === null ) {
-                       $this->getComment();
-               }
-
-               $params = $this->entry->getParameters();
-
-               Hooks::run( 'LogLine', [ $type, $subtype, $title, $params,
-                       &$this->comment, &$this->revert, $this->entry->getTimestamp() ] );
-
-               return $this->revert;
-       }
-}
diff --git a/includes/logging/ManualLogEntry.php b/includes/logging/ManualLogEntry.php
new file mode 100644 (file)
index 0000000..90c0a72
--- /dev/null
@@ -0,0 +1,515 @@
+<?php
+/**
+ * Contains a class for dealing with manual log entries
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ * @since 1.19
+ */
+
+use MediaWiki\ChangeTags\Taggable;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Class for creating new log entries and inserting them into the database.
+ *
+ * @since 1.19
+ */
+class ManualLogEntry extends LogEntryBase implements Taggable {
+       /** @var string Type of log entry */
+       protected $type;
+
+       /** @var string Sub type of log entry */
+       protected $subtype;
+
+       /** @var array Parameters for log entry */
+       protected $parameters = [];
+
+       /** @var array */
+       protected $relations = [];
+
+       /** @var User Performer of the action for the log entry */
+       protected $performer;
+
+       /** @var Title Target title for the log entry */
+       protected $target;
+
+       /** @var string Timestamp of creation of the log entry */
+       protected $timestamp;
+
+       /** @var string Comment for the log entry */
+       protected $comment = '';
+
+       /** @var int A rev id associated to the log entry */
+       protected $revId = 0;
+
+       /** @var string[] Change tags add to the log entry */
+       protected $tags = [];
+
+       /** @var int Deletion state of the log entry */
+       protected $deleted;
+
+       /** @var int ID of the log entry */
+       protected $id;
+
+       /** @var bool Can this log entry be patrolled? */
+       protected $isPatrollable = false;
+
+       /** @var bool Whether this is a legacy log entry */
+       protected $legacy = false;
+
+       /**
+        * @since 1.19
+        * @param string $type
+        * @param string $subtype
+        */
+       public function __construct( $type, $subtype ) {
+               $this->type = $type;
+               $this->subtype = $subtype;
+       }
+
+       /**
+        * Set extra log parameters.
+        *
+        * You can pass params to the log action message by prefixing the keys with
+        * a number and optional type, using colons to separate the fields. The
+        * numbering should start with number 4, the first three parameters are
+        * hardcoded for every message.
+        *
+        * If you want to store stuff that should not be available in messages, don't
+        * prefix the array key with a number and don't use the colons.
+        *
+        * Example:
+        *   $entry->setParameters(
+        *     '4::color' => 'blue',
+        *     '5:number:count' => 3000,
+        *     'animal' => 'dog'
+        *   );
+        *
+        * @since 1.19
+        * @param array $parameters Associative array
+        */
+       public function setParameters( $parameters ) {
+               $this->parameters = $parameters;
+       }
+
+       /**
+        * Declare arbitrary tag/value relations to this log entry.
+        * These can be used to filter log entries later on.
+        *
+        * @param array $relations Map of (tag => (list of values|value))
+        * @since 1.22
+        */
+       public function setRelations( array $relations ) {
+               $this->relations = $relations;
+       }
+
+       /**
+        * Set the user that performed the action being logged.
+        *
+        * @since 1.19
+        * @param UserIdentity $performer
+        */
+       public function setPerformer( UserIdentity $performer ) {
+               $this->performer = User::newFromIdentity( $performer );
+       }
+
+       /**
+        * Set the title of the object changed.
+        *
+        * @since 1.19
+        * @param LinkTarget $target
+        */
+       public function setTarget( LinkTarget $target ) {
+               $this->target = Title::newFromLinkTarget( $target );
+       }
+
+       /**
+        * Set the timestamp of when the logged action took place.
+        *
+        * @since 1.19
+        * @param string $timestamp
+        */
+       public function setTimestamp( $timestamp ) {
+               $this->timestamp = $timestamp;
+       }
+
+       /**
+        * Set a comment associated with the action being logged.
+        *
+        * @since 1.19
+        * @param string $comment
+        */
+       public function setComment( $comment ) {
+               $this->comment = $comment;
+       }
+
+       /**
+        * Set an associated revision id.
+        *
+        * For example, the ID of the revision that was inserted to mark a page move
+        * or protection, file upload, etc.
+        *
+        * @since 1.27
+        * @param int $revId
+        */
+       public function setAssociatedRevId( $revId ) {
+               $this->revId = $revId;
+       }
+
+       /**
+        * Set change tags for the log entry.
+        *
+        * Passing `null` means the same as empty array,
+        * for compatibility with WikiPage::doUpdateRestrictions().
+        *
+        * @since 1.27
+        * @param string|string[]|null $tags
+        * @deprecated since 1.33 Please use addTags() instead
+        */
+       public function setTags( $tags ) {
+               if ( $this->tags ) {
+                       wfDebug( 'Overwriting existing ManualLogEntry tags' );
+               }
+               $this->tags = [];
+               if ( $tags !== null ) {
+                       $this->addTags( $tags );
+               }
+       }
+
+       /**
+        * Add change tags for the log entry
+        *
+        * @since 1.33
+        * @param string|string[] $tags Tags to apply
+        */
+       public function addTags( $tags ) {
+               if ( is_string( $tags ) ) {
+                       $tags = [ $tags ];
+               }
+               Assert::parameterElementType( 'string', $tags, 'tags' );
+               $this->tags = array_unique( array_merge( $this->tags, $tags ) );
+       }
+
+       /**
+        * Set whether this log entry should be made patrollable
+        * This shouldn't depend on config, only on whether there is full support
+        * in the software for patrolling this log entry.
+        * False by default
+        *
+        * @since 1.27
+        * @param bool $patrollable
+        */
+       public function setIsPatrollable( $patrollable ) {
+               $this->isPatrollable = (bool)$patrollable;
+       }
+
+       /**
+        * Set the 'legacy' flag
+        *
+        * @since 1.25
+        * @param bool $legacy
+        */
+       public function setLegacy( $legacy ) {
+               $this->legacy = $legacy;
+       }
+
+       /**
+        * Set the 'deleted' flag.
+        *
+        * @since 1.19
+        * @param int $deleted One of LogPage::DELETED_* bitfield constants
+        */
+       public function setDeleted( $deleted ) {
+               $this->deleted = $deleted;
+       }
+
+       /**
+        * Insert the entry into the `logging` table.
+        *
+        * @param IDatabase|null $dbw
+        * @return int ID of the log entry
+        * @throws MWException
+        */
+       public function insert( IDatabase $dbw = null ) {
+               global $wgActorTableSchemaMigrationStage;
+
+               $dbw = $dbw ?: wfGetDB( DB_MASTER );
+
+               if ( $this->timestamp === null ) {
+                       $this->timestamp = wfTimestampNow();
+               }
+
+               // Trim spaces on user supplied text
+               $comment = trim( $this->getComment() );
+
+               $params = $this->getParameters();
+               $relations = $this->relations;
+
+               // Ensure actor relations are set
+               if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) &&
+                       empty( $relations['target_author_actor'] )
+               ) {
+                       $actorIds = [];
+                       if ( !empty( $relations['target_author_id'] ) ) {
+                               foreach ( $relations['target_author_id'] as $id ) {
+                                       $actorIds[] = User::newFromId( $id )->getActorId( $dbw );
+                               }
+                       }
+                       if ( !empty( $relations['target_author_ip'] ) ) {
+                               foreach ( $relations['target_author_ip'] as $ip ) {
+                                       $actorIds[] = User::newFromName( $ip, false )->getActorId( $dbw );
+                               }
+                       }
+                       if ( $actorIds ) {
+                               $relations['target_author_actor'] = $actorIds;
+                               $params['authorActors'] = $actorIds;
+                       }
+               }
+               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
+                       unset( $relations['target_author_id'], $relations['target_author_ip'] );
+                       unset( $params['authorIds'], $params['authorIPs'] );
+               }
+
+               // Additional fields for which there's no space in the database table schema
+               $revId = $this->getAssociatedRevId();
+               if ( $revId ) {
+                       $params['associated_rev_id'] = $revId;
+                       $relations['associated_rev_id'] = $revId;
+               }
+
+               $data = [
+                       'log_type' => $this->getType(),
+                       'log_action' => $this->getSubtype(),
+                       'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
+                       'log_namespace' => $this->getTarget()->getNamespace(),
+                       'log_title' => $this->getTarget()->getDBkey(),
+                       'log_page' => $this->getTarget()->getArticleID(),
+                       'log_params' => LogEntryBase::makeParamBlob( $params ),
+               ];
+               if ( isset( $this->deleted ) ) {
+                       $data['log_deleted'] = $this->deleted;
+               }
+               $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $comment );
+               $data += ActorMigration::newMigration()
+                       ->getInsertValues( $dbw, 'log_user', $this->getPerformer() );
+
+               $dbw->insert( 'logging', $data, __METHOD__ );
+               $this->id = $dbw->insertId();
+
+               $rows = [];
+               foreach ( $relations as $tag => $values ) {
+                       if ( !strlen( $tag ) ) {
+                               throw new MWException( "Got empty log search tag." );
+                       }
+
+                       if ( !is_array( $values ) ) {
+                               $values = [ $values ];
+                       }
+
+                       foreach ( $values as $value ) {
+                               $rows[] = [
+                                       'ls_field' => $tag,
+                                       'ls_value' => $value,
+                                       'ls_log_id' => $this->id
+                               ];
+                       }
+               }
+               if ( count( $rows ) ) {
+                       $dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' );
+               }
+
+               return $this->id;
+       }
+
+       /**
+        * Get a RecentChanges object for the log entry
+        *
+        * @param int $newId
+        * @return RecentChange
+        * @since 1.23
+        */
+       public function getRecentChange( $newId = 0 ) {
+               $formatter = LogFormatter::newFromEntry( $this );
+               $context = RequestContext::newExtraneousContext( $this->getTarget() );
+               $formatter->setContext( $context );
+
+               $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() );
+               $user = $this->getPerformer();
+               $ip = "";
+               if ( $user->isAnon() ) {
+                       // "MediaWiki default" and friends may have
+                       // no IP address in their name
+                       if ( IP::isIPAddress( $user->getName() ) ) {
+                               $ip = $user->getName();
+                       }
+               }
+
+               return RecentChange::newLogEntry(
+                       $this->getTimestamp(),
+                       $logpage,
+                       $user,
+                       $formatter->getPlainActionText(),
+                       $ip,
+                       $this->getType(),
+                       $this->getSubtype(),
+                       $this->getTarget(),
+                       $this->getComment(),
+                       LogEntryBase::makeParamBlob( $this->getParameters() ),
+                       $newId,
+                       $formatter->getIRCActionComment(), // Used for IRC feeds
+                       $this->getAssociatedRevId(), // Used for e.g. moves and uploads
+                       $this->getIsPatrollable()
+               );
+       }
+
+       /**
+        * Publish the log entry.
+        *
+        * @param int $newId Id of the log entry.
+        * @param string $to One of: rcandudp (default), rc, udp
+        */
+       public function publish( $newId, $to = 'rcandudp' ) {
+               $canAddTags = true;
+               // FIXME: this code should be removed once all callers properly call publish()
+               if ( $to === 'udp' && !$newId && !$this->getAssociatedRevId() ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'logging' )->warning(
+                               'newId and/or revId must be set when calling ManualLogEntry::publish()',
+                               [
+                                       'newId' => $newId,
+                                       'to' => $to,
+                                       'revId' => $this->getAssociatedRevId(),
+                                       // pass a new exception to register the stack trace
+                                       'exception' => new RuntimeException()
+                               ]
+                       );
+                       $canAddTags = false;
+               }
+
+               DeferredUpdates::addCallableUpdate(
+                       function () use ( $newId, $to, $canAddTags ) {
+                               $log = new LogPage( $this->getType() );
+                               if ( !$log->isRestricted() ) {
+                                       Hooks::runWithoutAbort( 'ManualLogEntryBeforePublish', [ $this ] );
+                                       $rc = $this->getRecentChange( $newId );
+
+                                       if ( $to === 'rc' || $to === 'rcandudp' ) {
+                                               // save RC, passing tags so they are applied there
+                                               $rc->addTags( $this->getTags() );
+                                               $rc->save( $rc::SEND_NONE );
+                                       } else {
+                                               $tags = $this->getTags();
+                                               if ( $tags && $canAddTags ) {
+                                                       $revId = $this->getAssociatedRevId();
+                                                       ChangeTags::addTags(
+                                                               $tags,
+                                                               null,
+                                                               $revId > 0 ? $revId : null,
+                                                               $newId > 0 ? $newId : null
+                                                       );
+                                               }
+                                       }
+
+                                       if ( $to === 'udp' || $to === 'rcandudp' ) {
+                                               $rc->notifyRCFeeds();
+                                       }
+                               }
+                       },
+                       DeferredUpdates::POSTSEND,
+                       wfGetDB( DB_MASTER )
+               );
+       }
+
+       public function getType() {
+               return $this->type;
+       }
+
+       public function getSubtype() {
+               return $this->subtype;
+       }
+
+       public function getParameters() {
+               return $this->parameters;
+       }
+
+       /**
+        * @return User
+        */
+       public function getPerformer() {
+               return $this->performer;
+       }
+
+       /**
+        * @return Title
+        */
+       public function getTarget() {
+               return $this->target;
+       }
+
+       public function getTimestamp() {
+               $ts = $this->timestamp ?? wfTimestampNow();
+
+               return wfTimestamp( TS_MW, $ts );
+       }
+
+       public function getComment() {
+               return $this->comment;
+       }
+
+       /**
+        * @since 1.27
+        * @return int
+        */
+       public function getAssociatedRevId() {
+               return $this->revId;
+       }
+
+       /**
+        * @since 1.27
+        * @return string[]
+        */
+       public function getTags() {
+               return $this->tags;
+       }
+
+       /**
+        * Whether this log entry is patrollable
+        *
+        * @since 1.27
+        * @return bool
+        */
+       public function getIsPatrollable() {
+               return $this->isPatrollable;
+       }
+
+       /**
+        * @since 1.25
+        * @return bool
+        */
+       public function isLegacy() {
+               return $this->legacy;
+       }
+
+       public function getDeleted() {
+               return (int)$this->deleted;
+       }
+}
diff --git a/includes/logging/RCDatabaseLogEntry.php b/includes/logging/RCDatabaseLogEntry.php
new file mode 100644 (file)
index 0000000..4dc4037
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Contains a class for dealing with recent changes database log entries
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0-or-later
+ * @since 1.19
+ */
+
+/**
+ * A subclass of DatabaseLogEntry for objects constructed from entries in the
+ * recentchanges table (rather than the logging table).
+ */
+class RCDatabaseLogEntry extends DatabaseLogEntry {
+
+       public function getId() {
+               return $this->row->rc_logid;
+       }
+
+       protected function getRawParameters() {
+               return $this->row->rc_params;
+       }
+
+       public function getAssociatedRevId() {
+               return $this->row->rc_this_oldid;
+       }
+
+       public function getType() {
+               return $this->row->rc_log_type;
+       }
+
+       public function getSubtype() {
+               return $this->row->rc_log_action;
+       }
+
+       public function getPerformer() {
+               if ( !$this->performer ) {
+                       $actorId = isset( $this->row->rc_actor ) ? (int)$this->row->rc_actor : 0;
+                       $userId = (int)$this->row->rc_user;
+                       if ( $actorId !== 0 ) {
+                               $this->performer = User::newFromActorId( $actorId );
+                       } elseif ( $userId !== 0 ) {
+                               $this->performer = User::newFromId( $userId );
+                       } else {
+                               $userText = $this->row->rc_user_text;
+                               // Might be an IP, don't validate the username
+                               $this->performer = User::newFromName( $userText, false );
+                       }
+               }
+
+               return $this->performer;
+       }
+
+       public function getTarget() {
+               $namespace = $this->row->rc_namespace;
+               $page = $this->row->rc_title;
+               return Title::makeTitle( $namespace, $page );
+       }
+
+       public function getTimestamp() {
+               return wfTimestamp( TS_MW, $this->row->rc_timestamp );
+       }
+
+       public function getComment() {
+               return CommentStore::getStore()
+                       // Legacy because the row may have used RecentChange::selectFields()
+                       ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $this->row )->text;
+       }
+
+       public function getDeleted() {
+               return $this->row->rc_deleted;
+       }
+}
index bc5eb09..ac332b7 100644 (file)
@@ -35,361 +35,3 @@ class SVGMetadataExtractor {
                return $svg->getMetadata();
        }
 }
-
-/**
- * @ingroup Media
- */
-class SVGReader {
-       const DEFAULT_WIDTH = 512;
-       const DEFAULT_HEIGHT = 512;
-       const NS_SVG = 'http://www.w3.org/2000/svg';
-       const LANG_PREFIX_MATCH = 1;
-       const LANG_FULL_MATCH = 2;
-
-       /** @var null|XMLReader */
-       private $reader = null;
-
-       /** @var bool */
-       private $mDebug = false;
-
-       /** @var array */
-       private $metadata = [];
-       private $languages = [];
-       private $languagePrefixes = [];
-
-       /**
-        * Creates an SVGReader drawing from the source provided
-        * @param string $source URI from which to read
-        * @throws MWException|Exception
-        */
-       function __construct( $source ) {
-               global $wgSVGMetadataCutoff;
-               $this->reader = new XMLReader();
-
-               // Don't use $file->getSize() since file object passed to SVGHandler::getMetadata is bogus.
-               $size = filesize( $source );
-               if ( $size === false ) {
-                       throw new MWException( "Error getting filesize of SVG." );
-               }
-
-               if ( $size > $wgSVGMetadataCutoff ) {
-                       $this->debug( "SVG is $size bytes, which is bigger than $wgSVGMetadataCutoff. Truncating." );
-                       $contents = file_get_contents( $source, false, null, 0, $wgSVGMetadataCutoff );
-                       if ( $contents === false ) {
-                               throw new MWException( 'Error reading SVG file.' );
-                       }
-                       $this->reader->XML( $contents, null, LIBXML_NOERROR | LIBXML_NOWARNING );
-               } else {
-                       $this->reader->open( $source, null, LIBXML_NOERROR | LIBXML_NOWARNING );
-               }
-
-               // Expand entities, since Adobe Illustrator uses them for xmlns
-               // attributes (T33719). Note that libxml2 has some protection
-               // against large recursive entity expansions so this is not as
-               // insecure as it might appear to be. However, it is still extremely
-               // insecure. It's necessary to wrap any read() calls with
-               // libxml_disable_entity_loader() to avoid arbitrary local file
-               // inclusion, or even arbitrary code execution if the expect
-               // extension is installed (T48859).
-               $oldDisable = libxml_disable_entity_loader( true );
-               $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES, true );
-
-               $this->metadata['width'] = self::DEFAULT_WIDTH;
-               $this->metadata['height'] = self::DEFAULT_HEIGHT;
-
-               // The size in the units specified by the SVG file
-               // (for the metadata box)
-               // Per the SVG spec, if unspecified, default to '100%'
-               $this->metadata['originalWidth'] = '100%';
-               $this->metadata['originalHeight'] = '100%';
-
-               // Because we cut off the end of the svg making an invalid one. Complicated
-               // try catch thing to make sure warnings get restored. Seems like there should
-               // be a better way.
-               Wikimedia\suppressWarnings();
-               try {
-                       $this->read();
-               } catch ( Exception $e ) {
-                       // Note, if this happens, the width/height will be taken to be 0x0.
-                       // Should we consider it the default 512x512 instead?
-                       Wikimedia\restoreWarnings();
-                       libxml_disable_entity_loader( $oldDisable );
-                       throw $e;
-               }
-               Wikimedia\restoreWarnings();
-               libxml_disable_entity_loader( $oldDisable );
-       }
-
-       /**
-        * @return array Array with the known metadata
-        */
-       public function getMetadata() {
-               return $this->metadata;
-       }
-
-       /**
-        * Read the SVG
-        * @throws MWException
-        * @return bool
-        */
-       protected function read() {
-               $keepReading = $this->reader->read();
-
-               /* Skip until first element */
-               while ( $keepReading && $this->reader->nodeType != XMLReader::ELEMENT ) {
-                       $keepReading = $this->reader->read();
-               }
-
-               if ( $this->reader->localName != 'svg' || $this->reader->namespaceURI != self::NS_SVG ) {
-                       throw new MWException( "Expected <svg> tag, got " .
-                               $this->reader->localName . " in NS " . $this->reader->namespaceURI );
-               }
-               $this->debug( "<svg> tag is correct." );
-               $this->handleSVGAttribs();
-
-               $exitDepth = $this->reader->depth;
-               $keepReading = $this->reader->read();
-               while ( $keepReading ) {
-                       $tag = $this->reader->localName;
-                       $type = $this->reader->nodeType;
-                       $isSVG = ( $this->reader->namespaceURI == self::NS_SVG );
-
-                       $this->debug( "$tag" );
-
-                       if ( $isSVG && $tag == 'svg' && $type == XMLReader::END_ELEMENT
-                               && $this->reader->depth <= $exitDepth
-                       ) {
-                               break;
-                       } elseif ( $isSVG && $tag == 'title' ) {
-                               $this->readField( $tag, 'title' );
-                       } elseif ( $isSVG && $tag == 'desc' ) {
-                               $this->readField( $tag, 'description' );
-                       } elseif ( $isSVG && $tag == 'metadata' && $type == XMLReader::ELEMENT ) {
-                               $this->readXml( 'metadata' );
-                       } elseif ( $isSVG && $tag == 'script' ) {
-                               // We normally do not allow scripted svgs.
-                               // However its possible to configure MW to let them
-                               // in, and such files should be considered animated.
-                               $this->metadata['animated'] = true;
-                       } elseif ( $tag !== '#text' ) {
-                               $this->debug( "Unhandled top-level XML tag $tag" );
-
-                               // Recurse into children of current tag, looking for animation and languages.
-                               $this->animateFilterAndLang( $tag );
-                       }
-
-                       // Goto next element, which is sibling of current (Skip children).
-                       $keepReading = $this->reader->next();
-               }
-
-               $this->reader->close();
-
-               $this->metadata['translations'] = $this->languages + $this->languagePrefixes;
-
-               return true;
-       }
-
-       /**
-        * Read a textelement from an element
-        *
-        * @param string $name Name of the element that we are reading from
-        * @param string $metafield Field that we will fill with the result
-        */
-       private function readField( $name, $metafield = null ) {
-               $this->debug( "Read field $metafield" );
-               if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) {
-                       return;
-               }
-               $keepReading = $this->reader->read();
-               while ( $keepReading ) {
-                       if ( $this->reader->localName == $name
-                               && $this->reader->namespaceURI == self::NS_SVG
-                               && $this->reader->nodeType == XMLReader::END_ELEMENT
-                       ) {
-                               break;
-                       } elseif ( $this->reader->nodeType == XMLReader::TEXT ) {
-                               $this->metadata[$metafield] = trim( $this->reader->value );
-                       }
-                       $keepReading = $this->reader->read();
-               }
-       }
-
-       /**
-        * Read an XML snippet from an element
-        *
-        * @param string $metafield Field that we will fill with the result
-        * @throws MWException
-        */
-       private function readXml( $metafield = null ) {
-               $this->debug( "Read top level metadata" );
-               if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) {
-                       return;
-               }
-               // @todo Find and store type of xml snippet. metadata['metadataType'] = "rdf"
-               $this->metadata[$metafield] = trim( $this->reader->readInnerXml() );
-
-               $this->reader->next();
-       }
-
-       /**
-        * Filter all children, looking for animated elements.
-        * Also get a list of languages that can be targeted.
-        *
-        * @param string $name Name of the element that we are reading from
-        */
-       private function animateFilterAndLang( $name ) {
-               $this->debug( "animate filter for tag $name" );
-               if ( $this->reader->nodeType != XMLReader::ELEMENT ) {
-                       return;
-               }
-               if ( $this->reader->isEmptyElement ) {
-                       return;
-               }
-               $exitDepth = $this->reader->depth;
-               $keepReading = $this->reader->read();
-               while ( $keepReading ) {
-                       if ( $this->reader->localName == $name && $this->reader->depth <= $exitDepth
-                               && $this->reader->nodeType == XMLReader::END_ELEMENT
-                       ) {
-                               break;
-                       } elseif ( $this->reader->namespaceURI == self::NS_SVG
-                               && $this->reader->nodeType == XMLReader::ELEMENT
-                       ) {
-                               $sysLang = $this->reader->getAttribute( 'systemLanguage' );
-                               if ( !is_null( $sysLang ) && $sysLang !== '' ) {
-                                       // See https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
-                                       $langList = explode( ',', $sysLang );
-                                       foreach ( $langList as $langItem ) {
-                                               $langItem = trim( $langItem );
-                                               if ( Language::isWellFormedLanguageTag( $langItem ) ) {
-                                                       $this->languages[$langItem] = self::LANG_FULL_MATCH;
-                                               }
-                                               // Note, the standard says that any prefix should work,
-                                               // here we do only the initial prefix, since that will catch
-                                               // 99% of cases, and we are going to compare against fallbacks.
-                                               // This differs mildly from how the spec says languages should be
-                                               // handled, however it matches better how the MediaWiki language
-                                               // preference is generally handled.
-                                               $dash = strpos( $langItem, '-' );
-                                               // Intentionally checking both !false and > 0 at the same time.
-                                               if ( $dash ) {
-                                                       $itemPrefix = substr( $langItem, 0, $dash );
-                                                       if ( Language::isWellFormedLanguageTag( $itemPrefix ) ) {
-                                                               $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH;
-                                                       }
-                                               }
-                                       }
-                               }
-                               switch ( $this->reader->localName ) {
-                                       case 'script':
-                                               // Normally we disallow files with
-                                               // <script>, but its possible
-                                               // to configure MW to disable
-                                               // such checks.
-                                       case 'animate':
-                                       case 'set':
-                                       case 'animateMotion':
-                                       case 'animateColor':
-                                       case 'animateTransform':
-                                               $this->debug( "HOUSTON WE HAVE ANIMATION" );
-                                               $this->metadata['animated'] = true;
-                                               break;
-                               }
-                       }
-                       $keepReading = $this->reader->read();
-               }
-       }
-
-       private function debug( $data ) {
-               if ( $this->mDebug ) {
-                       wfDebug( "SVGReader: $data\n" );
-               }
-       }
-
-       /**
-        * Parse the attributes of an SVG element
-        *
-        * The parser has to be in the start element of "<svg>"
-        */
-       private function handleSVGAttribs() {
-               $defaultWidth = self::DEFAULT_WIDTH;
-               $defaultHeight = self::DEFAULT_HEIGHT;
-               $aspect = 1.0;
-               $width = null;
-               $height = null;
-
-               if ( $this->reader->getAttribute( 'viewBox' ) ) {
-                       // min-x min-y width height
-                       $viewBox = preg_split( '/\s*[\s,]\s*/', trim( $this->reader->getAttribute( 'viewBox' ) ) );
-                       if ( count( $viewBox ) == 4 ) {
-                               $viewWidth = $this->scaleSVGUnit( $viewBox[2] );
-                               $viewHeight = $this->scaleSVGUnit( $viewBox[3] );
-                               if ( $viewWidth > 0 && $viewHeight > 0 ) {
-                                       $aspect = $viewWidth / $viewHeight;
-                                       $defaultHeight = $defaultWidth / $aspect;
-                               }
-                       }
-               }
-               if ( $this->reader->getAttribute( 'width' ) ) {
-                       $width = $this->scaleSVGUnit( $this->reader->getAttribute( 'width' ), $defaultWidth );
-                       $this->metadata['originalWidth'] = $this->reader->getAttribute( 'width' );
-               }
-               if ( $this->reader->getAttribute( 'height' ) ) {
-                       $height = $this->scaleSVGUnit( $this->reader->getAttribute( 'height' ), $defaultHeight );
-                       $this->metadata['originalHeight'] = $this->reader->getAttribute( 'height' );
-               }
-
-               if ( !isset( $width ) && !isset( $height ) ) {
-                       $width = $defaultWidth;
-                       $height = $width / $aspect;
-               } elseif ( isset( $width ) && !isset( $height ) ) {
-                       $height = $width / $aspect;
-               } elseif ( isset( $height ) && !isset( $width ) ) {
-                       $width = $height * $aspect;
-               }
-
-               if ( $width > 0 && $height > 0 ) {
-                       $this->metadata['width'] = intval( round( $width ) );
-                       $this->metadata['height'] = intval( round( $height ) );
-               }
-       }
-
-       /**
-        * Return a rounded pixel equivalent for a labeled CSS/SVG length.
-        * https://www.w3.org/TR/SVG11/coords.html#Units
-        *
-        * @param string $length CSS/SVG length.
-        * @param float|int $viewportSize Optional scale for percentage units...
-        * @return float Length in pixels
-        */
-       static function scaleSVGUnit( $length, $viewportSize = 512 ) {
-               static $unitLength = [
-                       'px' => 1.0,
-                       'pt' => 1.25,
-                       'pc' => 15.0,
-                       'mm' => 3.543307,
-                       'cm' => 35.43307,
-                       'in' => 90.0,
-                       'em' => 16.0, // fake it?
-                       'ex' => 12.0, // fake it?
-                       '' => 1.0, // "User units" pixels by default
-               ];
-               $matches = [];
-               if ( preg_match(
-                       '/^\s*([-+]?\d*(?:\.\d+|\d+)(?:[Ee][-+]?\d+)?)\s*(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/',
-                       $length,
-                       $matches
-               ) ) {
-                       $length = floatval( $matches[1] );
-                       $unit = $matches[2];
-                       if ( $unit == '%' ) {
-                               return $length * 0.01 * $viewportSize;
-                       } else {
-                               return $length * $unitLength[$unit];
-                       }
-               } else {
-                       // Assume pixels
-                       return floatval( $length );
-               }
-       }
-}
diff --git a/includes/media/SVGReader.php b/includes/media/SVGReader.php
new file mode 100644 (file)
index 0000000..480aec5
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+/**
+ * Extraction of SVG image metadata.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Media
+ * @author "Derk-Jan Hartman <hartman _at_ videolan d0t org>"
+ * @author Brion Vibber
+ * @copyright Copyright Â© 2010-2010 Brion Vibber, Derk-Jan Hartman
+ * @license GPL-2.0-or-later
+ */
+
+/**
+ * @ingroup Media
+ */
+class SVGReader {
+       const DEFAULT_WIDTH = 512;
+       const DEFAULT_HEIGHT = 512;
+       const NS_SVG = 'http://www.w3.org/2000/svg';
+       const LANG_PREFIX_MATCH = 1;
+       const LANG_FULL_MATCH = 2;
+
+       /** @var null|XMLReader */
+       private $reader = null;
+
+       /** @var bool */
+       private $mDebug = false;
+
+       /** @var array */
+       private $metadata = [];
+       private $languages = [];
+       private $languagePrefixes = [];
+
+       /**
+        * Creates an SVGReader drawing from the source provided
+        * @param string $source URI from which to read
+        * @throws MWException|Exception
+        */
+       function __construct( $source ) {
+               global $wgSVGMetadataCutoff;
+               $this->reader = new XMLReader();
+
+               // Don't use $file->getSize() since file object passed to SVGHandler::getMetadata is bogus.
+               $size = filesize( $source );
+               if ( $size === false ) {
+                       throw new MWException( "Error getting filesize of SVG." );
+               }
+
+               if ( $size > $wgSVGMetadataCutoff ) {
+                       $this->debug( "SVG is $size bytes, which is bigger than $wgSVGMetadataCutoff. Truncating." );
+                       $contents = file_get_contents( $source, false, null, 0, $wgSVGMetadataCutoff );
+                       if ( $contents === false ) {
+                               throw new MWException( 'Error reading SVG file.' );
+                       }
+                       $this->reader->XML( $contents, null, LIBXML_NOERROR | LIBXML_NOWARNING );
+               } else {
+                       $this->reader->open( $source, null, LIBXML_NOERROR | LIBXML_NOWARNING );
+               }
+
+               // Expand entities, since Adobe Illustrator uses them for xmlns
+               // attributes (T33719). Note that libxml2 has some protection
+               // against large recursive entity expansions so this is not as
+               // insecure as it might appear to be. However, it is still extremely
+               // insecure. It's necessary to wrap any read() calls with
+               // libxml_disable_entity_loader() to avoid arbitrary local file
+               // inclusion, or even arbitrary code execution if the expect
+               // extension is installed (T48859).
+               $oldDisable = libxml_disable_entity_loader( true );
+               $this->reader->setParserProperty( XMLReader::SUBST_ENTITIES, true );
+
+               $this->metadata['width'] = self::DEFAULT_WIDTH;
+               $this->metadata['height'] = self::DEFAULT_HEIGHT;
+
+               // The size in the units specified by the SVG file
+               // (for the metadata box)
+               // Per the SVG spec, if unspecified, default to '100%'
+               $this->metadata['originalWidth'] = '100%';
+               $this->metadata['originalHeight'] = '100%';
+
+               // Because we cut off the end of the svg making an invalid one. Complicated
+               // try catch thing to make sure warnings get restored. Seems like there should
+               // be a better way.
+               Wikimedia\suppressWarnings();
+               try {
+                       $this->read();
+               } catch ( Exception $e ) {
+                       // Note, if this happens, the width/height will be taken to be 0x0.
+                       // Should we consider it the default 512x512 instead?
+                       Wikimedia\restoreWarnings();
+                       libxml_disable_entity_loader( $oldDisable );
+                       throw $e;
+               }
+               Wikimedia\restoreWarnings();
+               libxml_disable_entity_loader( $oldDisable );
+       }
+
+       /**
+        * @return array Array with the known metadata
+        */
+       public function getMetadata() {
+               return $this->metadata;
+       }
+
+       /**
+        * Read the SVG
+        * @throws MWException
+        * @return bool
+        */
+       protected function read() {
+               $keepReading = $this->reader->read();
+
+               /* Skip until first element */
+               while ( $keepReading && $this->reader->nodeType != XMLReader::ELEMENT ) {
+                       $keepReading = $this->reader->read();
+               }
+
+               if ( $this->reader->localName != 'svg' || $this->reader->namespaceURI != self::NS_SVG ) {
+                       throw new MWException( "Expected <svg> tag, got " .
+                               $this->reader->localName . " in NS " . $this->reader->namespaceURI );
+               }
+               $this->debug( "<svg> tag is correct." );
+               $this->handleSVGAttribs();
+
+               $exitDepth = $this->reader->depth;
+               $keepReading = $this->reader->read();
+               while ( $keepReading ) {
+                       $tag = $this->reader->localName;
+                       $type = $this->reader->nodeType;
+                       $isSVG = ( $this->reader->namespaceURI == self::NS_SVG );
+
+                       $this->debug( "$tag" );
+
+                       if ( $isSVG && $tag == 'svg' && $type == XMLReader::END_ELEMENT
+                               && $this->reader->depth <= $exitDepth
+                       ) {
+                               break;
+                       } elseif ( $isSVG && $tag == 'title' ) {
+                               $this->readField( $tag, 'title' );
+                       } elseif ( $isSVG && $tag == 'desc' ) {
+                               $this->readField( $tag, 'description' );
+                       } elseif ( $isSVG && $tag == 'metadata' && $type == XMLReader::ELEMENT ) {
+                               $this->readXml( 'metadata' );
+                       } elseif ( $isSVG && $tag == 'script' ) {
+                               // We normally do not allow scripted svgs.
+                               // However its possible to configure MW to let them
+                               // in, and such files should be considered animated.
+                               $this->metadata['animated'] = true;
+                       } elseif ( $tag !== '#text' ) {
+                               $this->debug( "Unhandled top-level XML tag $tag" );
+
+                               // Recurse into children of current tag, looking for animation and languages.
+                               $this->animateFilterAndLang( $tag );
+                       }
+
+                       // Goto next element, which is sibling of current (Skip children).
+                       $keepReading = $this->reader->next();
+               }
+
+               $this->reader->close();
+
+               $this->metadata['translations'] = $this->languages + $this->languagePrefixes;
+
+               return true;
+       }
+
+       /**
+        * Read a textelement from an element
+        *
+        * @param string $name Name of the element that we are reading from
+        * @param string $metafield Field that we will fill with the result
+        */
+       private function readField( $name, $metafield = null ) {
+               $this->debug( "Read field $metafield" );
+               if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) {
+                       return;
+               }
+               $keepReading = $this->reader->read();
+               while ( $keepReading ) {
+                       if ( $this->reader->localName == $name
+                               && $this->reader->namespaceURI == self::NS_SVG
+                               && $this->reader->nodeType == XMLReader::END_ELEMENT
+                       ) {
+                               break;
+                       } elseif ( $this->reader->nodeType == XMLReader::TEXT ) {
+                               $this->metadata[$metafield] = trim( $this->reader->value );
+                       }
+                       $keepReading = $this->reader->read();
+               }
+       }
+
+       /**
+        * Read an XML snippet from an element
+        *
+        * @param string $metafield Field that we will fill with the result
+        * @throws MWException
+        */
+       private function readXml( $metafield = null ) {
+               $this->debug( "Read top level metadata" );
+               if ( !$metafield || $this->reader->nodeType != XMLReader::ELEMENT ) {
+                       return;
+               }
+               // @todo Find and store type of xml snippet. metadata['metadataType'] = "rdf"
+               $this->metadata[$metafield] = trim( $this->reader->readInnerXml() );
+
+               $this->reader->next();
+       }
+
+       /**
+        * Filter all children, looking for animated elements.
+        * Also get a list of languages that can be targeted.
+        *
+        * @param string $name Name of the element that we are reading from
+        */
+       private function animateFilterAndLang( $name ) {
+               $this->debug( "animate filter for tag $name" );
+               if ( $this->reader->nodeType != XMLReader::ELEMENT ) {
+                       return;
+               }
+               if ( $this->reader->isEmptyElement ) {
+                       return;
+               }
+               $exitDepth = $this->reader->depth;
+               $keepReading = $this->reader->read();
+               while ( $keepReading ) {
+                       if ( $this->reader->localName == $name && $this->reader->depth <= $exitDepth
+                               && $this->reader->nodeType == XMLReader::END_ELEMENT
+                       ) {
+                               break;
+                       } elseif ( $this->reader->namespaceURI == self::NS_SVG
+                               && $this->reader->nodeType == XMLReader::ELEMENT
+                       ) {
+                               $sysLang = $this->reader->getAttribute( 'systemLanguage' );
+                               if ( !is_null( $sysLang ) && $sysLang !== '' ) {
+                                       // See https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
+                                       $langList = explode( ',', $sysLang );
+                                       foreach ( $langList as $langItem ) {
+                                               $langItem = trim( $langItem );
+                                               if ( Language::isWellFormedLanguageTag( $langItem ) ) {
+                                                       $this->languages[$langItem] = self::LANG_FULL_MATCH;
+                                               }
+                                               // Note, the standard says that any prefix should work,
+                                               // here we do only the initial prefix, since that will catch
+                                               // 99% of cases, and we are going to compare against fallbacks.
+                                               // This differs mildly from how the spec says languages should be
+                                               // handled, however it matches better how the MediaWiki language
+                                               // preference is generally handled.
+                                               $dash = strpos( $langItem, '-' );
+                                               // Intentionally checking both !false and > 0 at the same time.
+                                               if ( $dash ) {
+                                                       $itemPrefix = substr( $langItem, 0, $dash );
+                                                       if ( Language::isWellFormedLanguageTag( $itemPrefix ) ) {
+                                                               $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH;
+                                                       }
+                                               }
+                                       }
+                               }
+                               switch ( $this->reader->localName ) {
+                                       case 'script':
+                                               // Normally we disallow files with
+                                               // <script>, but its possible
+                                               // to configure MW to disable
+                                               // such checks.
+                                       case 'animate':
+                                       case 'set':
+                                       case 'animateMotion':
+                                       case 'animateColor':
+                                       case 'animateTransform':
+                                               $this->debug( "HOUSTON WE HAVE ANIMATION" );
+                                               $this->metadata['animated'] = true;
+                                               break;
+                               }
+                       }
+                       $keepReading = $this->reader->read();
+               }
+       }
+
+       private function debug( $data ) {
+               if ( $this->mDebug ) {
+                       wfDebug( "SVGReader: $data\n" );
+               }
+       }
+
+       /**
+        * Parse the attributes of an SVG element
+        *
+        * The parser has to be in the start element of "<svg>"
+        */
+       private function handleSVGAttribs() {
+               $defaultWidth = self::DEFAULT_WIDTH;
+               $defaultHeight = self::DEFAULT_HEIGHT;
+               $aspect = 1.0;
+               $width = null;
+               $height = null;
+
+               if ( $this->reader->getAttribute( 'viewBox' ) ) {
+                       // min-x min-y width height
+                       $viewBox = preg_split( '/\s*[\s,]\s*/', trim( $this->reader->getAttribute( 'viewBox' ) ) );
+                       if ( count( $viewBox ) == 4 ) {
+                               $viewWidth = $this->scaleSVGUnit( $viewBox[2] );
+                               $viewHeight = $this->scaleSVGUnit( $viewBox[3] );
+                               if ( $viewWidth > 0 && $viewHeight > 0 ) {
+                                       $aspect = $viewWidth / $viewHeight;
+                                       $defaultHeight = $defaultWidth / $aspect;
+                               }
+                       }
+               }
+               if ( $this->reader->getAttribute( 'width' ) ) {
+                       $width = $this->scaleSVGUnit( $this->reader->getAttribute( 'width' ), $defaultWidth );
+                       $this->metadata['originalWidth'] = $this->reader->getAttribute( 'width' );
+               }
+               if ( $this->reader->getAttribute( 'height' ) ) {
+                       $height = $this->scaleSVGUnit( $this->reader->getAttribute( 'height' ), $defaultHeight );
+                       $this->metadata['originalHeight'] = $this->reader->getAttribute( 'height' );
+               }
+
+               if ( !isset( $width ) && !isset( $height ) ) {
+                       $width = $defaultWidth;
+                       $height = $width / $aspect;
+               } elseif ( isset( $width ) && !isset( $height ) ) {
+                       $height = $width / $aspect;
+               } elseif ( isset( $height ) && !isset( $width ) ) {
+                       $width = $height * $aspect;
+               }
+
+               if ( $width > 0 && $height > 0 ) {
+                       $this->metadata['width'] = intval( round( $width ) );
+                       $this->metadata['height'] = intval( round( $height ) );
+               }
+       }
+
+       /**
+        * Return a rounded pixel equivalent for a labeled CSS/SVG length.
+        * https://www.w3.org/TR/SVG11/coords.html#Units
+        *
+        * @param string $length CSS/SVG length.
+        * @param float|int $viewportSize Optional scale for percentage units...
+        * @return float Length in pixels
+        */
+       static function scaleSVGUnit( $length, $viewportSize = 512 ) {
+               static $unitLength = [
+                       'px' => 1.0,
+                       'pt' => 1.25,
+                       'pc' => 15.0,
+                       'mm' => 3.543307,
+                       'cm' => 35.43307,
+                       'in' => 90.0,
+                       'em' => 16.0, // fake it?
+                       'ex' => 12.0, // fake it?
+                       '' => 1.0, // "User units" pixels by default
+               ];
+               $matches = [];
+               if ( preg_match(
+                       '/^\s*([-+]?\d*(?:\.\d+|\d+)(?:[Ee][-+]?\d+)?)\s*(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/',
+                       $length,
+                       $matches
+               ) ) {
+                       $length = floatval( $matches[1] );
+                       $unit = $matches[2];
+                       if ( $unit == '%' ) {
+                               return $length * 0.01 * $viewportSize;
+                       } else {
+                               return $length * $unitLength[$unit];
+                       }
+               } else {
+                       // Assume pixels
+                       return floatval( $length );
+               }
+       }
+}
index bdca848..66b1612 100644 (file)
@@ -123,7 +123,7 @@ class ParserOptions {
         */
 
        /**
-        * Fetch an option, generically
+        * Fetch an option and track that is was accessed
         * @since 1.30
         * @param string $name Option name
         * @return mixed
@@ -133,15 +133,22 @@ class ParserOptions {
                        throw new InvalidArgumentException( "Unknown parser option $name" );
                }
 
-               if ( isset( self::$lazyOptions[$name] ) && $this->options[$name] === null ) {
-                       $this->options[$name] = call_user_func( self::$lazyOptions[$name], $this, $name );
-               }
+               $this->lazyLoadOption( $name );
                if ( !empty( self::$inCacheKey[$name] ) ) {
                        $this->optionUsed( $name );
                }
                return $this->options[$name];
        }
 
+       /**
+        * @param string $name Lazy load option without tracking usage
+        */
+       private function lazyLoadOption( $name ) {
+               if ( isset( self::$lazyOptions[$name] ) && $this->options[$name] === null ) {
+                       $this->options[$name] = call_user_func( self::$lazyOptions[$name], $this, $name );
+               }
+       }
+
        /**
         * Set an option, generically
         * @since 1.30
@@ -1197,22 +1204,16 @@ class ParserOptions {
         * @since 1.25
         */
        public function matches( ParserOptions $other ) {
-               // Populate lazy options
-               foreach ( self::$lazyOptions as $name => $callback ) {
-                       if ( $this->options[$name] === null ) {
-                               $this->options[$name] = call_user_func( $callback, $this, $name );
-                       }
-                       if ( $other->options[$name] === null ) {
-                               $other->options[$name] = call_user_func( $callback, $other, $name );
-                       }
-               }
-
                // Compare most options
                $options = array_keys( $this->options );
                $options = array_diff( $options, [
                        'enableLimitReport', // only affects HTML comments
                ] );
                foreach ( $options as $option ) {
+                       // Resolve any lazy options
+                       $this->lazyLoadOption( $option );
+                       $other->lazyLoadOption( $option );
+
                        $o1 = $this->optionToString( $this->options[$option] );
                        $o2 = $this->optionToString( $other->options[$option] );
                        if ( $o1 !== $o2 ) {
@@ -1238,6 +1239,27 @@ class ParserOptions {
                return true;
        }
 
+       /**
+        * @param ParserOptions $other
+        * @return bool Whether the cache key relevant options match those of $other
+        * @since 1.33
+        */
+       public function matchesForCacheKey( ParserOptions $other ) {
+               foreach ( self::allCacheVaryingOptions() as $option ) {
+                       // Populate any lazy options
+                       $this->lazyLoadOption( $option );
+                       $other->lazyLoadOption( $option );
+
+                       $o1 = $this->optionToString( $this->options[$option] );
+                       $o2 = $this->optionToString( $other->options[$option] );
+                       if ( $o1 !== $o2 ) {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
        /**
         * Registers a callback for tracking which ParserOptions which are used.
         * This is a private API with the parser.
@@ -1314,10 +1336,9 @@ class ParserOptions {
                $inCacheKey = self::allCacheVaryingOptions();
 
                // Resolve any lazy options
-               foreach ( array_intersect( $forOptions, $inCacheKey, array_keys( self::$lazyOptions ) ) as $k ) {
-                       if ( $this->options[$k] === null ) {
-                               $this->options[$k] = call_user_func( self::$lazyOptions[$k], $this, $k );
-                       }
+               $lazyOpts = array_intersect( $forOptions, $inCacheKey, array_keys( self::$lazyOptions ) );
+               foreach ( $lazyOpts as $k ) {
+                       $this->lazyLoadOption( $k );
                }
 
                $options = $this->options;
index be2bf08..e354d55 100644 (file)
@@ -43,7 +43,7 @@ use MWTimestamp;
 use OutputPage;
 use Parser;
 use ParserOptions;
-use PreferencesFormLegacy;
+use PreferencesFormOOUI;
 use Psr\Log\LoggerAwareTrait;
 use Psr\Log\NullLogger;
 use Skin;
@@ -1430,7 +1430,7 @@ class DefaultPreferencesFactory implements PreferencesFactory {
        public function getForm(
                User $user,
                IContextSource $context,
-               $formClass = PreferencesFormLegacy::class,
+               $formClass = PreferencesFormOOUI::class,
                array $remove = []
        ) {
                // We use ButtonWidgets in some of the getPreferences() functions
index 478edce..6602a0a 100644 (file)
@@ -22,13 +22,14 @@ namespace MediaWiki\Preferences;
 
 use HTMLForm;
 use IContextSource;
+use PreferencesFormOOUI;
 use User;
 
 /**
  * A PreferencesFactory is a MediaWiki service that provides the definitions of preferences for a
  * given user. These definitions are in the form of an HTMLForm descriptor.
  *
- * PreferencesFormLegacy (a subclass of HTMLForm) is used to generate the Preferences form, and
+ * PreferencesFormOOUI (a subclass of HTMLForm) is used to generate the Preferences form, and
  * handles generic submission, CSRF protection, layout and other logic in a reusable manner.
  *
  * In order to generate the form, the HTMLForm object needs an array structure detailing the
@@ -62,7 +63,7 @@ interface PreferencesFactory {
        public function getForm(
                User $user,
                IContextSource $contextSource,
-               $formClass = \PreferencesFormLegacy::class,
+               $formClass = PreferencesFormOOUI::class,
                array $remove = []
        );
 
index c27cd2c..5329572 100644 (file)
@@ -58,6 +58,11 @@ class ExtensionDependencyError extends Exception {
         */
        public $missingPhpExtensions = [];
 
+       /**
+        * @var string[]
+        */
+       public $missingAbilities = [];
+
        /**
         * @param array $errors Each error has a 'msg' and 'type' key at minimum
         */
@@ -75,6 +80,9 @@ class ExtensionDependencyError extends Exception {
                                case 'missing-phpExtension':
                                        $this->missingPhpExtensions[] = $info['missing'];
                                        break;
+                               case 'missing-ability':
+                                       $this->missingAbilities[] = $info['missing'];
+                                       break;
                                case 'missing-skins':
                                        $this->missingSkins[] = $info['missing'];
                                        break;
index e3df499..2607e5a 100644 (file)
@@ -2,6 +2,8 @@
 
 use Composer\Semver\Semver;
 use Wikimedia\ScopedCallback;
+use MediaWiki\Shell\Shell;
+use MediaWiki\ShellDisabledError;
 
 /**
  * ExtensionRegistry class
@@ -144,7 +146,8 @@ class ExtensionRegistry {
                // A few more things to vary the cache on
                $versions = [
                        'registration' => self::CACHE_VERSION,
-                       'mediawiki' => $wgVersion
+                       'mediawiki' => $wgVersion,
+                       'abilities' => $this->getAbilities(),
                ];
 
                // We use a try/catch because we don't want to fail here
@@ -207,6 +210,38 @@ class ExtensionRegistry {
                $this->finished = true;
        }
 
+       /**
+        * Get the list of abilities and their values
+        * @return bool[]
+        */
+       private function getAbilities() {
+               return [
+                       'shell' => !Shell::isDisabled(),
+               ];
+       }
+
+       /**
+        * Queries information about the software environment and constructs an appropiate version checker
+        *
+        * @return VersionChecker
+        */
+       private function buildVersionChecker() {
+               global $wgVersion;
+               // array to optionally specify more verbose error messages for
+               // missing abilities
+               $abilityErrors = [
+                       'shell' => ( new ShellDisabledError() )->getMessage(),
+               ];
+
+               return new VersionChecker(
+                       $wgVersion,
+                       PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
+                       get_loaded_extensions(),
+                       $this->getAbilities(),
+                       $abilityErrors
+               );
+       }
+
        /**
         * Process a queue of extensions and return their extracted data
         *
@@ -216,16 +251,11 @@ class ExtensionRegistry {
         * @throws ExtensionDependencyError
         */
        public function readFromQueue( array $queue ) {
-               global $wgVersion;
                $autoloadClasses = [];
                $autoloadNamespaces = [];
                $autoloaderPaths = [];
                $processor = new ExtensionProcessor();
-               $versionChecker = new VersionChecker(
-                       $wgVersion,
-                       PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
-                       get_loaded_extensions()
-               );
+               $versionChecker = $this->buildVersionChecker();
                $extDependencies = [];
                $incompatible = [];
                $warnings = false;
index 586729d..a5d1fa1 100644 (file)
@@ -45,6 +45,16 @@ class VersionChecker {
         */
        private $phpExtensions = [];
 
+       /**
+        * @var bool[] List of provided abilities
+        */
+       private $abilities = [];
+
+       /**
+        * @var string[] List of provided ability errors
+        */
+       private $abilityErrors = [];
+
        /**
         * @var array Loaded extensions
         */
@@ -59,12 +69,19 @@ class VersionChecker {
         * @param string $coreVersion Current version of core
         * @param string $phpVersion Current PHP version
         * @param string[] $phpExtensions List of installed PHP extensions
+        * @param bool[] $abilities List of provided abilities
+        * @param string[] $abilityErrors Error messages for the abilities
         */
-       public function __construct( $coreVersion, $phpVersion, array $phpExtensions ) {
+       public function __construct(
+               $coreVersion, $phpVersion, array $phpExtensions,
+               array $abilities = [], array $abilityErrors = []
+       ) {
                $this->versionParser = new VersionParser();
                $this->setCoreVersion( $coreVersion );
                $this->setPhpVersion( $phpVersion );
                $this->phpExtensions = $phpExtensions;
+               $this->abilities = $abilities;
+               $this->abilityErrors = $abilityErrors;
        }
 
        /**
@@ -121,7 +138,8 @@ class VersionChecker {
         *         'MediaWiki' => '>= 1.25.0',
         *         'platform': {
         *           'php': '>= 7.0.0',
-        *           'ext-foo': '*'
+        *           'ext-foo': '*',
+        *           'ability-bar': true
         *         },
         *         'extensions' => {
         *           'FooBaz' => '>= 1.25.0'
@@ -193,6 +211,37 @@ class VersionChecker {
                                                                                'missing' => $phpExtension,
                                                                        ];
                                                                }
+                                                       } elseif ( substr( $dependency, 0, 8 ) === 'ability-' ) {
+                                                               // Other abilities the environment might provide.
+                                                               $ability = substr( $dependency, 8 );
+                                                               if ( !isset( $this->abilities[$ability] ) ) {
+                                                                       throw new UnexpectedValueException( 'Dependency type '
+                                                                       . $dependency . ' unknown in ' . $extension );
+                                                               }
+                                                               if ( !is_bool( $constraint ) ) {
+                                                                       throw new UnexpectedValueException( 'Only booleans are '
+                                                                               . 'allowed to to indicate the presence of abilities '
+                                                                               . 'in ' . $extension );
+                                                               }
+
+                                                               if ( $constraint === true &&
+                                                                       $this->abilities[$ability] !== true
+                                                               ) {
+                                                                       // add custom error message for missing ability if specified
+                                                                       $customMessage = '';
+                                                                       if ( isset( $this->abilityErrors[$ability] ) ) {
+                                                                               $customMessage = ': ' . $this->abilityErrors[$ability];
+                                                                       }
+
+                                                                       $errors[] = [
+                                                                               'msg' =>
+                                                                                       "{$extension} requires \"{$ability}\" ability"
+                                                                                       . $customMessage
+                                                                               ,
+                                                                               'type' => 'missing-ability',
+                                                                               'missing' => $ability,
+                                                                       ];
+                                                               }
                                                        } else {
                                                                // add other platform dependencies here
                                                                throw new UnexpectedValueException( 'Dependency type ' . $dependency .
index 7fb2df2..8d60e0f 100644 (file)
@@ -1119,6 +1119,10 @@ MESSAGE;
 
                                if ( !$context->getDebug() ) {
                                        $strContent = self::filter( $filter, $strContent );
+                               } else {
+                                       // In debug mode, separate each response by a new line.
+                                       // For example, between 'mw.loader.implement();' statements.
+                                       $strContent = $this->ensureNewline( $strContent );
                                }
 
                                if ( $context->getOnly() === 'scripts' ) {
index cc7ed55..0bc9147 100644 (file)
@@ -106,7 +106,7 @@ class SpecialPreferences extends SpecialPage {
         * Get the preferences form to use.
         * @param User $user The user.
         * @param IContextSource $context The context.
-        * @return PreferencesFormLegacy|HTMLForm
+        * @return PreferencesFormOOUI|HTMLForm
         */
        protected function getFormObject( $user, IContextSource $context ) {
                $preferencesFactory = MediaWikiServices::getInstance()->getPreferencesFactory();
diff --git a/includes/specials/forms/PreferencesFormLegacy.php b/includes/specials/forms/PreferencesFormLegacy.php
deleted file mode 100644 (file)
index 951e5ce..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-/**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * Form to edit user preferences.
- *
- * @since 1.32
- */
-class PreferencesFormLegacy extends PreferencesFormOOUI {
-       // No-op
-}
-
-/**
- * Retain the old class name for backwards compatibility.
- *
- * @deprecated since 1.32
- */
-class_alias( PreferencesFormLegacy::class, 'PreferencesForm' );
index 511cd03..81cb92d 100644 (file)
@@ -2,6 +2,8 @@
  * Copy of CC standard stylesheet, plus tweaks for iframe usage
  */
 
+/* stylelint-disable selector-class-pattern */
+
 body {
        margin: 0;
        background: #eee;
index 1b2574d..8b3b39e 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 .env-check {
        font-size: 90%;
        margin: 1em 0 1em 2.5em;
index c941da0..7ff7c11 100644 (file)
@@ -1,5 +1,7 @@
 @import 'mediawiki.mixins';
 
+/* stylelint-disable selector-class-pattern */
+
 /* Table Sorting */
 
 .client-js .sortable:not( .jquery-tablesorter ) > thead > :last-of-type > th:not( .unsortable ),
index ea60702..c239a8f 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 .tipsy {
        padding: 5px;
        position: absolute;
index 78c4c04..ac68b7a 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 .jquery-confirmable-button {
        /* Automatically flipped */
        margin-left: 1ex;
index 825c7ca..7c6d032 100644 (file)
@@ -1,5 +1,7 @@
 /* suggestions plugin */
 
+/* stylelint-disable selector-class-pattern */
+
 .suggestions {
        overflow: hidden;
        position: absolute;
index b5a9665..b8c3a44 100644 (file)
@@ -12,6 +12,7 @@
 
 /* Show/hide animation is incorrect if the table has a margin set. Extra
  * ".wikitable" is needed in the selector for CSS specificity. */
+/* stylelint-disable-next-line selector-class-pattern */
 .wikitable.preview-limit-report {
        margin: 0;
 }
index 1367426..fc806c6 100644 (file)
@@ -2,6 +2,8 @@
  * Styles for elements of the editing form.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /*
  * Add a bit of margin space between the preview and the toolbar.
  * This replaces the ugly <p><br /></p> we used to insert into the page source
index 520917a..5425990 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 /* Styles for the JavaScript enhancements of the history page */
 
 #pagehistory li.before input[ name='oldid' ],
index af91818..1265637 100644 (file)
@@ -2,6 +2,8 @@
  * Basic styles for the edit revision history page 'HistoryAction.php'
  */
 
+/* stylelint-disable selector-class-pattern */
+
 // Trigger only when collapsible & JS is available via `.mw-collapsed`.
 #mw-history-search.mw-collapsed .oo-ui-fieldsetLayout-header .oo-ui-labelElement-label {
        margin-bottom: 0;
index abdee12..274b3d3 100644 (file)
@@ -16,6 +16,7 @@
        }
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .redirect-in-category {
        font-style: italic;
 }
index b643d76..b8d4e70 100644 (file)
@@ -2,6 +2,8 @@
  * File description page
  */
 
+/* stylelint-disable selector-class-pattern */
+
 .mw-filepage-resolutioninfo {
        font-size: smaller;
 }
index f21b111..dad3238 100644 (file)
@@ -13,6 +13,7 @@
 }
 
 @media print {
+       /* stylelint-disable-next-line selector-class-pattern */
        .mw_metadata .mw-metadata-show-hide-extended {
                display: none;
        }
index 46976d4..c40b1c3 100644 (file)
@@ -1,5 +1,7 @@
 @import 'mediawiki.mixins';
 
+/* stylelint-disable selector-class-pattern */
+
 .postedit-container {
        margin: 0 auto;
        position: fixed;
index dccbacc..b5eaf8e 100644 (file)
@@ -2,6 +2,8 @@
  * Display neat icons on redirect pages.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /* Hide, but keep accessible for screen-readers. */
 .redirectMsg p {
        overflow: hidden;
index 7528fdb..d1f32ab 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 .apihelp-header {
        clear: both;
        margin-bottom: 0.1em;
index 99e4569..3e921f4 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 .mw-special-ApiHelp h1.firstHeading {
        display: none;
 }
index e084ab8..ca950d5 100644 (file)
@@ -18,6 +18,7 @@
        padding: 0.5em 1em;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .mw-json .value,
 .mw-json-single-value {
        background-color: #dcfae3;
index a56e459..272e7e0 100644 (file)
                        border-bottom: 1px solid #eee;
                        word-wrap: break-word;
 
+                       /* stylelint-disable-next-line selector-class-pattern */
                        &.nr {
                                text-align: right;
                        }
 
+                       /* stylelint-disable-next-line selector-class-pattern */
                        span.stats {
                                color: #727272;
                        }
@@ -78,6 +80,7 @@
                cursor: pointer;
        }
 
+       /* stylelint-disable-next-line selector-class-pattern */
        &.current {
                background-color: #dedede;
        }
index 2053843..6382ac8 100644 (file)
@@ -2,6 +2,8 @@
  * Diff rendering
  */
 
+/* stylelint-disable selector-class-pattern */
+
 .diff {
        border: 0;
        border-spacing: 4px;
index 76b5c9b..159e7ae 100644 (file)
@@ -1,3 +1,4 @@
+/* stylelint-disable selector-class-pattern */
 /*!
  * Diff rendering
  */
index 37808d5..13d0ba1 100644 (file)
@@ -1,5 +1,6 @@
 /* Styles for links to RSS/Atom feeds in sidebar */
 
+/* stylelint-disable-next-line selector-class-pattern */
 a.feedlink {
        /* SVG support using a transparent gradient to guarantee cross-browser
         * compatibility (browsers able to understand gradient syntax support also SVG).
index bf9634f..a608437 100644 (file)
@@ -1,5 +1,7 @@
 @import 'mediawiki.ui/variables';
 
+/* stylelint-disable selector-class-pattern */
+
 // Increase the area of the button, so that the user can move the mouse cursor
 // to the popup without the popup disappearing. (T157544)
 .mediawiki-filewarning-anchor {
index e25a92f..d9612a8 100644 (file)
@@ -2,6 +2,9 @@
  * Stylesheet for mediawiki.hlist module
  * @author [[User:Edokter]]
  */
+
+/* stylelint-disable selector-class-pattern */
+
 /* Generate interpuncts */
 .hlist dt:after {
        content: ':';
index d7071e4..5bc6a68 100644 (file)
@@ -1,3 +1,4 @@
+/* stylelint-disable-next-line selector-class-pattern */
 .hlist {
        dl,
        ol,
index ecf728b..f356fa2 100644 (file)
@@ -1,5 +1,7 @@
 @import 'mediawiki.mixins';
 
+/* stylelint-disable selector-class-pattern */
+
 // OOUIHTMLForm styles
 @ooui-font-size-browser: 16; // assumed browser default of `16px`
 @ooui-font-size-base: 0.875em; // equals `14px` at browser default of `16px`
index cfabab6..a0e9f15 100644 (file)
@@ -8,6 +8,7 @@
        content: '. .';
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .comment--without-parentheses,
 .mw-changeslist-links,
 .mw-diff-bytes,
index c21b254..e58e677 100644 (file)
@@ -6,6 +6,8 @@
  * Copyright Alexander Limi
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /**
  * Hide all the elements irrelevant for printing
  * Skins however can and should override.
index caaebad..92c0207 100644 (file)
@@ -4,6 +4,8 @@
  * CologneBlue, the old pre-Monobook skins
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /* For clarity, explicitly state some recommendations from
  * https://www.w3.org/TR/CSS21/sample.html to make sure the editsection links scale right
  */
index a63c5c6..baf2c56 100644 (file)
@@ -9,6 +9,8 @@
  * blocking CSS common to all pages.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /* GENERAL CLASSES FOR DIRECTIONALITY SUPPORT */
 
 /**
index 6a331b6..b7a424f 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 /* Galleries */
 /* These display attributes look nonsensical, but are needed to support IE and FF2 */
 /* Don't forget to update gallery.print.css */
index f7a3f0d..2b596ab 100644 (file)
@@ -1,3 +1,4 @@
+/* stylelint-disable selector-class-pattern */
 li.gallerybox {
        vertical-align: top;
        display: inline-block;
index 1cccb88..4c82192 100644 (file)
@@ -3,6 +3,8 @@
  * in MediaWiki (used e.g. on Special:ListFiles).
  */
 
+/* stylelint-disable selector-class-pattern */
+
 @import 'mediawiki.mixins';
 
 // TablePager uses `.mw-datatable` and is loaded in the right order by RL
index 689f322..bc558a5 100644 (file)
@@ -2,6 +2,8 @@
 @import 'mediawiki.ui/variables';
 @import 'mw.rcfilters.mixins';
 
+/* stylelint-disable selector-class-pattern */
+
 @rcfilters-spinner-size: 12px;
 @rcfilters-head-min-height: 210px;
 @rcfilters-head-margin-bottom: 20px;
index 8d56906..3907329 100644 (file)
@@ -1,3 +1,5 @@
+/* stylelint-disable selector-class-pattern */
+
 /* Make sure the links are not underlined or colored, ever. */
 /* There is already a :focus / :hover indication on the <div>. */
 .suggestions a.mw-searchSuggest-link,
index 3104a69..054bc27 100644 (file)
@@ -5,6 +5,8 @@
  * (ie: the CSS classing built into the system), like the TOC.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /* Table of Contents */
 .toc,
 .mw-warning,
index c6390c0..b01c518 100644 (file)
@@ -2,6 +2,8 @@
  * Icons and colors for external links.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 @import 'mediawiki.mixins';
 
 .mw-parser-output a.external,
index 8b2657d..51018f7 100644 (file)
@@ -2,6 +2,8 @@
  * Style Parsoid HTML+RDFa output consistent with wikitext from PHP parser.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /*
  * Auto-numbered external links
  * Parsoid renders those as link without content, and lets CSS do the
index a33595c..fed8235 100644 (file)
@@ -6,6 +6,8 @@
  * This style sheet is used by the Monobook and Vector skins.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /* Links */
 a {
        text-decoration: none;
index e9a2b08..c559048 100644 (file)
@@ -6,6 +6,8 @@
  * they are outputted by the actual MonoBook/Vector code by convention.
  */
 
+/* stylelint-disable selector-class-pattern */
+
 /* Categories */
 .catlinks {
        border: 1px solid #a2a9b1;
index d7415c9..c071199 100644 (file)
        min-width: 6em;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .apihelp-deprecated {
        font-weight: bold;
        color: #d33;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .apihelp-deprecated-value .oo-ui-labelElement-label {
        text-decoration: line-through;
 }
index d7923f4..f0b6913 100644 (file)
@@ -28,6 +28,7 @@ td.mw-enhanced-rc {
 }
 
 /* Show/hide arrows in enhanced changeslist */
+/* stylelint-disable-next-line selector-class-pattern */
 .mw-enhanced-rc .collapsible-expander {
        float: none;
 }
@@ -53,6 +54,7 @@ td.mw-enhanced-rc {
        font-weight: bold;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 span.changedby {
        font-size: 95%;
 }
index 81c8dc9..5f99f82 100644 (file)
@@ -4,6 +4,8 @@
 @import 'mediawiki.ui/variables.less';
 @import 'mediawiki.mixins';
 
+/* stylelint-disable selector-class-pattern */
+
 .mw-searchresults-has-iw {
        .iw-headline {
                font-weight: bold;
index 0f27420..dcb51fa 100644 (file)
@@ -1,5 +1,7 @@
 /* Special:Search */
 
+/* stylelint-disable selector-class-pattern */
+
 /*
  * Fixes sister projects box moving down the extract
  * of the first result (T18886).
index 2366249..9f27150 100644 (file)
@@ -39,6 +39,8 @@ section.mw-form-header {
        margin-top: 6px;
 }
 
+/* FIXME: These should be namespaced to mw-ext-confirmedit-fancycaptcha-, and really shouldn't be in core at all */
+/* stylelint-disable-next-line selector-class-pattern */
 .fancycaptcha-captcha-container {
        background-color: #f8f9fa;
        margin-bottom: 15px;
@@ -54,6 +56,7 @@ section.mw-form-header {
 }
 
 /* Put a border around the fancycaptcha-image-container. */
+/* stylelint-disable-next-line selector-class-pattern */
 .fancycaptcha-captcha-and-reload {
        border: 1px solid #c8ccd1;
        border-radius: 2px 2px 0 0;
@@ -63,6 +66,7 @@ section.mw-form-header {
        background-color: #fff;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .fancycaptcha-captcha-container .mw-ui-input {
        margin-top: -1px;
        border-color: #c8ccd1;
@@ -70,6 +74,7 @@ section.mw-form-header {
 }
 
 /* Make the fancycaptcha-image-container full-width within its parent. */
+/* stylelint-disable-next-line selector-class-pattern */
 .fancycaptcha-image-container {
        width: 100%;
 }
index 3cfa5a8..d8b773c 100644 (file)
        margin-bottom: 30px;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .mw-number-text.icon-edits {
        /* @embed */
        background: url( images/icon-edits.png ) no-repeat left center;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .mw-number-text.icon-pages {
        /* @embed */
        background: url( images/icon-pages.png ) no-repeat left center;
 }
 
+/* stylelint-disable-next-line selector-class-pattern */
 .mw-number-text.icon-contributors {
        /* @embed */
        background: url( images/icon-contributors.png ) no-repeat left center;
index 9428fed..25113ea 100644 (file)
@@ -2,6 +2,7 @@
  * Styles for Special:MovePage
  */
 
+/* stylelint-disable-next-line selector-class-pattern */
 .movepage-wrapper {
        width: 50em;
 }
index 7240bd4..b0cc932 100644 (file)
@@ -3,6 +3,7 @@
  */
 
 /* Distinguish actual data from information about it being hidden visually. */
+/* stylelint-disable-next-line selector-class-pattern */
 .prop-value-hidden {
        font-style: italic;
 }
index 3798f1e..3f76cf0 100644 (file)
@@ -3,6 +3,8 @@
  */
 @import 'mediawiki.mixins';
 
+/* stylelint-disable selector-class-pattern */
+
 /* Special:AllMessages */
 /* Visually hide repeating text, but leave in for better form navigation on screen readers */
 .mw-special-Allmessages .mw-htmlform-ooui .oo-ui-fieldsetLayout:first-child .oo-ui-fieldsetLayout-header {
index 31a8826..d89cc2a 100644 (file)
@@ -1,6 +1,7 @@
 /* This style is loaded on all media. */
 
 /* Hide the content of the TOC when the checkbox is checked. */
+/* stylelint-disable-next-line selector-class-pattern */
 .toctogglecheckbox:checked ~ ul {
        display: none;
 }
index e905dbe..2081d35 100644 (file)
@@ -1,4 +1,5 @@
 /* Hide the complete TOC on print when the TOC is hidden. */
+/* stylelint-disable-next-line selector-class-pattern */
 .toctogglecheckbox:checked + .toctitle {
        display: none;
 }
index ff41b5e..7d7727c 100644 (file)
@@ -1,5 +1,7 @@
 /* This style adds a toggle button with internationalized message for the TOC. */
 
+/* stylelint-disable selector-class-pattern */
+
 /* When the browser supports :checked then overwrite the style="display:none" and make the */
 /* checkbox invisible on another way to allow to focus the checkbox with keyboard. */
 :not( :checked ) > .toctogglecheckbox {
index 3490ebc..a85ecd7 100644 (file)
@@ -1,6 +1,8 @@
 @import 'mediawiki.mixins';
 @import 'mediawiki.ui/variables';
 
+/* stylelint-disable selector-class-pattern */
+
 // Buttons
 // Helper mixins
 // Primary buttons mixin
index 5fa8e5a..d08fff5 100644 (file)
@@ -3,6 +3,8 @@
 @import 'mediawiki.mixins';
 @import 'mediawiki.ui/variables';
 
+/* stylelint-disable selector-class-pattern */
+
 // --------------------------------------------------------------------------
 // Layouts
 // --------------------------------------------------------------------------
index 92c6f62..cd19cca 100644 (file)
@@ -24,6 +24,7 @@ use User;
 use Wikimedia\TestingAccessWrapper;
 use WikiPage;
 use WikitextContent;
+use DeferredUpdates;
 
 /**
  * @group Database
@@ -60,16 +61,20 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
 
        /**
         * @param string|Title|WikiPage $page
+        * @param RevisionRecord|null $rec
+        * @param User|null $user
         *
         * @return DerivedPageDataUpdater
         */
-       private function getDerivedPageDataUpdater( $page, RevisionRecord $rec = null ) {
+       private function getDerivedPageDataUpdater(
+               $page, RevisionRecord $rec = null, User $user = null
+       ) {
                if ( is_string( $page ) || $page instanceof Title ) {
                        $page = $this->getPage( $page );
                }
 
                $page = TestingAccessWrapper::newFromObject( $page );
-               return $page->getDerivedDataUpdater( null, $rec );
+               return $page->getDerivedDataUpdater( $user, $rec );
        }
 
        /**
@@ -78,11 +83,12 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
         * @param WikiPage $page
         * @param string|Message|CommentStoreComment $summary
         * @param null|string|Content $content
+        * @param User|null $user
         *
         * @return RevisionRecord|null
         */
-       private function createRevision( WikiPage $page, $summary, $content = null ) {
-               $user = $this->getTestUser()->getUser();
+       private function createRevision( WikiPage $page, $summary, $content = null, $user = null ) {
+               $user = $user ?: $this->getTestUser()->getUser();
                $comment = CommentStoreComment::newUnsavedComment( $summary );
 
                if ( $content === null || is_string( $content ) ) {
@@ -945,6 +951,68 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
                // TODO: test category membership update (with setRcWatchCategoryMembership())
        }
 
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
+        */
+       public function testDoUpdatesCacheSaveDeferral_canonical() {
+               $page = $this->getPage( __METHOD__ );
+
+               // Case where user has canonical parser options
+               $content = [ 'main' => new WikitextContent( 'rev ID ver #1: {{REVISIONID}}' ) ];
+               $rev = $this->createRevision( $page, 'first', $content );
+               $pcache = MediaWikiServices::getInstance()->getParserCache();
+               $pcache->deleteOptionsKey( $page );
+
+               $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up
+
+               $updater = $this->getDerivedPageDataUpdater( $page, $rev );
+               $updater->prepareUpdate( $rev, [] );
+               $updater->doUpdates();
+
+               $this->assertGreaterThan( 0, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' );
+               $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
+
+               $this->db->endAtomic( __METHOD__ ); // run deferred updates
+
+               $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
+        */
+       public function testDoUpdatesCacheSaveDeferral_noncanonical() {
+               $page = $this->getPage( __METHOD__ );
+
+               // Case where user does not have canonical parser options
+               $user = $this->getMutableTestUser()->getUser();
+               $user->setOption(
+                       'thumbsize',
+                       $user->getOption( 'thumbsize' ) + 1
+               );
+               $content = [ 'main' => new WikitextContent( 'rev ID ver #2: {{REVISIONID}}' ) ];
+               $rev = $this->createRevision( $page, 'first', $content, $user );
+               $pcache = MediaWikiServices::getInstance()->getParserCache();
+               $pcache->deleteOptionsKey( $page );
+
+               $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up
+
+               $updater = $this->getDerivedPageDataUpdater( $page, $rev, $user );
+               $updater->prepareUpdate( $rev, [] );
+               $updater->doUpdates();
+
+               $this->assertGreaterThan( 1, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' );
+               $this->assertFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
+
+               $this->db->endAtomic( __METHOD__ ); // run deferred updates
+
+               $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' );
+               $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
+       }
+
        /**
         * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
         */
diff --git a/tests/phpunit/includes/Storage/PreparedEditTest.php b/tests/phpunit/includes/Storage/PreparedEditTest.php
new file mode 100644 (file)
index 0000000..29999ee
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+namespace MediaWiki\Edit;
+
+use ParserOutput;
+use MediaWikiTestCase;
+
+/**
+ * @covers \MediaWiki\Edit\PreparedEdit
+ */
+class PreparedEditTest extends MediaWikiTestCase {
+       function testCallback() {
+               $output = new ParserOutput();
+               $edit = new PreparedEdit();
+               $edit->parserOutputCallback = function () {
+                       return new ParserOutput();
+               };
+
+               $this->assertEquals( $output, $edit->getOutput() );
+               $this->assertEquals( $output, $edit->output );
+       }
+}
index 6413ddd..01fde35 100644 (file)
@@ -284,6 +284,31 @@ class ParserOptionsTest extends MediaWikiTestCase {
                ScopedCallback::consume( $reset );
        }
 
+       public function testMatchesForCacheKey() {
+               $cOpts = ParserOptions::newCanonical( null, 'en' );
+
+               $uOpts = ParserOptions::newFromAnon();
+               $this->assertTrue( $cOpts->matchesForCacheKey( $uOpts ) );
+
+               $user = new User();
+               $uOpts = ParserOptions::newFromUser( $user );
+               $this->assertTrue( $cOpts->matchesForCacheKey( $uOpts ) );
+
+               $user = new User();
+               $user->setOption( 'thumbsize', 251 );
+               $uOpts = ParserOptions::newFromUser( $user );
+               $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
+
+               $user = new User();
+               $user->setOption( 'stubthreshold', 800 );
+               $uOpts = ParserOptions::newFromUser( $user );
+               $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
+
+               $user = new User();
+               $uOpts = ParserOptions::newFromUserAndLang( $user, Language::factory( 'zh' ) );
+               $this->assertFalse( $cOpts->matchesForCacheKey( $uOpts ) );
+       }
+
        public function testAllCacheVaryingOptions() {
                $this->setTemporaryHook( 'ParserOptionsRegister', null );
                $this->assertSame( [
index 94c0667..48a9ecd 100644 (file)
@@ -67,7 +67,7 @@ class DefaultPreferencesFactoryTest extends \MediaWikiTestCase {
 
                $testUser = $this->getTestUser();
                $form = $this->getPreferencesFactory()->getForm( $testUser->getUser(), $this->context );
-               $this->assertInstanceOf( PreferencesFormLegacy::class, $form );
+               $this->assertInstanceOf( PreferencesFormOOUI::class, $form );
                $this->assertCount( 5, $form->getPreferenceSections() );
        }
 
@@ -162,7 +162,7 @@ class DefaultPreferencesFactoryTest extends \MediaWikiTestCase {
                $configMock = new HashConfig( [
                        'HiddenPrefs' => []
                ] );
-               $form = $this->getMockBuilder( PreferencesFormLegacy::class )
+               $form = $this->getMockBuilder( PreferencesFormOOUI::class )
                        ->disableOriginalConstructor()
                        ->getMock();
 
index 6b92444..e824e3f 100644 (file)
@@ -101,7 +101,21 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
         * @dataProvider provideType
         */
        public function testType( $given, $expected ) {
-               $checker = new VersionChecker( '1.0.0', '7.0.0', [ 'phpLoadedExtension' ] );
+               $checker = new VersionChecker(
+                       '1.0.0',
+                       '7.0.0',
+                       [ 'phpLoadedExtension' ],
+                       [
+                               'presentAbility' => true,
+                               'presentAbilityWithMessage' => true,
+                               'missingAbility' => false,
+                               'missingAbilityWithMessage' => false,
+                       ],
+                       [
+                               'presentAbilityWithMessage' => 'Present.',
+                               'missingAbilityWithMessage' => 'Missing.',
+                       ]
+               );
                $checker->setLoadedExtensionsAndSkins( [
                                'FakeDependency' => [
                                        'version' => '1.0.0',
@@ -218,6 +232,83 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
                                        ],
                                ],
                        ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbility' => true,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbilityWithMessage' => true,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbility' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-presentAbilityWithMessage' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbility' => true,
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'missing' => 'missingAbility',
+                                               'type' => 'missing-ability',
+                                               'msg' => 'FakeExtension requires "missingAbility" ability',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbilityWithMessage' => true,
+                                       ],
+                               ],
+                               [
+                                       [
+                                               'missing' => 'missingAbilityWithMessage',
+                                               'type' => 'missing-ability',
+                                               // phpcs:ignore Generic.Files.LineLength.TooLong
+                                               'msg' => 'FakeExtension requires "missingAbilityWithMessage" ability: Missing.',
+                                       ],
+                               ],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbility' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'platform' => [
+                                               'ability-missingAbilityWithMessage' => false,
+                                       ],
+                               ],
+                               [],
+                       ],
                ];
        }
 
@@ -282,6 +373,26 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
                                ],
                                'phpLoadedExtension',
                        ],
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'platform' => [
+                                                       'ability-invalidAbility' => true,
+                                               ],
+                                       ],
+                               ],
+                               'ability-invalidAbility',
+                       ],
+                       [
+                               [
+                                       'FakeExtension' => [
+                                               'platform' => [
+                                                       'presentAbility' => true,
+                                               ],
+                                       ],
+                               ],
+                               'presentAbility',
+                       ],
                        [
                                [
                                        'FakeExtension' => [
@@ -308,7 +419,15 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
         * @dataProvider provideInvalidDependency
         */
        public function testInvalidDependency( $depencency, $type ) {
-               $checker = new VersionChecker( '1.0.0', '7.0.0', [ 'phpLoadedExtension' ] );
+               $checker = new VersionChecker(
+                       '1.0.0',
+                       '7.0.0',
+                       [ 'phpLoadedExtension' ],
+                       [
+                               'presentAbility' => true,
+                               'missingAbility' => false,
+                       ]
+               );
                $this->setExpectedException(
                        UnexpectedValueException::class,
                        "Dependency type $type unknown in FakeExtension"
@@ -330,4 +449,31 @@ class VersionCheckerTest extends PHPUnit\Framework\TestCase {
                        ],
                ] );
        }
+
+       /**
+        * @dataProvider provideInvalidAbilityType
+        */
+       public function testInvalidAbilityType( $value ) {
+               $checker = new VersionChecker( '1.0.0', '7.0.0', [], [ 'presentAbility' => true ] );
+               $this->setExpectedException(
+                       UnexpectedValueException::class,
+                       'Only booleans are allowed to to indicate the presence of abilities in FakeExtension'
+               );
+               $checker->checkArray( [
+                       'FakeExtension' => [
+                               'platform' => [
+                                       'ability-presentAbility' => $value,
+                               ],
+                       ],
+               ] );
+       }
+
+       public function provideInvalidAbilityType() {
+               return [
+                       [ null ],
+                       [ 1 ],
+                       [ '1' ],
+               ];
+       }
+
 }