Merge "Use HTML::hidden to create input fields"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 7 Aug 2017 10:07:07 +0000 (10:07 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 7 Aug 2017 10:07:07 +0000 (10:07 +0000)
324 files changed:
.gitignore
.mailmap
.travis.yml
CREDITS
Gruntfile.js
RELEASE-NOTES-1.30
autoload.php
composer.json
docs/database.txt
docs/hooks.txt
includes/Block.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/Linker.php
includes/ListToggle.php
includes/Preferences.php
includes/Sanitizer.php
includes/Setup.php
includes/SiteStats.php
includes/Title.php
includes/actions/InfoAction.php
includes/api/ApiBase.php
includes/api/ApiMain.php
includes/api/ApiQueryLinks.php
includes/api/i18n/he.json
includes/api/i18n/ko.json
includes/api/i18n/nb.json
includes/changes/ChangesList.php
includes/changes/EnhancedChangesList.php
includes/changetags/ChangeTags.php
includes/deferred/WANCacheReapUpdate.php
includes/diff/DifferenceEngine.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormField.php
includes/htmlform/OOUIHTMLForm.php
includes/htmlform/fields/HTMLCheckField.php
includes/htmlform/fields/HTMLFormFieldCloner.php
includes/htmlform/fields/HTMLMultiSelectField.php
includes/htmlform/fields/HTMLRadioField.php
includes/htmlform/fields/HTMLTextAreaField.php
includes/htmlform/fields/HTMLTextField.php
includes/htmlform/fields/HTMLUsersMultiselectField.php
includes/installer/DatabaseUpdater.php
includes/installer/MssqlUpdater.php
includes/installer/MysqlUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/installer/i18n/csb.json
includes/installer/i18n/ms.json
includes/installer/i18n/pt.json
includes/installer/i18n/roa-tara.json
includes/jobqueue/JobQueueFederated.php
includes/jobqueue/JobQueueSecondTestQueue.php [new file with mode: 0644]
includes/libs/CSSMin.php
includes/libs/StatusValue.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/mime/MimeAnalyzer.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/logging/BlockLogFormatter.php
includes/mail/EmailNotification.php
includes/page/CategoryPage.php
includes/page/ImagePage.php
includes/parser/Parser.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php [new file with mode: 0644]
includes/skins/BaseTemplate.php
includes/skins/Skin.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specials/SpecialBrokenRedirects.php
includes/specials/SpecialContributions.php
includes/specials/SpecialDoubleRedirects.php
includes/specials/SpecialImport.php
includes/specials/SpecialListgrants.php
includes/specials/SpecialListgrouprights.php
includes/specials/SpecialListredirects.php
includes/specials/SpecialPagesWithProp.php
includes/specials/SpecialRecentchanges.php
includes/specials/SpecialRecentchangeslinked.php
includes/specials/SpecialSearch.php
includes/specials/SpecialShortpages.php
includes/specials/SpecialUncategorizedcategories.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialVersion.php
includes/specials/SpecialWatchlist.php
includes/specials/pagers/AllMessagesTablePager.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/UsersPager.php
includes/widget/search/BasicSearchResultSetWidget.php
jsduck.json
languages/Language.php
languages/data/Names.php
languages/i18n/af.json
languages/i18n/ar.json
languages/i18n/as.json
languages/i18n/ast.json
languages/i18n/awa.json
languages/i18n/azb.json
languages/i18n/ba.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bg.json
languages/i18n/bho.json
languages/i18n/bn.json
languages/i18n/bs.json
languages/i18n/ca.json
languages/i18n/ce.json
languages/i18n/ckb.json
languages/i18n/cs.json
languages/i18n/cv.json
languages/i18n/cy.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/eu.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/frr.json
languages/i18n/gl.json
languages/i18n/gor.json
languages/i18n/gsw.json
languages/i18n/gu.json
languages/i18n/ha.json
languages/i18n/he.json
languages/i18n/hi.json
languages/i18n/hif-latn.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hy.json
languages/i18n/ia.json
languages/i18n/id.json
languages/i18n/inh.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/jam.json
languages/i18n/jv.json
languages/i18n/kab.json
languages/i18n/ko.json
languages/i18n/lb.json
languages/i18n/li.json
languages/i18n/lv.json
languages/i18n/mk.json
languages/i18n/ms.json
languages/i18n/mwl.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/nn.json
languages/i18n/pl.json
languages/i18n/pnb.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/rif.json
languages/i18n/rm.json
languages/i18n/ro.json
languages/i18n/roa-tara.json
languages/i18n/ru.json
languages/i18n/sd.json
languages/i18n/shi.json
languages/i18n/skr-arab.json
languages/i18n/sl.json
languages/i18n/sq.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/sv.json
languages/i18n/te.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/vi.json
languages/i18n/vro.json
languages/i18n/yi.json
languages/i18n/yue.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesSkr.php [new file with mode: 0644]
languages/messages/MessagesSkr_arab.php [new file with mode: 0644]
maintenance/addRFCandPMIDInterwiki.php
maintenance/benchmarks/benchmarkLruHash.php [new file with mode: 0644]
maintenance/jsduck/categories.json
maintenance/mssql/archives/patch-add-3d.sql [new file with mode: 0644]
maintenance/mssql/tables.sql
maintenance/populatePPSortKey.php [new file with mode: 0644]
maintenance/postgres/archives/patch-add-3d.sql [new file with mode: 0644]
maintenance/postgres/tables.sql
maintenance/rebuildrecentchanges.php
maintenance/refreshLinks.php
maintenance/sqlite/archives/patch-add-3d.sql [new file with mode: 0644]
maintenance/updateCredits.php
profileinfo.php
resources/Resources.php
resources/lib/oojs-ui/i18n/as.json
resources/lib/oojs-ui/i18n/bs.json
resources/lib/oojs-ui/i18n/pt-br.json
resources/lib/oojs-ui/oojs-ui-apex.js
resources/lib/oojs-ui/oojs-ui-core-apex.css
resources/lib/oojs-ui/oojs-ui-core-wikimediaui.css
resources/lib/oojs-ui/oojs-ui-core.js
resources/lib/oojs-ui/oojs-ui-core.js.map
resources/lib/oojs-ui/oojs-ui-toolbars-apex.css
resources/lib/oojs-ui/oojs-ui-toolbars-wikimediaui.css
resources/lib/oojs-ui/oojs-ui-toolbars.js
resources/lib/oojs-ui/oojs-ui-widgets-apex.css
resources/lib/oojs-ui/oojs-ui-widgets-wikimediaui.css
resources/lib/oojs-ui/oojs-ui-widgets.js
resources/lib/oojs-ui/oojs-ui-widgets.js.map
resources/lib/oojs-ui/oojs-ui-wikimediaui.js
resources/lib/oojs-ui/oojs-ui-windows-apex.css
resources/lib/oojs-ui/oojs-ui-windows-wikimediaui.css
resources/lib/oojs-ui/oojs-ui-windows.js
resources/lib/oojs-ui/themes/apex/icons-media.json
resources/lib/oojs-ui/themes/apex/images/icons/play-ltr.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/play-ltr.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/play-rtl.png [new file with mode: 0644]
resources/lib/oojs-ui/themes/apex/images/icons/play-rtl.svg [new file with mode: 0644]
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-ltr-invert.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-ltr-invert.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-ltr-progressive.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-ltr-progressive.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-ltr.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-ltr.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-rtl-invert.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-rtl-invert.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-rtl-progressive.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-rtl-progressive.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-rtl.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/play-rtl.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/stop-invert.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/stop-invert.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/stop-progressive.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/stop-progressive.svg
resources/lib/oojs-ui/themes/wikimediaui/images/icons/stop.png
resources/lib/oojs-ui/themes/wikimediaui/images/icons/stop.svg
resources/lib/qunitjs/qunit.css
resources/lib/qunitjs/qunit.js
resources/src/mediawiki.action/mediawiki.action.edit.js
resources/src/mediawiki.action/mediawiki.action.edit.preview.js
resources/src/mediawiki.action/mediawiki.action.edit.styles.css
resources/src/mediawiki.legacy/commonPrint.css
resources/src/mediawiki.less/mediawiki.mixins.animation.less
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js
resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/mw.rcfilters.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.mixins.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesLimitPopupWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListItemWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js
resources/src/mediawiki.skinning/content.parsoid.less
resources/src/mediawiki.special/mediawiki.special.block.js
resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
resources/src/mediawiki.special/mediawiki.special.preferences.styles.css
resources/src/mediawiki.special/mediawiki.special.search.styles.css
resources/src/mediawiki.widgets.visibleByteLimit/mediawiki.widgets.visibleByteLimit.js
resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js
resources/src/mediawiki/htmlform/selectorother.js
resources/src/mediawiki/mediawiki.checkboxtoggle.js
resources/src/mediawiki/mediawiki.util.js
resources/src/mediawiki/page/gallery.css
resources/src/mediawiki/page/gallery.print.css
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/parser/parserTests.txt
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/ResourceLoaderTestCase.php
tests/phpunit/includes/DeprecatedGlobalTest.php
tests/phpunit/includes/RevisionStorageTest.php
tests/phpunit/includes/SanitizerTest.php
tests/phpunit/includes/SiteStatsTest.php [new file with mode: 0644]
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
tests/phpunit/includes/WatchedItemStoreUnitTest.php
tests/phpunit/includes/changes/EnhancedChangesListTest.php
tests/phpunit/includes/changes/OldChangesListTest.php
tests/phpunit/includes/changetags/ChangeTagsTest.php [new file with mode: 0644]
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/libs/CSSMinTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseTest.php
tests/phpunit/includes/page/WikiPageTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php
tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php
tests/phpunit/includes/specials/SpecialRecentchangesTest.php
tests/phpunit/includes/specials/SpecialShortpagesTest.php [new file with mode: 0644]
tests/phpunit/suite.xml
tests/qunit/data/testrunner.js
tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js
tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js

index a82ae21..3e97aab 100644 (file)
@@ -48,7 +48,6 @@ node_modules/
 # Composer
 /vendor
 /composer.lock
-/composer.json
 /composer.local.json
 
 # MediaWiki UI documentation
index 2134fc5..5a76fb9 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -63,6 +63,7 @@ aude <aude.wiki@gmail.com>
 Audrey Tang <audreyt@audreyt.org>
 Audrey Tang <audreyt@audreyt.org> <au@localhost>
 ayush_garg <ayush.ce13@iitp.ac.in>
+Bae Junehyeon <devunt@gmail.com>
 Bahodir Mansurov <bmansurov@wikimedia.org>
 Bartosz Dziewoński <matma.rex@gmail.com>
 Bartosz Dziewoński <matma.rex@gmail.com> <bdziewonski@wikimedia.org>
@@ -227,7 +228,6 @@ Jon Robson <jrobson@wikimedia.org>
 Jon Robson <jrobson@wikimedia.org> <jdlrobson@gmail.com>
 Juliusz Gonera <jgonera@gmail.com>
 Juliusz Gonera <jgonera@gmail.com> <jgonera@wikimedia.org>
-JuneHyeon Bae <devunt@gmail.com>
 Jure Kajzer <freak@drajv.si>
 Jure Kajzer <freak@drajv.si> <freakolowsky@users.mediawiki.org>
 Justin Du <justin.d128@gmail.com>
index 5e2c7a0..cde7193 100644 (file)
@@ -7,11 +7,12 @@
 # complement that setup by testing MediaWiki on travis
 #
 language: php
-# Use the slower sudo-enabled VMs instead of fast containers:
-# - Package 'djvulibre-bin' is not yet whitelisted for trusty containers.
-#   https://github.com/travis-ci/apt-package-whitelist/issues/4036
-sudo: required
-# Use Trusty instead of Travis default (precise)
+
+
+# Use fast containers instead of the slower sudo-enabled VMs:
+sudo: false
+# Use Ubuntu 14 Trusty (not Ubuntu 12 Precise)
+# <https://docs.travis-ci.com/user/reference/trusty/>
 # - Required in order to use HHVM 3.6 or higher.
 # - Required for non-buggy xml library for XmlTypeCheck/UploadBaseTest (T75176).
 dist: trusty
@@ -25,10 +26,11 @@ matrix:
       php: 5.5
     - env: dbtype=postgres dbuser=travis
       php: 5.5
+    # https://docs.travis-ci.com/user/languages/php#HHVM-versions
     - env: dbtype=mysql dbuser=root
-      # https://docs.travis-ci.com/user/languages/php#HHVM-versions
-      # https://github.com/travis-ci/travis-ci/issues/7368
       php: hhvm-3.12
+    - env: dbtype=mysql dbuser=root
+      php: hhvm-3.18
     - env: dbtype=mysql dbuser=root
       php: 7
 
diff --git a/CREDITS b/CREDITS
index 14c454e..c38c3fc 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -78,6 +78,7 @@ The following list can be found parsed under Special:Version/Credits -->
 * awu42
 * ayush_garg
 * Azliq7
+* Bae Junehyeon
 * Bagariavivek
 * Bahodir Mansurov
 * balloonguy
@@ -312,7 +313,6 @@ The following list can be found parsed under Special:Version/Credits -->
 * Julian Ostrow
 * Juliano F. Ravasi
 * Juliusz Gonera
-* JuneHyeon Bae
 * Jure Kajzer
 * Justin Du
 * Kai Nissen
index 811d2c0..dbbfcb8 100644 (file)
@@ -44,8 +44,6 @@ module.exports = function ( grunt ) {
                                '!extensions/**/*.js',
                                '!skins/**/*.js',
                                // Skip functions aren't even parseable
-                               '!resources/src/dom-level2-skip.js',
-                               '!resources/src/es5-skip.js',
                                '!resources/src/mediawiki.hidpi-skip.js'
                        ]
                },
index 51f9764..452cb35 100644 (file)
@@ -26,6 +26,13 @@ section).
   array. This allows dependency injection to be used for ResourceLoader modules.
 * $wgExceptionHooks has been removed.
 * (T45547) $wgUsePigLatinVariant added (off by default).
+* (T152540) MediaWiki now supports a section ID escaping style that allows to display
+  non-Latin characters verbatim on many modern browsers. This is controlled by the
+  new configuration setting, $wgFragmentMode.
+* $wgExperimentalHtmlIds is now deprecated and will be removed in a future version,
+  use $wgFragmentMode to migrate off it to a modern alternative.
+* $wgExternalInterwikiFragmentMode was introduced to control how fragments in
+  sinterwikis going outside of current wiki farm are encoded.
 
 === New features in 1.30 ===
 * (T37247) Output from Parser::parse() will now be wrapped in a div with
@@ -46,6 +53,7 @@ section).
 === Languages updated in 1.30 ===
 
 * Support for kbp (Kabɩyɛ / Kabiyè) was added.
+* Support for skr (Saraiki, سرائیکی) was added.
 
 === External library changes in 1.30 ===
 
@@ -142,6 +150,14 @@ changes to languages because of Phabricator reports.
   MediaWikiServices instead. Access to the underlying BagOStuff is possible
   through the new ParserCache::getCacheStorage() method.
 * .mw-ui-constructive CSS class (deprecated in 1.27) was removed.
+* Sanitizer::escapeId() was deprecated, use escapeIdForAttribute(),
+  escapeIdForLink() or escapeIdForExternalInterwiki() instead.
+* Title::escapeFragmentForURL() was deprecated, use one of the aforementioned
+  Sanitizer functions or, if possible, Title::getFragmentForURL().
+* Second parameter to Sanitizer::escapeIdReferenceList() ($options) now does
+  nothing and is deprecated.
+* mw.util.escapeId() was deprecated, use escapeIdForAttribute() or
+  escapeIdForLink().
 
 == Compatibility ==
 MediaWiki 1.30 requires PHP 5.5.9 or later. There is experimental support for
index a6128a4..d44a305 100644 (file)
@@ -193,6 +193,7 @@ $wgAutoloadLocalClasses = [
        'BenchmarkDeleteTruncate' => __DIR__ . '/maintenance/benchmarks/bench_delete_truncate.php',
        'BenchmarkHooks' => __DIR__ . '/maintenance/benchmarks/benchmarkHooks.php',
        'BenchmarkJSMinPlus' => __DIR__ . '/maintenance/benchmarks/benchmarkJSMinPlus.php',
+       'BenchmarkLruHash' => __DIR__ . '/maintenance/benchmarks/benchmarkLruHash.php',
        'BenchmarkParse' => __DIR__ . '/maintenance/benchmarks/benchmarkParse.php',
        'BenchmarkPurge' => __DIR__ . '/maintenance/benchmarks/benchmarkPurge.php',
        'BenchmarkTidy' => __DIR__ . '/maintenance/benchmarks/benchmarkTidy.php',
@@ -679,6 +680,7 @@ $wgAutoloadLocalClasses = [
        'JobQueueMemory' => __DIR__ . '/includes/jobqueue/JobQueueMemory.php',
        'JobQueueReadOnlyError' => __DIR__ . '/includes/jobqueue/JobQueue.php',
        'JobQueueRedis' => __DIR__ . '/includes/jobqueue/JobQueueRedis.php',
+       'JobQueueSecondTestQueue' => __DIR__ . '/includes/jobqueue/JobQueueSecondTestQueue.php',
        'JobRunner' => __DIR__ . '/includes/jobqueue/JobRunner.php',
        'JobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
        'JpegHandler' => __DIR__ . '/includes/media/Jpeg.php',
@@ -1118,6 +1120,7 @@ $wgAutoloadLocalClasses = [
        'PopulateInterwiki' => __DIR__ . '/maintenance/populateInterwiki.php',
        'PopulateLogSearch' => __DIR__ . '/maintenance/populateLogSearch.php',
        'PopulateLogUsertext' => __DIR__ . '/maintenance/populateLogUsertext.php',
+       'PopulatePPSortKey' => __DIR__ . '/maintenance/populatePPSortKey.php',
        'PopulateParentId' => __DIR__ . '/maintenance/populateParentId.php',
        'PopulateRecentChangesSource' => __DIR__ . '/maintenance/populateRecentChangesSource.php',
        'PopulateRevisionLength' => __DIR__ . '/maintenance/populateRevisionLength.php',
@@ -1228,6 +1231,7 @@ $wgAutoloadLocalClasses = [
        'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php',
        'ResourceLoaderLanguageDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageDataModule.php',
        'ResourceLoaderLanguageNamesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageNamesModule.php',
+       'ResourceLoaderMediaWikiUtilModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php',
        'ResourceLoaderModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderModule.php',
        'ResourceLoaderOOUIFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIFileModule.php',
        'ResourceLoaderOOUIImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIImageModule.php',
index d04e9f5..bc48360 100644 (file)
@@ -25,7 +25,7 @@
                "ext-xml": "*",
                "liuggio/statsd-php-client": "1.0.18",
                "mediawiki/at-ease": "1.1.0",
-               "oojs/oojs-ui": "0.22.3",
+               "oojs/oojs-ui": "0.22.4",
                "oyejorge/less.php": "1.7.0.14",
                "php": ">=5.5.9",
                "psr/log": "1.0.2",
@@ -37,7 +37,7 @@
                "wikimedia/html-formatter": "1.0.1",
                "wikimedia/ip-set": "1.1.0",
                "wikimedia/php-session-serializer": "1.0.4",
-               "wikimedia/relpath": "1.0.3",
+               "wikimedia/relpath": "2.0.0",
                "wikimedia/remex-html": "1.0.1",
                "wikimedia/running-stat": "1.1.0",
                "wikimedia/scoped-callback": "1.0.0",
index 44ec764..dbc9204 100644 (file)
@@ -17,7 +17,7 @@ description of the tables and their contents, please see:
 
 To make a read query, something like this usually suffices:
 
-$dbr = wfGetDB( DB_SLAVE );
+$dbr = wfGetDB( DB_REPLICA );
 $res = $dbr->select( /* ...see docs... */ );
 foreach ( $res as $row ) {
        ...
index 3ff3365..8912b82 100644 (file)
@@ -2764,9 +2764,10 @@ configuration variables to JavaScript. Things that depend on the current page
 or request state must be added through MakeGlobalVariablesScript instead.
 &$vars: array( variable name => value )
 
-'ResourceLoaderGetLessVars': Called in ResourceLoader::getLessVars after
-variables from $wgResourceLoaderLESSVars are added. Can be used to add
-context-based variables.
+'ResourceLoaderGetLessVars': DEPRECATED! Called in ResourceLoader::getLessVars
+to add global LESS variables. Loaded after $wgResourceLoaderLESSVars is added.
+Global LESS variables are deprecated. Use ResourceLoaderModule::getLessVars()
+instead to expose variables only in modules that need them.
 &$lessVars: array of variables already added
 
 'ResourceLoaderJqueryMsgModuleMagicWords': Called in
index 2a04879..2ca56a5 100644 (file)
@@ -958,6 +958,7 @@ class Block {
 
        /**
         * Get the system block type, if any
+        * @since 1.29
         * @return string|null
         */
        public function getSystemBlockType() {
@@ -1450,6 +1451,8 @@ class Block {
         * Set the 'BlockID' cookie to this block's ID and expiry time. The cookie's expiry will be
         * the same as the block's, to a maximum of 24 hours.
         *
+        * @since 1.29
+        *
         * @param WebResponse $response The response on which to set the cookie.
         */
        public function setCookie( WebResponse $response ) {
@@ -1472,6 +1475,8 @@ class Block {
        /**
         * Unset the 'BlockID' cookie.
         *
+        * @since 1.29
+        *
         * @param WebResponse $response The response on which to unset the cookie.
         */
        public static function clearCookie( WebResponse $response ) {
@@ -1482,6 +1487,9 @@ class Block {
         * Get the BlockID cookie's value for this block. This is usually the block ID concatenated
         * with an HMAC in order to avoid spoofing (T152951), but if wgSecretKey is not set will just
         * be the block ID.
+        *
+        * @since 1.29
+        *
         * @return string The block ID, probably concatenated with "!" and the HMAC.
         */
        public function getCookieValue() {
@@ -1500,7 +1508,11 @@ class Block {
        /**
         * Get the stored ID from the 'BlockID' cookie. The cookie's value is usually a combination of
         * the ID and a HMAC (see Block::setCookie), but will sometimes only be the ID.
+        *
+        * @since 1.29
+        *
         * @param string $cookieValue The string in which to find the ID.
+        *
         * @return integer|null The block ID, or null if the HMAC is present and invalid.
         */
        public static function getIdFromCookieValue( $cookieValue ) {
index f35715e..a28aa5b 100644 (file)
@@ -1304,7 +1304,7 @@ $wgMimeInfoFile = 'includes/mime.info';
  * Sets an external MIME detector program. The command must print only
  * the MIME type to standard output.
  * The name of the file to process will be appended to the command given here.
- * If not set or NULL, PHP's fileinfo extension will be used if available.
+ * If not set or NULL, PHP's mime_content_type function will be used.
  *
  * @par Example:
  * @code
@@ -3372,16 +3372,56 @@ $wgApiFrameOptions = 'DENY';
 $wgDisableOutputCompression = false;
 
 /**
- * Should we allow a broader set of characters in id attributes, per HTML5?  If
- * not, use only HTML 4-compatible IDs.  This option is for testing -- when the
- * functionality is ready, it will be on by default with no option.
+ * Abandoned experiment with HTML5-style ID escaping. Normalized IDs a bit
+ * too aggressively, breaking preexisting content (particularly Cite).
+ * See T29733, T29694, T29474.
  *
- * Currently this appears to work fine in all browsers, but it's disabled by
- * default because it normalizes id's a bit too aggressively, breaking preexisting
- * content (particularly Cite).  See T29733, T29694, T29474.
+ * @deprecated since 1.30, use $wgFragmentMode
  */
 $wgExperimentalHtmlIds = false;
 
+/**
+ * How should section IDs be encoded?
+ * This array can contain 1 or 2 elements, each of them can be one of:
+ * - 'html5'  is modern HTML5 style encoding with minimal escaping. Allows to
+ *            display Unicode characters in many browsers' address bars.
+ * - 'legacy' is old MediaWiki-style encoding, e.g. 啤酒 turns into .E5.95.A4.E9.85.92
+ * - 'html5-legacy' corresponds to DEPRECATED $wgExperimentalHtmlIds mode. DO NOT use
+ *            it for anything but migration off that mode (see below).
+ *
+ * The first element of this array specifies the primary mode of escaping IDs. This
+ * is what users will see when they e.g. follow an [[#internal link]] to a section of
+ * a page.
+ *
+ * The optional second element defines a fallback mode, useful for migrations.
+ * If present, it will direct MediaWiki to add empty <span>s to every section with its
+ * id attribute set to fallback encoded title so that links using the previous encoding
+ * would still work.
+ *
+ * Example: you want to migrate your wiki from 'legacy' to 'html5'
+ *
+ * On the first step, set this variable to [ 'legacy', 'html5' ]. After a while, when
+ * all caches (parser, HTTP, etc.) contain only pages generated with this setting,
+ * flip the value to [ 'html5', 'legacy' ]. This will result in all internal links being
+ * generated in the new encoding while old links (both external and cached internal) will
+ * still work. After a long time, you might want to ditch backwards compatibility and
+ * set it to [ 'html5' ]. After all, pages get edited, breaking incoming links no matter which
+ * fragment mode is used.
+ *
+ * @since 1.30
+ */
+$wgFragmentMode = [ 'legacy' ];
+
+/**
+ * Which ID escaping mode should be used for external interwiki links? See documentation
+ * for $wgFragmentMode above for details of each mode. Because you can't control external sites,
+ * this setting should probably always be 'legacy', unless every wiki you link to has converted
+ * to 'html5'.
+ *
+ * @since 1.30
+ */
+$wgExternalInterwikiFragmentMode = 'legacy';
+
 /**
  * Abstract list of footer icons for skins in place of old copyrightico and poweredbyico code
  * You can add new icons to the built in copyright or poweredby, or you can create
@@ -3754,20 +3794,18 @@ $wgResourceLoaderValidateStaticJS = false;
  * at the beginning of all your .less files, with all the consequences.
  * In particular, string values must be escaped and quoted.
  *
- * Changes to LESS variables do not trigger cache invalidation.
- *
- * If the LESS variables need to be dynamic, you can use the
- * ResourceLoaderGetLessVars hook (since 1.25).
+ * Changes to this configuration do NOT trigger cache invalidation.
  *
  * @par Example:
  * @code
  *   $wgResourceLoaderLESSVars = [
- *     'baseFontSize'  => '1em',
- *     'smallFontSize' => '0.75em',
- *     'WikimediaBlue' => '#006699',
+ *     'exampleFontSize'  => '1em',
+ *     'exampleBlue' => '#eee',
  *   ];
  * @endcode
  * @since 1.22
+ * @deprecated since 1.30 Use ResourceLoaderModule::getLessVars() instead to
+ *  add variables to individual modules that need them.
  */
 $wgResourceLoaderLESSVars = [
        /**
@@ -4091,6 +4129,14 @@ $wgTrackingCategories = [];
  */
 $wgContentNamespaces = [ NS_MAIN ];
 
+/**
+ * Optional array of namespaces which should be blacklisted from Special:ShortPages
+ * Only pages inside $wgContentNamespaces but not $wgShortPagesNamespaceBlacklist will
+ * be shown on that page.
+ * @since 1.30
+ */
+$wgShortPagesNamespaceBlacklist = [];
+
 /**
  * Array of namespaces, in addition to the talk namespaces, where signatures
  * (~~~~) are likely to be used. This determines whether to display the
@@ -6771,11 +6817,6 @@ $wgRCWatchCategoryMembership = false;
  */
 $wgUseRCPatrol = true;
 
-/**
- * Whether to allow users to save their RecentChanges filters
- */
-$wgStructuredChangeFiltersEnableSaving = true;
-
 /**
  * Whether to show the new experimental views (like namespaces, tags, and users) in
  * RecentChanges filters
@@ -8238,10 +8279,15 @@ $wgHTTPProxy = false;
  * Local virtual hosts.
  *
  * This lists domains that are configured as virtual hosts on the same machine.
- * If a request is to be made to a domain listed here, or any subdomain thereof,
- * then no proxy will be used.
- * Command-line scripts are not affected by this setting and will always use
- * proxy if it is configured.
+ *
+ * This affects the following:
+ * - MWHttpRequest: If a request is to be made to a domain listed here, or any
+ *   subdomain thereof, then no proxy will be used.
+ *   Command-line scripts are not affected by this setting and will always use
+ *   the proxy if it is configured.
+ * - ChronologyProtector: Decide to shutdown LBFactory asynchronously instead
+ *   synchronously if the current response redirects to a local virtual host.
+ *
  * @since 1.25
  */
 $wgLocalVirtualHosts = [];
index e5fb4c5..cfb78cd 100644 (file)
@@ -1698,7 +1698,7 @@ class EditPage {
                global $wgParser;
 
                if ( $this->sectiontitle !== '' ) {
-                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
+                       $sectionanchor = $this->guessSectionName( $this->sectiontitle );
                        // If no edit summary was specified, create one automatically from the section
                        // title and have it link to the new section. Otherwise, respect the summary as
                        // passed.
@@ -1708,7 +1708,7 @@ class EditPage {
                                        ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
                        }
                } elseif ( $this->summary !== '' ) {
-                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
+                       $sectionanchor = $this->guessSectionName( $this->summary );
                        # This is a new section, so create a link to the new section
                        # in the revision summary.
                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
@@ -1743,7 +1743,7 @@ class EditPage {
         * time.
         */
        public function internalAttemptSave( &$result, $bot = false ) {
-               global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
+               global $wgUser, $wgRequest, $wgMaxArticleSize;
                global $wgContentHandlerUseDB;
 
                $status = Status::newGood();
@@ -2117,7 +2117,7 @@ class EditPage {
                                # We can't deal with anchors, includes, html etc in the header for now,
                                # headline would need to be parsed to improve this.
                                if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
+                                       $sectionanchor = $this->guessSectionName( $matches[2] );
                                }
                        }
                        $result['sectionanchor'] = $sectionanchor;
@@ -3076,7 +3076,7 @@ class EditPage {
                        'tabindex' => 1,
                        'size' => 60,
                        'spellcheck' => 'true',
-               ] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
+               ];
        }
 
        /**
@@ -3097,6 +3097,7 @@ class EditPage {
                $inputAttrs = null, $spanLabelAttrs = null
        ) {
                $inputAttrs = $this->getSummaryInputAttributes( $inputAttrs );
+               $inputAttrs += Linker::tooltipAndAccesskeyAttribs( 'summary' );
 
                $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
                        'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
@@ -3132,6 +3133,10 @@ class EditPage {
                $inputAttrs = OOUI\Element::configFromHtmlAttributes(
                        $this->getSummaryInputAttributes( $inputAttrs )
                );
+               $inputAttrs += [
+                       'title' => Linker::titleAttrib( 'summary' ),
+                       'accessKey' => Linker::accesskey( 'summary' ),
+               ];
 
                // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
                $inputAttrs['inputId'] = $inputAttrs['id'];
@@ -4274,7 +4279,7 @@ class EditPage {
                        $accesskey = null;
                        if ( isset( $options['tooltip'] ) ) {
                                $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
-                               $title = Linker::titleAttrib( $options['tooltip'], 'withaccess' );
+                               $title = Linker::titleAttrib( $options['tooltip'] );
                        }
                        if ( isset( $options['title-message'] ) ) {
                                $title = $this->context->msg( $options['title-message'] )->text();
@@ -4352,8 +4357,7 @@ class EditPage {
                $attribs = [
                        'name' => 'wpSave',
                        'tabindex' => ++$tabindex,
-               ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
-
+               ];
                if ( $this->oouiEnabled ) {
                        $saveConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
                        $buttons['save'] = new OOUI\ButtonInputWidget( [
@@ -4365,11 +4369,13 @@ class EditPage {
                                'label' => $buttonLabel,
                                'infusable' => true,
                                'type' => 'submit',
+                               'title' => Linker::titleAttrib( 'save' ),
+                               'accessKey' => Linker::accesskey( 'save' ),
                        ] + $saveConfig );
                } else {
                        $buttons['save'] = Html::submitButton(
                                $buttonLabel,
-                               $attribs + [ 'id' => 'wpSave' ],
+                               $attribs + Linker::tooltipAndAccesskeyAttribs( 'save' ) + [ 'id' => 'wpSave' ],
                                [ 'mw-ui-progressive' ]
                        );
                }
@@ -4377,7 +4383,7 @@ class EditPage {
                $attribs = [
                        'name' => 'wpPreview',
                        'tabindex' => ++$tabindex,
-               ] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
+               ];
                if ( $this->oouiEnabled ) {
                        $previewConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
                        $buttons['preview'] = new OOUI\ButtonInputWidget( [
@@ -4387,18 +4393,20 @@ class EditPage {
                                'useInputTag' => true,
                                'label' => $this->context->msg( 'showpreview' )->text(),
                                'infusable' => true,
-                               'type' => 'submit'
+                               'type' => 'submit',
+                               'title' => Linker::titleAttrib( 'preview' ),
+                               'accessKey' => Linker::accesskey( 'preview' ),
                        ] + $previewConfig );
                } else {
                        $buttons['preview'] = Html::submitButton(
                                $this->context->msg( 'showpreview' )->text(),
-                               $attribs + [ 'id' => 'wpPreview' ]
+                               $attribs + Linker::tooltipAndAccesskeyAttribs( 'preview' ) + [ 'id' => 'wpPreview' ]
                        );
                }
                $attribs = [
                        'name' => 'wpDiff',
                        'tabindex' => ++$tabindex,
-               ] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
+               ];
                if ( $this->oouiEnabled ) {
                        $diffConfig = OOUI\Element::configFromHtmlAttributes( $attribs );
                        $buttons['diff'] = new OOUI\ButtonInputWidget( [
@@ -4409,11 +4417,13 @@ class EditPage {
                                'label' => $this->context->msg( 'showdiff' )->text(),
                                'infusable' => true,
                                'type' => 'submit',
+                               'title' => Linker::titleAttrib( 'diff' ),
+                               'accessKey' => Linker::accesskey( 'diff' ),
                        ] + $diffConfig );
                } else {
                        $buttons['diff'] = Html::submitButton(
                                $this->context->msg( 'showdiff' )->text(),
-                               $attribs + [ 'id' => 'wpDiff' ]
+                               $attribs + Linker::tooltipAndAccesskeyAttribs( 'diff' ) + [ 'id' => 'wpDiff' ]
                        );
                }
 
@@ -4782,4 +4792,27 @@ class EditPage {
                }
                return $wikitext;
        }
+
+       /**
+        * Turns section name wikitext into anchors for use in HTTP redirects. Various
+        * versions of Microsoft browsers misinterpret fragment encoding of Location: headers
+        * resulting in mojibake in address bar. Redirect them to legacy section IDs,
+        * if possible. All the other browsers get HTML5 if the wiki is configured for it, to
+        * spread the new style links more efficiently.
+        *
+        * @param string $text
+        * @return string
+        */
+       private function guessSectionName( $text ) {
+               global $wgParser;
+
+               // Detect Microsoft browsers
+               $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
+               if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
+                       // ...and redirect them to legacy encoding, if available
+                       return $wgParser->guessLegacySectionNameFromWikiText( $text );
+               }
+               // Meanwhile, real browsers get real anchors
+               return $wgParser->guessSectionNameFromWikiText( $text );
+       }
 }
index 92cb8d8..70784ba 100644 (file)
@@ -3617,6 +3617,7 @@ function wfCanIPUseHTTPS( $ip ) {
  * @since 1.25
  */
 function wfIsInfinity( $str ) {
+       // These are hardcoded elsewhere in MediaWiki (e.g. mediawiki.special.block.js).
        $infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ];
        return in_array( $str, $infinityValues );
 }
index 4aae3ba..2ca851c 100644 (file)
@@ -1608,22 +1608,24 @@ class Linker {
         *   a space and ending with '>'
         *   This *must* be at least '>' for no attribs
         * @param string $anchor The anchor to give the headline (the bit after the #)
-        * @param string $html Html for the text of the header
+        * @param string $html HTML for the text of the header
         * @param string $link HTML to add for the section edit link
-        * @param bool|string $legacyAnchor A second, optional anchor to give for
+        * @param string|bool $fallbackAnchor A second, optional anchor to give for
         *   backward compatibility (false to omit)
         *
         * @return string HTML headline
         */
        public static function makeHeadline( $level, $attribs, $anchor, $html,
-               $link, $legacyAnchor = false
+               $link, $fallbackAnchor = false
        ) {
+               $anchorEscaped = htmlspecialchars( $anchor );
                $ret = "<h$level$attribs"
-                       . "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
+                       . "<span class=\"mw-headline\" id=\"$anchorEscaped\">$html</span>"
                        . $link
                        . "</h$level>";
-               if ( $legacyAnchor !== false ) {
-                       $ret = "<div id=\"$legacyAnchor\"></div>$ret";
+               if ( $fallbackAnchor !== false && $fallbackAnchor !== $anchor ) {
+                       $fallbackAnchor = htmlspecialchars( $fallbackAnchor );
+                       $ret = "<div id=\"$fallbackAnchor\"></div>$ret";
                }
                return $ret;
        }
index 2c87b8b..7a5fd9a 100644 (file)
@@ -42,7 +42,7 @@ class ListToggle {
        private function checkboxLink( $checkboxType ) {
                return Html::element(
                        // CSS classes: mw-checkbox-all, mw-checkbox-none, mw-checkbox-invert
-                       'a', [ 'href' => '#', 'class' => 'mw-checkbox-' . $checkboxType ],
+                       'a', [ 'class' => 'mw-checkbox-' . $checkboxType, 'role' => 'button', 'tabindex' => 0 ],
                        $this->output->msg( 'checkbox-' . $checkboxType )->text()
                );
        }
index 7efbef1..15ed2d4 100644 (file)
@@ -901,6 +901,8 @@ class Preferences {
                ];
                $defaultPreferences['rclimit'] = [
                        'type' => 'int',
+                       'min' => 0,
+                       'max' => 1000,
                        'label-message' => 'recentchangescount',
                        'help-message' => 'prefs-help-recentchangescount',
                        'section' => 'rc/displayrc',
@@ -918,6 +920,9 @@ class Preferences {
                $defaultPreferences['rcfilters-saved-queries'] = [
                        'type' => 'api',
                ];
+               $defaultPreferences['rcfilters-rclimit'] = [
+                       'type' => 'api',
+               ];
 
                if ( $config->get( 'RCWatchCategoryMembership' ) ) {
                        $defaultPreferences['hidecategorization'] = [
index 2def06a..907da16 100644 (file)
@@ -56,6 +56,21 @@ class Sanitizer {
        const EVIL_URI_PATTERN = '!(^|\s|\*/\s*)(javascript|vbscript)([^\w]|$)!i';
        const XMLNS_ATTRIBUTE_PATTERN = "/^xmlns:[:A-Z_a-z-.0-9]+$/";
 
+       /**
+        * Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
+        *
+        * @since 1.30
+        */
+       const ID_PRIMARY = 0;
+
+       /**
+        * Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false
+        * if no fallback is configured.
+        *
+        * @since 1.30
+        */
+       const ID_FALLBACK = 1;
+
        /**
         * List of all named character entities defined in HTML 4.01
         * https://www.w3.org/TR/html4/sgml/entities.html
@@ -800,7 +815,7 @@ class Sanitizer {
 
                        # Escape HTML id attributes
                        if ( $attribute === 'id' ) {
-                               $value = self::escapeId( $value, 'noninitial' );
+                               $value = self::escapeIdForAttribute( $value, Sanitizer::ID_PRIMARY );
                        }
 
                        # Escape HTML id reference lists
@@ -1164,6 +1179,8 @@ class Sanitizer {
         * ambiguous if it's part of something that looks like a percent escape
         * (which don't work reliably in fragments cross-browser).
         *
+        * @deprecated since 1.30, use one of this class' escapeIdFor*() functions
+        *
         * @see https://www.w3.org/TR/html401/types.html#type-name Valid characters
         *   in the id and name attributes
         * @see https://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with
@@ -1215,21 +1232,146 @@ class Sanitizer {
                return $id;
        }
 
+       /**
+        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
+        * a valid HTML id attribute.
+        *
+        * WARNING: unlike escapeId(), the output of this function is not guaranteed to be HTML safe,
+        * be sure to use proper escaping.
+        *
+        * @param string $id String to escape
+        * @param int $mode One of ID_* constants, specifying whether the primary or fallback encoding
+        *     should be used.
+        * @return string|bool Escaped ID or false if fallback encoding is requested but it's not
+        *     configured.
+        *
+        * @since 1.30
+        */
+       public static function escapeIdForAttribute( $id, $mode = self::ID_PRIMARY ) {
+               global $wgFragmentMode;
+
+               if ( !isset( $wgFragmentMode[$mode] ) ) {
+                       if ( $mode === self::ID_PRIMARY ) {
+                               throw new UnexpectedValueException( '$wgFragmentMode is configured with no primary mode' );
+                       }
+                       return false;
+               }
+
+               $internalMode = $wgFragmentMode[$mode];
+
+               return self::escapeIdInternal( $id, $internalMode );
+       }
+
+       /**
+        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
+        * a valid URL fragment.
+        *
+        * WARNING: unlike escapeId(), the output of this function is not guaranteed to be HTML safe,
+        * be sure to use proper escaping.
+        *
+        * @param string $id String to escape
+        * @return string Escaped ID
+        *
+        * @since 1.30
+        */
+       public static function escapeIdForLink( $id ) {
+               global $wgFragmentMode;
+
+               if ( !isset( $wgFragmentMode[self::ID_PRIMARY] ) ) {
+                       throw new UnexpectedValueException( '$wgFragmentMode is configured with no primary mode' );
+               }
+
+               $mode = $wgFragmentMode[self::ID_PRIMARY];
+
+               $id = self::escapeIdInternal( $id, $mode );
+               $id = self::urlEscapeId( $id, $mode );
+
+               return $id;
+       }
+
+       /**
+        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
+        * a valid URL fragment for external interwikis.
+        *
+        * @param string $id String to escape
+        * @return string Escaped ID
+        *
+        * @since 1.30
+        */
+       public static function escapeIdForExternalInterwiki( $id ) {
+               global $wgExternalInterwikiFragmentMode;
+
+               $id = self::escapeIdInternal( $id, $wgExternalInterwikiFragmentMode );
+               $id = self::urlEscapeId( $id, $wgExternalInterwikiFragmentMode );
+
+               return $id;
+       }
+
+       /**
+        * Helper for escapeIdFor*() functions. URL-escapes the ID if needed.
+        *
+        * @param string $id String to escape
+        * @param string $mode One of modes from $wgFragmentMode
+        * @return string
+        */
+       private static function urlEscapeId( $id, $mode ) {
+               if ( $mode === 'html5' ) {
+                       $id = urlencode( $id );
+                       $id = str_replace( '%3A', ':', $id );
+               }
+
+               return $id;
+       }
+
+       /**
+        * Helper for escapeIdFor*() functions. Performs most of the actual escaping.
+        *
+        * @param string $id String to escape
+        * @param string $mode One of modes from $wgFragmentMode
+        * @return string
+        */
+       private static function escapeIdInternal( $id, $mode ) {
+               $id = Sanitizer::decodeCharReferences( $id );
+
+               switch ( $mode ) {
+                       case 'html5':
+                               $id = str_replace( ' ', '_', $id );
+                               break;
+                       case 'legacy':
+                               // This corresponds to 'noninitial' mode of the old escapeId()
+                               static $replace = [
+                                       '%3A' => ':',
+                                       '%' => '.'
+                               ];
+
+                               $id = urlencode( str_replace( ' ', '_', $id ) );
+                               $id = strtr( $id, $replace );
+                               break;
+                       case 'html5-legacy':
+                               $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
+                               $id = trim( $id, '_' );
+                               if ( $id === '' ) {
+                                       // Must have been all whitespace to start with.
+                                       $id = '_';
+                               }
+                               break;
+                       default:
+                               throw new InvalidArgumentException( "Invalid mode '$mode' passed to '" . __METHOD__ );
+               }
+
+               return $id;
+       }
+
        /**
         * Given a string containing a space delimited list of ids, escape each id
         * to match ids escaped by the escapeId() function.
         *
+        * @todo wfDeprecated() uses of $options in 1.31, remove completely in 1.32
+        *
         * @since 1.27
         *
         * @param string $referenceString Space delimited list of ids
-        * @param string|array $options String or array of strings (default is array()):
-        *   'noninitial': This is a non-initial fragment of an id, not a full id,
-        *       so don't pay attention if the first character isn't valid at the
-        *       beginning of an id.  Only matters if $wgExperimentalHtmlIds is
-        *       false.
-        *   'legacy': Behave the way the old HTML 4-based ID escaping worked even
-        *       if $wgExperimentalHtmlIds is used, so we can generate extra
-        *       anchors and links won't break.
+        * @param string|array $options Deprecated and does nothing.
         * @return string
         */
        static function escapeIdReferenceList( $referenceString, $options = [] ) {
@@ -1238,7 +1380,7 @@ class Sanitizer {
 
                # Escape each token as an id
                foreach ( $references as &$ref ) {
-                       $ref = self::escapeId( $ref, $options );
+                       $ref = self::escapeIdForAttribute( $ref );
                }
 
                # Merge the array back to a space delimited list string
index 3d5bee2..68e3d96 100644 (file)
@@ -282,6 +282,11 @@ foreach ( $wgForeignFileRepos as &$repo ) {
 }
 unset( $repo ); // no global pollution; destroy reference
 
+// Convert this deprecated setting to modern system
+if ( $wgExperimentalHtmlIds ) {
+       $wgFragmentMode = [ 'html5-legacy', 'legacy' ];
+}
+
 $rcMaxAgeDays = $wgRCMaxAge / ( 3600 * 24 );
 if ( $wgRCFilterByAge ) {
        // Trim down $wgRCLinkDays so that it only lists links which are valid
index df3e305..6a2d0e2 100644 (file)
@@ -34,9 +34,6 @@ class SiteStats {
        /** @var bool */
        private static $loaded = false;
 
-       /** @var int */
-       private static $jobs;
-
        /** @var int[] */
        private static $pageCount = [];
 
@@ -213,17 +210,24 @@ class SiteStats {
        }
 
        /**
+        * Total number of jobs in the job queue.
         * @return int
         */
        static function jobs() {
-               if ( !isset( self::$jobs ) ) {
-                       try{
-                               self::$jobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
-                       } catch ( JobQueueError $e ) {
-                               self::$jobs = 0;
-                       }
-               }
-               return self::$jobs;
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+               return $cache->getWithSetCallback(
+                       $cache->makeKey( 'SiteStats', 'jobscount' ),
+                       $cache::TTL_MINUTE,
+                       function ( $oldValue, &$ttl, array &$setOpts ) {
+                               try{
+                                       $jobs = array_sum( JobQueueGroup::singleton()->getQueueSizes() );
+                               } catch ( JobQueueError $e ) {
+                                       $jobs = 0;
+                               }
+                               return $jobs;
+                       },
+                       [ 'pcTTL' => $cache::TTL_PROC_LONG ]
+               );
        }
 
        /**
index 6538538..5decece 100644 (file)
@@ -748,6 +748,8 @@ class Title implements LinkTarget {
        /**
         * Escape a text fragment, say from a link, for a URL
         *
+        * @deprecated since 1.30, use Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki()
+        *
         * @param string $fragment Containing a URL or link fragment (after the "#")
         * @return string Escaped string
         */
@@ -1316,6 +1318,23 @@ class Title implements LinkTarget {
                return self::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
        }
 
+       /**
+        * Get a Title object associated with the talk page of this article,
+        * if such a talk page can exist.
+        *
+        * @since 1.30
+        *
+        * @return Title The object for the talk page,
+        *         or null if no associated talk page can exist, according to canHaveTalkPage().
+        */
+       public function getTalkPageIfDefined() {
+               if ( !$this->canHaveTalkPage() ) {
+                       return null;
+               }
+
+               return $this->getTalkPage();
+       }
+
        /**
         * Get a title object associated with the subject page of this
         * talk page
@@ -1382,14 +1401,16 @@ class Title implements LinkTarget {
 
        /**
         * Get the fragment in URL form, including the "#" character if there is one
+        *
         * @return string Fragment in URL form
         */
        public function getFragmentForURL() {
                if ( !$this->hasFragment() ) {
                        return '';
-               } else {
-                       return '#' . self::escapeFragmentForURL( $this->getFragment() );
+               } elseif ( $this->isExternal() && !$this->getTransWikiID() ) {
+                       return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->getFragment() );
                }
+               return '#' . Sanitizer::escapeIdForLink( $this->getFragment() );
        }
 
        /**
@@ -3172,7 +3193,7 @@ class Title implements LinkTarget {
                if ( $limit > -1 ) {
                        $options['LIMIT'] = $limit;
                }
-               $this->mSubpages = TitleArray::newFromResult(
+               return TitleArray::newFromResult(
                        $dbr->select( 'page',
                                [ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
                                $conds,
@@ -3180,7 +3201,6 @@ class Title implements LinkTarget {
                                $options
                        )
                );
-               return $this->mSubpages;
        }
 
        /**
index baec944..68dda37 100644 (file)
@@ -156,8 +156,8 @@ class InfoAction extends FormlessAction {
         * @return string The HTML.
         */
        protected function makeHeader( $header, $canonicalId ) {
-               $spanAttribs = [ 'class' => 'mw-headline', 'id' => Sanitizer::escapeId( $header ) ];
-               $h2Attribs = [ 'id' => Sanitizer::escapeId( $canonicalId ) ];
+               $spanAttribs = [ 'class' => 'mw-headline', 'id' => Sanitizer::escapeIdForAttribute( $header ) ];
+               $h2Attribs = [ 'id' => Sanitizer::escapeIdForAttribute( $canonicalId ) ];
 
                return Html::rawElement( 'h2', $h2Attribs, Html::element( 'span', $spanAttribs, $header ) );
        }
index 034d243..500f432 100644 (file)
@@ -2860,7 +2860,7 @@ abstract class ApiBase extends ContextSource {
         * Return the error message related to a certain array
         * @deprecated since 1.29
         * @param array|string|MessageSpecifier $error Element of a getUserPermissionsErrors()-style array
-        * @return [ 'code' => code, 'info' => info ]
+        * @return array [ 'code' => code, 'info' => info ]
         */
        public function parseMsg( $error ) {
                wfDeprecated( __METHOD__, '1.29' );
index b7d4529..6468235 100644 (file)
@@ -1931,14 +1931,15 @@ class ApiMain extends ApiBase {
 
                        $header = $this->msg( 'api-help-datatypes-header' )->parse();
 
-                       // Add an additional span with sanitized ID
-                       if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) {
-                               $header = Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/datatypes' ) ] ) .
-                                       $header;
-                       }
-                       $help['datatypes'] .= Html::rawElement( 'h' . min( 6, $level ),
-                               [ 'id' => 'main/datatypes', 'class' => 'apihelp-header' ],
-                               $header
+                       $id = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_PRIMARY );
+                       $idFallback = Sanitizer::escapeIdForAttribute( 'main/datatypes', Sanitizer::ID_FALLBACK );
+
+                       $help['datatypes'] .= Linker::makeHeadline( min( 6, $level ),
+                               ' class="apihelp-header"',
+                               $id,
+                               $header,
+                               '',
+                               $idFallback
                        );
                        $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
                        if ( !isset( $tocData['main/datatypes'] ) ) {
@@ -1953,15 +1954,15 @@ class ApiMain extends ApiBase {
                                ];
                        }
 
-                       // Add an additional span with sanitized ID
-                       if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) {
-                               $header = Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/credits' ) ] ) .
-                                       $header;
-                       }
                        $header = $this->msg( 'api-credits-header' )->parse();
-                       $help['credits'] .= Html::rawElement( 'h' . min( 6, $level ),
-                               [ 'id' => 'main/credits', 'class' => 'apihelp-header' ],
-                               $header
+                       $id = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_PRIMARY );
+                       $idFallback = Sanitizer::escapeIdForAttribute( 'main/credits', Sanitizer::ID_FALLBACK );
+                       $help['credits'] .= Linker::makeHeadline( min( 6, $level ),
+                               ' class="apihelp-header"',
+                               $id,
+                               $header,
+                               '',
+                               $idFallback
                        );
                        $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
                        if ( !isset( $tocData['main/credits'] ) ) {
index 29c0b74..3639c06 100644 (file)
@@ -34,7 +34,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
        const LINKS = 'links';
        const TEMPLATES = 'templates';
 
-       private $table, $prefix, $helpUrl;
+       private $table, $prefix, $titlesParam, $helpUrl;
 
        public function __construct( ApiQuery $query, $moduleName ) {
                switch ( $moduleName ) {
index 6fea718..8918620 100644 (file)
        "apihelp-query+langlinks-param-lang": "להחזיר רק קישורי שפה עם קוד השפה הזה.",
        "apihelp-query+langlinks-param-title": "קישור לחיפוש. חובה להשתמש עם <var>$1lang</var>.",
        "apihelp-query+langlinks-param-dir": "באיזה כיוון לרשום.",
-       "apihelp-query+langlinks-param-inlanguagecode": "ק×\95×\93 ×©×¤×\94 ×©שמות שפות מתורגמות.",
+       "apihelp-query+langlinks-param-inlanguagecode": "ק×\95×\93 ×©×¤×\94 ×\91ש×\91×\99×\9c שמות שפות מתורגמות.",
        "apihelp-query+langlinks-example-simple": "קבלת קישורים בין־לשוניים מהדף <kbd>Main Page</kbd>.",
        "apihelp-query+links-summary": "החזרת כל הקישורים מהדפים שצוינו.",
        "apihelp-query+links-param-namespace": "להציג קישורים רק במרחבי השם האלה.",
        "apihelp-query+siteinfo-param-filteriw": "החזרה רק של עיולים מקומיים או רק של עיולים לא מקומיים ממפת הבינוויקי.",
        "apihelp-query+siteinfo-param-showalldb": "רשימת כל שרתי מסד הנתונים, לא רק אלה שהכי מתעכבים.",
        "apihelp-query+siteinfo-param-numberingroup": "רשימת מספרי משתמשים בקבוצות משתמשים.",
-       "apihelp-query+siteinfo-param-inlanguagecode": "ק×\95×\93 ×©×¤×\94 ×©שמות שפות מתורגמות (מאמץ טוב ביותר) ושמות עיצובים.",
+       "apihelp-query+siteinfo-param-inlanguagecode": "ק×\95×\93 ×©×¤×\94 ×\91ש×\91×\99×\9c שמות שפות מתורגמות (מאמץ טוב ביותר) ושמות עיצובים.",
        "apihelp-query+siteinfo-example-simple": "איזור מידע על האתר.",
        "apihelp-query+siteinfo-example-interwiki": "אחזור תחיליות בינוויקי מקומיות.",
        "apihelp-query+siteinfo-example-replag": "בדיקת שיהוי השכפול הנוכחי.",
index 36b580e..bff361e 100644 (file)
        "apihelp-move-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.",
        "apihelp-move-param-ignorewarnings": "모든 경고 무시하기",
        "apihelp-move-example-move": "<kbd>기존 제목</kbd>에서 <kbd>대상 제목</kbd>으로 넘겨주기를 만들지 않고 이동하기.",
-       "apihelp-opensearch-summary": "OpenSearch 프로토콜을 이용하여 위키 검색하기",
+       "apihelp-opensearch-summary": "OpenSearch 프로토콜을 이용하여 위키를 검색합니다.",
        "apihelp-opensearch-param-search": "문자열 검색",
        "apihelp-opensearch-param-limit": "반환할 결과의 최대 수",
        "apihelp-opensearch-param-namespace": "검색할 이름공간.",
        "apihelp-setpagelanguage-param-lang": "문서를 변경할 언어의 언어 코드입니다. 문서를 위키의 기본 콘텐츠 언어로 재설정하려면 <kbd>default</kbd>를 사용하십시오.",
        "apihelp-setpagelanguage-param-reason": "변경 이유.",
        "apihelp-setpagelanguage-example-language": "<kbd>Main Page</kbd>의 언어를 바스크어로 변경합니다.",
+       "apihelp-stashedit-summary": "공유된 캐시에서 편집을 준비합니다.",
        "apihelp-stashedit-param-sectiontitle": "새 문단을 위한 제목.",
        "apihelp-stashedit-param-text": "문서 내용.",
        "apihelp-stashedit-param-contentmodel": "새 콘텐츠의 콘텐츠 모델.",
        "api-help-authmanagerhelper-continue": "이 요청은 초기 <samp>UI</samp> 또는 <samp>REDIRECT</samp> 응답 이후에 계속됩니다. 이것 또는 <var>$1returnurl</var> 중 하나가 필요합니다.",
        "api-help-authmanagerhelper-additional-params": "이 모듈은 사용 가능한 인증 요청에 따라 추가 변수를 허용합니다. 사용 가능한 요청 및 사용되는 필드를 결정하려면 <kbd>amirequestsfor=$1</kbd>(또는 해당되는 경우 이 모듈의 과거 응답)과 함께 <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>을(를) 사용하십시오.",
        "apierror-articleexists": "작성하려는 문서가 이미 만들어져 있습니다.",
+       "apierror-assertbotfailed": "사용자의 <code>bot</code> 권한 보유 표명이 실패했습니다.",
+       "apierror-assertnameduserfailed": "사용자의 \"$1\" 지정 표명이 실패했습니다.",
+       "apierror-assertuserfailed": "사용자의 로그인 표명이 실패했습니다.",
        "apierror-autoblocked": "사용자의 IP 주소는 차단된 사용자에 의해 사용되었으므로 자동으로 차단된 상태입니다.",
        "apierror-badgenerator-unknown": "알 수 없는 <kbd>generator=$1</kbd>.",
        "apierror-badip": "IP 변수가 유효하지 않습니다.",
index ca3462d..8b86af5 100644 (file)
@@ -24,7 +24,7 @@
        "apihelp-main-param-origin": "Når man aksesserer API-en som bruker en domene-kryssende AJAX-forespørsel (CORS), sett denne til det opprinnelige domenet. Denne må tas med i alle pre-flight-forespørsler, og derfor være en del av spørre-URI-en (ikke POST-kroppen).\n\nFor autentiserte forespørsler må denne stemme helt med en av de opprinnelige i <code>Origin</code>-headeren, slik at den må settes til noe a la <kbd>https://en.wikipedia.org</kbd> eller <kbd>https://meta.wikimedia.org</kbd>. Hvis denne parameteren ikke stemmer med <code>Origin</code>-headeren, returneres et 403-svar. Hvis denne parameteren stemmer med <code>Origin</code>-headeren og originalen er hvitlistet, vil <code>Access-Control-Allow-Origin</code> og <code>Access-Control-Allow-Credentials</code>-headere bli satt.\n\nFor ikke-autentiserte forepørsler, spesifiser <kbd>*</kbd>. Denne vil gjøre at <code>Access-Control-Allow-Origin</code>-headeren blir satt, men <code>Access-Control-Allow-Credentials</code> blir <code>false</code> og alle bruerspesifikke data blir begrenset.",
        "apihelp-main-param-uselang": "Språk å bruke for meldingsoversettelser. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> med <kbd>siprop=languages</kbd> returnerer en liste over språkkoder, eller spesifiser <kbd>user</kbd> for å bruke den nåværende brukerens språkpreferanser, eller spesifiser <kbd>content</kbd> for å bruke denne wikiens innholdsspråk.",
        "apihelp-main-param-errorformat": "Formater som kan brukes for advarsels- og feiltekster.\n; plaintext: Wikitext der HTML-tagger er fjernet og elementer byttet ut.\n; wikitext: Ubehandlet wikitext.\n; html: HTML.\n; raw: Meldingsnøkler og -parametre.\n; none: Ingen tekst, bare feilkoder.\n; bc: Format brukt før MediaWiki 1.29. <var>errorlang</var> og <var>errorsuselocal</var> ses bort fra.",
-       "apihelp-main-param-errorlang": "Språk som skal brukes for advarsler og feil. <kbd>[[Specia:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> med <kbd>siprop=languages/<kbd> returnerer ei liste over språkkoder, eller angi <kbd>content</kbd> for å bruke wikiens innholdsspråk, eller angi <kbd>uselang</kbd> for å bruke samme verdi som <var>uselang</var>-parameteren.",
+       "apihelp-main-param-errorlang": "Språk som skal brukes for advarsler og feil. <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> med <kbd>siprop=languages/<kbd> returnerer ei liste over språkkoder, eller angi <kbd>content</kbd> for å bruke wikiens innholdsspråk, eller angi <kbd>uselang</kbd> for å bruke samme verdi som <var>uselang</var>-parameteren.",
        "apihelp-main-param-errorsuselocal": "Hvis gitt, vil feiltekster bruke lokalt tilpassede meldinger fra {{ns:MediaWiki}}-navnerommet.",
        "apihelp-block-summary": "Blokker en bruker.",
        "apihelp-block-param-user": "Brukernavn, IP-adresse eller IP-intervall som skal blokkeres. Kan ikke brukes sammen med <var>$1userid</var>",
        "apihelp-compare-param-fromid": "Første side-ID å sammenligne.",
        "apihelp-compare-param-fromrev": "Første revisjon å sammenligne.",
        "apihelp-compare-param-fromtext": "Bruk denne teksten i stedet for innholdet i revisjonen som angis med <var>fromtitle</var>, <var>fromid</var> eller <var>fromrev</var>.",
+       "apihelp-compare-param-frompst": "Gjør en transformering av <var>fromtext</var> før lagring.",
        "apihelp-compare-param-fromcontentmodel": "Innholdsmodell for <var>fromtext</var>. Om den ikke angis vil den gjettes basert på de andre parameterne.",
        "apihelp-compare-param-fromcontentformat": "Innholdsserialiseringsformat for <var>fromtext</var>.",
        "apihelp-compare-param-totitle": "Andre tittel å sammenligne.",
        "apihelp-compare-param-toid": "Andre side-ID å sammenligne.",
        "apihelp-compare-param-torev": "Andre revisjon å sammenligne.",
+       "apihelp-compare-param-totext": "Bruk denne teksten i stedet for innholdet i revisjonen spesifisert av <var>totitle</var>, <var>toid</var> eller <var>torev</var>.",
+       "apihelp-compare-param-topst": "Gjør en transformering av <var>totext</var> før lagring.",
+       "apihelp-compare-param-tocontentmodel": "Innholdsmodellen til <var>totext</var>. Om denne ikke angis vil den bli gjettet ut fra andre parametere.",
+       "apihelp-compare-param-tocontentformat": "Innholdsserialiseringsformat for <var>totext</var>.",
+       "apihelp-compare-param-prop": "Hvilke informasjonsdeler som skal hentes.",
+       "apihelp-compare-paramvalue-prop-diff": "Diffens HTML.",
+       "apihelp-compare-paramvalue-prop-diffsize": "Størrelsen på diffens HTML i byte.",
+       "apihelp-compare-paramvalue-prop-rel": "Revisjons-ID-en for revisjonene foran «from» og etter «to», om de finnes.",
+       "apihelp-compare-paramvalue-prop-ids": "Side- og revisjons-ID-ene til «from»- og «to»-revisjonene.",
+       "apihelp-compare-paramvalue-prop-title": "Sidetitlene for «from»- og «to»-revisjonene.",
+       "apihelp-compare-paramvalue-prop-user": "Brukernavnet og ID-en til «from»- og «to»-revisjonene.",
+       "apihelp-compare-paramvalue-prop-comment": "Kommentaren til «from»- og «to»-revisjonene.",
+       "apihelp-compare-paramvalue-prop-size": "Størrelsen til «from»- og «to»-revisjonene.",
        "apihelp-compare-example-1": "Lag en diff mellom revisjon 1 og 2.",
        "apihelp-createaccount-summary": "Opprett en ny brukerkonto.",
        "apihelp-createaccount-param-preservestate": "Om <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> returnerte true for <samp>hashprimarypreservedstate</samp> bør forespørsler merket som <samp>primary-required</samp> omgås. Om den returnerte en ikke-tom verdi for <samp>preservedusername</samp> kan det brukernavnet brukes for <var>username</var>-parameteren.",
@@ -81,6 +95,7 @@
        "apihelp-createaccount-example-pass": "Opprett bruker <kbd>testuser</kbd> med passordet <kbd>test123</kbd>.",
        "apihelp-createaccount-example-mail": "Opprett bruker <kbd>testmailuser</kbd> og send et tilfeldig generert passord med e-post.",
        "apihelp-cspreport-summary": "Brukes av nettlesere for å rapportere brudd på Content Security Policy. Denne modulen bør aldri brukes utenom av en CSP-mottakelig nettleser.",
+       "apihelp-cspreport-param-source": "Hva som genererte CSP-headeren som utløste denne rapporten",
        "apihelp-delete-summary": "Slett en side.",
        "apihelp-delete-param-title": "Tittel til siden som skal slettes. Kan ikke brukes sammen med <var>$1pageid</var>.",
        "apihelp-delete-param-pageid": "Side-ID til siden som skal slettes. Kan ikke brukes sammen med <var>$1title</var>.",
index 5aa693d..2182c6c 100644 (file)
@@ -747,20 +747,22 @@ class ChangesList extends ContextSource {
         * @return string[] attribute name => value
         */
        protected function getDataAttributes( RecentChange $rc ) {
+               $attrs = [];
+
                $type = $rc->getAttribute( 'rc_source' );
                switch ( $type ) {
                        case RecentChange::SRC_EDIT:
                        case RecentChange::SRC_NEW:
-                               return [
-                                       'data-mw-revid' => $rc->mAttribs['rc_this_oldid'],
-                               ];
+                               $attrs[ 'data-mw-revid' ] = $rc->mAttribs['rc_this_oldid'];
+                               break;
                        case RecentChange::SRC_LOG:
-                               return [
-                                       'data-mw-logid' => $rc->mAttribs['rc_logid'],
-                                       'data-mw-logaction' => $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'],
-                               ];
-                       default:
-                               return [];
+                               $attrs[ 'data-mw-logid' ] = $rc->mAttribs['rc_logid'];
+                               $attrs[ 'data-mw-logaction' ] = $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'];
+                               break;
                }
+
+               $attrs[ 'data-mw-ts' ] = $rc->getAttribute( 'rc_timestamp' );
+
+               return $attrs;
        }
 }
index 30c6995..55cb9ed 100644 (file)
@@ -102,15 +102,17 @@ class EnhancedChangesList extends ChangesList {
                        $rc->mAttribs['rc_timestamp'],
                        $this->getUser()
                );
+               if ( $this->lastdate === '' ) {
+                       $this->lastdate = $date;
+               }
 
                $ret = '';
 
-               # If it's a new day, add the headline and flush the cache
-               if ( $date != $this->lastdate ) {
-                       # Process current cache
+               # If it's a new day, flush the cache and update $this->lastdate
+               if ( $date !== $this->lastdate ) {
+                       # Process current cache (uses $this->lastdate to generate a heading)
                        $ret = $this->recentChangesBlock();
                        $this->rc_cache = [];
-                       $ret .= Xml::element( 'h4', null, $date ) . "\n";
                        $this->lastdate = $date;
                }
 
@@ -763,7 +765,11 @@ class EnhancedChangesList extends ChangesList {
                        }
                }
 
-               return '<div>' . $blockOut . '</div>';
+               if ( $blockOut === '' ) {
+                       return '';
+               }
+               // $this->lastdate is kept up to date by recentChangesLine()
+               return Xml::element( 'h4', null, $this->lastdate ) . "\n<div>" . $blockOut . '</div>';
        }
 
        /**
index c9b5f96..2eb9b22 100644 (file)
@@ -643,24 +643,32 @@ class ChangeTags {
         * Handles selecting tags, and filtering.
         * Needs $tables to be set up properly, so we can figure out which join conditions to use.
         *
+        * WARNING: If $filter_tag contains more than one tag, this function will add DISTINCT,
+        * which may cause performance problems for your query unless you put the ID field of your
+        * table at the end of the ORDER BY, and set a GROUP BY equal to the ORDER BY. For example,
+        * if you had ORDER BY foo_timestamp DESC, you will now need GROUP BY foo_timestamp, foo_id
+        * ORDER BY foo_timestamp DESC, foo_id DESC.
+        *
         * @param string|array $tables Table names, see Database::select
         * @param string|array $fields Fields used in query, see Database::select
         * @param string|array $conds Conditions used in query, see Database::select
         * @param array $join_conds Join conditions, see Database::select
-        * @param array $options Options, see Database::select
-        * @param bool|string $filter_tag Tag to select on
+        * @param string|array $options Options, see Database::select
+        * @param string|array $filter_tag Tag(s) to select on
         *
         * @throws MWException When unable to determine appropriate JOIN condition for tagging
         */
        public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
-                                                                               &$join_conds, &$options, $filter_tag = false ) {
-               global $wgRequest, $wgUseTagFilter;
+                                                                               &$join_conds, &$options, $filter_tag = '' ) {
+               global $wgUseTagFilter;
 
-               if ( $filter_tag === false ) {
-                       $filter_tag = $wgRequest->getVal( 'tagfilter' );
-               }
+               // Normalize to arrays
+               $tables = (array)$tables;
+               $fields = (array)$fields;
+               $conds = (array)$conds;
+               $options = (array)$options;
 
-               // Figure out which conditions can be done.
+               // Figure out which ID field to use
                if ( in_array( 'recentchanges', $tables ) ) {
                        $join_cond = 'ct_rc_id=rc_id';
                } elseif ( in_array( 'logging', $tables ) ) {
@@ -683,7 +691,13 @@ class ChangeTags {
 
                        $tables[] = 'change_tag';
                        $join_conds['change_tag'] = [ 'INNER JOIN', $join_cond ];
-                       $conds['ct_tag'] = explode( '|', $filter_tag );
+                       $conds['ct_tag'] = $filter_tag;
+                       if (
+                               is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
+                               !in_array( 'DISTINCT', $options )
+                       ) {
+                               $options[] = 'DISTINCT';
+                       }
                }
        }
 
@@ -962,7 +976,7 @@ class ChangeTags {
 
                // tags cannot contain commas (used as a delimiter in tag_summary table),
                // pipe (used as a delimiter between multiple tags in
-               // modifyDisplayQuery), or slashes (would break tag description messages in
+               // SpecialRecentchanges and friends), or slashes (would break tag description messages in
                // MediaWiki namespace)
                if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
                        || strpos( $tag, '/' ) !== false ) {
index b12af19..cbeb1fc 100644 (file)
@@ -104,7 +104,13 @@ class WANCacheReapUpdate implements DeferrableUpdate {
                /** @var WikiPage[]|LocalFile[]|User[] $entities */
                $entities = [];
 
-               $entities[] = WikiPage::factory( Title::newFromTitleValue( $t ) );
+               // You can't create a WikiPage for special pages (-1) or other virtual
+               // namespaces, but special pages do appear in RC sometimes, e.g. for logs
+               // of AbuseFilter filter changes.
+               if ( $t->getNamespace() >= 0 ) {
+                       $entities[] = WikiPage::factory( Title::newFromTitleValue( $t ) );
+               }
+
                if ( $t->inNamespace( NS_FILE ) ) {
                        $entities[] = wfLocalFile( $t->getText() );
                }
index d4bee29..7f9af60 100644 (file)
@@ -1174,17 +1174,17 @@ class DifferenceEngine extends ContextSource {
 
                if ( !$diff && !$otitle ) {
                        $header .= "
-                       <tr style='vertical-align: top;' lang='{$userLang}'>
-                       <td class='diff-ntitle'>{$ntitle}</td>
+                       <tr style=\"vertical-align: top;\" lang=\"{$userLang}\">
+                       <td class=\"diff-ntitle\">{$ntitle}</td>
                        </tr>";
                        $multiColspan = 1;
                } else {
                        if ( $diff ) { // Safari/Chrome show broken output if cols not used
                                $header .= "
-                               <col class='diff-marker' />
-                               <col class='diff-content' />
-                               <col class='diff-marker' />
-                               <col class='diff-content' />";
+                               <col class=\"diff-marker\" />
+                               <col class=\"diff-content\" />
+                               <col class=\"diff-marker\" />
+                               <col class=\"diff-content\" />";
                                $colspan = 2;
                                $multiColspan = 4;
                        } else {
@@ -1193,20 +1193,20 @@ class DifferenceEngine extends ContextSource {
                        }
                        if ( $otitle || $ntitle ) {
                                $header .= "
-                               <tr style='vertical-align: top;' lang='{$userLang}'>
-                               <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
-                               <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
+                               <tr style=\"vertical-align: top;\" lang=\"{$userLang}\">
+                               <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
+                               <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
                                </tr>";
                        }
                }
 
                if ( $multi != '' ) {
-                       $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
-                               "class='diff-multi' lang='{$userLang}'>{$multi}</td></tr>";
+                       $header .= "<tr><td colspan=\"{$multiColspan}\" style=\"text-align: center;\" " .
+                               "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
                }
                if ( $notice != '' ) {
-                       $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " .
-                               "lang='{$userLang}'>{$notice}</td></tr>";
+                       $header .= "<tr><td colspan=\"{$multiColspan}\" style=\"text-align: center;\" " .
+                               "lang=\"{$userLang}\">{$notice}</td></tr>";
                }
 
                return $header . $diff . "</table>";
index 61d0d89..702c2eb 100644 (file)
@@ -400,7 +400,13 @@ class HTMLForm extends ContextSource {
 
                if ( !in_array( $format, $this->availableDisplayFormats, true ) ) {
                        throw new MWException( 'Display format must be one of ' .
-                               print_r( $this->availableDisplayFormats, true ) );
+                               print_r(
+                                       array_merge(
+                                               $this->availableDisplayFormats,
+                                               $this->availableSubclassDisplayFormats
+                                       ),
+                                       true
+                               ) );
                }
 
                // Evil hack for mobile :(
@@ -1686,7 +1692,7 @@ class HTMLForm extends ContextSource {
 
                                        $attributes = [];
                                        if ( $fieldsetIDPrefix ) {
-                                               $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
+                                               $attributes['id'] = Sanitizer::escapeIdForAttribute( "$fieldsetIDPrefix$key" );
                                        }
                                        $subsectionHtml .= $this->wrapFieldSetSection( $legend, $section, $attributes );
                                } else {
@@ -1735,7 +1741,7 @@ class HTMLForm extends ContextSource {
                ];
 
                if ( $sectionName ) {
-                       $attribs['id'] = Sanitizer::escapeId( $sectionName );
+                       $attribs['id'] = Sanitizer::escapeIdForAttribute( $sectionName );
                }
 
                if ( $displayFormat === 'table' ) {
index 83a8023..77ddc1a 100644 (file)
@@ -416,8 +416,8 @@ abstract class HTMLFormField {
                        $this->mDir = $params['dir'];
                }
 
-               $validName = Sanitizer::escapeId( $this->mName );
-               $validName = str_replace( [ '.5B', '.5D' ], [ '[', ']' ], $validName );
+               $validName = urlencode( $this->mName );
+               $validName = str_replace( [ '%5B', '%5D' ], [ '[', ']' ], $validName );
                if ( $this->mName != $validName && !isset( $params['nodata'] ) ) {
                        throw new MWException( "Invalid name '{$this->mName}' passed to " . __METHOD__ );
                }
@@ -430,7 +430,7 @@ abstract class HTMLFormField {
 
                if ( isset( $params['id'] ) ) {
                        $id = $params['id'];
-                       $validId = Sanitizer::escapeId( $id );
+                       $validId = urlencode( $id );
 
                        if ( $id != $validId ) {
                                throw new MWException( "Invalid id '$id' passed to " . __METHOD__ );
@@ -976,7 +976,7 @@ abstract class HTMLFormField {
        }
 
        /**
-        * Returns the attributes required for the tooltip and accesskey.
+        * Returns the attributes required for the tooltip and accesskey, for Html::element() etc.
         *
         * @return array Attributes
         */
@@ -988,6 +988,22 @@ abstract class HTMLFormField {
                return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
        }
 
+       /**
+        * Returns the attributes required for the tooltip and accesskey, for OOUI widgets' config.
+        *
+        * @return array Attributes
+        */
+       public function getTooltipAndAccessKeyOOUI() {
+               if ( empty( $this->mParams['tooltip'] ) ) {
+                       return [];
+               }
+
+               return [
+                       'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
+                       'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
+               ];
+       }
+
        /**
         * Returns the given attributes from the parameters
         *
index ed99802..e47de61 100644 (file)
@@ -66,7 +66,10 @@ class OOUIHTMLForm extends HTMLForm {
                        }
 
                        if ( isset( $this->mSubmitTooltip ) ) {
-                               $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
+                               $attribs += [
+                                       'title' => Linker::titleAttrib( $this->mSubmitTooltip ),
+                                       'accessKey' => Linker::accesskey( $this->mSubmitTooltip ),
+                               ];
                        }
 
                        $attribs['classes'] = [ 'mw-htmlform-submit' ];
@@ -177,7 +180,7 @@ class OOUIHTMLForm extends HTMLForm {
                        'items' => $fieldsHtml,
                ];
                if ( $sectionName ) {
-                       $config['id'] = Sanitizer::escapeId( $sectionName );
+                       $config['id'] = Sanitizer::escapeIdForAttribute( $sectionName );
                }
                if ( is_string( $this->mWrapperLegend ) ) {
                        $config['label'] = $this->mWrapperLegend;
index b080e18..9a956fb 100644 (file)
@@ -52,7 +52,7 @@ class HTMLCheckField extends HTMLFormField {
                        $value = !$value;
                }
 
-               $attr = $this->getTooltipAndAccessKey();
+               $attr = $this->getTooltipAndAccessKeyOOUI();
                $attr['id'] = $this->mID;
                $attr['name'] = $this->mName;
 
index dd9184b..53c6835 100644 (file)
@@ -93,9 +93,9 @@ class HTMLFormFieldCloner extends HTMLFormField {
                                $info['name'] = $name;
                        }
                        if ( isset( $info['id'] ) ) {
-                               $info['id'] = Sanitizer::escapeId( "{$this->mID}--$key--{$info['id']}" );
+                               $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--{$info['id']}" );
                        } else {
-                               $info['id'] = Sanitizer::escapeId( "{$this->mID}--$key--$fieldname" );
+                               $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--$fieldname" );
                        }
                        // Copy the hide-if rules to "child" fields, so that the JavaScript code handling them
                        // (resources/src/mediawiki/htmlform/hide-if.js) doesn't have to handle nested fields.
@@ -313,7 +313,7 @@ class HTMLFormFieldCloner extends HTMLFormField {
                                'type' => 'submit',
                                'formnovalidate' => true,
                                'name' => $name,
-                               'id' => Sanitizer::escapeId( "{$this->mID}--$key--delete" ),
+                               'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--delete" ),
                                'cssclass' => 'mw-htmlform-cloner-delete-button',
                                'default' => $this->getMessage( $label )->text(),
                        ], $this->mParent );
@@ -386,7 +386,7 @@ class HTMLFormFieldCloner extends HTMLFormField {
                        'type' => 'submit',
                        'formnovalidate' => true,
                        'name' => $name,
-                       'id' => Sanitizer::escapeId( "{$this->mID}--create" ),
+                       'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--create" ),
                        'cssclass' => 'mw-htmlform-cloner-create-button',
                        'default' => $this->getMessage( $label )->text(),
                ], $this->mParent );
index 2b6e066..0d5eeba 100644 (file)
@@ -142,7 +142,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
        public function getInputOOUI( $value ) {
                $this->mParent->getOutput()->addModules( 'oojs-ui-widgets' );
 
-               $attr = $this->getTooltipAndAccessKey();
+               $attr = [];
                $attr['id'] = $this->mID;
                $attr['name'] = "{$this->mName}[]";
 
index 06ec372..77ea7cd 100644 (file)
@@ -90,7 +90,7 @@ class HTMLRadioField extends HTMLFormField {
                                $html .= Html::rawElement( 'h1', [], $label ) . "\n";
                                $html .= $this->formatOptions( $info, $value );
                        } else {
-                               $id = Sanitizer::escapeId( $this->mID . "-$info" );
+                               $id = Sanitizer::escapeIdForAttribute( $this->mID . "-$info" );
                                $classes = [ 'mw-htmlform-flatlist-item' ];
                                if ( $wgUseMediaWikiUIEverywhere || $this->mParent instanceof VFormHTMLForm ) {
                                        $classes[] = 'mw-ui-radio';
index 82ec3bf..480c5bb 100644 (file)
@@ -71,7 +71,7 @@ class HTMLTextAreaField extends HTMLFormField {
                        throw new Exception( "OOUIHTMLForm does not support the 'cols' parameter for textareas" );
                }
 
-               $attribs = $this->getTooltipAndAccessKey();
+               $attribs = $this->getTooltipAndAccessKeyOOUI();
 
                if ( $this->mClass !== '' ) {
                        $attribs['classes'] = [ $this->mClass ];
index b0b66ca..1c5a43d 100644 (file)
@@ -140,7 +140,7 @@ class HTMLTextField extends HTMLFormField {
                        $value = '';
                }
 
-               $attribs = $this->getTooltipAndAccessKey();
+               $attribs = $this->getTooltipAndAccessKeyOOUI();
 
                if ( $this->mClass !== '' ) {
                        $attribs['classes'] = [ $this->mClass ];
index c2d26a3..f094745 100644 (file)
@@ -63,9 +63,7 @@ class HTMLUsersMultiselectField extends HTMLUserTextField {
                if ( isset( $this->mParams['placeholder'] ) ) {
                        $params['placeholder'] = $this->mParams['placeholder'];
                } else {
-                       $params['placeholder'] = $this->msg( 'mw-widgets-usersmultiselect-placeholder' )
-                                                       ->inContentLanguage()
-                                                       ->plain();
+                       $params['placeholder'] = $this->msg( 'mw-widgets-usersmultiselect-placeholder' )->plain();
                }
 
                if ( !is_null( $value ) ) {
index 7e43fb5..8f5858b 100644 (file)
@@ -83,6 +83,7 @@ abstract class DatabaseUpdater {
                FixDefaultJsonContentPages::class,
                CleanupEmptyCategories::class,
                AddRFCAndPMIDInterwiki::class,
+               PopulatePPSortKey::class
        ];
 
        /**
@@ -939,10 +940,10 @@ abstract class DatabaseUpdater {
         *
         * @param string $table Name of the table to modify
         * @param string $patch Name of the patch file to apply
-        * @param string $fullpath Whether to treat $patch path as relative or not, defaults to false
+        * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false
         * @return bool False if this was skipped because of schema changes being skipped
         */
-       public function modifyTable( $table, $patch,  $fullpath = false ) {
+       public function modifyTable( $table, $patch, $fullpath = false ) {
                if ( !$this->doTable( $table ) ) {
                        return true;
                }
index 1a9915d..b4de44d 100644 (file)
@@ -101,6 +101,9 @@ class MssqlUpdater extends DatabaseUpdater {
                        [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ],
                        [ 'dropIndex', 'oldimage', 'oi_name_archive_name',
                                'patch-alter-table-oldimage.sql' ],
+
+                       // 1.30
+                       [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
 
index adfe2f6..b4ae1dd 100644 (file)
@@ -301,6 +301,8 @@ class MysqlUpdater extends DatabaseUpdater {
                        [ 'dropIndex', 'user_groups', 'ug_user_group', 'patch-user_groups-primary-key.sql' ],
                        [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ],
                        [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ],
+
+                       // 1.30
                        [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
index 0172f1a..d8db6a2 100644 (file)
@@ -452,6 +452,9 @@ class PostgresUpdater extends DatabaseUpdater {
                        [ 'addPgIndex', 'externallinks', 'el_from_index_60', '( el_from, el_index_60, el_id )' ],
                        [ 'addPgField', 'user_groups', 'ug_expiry', "TIMESTAMPTZ NULL" ],
                        [ 'addPgIndex', 'user_groups', 'user_groups_expiry', '( ug_expiry )' ],
+
+                       // 1.30
+                       [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
 
index 9c90283..46e3e7e 100644 (file)
@@ -165,6 +165,9 @@ class SqliteUpdater extends DatabaseUpdater {
                        [ 'addField', 'externallinks', 'el_index_60', 'patch-externallinks-el_index_60.sql' ],
                        [ 'addField', 'user_groups', 'ug_expiry', 'patch-user_groups-ug_expiry.sql' ],
                        [ 'addIndex', 'image', 'img_user_timestamp', 'patch-image-user-index-2.sql' ],
+
+                       // 1.30
+                       [ 'modifyField', 'image', 'img_media_type', 'patch-add-3d.sql' ],
                ];
        }
 
index 27963bd..defaf1b 100644 (file)
        "config-restart": "Jo, zrëszë znowa",
        "config-env-php": "PHP $1 je wjinastalowóné",
        "config-env-hhvm": "HHVM $1 je wjinastalowóné",
+       "config-memory-raised": "Paraméter PHP <code>memory_limit</code> $1 òstôł zwikszony do $2.",
+       "config-xcache": "[Http://trac.lighttpd.net/xcache/ XCache] je wjinstalowóny",
+       "config-apc": "[Http://www.php.net/apc APC] je wjinstalowóny",
+       "config-apcu": "[http://www.php.net/apcu APCu] je wjinstalowóny",
+       "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] je wjinstalowóny",
+       "config-diff3-bad": "Felënk GNU diff3.",
+       "config-mysql-innodb": "InnoDB",
+       "config-mysql-myisam": "MyISAM",
+       "config-mysql-binary": "binarny",
+       "config-mysql-utf8": "UTF‐8",
+       "config-site-name": "Miono wiki:",
+       "config-site-name-blank": "Wpiszë miono starnów.",
+       "config-ns-other-default": "MòjôWiki",
        "config-admin-box": "Kònto sprôwnika",
        "config-admin-name": "Twòjé miono brëkòwnika:",
        "config-admin-password": "Parola:",
index 71b50de..61acd3e 100644 (file)
@@ -6,7 +6,8 @@
                        "SNN95",
                        "MaxSem",
                        "Aviator",
-                       "Macofe"
+                       "Macofe",
+                       "Jeluang Terluang"
                ]
        },
        "config-desc": "Pemasang MediaWiki",
@@ -56,7 +57,7 @@
        "config-outdated-sqlite": "<strong>Amaran:</strong> anda mempunyai SQLite $1 yang lebih rendah daripada versi keperluan minimum $1. SQLite tidak akan disediakan.",
        "config-no-fts3": "<strong>Amaran:</strong> SQLite disusun tanpa [//sqlite.org/fts3.html modil FTS3], maka ciri-ciri pencarian tidak akan disediakan pada backend ini.",
        "config-pcre-old": "<strong>Amaran keras:</strong> PCRE $1 ke atas diperlukan.\nBinari PHP anda berpaut dengan PCRE $2.\n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Keterangan lanjut].",
-       "config-memory-bad": "<strong>Amaran:</strong> <code>memory_limit</code> (Had memori) PHP adalah $1.\nIni mungkin terlalu rendah.\nPemasangan mungkin akan gagal!",
+       "config-memory-bad": "<strong>Amaran:</strong> <code>memory_limit</code> (Had memori) PHP ialah $1.\nIni mungkin terlalu rendah.\nPemasangan mungkin akan gagal!",
        "config-xcache": "[http://xcache.lighttpd.net/ XCache] dipasang",
        "config-apc": "[http://www.php.net/apc APC] dipasang",
        "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] dipasang",
@@ -97,7 +98,7 @@
        "config-mysql-engine": "Enjin storan:",
        "config-mysql-innodb": "InnoDB",
        "config-mysql-myisam": "MyISAM",
-       "config-mysql-only-myisam-dep": "<strong>Amaran:</strong> MyISAM adalah satu-satunya enjin storan yang terdapat untuk MySQL di mesin ini, dan penggunaannya dengan MediaWiki tidak digalakkan kerana:\n* ia tidak menyokong keserempakan (''concurrency'') disebabkan penguncian jadual\n* ia lebih terdedah kepada korupsi daripada enjin-enjin lain\n* pangkalan kod MediaWiki tidak sentiasa mengendalikan MyISAM seperti yang diharapkan\n\nPemasangan MySQL anda tidak menyokong InnoDB. Mungkin tiba masanya untuk naik taraf.",
+       "config-mysql-only-myisam-dep": "<strong>Amaran:</strong> MyISAM ialah satu-satunya enjin storan yang terdapat untuk MySQL di mesin ini, dan penggunaannya dengan MediaWiki tidak digalakkan kerana:\n* ia tidak menyokong keserempakan (''concurrency'') disebabkan penguncian jadual\n* ia lebih terdedah kepada korupsi daripada enjin-enjin lain\n* pangkalan kod MediaWiki tidak sentiasa mengendalikan MyISAM seperti yang diharapkan\n\nPemasangan MySQL anda tidak menyokong InnoDB. Mungkin tiba masanya untuk naik taraf.",
        "config-mysql-charset": "Peranggu aksara pangkalan data:",
        "config-mysql-binary": "Perduaan",
        "config-mysql-utf8": "UTF-8",
index 686ebd2..f892527 100644 (file)
        "config-mssql-windowsauth": "Autenticação do Windows",
        "config-site-name": "Nome da wiki:",
        "config-site-name-help": "Este nome aparecerá no título da janela do seu navegador e em vários outros sítios.",
-       "config-site-name-blank": "Introduza o nome do sítio.",
+       "config-site-name-blank": "Introduza o nome do site.",
        "config-project-namespace": "Espaço nominal do projeto:",
        "config-ns-generic": "Projeto",
        "config-ns-site-name": "O mesmo que o nome da wiki: $1",
        "config-logo": "URL do logótipo:",
        "config-logo-help": "O tema padrão do MediaWiki inclui espaço para um logótipo de 135x160 píxeis acima do menu da barra lateral.\nColoque na wiki uma imagem com estas dimensões e introduza aqui o URL dessa imagem.\n\nSe não pretende usar um logótipo, deixe este campo em branco.",
        "config-instantcommons": "Ativar Instant Commons",
-       "config-instantcommons-help": "O [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] é uma funcionalidade que permite que as wikis usem imagens, áudio e outros ficheiros multimédia disponíveis no sítio [https://commons.wikimedia.org/ Wikimedia Commons].\nPara poder usá-los, o MediaWiki necessita de acesso à Internet.\n\nPara mais informações sobre esta funcionalidade, incluindo instruções sobre como configurá-la para usar outras wikis em vez da Wikimedia Commons, consulte o [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos Manual Técnico].",
+       "config-instantcommons-help": "O [https://www.mediawiki.org/wiki/InstantCommons Instant Commons] é uma funcionalidade que permite que as wikis usem imagens, áudio e outros ficheiros multimédia disponíveis no site [https://commons.wikimedia.org/ Wikimedia Commons].\nPara poder usá-los, o MediaWiki necessita de acesso à Internet.\n\nPara mais informações sobre esta funcionalidade, incluindo instruções sobre como configurá-la para usar outras wikis em vez da Wikimedia Commons, consulte o [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgForeignFileRepos Manual Técnico].",
        "config-cc-error": "O auxiliar de escolha de licenças da Creative Commons não produziu resultados.\nIntroduza o nome da licença manualmente.",
        "config-cc-again": "Escolha outra vez...",
        "config-cc-not-chosen": "Escolha a licença da Creative Commons que pretende e clique \"proceed\".",
index 09f2537..11c13d7 100644 (file)
@@ -53,6 +53,9 @@
        "config-invalid-db-type": "Tipe de database invalide.",
        "config-mysql-innodb": "InnoDB",
        "config-mysql-myisam": "MyISAM",
+       "config-mysql-binary": "Binarie",
+       "config-mysql-utf8": "UTF-8",
+       "config-ns-generic": "Proggette",
        "config-admin-email": "Indirizze e-mail:",
        "config-install-step-done": "fatte",
        "config-install-step-failed": "fallite",
index 7fdd617..44721d9 100644 (file)
@@ -28,7 +28,7 @@
  * For example, one can set $wgJobTypeConf['refreshLinks'] to point to a
  * JobQueueFederated instance, which itself would consist of three JobQueueRedis
  * instances, each using their own redis server. This would allow for the jobs
- * to be split (evenly or based on weights) accross multiple servers if a single
+ * to be split (evenly or based on weights) across multiple servers if a single
  * server becomes impractical or expensive. Different JobQueue classes can be mixed.
  *
  * The basic queue configuration (e.g. "order", "claimTTL") of a federated queue
diff --git a/includes/jobqueue/JobQueueSecondTestQueue.php b/includes/jobqueue/JobQueueSecondTestQueue.php
new file mode 100644 (file)
index 0000000..a1935df
--- /dev/null
@@ -0,0 +1,280 @@
+<?php
+
+/**
+ * A wrapper for the JobQueue that delegates all the method calls to a single,
+ * main queue, and also pushes all the jobs to a second job queue that's being
+ * debugged.
+ *
+ * This class was temporary added to test transitioning to the JobQueueEventBus
+ * and will removed after the transition is completed. This code is only needed
+ * while we are testing the new infrastructure to be able to compare the results
+ * between the queue implementations and make sure that they process the same jobs,
+ * deduplicate correctly, compare the delays, backlogs and make sure no jobs are lost.
+ * When the new infrastructure is well tested this will not be needed any more.
+ *
+ * @deprecated since 1.30
+ * @since 1.30
+ */
+class JobQueueSecondTestQueue extends JobQueue {
+
+       /**
+        * @var JobQueue
+        */
+       private $mainQueue;
+
+       /**
+        * @var JobQueue
+        */
+       private $debugQueue;
+
+       protected function __construct( array $params ) {
+               if ( !isset( $params['mainqueue'] ) ) {
+                       throw new MWException( "mainqueue parameter must be provided to the debug queue" );
+               }
+
+               if ( !isset( $params['debugqueue'] ) ) {
+                       throw new MWException( "debugqueue parameter must be provided to the debug queue" );
+               }
+
+               $conf = [ 'wiki' => $params['wiki'], 'type' => $params['type'] ];
+               $this->mainQueue = JobQueue::factory( $params['mainqueue'] + $conf );
+               $this->debugQueue = JobQueue::factory( $params['debugqueue'] + $conf );
+
+               // We need to construct parent after creating the main and debug queue
+               // because super constructor calls some methods we delegate to the main queue.
+               parent::__construct( $params );
+       }
+
+       /**
+        * Get the allowed queue orders for configuration validation
+        *
+        * @return array Subset of (random, timestamp, fifo, undefined)
+        */
+       protected function supportedOrders() {
+               return $this->mainQueue->supportedOrders();
+       }
+
+       /**
+        * Get the default queue order to use if configuration does not specify one
+        *
+        * @return string One of (random, timestamp, fifo, undefined)
+        */
+       protected function optimalOrder() {
+               return $this->mainQueue->optimalOrder();
+       }
+
+       /**
+        * Find out if delayed jobs are supported for configuration validation
+        *
+        * @return bool Whether delayed jobs are supported
+        */
+       protected function supportsDelayedJobs() {
+               return $this->mainQueue->supportsDelayedJobs();
+       }
+
+       /**
+        * @see JobQueue::isEmpty()
+        * @return bool
+        */
+       protected function doIsEmpty() {
+               return $this->mainQueue->doIsEmpty();
+       }
+
+       /**
+        * @see JobQueue::getSize()
+        * @return int
+        */
+       protected function doGetSize() {
+               return $this->mainQueue->doGetSize();
+       }
+
+       /**
+        * @see JobQueue::getAcquiredCount()
+        * @return int
+        */
+       protected function doGetAcquiredCount() {
+               return $this->mainQueue->doGetAcquiredCount();
+       }
+
+       /**
+        * @see JobQueue::getDelayedCount()
+        * @return int
+        */
+       protected function doGetDelayedCount() {
+               return $this->mainQueue->doGetDelayedCount();
+       }
+
+       /**
+        * @see JobQueue::getAbandonedCount()
+        * @return int
+        */
+       protected function doGetAbandonedCount() {
+               return $this->mainQueue->doGetAbandonedCount();
+       }
+
+       /**
+        * @see JobQueue::batchPush()
+        * @param IJobSpecification[] $jobs
+        * @param int $flags
+        */
+       protected function doBatchPush( array $jobs, $flags ) {
+               $this->mainQueue->doBatchPush( $jobs, $flags );
+
+               try {
+                       $this->debugQueue->doBatchPush( $jobs, $flags );
+               } catch ( Exception $exception ) {
+                       MWExceptionHandler::logException( $exception );
+               }
+       }
+
+       /**
+        * @see JobQueue::pop()
+        * @return Job|bool
+        */
+       protected function doPop() {
+               return $this->mainQueue->doPop();
+       }
+
+       /**
+        * @see JobQueue::ack()
+        * @param Job $job
+        */
+       protected function doAck( Job $job ) {
+               return $this->mainQueue->doAck( $job );
+       }
+
+       /**
+        * @see JobQueue::deduplicateRootJob()
+        * @param IJobSpecification $job
+        * @throws MWException
+        * @return bool
+        */
+       protected function doDeduplicateRootJob( IJobSpecification $job ) {
+               return $this->mainQueue->doDeduplicateRootJob( $job );
+       }
+
+       /**
+        * @see JobQueue::isRootJobOldDuplicate()
+        * @param Job $job
+        * @return bool
+        */
+       protected function doIsRootJobOldDuplicate( Job $job ) {
+               return $this->mainQueue->doIsRootJobOldDuplicate( $job );
+       }
+
+       /**
+        * @param string $signature Hash identifier of the root job
+        * @return string
+        */
+       protected function getRootJobCacheKey( $signature ) {
+               return $this->mainQueue->getRootJobCacheKey( $signature );
+       }
+
+       /**
+        * @see JobQueue::delete()
+        * @throws MWException
+        */
+       protected function doDelete() {
+               return $this->mainQueue->doDelete();
+       }
+
+       /**
+        * @see JobQueue::waitForBackups()
+        * @return void
+        */
+       protected function doWaitForBackups() {
+               $this->mainQueue->doWaitForBackups();
+       }
+
+       /**
+        * @see JobQueue::flushCaches()
+        * @return void
+        */
+       protected function doFlushCaches() {
+               $this->mainQueue->doFlushCaches();
+       }
+
+       /**
+        * Get an iterator to traverse over all available jobs in this queue.
+        * This does not include jobs that are currently acquired or delayed.
+        * Note: results may be stale if the queue is concurrently modified.
+        *
+        * @return Iterator
+        * @throws JobQueueError
+        */
+       public function getAllQueuedJobs() {
+               return $this->mainQueue->getAllQueuedJobs();
+       }
+
+       /**
+        * Get an iterator to traverse over all delayed jobs in this queue.
+        * Note: results may be stale if the queue is concurrently modified.
+        *
+        * @return Iterator
+        * @throws JobQueueError
+        * @since 1.22
+        */
+       public function getAllDelayedJobs() {
+               return $this->mainQueue->getAllDelayedJobs();
+       }
+
+       /**
+        * Get an iterator to traverse over all claimed jobs in this queue
+        *
+        * Callers should be quick to iterator over it or few results
+        * will be returned due to jobs being acknowledged and deleted
+        *
+        * @return Iterator
+        * @throws JobQueueError
+        * @since 1.26
+        */
+       public function getAllAcquiredJobs() {
+               return $this->mainQueue->getAllAcquiredJobs();
+       }
+
+       /**
+        * Get an iterator to traverse over all abandoned jobs in this queue
+        *
+        * @return Iterator
+        * @throws JobQueueError
+        * @since 1.25
+        */
+       public function getAllAbandonedJobs() {
+               return $this->mainQueue->getAllAbandonedJobs();
+       }
+
+       /**
+        * Do not use this function outside of JobQueue/JobQueueGroup
+        *
+        * @return string
+        * @since 1.22
+        */
+       public function getCoalesceLocationInternal() {
+               return $this->mainQueue->getCoalesceLocationInternal();
+       }
+
+       /**
+        * @see JobQueue::getSiblingQueuesWithJobs()
+        * @param array $types List of queues types
+        * @return array|null (list of queue types) or null if unsupported
+        */
+       protected function doGetSiblingQueuesWithJobs( array $types ) {
+               return $this->mainQueue->doGetSiblingQueuesWithJobs( $types );
+       }
+
+       /**
+        * @see JobQueue::getSiblingQueuesSize()
+        * @param array $types List of queues types
+        * @return array|null (list of queue types) or null if unsupported
+        */
+       protected function doGetSiblingQueueSizes( array $types ) {
+               return $this->mainQueue->doGetSiblingQueueSizes( $types );
+       }
+
+       /**
+        * @throws JobQueueReadOnlyError
+        */
+       protected function assertNotReadOnly() {
+               $this->mainQueue->assertNotReadOnly();
+       }
+}
index 9e060cd..cd80066 100644 (file)
@@ -188,17 +188,7 @@ class CSSMin {
                        return self::$mimeTypes[$ext];
                }
 
-               $realpath = realpath( $file );
-               if (
-                       $realpath
-                       && function_exists( 'finfo_file' )
-                       && function_exists( 'finfo_open' )
-                       && defined( 'FILEINFO_MIME_TYPE' )
-               ) {
-                       return finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $realpath );
-               }
-
-               return false;
+               return mime_content_type( realpath( $file ) );
        }
 
        /**
@@ -394,6 +384,9 @@ class CSSMin {
                return false;
        }
 
+       /**
+        * @codeCoverageIgnore
+        */
        private static function getUrlRegex() {
                static $urlRegex;
                if ( $urlRegex === null ) {
index e860ec4..f9dcc1b 100644 (file)
  * @since 1.25
  */
 class StatusValue {
+
        /** @var bool */
        protected $ok = true;
-       /** @var array */
+
+       /** @var array[] */
        protected $errors = [];
 
        /** @var mixed */
        public $value;
-       /** @var array Map of (key => bool) to indicate success of each part of batch operations */
+
+       /** @var bool[] Map of (key => bool) to indicate success of each part of batch operations */
        public $success = [];
+
        /** @var int Counter for batch operations */
        public $successCount = 0;
+
        /** @var int Counter for batch operations */
        public $failCount = 0;
 
@@ -138,7 +143,7 @@ class StatusValue {
         *
         * Each error is a (message:string or MessageSpecifier,params:array) map
         *
-        * @return array
+        * @return array[]
         */
        public function getErrors() {
                return $this->errors;
@@ -230,7 +235,7 @@ class StatusValue {
         *   - params: array list of parameters
         *
         * @param string $type
-        * @return array
+        * @return array[]
         */
        public function getErrorsByType( $type ) {
                $result = [];
index 9bfdbe8..77473d1 100644 (file)
@@ -1840,14 +1840,8 @@ abstract class FileBackendStore extends FileBackend {
                        return call_user_func_array( $this->mimeCallback, func_get_args() );
                }
 
-               $mime = null;
-               if ( $fsPath !== null && function_exists( 'finfo_file' ) ) {
-                       $finfo = finfo_open( FILEINFO_MIME_TYPE );
-                       $mime = finfo_file( $finfo, $fsPath );
-                       finfo_close( $finfo );
-               }
-
-               return is_string( $mime ) ? $mime : 'unknown/unknown';
+               $mime = ( $fsPath !== null ) ? mime_content_type( $fsPath ) : false;
+               return $mime ?: 'unknown/unknown';
        }
 }
 
index 631bb17..4d860bb 100644 (file)
@@ -988,18 +988,8 @@ EOT;
                $m = null;
                if ( $callback ) {
                        $m = $callback( $file );
-               } elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) {
-                       $mime_magic_resource = finfo_open( FILEINFO_MIME );
-
-                       if ( $mime_magic_resource ) {
-                               $m = finfo_file( $mime_magic_resource, $file );
-                               finfo_close( $mime_magic_resource );
-                       } else {
-                               $this->logger->info( __METHOD__ .
-                                       ": finfo_open failed on " . FILEINFO_MIME . "!\n" );
-                       }
                } else {
-                       $this->logger->info( __METHOD__ . ": no magic mime detector found!\n" );
+                       $m = mime_content_type( $file );
                }
 
                if ( $m ) {
index 723a4a6..b8b44e6 100644 (file)
@@ -2664,10 +2664,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
-               if ( $this->mTrxLevel ) {
+               if ( $this->mTrxLevel || $this->getFlag( self::DBO_TRX ) ) {
+                       // As long as DBO_TRX is set, writes will accumulate until the load balancer issues
+                       // an implicit commit of all peer databases. This is true even if a transaction has
+                       // not yet been triggered by writes; make sure $callback runs *after* any such writes.
                        $this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
                } else {
-                       // If no transaction is active, then make one for this callback
+                       // No transaction is active nor will start implicitly, so make one for this callback
                        $this->startAtomic( __METHOD__ );
                        try {
                                call_user_func( $callback );
index 9242414..870fc3e 100644 (file)
@@ -567,7 +567,7 @@ class DatabaseSqlite extends Database {
 
        /**
         * @param array $options
-        * @return string
+        * @return array
         */
        protected function makeUpdateOptionsArray( $options ) {
                $options = parent::makeUpdateOptionsArray( $options );
index a0bfb59..1ed18cd 100644 (file)
@@ -60,7 +60,7 @@ class BlockLogFormatter extends LogFormatter {
                        // is shown on the correct side of the tooltip text.
                        $durationTooltip = '&lrm;' . htmlspecialchars( $params[4] );
                        $params[4] = Message::rawParam(
-                               "<span class='blockExpiry' title='$durationTooltip'>" .
+                               "<span class=\"blockExpiry\" title=\"$durationTooltip\">" .
                                $this->context->getLanguage()->translateBlockExpiry(
                                        $params[4],
                                        $this->context->getUser(),
index 932797a..2931d9d 100644 (file)
@@ -343,7 +343,7 @@ class EmailNotification {
                $keys['$PAGETITLE'] = $this->title->getPrefixedText();
                $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
                $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
-                       wfMessage( 'minoredit' )->inContentLanguage()->text() : '';
+                       wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() : '';
                $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
 
                if ( $this->editor->isAnon() ) {
index 7dea271..2d7e8f2 100644 (file)
@@ -27,7 +27,7 @@
  */
 class CategoryPage extends Article {
        # Subclasses can change this to override the viewer class.
-       public $mCategoryViewerClass = 'CategoryViewer';
+       protected $mCategoryViewerClass = 'CategoryViewer';
 
        /**
         * @var WikiCategoryPage
@@ -117,4 +117,12 @@ class CategoryPage extends Article {
                $out->addHTML( $viewer->getHTML() );
                $this->addHelpLink( 'Help:Categories' );
        }
+
+       function getCategoryViewerClass() {
+               return $this->mCategoryViewerClass;
+       }
+
+       function setCategoryViewerClass( $class ) {
+               $this->mCategoryViewerClass = $class;
+       }
 }
index d37700b..b870831 100644 (file)
@@ -254,15 +254,16 @@ class ImagePage extends Article {
                $r .= "<table id=\"mw_metadata\" class=\"mw_metadata\">\n";
                foreach ( $metadata as $type => $stuff ) {
                        foreach ( $stuff as $v ) {
-                               # @todo FIXME: Why is this using escapeId for a class?!
-                               $class = Sanitizer::escapeId( $v['id'] );
+                               $class = str_replace( ' ', '_', $v['id'] );
                                if ( $type == 'collapsed' ) {
                                        // Handled by mediawiki.action.view.metadata module.
                                        $class .= ' collapsable';
                                }
-                               $r .= "<tr class=\"$class\">\n";
-                               $r .= "<th>{$v['name']}</th>\n";
-                               $r .= "<td>{$v['value']}</td>\n</tr>";
+                               $r .= Html::rawElement( 'tr',
+                                       [ 'class' => $class ],
+                                       Html::rawElement( 'th', [], $v['name'] )
+                                               . Html::rawElement( 'td', [], $v['value'] )
+                               );
                        }
                }
                $r .= "</table>\n</div>\n";
index b035b02..88439db 100644 (file)
@@ -4035,7 +4035,7 @@ class Parser {
         * @private
         */
        public function formatHeadings( $text, $origText, $isMain = true ) {
-               global $wgMaxTocLevel, $wgExperimentalHtmlIds;
+               global $wgMaxTocLevel;
 
                # Inhibit editsection links if requested in the page
                if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
@@ -4229,61 +4229,44 @@ class Parser {
                        # Save headline for section edit hint before it's escaped
                        $headlineHint = $safeHeadline;
 
-                       if ( $wgExperimentalHtmlIds ) {
-                               # For reverse compatibility, provide an id that's
-                               # HTML4-compatible, like we used to.
-                               # It may be worth noting, academically, that it's possible for
-                               # the legacy anchor to conflict with a non-legacy headline
-                               # anchor on the page.  In this case likely the "correct" thing
-                               # would be to either drop the legacy anchors or make sure
-                               # they're numbered first.  However, this would require people
-                               # to type in section names like "abc_.D7.93.D7.90.D7.A4"
-                               # manually, so let's not bother worrying about it.
-                               $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
-                                       [ 'noninitial', 'legacy' ] );
-                               $safeHeadline = Sanitizer::escapeId( $safeHeadline );
-
-                               if ( $legacyHeadline == $safeHeadline ) {
-                                       # No reason to have both (in fact, we can't)
-                                       $legacyHeadline = false;
-                               }
-                       } else {
-                               $legacyHeadline = false;
-                               $safeHeadline = Sanitizer::escapeId( $safeHeadline,
-                                       'noninitial' );
+                       $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
+                       $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
+                       $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
+                       if ( $fallbackHeadline === $safeHeadline ) {
+                               # No reason to have both (in fact, we can't)
+                               $fallbackHeadline = false;
                        }
 
-                       # HTML names must be case-insensitively unique (T12721).
-                       # This does not apply to Unicode characters per
-                       # https://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison
+                       # HTML IDs must be case-insensitively unique for IE compatibility (T12721).
                        # @todo FIXME: We may be changing them depending on the current locale.
                        $arrayKey = strtolower( $safeHeadline );
-                       if ( $legacyHeadline === false ) {
-                               $legacyArrayKey = false;
+                       if ( $fallbackHeadline === false ) {
+                               $fallbackArrayKey = false;
                        } else {
-                               $legacyArrayKey = strtolower( $legacyHeadline );
+                               $fallbackArrayKey = strtolower( $fallbackHeadline );
                        }
 
                        # Create the anchor for linking from the TOC to the section
                        $anchor = $safeHeadline;
-                       $legacyAnchor = $legacyHeadline;
+                       $fallbackAnchor = $fallbackHeadline;
                        if ( isset( $refers[$arrayKey] ) ) {
                                // @codingStandardsIgnoreStart
                                for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
                                // @codingStandardsIgnoreEnd
                                $anchor .= "_$i";
+                               $linkAnchor .= "_$i";
                                $refers["${arrayKey}_$i"] = true;
                        } else {
                                $refers[$arrayKey] = true;
                        }
-                       if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
+                       if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
                                // @codingStandardsIgnoreStart
-                               for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
+                               for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
                                // @codingStandardsIgnoreEnd
-                               $legacyAnchor .= "_$i";
-                               $refers["${legacyArrayKey}_$i"] = true;
+                               $fallbackAnchor .= "_$i";
+                               $refers["${fallbackArrayKey}_$i"] = true;
                        } else {
-                               $refers[$legacyArrayKey] = true;
+                               $refers[$fallbackArrayKey] = true;
                        }
 
                        # Don't number the heading if it is the only one (looks silly)
@@ -4297,7 +4280,7 @@ class Parser {
                        }
 
                        if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
-                               $toc .= Linker::tocLine( $anchor, $tocline,
+                               $toc .= Linker::tocLine( $linkAnchor, $tocline,
                                        $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
                        }
 
@@ -4364,7 +4347,7 @@ class Parser {
                        }
                        $head[$headlineCount] = Linker::makeHeadline( $level,
                                $matches['attrib'][$headlineCount], $anchor, $headline,
-                               $editlink, $legacyAnchor );
+                               $editlink, $fallbackAnchor );
 
                        $headlineCount++;
                }
@@ -5806,22 +5789,33 @@ class Parser {
                # Strip out wikitext links(they break the anchor)
                $text = $this->stripSectionName( $text );
                $text = Sanitizer::normalizeSectionNameWhitespace( $text );
-               return '#' . Sanitizer::escapeId( $text, 'noninitial' );
+               return '#' . Sanitizer::escapeIdForLink( $text );
        }
 
        /**
         * Same as guessSectionNameFromWikiText(), but produces legacy anchors
-        * instead.  For use in redirects, since IE6 interprets Redirect: headers
-        * as something other than UTF-8 (apparently?), resulting in breakage.
+        * instead, if possible. For use in redirects, since various versions
+        * of Microsoft browsers interpret Location: headers as something other
+        * than UTF-8, resulting in breakage.
         *
         * @param string $text The section name
         * @return string An anchor
         */
        public function guessLegacySectionNameFromWikiText( $text ) {
+               global $wgFragmentMode;
+
                # Strip out wikitext links(they break the anchor)
                $text = $this->stripSectionName( $text );
                $text = Sanitizer::normalizeSectionNameWhitespace( $text );
-               return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] );
+
+               if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
+                       // ForAttribute() and ForLink() are the same for legacy encoding
+                       $id = Sanitizer::escapeIdForAttribute( $text, Sanitizer::ID_FALLBACK );
+               } else {
+                       $id = Sanitizer::escapeIdForLink( $text );
+               }
+
+               return "#$id";
        }
 
        /**
index 79b8e79..4675191 100644 (file)
@@ -580,6 +580,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                        'fileHashes' => $this->getFileHashes( $context ),
                        'messageBlob' => $this->getMessageBlob( $context ),
                ];
+
+               $lessVars = $this->getLessVars( $context );
+               if ( $lessVars ) {
+                       $summary[] = [ 'lessVars' => $lessVars ];
+               }
+
                return $summary;
        }
 
diff --git a/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php b/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php
new file mode 100644 (file)
index 0000000..166f1ba
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+/**
+ * ResourceLoader mediawiki.util module
+ *
+ * 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
+ */
+
+/**
+ * ResourceLoader module for mediawiki.util
+ *
+ * @since 1.30
+ */
+class ResourceLoaderMediaWikiUtilModule extends ResourceLoaderFileModule {
+       /**
+        * @inheritdoc
+        */
+       public function getScript( ResourceLoaderContext $context ) {
+               return ResourceLoader::makeConfigSetScript(
+                               [ 'wgFragmentMode' => $this->getConfig()->get( 'FragmentMode' ) ]
+                       )
+                       . "\n"
+                       . parent::getScript( $context );
+       }
+
+       /**
+        * @inheritdoc
+        */
+       public function supportsURLLoading() {
+               return false;
+       }
+
+       /**
+        * @inheritdoc
+        */
+       public function enableModuleContentVersion() {
+               return true;
+       }
+}
index 0b7fc2f..aad676f 100644 (file)
@@ -678,7 +678,7 @@ abstract class BaseTemplate extends QuickTemplate {
                }
                foreach ( $validFooterIcons as $blockName => $footerIcons ) {
                        $html .= Html::openElement( 'div', [
-                               'id' => 'f-' . Sanitizer::escapeId( $blockName ) . 'ico',
+                               'id' => Sanitizer::escapeIdForAttribute( "f-{$blockName}ico" ),
                                'class' => 'footer-icons'
                        ] );
                        foreach ( $footerIcons as $icon ) {
@@ -691,7 +691,7 @@ abstract class BaseTemplate extends QuickTemplate {
                        foreach ( $validFooterLinks as $aLink ) {
                                $html .= Html::rawElement(
                                        'li',
-                                       [ 'id' => Sanitizer::escapeId( $aLink ) ],
+                                       [ 'id' => Sanitizer::escapeIdForAttribute( $aLink ) ],
                                        $this->get( $aLink )
                                );
                        }
@@ -734,7 +734,7 @@ abstract class BaseTemplate extends QuickTemplate {
                        $out .= Html::rawElement(
                                'div',
                                [
-                                       'id' => Sanitizer::escapeId( "mw-indicator-$id" ),
+                                       'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
                                        'class' => 'mw-indicator',
                                ],
                                $content
index 40905a5..849362a 100644 (file)
@@ -1375,8 +1375,8 @@ abstract class Skin extends ContextSource {
                                        $bar[$heading][] = array_merge( [
                                                'text' => $text,
                                                'href' => $href,
-                                               'id' => 'n-' . Sanitizer::escapeId( strtr( $line[1], ' ', '-' ), 'noninitial' ),
-                                               'active' => false
+                                               'id' => Sanitizer::escapeIdForAttribute( 'n-' . strtr( $line[1], ' ', '-' ) ),
+                                               'active' => false,
                                        ], $extraAttribs );
                                } else {
                                        continue;
index 5c048a2..9d6fd5b 100644 (file)
@@ -26,7 +26,9 @@ use MediaWiki\Auth\AuthenticationResponse;
 use MediaWiki\Auth\AuthManager;
 use MediaWiki\Auth\Throttler;
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Session\SessionManager;
+use Wikimedia\ScopedCallback;
 
 /**
  * Holds shared logic for login and account creation pages.
@@ -212,6 +214,15 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
         * @param string|null $subPage
         */
        public function execute( $subPage ) {
+               if ( $this->mPosted ) {
+                       $time = microtime( true );
+                       $profilingScope = new ScopedCallback( function () use ( $time ) {
+                               $time = microtime( true ) - $time;
+                               $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
+                               $statsd->timing( "timing.login.ui.{$this->authAction}", $time * 1000 );
+                       } );
+               }
+
                $authManager = AuthManager::singleton();
                $session = SessionManager::getGlobalSession();
 
@@ -761,7 +772,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                if ( $this->showCreateAccountLink() ) {
                        # Pass any language selection on to the mode switch link
                        if ( $this->mLanguage ) {
-                               $linkq .= '&uselang=' . $this->mLanguage;
+                               $linkq .= '&uselang=' . urlencode( $this->mLanguage );
                        }
                        // Supply URL, login template creates the button.
                        $template->set( 'createOrLoginHref', $titleObj->getLocalURL( $linkq ) );
@@ -1149,7 +1160,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                $linkq = $this->getReturnToQueryStringFragment();
                                // Pass any language selection on to the mode switch link
                                if ( $this->mLanguage ) {
-                                       $linkq .= '&uselang=' . $this->mLanguage;
+                                       $linkq .= '&uselang=' . urlencode( $this->mLanguage );
                                }
                                $loggedIn = $this->getUser()->isLoggedIn();
 
index cd9345d..cf9ae07 100644 (file)
@@ -66,6 +66,7 @@ class BrokenRedirectsPage extends QueryPage {
                                'value' => 'p1.page_title',
                                'rd_namespace',
                                'rd_title',
+                               'rd_fragment',
                        ],
                        'conds' => [
                                // Exclude pages that don't exist locally as wiki pages,
@@ -102,7 +103,7 @@ class BrokenRedirectsPage extends QueryPage {
        function formatResult( $skin, $result ) {
                $fromObj = Title::makeTitle( $result->namespace, $result->title );
                if ( isset( $result->rd_title ) ) {
-                       $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title );
+                       $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title, $result->rd_fragment );
                } else {
                        $blinks = $fromObj->getBrokenLinksFrom(); # TODO: check for redirect, not for links
                        if ( $blinks ) {
@@ -139,7 +140,7 @@ class BrokenRedirectsPage extends QueryPage {
                                [ 'action' => 'edit' ]
                        );
                }
-               $to = $linkRenderer->makeBrokenLink( $toObj );
+               $to = $linkRenderer->makeBrokenLink( $toObj, $toObj->getFullText() );
                $arr = $this->getLanguage()->getArrow();
 
                $out = $from . $this->msg( 'word-separator' )->escaped();
index 3845649..1b14fcb 100644 (file)
@@ -131,17 +131,14 @@ class SpecialContributions extends IncludableSpecialPage {
 
                $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
                # Offset overrides year/month selection
-               if ( $skip ) {
-                       $this->opts['year'] = '';
-                       $this->opts['month'] = '';
-               } else {
+               if ( !$skip ) {
                        $this->opts['year'] = $request->getVal( 'year' );
                        $this->opts['month'] = $request->getVal( 'month' );
 
                        $this->opts['start'] = $request->getVal( 'start' );
                        $this->opts['end'] = $request->getVal( 'end' );
-                       $this->opts = ContribsPager::processDateFilter( $this->opts );
                }
+               $this->opts = ContribsPager::processDateFilter( $this->opts );
 
                $feedType = $request->getVal( 'feed' );
 
index d7e99db..ba14c66 100644 (file)
@@ -66,14 +66,15 @@ class DoubleRedirectsPage extends QueryPage {
                                'title' => 'pa.page_title',
                                'value' => 'pa.page_title',
 
-                               'nsb' => 'pb.page_namespace',
-                               'tb' => 'pb.page_title',
+                               'b_namespace' => 'pb.page_namespace',
+                               'b_title' => 'pb.page_title',
 
                                // Select fields from redirect instead of page. Because there may
                                // not actually be a page table row for this target (e.g. for interwiki redirects)
-                               'nsc' => 'rb.rd_namespace',
-                               'tc' => 'rb.rd_title',
-                               'iwc' => 'rb.rd_interwiki',
+                               'c_namespace' => 'rb.rd_namespace',
+                               'c_title' => 'rb.rd_title',
+                               'c_fragment' => 'rb.rd_fragment',
+                               'c_interwiki' => 'rb.rd_interwiki',
                        ],
                        'conds' => [
                                'ra.rd_from = pa.page_id',
@@ -116,14 +117,12 @@ class DoubleRedirectsPage extends QueryPage {
         * @return string
         */
        function formatResult( $skin, $result ) {
-               $titleA = Title::makeTitle( $result->namespace, $result->title );
-
-               // If only titleA is in the query, it means this came from
-               // querycache (which only saves 3 columns).
+               // If no Title B or C is in the query, it means this came from
+               // querycache (which only saves the 3 columns for title A).
                // That does save the bulk of the query cost, but now we need to
                // get a little more detail about each individual entry quickly
                // using the filter of reallyGetQueryInfo.
-               if ( $result && !isset( $result->nsb ) ) {
+               if ( $result && !isset( $result->b_namespace ) ) {
                        $dbr = wfGetDB( DB_REPLICA );
                        $qi = $this->reallyGetQueryInfo(
                                $result->namespace,
@@ -140,21 +139,14 @@ class DoubleRedirectsPage extends QueryPage {
                                $result = $dbr->fetchObject( $res );
                        }
                }
+
+               $titleA = Title::makeTitle( $result->namespace, $result->title );
+
                $linkRenderer = $this->getLinkRenderer();
                if ( !$result ) {
                        return '<del>' . $linkRenderer->makeLink( $titleA, null, [], [ 'redirect' => 'no' ] ) . '</del>';
                }
 
-               $titleB = Title::makeTitle( $result->nsb, $result->tb );
-               $titleC = Title::makeTitle( $result->nsc, $result->tc, '', $result->iwc );
-
-               $linkA = $linkRenderer->makeKnownLink(
-                       $titleA,
-                       null,
-                       [],
-                       [ 'redirect' => 'no' ]
-               );
-
                // if the page is editable, add an edit link
                if (
                        // check user permissions
@@ -172,6 +164,14 @@ class DoubleRedirectsPage extends QueryPage {
                        $edit = '';
                }
 
+               $linkA = $linkRenderer->makeKnownLink(
+                       $titleA,
+                       null,
+                       [],
+                       [ 'redirect' => 'no' ]
+               );
+
+               $titleB = Title::makeTitle( $result->b_namespace, $result->b_title );
                $linkB = $linkRenderer->makeKnownLink(
                        $titleB,
                        null,
@@ -179,7 +179,13 @@ class DoubleRedirectsPage extends QueryPage {
                        [ 'redirect' => 'no' ]
                );
 
-               $linkC = $linkRenderer->makeKnownLink( $titleC );
+               $titleC = Title::makeTitle(
+                       $result->c_namespace,
+                       $result->c_title,
+                       $result->c_fragment,
+                       $result->c_interwiki
+               );
+               $linkC = $linkRenderer->makeKnownLink( $titleC, $titleC->getFullText() );
 
                $lang = $this->getLanguage();
                $arr = $lang->getArrow() . $lang->getDirMark();
@@ -201,13 +207,13 @@ class DoubleRedirectsPage extends QueryPage {
                $batch = new LinkBatch;
                foreach ( $res as $row ) {
                        $batch->add( $row->namespace, $row->title );
-                       if ( isset( $row->nsb ) ) {
+                       if ( isset( $row->b_namespace ) ) {
                                // lazy loaded when using cached results
-                               $batch->add( $row->nsb, $row->tb );
+                               $batch->add( $row->b_namespace, $row->b_title );
                        }
-                       if ( isset( $row->iwc ) && !$row->iwc ) {
+                       if ( isset( $row->c_interwiki ) && !$row->c_interwiki ) {
                                // lazy loaded when using cached result, not added when interwiki link
-                               $batch->add( $row->nsc, $row->tc );
+                               $batch->add( $row->c_namespace, $row->c_title );
                        }
                }
                $batch->execute();
index a827e89..beb454d 100644 (file)
@@ -173,10 +173,8 @@ class SpecialImport extends SpecialPage {
 
                $out = $this->getOutput();
                if ( !$source->isGood() ) {
-                       $out->wrapWikiMsg(
-                               "<p class=\"error\">\n$1\n</p>",
-                               [ 'importfailed', $source->getWikiText() ]
-                       );
+                       $out->addWikiText( "<p class=\"error\">\n" .
+                               $this->msg( 'importfailed', $source->getWikiText() )->parse() . "\n</p>" );
                } else {
                        $importer = new WikiImporter( $source->value, $this->getConfig() );
                        if ( !is_null( $this->namespace ) ) {
index 2c92410..1a04eec 100644 (file)
@@ -69,7 +69,7 @@ class SpecialListGrants extends SpecialPage {
                                $grantCellHtml = '<ul><li>' . implode( "</li>\n<li>", $descs ) . '</li></ul>';
                        }
 
-                       $id = \Sanitizer::escapeId( $grant );
+                       $id = Sanitizer::escapeIdForAttribute( $grant );
                        $out->addHTML( \Html::rawElement( 'tr', [ 'id' => $id ],
                                "<td>" .
                                $this->msg(
index 7a25e55..2315887 100644 (file)
@@ -126,7 +126,7 @@ class SpecialListGroupRights extends SpecialPage {
                                ? $groupsRemoveFromSelf[$group]
                                : [];
 
-                       $id = $group == '*' ? false : Sanitizer::escapeId( $group );
+                       $id = $group == '*' ? false : Sanitizer::escapeIdForAttribute( $group );
                        $out->addHTML( Html::rawElement( 'tr', [ 'id' => $id ], "
                                <td>$grouppage$grouplink</td>
                                        <td>" .
index 5f38629..f81c03c 100644 (file)
@@ -137,7 +137,7 @@ class ListredirectsPage extends QueryPage {
                        # Make a link to the destination page
                        $lang = $this->getLanguage();
                        $arr = $lang->getArrow() . $lang->getDirMark();
-                       $targetLink = $linkRenderer->makeLink( $target );
+                       $targetLink = $linkRenderer->makeLink( $target, $target->getFullText() );
 
                        return "$rd_link $arr $targetLink";
                } else {
index 5878e1f..34fcc78 100644 (file)
  * @since 1.21
  */
 class SpecialPagesWithProp extends QueryPage {
+
+       /**
+        * @var string|null
+        */
        private $propName = null;
+
+       /**
+        * @var string[]|null
+        */
        private $existingPropNames = null;
 
+       /**
+        * @var bool
+        */
+       private $reverse = false;
+
+       /**
+        * @var bool
+        */
+       private $sortByValue = false;
+
        function __construct( $name = 'PagesWithProp' ) {
                parent::__construct( $name );
        }
@@ -46,6 +64,8 @@ class SpecialPagesWithProp extends QueryPage {
 
                $request = $this->getRequest();
                $propname = $request->getVal( 'propname', $par );
+               $this->reverse = $request->getBool( 'reverse' );
+               $this->sortByValue = $request->getBool( 'sortbyvalue' );
 
                $propnames = $this->getExistingPropNames();
 
@@ -58,6 +78,20 @@ class SpecialPagesWithProp extends QueryPage {
                                'label-message' => 'pageswithprop-prop',
                                'required' => true,
                        ],
+                       'reverse' => [
+                               'type' => 'check',
+                               'name' => 'reverse',
+                               'default' => $this->reverse,
+                               'label-message' => 'pageswithprop-reverse',
+                               'required' => false,
+                       ],
+                       'sortbyvalue' => [
+                               'type' => 'check',
+                               'name' => 'sortbyvalue',
+                               'default' => $this->sortByValue,
+                               'label-message' => 'pageswithprop-sortbyvalue',
+                               'required' => false,
+                       ]
                ], $this->getContext() );
                $form->setMethod( 'get' );
                $form->setSubmitCallback( [ $this, 'onSubmit' ] );
@@ -122,7 +156,18 @@ class SpecialPagesWithProp extends QueryPage {
        }
 
        function getOrderFields() {
-               return [ 'page_id' ];
+               $sort = [ 'page_id' ];
+               if ( $this->sortByValue ) {
+                       array_unshift( $sort, 'pp_sortkey' );
+               }
+               return $sort;
+       }
+
+       /**
+        * @return bool
+        */
+       public function sortDescending() {
+               return !$this->reverse;
        }
 
        /**
index c9c2475..6ef75e0 100644 (file)
@@ -181,10 +181,6 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                                $this->getConfig()->get( 'StructuredChangeFiltersEnableExperimentalViews' );
 
                        $out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] );
-                       $out->addJsConfigVars(
-                               'wgStructuredChangeFiltersEnableSaving',
-                               $this->getConfig()->get( 'StructuredChangeFiltersEnableSaving' )
-                       );
                        $out->addJsConfigVars(
                                'wgStructuredChangeFiltersEnableExperimentalViews',
                                $experimentalStructuredChangeFilters
@@ -193,12 +189,18 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                                'wgStructuredChangeFiltersEnableLiveUpdate',
                                $this->getConfig()->get( 'StructuredChangeFiltersEnableLiveUpdate' )
                        );
-                       if ( $experimentalStructuredChangeFilters ) {
-                               $out->addJsConfigVars(
-                                       'wgRCFiltersChangeTags',
-                                       $this->buildChangeTagList()
-                               );
-                       }
+                       $out->addJsConfigVars(
+                               'wgRCFiltersChangeTags',
+                               $this->buildChangeTagList()
+                       );
+                       $out->addJsConfigVars(
+                               'StructuredChangeFiltersDisplayConfig',
+                               [
+                                       'maxDays' => (int)$this->getConfig()->get( 'RCMaxAge' ) / ( 24 * 3600 ), // Translate to days
+                                       'limitArray' => $this->getConfig()->get( 'RCLinkLimits' ),
+                                       'daysArray' => $this->getConfig()->get( 'RCLinkDays' ),
+                               ]
+                       );
                }
        }
 
@@ -315,7 +317,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                $opts = parent::getDefaultOptions();
                $user = $this->getUser();
 
-               $opts->add( 'days', $user->getIntOption( 'rcdays' ) );
+               $opts->add( 'days', $user->getIntOption( 'rcdays' ), FormOptions::FLOAT );
                $opts->add( 'limit', $user->getIntOption( 'rclimit' ) );
                $opts->add( 'from', '' );
 
@@ -359,7 +361,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
                                $opts['limit'] = $m[1];
                        }
-                       if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
+                       if ( preg_match( '/^days=(\d+(?:\.\d+)?)$/', $bit, $m ) ) {
                                $opts['days'] = $m[1];
                        }
                        if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
@@ -373,6 +375,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
 
        public function validateOptions( FormOptions $opts ) {
                $opts->validateIntBounds( 'limit', 0, 5000 );
+               $opts->validateBounds( 'days', 0, $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
                parent::validateOptions( $opts );
        }
 
@@ -387,8 +390,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        $query_options, $join_conds, $opts );
 
                // Calculate cutoff
-               $cutoff_unixtime = time() - ( $opts['days'] * 86400 );
-               $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
+               $cutoff_unixtime = time() - $opts['days'] * 3600 * 24;
                $cutoff = $dbr->timestamp( $cutoff_unixtime );
 
                $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] );
@@ -430,13 +432,14 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                $fields[] = 'page_latest';
                $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
 
+               $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
                ChangeTags::modifyDisplayQuery(
                        $tables,
                        $fields,
                        $conds,
                        $join_conds,
                        $query_options,
-                       $opts['tagfilter']
+                       $tagFilter
                );
 
                if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
@@ -449,13 +452,24 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        return false;
                }
 
+               $orderByAndLimit = [
+                       'ORDER BY' => 'rc_timestamp DESC',
+                       'LIMIT' => $opts['limit']
+               ];
+               if ( in_array( 'DISTINCT', $query_options ) ) {
+                       // ChangeTags::modifyDisplayQuery() adds DISTINCT when filtering on multiple tags.
+                       // In order to prevent DISTINCT from causing query performance problems,
+                       // we have to GROUP BY the primary key. This in turn requires us to add
+                       // the primary key to the end of the ORDER BY, and the old ORDER BY to the
+                       // start of the GROUP BY
+                       $orderByAndLimit['ORDER BY'] = 'rc_timestamp DESC, rc_id DESC';
+                       $orderByAndLimit['GROUP BY'] = 'rc_timestamp, rc_id';
+               }
                // array_merge() is used intentionally here so that hooks can, should
                // they so desire, override the ORDER BY / LIMIT condition(s); prior to
                // MediaWiki 1.26 this used to use the plus operator instead, which meant
                // that extensions weren't able to change these conditions
-               $query_options = array_merge( [
-                       'ORDER BY' => 'rc_timestamp DESC',
-                       'LIMIT' => $opts['limit'] ], $query_options );
+               $query_options = array_merge( $orderByAndLimit, $query_options );
                $rows = $dbr->select(
                        $tables,
                        $fields,
@@ -706,17 +720,35 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
 
                $message = $this->msg( 'recentchangestext' )->inContentLanguage();
                if ( !$message->isDisabled() ) {
-                       $this->getOutput()->addWikiText(
-                               Html::rawElement( 'div',
-                                       [
-                                               'class' => 'mw-recentchanges-toplinks',
-                                               'lang' => $wgContLang->getHtmlCode(),
-                                               'dir' => $wgContLang->getDir()
-                                       ],
-                                       "\n" . $message->plain() . "\n"
-                               ),
-                               /* $lineStart */ true,
-                               /* $interface */ false
+                       $content = $message->parse();
+
+                       $langAttributes = [
+                               'lang' => $wgContLang->getHtmlCode(),
+                               'dir' => $wgContLang->getDir(),
+                       ];
+
+                       $topLinksAttributes = [ 'class' => 'mw-recentchanges-toplinks' ];
+
+                       if ( $this->getUser()->getOption( 'rcenhancedfilters' ) ) {
+                               $contentTitle = Html::rawElement( 'div',
+                                       [ 'class' => 'mw-recentchanges-toplinks-title' ],
+                                       $this->msg( 'rcfilters-other-review-tools' )->parse()
+                               );
+                               $contentWrapper = Html::rawElement( 'div',
+                                       array_merge( [ 'class' => 'mw-collapsible-content' ], $langAttributes ),
+                                       $content
+                               );
+                               $content = $contentTitle . $contentWrapper;
+                       } else {
+                               // Language direction should be on the top div only
+                               // if the title is not there. If it is there, it's
+                               // interface direction, and the language/dir attributes
+                               // should be on the content itself
+                               $topLinksAttributes = array_merge( $topLinksAttributes, $langAttributes );
+                       }
+
+                       $this->getOutput()->addHTML(
+                               Html::rawElement( 'div', $topLinksAttributes, $content )
                        );
                }
        }
@@ -943,15 +975,20 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        $resetLink = $this->makeOptionsLink( $this->msg( 'rclistfromreset' ),
                                [ 'from' => '' ], $nondefaults );
 
-                       $note .= $this->msg( 'rcnotefrom' )
+                       $noteFromMsg = $this->msg( 'rcnotefrom' )
                                ->numParams( $options['limit'] )
                                ->params(
                                        $lang->userTimeAndDate( $options['from'], $user ),
                                        $lang->userDate( $options['from'], $user ),
                                        $lang->userTime( $options['from'], $user )
                                )
-                               ->numParams( $numRows )
-                               ->parse() . ' ' .
+                               ->numParams( $numRows );
+                       $note .= Html::rawElement(
+                                       'span',
+                                       [ 'class' => 'rcnotefrom' ],
+                                       $noteFromMsg->parse()
+                               ) .
+                               ' ' .
                                Html::rawElement(
                                        'span',
                                        [ 'class' => 'rcoptions-listfromreset' ],
index b3b9210..fee336e 100644 (file)
@@ -103,15 +103,33 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
                        $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
                        $select[] = 'page_latest';
                }
+
+               $tagFilter = $opts['tagfilter'] ? explode( '|', $opts['tagfilter'] ) : [];
                ChangeTags::modifyDisplayQuery(
                        $tables,
                        $select,
                        $conds,
                        $join_conds,
                        $query_options,
-                       $opts['tagfilter']
+                       $tagFilter
                );
 
+               if ( $dbr->unionSupportsOrderAndLimit() ) {
+                       if ( count( $tagFilter ) > 1 ) {
+                               // ChangeTags::modifyDisplayQuery() will have added DISTINCT.
+                               // To prevent this from causing query performance problems, we need to add
+                               // a GROUP BY, and add rc_id to the ORDER BY.
+                               $order = [
+                                       'GROUP BY' => 'rc_timestamp, rc_id',
+                                       'ORDER BY' => 'rc_timestamp DESC, rc_id DESC'
+                               ];
+                       } else {
+                               $order = [ 'ORDER BY' => 'rc_timestamp DESC' ];
+                       }
+               } else {
+                       $order = [];
+               }
+
                if ( !$this->runMainQueryHook( $tables, $select, $conds, $query_options, $join_conds,
                        $opts )
                ) {
@@ -181,12 +199,6 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
                                }
                        }
 
-                       if ( $dbr->unionSupportsOrderAndLimit() ) {
-                               $order = [ 'ORDER BY' => 'rc_timestamp DESC' ];
-                       } else {
-                               $order = [];
-                       }
-
                        $query = $dbr->selectSQLText(
                                array_merge( $tables, [ $link_table ] ),
                                $select,
index eeb8823..8afea0b 100644 (file)
@@ -352,7 +352,7 @@ class SpecialSearch extends SpecialPage {
                        $out->addHTML( $dymWidget->render( $term, $textMatches ) );
                }
 
-               $hasErrors = $textStatus && $textStatus->getErrors();
+               $hasErrors = $textStatus && $textStatus->getErrors() !== [];
                $hasOtherResults = $textMatches &&
                        $textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS );
 
index f980e71..e9c15e7 100644 (file)
@@ -41,9 +41,11 @@ class ShortPagesPage extends QueryPage {
        }
 
        public function getQueryInfo() {
+               $config = $this->getConfig();
+               $blacklist = $config->get( 'ShortPagesNamespaceBlacklist' );
                $tables = [ 'page' ];
                $conds = [
-                       'page_namespace' => MWNamespace::getContentNamespaces(),
+                       'page_namespace' => array_diff( MWNamespace::getContentNamespaces(), $blacklist ),
                        'page_is_redirect' => 0
                ];
                $joinConds = [];
index 77b6926..5ff9e04 100644 (file)
@@ -68,7 +68,7 @@ class UncategorizedCategoriesPage extends UncategorizedPagesPage {
        }
 
        public function getQueryInfo() {
-               $dbr = wfGetDB( DB_SLAVE );
+               $dbr = wfGetDB( DB_REPLICA );
                $query = parent::getQueryInfo();
                $exceptionList = $this->getExceptionList();
                if ( $exceptionList ) {
index 810f8fb..740207d 100644 (file)
@@ -235,7 +235,7 @@ class SpecialUndelete extends SpecialPage {
        function showSearchForm() {
                $out = $this->getOutput();
                $out->setPageTitle( $this->msg( 'undelete-search-title' ) );
-               $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', false );
+               $fuzzySearch = $this->getRequest()->getVal( 'fuzzy', true );
 
                $out->enableOOUI();
 
@@ -277,7 +277,7 @@ class SpecialUndelete extends SpecialPage {
                        $fieldset,
                        new OOUI\HtmlSnippet(
                                Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
-                               Html::hidden( 'fuzzy', $this->getRequest()->getVal( 'fuzzy' ) )
+                               Html::hidden( 'fuzzy', $fuzzySearch )
                        )
                );
 
@@ -669,13 +669,7 @@ class SpecialUndelete extends SpecialPage {
 
                $archive = new PageArchive( $this->mTargetObj, $this->getConfig() );
                Hooks::run( 'UndeleteForm::showHistory', [ &$archive, $this->mTargetObj ] );
-               /*
-               $text = $archive->getLastRevisionText();
-               if( is_null( $text ) ) {
-                       $out->addWikiMsg( 'nohistory' );
-                       return;
-               }
-               */
+
                $out->addHTML( '<div class="mw-undelete-history">' );
                if ( $this->mAllowed ) {
                        $out->addWikiMsg( 'undeletehistory' );
@@ -858,11 +852,12 @@ class SpecialUndelete extends SpecialPage {
                        $misc = Html::hidden( 'target', $this->mTarget );
                        $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
                        $history .= $misc;
-               }
-
-               $form->appendContent( new OOUI\HtmlSnippet( $history ) );
 
-               $out->addHTML( $form );
+                       $form->appendContent( new OOUI\HtmlSnippet( $history ) );
+                       $out->addHTML( $form );
+               } else {
+                       $out->addHTML( $history );
+               }
 
                return true;
        }
index 30c4a0b..3ea1d03 100644 (file)
@@ -840,7 +840,7 @@ class SpecialVersion extends SpecialPage {
                // Finally! Create the table
                $html = Html::openElement( 'tr', [
                                'class' => 'mw-version-ext',
-                               'id' => Sanitizer::escapeId( 'mw-version-ext-' . $type . '-' . $extension['name'] )
+                               'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
                        ]
                );
 
index 65131ec..9e01d2d 100644 (file)
@@ -34,6 +34,8 @@ use Wikimedia\Rdbms\IDatabase;
 class SpecialWatchlist extends ChangesListSpecialPage {
        public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) {
                parent::__construct( $page, $restriction );
+
+               $this->maxDays = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
        }
 
        public function doesWrites() {
@@ -173,6 +175,11 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                return $opts;
        }
 
+       public function validateOptions( FormOptions $opts ) {
+               $opts->validateBounds( 'days', 0, $this->maxDays );
+               parent::validateOptions( $opts );
+       }
+
        /**
         * Get all custom filters
         *
@@ -255,7 +262,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                // Calculate cutoff
                if ( $opts['days'] > 0 ) {
                        $conds[] = 'rc_timestamp > ' .
-                               $dbr->addQuotes( $dbr->timestamp( time() - intval( $opts['days'] * 86400 ) ) );
+                               $dbr->addQuotes( $dbr->timestamp( time() - $opts['days'] * 3600 * 24 ) );
                }
        }
 
@@ -270,7 +277,6 @@ class SpecialWatchlist extends ChangesListSpecialPage {
 
                # Toggle watchlist content (all recent edits or just the latest)
                if ( $opts['extended'] ) {
-                       $limitWatchlist = $user->getIntOption( 'wllimit' );
                        $usePage = false;
                } else {
                        # Top log Ids for a page are not stored
@@ -285,14 +291,16 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                        LIST_OR
                                );
                        }
-                       $limitWatchlist = 0;
                        $usePage = true;
                }
 
                $tables = array_merge( [ 'recentchanges', 'watchlist' ], $tables );
                $fields = array_merge( RecentChange::selectFields(), $fields );
 
-               $query_options = array_merge( [ 'ORDER BY' => 'rc_timestamp DESC' ], $query_options );
+               $query_options = array_merge( [
+                       'ORDER BY' => 'rc_timestamp DESC',
+                       'LIMIT' => $user->getIntOption( 'wllimit' )
+               ], $query_options );
                $join_conds = array_merge(
                        [
                                'watchlist' => [
@@ -310,9 +318,6 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
                        $fields[] = 'wl_notificationtimestamp';
                }
-               if ( $limitWatchlist ) {
-                       $query_options['LIMIT'] = $limitWatchlist;
-               }
 
                $rollbacker = $user->isAllowed( 'rollback' );
                if ( $usePage || $rollbacker ) {
@@ -499,7 +504,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                if ( $opts['days'] > 0 ) {
                        $days = $opts['days'];
                } else {
-                       $days = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
+                       $days = $this->maxDays;
                }
                $timestamp = wfTimestampNow();
                $wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $days * 24 ) )->params(
@@ -599,7 +604,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                        $days[] = $userWatchlistOption;
                }
 
-               $maxDays = (string)( $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
+               $maxDays = (string)$this->maxDays;
                // add the maximum possible value, if it isn't available already
                if ( !in_array( $maxDays, $days ) ) {
                        $days[] = $maxDays;
index ca1b7dc..e6a0f0b 100644 (file)
@@ -375,7 +375,9 @@ class AllMessagesTablePager extends TablePager {
                }
 
                if ( !$isSecond ) {
-                       $arr['id'] = Sanitizer::escapeId( 'msg_' . $this->getLanguage()->lcfirst( $row->am_title ) );
+                       $arr['id'] = Sanitizer::escapeIdForAttribute(
+                               'msg_' . $this->getLanguage()->lcfirst( $row->am_title )
+                       );
                }
 
                return $arr;
index 6bd7eb0..d7819c4 100644 (file)
@@ -583,10 +583,10 @@ class ContribsPager extends RangeChronologicalPager {
         * @return array Options array with processed start and end date filter options
         */
        public static function processDateFilter( $opts ) {
-               $start = $opts['start'] ?: '';
-               $end = $opts['end'] ?: '';
-               $year = $opts['year'] ?: '';
-               $month = $opts['month'] ?: '';
+               $start = isset( $opts['start'] ) ? $opts['start'] : '';
+               $end = isset( $opts['end'] ) ? $opts['end'] : '';
+               $year = isset( $opts['year'] ) ? $opts['year'] : '';
+               $month = isset( $opts['month'] ) ? $opts['month'] : '';
 
                if ( $start !== '' && $end !== '' && $start > $end ) {
                        $temp = $start;
index 10baadf..cdb9130 100644 (file)
@@ -280,12 +280,12 @@ class UsersPager extends AlphabeticPager {
                                'class' => 'HTMLUserTextField',
                                'label' => $this->msg( 'listusersfrom' )->text(),
                                'name' => 'username',
-                               'value' => $this->requestedUser,
+                               'default' => $this->requestedUser,
                        ],
                        'dropdown' => [
-                               'label' => $this->msg( 'group' ),
+                               'label' => $this->msg( 'group' )->text(),
                                'name' => 'group',
-                               'value' => $this->requestedGroup,
+                               'default' => $this->requestedGroup,
                                'class' => 'HTMLSelectField',
                                'options' => $groupOptions,
                        ],
@@ -294,26 +294,26 @@ class UsersPager extends AlphabeticPager {
                                'label' => $this->msg( 'listusers-editsonly' )->text(),
                                'name' => 'editsOnly',
                                'id' => 'editsOnly',
-                               'value' => $this->editsOnly
+                               'default' => $this->editsOnly
                        ],
                        'creationSort' => [
                                'type' => 'check',
                                'label' => $this->msg( 'listusers-creationsort' )->text(),
                                'name' => 'creationSort',
                                'id' => 'creationSort',
-                               'value' => $this->creationSort
+                               'default' => $this->creationSort
                        ],
                        'desc' => [
                                'type' => 'check',
                                'label' => $this->msg( 'listusers-desc' )->text(),
                                'name' => 'desc',
                                'id' => 'desc',
-                               'value' => $this->mDefaultDirection
+                               'default' => $this->mDefaultDirection
                        ],
                        'limithiddenfield' => [
                                'class' => 'HTMLHiddenField',
                                'name' => 'limit',
-                               'value' => $this->mLimit
+                               'default' => $this->mLimit
                        ]
                ];
 
@@ -347,6 +347,7 @@ class UsersPager extends AlphabeticPager {
                $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
                $htmlForm
                        ->setMethod( 'get' )
+                       ->setAction( Title::newFromText( $self )->getLocalURL() )
                        ->setId( 'mw-listusers-form' )
                        ->setFormIdentifier( 'mw-listusers-form' )
                        ->suppressDefaultSubmit()
index 07094af..bf59fe9 100644 (file)
@@ -79,9 +79,9 @@ class BasicSearchResultSetWidget {
                                        continue;
                                }
                                $out .=
-                                       "<p class='mw-search-interwiki-header mw-search-visualclear'>" .
+                                       "<h2 class='mw-search-interwiki-header mw-search-visualclear'>" .
                                                $this->specialPage->msg( "search-interwiki-results-{$interwiki}" )->parse() .
-                                       "</p>";
+                                       "</h2>";
                                $out .= $this->renderResultSet( $results, $offset );
                        }
                }
index f7771d1..7e59432 100644 (file)
@@ -16,6 +16,7 @@
                "resources/src/mediawiki.action",
                "resources/src/mediawiki.language",
                "resources/src/mediawiki.messagePoster",
+               "resources/src/mediawiki.rcfilters",
                "resources/src/mediawiki.special",
                "resources/src/mediawiki.toolbar",
                "resources/src/mediawiki.widgets",
                "resources/src/jquery/jquery.suggestions.js",
                "resources/src/jquery/jquery.tabIndex.js",
                "resources/lib/jquery.client/jquery.client.js",
-               "resources/lib/oojs",
-               "resources/lib/oojs-ui"
+               "resources/lib/oojs/oojs.jquery.js",
+               "resources/lib/oojs-ui/oojs-ui-core.js",
+               "resources/lib/oojs-ui/oojs-ui-widgets.js",
+               "resources/lib/oojs-ui/oojs-ui-toolbars.js",
+               "resources/lib/oojs-ui/oojs-ui-windows.js",
+               "resources/lib/oojs-ui/oojs-ui-wikimediaui.js",
+               "resources/lib/oojs-ui/oojs-ui-apex.js"
        ]
 }
index 12f26c3..92dad9b 100644 (file)
@@ -1092,7 +1092,7 @@ class Language {
         *      YYYYMMDDHHMMSS
         *      01234567890123
         * @param DateTimeZone $zone Timezone of $ts
-        * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
+        * @param int &$ttl The amount of time (in seconds) the output may be cached for.
         * Only makes sense if $ts is the current time.
         * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
         *
@@ -4537,7 +4537,7 @@ class Language {
        public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
                static $dbInfinity;
                if ( $dbInfinity === null ) {
-                       $dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
+                       $dbInfinity = wfGetDB( DB_REPLICA )->getInfinity();
                }
 
                if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
index 00d91ce..19ff2a4 100644 (file)
@@ -383,6 +383,8 @@ class Names {
                'si' => 'සිංහල', # Sinhalese
                'simple' => 'Simple English', # Simple English
                'sk' => 'slovenčina', # Slovak
+               'skr' => 'سرائیکی', # Saraiki (multiple scripts - defaults to Arabic)
+               'skr-arab' => 'سرائیکی', # Saraiki (Arabic script)
                'sl' => 'slovenščina', # Slovenian
                'sli' => 'Schläsch', # Lower Selisian
                'sm' => 'Gagana Samoa', # Samoan
index 97661f6..0affa8c 100644 (file)
        "actionthrottled": "Outo-rem op aksie uitgevoer",
        "actionthrottledtext": "As 'n teen-strooi aksie, word u beperk om hierdie aksie te veel keer in 'n kort tyd uit te voer, en u het hierdie limiet oorskry.\nProbeer asseblief weer oor 'n paar minute.",
        "protectedpagetext": "Hierdie bladsy is beskerm om wysigings en ander aksies te verhoed.",
-       "viewsourcetext": "U mag die bronteks van hierdie bladsy lees en kopieer:",
+       "viewsourcetext": "U mag die bronteks van hierdie bladsy lees en kopieer.",
        "viewyourtext": "U kan '''u wysigings''' aan die bronteks van hierdie bladsy bekyk en kopieer:",
        "protectedinterface": "Hierdie bladsy verskaf teks vir die koppelvlak van die sagteware, en is beskerm om misbruik te voorkom.\nGebruik asseblief [https://translatewiki.net/ translatewiki.net] om vertalings by te voeg of te wysig.",
        "editinginterface": "'''Waarskuwing:''' U is besig om 'n bladsy te redigeer wat koppelvlakinligting aan die programmatuur voorsien. Wysigings aan hierdie bladsy sal die voorkoms van die gebruikerskoppelvlak vir ander gebruikers beïnvloed. Vir vertalings, oorweeg om eerder [https://translatewiki.net/wiki/Main_Page?setlang=af translatewiki.net] (die vertalingsprojek vir MediaWiki) te gebruik.",
        "page_first": "eerste",
        "page_last": "laaste",
        "histlegend": "Byskrif: (huidige) = verskil van huidige weergawe,\n(vorige) = verskil van vorige weergawe, M = klein wysiging",
-       "history-fieldset-title": "Blaai deur geskiedenis",
+       "history-fieldset-title": "Soek vir wysigings",
        "history-show-deleted": "Slegs geskrapte",
        "histfirst": "oudste",
        "histlast": "nuutste",
        "search-category": "(kategorie $1)",
        "search-file-match": "(stem ooreen met die inhoud van die leêr)",
        "search-suggest": "Het u $1 bedoel?",
-       "search-interwiki-caption": "Susterprojekte",
+       "search-interwiki-caption": "Resultate vanaf susterprojekte",
        "search-interwiki-default": "Resultate van $1:",
        "search-interwiki-more": "(meer)",
+       "search-interwiki-more-results": "meer resultate",
        "search-relatedarticle": "Verwante",
        "searchrelated": "verwante",
        "searchall": "alle",
        "recentchanges-legend-heading": "<strong>Sleutel:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (sien ook die [[Special:NewPages|lys van nuwe bladsye]])",
        "recentchanges-submit": "Wys",
-