From: jenkins-bot Date: Thu, 28 Mar 2019 22:45:54 +0000 (+0000) Subject: Merge "Recalculate user default options for each test" X-Git-Tag: 1.34.0-rc.0~2264 X-Git-Url: http://git.heureux-cyclage.org/?a=commitdiff_plain;h=ee09d4f0ee2e592e721f00805735afcb9e3e7e22;hp=fe2cb4efc64bad506294adeea516efe70368a9b1;p=lhc%2Fweb%2Fwiklou.git Merge "Recalculate user default options for each test" --- diff --git a/.phpcs.xml b/.phpcs.xml index d1e54a706c..2436fa7be8 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -215,7 +215,6 @@ */includes/Feed\.php */includes/filerepo/file/LocalFile\.php */includes/gallery/PackedOverlayImageGallery\.php - */includes/HistoryBlob\.php */includes/htmlform/HTMLFormElement\.php */includes/libs/filebackend/FileBackendStore\.php */includes/libs/filebackend/FSFileBackend\.php @@ -228,7 +227,6 @@ */includes/parser/Preprocessor_Hash\.php */includes/parser/Preprocessor\.php */includes/PathRouter\.php - */includes/PrefixSearch\.php */includes/profiler/SectionProfiler\.php */includes/search/SearchEngine\.php */includes/specialpage/LoginSignupSpecialPage\.php diff --git a/HISTORY b/HISTORY index 7895316cd2..36e398ecd2 100644 --- a/HISTORY +++ b/HISTORY @@ -19159,3 +19159,502 @@ going to run a public MediaWiki, so you can be notified of security fixes. === IRC help === There's usually someone online in #mediawiki on irc.freenode.net + +=MediaWiki 1.3= + +== MediaWiki 1.3.18 == +(released 2005-11-02) +MediaWiki 1.3.18 is a bugfix and security maintenance release. A change in PHP +4.4.1 broke handling of extension and
 sections, causing
+garbage data to be inserted in output and saved edits. This version works
+around the change. This release includes further corrections to the inline CSS
+style sanitation which works around a JavaScript "feature" on Microsoft
+Internet Explorer. Users of Microsoft Internet Explorer for Windows may be
+vulnerable to XSS injections on prior 1.3 releases; users of
+standards-compliant browsers are not vulnerable.
+
+== MediaWiki 1.3.17 ==
+(released 2005-10-05)
+MediaWiki 1.3.17 is a security maintenance release. Unsafe handling of CSS by
+Microsoft Internet Explorer could be exploited to produce cross-site scripting
+attacks by JavaScript injection to clients running that browser. This release
+blacklists several additional variants from use in HTML inline style
+attributes. All publicly accessible wikis are recommended to upgrade to reduce
+the risk to visitors using Microsoft web browsers.Note: the MediaWiki 1.3.x
+series is not compatible with PHP 5.0.5 or higher. Upgrade to the 1.5.0 release
+if you require this version of PHP 5.
+
+== MediaWiki 1.3.16 ==
+(released 2005-09-21)
+MediaWiki 1.3.16 is a security maintenance release. A bug in edit submission
+handling could cause corruption of the previous revision in the database if an
+abnormal URL was used, such as those used by some spambots. Affected releases:
+* 1.4.x <= 1.4.9; fixed in 1.4.10
+* 1.3.x <= 1.3.15; fixed in 1.3.16
+1.5 release candidates are not affected by this problem. All publicly editable
+wikis are strongly recommended to upgrade immediately.
+1.3 releases can be manually patched by changing this bit in
+{{manual|EditPage.php}}:
+
+    if( $this->tokenOk( $request ) ) {
+        $this->save    = $request->wasPosted() && !$this->preview;
+    } else {
+
+to:
+
+    if( $this->tokenOk( $request ) ) {
+        $this->save    = $request->getVal( 'action' ) == 'submit' &&
+                         $request->wasPosted() && !$this->preview;
+    } else {
+
+
+== MediaWiki 1.3.15, 2005-08-29 ==
+MediaWiki 1.3.15 is a security maintenance release. It corrects across-site
+scripting security bug:
+*  tags were handled incorrectly when TeX rendering
+support is off, as in the default configuration. Wikis where the optional math
+support has been *enabled* are not vulnerable. The 1.3.x series is no longer
+maintained except for security fixes; new users and those seeking bug fixes
+should upgrade to 1.4.9 or 1.5.0.
+
+== MediaWiki 1.3.14, 2005-08-23 ==
+MediaWiki 1.3.14 is a security maintenance release. A flaw in the interaction
+between extensions and HTML attribute sanitization was discovered which could
+allow unauthorized use of offsite resources in style sheets, and possible
+exploitation of a JavaScript injection feature on Microsoft Internet Explorer.
+The 1.3.x series is no longer maintained except for security fixes; new users
+and those seeking bug fixes should upgrade to 1.4.8 or 1.5.0. Existing 1.3.x
+installations not willing to upgrade to the current stable release should apply
+the change manually:
+In includes/Parser.php, function {{code|inline=y|lang=php|fixTagAttributes()}}
+add:
+
+       # Any placeholder items should have been unstripped already before
+       # we got to this point. Raw text inserted later could be dangerous.
+       if( strpos( $t, UNIQ_PREFIX ) !== false ) {
+           wfDebug( "Parser::fixTagAttributes found stripped data placeholder;
+           dropping attributes\n" );
+           $t = '';
+       }
+
+If you are actively using extensions to generate HTML attribute values, upgrade
+to 1.4 or 1.5 for a more thorough fix.
+
+== MediaWiki 1.3.13, 2005-06-03 ==
+MediaWiki 1.3.13 is a security maintenance release. Incorrect handling of page
+template inclusions made it possible to inject JavaScript code into HTML
+attributes, which could lead to cross-site scripting attacks on a publicly
+editable wiki. Vulnerable releases and fix:
+* 1.5 prerelease: fixed in 1.5alpha2
+* 1.4 stable series: fixed in 1.4.5
+* 1.3 legacy series: fixed in 1.3.13
+* 1.2 series no longer supported; upgrade to 1.4.5 strongly recommended The
+1.3.x series is no longer maintained except for security fixes; new users and
+those seeking general bug fixes should install 1.4.5. Existing 1.3.x
+installations not willing or able to upgrade to the current stable relase
+should update the installation to 1.3.13; only includes/Parser.php has changed
+from 1.3.12.
+
+== MediaWiki 1.3.12, 2005-02-20 ==
+MediaWiki 1.3.12 is a security maintenance release. A cross-site scripting
+injection vulnerability was discovered, which affects only MSIE clients and is
+only open if MediaWiki has been manually configured to run output through HTML
+Tidy ($wgUseTidy). The 1.3.x series is no longer maintained except for security
+fixes; new users and those seeking bug fixes should upgrade to 1.4.2. Existing
+1.3.x installations using Tidy not willing to upgrade to the current stable
+relase should either turn off Tidy or update the installation to 1.3.12.
+
+== MediaWiki 1.3.11, 2005-02-20 ==
+MediaWiki 1.3.11 is a security release.
+A security audit found and fixed a number of problems. Users of MediaWiki
+1.3.10 and earlier should upgrade to 1.3.11; users of 1.4 beta releases should
+upgrade to 1.4rc1.
+
+=== Cross-site scripting vulnerability ===
+XSS injection points can be used to hijack session and authentication cookies
+as well as more serious attacks.
+* Media: links output raw text into an attribute value, potentially abusable
+for JavaScript injection. This has been corrected.
+* Additional checks added to file upload to protect against MSIE and Safari
+MIME-type autodetection bugs.
+As of 1.3.10/1.4beta6, per-user customized CSS and JavaScript is
+disabled by default as a general precaution. Sites which want this ability may
+set {{wg|AllowUserCss}} and {{wg|AllowUserJs}} in LocalSettings.php.
+
+=== Cross-site request forgery ===
+An attacker could use JavaScript-submitted forms to perform various restricted
+actions by tricking an authenticated user into visiting a malicious web page. A
+fix for page editing in 1.3.10/1.4beta6 has been expanded in this release to
+other forms and functions. Authors of bot tools may need to update their code
+to include the additional fields.
+
+=== Directory traversal ===
+An unchecked parameter in image deletion could allow an authenticated
+administrator to delete arbitary files in directories writable by the web
+server, and confirm existence of files not deletable.
+
+== MediaWiki 1.3.10, 2005-02-03 ==
+MediaWiki 1.3.10 is a security release.
+An attacker could craft a URL which, when visited by a particular logged-in
+user, would execute arbitrary JavaScript code on the user's browser in the
+wiki's site context. This attack has been blocked, and as an extra precaution
+the user CSS and JavaScript subpage support is now disabled by default. Sites
+which want this ability may set {{wg|AllowUserCss}} and {{wg|AllowUserJs}} in
+{{manual|LocalSettings.php}}. Additional protections have been added against
+off-site form submissions
+hijacking user credentials. Authors of bot tools may need to update their code
+to include additional fields. All wikis running 1.3.x are strongly urged to
+upgrade to 1.3.10.
+Changes from 1.3.9:
+* Logged-in edits and preview of user CSS/JS are now locked to a session token.
+* Per-user CSS and JavaScript subpage customizations now disabled by default.
+They can be re-enabled via {{wg|AllowUserJs}} and {{wg|AllowUserCss}}.
+* Removed .ogg from the default uploads whitelist as an extra precaution. If
+your web server is configured to serve Ogg files with the correct Content-Type
+header, you can re-add it in LocalSettings.php: {{wg|FileExtensions}}[] =
+'ogg'
+
+== MediaWiki 1.3.9, 2004-12-12 ==
+MediaWiki 1.3.9 is a security and bug fix release.
+A flaw in upload handling has been found which may allow upload and  execution
+of arbitrary scripts with the permissions of the web server. Only wikis that
+have enabled uploads and have a vulnerable Apache  configuration will be
+affected, but to be safe all wikis should upgrade. Wikis with uploads available
+should either disable uploads or upgrade to 1.3.9 immediately; if other files
+are customized and require merging changes,
+includes/{{manual|SpecialUpload.php}} may be replaced individually to add the
+fix. (It is also recommended to configure your web server to disable script
+execution in the 'images' subdirectory where uploads are placed, which prevents
+most attacks even if the wiki fails.)
+Changes from 1.3.8:
+* Backported "Templates used in this page"-feature of EditPage
+* Allow "MySkin" as a default skin.
+* ({{bugzilla|938}}) Parse namespaces correctly on self-interwiki links
+* ({{bugzilla|1010}}) fix broken Commons image link on [[Skin:Classic|Classic]]
+& [[Skin:Cologne Blue|Cologne Blue]]
+* ({{bugzilla|1004}}) Norsk language names for interwiki links changed, Nauruan
+language name changed
+* Enhance upload extension blacklist to protect against vulnerable Apache
+configurations
+
+== MediaWiki 1.3.8, 2004-11-15 ==
+MediaWiki 1.3.8 is a bugfix release. Those running wikis with uploads enabled
+are strongly recommended to upgrade as this fixes several problems with
+overwriting previously-uploaded files.
+Changes from 1.3.7:
+* ({{bugzilla|506}}) fix {{code|inline=y|lang=html|array_key_exists()}} warning
+for IIS servers using ISAPI mode
+* ({{bugzilla|718}}) fix bad charset in (file) cached pages
+* use local numerals in category page (for Hindi et al)
+* alias month abbreviations to month names in Hindi
+* add localized numerals for Gujarati and Kannada
+* fix Category and project namespaces for Hindi
+* Don't output bogus timestamp on [[Special:RecentChanges]] if no entries
+* Correct template include path which broke some but not all Windows installs
+* Fix edit form submission problem with some PHP versions
+* Disallow unreachable titles with %XX hex codes
+* Allow page [[0]] to be renamed
+* ({{bugzilla|774}}) when saving with section=new, return to the
+anchor as with existing numbered section edits
+* Experimental shared upload overlay area (disabled by default)
+* ({{bugzilla|806}}) Removed some "Wikipedia" hardcoding in German localization
+* User option localization fix for some extensions
+* ({{bugzilla|809}}) now try to load the mysql php extension if it isn't loaded
+* ({{bugzilla|848}}) fix error message in [[Special:Newpages]] RSS and Atom
+feeds
+* ({{bugzilla|26}}) fix cache headers on anon talk page notification
+* ({{bugzilla|874}}) added 'cgi' to {{wg|FileBlacklist}}
+* ({{bugzilla|862}}) localize date and time format for Finnish
+* ({{bugzilla|548}}) Don't overwrite images until the user confirms it
+
+== MediaWiki 1.3.7, 2004-10-18 ==
+Changes from 1.3.6:
+* Fix protected-page related security issue.
+
+== MediaWiki 1.3.6, 2004-10-14 ==
+Changes from 1.3.5:
+* ({{bugzilla|296}}) Variables in user interface messages are no longer
+substituted at install time, so changes to the site name etc should be easier
+to make
+* ({{bugzilla|149}}) [[Special:RecentChanges]] "changes from" link preserves
+limit
+* ({{bugzilla|433}}) tooltip for "Undelete" tab now labeled correctly
+* ({{bugzilla|439}}) unclickable "Move" tab no longer displays on protected
+pages
+* ({{bugzilla|484}}) graceful deletion of images where the actual file is
+missing
+* ({{bugzilla|686}}) fixed [[plural]]s in Catalan localization
+* Fixed potential HTML/JavaScript injection attack in the
+[[Extension:UnicodeConverter|UnicodeConverter]] extension. (This extension is
+not enabled by default.)
+* Fixed potential HTML/JavaScript injection attack via raw page views to a
+maliciously crafted wiki page.
+* ({{bugzilla|187}}, {{bugzilla|669}}) Fixed centered thumbnails, using
+{{code|inline=y|lang=html|
}} instead of {{code|inline=y|lang=html|}}. +* catch MySQL error 2000 during installation. +* ({{bugzilla|704}}) Removed misleading LocalSettings.sample +* Fix cross site scripting bugs in [[Special:Ipblocklist]], +[[Special:EmailUser]] +* Fix SQL injection and cross site scripting bugs in Special:Maintenance +* Fix cross site scripting bugs and possible filename validation vulnerability +in ImagePage. +* and more of that sort + +== MediaWiki 1.3.5, 2004-09-30 == +Changes from 1.3.4: +* Clean up input validation in 'raw' page output mode which was a potential +cross-site scripting opportunity. + +== MediaWiki 1.3.4, 2004-09-28 == +=== SECURITY NOTE === +As of 1.3.4, MediaWiki performs some screening of newly uploaded files for +validity. (Some) corrupt image files, and HTML files mistakenly or maliciously +masquerading as images, should now be rejected. These checks protect against +Internet Explorer security holes relating to type autodetection which are a +potential cross-site scripting attack vector, and also rejects at least one +known version of the "JPEG virus" which might attack unpatched clients. If you +already have invalid files uploaded this will not protect against them. If you +have expanded the filetype whitelist or disabled the strict type +checking, other dangerous file types may still get through. You should always +be careful when allowing uploads! +Changes from 1.3.3: +* Fixed lots of template-related bugs, esp. for cases where template variables +are used for links, images, etc. +* Fixed transformation of page messages when viewing [[Special:Allmessages]] +* Handle "ISBN ISBN 1234" correctly +* Fixed warning on Category pages +* Fixed some bad error messages on login page +* Fixed history entry for initial main page on install +* Removed problematic { and } from legal title +characters +* Strip leading blank from output in preformatted text. +* Fixed problem when moving pages to titles with '#' in +* Optional {{wg|RawHtml}} for raw {{code|inline=y|lang=html|}} sections. +Use only on limited- participation 'trusted' wikis, as it does not protect +against cross-site scripting attacks. For security, this option can only be +enabled if in {{wg|WhitelistEdit}} mode. +* Fixed problem where pages which were created as a redirect following a move +never showed on [[Special:Randompage]]. +* Fixed line spacing on printed table of contents +* Allow links to pages with names of the form [[RFC 1234]] +* Fixed broken edit links being shown for sections from included templates +* Verify that uploaded image files are of the claimed type. + +== MediaWiki 1.3.3, 2004-09-09 == +Changes from 1.3.2: +* Fix for long numeric page titles +* Fix Go search for "0", numeric almost-self-links +* Avoid caching of pages with "You have new messages" headers +* Fix for upgrades as non-root users from 1.2 command-line installs. +* Fix for {{wg|DebugDumpSql}} debug mode. +* {{wg|ExtraNamespaces}} setting for configuring additional namespaces (see +note in {{manual|DefaultSettings.php}}) +* 'recache' on query pages now disabled when miser mode is on; special case the +global settings in your {{manual|LocalSettings.php}} to do automatic updates. +* Don't block UTF-8 titles containing byte 0xA0 (bug added in 1.3.2) +* Watch/unwatch tabs now shown on edit pages in MonoBook. +* Fix default skin in Irish localization (ga) +* Add Traditional Chinese localization (zh-tw) +* Changed default sortkey of subcategories. Don't include "Category:"-prefix +any longer +* More helpful info on spam catcher. +* Allow larger offsets for queries such as [[Special:Listusers]] +* Semicolon (;) added to French non-break space rules +* Possible fix for some install errors with path names permission problems. +* Removed [[Project:All system messages]], which has been superseded by the +much faster [[Special:Allmessages]]. This speeds up installation considerably. + +== MediaWiki 1.3.2, 2004-08-30 == +Changes from 1.3.1: +* Fix namespaced page creation links when no go match +* When cookies are disabled, don't show login screen twice +* Install should no longer die when PHP is pre-configured to compress output +* Fixed bug that caused long Japanese pages to time out with Tidy active +* When session.handler is set incorrectly, try automatic override to 'files' +* Watch/Unwatch links back to the affected page instead of Main Page +* Upload link no longer displayed on Monobook if uploading is disabled +* Special:Allmessages faster, shows correct original text, works in safe mode + +== MediaWiki 1.3.1, 2004-08-14 == +Changes from 1.3.0: +* Watchlist parameters now work with register_globals off +* Fixed parsing of ''italics'' and '''bold''' mark-up (again) +* Special:Allpages display is more sensible on smaller wikis +* Fixed XHTML parsing error in classic skins +* Moved pages update watchlist correctly +* Fixed rebuildall.php on case-sensitive Unix filesystems +* Disabled file cache compression by default due to incompatibility with output +buffer compression (ob_gzhandler) +* New magic word {{code|inline=y|PAGENAMEE}} (URL-escaped version of +{{code|inline=y|PAGENAME}}) +* Installation avoids blank username; better message on missing XML module +* {{wg|WhitelistAccount}} no longer breaks all logins. + +== MediaWiki 1.3.0, 2004-08-11 == +Look & layout: +* New default layout '[[Skin:MonoBook|MonoBook]]' (available on PHP4 only +currently) +* Print stylesheet now built-in to every page +* More or less correct XHTML 1.0 (served as text/html by default) +Wiki features: +* Image captions can now include links and other basic formatting +* Image bounding box can be specified instead of width, e.g. as 100x100px, +making the image not wider than 100px and not higher than 100px, keeping aspect +ratio. +* Templates have been expanded with parameters, and separated from the +MediaWiki: localization scheme. +* Categories more or less work +* added a special page for listing users with sysop rights. +Editing: +* Automatic merging of edit conflicts that don't directly interfere +* Edit summaries can now include basic formatting and links +Metadata and output: +* Linked Creative Commons copyright metadata (optional) +* RSS 2.0 & Atom 0.3 feeds for Recent Changes, New Pages +Optional modules: +* WikiHiero hieroglyphic module can be added (separate download) +* Timeline module can be added (separate download). Requires ploticus. +* TeX now has an experimental MathML output mode (incomplete!) +Installation and upgrading: +* The old install.php and update.php have been removed. In-place installation +introduced in 1.2 is now the standard installation and upgrade method, see +INSTALL and UPGRADE for directions. +Database: +* The links table has been changed to use a cur_id for l_from. The link tables +must be converted on upgrade, which may entail some downtime. +Code and compatibility: +* Should now run clean with error reporting set to E_ALL. +* register_globals hack from 1.2 has been replaced with safer code +* Bundled PHPTAL 0.7.0 from http://phptal.sourceforge.net/ (with some patches) +* Most image-related code moved to Image.php +* More fixes for PHP 4.1.2 (thanks to Asheesh Laroia) +* URL encoding fix for anchors +* All languages now available in UTF-8 mode +* Various other fixes + +=== Caveats === +Some output, particularly involving user-supplied inline HTML, may not produce +100% valid or well-formed XHTML output. Testers are welcome to set $wgMimeType += "application/xhtml+xml"; to test for remaining problem cases, but this is not +recommended on live sites. (This must be set for MathML to display properly in +Mozilla.) The new 'MonoBook' skin is not compatible with PHP 5 due to bugs in +the underlying PHPTAL library. It will be automatically disabled when running +on PHP5; the older look and feel will be used instead. + += pre-MediaWiki 1.1.0 = + +== Mediawiki-20031118 == +* Image deletion fixed. +* Deletion of image old revisions now restricted to sysops (this is an +irreversible action and not well logged) +* Fixed maintenance scripts broken by last release's security fix +* Many errors in {{manual|rebuildlinks.php|rebuildlinks}} script fixed. + +== Mediawiki-20031117 == +* SECURITY FIX: stricter checking of include path +* Fixed user contributions next/prev bug +* Login cookies now have the database name prefixed to allow wikis to coexist +in the same domain. This will invalidate any old saved password cookies. +* Update cache timestamp when talk pages are created +* Saving the login form in Mozilla no longer blanks password in prefs. +* Check existence of source page before performing a move. +* Detect invalid titles in Special:Allpages +* Q-encode headers on outgoing inter-user e-mail +* Updates to some translations. +* Added table of contents border/bg to Cologne Blue, Nostalgia skins +* Protected pages no longer appear unprotected when visited via redirect +* Swapped old Wikipedia logo for the MediaWiki sunflower logo +* install.php, update.php print warning on old PHP versions, added +compatibility functions that might or might not help No database changes since +20031107; upgrading should be clean. + +== Mediawiki-20031107 == +* Fixed various bugs! +* Some speed improvements from tweaks to the table indexes +* Limited support for memcached (see below) +* New translations (see below) +* Interwiki link data now kept in database for flexibility +* Friendlier read-only source view if asked to edit a page when the db is +locked or the page is protected. +* Normal IP blocks auto-expire after 24 hours +* Optional support for blocking usernames +* Uploads disabled by default (see below) + +== Mediawiki-20030829 == +First release under MediaWiki name. + +=== Security note === +Uploads are now disabled by default. If you've set up a secure configuration +you can reenable uploads by putting: $wgDisableUploads = false; +into LocalSettings.php. Earlier versions of MediaWiki included a bug that +potentially allows logged- in users to delete arbitrary files in directories +writable by the web server user by manually feeding false form data; this is +now fixed. As a reminder, disable PHP script execution in the upload directory! +You may also wish to serve HTML pages as plaintext to prevent cookie- stealing +JavaScript attacks. Example Apache config fragment: +
+
+     # Ignore .htaccess files
+     AllowOverride None
+
+     # Serve HTML as plaintext
+     AddType text/plain .html .htm .shtml
+
+     # Don't run arbitrary PHP code.
+     php_admin_flag engine off
+
+     # If you've other scripting languages, disable them too.
+
+
+ +=== Database updates === +If you're using {{manual|update.php}}, the necessary database changes should be +made automatically. To manually upgrade your database from the 2003-08-29 +release, run the following SQL scripts from the maintenance subdirectory: +archives/patch-ipblocks.sql archives/patch-interwiki.sql +archives/patch-indexes.sql interwiki.sql To copy in the Wikipedia +language-prefix interwikis as well, add: wikipedia-interwiki.sql + +=== Translations === +New interface localization files are included for: +*fy - Frisian +*ro - Romanian +*sl - Slovene +*sq - Albanian +*sr - Serbian + +=== Memcached === +Memcached is a distributed cache system. See http://www.danga.com/memcached/ +MediaWiki can optionally use memcached to store some data between calls to +reduce load on the database. Currently this is limited to user and talk page +notification data, interwiki prefix/URL matches, and the UTF-8 conversion +tables. MediaWiki includes version 1.0.10 of the (GPL'd) PHP memcached client +by Ryan Gilfether; if memcached is disabled it acts as a dummy object with +minimal overhead. To use memcached you'll need PHP installed with sockets +support (this is not in the default configure options). See docs/memcached for +some more details. Additionally, you can store login session data in memcached +instead of the local filesystem, which can help to enable load-balancing by +letting login sessions transparently work on multiple front-end web servers. +(The primary other issue is with uploads, which requires some care in +handling.) To enable this, set $wgSessionsInMemcached = true; and set +$wgCookieDomain appropriately if exposing multiple hostnames. This system is +new and may be volatile; login sessions will fail dramatically if memcached is +unavailable when this option is turned on. + +=== Online documentation === +Documentation for both end-users and site administrators is currently being +built up on Meta-Wikipedia, and is covered under the GNU Free Documentation +License: http://meta.wikipedia.org/wiki/MediaWiki_User%27s_Guide + +=== Mailing list === +A MediaWiki-l mailing list has been set up distinct from the Wikipedia +wikitech-l list: http://mail.wikipedia.org/mailman/listinfo/mediawiki-l + +=== UseModWiki import script === +A stripped-down UseModWiki import script is available in the maintenance +subdirectory. It is incomplete and requires a lot of manual clean-up, but does +function for the brave and pure of heart. + +=== Test suite removed === +The unmaintained Java-based test suite has been removed from the tarball +release. If you really want it you can check it out from CVS. diff --git a/RELEASE-NOTES-1.33 b/RELEASE-NOTES-1.33 index d3a09c5e78..ddd6da96d3 100644 --- a/RELEASE-NOTES-1.33 +++ b/RELEASE-NOTES-1.33 @@ -134,7 +134,7 @@ For notes on 1.32.x and older releases, see HISTORY. * Updated wikimedia/php-session-serializer from 1.0.6 to 1.0.7. ==== Removed external libraries ==== -* … +* (T219403) jquery.ui.spinner, deprecated since 1.31, was removed. === Bug fixes in 1.33 === * (T164211) Special:UserRights could sometimes fail with a @@ -341,6 +341,10 @@ because of Phabricator reports. Use CdnCacheUpdate::newFromTitles() instead. * Handling of multiple arguments by the Block constructor, deprecated in 1.26, has been removed. +* The translation of main page in Sardinian (sc) was changed from "Pàgina Base" + to "Pàgina printzipale". Existing wikis using this content language need to + move the main page or change the name through MediaWiki:Mainpage page. +* wfSplitWikiID(), deprecated in 1.32, has been removed. === Deprecations in 1.33 === * The configuration option $wgUseESI has been deprecated, and is expected @@ -404,6 +408,7 @@ because of Phabricator reports. * ManualLogEntry::setTags() is deprecated, use ManualLogEntry::addTags() instead. The setTags() method was overriding the tags, addTags() doesn't override, only adds new tags. +* Block::isValid is deprecated, since it is no longer needed in core. === Other changes in 1.33 === * (T201747) Html::openElement() warns if given an element name with a space diff --git a/autoload.php b/autoload.php index 528b7fe372..0d2bac98f7 100644 --- a/autoload.php +++ b/autoload.php @@ -298,7 +298,7 @@ $wgAutoloadLocalClasses = [ 'ComposerVendorHtaccessCreator' => __DIR__ . '/includes/composer/ComposerVendorHtaccessCreator.php', 'ComposerVersionNormalizer' => __DIR__ . '/includes/composer/ComposerVersionNormalizer.php', 'CompressOld' => __DIR__ . '/maintenance/storage/compressOld.php', - 'ConcatenatedGzipHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php', + 'ConcatenatedGzipHistoryBlob' => __DIR__ . '/includes/historyblob/ConcatenatedGzipHistoryBlob.php', 'Config' => __DIR__ . '/includes/config/Config.php', 'ConfigException' => __DIR__ . '/includes/config/ConfigException.php', 'ConfigFactory' => __DIR__ . '/includes/config/ConfigFactory.php', @@ -398,7 +398,7 @@ $wgAutoloadLocalClasses = [ 'Diff' => __DIR__ . '/includes/diff/DairikiDiff.php', 'DiffEngine' => __DIR__ . '/includes/diff/DiffEngine.php', 'DiffFormatter' => __DIR__ . '/includes/diff/DiffFormatter.php', - 'DiffHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php', + 'DiffHistoryBlob' => __DIR__ . '/includes/historyblob/DiffHistoryBlob.php', 'DiffOp' => __DIR__ . '/includes/diff/DairikiDiff.php', 'DiffOpAdd' => __DIR__ . '/includes/diff/DairikiDiff.php', 'DiffOpChange' => __DIR__ . '/includes/diff/DairikiDiff.php', @@ -628,9 +628,9 @@ $wgAutoloadLocalClasses = [ 'HashSiteStore' => __DIR__ . '/includes/site/HashSiteStore.php', 'HashtableReplacer' => __DIR__ . '/includes/libs/replacers/HashtableReplacer.php', 'HistoryAction' => __DIR__ . '/includes/actions/HistoryAction.php', - 'HistoryBlob' => __DIR__ . '/includes/HistoryBlob.php', - 'HistoryBlobCurStub' => __DIR__ . '/includes/HistoryBlob.php', - 'HistoryBlobStub' => __DIR__ . '/includes/HistoryBlob.php', + 'HistoryBlob' => __DIR__ . '/includes/historyblob/HistoryBlob.php', + 'HistoryBlobCurStub' => __DIR__ . '/includes/historyblob/HistoryBlobCurStub.php', + 'HistoryBlobStub' => __DIR__ . '/includes/historyblob/HistoryBlobStub.php', 'HistoryPager' => __DIR__ . '/includes/actions/pagers/HistoryPager.php', 'Hooks' => __DIR__ . '/includes/Hooks.php', 'Html' => __DIR__ . '/includes/Html.php', @@ -974,7 +974,7 @@ $wgAutoloadLocalClasses = [ 'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php', 'MergeableUpdate' => __DIR__ . '/includes/deferred/MergeableUpdate.php', 'Message' => __DIR__ . '/includes/Message.php', - 'MessageBlobStore' => __DIR__ . '/includes/cache/MessageBlobStore.php', + 'MessageBlobStore' => __DIR__ . '/includes/resourceloader/MessageBlobStore.php', 'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php', 'MessageCacheUpdate' => __DIR__ . '/includes/deferred/MessageCacheUpdate.php', 'MessageContent' => __DIR__ . '/includes/content/MessageContent.php', @@ -1138,7 +1138,7 @@ $wgAutoloadLocalClasses = [ 'PreferencesForm' => __DIR__ . '/includes/specials/forms/PreferencesFormLegacy.php', 'PreferencesFormLegacy' => __DIR__ . '/includes/specials/forms/PreferencesFormLegacy.php', 'PreferencesFormOOUI' => __DIR__ . '/includes/specials/forms/PreferencesFormOOUI.php', - 'PrefixSearch' => __DIR__ . '/includes/PrefixSearch.php', + 'PrefixSearch' => __DIR__ . '/includes/search/PrefixSearch.php', 'PrefixingStatsdDataFactoryProxy' => __DIR__ . '/includes/libs/stats/PrefixingStatsdDataFactoryProxy.php', 'PreprocessDump' => __DIR__ . '/maintenance/preprocessDump.php', 'Preprocessor' => __DIR__ . '/includes/parser/Preprocessor.php', @@ -1450,7 +1450,7 @@ $wgAutoloadLocalClasses = [ 'StorageTypeStats' => __DIR__ . '/maintenance/storage/storageTypeStats.php', 'StoreFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/StoreFileOp.php', 'StreamFile' => __DIR__ . '/includes/StreamFile.php', - 'StringPrefixSearch' => __DIR__ . '/includes/PrefixSearch.php', + 'StringPrefixSearch' => __DIR__ . '/includes/search/StringPrefixSearch.php', 'StringUtils' => __DIR__ . '/includes/libs/StringUtils.php', 'StripState' => __DIR__ . '/includes/parser/StripState.php', 'StubObject' => __DIR__ . '/includes/StubObject.php', @@ -1491,7 +1491,7 @@ $wgAutoloadLocalClasses = [ 'TitleCleanup' => __DIR__ . '/maintenance/cleanupTitles.php', 'TitleFormatter' => __DIR__ . '/includes/title/TitleFormatter.php', 'TitleParser' => __DIR__ . '/includes/title/TitleParser.php', - 'TitlePrefixSearch' => __DIR__ . '/includes/PrefixSearch.php', + 'TitlePrefixSearch' => __DIR__ . '/includes/search/TitlePrefixSearch.php', 'TitleValue' => __DIR__ . '/includes/title/TitleValue.php', 'TrackBlobs' => __DIR__ . '/maintenance/storage/trackBlobs.php', 'TrackingCategories' => __DIR__ . '/includes/TrackingCategories.php', @@ -1707,8 +1707,8 @@ $wgAutoloadLocalClasses = [ 'ZhConverter' => __DIR__ . '/languages/classes/LanguageZh.php', 'ZipDirectoryReader' => __DIR__ . '/includes/utils/ZipDirectoryReader.php', 'ZipDirectoryReaderError' => __DIR__ . '/includes/utils/ZipDirectoryReaderError.php', - 'concatenatedgziphistoryblob' => __DIR__ . '/includes/HistoryBlob.php', - 'historyblobcurstub' => __DIR__ . '/includes/HistoryBlob.php', - 'historyblobstub' => __DIR__ . '/includes/HistoryBlob.php', + 'concatenatedgziphistoryblob' => __DIR__ . '/includes/historyblob/ConcatenatedGzipHistoryBlob.php', + 'historyblobcurstub' => __DIR__ . '/includes/historyblob/HistoryBlobCurStub.php', + 'historyblobstub' => __DIR__ . '/includes/historyblob/HistoryBlobStub.php', 'profile_point' => __DIR__ . '/profileinfo.php', ]; diff --git a/includes/Block.php b/includes/Block.php index 700e551eb9..b17ec86666 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -964,9 +964,12 @@ class Block { /** * Is the block address valid (i.e. not a null string?) + * + * @deprecated since 1.33 No longer needed in core. * @return bool */ public function isValid() { + wfDeprecated( __METHOD__, '1.33' ); return $this->getTarget() != null; } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index d173d355ef..3a040c8e5d 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -9030,16 +9030,6 @@ $wgPriorityHints = false; */ $wgElementTiming = false; -/** - * Temporary option to show rollback confirmation user settings - * without activating the feature itself - * @see T217039 - * @since 1.33 - * @deprecated 1.33 - * @var bool - */ -$wgDisableRollbackConfirmationFeature = false; - /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 55b78acf53..20f3fd0417 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2610,22 +2610,6 @@ function wfWikiID() { } } -/** - * Split a wiki ID into DB name and table prefix - * - * @param string $wiki - * - * @return array - * @deprecated 1.32 - */ -function wfSplitWikiID( $wiki ) { - $bits = explode( '-', $wiki, 2 ); - if ( count( $bits ) < 2 ) { - $bits[] = ''; - } - return $bits; -} - /** * Get a Database object. * diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php deleted file mode 100644 index bca6c7e5bc..0000000000 --- a/includes/HistoryBlob.php +++ /dev/null @@ -1,711 +0,0 @@ -uncompress(); - $hash = md5( $text ); - if ( !isset( $this->mItems[$hash] ) ) { - $this->mItems[$hash] = $text; - $this->mSize += strlen( $text ); - } - return $hash; - } - - /** - * @param string $hash - * @return array|bool - */ - public function getItem( $hash ) { - $this->uncompress(); - if ( array_key_exists( $hash, $this->mItems ) ) { - return $this->mItems[$hash]; - } else { - return false; - } - } - - /** - * @param string $text - * @return void - */ - public function setText( $text ) { - $this->uncompress(); - $this->mDefaultHash = $this->addItem( $text ); - } - - /** - * @return array|bool - */ - public function getText() { - $this->uncompress(); - return $this->getItem( $this->mDefaultHash ); - } - - /** - * Remove an item - * - * @param string $hash - */ - public function removeItem( $hash ) { - $this->mSize -= strlen( $this->mItems[$hash] ); - unset( $this->mItems[$hash] ); - } - - /** - * Compress the bulk data in the object - */ - public function compress() { - if ( !$this->mCompressed ) { - $this->mItems = gzdeflate( serialize( $this->mItems ) ); - $this->mCompressed = true; - } - } - - /** - * Uncompress bulk data - */ - public function uncompress() { - if ( $this->mCompressed ) { - $this->mItems = unserialize( gzinflate( $this->mItems ) ); - $this->mCompressed = false; - } - } - - /** - * @return array - */ - function __sleep() { - $this->compress(); - return [ 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' ]; - } - - function __wakeup() { - $this->uncompress(); - } - - /** - * Helper function for compression jobs - * Returns true until the object is "full" and ready to be committed - * - * @return bool - */ - public function isHappy() { - return $this->mSize < $this->mMaxSize - && count( $this->mItems ) < $this->mMaxCount; - } -} - -/** - * Pointer object for an item within a CGZ blob stored in the text table. - */ -class HistoryBlobStub { - /** - * @var array One-step cache variable to hold base blobs; operations that - * pull multiple revisions may often pull multiple times from the same - * blob. By keeping the last-used one open, we avoid redundant - * unserialization and decompression overhead. - */ - protected static $blobCache = []; - - /** @var int */ - public $mOldId; - - /** @var string */ - public $mHash; - - /** @var string */ - public $mRef; - - /** - * @param string $hash The content hash of the text - * @param int $oldid The old_id for the CGZ object - */ - function __construct( $hash = '', $oldid = 0 ) { - $this->mHash = $hash; - } - - /** - * Sets the location (old_id) of the main object to which this object - * points - * @param int $id - */ - function setLocation( $id ) { - $this->mOldId = $id; - } - - /** - * Sets the location (old_id) of the referring object - * @param string $id - */ - function setReferrer( $id ) { - $this->mRef = $id; - } - - /** - * Gets the location of the referring object - * @return string - */ - function getReferrer() { - return $this->mRef; - } - - /** - * @return string|false - */ - function getText() { - if ( isset( self::$blobCache[$this->mOldId] ) ) { - $obj = self::$blobCache[$this->mOldId]; - } else { - $dbr = wfGetDB( DB_REPLICA ); - $row = $dbr->selectRow( - 'text', - [ 'old_flags', 'old_text' ], - [ 'old_id' => $this->mOldId ] - ); - - if ( !$row ) { - return false; - } - - $flags = explode( ',', $row->old_flags ); - if ( in_array( 'external', $flags ) ) { - $url = $row->old_text; - $parts = explode( '://', $url, 2 ); - if ( !isset( $parts[1] ) || $parts[1] == '' ) { - return false; - } - $row->old_text = ExternalStore::fetchFromURL( $url ); - - } - - if ( !in_array( 'object', $flags ) ) { - return false; - } - - if ( in_array( 'gzip', $flags ) ) { - // This shouldn't happen, but a bug in the compress script - // may at times gzip-compress a HistoryBlob object row. - $obj = unserialize( gzinflate( $row->old_text ) ); - } else { - $obj = unserialize( $row->old_text ); - } - - if ( !is_object( $obj ) ) { - // Correct for old double-serialization bug. - $obj = unserialize( $obj ); - } - - // Save this item for reference; if pulling many - // items in a row we'll likely use it again. - $obj->uncompress(); - self::$blobCache = [ $this->mOldId => $obj ]; - } - - return $obj->getItem( $this->mHash ); - } - - /** - * Get the content hash - * - * @return string - */ - function getHash() { - return $this->mHash; - } -} - -/** - * To speed up conversion from 1.4 to 1.5 schema, text rows can refer to the - * leftover cur table as the backend. This avoids expensively copying hundreds - * of megabytes of data during the conversion downtime. - * - * Serialized HistoryBlobCurStub objects will be inserted into the text table - * on conversion if $wgLegacySchemaConversion is set to true. - */ -class HistoryBlobCurStub { - /** @var int */ - public $mCurId; - - /** - * @param int $curid The cur_id pointed to - */ - function __construct( $curid = 0 ) { - $this->mCurId = $curid; - } - - /** - * Sets the location (cur_id) of the main object to which this object - * points - * - * @param int $id - */ - function setLocation( $id ) { - $this->mCurId = $id; - } - - /** - * @return string|bool - */ - function getText() { - $dbr = wfGetDB( DB_REPLICA ); - $row = $dbr->selectRow( 'cur', [ 'cur_text' ], [ 'cur_id' => $this->mCurId ] ); - if ( !$row ) { - return false; - } - return $row->cur_text; - } -} - -/** - * Diff-based history compression - * Requires xdiff 1.5+ and zlib - */ -class DiffHistoryBlob implements HistoryBlob { - /** @var array Uncompressed item cache */ - public $mItems = []; - - /** @var int Total uncompressed size */ - public $mSize = 0; - - /** - * @var array Array of diffs. If a diff D from A to B is notated D = B - A, - * and Z is an empty string: - * - * { item[map[i]] - item[map[i-1]] where i > 0 - * diff[i] = { - * { item[map[i]] - Z where i = 0 - */ - public $mDiffs; - - /** @var array The diff map, see above */ - public $mDiffMap; - - /** @var int The key for getText() - */ - public $mDefaultKey; - - /** @var string Compressed storage */ - public $mCompressed; - - /** @var bool True if the object is locked against further writes */ - public $mFrozen = false; - - /** - * @var int The maximum uncompressed size before the object becomes sad - * Should be less than max_allowed_packet - */ - public $mMaxSize = 10000000; - - /** @var int The maximum number of text items before the object becomes sad */ - public $mMaxCount = 100; - - /** Constants from xdiff.h */ - const XDL_BDOP_INS = 1; - const XDL_BDOP_CPY = 2; - const XDL_BDOP_INSB = 3; - - function __construct() { - if ( !function_exists( 'gzdeflate' ) ) { - throw new MWException( "Need zlib support to read or write DiffHistoryBlob\n" ); - } - } - - /** - * @throws MWException - * @param string $text - * @return int - */ - function addItem( $text ) { - if ( $this->mFrozen ) { - throw new MWException( __METHOD__ . ": Cannot add more items after sleep/wakeup" ); - } - - $this->mItems[] = $text; - $this->mSize += strlen( $text ); - $this->mDiffs = null; // later - return count( $this->mItems ) - 1; - } - - /** - * @param string $key - * @return string - */ - function getItem( $key ) { - return $this->mItems[$key]; - } - - /** - * @param string $text - */ - function setText( $text ) { - $this->mDefaultKey = $this->addItem( $text ); - } - - /** - * @return string - */ - function getText() { - return $this->getItem( $this->mDefaultKey ); - } - - /** - * @throws MWException - */ - function compress() { - if ( !function_exists( 'xdiff_string_rabdiff' ) ) { - throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" ); - } - if ( isset( $this->mDiffs ) ) { - // Already compressed - return; - } - if ( $this->mItems === [] ) { - return; - } - - // Create two diff sequences: one for main text and one for small text - $sequences = [ - 'small' => [ - 'tail' => '', - 'diffs' => [], - 'map' => [], - ], - 'main' => [ - 'tail' => '', - 'diffs' => [], - 'map' => [], - ], - ]; - $smallFactor = 0.5; - - $mItemsCount = count( $this->mItems ); - for ( $i = 0; $i < $mItemsCount; $i++ ) { - $text = $this->mItems[$i]; - if ( $i == 0 ) { - $seqName = 'main'; - } else { - $mainTail = $sequences['main']['tail']; - if ( strlen( $text ) < strlen( $mainTail ) * $smallFactor ) { - $seqName = 'small'; - } else { - $seqName = 'main'; - } - } - $seq =& $sequences[$seqName]; - $tail = $seq['tail']; - $diff = $this->diff( $tail, $text ); - $seq['diffs'][] = $diff; - $seq['map'][] = $i; - $seq['tail'] = $text; - } - unset( $seq ); // unlink dangerous alias - - // Knit the sequences together - $tail = ''; - $this->mDiffs = []; - $this->mDiffMap = []; - foreach ( $sequences as $seq ) { - if ( $seq['diffs'] === [] ) { - continue; - } - if ( $tail === '' ) { - $this->mDiffs[] = $seq['diffs'][0]; - } else { - $head = $this->patch( '', $seq['diffs'][0] ); - $this->mDiffs[] = $this->diff( $tail, $head ); - } - $this->mDiffMap[] = $seq['map'][0]; - $diffsCount = count( $seq['diffs'] ); - for ( $i = 1; $i < $diffsCount; $i++ ) { - $this->mDiffs[] = $seq['diffs'][$i]; - $this->mDiffMap[] = $seq['map'][$i]; - } - $tail = $seq['tail']; - } - } - - /** - * @param string $t1 - * @param string $t2 - * @return string - */ - function diff( $t1, $t2 ) { - # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff - # "String is not zero-terminated" - Wikimedia\suppressWarnings(); - $diff = xdiff_string_rabdiff( $t1, $t2 ) . ''; - Wikimedia\restoreWarnings(); - return $diff; - } - - /** - * @param string $base - * @param string $diff - * @return bool|string - */ - function patch( $base, $diff ) { - if ( function_exists( 'xdiff_string_bpatch' ) ) { - Wikimedia\suppressWarnings(); - $text = xdiff_string_bpatch( $base, $diff ) . ''; - Wikimedia\restoreWarnings(); - return $text; - } - - # Pure PHP implementation - - $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) ); - - # Check the checksum if hash extension is available - $ofp = $this->xdiffAdler32( $base ); - if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) { - wfDebug( __METHOD__ . ": incorrect base checksum\n" ); - return false; - } - if ( $header['csize'] != strlen( $base ) ) { - wfDebug( __METHOD__ . ": incorrect base length\n" ); - return false; - } - - $p = 8; - $out = ''; - while ( $p < strlen( $diff ) ) { - $x = unpack( 'Cop', substr( $diff, $p, 1 ) ); - $op = $x['op']; - ++$p; - switch ( $op ) { - case self::XDL_BDOP_INS: - $x = unpack( 'Csize', substr( $diff, $p, 1 ) ); - $p++; - $out .= substr( $diff, $p, $x['size'] ); - $p += $x['size']; - break; - case self::XDL_BDOP_INSB: - $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) ); - $p += 4; - $out .= substr( $diff, $p, $x['csize'] ); - $p += $x['csize']; - break; - case self::XDL_BDOP_CPY: - $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) ); - $p += 8; - $out .= substr( $base, $x['off'], $x['csize'] ); - break; - default: - wfDebug( __METHOD__ . ": invalid op\n" ); - return false; - } - } - return $out; - } - - /** - * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with - * the bytes backwards and initialised with 0 instead of 1. See T36428. - * - * @param string $s - * @return string|bool False if the hash extension is not available - */ - function xdiffAdler32( $s ) { - if ( !function_exists( 'hash' ) ) { - return false; - } - - static $init; - if ( $init === null ) { - $init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02"; - } - - // The real Adler-32 checksum of $init is zero, so it initialises the - // state to zero, as it is at the start of LibXDiff's checksum - // algorithm. Appending the subject string then simulates LibXDiff. - return strrev( hash( 'adler32', $init . $s, true ) ); - } - - function uncompress() { - if ( !$this->mDiffs ) { - return; - } - $tail = ''; - $mDiffsCount = count( $this->mDiffs ); - for ( $diffKey = 0; $diffKey < $mDiffsCount; $diffKey++ ) { - $textKey = $this->mDiffMap[$diffKey]; - $text = $this->patch( $tail, $this->mDiffs[$diffKey] ); - $this->mItems[$textKey] = $text; - $tail = $text; - } - } - - /** - * @return array - */ - function __sleep() { - $this->compress(); - if ( $this->mItems === [] ) { - $info = false; - } else { - // Take forward differences to improve the compression ratio for sequences - $map = ''; - $prev = 0; - foreach ( $this->mDiffMap as $i ) { - if ( $map !== '' ) { - $map .= ','; - } - $map .= $i - $prev; - $prev = $i; - } - $info = [ - 'diffs' => $this->mDiffs, - 'map' => $map - ]; - } - if ( isset( $this->mDefaultKey ) ) { - $info['default'] = $this->mDefaultKey; - } - $this->mCompressed = gzdeflate( serialize( $info ) ); - return [ 'mCompressed' ]; - } - - function __wakeup() { - // addItem() doesn't work if mItems is partially filled from mDiffs - $this->mFrozen = true; - $info = unserialize( gzinflate( $this->mCompressed ) ); - unset( $this->mCompressed ); - - if ( !$info ) { - // Empty object - return; - } - - if ( isset( $info['default'] ) ) { - $this->mDefaultKey = $info['default']; - } - $this->mDiffs = $info['diffs']; - if ( isset( $info['base'] ) ) { - // Old format - $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 ); - array_unshift( $this->mDiffs, - pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) . - $info['base'] ); - } else { - // New format - $map = explode( ',', $info['map'] ); - $cur = 0; - $this->mDiffMap = []; - foreach ( $map as $i ) { - $cur += $i; - $this->mDiffMap[] = $cur; - } - } - $this->uncompress(); - } - - /** - * Helper function for compression jobs - * Returns true until the object is "full" and ready to be committed - * - * @return bool - */ - function isHappy() { - return $this->mSize < $this->mMaxSize - && count( $this->mItems ) < $this->mMaxCount; - } - -} - -// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found -if ( false ) { - // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the - // class name coerced to lowercase. We can improve efficiency by adding - // autoload entries for the lowercase variants of these classes (T166759). - // The code below is never executed, but it is picked up by the AutoloadGenerator - // parser, which scans for class_alias() calls. - class_alias( ConcatenatedGzipHistoryBlob::class, 'concatenatedgziphistoryblob' ); - class_alias( HistoryBlobCurStub::class, 'historyblobcurstub' ); - class_alias( HistoryBlobStub::class, 'historyblobstub' ); -} diff --git a/includes/Linker.php b/includes/Linker.php index df9955609c..17dc0370f0 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1765,15 +1765,7 @@ class Linker { $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped(); } - /** - * FIXME - * Remove all references to DisableRollbackConfirmationFeature - * after release of rollback feature. See T199534 - */ - if ( !MediaWikiServices::getInstance() - ->getMainConfig()->get( 'DisableRollbackConfirmationFeature' ) && - $context->getUser()->getBoolOption( 'showrollbackconfirmation' ) - ) { + if ( $context->getUser()->getBoolOption( 'showrollbackconfirmation' ) ) { $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); $stats->increment( 'rollbackconfirmation.event.load' ); $context->getOutput()->addModules( 'mediawiki.page.rollback.confirmation' ); diff --git a/includes/MovePage.php b/includes/MovePage.php index bcec0a188d..db5750a1be 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -106,7 +106,7 @@ class MovePage { $oldid = $this->oldTitle->getArticleID(); - if ( strlen( $this->newTitle->getDBkey() ) < 1 ) { + if ( $this->newTitle->getDBkey() === '' ) { $status->fatal( 'articleexists' ); } if ( diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php deleted file mode 100644 index 7bc7a084a5..0000000000 --- a/includes/PrefixSearch.php +++ /dev/null @@ -1,365 +0,0 @@ -search( $search, $limit, $namespaces, $offset ); - } - - /** - * Do a prefix search of titles and return a list of matching page names. - * - * @param string $search - * @param int $limit - * @param array $namespaces Used if query is not explicitly prefixed - * @param int $offset How many results to offset from the beginning - * @return array Array of strings or Title objects - */ - public function search( $search, $limit, $namespaces = [], $offset = 0 ) { - $search = trim( $search ); - if ( $search == '' ) { - return []; // Return empty result - } - - $hasNamespace = SearchEngine::parseNamespacePrefixes( $search, false, true ); - if ( $hasNamespace !== false ) { - list( $search, $namespaces ) = $hasNamespace; - } - - return $this->searchBackend( $namespaces, $search, $limit, $offset ); - } - - /** - * Do a prefix search for all possible variants of the prefix - * @param string $search - * @param int $limit - * @param array $namespaces - * @param int $offset How many results to offset from the beginning - * - * @return array - */ - public function searchWithVariants( $search, $limit, array $namespaces, $offset = 0 ) { - $searches = $this->search( $search, $limit, $namespaces, $offset ); - - // if the content language has variants, try to retrieve fallback results - $fallbackLimit = $limit - count( $searches ); - if ( $fallbackLimit > 0 ) { - $fallbackSearches = MediaWikiServices::getInstance()->getContentLanguage()-> - autoConvertToAllVariants( $search ); - $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] ); - - foreach ( $fallbackSearches as $fbs ) { - $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces ); - $searches = array_merge( $searches, $fallbackSearchResult ); - $fallbackLimit -= count( $fallbackSearchResult ); - - if ( $fallbackLimit == 0 ) { - break; - } - } - } - return $searches; - } - - /** - * When implemented in a descendant class, receives an array of Title objects and returns - * either an unmodified array or an array of strings corresponding to titles passed to it. - * - * @param array $titles - * @return array - */ - abstract protected function titles( array $titles ); - - /** - * When implemented in a descendant class, receives an array of titles as strings and returns - * either an unmodified array or an array of Title objects corresponding to strings received. - * - * @param array $strings - * - * @return array - */ - abstract protected function strings( array $strings ); - - /** - * Do a prefix search of titles and return a list of matching page names. - * @param array $namespaces - * @param string $search - * @param int $limit - * @param int $offset How many results to offset from the beginning - * @return array Array of strings - */ - protected function searchBackend( $namespaces, $search, $limit, $offset ) { - if ( count( $namespaces ) == 1 ) { - $ns = $namespaces[0]; - if ( $ns == NS_MEDIA ) { - $namespaces = [ NS_FILE ]; - } elseif ( $ns == NS_SPECIAL ) { - return $this->titles( $this->specialSearch( $search, $limit, $offset ) ); - } - } - $srchres = []; - if ( Hooks::run( - 'PrefixSearchBackend', - [ $namespaces, $search, $limit, &$srchres, $offset ] - ) ) { - return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) ); - } - return $this->strings( - $this->handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) ); - } - - private function handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) { - if ( $offset === 0 ) { - // Only perform exact db match if offset === 0 - // This is still far from perfect but at least we avoid returning the - // same title afain and again when the user is scrolling with a query - // that matches a title in the db. - $rescorer = new SearchExactMatchRescorer(); - $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit ); - } - return $srchres; - } - - /** - * Prefix search special-case for Special: namespace. - * - * @param string $search Term - * @param int $limit Max number of items to return - * @param int $offset Number of items to offset - * @return array - */ - protected function specialSearch( $search, $limit, $offset ) { - $searchParts = explode( '/', $search, 2 ); - $searchKey = $searchParts[0]; - $subpageSearch = $searchParts[1] ?? null; - - // Handle subpage search separately. - $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); - if ( $subpageSearch !== null ) { - // Try matching the full search string as a page name - $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey ); - if ( !$specialTitle ) { - return []; - } - $special = $spFactory->getPage( $specialTitle->getText() ); - if ( $special ) { - $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset ); - return array_map( function ( $sub ) use ( $specialTitle ) { - return $specialTitle->getSubpage( $sub ); - }, $subpages ); - } else { - return []; - } - } - - # normalize searchKey, so aliases with spaces can be found - T27675 - $contLang = MediaWikiServices::getInstance()->getContentLanguage(); - $searchKey = str_replace( ' ', '_', $searchKey ); - $searchKey = $contLang->caseFold( $searchKey ); - - // Unlike SpecialPage itself, we want the canonical forms of both - // canonical and alias title forms... - $keys = []; - foreach ( $spFactory->getNames() as $page ) { - $keys[$contLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ]; - } - - foreach ( $contLang->getSpecialPageAliases() as $page => $aliases ) { - if ( !in_array( $page, $spFactory->getNames() ) ) {# T22885 - continue; - } - - foreach ( $aliases as $key => $alias ) { - $keys[$contLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ]; - } - } - ksort( $keys ); - - $matches = []; - foreach ( $keys as $pageKey => $page ) { - if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) { - // T29671: Don't use SpecialPage::getTitleFor() here because it - // localizes its input leading to searches for e.g. Special:All - // returning Spezial:MediaWiki-Systemnachrichten and returning - // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de' - $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] ); - - if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) { - // We have enough items in primary rank, no use to continue - break; - } - } - - } - - // Ensure keys are in order - ksort( $matches ); - // Flatten the array - $matches = array_reduce( $matches, 'array_merge', [] ); - - return array_slice( $matches, $offset, $limit ); - } - - /** - * Unless overridden by PrefixSearchBackend hook... - * This is case-sensitive (First character may - * be automatically capitalized by Title::secureAndSpit() - * later on depending on $wgCapitalLinks) - * - * @param array|null $namespaces Namespaces to search in - * @param string $search Term - * @param int $limit Max number of items to return - * @param int $offset Number of items to skip - * @return Title[] Array of Title objects - */ - public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) { - // Backwards compatability with old code. Default to NS_MAIN if no namespaces provided. - if ( $namespaces === null ) { - $namespaces = []; - } - if ( !$namespaces ) { - $namespaces[] = NS_MAIN; - } - - // Construct suitable prefix for each namespace. They differ in cases where - // some namespaces always capitalize and some don't. - $prefixes = []; - foreach ( $namespaces as $namespace ) { - // For now, if special is included, ignore the other namespaces - if ( $namespace == NS_SPECIAL ) { - return $this->specialSearch( $search, $limit, $offset ); - } - - $title = Title::makeTitleSafe( $namespace, $search ); - // Why does the prefix default to empty? - $prefix = $title ? $title->getDBkey() : ''; - $prefixes[$prefix][] = $namespace; - } - - $dbr = wfGetDB( DB_REPLICA ); - // Often there is only one prefix that applies to all requested namespaces, - // but sometimes there are two if some namespaces do not always capitalize. - $conds = []; - foreach ( $prefixes as $prefix => $namespaces ) { - $condition = [ - 'page_namespace' => $namespaces, - 'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() ), - ]; - $conds[] = $dbr->makeList( $condition, LIST_AND ); - } - - $table = 'page'; - $fields = [ 'page_id', 'page_namespace', 'page_title' ]; - $conds = $dbr->makeList( $conds, LIST_OR ); - $options = [ - 'LIMIT' => $limit, - 'ORDER BY' => [ 'page_title', 'page_namespace' ], - 'OFFSET' => $offset - ]; - - $res = $dbr->select( $table, $fields, $conds, __METHOD__, $options ); - - return iterator_to_array( TitleArray::newFromResult( $res ) ); - } - - /** - * Validate an array of numerical namespace indexes - * - * @param array $namespaces - * @return array (default: contains only NS_MAIN) - */ - protected function validateNamespaces( $namespaces ) { - // We will look at each given namespace against content language namespaces - $validNamespaces = MediaWikiServices::getInstance()->getContentLanguage()->getNamespaces(); - if ( is_array( $namespaces ) && count( $namespaces ) > 0 ) { - $valid = []; - foreach ( $namespaces as $ns ) { - if ( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) { - $valid[] = $ns; - } - } - if ( count( $valid ) > 0 ) { - return $valid; - } - } - - return [ NS_MAIN ]; - } -} - -/** - * Performs prefix search, returning Title objects - * @deprecated Since 1.27, Use SearchEngine::defaultPrefixSearch or SearchEngine::completionSearch - * @ingroup Search - */ -class TitlePrefixSearch extends PrefixSearch { - - protected function titles( array $titles ) { - return $titles; - } - - protected function strings( array $strings ) { - $titles = array_map( 'Title::newFromText', $strings ); - $lb = new LinkBatch( $titles ); - $lb->setCaller( __METHOD__ ); - $lb->execute(); - return $titles; - } -} - -/** - * Performs prefix search, returning strings - * @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch - * @ingroup Search - */ -class StringPrefixSearch extends PrefixSearch { - - protected function titles( array $titles ) { - return array_map( function ( Title $t ) { - return $t->getPrefixedText(); - }, $titles ); - } - - protected function strings( array $strings ) { - return $strings; - } -} diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index 7af80dcfd6..b400797c58 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -562,7 +562,7 @@ class SiteConfiguration { ->execute(); $data = trim( $result->getStdout() ); - if ( $result->getExitCode() != 0 || !strlen( $data ) ) { + if ( $result->getExitCode() || $data === '' ) { throw new MWException( "Failed to run getConfiguration.php: {$result->getStdout()}" ); } $res = unserialize( $data ); diff --git a/includes/Title.php b/includes/Title.php index 0f45839577..ce0b9595c5 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1702,16 +1702,18 @@ class Title implements LinkTarget, IDBAccessObject { * @return string Base name */ public function getBaseText() { + $text = $this->getText(); if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { - return $this->getText(); + return $text; } - $parts = explode( '/', $this->getText() ); - # Don't discard the real title if there's no subpage involved - if ( count( $parts ) > 1 ) { - unset( $parts[count( $parts ) - 1] ); + $lastSlashPos = strrpos( $text, '/' ); + // Don't discard the real title if there's no subpage involved + if ( $lastSlashPos === false ) { + return $text; } - return implode( '/', $parts ); + + return substr( $text, 0, $lastSlashPos ); } /** diff --git a/includes/WikiMap.php b/includes/WikiMap.php index 628fbc0257..dbad4b0741 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -255,7 +255,7 @@ class WikiMap { public static function getWikiIdFromDbDomain( $domain ) { $domain = DatabaseDomain::newFromId( $domain ); - if ( !in_array( $domain->getSchema(), [ null, 'mediawiki' ], true ) ) { + if ( !in_array( $domain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) { // Include the schema if it is set and is not the default placeholder. // This means a site admin may have specifically taylored the schemas. // Domain IDs might use the form --, meaning that @@ -298,7 +298,7 @@ class WikiMap { $domain = DatabaseDomain::newFromId( $domain ); $curDomain = self::getCurrentWikiDbDomain(); - if ( !in_array( $curDomain->getSchema(), [ null, 'mediawiki' ], true ) ) { + if ( !in_array( $curDomain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) { // Include the schema if it is set and is not the default placeholder. // This means a site admin may have specifically taylored the schemas. // Domain IDs might use the form --, meaning that diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index fbf43e0f3d..cc11233c05 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -78,7 +78,7 @@ class HistoryAction extends FormlessAction { ->rawParams( $this->getLanguage()->pipeList( $links ) ) ->escaped(); } - return $subtitle; + return Html::rawElement( 'div', [ 'class' => 'mw-history-subtitle' ], $subtitle ); } /** diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index 0e86fda81a..e2fc265f96 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -73,20 +73,12 @@ class RollbackAction extends FormAction { } /** - * @throws ConfigException * @throws ErrorPageError * @throws ReadOnlyError * @throws ThrottledError */ public function show() { - /** - * FIXME - * Remove temporary check of DisableRollbackConfirmationFeature - * after release of rollback feature. See T199534 - */ - $config = \MediaWiki\MediaWikiServices::getInstance()->getMainConfig(); - if ( $config->get( 'DisableRollbackConfirmationFeature' ) == true || - $this->getUser()->getOption( 'showrollbackconfirmation' ) == false || + if ( $this->getUser()->getOption( 'showrollbackconfirmation' ) == false || $this->getRequest()->wasPosted() ) { $this->handleRollbackRequest(); } else { diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 3781ab0f35..7e46c1a036 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -268,7 +268,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { [ 'ar_rev_id' => $revids ], __METHOD__ ), - ], false ); + ], $db::UNION_DISTINCT ); $res = $db->query( $sql, __METHOD__ ); foreach ( $res as $row ) { if ( (int)$row->id === (int)$params['startid'] ) { diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 96fa8a1380..82a52b40be 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -664,8 +664,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $data = [ - 'url' => strlen( $url ) ? $url : '', - 'text' => strlen( $text ) ? $text : '', + 'url' => (string)$url, + 'text' => (string)$text, ]; return $this->getResult()->addValue( 'query', $property, $data ); diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index fe5f6c4a44..51845627a6 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -477,7 +477,7 @@ class ApiStashEdit extends ApiBase { * @param string $newKey */ private static function pruneExcessStashedEntries( BagOStuff $cache, User $user, $newKey ) { - $key = $cache->makeKey( 'stash-edit-recent', $user->getId() ); + $key = $cache->makeKey( 'stash-edit-recent', sha1( $user->getName() ) ); $keyList = $cache->get( $key ) ?: []; if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) { diff --git a/includes/cache/MessageBlobStore.php b/includes/cache/MessageBlobStore.php deleted file mode 100644 index ceb51f2d3f..0000000000 --- a/includes/cache/MessageBlobStore.php +++ /dev/null @@ -1,240 +0,0 @@ -resourceloader = $rl; - $this->logger = $logger ?: new NullLogger(); - $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); - } - - /** - * @since 1.27 - * @param LoggerInterface $logger - */ - public function setLogger( LoggerInterface $logger ) { - $this->logger = $logger; - } - - /** - * Get the message blob for a module - * - * @since 1.27 - * @param ResourceLoaderModule $module - * @param string $lang Language code - * @return string JSON - */ - public function getBlob( ResourceLoaderModule $module, $lang ) { - $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang ); - return $blobs[$module->getName()]; - } - - /** - * Get the message blobs for a set of modules - * - * @since 1.27 - * @param ResourceLoaderModule[] $modules Array of module objects keyed by name - * @param string $lang Language code - * @return array An array mapping module names to message blobs - */ - public function getBlobs( array $modules, $lang ) { - // Each cache key for a message blob by module name and language code also has a generic - // check key without language code. This is used to invalidate any and all language subkeys - // that exist for a module from the updateMessage() method. - $cache = $this->wanCache; - $checkKeys = [ - // Global check key, see clear() - $cache->makeKey( __CLASS__ ) - ]; - $cacheKeys = []; - foreach ( $modules as $name => $module ) { - $cacheKey = $this->makeCacheKey( $module, $lang ); - $cacheKeys[$name] = $cacheKey; - // Per-module check key, see updateMessage() - $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name ); - } - $curTTLs = []; - $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys ); - - $blobs = []; - foreach ( $modules as $name => $module ) { - $key = $cacheKeys[$name]; - if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) { - $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang ); - } else { - // Use unexpired cache - $blobs[$name] = $result[$key]; - } - } - return $blobs; - } - - /** - * @deprecated since 1.27 Use getBlobs() instead - * @return array - */ - public function get( ResourceLoader $resourceLoader, $modules, $lang ) { - return $this->getBlobs( $modules, $lang ); - } - - /** - * @since 1.27 - * @param ResourceLoaderModule $module - * @param string $lang - * @return string Cache key - */ - private function makeCacheKey( ResourceLoaderModule $module, $lang ) { - $messages = array_values( array_unique( $module->getMessages() ) ); - sort( $messages ); - return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang, - md5( json_encode( $messages ) ) - ); - } - - /** - * @since 1.27 - * @param string $cacheKey - * @param ResourceLoaderModule $module - * @param string $lang - * @return string JSON blob - */ - protected function recacheMessageBlob( $cacheKey, ResourceLoaderModule $module, $lang ) { - $blob = $this->generateMessageBlob( $module, $lang ); - $cache = $this->wanCache; - $cache->set( $cacheKey, $blob, - // Add part of a day to TTL to avoid all modules expiring at once - $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ), - Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ) - ); - return $blob; - } - - /** - * Invalidate cache keys for modules using this message key. - * Called by MessageCache when a message has changed. - * - * @param string $key Message key - */ - public function updateMessage( $key ) { - $moduleNames = $this->getResourceLoader()->getModulesByMessage( $key ); - foreach ( $moduleNames as $moduleName ) { - // Uses a holdoff to account for database replica DB lag (for MessageCache) - $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) ); - } - } - - /** - * Invalidate cache keys for all known modules. - * Called by LocalisationCache after cache is regenerated. - */ - public function clear() { - $cache = $this->wanCache; - // Disable holdoff because this invalidates all modules and also not needed since - // LocalisationCache is stored outside the database and doesn't have lag. - $cache->touchCheckKey( $cache->makeKey( __CLASS__ ), $cache::HOLDOFF_NONE ); - } - - /** - * @since 1.27 - * @return ResourceLoader - */ - protected function getResourceLoader() { - return $this->resourceloader; - } - - /** - * @since 1.27 - * @param string $key Message key - * @param string $lang Language code - * @return string - */ - protected function fetchMessage( $key, $lang ) { - $message = wfMessage( $key )->inLanguage( $lang ); - $value = $message->plain(); - if ( !$message->exists() ) { - $this->logger->warning( 'Failed to find {messageKey} ({lang})', [ - 'messageKey' => $key, - 'lang' => $lang, - ] ); - } - return $value; - } - - /** - * Generate the message blob for a given module in a given language. - * - * @param ResourceLoaderModule $module - * @param string $lang Language code - * @return string JSON blob - */ - private function generateMessageBlob( ResourceLoaderModule $module, $lang ) { - $messages = []; - foreach ( $module->getMessages() as $key ) { - $messages[$key] = $this->fetchMessage( $key, $lang ); - } - - $json = FormatJson::encode( (object)$messages ); - // @codeCoverageIgnoreStart - if ( $json === false ) { - $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [ - 'module' => $module->getName(), - 'lang' => $lang, - ] ); - $json = '{}'; - } - // codeCoverageIgnoreEnd - return $json; - } -} diff --git a/includes/content/WikiTextStructure.php b/includes/content/WikiTextStructure.php index 0e03e72ef6..2f3a6f61d1 100644 --- a/includes/content/WikiTextStructure.php +++ b/includes/content/WikiTextStructure.php @@ -154,7 +154,7 @@ class WikiTextStructure { 'enableSectionEditTokens' => false, 'allowTOC' => false, ] ); - if ( strlen( $text ) == 0 ) { + if ( $text === '' ) { $this->allText = ""; // empty text - nothing to seek here return; diff --git a/includes/historyblob/ConcatenatedGzipHistoryBlob.php b/includes/historyblob/ConcatenatedGzipHistoryBlob.php new file mode 100644 index 0000000000..f6ca2f5a36 --- /dev/null +++ b/includes/historyblob/ConcatenatedGzipHistoryBlob.php @@ -0,0 +1,146 @@ +uncompress(); + $hash = md5( $text ); + if ( !isset( $this->mItems[$hash] ) ) { + $this->mItems[$hash] = $text; + $this->mSize += strlen( $text ); + } + return $hash; + } + + /** + * @param string $hash + * @return array|bool + */ + public function getItem( $hash ) { + $this->uncompress(); + if ( array_key_exists( $hash, $this->mItems ) ) { + return $this->mItems[$hash]; + } else { + return false; + } + } + + /** + * @param string $text + * @return void + */ + public function setText( $text ) { + $this->uncompress(); + $this->mDefaultHash = $this->addItem( $text ); + } + + /** + * @return array|bool + */ + public function getText() { + $this->uncompress(); + return $this->getItem( $this->mDefaultHash ); + } + + /** + * Remove an item + * + * @param string $hash + */ + public function removeItem( $hash ) { + $this->mSize -= strlen( $this->mItems[$hash] ); + unset( $this->mItems[$hash] ); + } + + /** + * Compress the bulk data in the object + */ + public function compress() { + if ( !$this->mCompressed ) { + $this->mItems = gzdeflate( serialize( $this->mItems ) ); + $this->mCompressed = true; + } + } + + /** + * Uncompress bulk data + */ + public function uncompress() { + if ( $this->mCompressed ) { + $this->mItems = unserialize( gzinflate( $this->mItems ) ); + $this->mCompressed = false; + } + } + + /** + * @return array + */ + function __sleep() { + $this->compress(); + return [ 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' ]; + } + + function __wakeup() { + $this->uncompress(); + } + + /** + * Helper function for compression jobs + * Returns true until the object is "full" and ready to be committed + * + * @return bool + */ + public function isHappy() { + return $this->mSize < $this->mMaxSize + && count( $this->mItems ) < $this->mMaxCount; + } +} + +// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found +if ( false ) { + // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the + // class name coerced to lowercase. We can improve efficiency by adding + // autoload entries for the lowercase variants of these classes (T166759). + // The code below is never executed, but it is picked up by the AutoloadGenerator + // parser, which scans for class_alias() calls. + class_alias( ConcatenatedGzipHistoryBlob::class, 'concatenatedgziphistoryblob' ); +} diff --git a/includes/historyblob/DiffHistoryBlob.php b/includes/historyblob/DiffHistoryBlob.php new file mode 100644 index 0000000000..8d92fe5312 --- /dev/null +++ b/includes/historyblob/DiffHistoryBlob.php @@ -0,0 +1,377 @@ + 0 + * diff[i] = { + * { item[map[i]] - Z where i = 0 + */ + public $mDiffs; + + /** @var array The diff map, see above */ + public $mDiffMap; + + /** @var int The key for getText() + */ + public $mDefaultKey; + + /** @var string Compressed storage */ + public $mCompressed; + + /** @var bool True if the object is locked against further writes */ + public $mFrozen = false; + + /** + * @var int The maximum uncompressed size before the object becomes sad + * Should be less than max_allowed_packet + */ + public $mMaxSize = 10000000; + + /** @var int The maximum number of text items before the object becomes sad */ + public $mMaxCount = 100; + + /** Constants from xdiff.h */ + const XDL_BDOP_INS = 1; + const XDL_BDOP_CPY = 2; + const XDL_BDOP_INSB = 3; + + function __construct() { + if ( !function_exists( 'gzdeflate' ) ) { + throw new MWException( "Need zlib support to read or write DiffHistoryBlob\n" ); + } + } + + /** + * @throws MWException + * @param string $text + * @return int + */ + function addItem( $text ) { + if ( $this->mFrozen ) { + throw new MWException( __METHOD__ . ": Cannot add more items after sleep/wakeup" ); + } + + $this->mItems[] = $text; + $this->mSize += strlen( $text ); + $this->mDiffs = null; // later + return count( $this->mItems ) - 1; + } + + /** + * @param string $key + * @return string + */ + function getItem( $key ) { + return $this->mItems[$key]; + } + + /** + * @param string $text + */ + function setText( $text ) { + $this->mDefaultKey = $this->addItem( $text ); + } + + /** + * @return string + */ + function getText() { + return $this->getItem( $this->mDefaultKey ); + } + + /** + * @throws MWException + */ + function compress() { + if ( !function_exists( 'xdiff_string_rabdiff' ) ) { + throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" ); + } + if ( isset( $this->mDiffs ) ) { + // Already compressed + return; + } + if ( $this->mItems === [] ) { + return; + } + + // Create two diff sequences: one for main text and one for small text + $sequences = [ + 'small' => [ + 'tail' => '', + 'diffs' => [], + 'map' => [], + ], + 'main' => [ + 'tail' => '', + 'diffs' => [], + 'map' => [], + ], + ]; + $smallFactor = 0.5; + + $mItemsCount = count( $this->mItems ); + for ( $i = 0; $i < $mItemsCount; $i++ ) { + $text = $this->mItems[$i]; + if ( $i == 0 ) { + $seqName = 'main'; + } else { + $mainTail = $sequences['main']['tail']; + if ( strlen( $text ) < strlen( $mainTail ) * $smallFactor ) { + $seqName = 'small'; + } else { + $seqName = 'main'; + } + } + $seq =& $sequences[$seqName]; + $tail = $seq['tail']; + $diff = $this->diff( $tail, $text ); + $seq['diffs'][] = $diff; + $seq['map'][] = $i; + $seq['tail'] = $text; + } + unset( $seq ); // unlink dangerous alias + + // Knit the sequences together + $tail = ''; + $this->mDiffs = []; + $this->mDiffMap = []; + foreach ( $sequences as $seq ) { + if ( $seq['diffs'] === [] ) { + continue; + } + if ( $tail === '' ) { + $this->mDiffs[] = $seq['diffs'][0]; + } else { + $head = $this->patch( '', $seq['diffs'][0] ); + $this->mDiffs[] = $this->diff( $tail, $head ); + } + $this->mDiffMap[] = $seq['map'][0]; + $diffsCount = count( $seq['diffs'] ); + for ( $i = 1; $i < $diffsCount; $i++ ) { + $this->mDiffs[] = $seq['diffs'][$i]; + $this->mDiffMap[] = $seq['map'][$i]; + } + $tail = $seq['tail']; + } + } + + /** + * @param string $t1 + * @param string $t2 + * @return string + */ + function diff( $t1, $t2 ) { + # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff + # "String is not zero-terminated" + Wikimedia\suppressWarnings(); + $diff = xdiff_string_rabdiff( $t1, $t2 ) . ''; + Wikimedia\restoreWarnings(); + return $diff; + } + + /** + * @param string $base + * @param string $diff + * @return bool|string + */ + function patch( $base, $diff ) { + if ( function_exists( 'xdiff_string_bpatch' ) ) { + Wikimedia\suppressWarnings(); + $text = xdiff_string_bpatch( $base, $diff ) . ''; + Wikimedia\restoreWarnings(); + return $text; + } + + # Pure PHP implementation + + $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) ); + + # Check the checksum if hash extension is available + $ofp = $this->xdiffAdler32( $base ); + if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) { + wfDebug( __METHOD__ . ": incorrect base checksum\n" ); + return false; + } + if ( $header['csize'] != strlen( $base ) ) { + wfDebug( __METHOD__ . ": incorrect base length\n" ); + return false; + } + + $p = 8; + $out = ''; + while ( $p < strlen( $diff ) ) { + $x = unpack( 'Cop', substr( $diff, $p, 1 ) ); + $op = $x['op']; + ++$p; + switch ( $op ) { + case self::XDL_BDOP_INS: + $x = unpack( 'Csize', substr( $diff, $p, 1 ) ); + $p++; + $out .= substr( $diff, $p, $x['size'] ); + $p += $x['size']; + break; + case self::XDL_BDOP_INSB: + $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) ); + $p += 4; + $out .= substr( $diff, $p, $x['csize'] ); + $p += $x['csize']; + break; + case self::XDL_BDOP_CPY: + $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) ); + $p += 8; + $out .= substr( $base, $x['off'], $x['csize'] ); + break; + default: + wfDebug( __METHOD__ . ": invalid op\n" ); + return false; + } + } + return $out; + } + + /** + * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with + * the bytes backwards and initialised with 0 instead of 1. See T36428. + * + * @param string $s + * @return string|bool False if the hash extension is not available + */ + function xdiffAdler32( $s ) { + if ( !function_exists( 'hash' ) ) { + return false; + } + + static $init; + if ( $init === null ) { + $init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02"; + } + + // The real Adler-32 checksum of $init is zero, so it initialises the + // state to zero, as it is at the start of LibXDiff's checksum + // algorithm. Appending the subject string then simulates LibXDiff. + return strrev( hash( 'adler32', $init . $s, true ) ); + } + + function uncompress() { + if ( !$this->mDiffs ) { + return; + } + $tail = ''; + $mDiffsCount = count( $this->mDiffs ); + for ( $diffKey = 0; $diffKey < $mDiffsCount; $diffKey++ ) { + $textKey = $this->mDiffMap[$diffKey]; + $text = $this->patch( $tail, $this->mDiffs[$diffKey] ); + $this->mItems[$textKey] = $text; + $tail = $text; + } + } + + /** + * @return array + */ + function __sleep() { + $this->compress(); + if ( $this->mItems === [] ) { + $info = false; + } else { + // Take forward differences to improve the compression ratio for sequences + $map = ''; + $prev = 0; + foreach ( $this->mDiffMap as $i ) { + if ( $map !== '' ) { + $map .= ','; + } + $map .= $i - $prev; + $prev = $i; + } + $info = [ + 'diffs' => $this->mDiffs, + 'map' => $map + ]; + } + if ( isset( $this->mDefaultKey ) ) { + $info['default'] = $this->mDefaultKey; + } + $this->mCompressed = gzdeflate( serialize( $info ) ); + return [ 'mCompressed' ]; + } + + function __wakeup() { + // addItem() doesn't work if mItems is partially filled from mDiffs + $this->mFrozen = true; + $info = unserialize( gzinflate( $this->mCompressed ) ); + unset( $this->mCompressed ); + + if ( !$info ) { + // Empty object + return; + } + + if ( isset( $info['default'] ) ) { + $this->mDefaultKey = $info['default']; + } + $this->mDiffs = $info['diffs']; + if ( isset( $info['base'] ) ) { + // Old format + $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 ); + array_unshift( $this->mDiffs, + pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) . + $info['base'] ); + } else { + // New format + $map = explode( ',', $info['map'] ); + $cur = 0; + $this->mDiffMap = []; + foreach ( $map as $i ) { + $cur += $i; + $this->mDiffMap[] = $cur; + } + } + $this->uncompress(); + } + + /** + * Helper function for compression jobs + * Returns true until the object is "full" and ready to be committed + * + * @return bool + */ + function isHappy() { + return $this->mSize < $this->mMaxSize + && count( $this->mItems ) < $this->mMaxCount; + } + +} diff --git a/includes/historyblob/HistoryBlob.php b/includes/historyblob/HistoryBlob.php new file mode 100644 index 0000000000..36c7c8f75e --- /dev/null +++ b/includes/historyblob/HistoryBlob.php @@ -0,0 +1,67 @@ +mCurId = $curid; + } + + /** + * Sets the location (cur_id) of the main object to which this object + * points + * + * @param int $id + */ + function setLocation( $id ) { + $this->mCurId = $id; + } + + /** + * @return string|bool + */ + function getText() { + $dbr = wfGetDB( DB_REPLICA ); + $row = $dbr->selectRow( 'cur', [ 'cur_text' ], [ 'cur_id' => $this->mCurId ] ); + if ( !$row ) { + return false; + } + return $row->cur_text; + } +} + +// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found +if ( false ) { + // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the + // class name coerced to lowercase. We can improve efficiency by adding + // autoload entries for the lowercase variants of these classes (T166759). + // The code below is never executed, but it is picked up by the AutoloadGenerator + // parser, which scans for class_alias() calls. + class_alias( HistoryBlobCurStub::class, 'historyblobcurstub' ); +} diff --git a/includes/historyblob/HistoryBlobStub.php b/includes/historyblob/HistoryBlobStub.php new file mode 100644 index 0000000000..4995d3b3f0 --- /dev/null +++ b/includes/historyblob/HistoryBlobStub.php @@ -0,0 +1,150 @@ +mHash = $hash; + } + + /** + * Sets the location (old_id) of the main object to which this object + * points + * @param int $id + */ + function setLocation( $id ) { + $this->mOldId = $id; + } + + /** + * Sets the location (old_id) of the referring object + * @param string $id + */ + function setReferrer( $id ) { + $this->mRef = $id; + } + + /** + * Gets the location of the referring object + * @return string + */ + function getReferrer() { + return $this->mRef; + } + + /** + * @return string|false + */ + function getText() { + if ( isset( self::$blobCache[$this->mOldId] ) ) { + $obj = self::$blobCache[$this->mOldId]; + } else { + $dbr = wfGetDB( DB_REPLICA ); + $row = $dbr->selectRow( + 'text', + [ 'old_flags', 'old_text' ], + [ 'old_id' => $this->mOldId ] + ); + + if ( !$row ) { + return false; + } + + $flags = explode( ',', $row->old_flags ); + if ( in_array( 'external', $flags ) ) { + $url = $row->old_text; + $parts = explode( '://', $url, 2 ); + if ( !isset( $parts[1] ) || $parts[1] == '' ) { + return false; + } + $row->old_text = ExternalStore::fetchFromURL( $url ); + + } + + if ( !in_array( 'object', $flags ) ) { + return false; + } + + if ( in_array( 'gzip', $flags ) ) { + // This shouldn't happen, but a bug in the compress script + // may at times gzip-compress a HistoryBlob object row. + $obj = unserialize( gzinflate( $row->old_text ) ); + } else { + $obj = unserialize( $row->old_text ); + } + + if ( !is_object( $obj ) ) { + // Correct for old double-serialization bug. + $obj = unserialize( $obj ); + } + + // Save this item for reference; if pulling many + // items in a row we'll likely use it again. + $obj->uncompress(); + self::$blobCache = [ $this->mOldId => $obj ]; + } + + return $obj->getItem( $this->mHash ); + } + + /** + * Get the content hash + * + * @return string + */ + function getHash() { + return $this->mHash; + } +} + +// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found +if ( false ) { + // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the + // class name coerced to lowercase. We can improve efficiency by adding + // autoload entries for the lowercase variants of these classes (T166759). + // The code below is never executed, but it is picked up by the AutoloadGenerator + // parser, which scans for class_alias() calls. + class_alias( HistoryBlobStub::class, 'historyblobstub' ); +} diff --git a/includes/http/PhpHttpRequest.php b/includes/http/PhpHttpRequest.php index 30ab181e1d..d2af8c8568 100644 --- a/includes/http/PhpHttpRequest.php +++ b/includes/http/PhpHttpRequest.php @@ -217,7 +217,7 @@ class PhpHttpRequest extends MWHttpRequest { break; } - if ( strlen( $buf ) ) { + if ( $buf !== '' ) { call_user_func( $this->callback, $fh, $buf ); } } diff --git a/includes/installer/Installer.php b/includes/installer/Installer.php index a954008516..ea022bb5f0 100644 --- a/includes/installer/Installer.php +++ b/includes/installer/Installer.php @@ -1607,9 +1607,7 @@ abstract class Installer { } if ( $status->isOk() ) { $this->showMessage( - 'config-install-success', - $this->getVar( 'wgServer' ), - $this->getVar( 'wgScriptPath' ) + 'config-install-db-success' ); $this->setVar( '_InstallDone', true ); } diff --git a/includes/installer/i18n/en.json b/includes/installer/i18n/en.json index c248468c32..66b657bbb6 100644 --- a/includes/installer/i18n/en.json +++ b/includes/installer/i18n/en.json @@ -301,6 +301,7 @@ "config-install-done": "Congratulations!\nYou have installed MediaWiki.\n\nThe installer has generated a LocalSettings.php file.\nIt contains all your configuration.\n\nYou will need to download it and put it in the base of your wiki installation (the same directory as index.php). The download should have started automatically.\n\nIf the download was not offered, or if you cancelled it, you can restart the download by clicking the link below:\n\n$3\n\nNote: If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.\n\nWhen that has been done, you can [$2 enter your wiki].", "config-install-done-path": "Congratulations!\nYou have installed MediaWiki.\n\nThe installer has generated a LocalSettings.php file.\nIt contains all your configuration.\n\nYou will need to download it and put it at $4. The download should have started automatically.\n\nIf the download was not offered, or if you cancelled it, you can restart the download by clicking the link below:\n\n$3\n\nNote: If you do not do this now, this generated configuration file will not be available to you later if you exit the installation without downloading it.\n\nWhen that has been done, you can [$2 enter your wiki].", "config-install-success": "MediaWiki has been successfully installed. You can now visit <$1$2> to view your wiki.\nIf you have questions, check out our frequently asked questions list:\n or use one of the\nsupport forums linked on that page.", + "config-install-db-success": "Database was successfully set up", "config-download-localsettings": "Download LocalSettings.php", "config-help": "help", "config-help-tooltip": "click to expand", diff --git a/includes/installer/i18n/qqq.json b/includes/installer/i18n/qqq.json index bf1976979c..7c86a5a0e0 100644 --- a/includes/installer/i18n/qqq.json +++ b/includes/installer/i18n/qqq.json @@ -322,7 +322,8 @@ "config-install-mainpage-failed": "Used as error message. Parameters:\n* $1 - detailed error message", "config-install-done": "Parameters:\n* $1 is the URL to LocalSettings download\n* $2 is a link to the wiki.\n* $3 is a download link with attached download icon. The config-download-localsettings message will be used as the link text.", "config-install-done-path": "Parameters:\n* $1 is the URL to LocalSettings download\n* $2 is a link to the wiki.\n* $3 is a download link with attached download icon. The config-download-localsettings message will be used as the link text.\n* $4 is the filesystem location of where the LocalSettings.php file should be saved to.", - "config-install-success": "Gives user information that installation was successful. Parameters:\n* $1 - server name\n* $2 - script path", + "config-install-db-success": "Shown after DB is set up. In web installer this is step prior to downloading LocalSettings.php", + "config-install-success": "Gives user information that installation was successful. Only shown in command line installer. Parameters:\n* $1 - server name\n* $2 - script path", "config-download-localsettings": "The link text used in the download link in config-install-done.", "config-help": "This is used in help boxes.\n{{Identical|Help}}", "config-help-tooltip": "Tooltip for the 'help' links ({{msg-mw|config-help}}), to make it clear they'll expand in place rather than open a new page", diff --git a/includes/jobqueue/jobs/ThumbnailRenderJob.php b/includes/jobqueue/jobs/ThumbnailRenderJob.php index 63575ebeca..eb8b1a2780 100644 --- a/includes/jobqueue/jobs/ThumbnailRenderJob.php +++ b/includes/jobqueue/jobs/ThumbnailRenderJob.php @@ -93,7 +93,7 @@ class ThumbnailRenderJob extends Job { if ( $wgUploadThumbnailRenderHttpCustomDomain ) { $parsedUrl = wfParseUrl( $thumbUrl ); - if ( !$parsedUrl || !isset( $parsedUrl['path'] ) || !strlen( $parsedUrl['path'] ) ) { + if ( !isset( $parsedUrl['path'] ) || $parsedUrl['path'] === '' ) { $this->setLastError( __METHOD__ . ": invalid thumb URL: $thumbUrl" ); return false; } diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php index 5669366595..0dd7b57c6d 100644 --- a/includes/libs/objectcache/BagOStuff.php +++ b/includes/libs/objectcache/BagOStuff.php @@ -288,13 +288,10 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface { */ protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { do { - $this->clearLastError(); - $reportDupes = $this->reportDupes; - $this->reportDupes = false; $casToken = null; // passed by reference + // Get the old value and CAS token from cache + $this->clearLastError(); $currentValue = $this->doGet( $key, self::READ_LATEST, $casToken ); - $this->reportDupes = $reportDupes; - if ( $this->getLastError() ) { $this->logger->warning( __METHOD__ . ' failed due to I/O error on get() for {key}.', diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php index 79859dba45..2c74d45916 100644 --- a/includes/libs/objectcache/RedisBagOStuff.php +++ b/includes/libs/objectcache/RedisBagOStuff.php @@ -325,21 +325,29 @@ class RedisBagOStuff extends BagOStuff { return $result; } - public function changeTTL( $key, $expiry = 0, $flags = 0 ) { + public function changeTTL( $key, $exptime = 0, $flags = 0 ) { list( $server, $conn ) = $this->getConnection( $key ); if ( !$conn ) { return false; } - $expiry = $this->convertToRelative( $expiry ); + $relative = $this->expiryIsRelative( $exptime ); try { - $result = $conn->expire( $key, $expiry ); + if ( $exptime == 0 ) { + $result = $conn->persist( $key ); + $this->logRequest( 'persist', $key, $server, $result ); + } elseif ( $relative ) { + $result = $conn->expire( $key, $this->convertToRelative( $exptime ) ); + $this->logRequest( 'expire', $key, $server, $result ); + } else { + $result = $conn->expireAt( $key, $this->convertToExpiry( $exptime ) ); + $this->logRequest( 'expireAt', $key, $server, $result ); + } } catch ( RedisException $e ) { $result = false; $this->handleException( $conn, $e ); } - $this->logRequest( 'expire', $key, $server, $result ); return $result; } diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index ba21156047..9f9cc3cc53 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -194,8 +194,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { /** Tiny positive float to use when using "minTime" to assert an inequality */ const TINY_POSTIVE = 0.000001; - /** Seconds of delay after get() where set() storms are a consideration with 'lockTSE' */ - const SET_DELAY_HIGH_SEC = 0.1; + /** Milliseconds of delay after get() where set() storms are a consideration with 'lockTSE' */ + const SET_DELAY_HIGH_MS = 50; /** Min millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */ const RECENT_SET_LOW_MS = 50; /** Max millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */ @@ -521,31 +521,34 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @param int $ttl Seconds to live. Special values are: * - WANObjectCache::TTL_INDEFINITE: Cache forever (default) * @param array $opts Options map: - * - lag : Seconds of replica DB lag. Typically, this is either the replica DB lag + * - lag: seconds of replica DB lag. Typically, this is either the replica DB lag * before the data was read or, if applicable, the replica DB lag before * the snapshot-isolated transaction the data was read from started. * Use false to indicate that replication is not running. * Default: 0 seconds - * - since : UNIX timestamp of the data in $value. Typically, this is either + * - since: UNIX timestamp of the data in $value. Typically, this is either * the current time the data was read or (if applicable) the time when * the snapshot-isolated transaction the data was read from started. * Default: 0 seconds - * - pending : Whether this data is possibly from an uncommitted write transaction. + * - pending: whether this data is possibly from an uncommitted write transaction. * Generally, other threads should not see values from the future and * they certainly should not see ones that ended up getting rolled back. * Default: false - * - lockTSE : if excessive replication/snapshot lag is detected, then store the value + * - lockTSE: if excessive replication/snapshot lag is detected, then store the value * with this TTL and flag it as stale. This is only useful if the reads for this key * use getWithSetCallback() with "lockTSE" set. Note that if "staleTTL" is set * then it will still add on to this TTL in the excessive lag scenario. * Default: WANObjectCache::TSE_NONE - * - staleTTL : Seconds to keep the key around if it is stale. The get()/getMulti() + * - staleTTL: seconds to keep the key around if it is stale. The get()/getMulti() * methods return such stale values with a $curTTL of 0, and getWithSetCallback() * will call the regeneration callback in such cases, passing in the old value * and its as-of time to the callback. This is useful if adaptiveTTL() is used * on the old value's as-of time when it is verified as still being correct. * Default: WANObjectCache::STALE_TTL_NONE. + * - creating: optimize for the case where the key does not already exist. + * Default: false * @note Options added in 1.28: staleTTL + * @note Options added in 1.33: creating * @return bool Success */ final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) { @@ -553,6 +556,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE; $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE; $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0; + $creating = $opts['creating'] ?? false; $lag = $opts['lag'] ?? 0; // Do not cache potentially uncommitted data as it might get rolled back @@ -618,14 +622,23 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { // Wrap that value with time/TTL/version metadata $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $now ); + $storeTTL = $ttl + $staleTTL; - $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) { - return ( is_string( $cWrapped ) ) - ? false // key is tombstoned; do nothing - : $wrapped; - }; + if ( $creating ) { + $ok = $this->cache->add( self::VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL ); + } else { + $ok = $this->cache->merge( + self::VALUE_KEY_PREFIX . $key, + function ( $cache, $key, $cWrapped ) use ( $wrapped ) { + // A string value means that it is a tombstone; do nothing in that case + return ( is_string( $cWrapped ) ) ? false : $wrapped; + }, + $storeTTL, + 1 // 1 attempt + ); + } - return $this->cache->merge( self::VALUE_KEY_PREFIX . $key, $func, $ttl + $staleTTL, 1 ); + return $ok; } /** @@ -1124,7 +1137,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * stampede is worth avoiding. Note that if the key falls out of cache then concurrent * threads will all run the callback on cache miss until the value is saved in cache. * The only stampede protection in that case is from duplicate cache sets when the - * callback takes longer than WANObjectCache::SET_DELAY_HIGH_SEC seconds; consider + * callback takes longer than WANObjectCache::SET_DELAY_HIGH_MS milliseconds; consider * using "busyValue" if such stampedes are a problem. Note that the higher "lockTSE" is * set, the higher the worst-case staleness of returned values can be. Also note that * this option does not by itself handle the case of the key simply expiring on account @@ -1150,10 +1163,17 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * It is generally preferable to use a class constant when setting this value. * This has no effect unless pcTTL is used. * Default: WANObjectCache::PC_PRIMARY. - * - version: Integer version number. This allows for callers to make breaking changes to - * how values are stored while maintaining compatability and correct cache purges. New - * versions are stored alongside older versions concurrently. Avoid storing class objects - * however, as this reduces compatibility (due to serialization). + * - version: Integer version number. This lets callers make breaking changes to the format + * of cached values without causing problems for sites that use non-instantaneous code + * deployments. Old and new code will recognize incompatible versions and purges from + * both old and new code will been seen by each other. When this method encounters an + * incompatibly versioned value at the provided key, a "variant key" will be used for + * reading from and saving to cache. The variant key is specific to the key and version + * number provided to this method. If the variant key value is older than that of the + * provided key, or the provided key is non-existant, then the variant key will be seen + * as non-existant. Therefore, delete() calls invalidate the provided key's variant keys. + * The "checkKeys" and "touchedCallback" options still apply to variant keys as usual. + * Avoid storing class objects, as this reduces compatibility (due to serialization). * Default: null. * - minAsOf: Reject values if they were generated before this UNIX timestamp. * This is useful if the source of a key is suspected of having possibly changed @@ -1411,6 +1431,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { } } elseif ( !$useMutex || $hasLock ) { if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) { + $setOpts['creating'] = ( $curValue === false ); // Save the value unless a lock-winning thread is already expected to do that $setOpts['lockTSE'] = $lockTSE; $setOpts['staleTTL'] = $staleTTL; @@ -1457,7 +1478,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { // consistent hashing). if ( $lockTSE < 0 || $hasLock ) { return true; // either not a priori hot or thread has the lock - } elseif ( $elapsed <= self::SET_DELAY_HIGH_SEC ) { + } elseif ( $elapsed <= self::SET_DELAY_HIGH_MS * 1e3 ) { return true; // not enough time for threads to pile up } diff --git a/includes/libs/rdbms/database/DatabaseDomain.php b/includes/libs/rdbms/database/DatabaseDomain.php index 8d8285426a..ca57938f2e 100644 --- a/includes/libs/rdbms/database/DatabaseDomain.php +++ b/includes/libs/rdbms/database/DatabaseDomain.php @@ -42,11 +42,11 @@ class DatabaseDomain { * @param string $prefix Table prefix */ public function __construct( $database, $schema, $prefix ) { - if ( $database !== null && ( !is_string( $database ) || !strlen( $database ) ) ) { + if ( $database !== null && ( !is_string( $database ) || $database === '' ) ) { throw new InvalidArgumentException( 'Database must be null or a non-empty string.' ); } $this->database = $database; - if ( $schema !== null && ( !is_string( $schema ) || !strlen( $schema ) ) ) { + if ( $schema !== null && ( !is_string( $schema ) || $schema === '' ) ) { throw new InvalidArgumentException( 'Schema must be null or a non-empty string.' ); } $this->schema = $schema; diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index eac9baed0e..b4440d6dfa 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -114,6 +114,11 @@ interface IDatabase { */ const QUERY_PSEUDO_PERMANENT = 2; + /** @var bool Parameter to unionQueries() for UNION ALL */ + const UNION_ALL = true; + /** @var bool Parameter to unionQueries() for UNION DISTINCT */ + const UNION_DISTINCT = false; + /** * A string describing the current software version, and possibly * other details in a user-friendly way. Will be listed on Special:Version, etc. @@ -1384,7 +1389,7 @@ interface IDatabase { * This is used for providing overload point for other DB abstractions * not compatible with the MySQL syntax. * @param array $sqls SQL statements to combine - * @param bool $all Use UNION ALL + * @param bool $all Either IDatabase::UNION_ALL or IDatabase::UNION_DISTINCT * @return string SQL fragment */ public function unionQueries( $sqls, $all ); diff --git a/includes/preferences/DefaultPreferencesFactory.php b/includes/preferences/DefaultPreferencesFactory.php index b2f5342ad2..a42726f7b4 100644 --- a/includes/preferences/DefaultPreferencesFactory.php +++ b/includes/preferences/DefaultPreferencesFactory.php @@ -870,17 +870,6 @@ class DefaultPreferencesFactory implements PreferencesFactory { 'section' => 'rendering/advancedrendering', 'label-message' => 'tog-showrollbackconfirmation', ]; - - /** - * FIXME - * Remove temporary help text and references to DisableRollbackConfirmationFeature - * after release of rollback feature. See T199534 - */ - if ( MediaWikiServices::getInstance() - ->getMainConfig()->get( 'DisableRollbackConfirmationFeature' ) ) { - $defaultPreferences['showrollbackconfirmation'] - ['help-message'] = 'tog-showrollbackconfirmation-prerelease-warning'; - } } } diff --git a/includes/resourceloader/MessageBlobStore.php b/includes/resourceloader/MessageBlobStore.php new file mode 100644 index 0000000000..ceb51f2d3f --- /dev/null +++ b/includes/resourceloader/MessageBlobStore.php @@ -0,0 +1,240 @@ +resourceloader = $rl; + $this->logger = $logger ?: new NullLogger(); + $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + } + + /** + * @since 1.27 + * @param LoggerInterface $logger + */ + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * Get the message blob for a module + * + * @since 1.27 + * @param ResourceLoaderModule $module + * @param string $lang Language code + * @return string JSON + */ + public function getBlob( ResourceLoaderModule $module, $lang ) { + $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang ); + return $blobs[$module->getName()]; + } + + /** + * Get the message blobs for a set of modules + * + * @since 1.27 + * @param ResourceLoaderModule[] $modules Array of module objects keyed by name + * @param string $lang Language code + * @return array An array mapping module names to message blobs + */ + public function getBlobs( array $modules, $lang ) { + // Each cache key for a message blob by module name and language code also has a generic + // check key without language code. This is used to invalidate any and all language subkeys + // that exist for a module from the updateMessage() method. + $cache = $this->wanCache; + $checkKeys = [ + // Global check key, see clear() + $cache->makeKey( __CLASS__ ) + ]; + $cacheKeys = []; + foreach ( $modules as $name => $module ) { + $cacheKey = $this->makeCacheKey( $module, $lang ); + $cacheKeys[$name] = $cacheKey; + // Per-module check key, see updateMessage() + $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name ); + } + $curTTLs = []; + $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys ); + + $blobs = []; + foreach ( $modules as $name => $module ) { + $key = $cacheKeys[$name]; + if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) { + $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang ); + } else { + // Use unexpired cache + $blobs[$name] = $result[$key]; + } + } + return $blobs; + } + + /** + * @deprecated since 1.27 Use getBlobs() instead + * @return array + */ + public function get( ResourceLoader $resourceLoader, $modules, $lang ) { + return $this->getBlobs( $modules, $lang ); + } + + /** + * @since 1.27 + * @param ResourceLoaderModule $module + * @param string $lang + * @return string Cache key + */ + private function makeCacheKey( ResourceLoaderModule $module, $lang ) { + $messages = array_values( array_unique( $module->getMessages() ) ); + sort( $messages ); + return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang, + md5( json_encode( $messages ) ) + ); + } + + /** + * @since 1.27 + * @param string $cacheKey + * @param ResourceLoaderModule $module + * @param string $lang + * @return string JSON blob + */ + protected function recacheMessageBlob( $cacheKey, ResourceLoaderModule $module, $lang ) { + $blob = $this->generateMessageBlob( $module, $lang ); + $cache = $this->wanCache; + $cache->set( $cacheKey, $blob, + // Add part of a day to TTL to avoid all modules expiring at once + $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ), + Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ) + ); + return $blob; + } + + /** + * Invalidate cache keys for modules using this message key. + * Called by MessageCache when a message has changed. + * + * @param string $key Message key + */ + public function updateMessage( $key ) { + $moduleNames = $this->getResourceLoader()->getModulesByMessage( $key ); + foreach ( $moduleNames as $moduleName ) { + // Uses a holdoff to account for database replica DB lag (for MessageCache) + $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) ); + } + } + + /** + * Invalidate cache keys for all known modules. + * Called by LocalisationCache after cache is regenerated. + */ + public function clear() { + $cache = $this->wanCache; + // Disable holdoff because this invalidates all modules and also not needed since + // LocalisationCache is stored outside the database and doesn't have lag. + $cache->touchCheckKey( $cache->makeKey( __CLASS__ ), $cache::HOLDOFF_NONE ); + } + + /** + * @since 1.27 + * @return ResourceLoader + */ + protected function getResourceLoader() { + return $this->resourceloader; + } + + /** + * @since 1.27 + * @param string $key Message key + * @param string $lang Language code + * @return string + */ + protected function fetchMessage( $key, $lang ) { + $message = wfMessage( $key )->inLanguage( $lang ); + $value = $message->plain(); + if ( !$message->exists() ) { + $this->logger->warning( 'Failed to find {messageKey} ({lang})', [ + 'messageKey' => $key, + 'lang' => $lang, + ] ); + } + return $value; + } + + /** + * Generate the message blob for a given module in a given language. + * + * @param ResourceLoaderModule $module + * @param string $lang Language code + * @return string JSON blob + */ + private function generateMessageBlob( ResourceLoaderModule $module, $lang ) { + $messages = []; + foreach ( $module->getMessages() as $key ) { + $messages[$key] = $this->fetchMessage( $key, $lang ); + } + + $json = FormatJson::encode( (object)$messages ); + // @codeCoverageIgnoreStart + if ( $json === false ) { + $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [ + 'module' => $module->getName(), + 'lang' => $lang, + ] ); + $json = '{}'; + } + // codeCoverageIgnoreEnd + return $json; + } +} diff --git a/includes/search/PrefixSearch.php b/includes/search/PrefixSearch.php new file mode 100644 index 0000000000..aa429b269d --- /dev/null +++ b/includes/search/PrefixSearch.php @@ -0,0 +1,327 @@ +search( $search, $limit, $namespaces, $offset ); + } + + /** + * Do a prefix search of titles and return a list of matching page names. + * + * @param string $search + * @param int $limit + * @param array $namespaces Used if query is not explicitly prefixed + * @param int $offset How many results to offset from the beginning + * @return array Array of strings or Title objects + */ + public function search( $search, $limit, $namespaces = [], $offset = 0 ) { + $search = trim( $search ); + if ( $search == '' ) { + return []; // Return empty result + } + + $hasNamespace = SearchEngine::parseNamespacePrefixes( $search, false, true ); + if ( $hasNamespace !== false ) { + list( $search, $namespaces ) = $hasNamespace; + } + + return $this->searchBackend( $namespaces, $search, $limit, $offset ); + } + + /** + * Do a prefix search for all possible variants of the prefix + * @param string $search + * @param int $limit + * @param array $namespaces + * @param int $offset How many results to offset from the beginning + * + * @return array + */ + public function searchWithVariants( $search, $limit, array $namespaces, $offset = 0 ) { + $searches = $this->search( $search, $limit, $namespaces, $offset ); + + // if the content language has variants, try to retrieve fallback results + $fallbackLimit = $limit - count( $searches ); + if ( $fallbackLimit > 0 ) { + $fallbackSearches = MediaWikiServices::getInstance()->getContentLanguage()-> + autoConvertToAllVariants( $search ); + $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] ); + + foreach ( $fallbackSearches as $fbs ) { + $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces ); + $searches = array_merge( $searches, $fallbackSearchResult ); + $fallbackLimit -= count( $fallbackSearchResult ); + + if ( $fallbackLimit == 0 ) { + break; + } + } + } + return $searches; + } + + /** + * When implemented in a descendant class, receives an array of Title objects and returns + * either an unmodified array or an array of strings corresponding to titles passed to it. + * + * @param array $titles + * @return array + */ + abstract protected function titles( array $titles ); + + /** + * When implemented in a descendant class, receives an array of titles as strings and returns + * either an unmodified array or an array of Title objects corresponding to strings received. + * + * @param array $strings + * + * @return array + */ + abstract protected function strings( array $strings ); + + /** + * Do a prefix search of titles and return a list of matching page names. + * @param array $namespaces + * @param string $search + * @param int $limit + * @param int $offset How many results to offset from the beginning + * @return array Array of strings + */ + protected function searchBackend( $namespaces, $search, $limit, $offset ) { + if ( count( $namespaces ) == 1 ) { + $ns = $namespaces[0]; + if ( $ns == NS_MEDIA ) { + $namespaces = [ NS_FILE ]; + } elseif ( $ns == NS_SPECIAL ) { + return $this->titles( $this->specialSearch( $search, $limit, $offset ) ); + } + } + $srchres = []; + if ( Hooks::run( + 'PrefixSearchBackend', + [ $namespaces, $search, $limit, &$srchres, $offset ] + ) ) { + return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) ); + } + return $this->strings( + $this->handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) ); + } + + private function handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) { + if ( $offset === 0 ) { + // Only perform exact db match if offset === 0 + // This is still far from perfect but at least we avoid returning the + // same title afain and again when the user is scrolling with a query + // that matches a title in the db. + $rescorer = new SearchExactMatchRescorer(); + $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit ); + } + return $srchres; + } + + /** + * Prefix search special-case for Special: namespace. + * + * @param string $search Term + * @param int $limit Max number of items to return + * @param int $offset Number of items to offset + * @return array + */ + protected function specialSearch( $search, $limit, $offset ) { + $searchParts = explode( '/', $search, 2 ); + $searchKey = $searchParts[0]; + $subpageSearch = $searchParts[1] ?? null; + + // Handle subpage search separately. + $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); + if ( $subpageSearch !== null ) { + // Try matching the full search string as a page name + $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey ); + if ( !$specialTitle ) { + return []; + } + $special = $spFactory->getPage( $specialTitle->getText() ); + if ( $special ) { + $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset ); + return array_map( function ( $sub ) use ( $specialTitle ) { + return $specialTitle->getSubpage( $sub ); + }, $subpages ); + } else { + return []; + } + } + + # normalize searchKey, so aliases with spaces can be found - T27675 + $contLang = MediaWikiServices::getInstance()->getContentLanguage(); + $searchKey = str_replace( ' ', '_', $searchKey ); + $searchKey = $contLang->caseFold( $searchKey ); + + // Unlike SpecialPage itself, we want the canonical forms of both + // canonical and alias title forms... + $keys = []; + foreach ( $spFactory->getNames() as $page ) { + $keys[$contLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ]; + } + + foreach ( $contLang->getSpecialPageAliases() as $page => $aliases ) { + if ( !in_array( $page, $spFactory->getNames() ) ) {# T22885 + continue; + } + + foreach ( $aliases as $key => $alias ) { + $keys[$contLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ]; + } + } + ksort( $keys ); + + $matches = []; + foreach ( $keys as $pageKey => $page ) { + if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) { + // T29671: Don't use SpecialPage::getTitleFor() here because it + // localizes its input leading to searches for e.g. Special:All + // returning Spezial:MediaWiki-Systemnachrichten and returning + // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de' + $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] ); + + if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) { + // We have enough items in primary rank, no use to continue + break; + } + } + + } + + // Ensure keys are in order + ksort( $matches ); + // Flatten the array + $matches = array_reduce( $matches, 'array_merge', [] ); + + return array_slice( $matches, $offset, $limit ); + } + + /** + * Unless overridden by PrefixSearchBackend hook... + * This is case-sensitive (First character may + * be automatically capitalized by Title::secureAndSpit() + * later on depending on $wgCapitalLinks) + * + * @param array|null $namespaces Namespaces to search in + * @param string $search Term + * @param int $limit Max number of items to return + * @param int $offset Number of items to skip + * @return Title[] Array of Title objects + */ + public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) { + // Backwards compatability with old code. Default to NS_MAIN if no namespaces provided. + if ( $namespaces === null ) { + $namespaces = []; + } + if ( !$namespaces ) { + $namespaces[] = NS_MAIN; + } + + // Construct suitable prefix for each namespace. They differ in cases where + // some namespaces always capitalize and some don't. + $prefixes = []; + foreach ( $namespaces as $namespace ) { + // For now, if special is included, ignore the other namespaces + if ( $namespace == NS_SPECIAL ) { + return $this->specialSearch( $search, $limit, $offset ); + } + + $title = Title::makeTitleSafe( $namespace, $search ); + // Why does the prefix default to empty? + $prefix = $title ? $title->getDBkey() : ''; + $prefixes[$prefix][] = $namespace; + } + + $dbr = wfGetDB( DB_REPLICA ); + // Often there is only one prefix that applies to all requested namespaces, + // but sometimes there are two if some namespaces do not always capitalize. + $conds = []; + foreach ( $prefixes as $prefix => $namespaces ) { + $condition = [ + 'page_namespace' => $namespaces, + 'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() ), + ]; + $conds[] = $dbr->makeList( $condition, LIST_AND ); + } + + $table = 'page'; + $fields = [ 'page_id', 'page_namespace', 'page_title' ]; + $conds = $dbr->makeList( $conds, LIST_OR ); + $options = [ + 'LIMIT' => $limit, + 'ORDER BY' => [ 'page_title', 'page_namespace' ], + 'OFFSET' => $offset + ]; + + $res = $dbr->select( $table, $fields, $conds, __METHOD__, $options ); + + return iterator_to_array( TitleArray::newFromResult( $res ) ); + } + + /** + * Validate an array of numerical namespace indexes + * + * @param array $namespaces + * @return array (default: contains only NS_MAIN) + */ + protected function validateNamespaces( $namespaces ) { + // We will look at each given namespace against content language namespaces + $validNamespaces = MediaWikiServices::getInstance()->getContentLanguage()->getNamespaces(); + if ( is_array( $namespaces ) && count( $namespaces ) > 0 ) { + $valid = []; + foreach ( $namespaces as $ns ) { + if ( is_numeric( $ns ) && array_key_exists( $ns, $validNamespaces ) ) { + $valid[] = $ns; + } + } + if ( count( $valid ) > 0 ) { + return $valid; + } + } + + return [ NS_MAIN ]; + } +} diff --git a/includes/search/StringPrefixSearch.php b/includes/search/StringPrefixSearch.php new file mode 100644 index 0000000000..517518e7fa --- /dev/null +++ b/includes/search/StringPrefixSearch.php @@ -0,0 +1,39 @@ +getPrefixedText(); + }, $titles ); + } + + protected function strings( array $strings ) { + return $strings; + } +} diff --git a/includes/search/TitlePrefixSearch.php b/includes/search/TitlePrefixSearch.php new file mode 100644 index 0000000000..a548dbf920 --- /dev/null +++ b/includes/search/TitlePrefixSearch.php @@ -0,0 +1,41 @@ +setCaller( __METHOD__ ); + $lb->execute(); + return $titles; + } +} diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index a8271acf10..82bc84dc59 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -616,9 +616,7 @@ abstract class ChangesListSpecialPage extends SpecialPage { } /** - * Main execution point - * - * @param string $subpage + * @param string|null $subpage */ public function execute( $subpage ) { $this->rcSubpage = $subpage; diff --git a/includes/specialpage/FormSpecialPage.php b/includes/specialpage/FormSpecialPage.php index d1c6aea294..939460f78e 100644 --- a/includes/specialpage/FormSpecialPage.php +++ b/includes/specialpage/FormSpecialPage.php @@ -31,7 +31,7 @@ abstract class FormSpecialPage extends SpecialPage { /** * The sub-page of the special page. - * @var string + * @var string|null */ protected $par = null; @@ -166,7 +166,7 @@ abstract class FormSpecialPage extends SpecialPage { /** * Basic SpecialPage workflow: get a form, send it to the user; get some data back, * - * @param string $par Subpage string if one was specified + * @param string|null $par Subpage string if one was specified */ public function execute( $par ) { $this->setParameter( $par ); @@ -188,7 +188,7 @@ abstract class FormSpecialPage extends SpecialPage { /** * Maybe do something interesting with the subpage parameter - * @param string $par + * @param string|null $par */ protected function setParameter( $par ) { $this->par = $par; diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php index b88479ade7..f0cb7e51c6 100644 --- a/includes/specialpage/QueryPage.php +++ b/includes/specialpage/QueryPage.php @@ -578,7 +578,7 @@ abstract class QueryPage extends SpecialPage { /** * This is the actual workhorse. It does everything needed to make a * real, honest-to-gosh query page. - * @param string $par + * @param string|null $par */ public function execute( $par ) { $user = $this->getUser(); diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index 0c709af761..f52a6f35c6 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -33,9 +33,7 @@ class SpecialActiveUsers extends SpecialPage { } /** - * Show the special page - * - * @param string $par Parameter passed to the page or null + * @param string|null $par Parameter passed to the page or null */ public function execute( $par ) { $out = $this->getOutput(); diff --git a/includes/specials/SpecialAllMessages.php b/includes/specials/SpecialAllMessages.php index 2482d740b9..878440db39 100644 --- a/includes/specials/SpecialAllMessages.php +++ b/includes/specials/SpecialAllMessages.php @@ -35,9 +35,7 @@ class SpecialAllMessages extends SpecialPage { } /** - * Show the special page - * - * @param string $par Parameter passed to the page or null + * @param string|null $par Parameter passed to the page or null */ public function execute( $par ) { $out = $this->getOutput(); diff --git a/includes/specials/SpecialAutoblockList.php b/includes/specials/SpecialAutoblockList.php index cab5a2ef49..34c3371bf2 100644 --- a/includes/specials/SpecialAutoblockList.php +++ b/includes/specials/SpecialAutoblockList.php @@ -34,9 +34,7 @@ class SpecialAutoblockList extends SpecialPage { } /** - * Main execution point - * - * @param string $par Title fragment + * @param string|null $par Title fragment */ public function execute( $par ) { $this->setHeaders(); diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index 186e5ad741..fd27fdc5fa 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -36,9 +36,7 @@ class SpecialBlockList extends SpecialPage { } /** - * Main execution point - * - * @param string $par Title fragment + * @param string|null $par Title fragment */ public function execute( $par ) { $this->setHeaders(); diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php index 2fe38ed215..ea9ddafed1 100644 --- a/includes/specials/SpecialBooksources.php +++ b/includes/specials/SpecialBooksources.php @@ -36,9 +36,7 @@ class SpecialBookSources extends SpecialPage { } /** - * Show the special page - * - * @param string $isbn ISBN passed as a subpage parameter + * @param string|null $isbn ISBN passed as a subpage parameter */ public function execute( $isbn ) { $out = $this->getOutput(); diff --git a/includes/specials/SpecialComparePages.php b/includes/specials/SpecialComparePages.php index d6fb10f6fd..9d1b79e74b 100644 --- a/includes/specials/SpecialComparePages.php +++ b/includes/specials/SpecialComparePages.php @@ -43,7 +43,7 @@ class SpecialComparePages extends SpecialPage { /** * Show a form for filtering namespace and username * - * @param string $par + * @param string|null $par * @return string */ public function execute( $par ) { diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index c0303b255c..055a6e2609 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -244,9 +244,9 @@ class SpecialContributions extends IncludableSpecialPage { $output = $pager->getBody(); if ( !$this->including() ) { - $output = '

' . $pager->getNavigationBar() . '

' . + $output = $pager->getNavigationBar() . $output . - '

' . $pager->getNavigationBar() . '

'; + $pager->getNavigationBar(); } $out->addHTML( $output ); } @@ -313,7 +313,11 @@ class SpecialContributions extends IncludableSpecialPage { $links = ''; if ( $talk ) { $tools = self::getUserLinks( $this, $userObj ); - $links = $this->getLanguage()->pipeList( $tools ); + $links = Html::openElement( 'span', [ 'class' => 'mw-changeslist-links' ] ); + foreach ( $tools as $tool ) { + $links .= Html::rawElement( 'span', [], $tool ) . ' '; + } + $links = trim( $links ) . Html::closeElement( 'span' ); // Show a note if the user is blocked and display the last block log entry. // Do not expose the autoblocks, since that may lead to a leak of accounts' IPs, @@ -353,7 +357,10 @@ class SpecialContributions extends IncludableSpecialPage { } } - return $this->msg( 'contribsub2' )->rawParams( $user, $links )->params( $userObj->getName() ); + return Html::rawElement( 'div', [ 'class' => 'mw-contributions-user-tools' ], + $this->msg( 'contributions-subtitle' )->rawParams( $user )->params( $userObj->getName() ) + . ' ' . $links + ); } /** diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php index 2c35815dba..7aef4aef28 100644 --- a/includes/specials/SpecialListusers.php +++ b/includes/specials/SpecialListusers.php @@ -35,9 +35,7 @@ class SpecialListUsers extends IncludableSpecialPage { } /** - * Show the special page - * - * @param string $par (optional) A group to list users from + * @param string|null $par (optional) A group to list users from */ public function execute( $par ) { $this->setHeaders(); diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 1f81cf0a6d..1b8ba85c70 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -39,6 +39,9 @@ class SpecialNewpages extends IncludableSpecialPage { parent::__construct( 'Newpages' ); } + /** + * @param string|null $par + */ protected function setup( $par ) { $opts = new FormOptions(); $this->opts = $opts; // bind @@ -70,6 +73,9 @@ class SpecialNewpages extends IncludableSpecialPage { $opts->validateIntBounds( 'limit', 0, 5000 ); } + /** + * @param string $par + */ protected function parseParams( $par ) { $bits = preg_split( '/\s*,\s*/', trim( $par ) ); foreach ( $bits as $bit ) { @@ -115,7 +121,7 @@ class SpecialNewpages extends IncludableSpecialPage { /** * Show a form for filtering namespace and username * - * @param string $par + * @param string|null $par */ public function execute( $par ) { $out = $this->getOutput(); diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 46b5520915..c8f65c1bcb 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -136,9 +136,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage { } /** - * Main execution point - * - * @param string $subpage + * @param string|null $subpage */ public function execute( $subpage ) { // Backwards-compatibility: redirect to new feed URLs diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index 62c867b44c..88656546e3 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -224,7 +224,8 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges { $sql = $subsql[0]; } else { // need to resort and relimit after union - $sql = $dbr->unionQueries( $subsql, false ) . ' ORDER BY rc_timestamp DESC'; + $sql = $dbr->unionQueries( $subsql, $dbr::UNION_DISTINCT ) . + ' ORDER BY rc_timestamp DESC'; $sql = $dbr->limitResult( $sql, $limit, false ); } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index e6d06329ad..171566b6c7 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -102,7 +102,7 @@ class SpecialSearch extends SpecialPage { /** * Entry point * - * @param string $par + * @param string|null $par */ public function execute( $par ) { $request = $this->getRequest(); @@ -115,7 +115,7 @@ class SpecialSearch extends SpecialPage { // parameter, but also as part of the primary url. This can have PII implications // in releasing page view data. As such issue a 301 redirect to the correct // URL. - if ( strlen( $par ) && !strlen( $term ) ) { + if ( $par !== null && $par !== '' && $term === '' ) { $query = $request->getValues(); unset( $query['title'] ); // Strip underscores from title parameter; most of the time we'll want diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php index dbb1481c49..5a1b8fbf6d 100644 --- a/includes/specials/SpecialUpload.php +++ b/includes/specials/SpecialUpload.php @@ -146,8 +146,7 @@ class SpecialUpload extends SpecialPage { } /** - * Special page entry point - * @param string $par + * @param string|null $par * @throws ErrorPageError * @throws Exception * @throws FatalError diff --git a/includes/specials/SpecialUploadStash.php b/includes/specials/SpecialUploadStash.php index 4d0c20c76a..fe55d9427f 100644 --- a/includes/specials/SpecialUploadStash.php +++ b/includes/specials/SpecialUploadStash.php @@ -60,7 +60,7 @@ class SpecialUploadStash extends UnlistedSpecialPage { /** * Execute page -- can output a file directly or show a listing of them. * - * @param string $subPage Subpage, e.g. in + * @param string|null $subPage Subpage, e.g. in * https://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part * @return bool Success */ diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index 382ba2fd2b..626fc48c00 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -154,6 +154,19 @@ class ContribsPager extends RangeChronologicalPager { return $query; } + /** + * Wrap the navigation bar in a p element with identifying class. + * In future we may want to change the `p` tag to a `div` and upstream + * this to the parent class. + * + * @return string HTML + */ + function getNavigationBar() { + return Html::rawElement( 'p', [ 'class' => 'mw-pager-navigation-bar' ], + parent::getNavigationBar() + ); + } + /** * This method basically executes the exact same code as the parent class, though with * a hook added, to allow extensions to add additional queries. diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 05bbf3cbfa..65b956e3f2 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -47,7 +47,6 @@ "tog-useeditwarning": "Warn me when I leave an edit page with unsaved changes", "tog-prefershttps": "Always use a secure connection while logged in", "tog-showrollbackconfirmation": "Show a confirmation prompt when clicking on a rollback link", - "tog-showrollbackconfirmation-prerelease-warning": "Please note: This feature is not available yet. If you set this preference now, your choice will be remembered [https://meta.wikimedia.org/wiki/WMDE_Technical_Wishes/Rollback#Status when the feature is released].", "underline-always": "Always", "underline-never": "Never", "underline-default": "Skin or browser default", @@ -2537,6 +2536,7 @@ "mycontris": "Contributions", "anoncontribs": "Contributions", "contribsub2": "For {{GENDER:$3|$1}} ($2)", + "contributions-subtitle": "For {{GENDER:$3|$1}}", "contributions-userdoesnotexist": "User account \"$1\" is not registered.", "negative-namespace-not-supported": "Namespaces with negative values are not supported.", "nocontribs": "No changes were found matching these criteria.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 7b0533a739..f37b5c7250 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -253,7 +253,6 @@ "tog-useeditwarning": "Used as label for the checkbox in [[Special:Preferences#mw-prefsection-editing|Special:Preferences]].", "tog-prefershttps": "Toggle option used in [[Special:Preferences]] that indicates if the user wants to use a secure connection when logged in", "tog-showrollbackconfirmation": "Toggle option used in [[Special:Preferences]] to enable/disable rollback confirmation prompt. Should be visible only to users with rollback rights.", - "tog-showrollbackconfirmation-prerelease-warning": "Notice for wikis where the option can be set before the feature is enabled.\n\nNote: This notice is temporary and will only appear before the rollback confirmation feature is released.", "underline-always": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"always underline links\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Always}}", "underline-never": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"never underline links\", there are also options {{msg-mw|Underline-always}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Never}}", "underline-default": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"underline links as in your user skin or your browser\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-always}}.\n\n{{Gender}}\n{{Identical|Browser default}}", @@ -2743,6 +2742,7 @@ "mycontris": "In the personal urls page section - right upper corner.\n\nSee also:\n* {{msg-mw|Mycontris}}\n* {{msg-mw|Accesskey-pt-mycontris}}\n* {{msg-mw|Tooltip-pt-mycontris}}\n{{Identical|Contribution}}", "anoncontribs": "Same as {{msg-mw|mycontris}} but used for non-logged-in users.\n\nSee also:\n* {{msg-mw|Accesskey-pt-anoncontribs}}\n* {{msg-mw|Tooltip-pt-anoncontribs}}\n{{Identical|Contribution}}", "contribsub2": "Contributions for \"user\" (links). Parameters:\n* $1 is an IP address or a username, with a link which points to the user page (if registered user).\n* $2 is list of tool links. The list contains a link which has text {{msg-mw|Sp-contributions-talk}}.\n* $3 is a plain text username used for GENDER.\n{{Identical|For $1}}", + "contributions-subtitle": "Successor to {{msg-mw|contribssub2}}. Contributions for \"user\". Parameters:\n* $1 is an IP address or a username, with a link which points to the user page (if registered user).\n", "contributions-userdoesnotexist": "This message is used in [[Special:Contributions]]. It is used to tell the user that the name he searched for doesn't exist.\n\nParameters:\n* $1 - a username\n{{Identical|Userdoesnotexist}}", "negative-namespace-not-supported": "This message is used in [[Special:Contributions]] to tell users that use namespaces with negative value. It not supported as associated namespace(s) doesn't exist.", "nocontribs": "Used in [[Special:Contributions]] and [[Special:DeletedContributions]].\n\nSee examples: [[Special:Contributions/x]] and [[Special:DeletedContributions/x]].\n\nParameters:\n* $1 - (Unused) the user name", diff --git a/languages/i18n/sc.json b/languages/i18n/sc.json index dee385943e..ae331b9902 100644 --- a/languages/i18n/sc.json +++ b/languages/i18n/sc.json @@ -10,7 +10,8 @@ "Via maxima", "Uharteko", "Taxandru", - "Macofe" + "Macofe", + "Zoranzoki21" ] }, "tog-underline": "Sutalìnia sos ligòngios", @@ -225,7 +226,7 @@ "disclaimerpage": "Project:Avertèntzias generales", "edithelp": "Agiudu pro su càmbiu o s'iscritura", "helppage-top-gethelp": "Agiudu", - "mainpage": "Pàgina Base", + "mainpage": "Pàgina printzipale", "mainpage-description": "Pàgina printzipale", "policy-url": "Project:Polìticas", "portal": "Portale comunidade", diff --git a/languages/messages/MessagesNqo.php b/languages/messages/MessagesNqo.php index 855e0144db..4de60f3f16 100644 --- a/languages/messages/MessagesNqo.php +++ b/languages/messages/MessagesNqo.php @@ -10,6 +10,25 @@ $rtl = true; +$namespaceNames = [ + NS_MEDIA => 'ߟߊߛߋߢߊߥߙߍ', + NS_SPECIAL => 'ߞߙߍߞߙߍߣߍ߲', + NS_TALK => 'ߢߊߝߐߞߣߍ', + NS_USER => 'ߟߊߓߊ߯ߙߟߊ', + NS_USER_TALK => 'ߟߊߓߊ߯ߙߟߊ ߟߊ߫ ߢߊߝߐߞߣߍ', + NS_PROJECT_TALK => '$1 ߢߊߝߐߞߣߍ', + NS_FILE => 'ߞߐߕߐ߮', + NS_FILE_TALK => 'ߞߐߕߐ߮ ߢߊߝߐߞߣߍ', + NS_MEDIAWIKI => 'ߡߘߌߦߊߥߞߌ', + NS_MEDIAWIKI_TALK => 'ߡߘߌߦߊߥߞߌ ߢߊߝߐߞߣߍ', + NS_TEMPLATE => 'ߞߙߊߞߏ', + NS_TEMPLATE_TALK => 'ߞߙߊߞߏ ߢߊߝߐߞߣߍ', + NS_HELP => 'ߡߊ߬ߘߍ߬ߡߍ߲߬ߠߌ߲', + NS_HELP_TALK => 'ߡߊ߬ߘߍ߬ߡߍ߲߬ߠߌ߲ ߢߊߝߐߞߣߍ', + NS_CATEGORY => 'ߦߌߟߡߊ', + NS_CATEGORY_TALK => 'ߦߌߟߡߊ ߢߊߝߐߞߣߍ', +]; + $digitTransformTable = [ '0' => '߀', # U+07C0 '1' => '߁', # U+07C1 diff --git a/maintenance/findOrphanedFiles.php b/maintenance/findOrphanedFiles.php index 57e04e0ea1..e81e1971ec 100644 --- a/maintenance/findOrphanedFiles.php +++ b/maintenance/findOrphanedFiles.php @@ -117,7 +117,7 @@ class FindOrphanedFiles extends Maintenance { $oiWheres ? $dbr->makeList( $oiWheres, LIST_OR ) : '1=0' ) ], - true // UNION ALL (performance) + $dbr::UNION_ALL ), __METHOD__ ); diff --git a/maintenance/install.php b/maintenance/install.php index 3395458d9f..1dd1909497 100644 --- a/maintenance/install.php +++ b/maintenance/install.php @@ -129,6 +129,11 @@ class CommandLineInstaller extends Maintenance { $installer->execute(); $installer->writeConfigurationFile( $this->getOption( 'confpath', $IP ) ); } + $installer->showMessage( + 'config-install-success', + $installer->getVar( 'wgServer' ), + $installer->getVar( 'wgScriptPath' ) + ); } private function setDbPassOption() { diff --git a/maintenance/mediawiki.Title/generateJsToUpperCaseList.js b/maintenance/mediawiki.Title/generateJsToUpperCaseList.js new file mode 100644 index 0000000000..fd742f6502 --- /dev/null +++ b/maintenance/mediawiki.Title/generateJsToUpperCaseList.js @@ -0,0 +1,8 @@ +/* eslint-env node, es6 */ +var i, chars = []; + +for ( i = 0; i < 65536; i++ ) { + chars.push( String.fromCharCode( i ).toUpperCase() ); +} +// eslint-disable-next-line no-console +console.log( JSON.stringify( chars ) ); diff --git a/maintenance/mediawiki.Title/generatePhpCharToUpperMappings.php b/maintenance/mediawiki.Title/generatePhpCharToUpperMappings.php new file mode 100755 index 0000000000..a04958c642 --- /dev/null +++ b/maintenance/mediawiki.Title/generatePhpCharToUpperMappings.php @@ -0,0 +1,34 @@ +#!/usr/bin/env php += 0xd800 && $i <= 0xdfff ) { + // Skip surrogate pairs + continue; + } + $char = mb_convert_encoding( '&#' . $i . ';', 'UTF-8', 'HTML-ENTITIES' ); + $phpUpper = mb_strtoupper( $char ); + $jsUpper = $jsUpperChars[$i]; + if ( $jsUpper !== $phpUpper ) { + $data[$char] = $phpUpper; + } +} + +echo str_replace( ' ', "\t", + json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE ) +) . "\n"; diff --git a/profileinfo.php b/profileinfo.php index d000972051..dccdd38ff6 100644 --- a/profileinfo.php +++ b/profileinfo.php @@ -412,6 +412,7 @@ $filter = $_REQUEST['filter'] ?? ''; $queries = []; $sqltotal = 0.0; + /** @var profile_point|false $last */ $last = false; foreach ( $res as $o ) { $next = new profile_point( $o->pf_name, $o->pf_count, $o->pf_time, $o->pf_memory ); @@ -435,7 +436,7 @@ $filter = $_REQUEST['filter'] ?? ''; } } - $s = new profile_point( 'SQL Queries', 0, $sqltotal, 0, 0 ); + $s = new profile_point( 'SQL Queries', 0, $sqltotal, 0 ); foreach ( $queries as $q ) { $s->add_child( $q ); } diff --git a/resources/Resources.php b/resources/Resources.php index 718cd839cf..bfa80a8421 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -591,19 +591,6 @@ return [ ], 'group' => 'jquery.ui', ], - 'jquery.ui.spinner' => [ - 'deprecated' => 'Please use "jquery.spinner" instead.', - 'scripts' => 'resources/lib/jquery.ui/jquery.ui.spinner.js', - 'dependencies' => [ - 'jquery.ui.core', - 'jquery.ui.widget', - 'jquery.ui.button', - ], - 'skinStyles' => [ - 'default' => 'resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css', - ], - 'group' => 'jquery.ui', - ], 'jquery.ui.tabs' => [ 'scripts' => 'resources/lib/jquery.ui/jquery.ui.tabs.js', 'dependencies' => [ @@ -1136,9 +1123,11 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.Title' => [ - 'scripts' => [ - 'resources/src/mediawiki.Title/Title.js', - 'resources/src/mediawiki.Title/phpCharToUpper.js', + 'localBasePath' => "$IP/resources/src/mediawiki.Title", + 'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.Title", + 'packageFiles' => [ + 'Title.js', + 'phpCharToUpper.json' ], 'dependencies' => [ 'mediawiki.String', @@ -1336,9 +1325,11 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.checkboxtoggle' => [ + 'targets' => [ 'desktop', 'mobile' ], 'scripts' => 'resources/src/mediawiki.checkboxtoggle.js', ], 'mediawiki.checkboxtoggle.styles' => [ + 'targets' => [ 'desktop', 'mobile' ], 'styles' => 'resources/src/mediawiki.checkboxtoggle.styles.css', ], 'mediawiki.cookie' => [ @@ -1457,6 +1448,7 @@ return [ 'skinStyles' => [ 'default' => 'resources/src/mediawiki.action/mediawiki.action.history.styles.css', ], + 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.action.view.dblClickEdit' => [ 'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js', @@ -1508,6 +1500,7 @@ return [ 'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.rightClickEdit.js', ], 'mediawiki.action.edit.editWarning' => [ + 'targets' => [ 'desktop', 'mobile' ], 'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js', 'dependencies' => [ 'jquery.textSelection', diff --git a/resources/lib/jquery.ui/jquery.ui.spinner.js b/resources/lib/jquery.ui/jquery.ui.spinner.js deleted file mode 100644 index 98dc9dfe10..0000000000 --- a/resources/lib/jquery.ui/jquery.ui.spinner.js +++ /dev/null @@ -1,478 +0,0 @@ -/*! - * jQuery UI Spinner 1.9.2 - * http://jqueryui.com - * - * Copyright 2012 jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/spinner/ - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - * jquery.ui.button.js - */ -(function( $ ) { - -function modifier( fn ) { - return function() { - var previous = this.element.val(); - fn.apply( this, arguments ); - this._refresh(); - if ( previous !== this.element.val() ) { - this._trigger( "change" ); - } - }; -} - -$.widget( "ui.spinner", { - version: "1.9.2", - defaultElement: "", - widgetEventPrefix: "spin", - options: { - culture: null, - icons: { - down: "ui-icon-triangle-1-s", - up: "ui-icon-triangle-1-n" - }, - incremental: true, - max: null, - min: null, - numberFormat: null, - page: 10, - step: 1, - - change: null, - spin: null, - start: null, - stop: null - }, - - _create: function() { - // handle string values that need to be parsed - this._setOption( "max", this.options.max ); - this._setOption( "min", this.options.min ); - this._setOption( "step", this.options.step ); - - // format the value, but don't constrain - this._value( this.element.val(), true ); - - this._draw(); - this._on( this._events ); - this._refresh(); - - // turning off autocomplete prevents the browser from remembering the - // value when navigating through history, so we re-enable autocomplete - // if the page is unloaded before the widget is destroyed. #7790 - this._on( this.window, { - beforeunload: function() { - this.element.removeAttr( "autocomplete" ); - } - }); - }, - - _getCreateOptions: function() { - var options = {}, - element = this.element; - - $.each( [ "min", "max", "step" ], function( i, option ) { - var value = element.attr( option ); - if ( value !== undefined && value.length ) { - options[ option ] = value; - } - }); - - return options; - }, - - _events: { - keydown: function( event ) { - if ( this._start( event ) && this._keydown( event ) ) { - event.preventDefault(); - } - }, - keyup: "_stop", - focus: function() { - this.previous = this.element.val(); - }, - blur: function( event ) { - if ( this.cancelBlur ) { - delete this.cancelBlur; - return; - } - - this._refresh(); - if ( this.previous !== this.element.val() ) { - this._trigger( "change", event ); - } - }, - mousewheel: function( event, delta ) { - if ( !delta ) { - return; - } - if ( !this.spinning && !this._start( event ) ) { - return false; - } - - this._spin( (delta > 0 ? 1 : -1) * this.options.step, event ); - clearTimeout( this.mousewheelTimer ); - this.mousewheelTimer = this._delay(function() { - if ( this.spinning ) { - this._stop( event ); - } - }, 100 ); - event.preventDefault(); - }, - "mousedown .ui-spinner-button": function( event ) { - var previous; - - // We never want the buttons to have focus; whenever the user is - // interacting with the spinner, the focus should be on the input. - // If the input is focused then this.previous is properly set from - // when the input first received focus. If the input is not focused - // then we need to set this.previous based on the value before spinning. - previous = this.element[0] === this.document[0].activeElement ? - this.previous : this.element.val(); - function checkFocus() { - var isActive = this.element[0] === this.document[0].activeElement; - if ( !isActive ) { - this.element.focus(); - this.previous = previous; - // support: IE - // IE sets focus asynchronously, so we need to check if focus - // moved off of the input because the user clicked on the button. - this._delay(function() { - this.previous = previous; - }); - } - } - - // ensure focus is on (or stays on) the text field - event.preventDefault(); - checkFocus.call( this ); - - // support: IE - // IE doesn't prevent moving focus even with event.preventDefault() - // so we set a flag to know when we should ignore the blur event - // and check (again) if focus moved off of the input. - this.cancelBlur = true; - this._delay(function() { - delete this.cancelBlur; - checkFocus.call( this ); - }); - - if ( this._start( event ) === false ) { - return; - } - - this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event ); - }, - "mouseup .ui-spinner-button": "_stop", - "mouseenter .ui-spinner-button": function( event ) { - // button will add ui-state-active if mouse was down while mouseleave and kept down - if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) { - return; - } - - if ( this._start( event ) === false ) { - return false; - } - this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event ); - }, - // TODO: do we really want to consider this a stop? - // shouldn't we just stop the repeater and wait until mouseup before - // we trigger the stop event? - "mouseleave .ui-spinner-button": "_stop" - }, - - _draw: function() { - var uiSpinner = this.uiSpinner = this.element - .addClass( "ui-spinner-input" ) - .attr( "autocomplete", "off" ) - .wrap( this._uiSpinnerHtml() ) - .parent() - // add buttons - .append( this._buttonHtml() ); - - this.element.attr( "role", "spinbutton" ); - - // button bindings - this.buttons = uiSpinner.find( ".ui-spinner-button" ) - .attr( "tabIndex", -1 ) - .button() - .removeClass( "ui-corner-all" ); - - // IE 6 doesn't understand height: 50% for the buttons - // unless the wrapper has an explicit height - if ( this.buttons.height() > Math.ceil( uiSpinner.height() * 0.5 ) && - uiSpinner.height() > 0 ) { - uiSpinner.height( uiSpinner.height() ); - } - - // disable spinner if element was already disabled - if ( this.options.disabled ) { - this.disable(); - } - }, - - _keydown: function( event ) { - var options = this.options, - keyCode = $.ui.keyCode; - - switch ( event.keyCode ) { - case keyCode.UP: - this._repeat( null, 1, event ); - return true; - case keyCode.DOWN: - this._repeat( null, -1, event ); - return true; - case keyCode.PAGE_UP: - this._repeat( null, options.page, event ); - return true; - case keyCode.PAGE_DOWN: - this._repeat( null, -options.page, event ); - return true; - } - - return false; - }, - - _uiSpinnerHtml: function() { - return ""; - }, - - _buttonHtml: function() { - return "" + - "" + - "" + - "" + - "" + - "" + - ""; - }, - - _start: function( event ) { - if ( !this.spinning && this._trigger( "start", event ) === false ) { - return false; - } - - if ( !this.counter ) { - this.counter = 1; - } - this.spinning = true; - return true; - }, - - _repeat: function( i, steps, event ) { - i = i || 500; - - clearTimeout( this.timer ); - this.timer = this._delay(function() { - this._repeat( 40, steps, event ); - }, i ); - - this._spin( steps * this.options.step, event ); - }, - - _spin: function( step, event ) { - var value = this.value() || 0; - - if ( !this.counter ) { - this.counter = 1; - } - - value = this._adjustValue( value + step * this._increment( this.counter ) ); - - if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false) { - this._value( value ); - this.counter++; - } - }, - - _increment: function( i ) { - var incremental = this.options.incremental; - - if ( incremental ) { - return $.isFunction( incremental ) ? - incremental( i ) : - Math.floor( i*i*i/50000 - i*i/500 + 17*i/200 + 1 ); - } - - return 1; - }, - - _precision: function() { - var precision = this._precisionOf( this.options.step ); - if ( this.options.min !== null ) { - precision = Math.max( precision, this._precisionOf( this.options.min ) ); - } - return precision; - }, - - _precisionOf: function( num ) { - var str = num.toString(), - decimal = str.indexOf( "." ); - return decimal === -1 ? 0 : str.length - decimal - 1; - }, - - _adjustValue: function( value ) { - var base, aboveMin, - options = this.options; - - // make sure we're at a valid step - // - find out where we are relative to the base (min or 0) - base = options.min !== null ? options.min : 0; - aboveMin = value - base; - // - round to the nearest step - aboveMin = Math.round(aboveMin / options.step) * options.step; - // - rounding is based on 0, so adjust back to our base - value = base + aboveMin; - - // fix precision from bad JS floating point math - value = parseFloat( value.toFixed( this._precision() ) ); - - // clamp the value - if ( options.max !== null && value > options.max) { - return options.max; - } - if ( options.min !== null && value < options.min ) { - return options.min; - } - - return value; - }, - - _stop: function( event ) { - if ( !this.spinning ) { - return; - } - - clearTimeout( this.timer ); - clearTimeout( this.mousewheelTimer ); - this.counter = 0; - this.spinning = false; - this._trigger( "stop", event ); - }, - - _setOption: function( key, value ) { - if ( key === "culture" || key === "numberFormat" ) { - var prevValue = this._parse( this.element.val() ); - this.options[ key ] = value; - this.element.val( this._format( prevValue ) ); - return; - } - - if ( key === "max" || key === "min" || key === "step" ) { - if ( typeof value === "string" ) { - value = this._parse( value ); - } - } - - this._super( key, value ); - - if ( key === "disabled" ) { - if ( value ) { - this.element.prop( "disabled", true ); - this.buttons.button( "disable" ); - } else { - this.element.prop( "disabled", false ); - this.buttons.button( "enable" ); - } - } - }, - - _setOptions: modifier(function( options ) { - this._super( options ); - this._value( this.element.val() ); - }), - - _parse: function( val ) { - if ( typeof val === "string" && val !== "" ) { - val = window.Globalize && this.options.numberFormat ? - Globalize.parseFloat( val, 10, this.options.culture ) : +val; - } - return val === "" || isNaN( val ) ? null : val; - }, - - _format: function( value ) { - if ( value === "" ) { - return ""; - } - return window.Globalize && this.options.numberFormat ? - Globalize.format( value, this.options.numberFormat, this.options.culture ) : - value; - }, - - _refresh: function() { - this.element.attr({ - "aria-valuemin": this.options.min, - "aria-valuemax": this.options.max, - // TODO: what should we do with values that can't be parsed? - "aria-valuenow": this._parse( this.element.val() ) - }); - }, - - // update the value without triggering change - _value: function( value, allowAny ) { - var parsed; - if ( value !== "" ) { - parsed = this._parse( value ); - if ( parsed !== null ) { - if ( !allowAny ) { - parsed = this._adjustValue( parsed ); - } - value = this._format( parsed ); - } - } - this.element.val( value ); - this._refresh(); - }, - - _destroy: function() { - this.element - .removeClass( "ui-spinner-input" ) - .prop( "disabled", false ) - .removeAttr( "autocomplete" ) - .removeAttr( "role" ) - .removeAttr( "aria-valuemin" ) - .removeAttr( "aria-valuemax" ) - .removeAttr( "aria-valuenow" ); - this.uiSpinner.replaceWith( this.element ); - }, - - stepUp: modifier(function( steps ) { - this._stepUp( steps ); - }), - _stepUp: function( steps ) { - this._spin( (steps || 1) * this.options.step ); - }, - - stepDown: modifier(function( steps ) { - this._stepDown( steps ); - }), - _stepDown: function( steps ) { - this._spin( (steps || 1) * -this.options.step ); - }, - - pageUp: modifier(function( pages ) { - this._stepUp( (pages || 1) * this.options.page ); - }), - - pageDown: modifier(function( pages ) { - this._stepDown( (pages || 1) * this.options.page ); - }), - - value: function( newVal ) { - if ( !arguments.length ) { - return this._parse( this.element.val() ); - } - modifier( this._value ).call( this, newVal ); - }, - - widget: function() { - return this.uiSpinner; - } -}); - -}( jQuery ) ); diff --git a/resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css b/resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css deleted file mode 100644 index e89b7206c6..0000000000 --- a/resources/lib/jquery.ui/themes/smoothness/jquery.ui.spinner.css +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * jQuery UI Spinner 1.9.2 - * http://jqueryui.com - * - * Copyright 2012 jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Spinner#theming - */ -.ui-spinner { position:relative; display: inline-block; overflow: hidden; padding: 0; vertical-align: middle; } -.ui-spinner-input { border: none; background: none; padding: 0; margin: .2em 0; vertical-align: middle; margin-left: .4em; margin-right: 22px; } -.ui-spinner-button { width: 16px; height: 50%; font-size: .5em; padding: 0; margin: 0; text-align: center; position: absolute; cursor: default; display: block; overflow: hidden; right: 0; } -.ui-spinner a.ui-spinner-button { border-top: none; border-bottom: none; border-right: none; } /* more specificity required here to overide default borders */ -.ui-spinner .ui-icon { position: absolute; margin-top: -8px; top: 50%; left: 0; } /* vertical centre icon */ -.ui-spinner-up { top: 0; } -.ui-spinner-down { bottom: 0; } - -/* TR overrides */ -.ui-spinner .ui-icon-triangle-1-s { - /* need to fix icons sprite */ - background-position:-65px -16px; -} diff --git a/resources/src/mediawiki.Title/Title.js b/resources/src/mediawiki.Title/Title.js index 6bb3bce2a6..78ae135150 100644 --- a/resources/src/mediawiki.Title/Title.js +++ b/resources/src/mediawiki.Title/Title.js @@ -34,6 +34,8 @@ var mwString = require( 'mediawiki.String' ), + toUpperMapping = require( './phpCharToUpper.json' ), + namespaceIds = mw.config.get( 'wgNamespaceIds' ), /** @@ -783,6 +785,17 @@ } }; + /** + * PHP's strtoupper differs from String.toUpperCase in a number of cases (T147646). + * + * @param {string} chr Unicode character + * @return {string} Unicode character, in upper case, according to the same rules as in PHP + */ + Title.phpCharToUpper = function ( chr ) { + var mapped = toUpperMapping[ chr ]; + return mapped || chr.toUpperCase(); + }; + /* Public members */ Title.prototype = { @@ -827,8 +840,6 @@ ) { return this.title; } - // PHP's strtoupper differs from String.toUpperCase in a number of cases - // Bug: T147646 return mw.Title.phpCharToUpper( this.title[ 0 ] ) + this.title.slice( 1 ); }, diff --git a/resources/src/mediawiki.Title/phpCharToUpper.js b/resources/src/mediawiki.Title/phpCharToUpper.js deleted file mode 100644 index ed700f0f81..0000000000 --- a/resources/src/mediawiki.Title/phpCharToUpper.js +++ /dev/null @@ -1,255 +0,0 @@ -// This file can't be parsed by JSDuck due to . -// (It is excluded in jsduck.json.) -// ESLint suggests unquoting some object keys, which would render the file unparseable by Opera 12. -/* eslint-disable quote-props */ -( function () { - var toUpperMapping = { - 'ß': 'ß', - 'ʼn': 'ʼn', - 'Dž': 'Dž', - 'dž': 'Dž', - 'Lj': 'Lj', - 'lj': 'Lj', - 'Nj': 'Nj', - 'nj': 'Nj', - 'ǰ': 'ǰ', - 'Dz': 'Dz', - 'dz': 'Dz', - 'ʝ': 'Ʝ', - 'ͅ': 'ͅ', - 'ΐ': 'ΐ', - 'ΰ': 'ΰ', - 'և': 'և', - 'ᏸ': 'Ᏸ', - 'ᏹ': 'Ᏹ', - 'ᏺ': 'Ᏺ', - 'ᏻ': 'Ᏻ', - 'ᏼ': 'Ᏼ', - 'ᏽ': 'Ᏽ', - 'ẖ': 'ẖ', - 'ẗ': 'ẗ', - 'ẘ': 'ẘ', - 'ẙ': 'ẙ', - 'ẚ': 'ẚ', - 'ὐ': 'ὐ', - 'ὒ': 'ὒ', - 'ὔ': 'ὔ', - 'ὖ': 'ὖ', - 'ᾀ': 'ᾈ', - 'ᾁ': 'ᾉ', - 'ᾂ': 'ᾊ', - 'ᾃ': 'ᾋ', - 'ᾄ': 'ᾌ', - 'ᾅ': 'ᾍ', - 'ᾆ': 'ᾎ', - 'ᾇ': 'ᾏ', - 'ᾈ': 'ᾈ', - 'ᾉ': 'ᾉ', - 'ᾊ': 'ᾊ', - 'ᾋ': 'ᾋ', - 'ᾌ': 'ᾌ', - 'ᾍ': 'ᾍ', - 'ᾎ': 'ᾎ', - 'ᾏ': 'ᾏ', - 'ᾐ': 'ᾘ', - 'ᾑ': 'ᾙ', - 'ᾒ': 'ᾚ', - 'ᾓ': 'ᾛ', - 'ᾔ': 'ᾜ', - 'ᾕ': 'ᾝ', - 'ᾖ': 'ᾞ', - 'ᾗ': 'ᾟ', - 'ᾘ': 'ᾘ', - 'ᾙ': 'ᾙ', - 'ᾚ': 'ᾚ', - 'ᾛ': 'ᾛ', - 'ᾜ': 'ᾜ', - 'ᾝ': 'ᾝ', - 'ᾞ': 'ᾞ', - 'ᾟ': 'ᾟ', - 'ᾠ': 'ᾨ', - 'ᾡ': 'ᾩ', - 'ᾢ': 'ᾪ', - 'ᾣ': 'ᾫ', - 'ᾤ': 'ᾬ', - 'ᾥ': 'ᾭ', - 'ᾦ': 'ᾮ', - 'ᾧ': 'ᾯ', - 'ᾨ': 'ᾨ', - 'ᾩ': 'ᾩ', - 'ᾪ': 'ᾪ', - 'ᾫ': 'ᾫ', - 'ᾬ': 'ᾬ', - 'ᾭ': 'ᾭ', - 'ᾮ': 'ᾮ', - 'ᾯ': 'ᾯ', - 'ᾲ': 'ᾲ', - 'ᾳ': 'ᾼ', - 'ᾴ': 'ᾴ', - 'ᾶ': 'ᾶ', - 'ᾷ': 'ᾷ', - 'ᾼ': 'ᾼ', - 'ῂ': 'ῂ', - 'ῃ': 'ῌ', - 'ῄ': 'ῄ', - 'ῆ': 'ῆ', - 'ῇ': 'ῇ', - 'ῌ': 'ῌ', - 'ῒ': 'ῒ', - 'ΐ': 'ΐ', - 'ῖ': 'ῖ', - 'ῗ': 'ῗ', - 'ῢ': 'ῢ', - 'ΰ': 'ΰ', - 'ῤ': 'ῤ', - 'ῦ': 'ῦ', - 'ῧ': 'ῧ', - 'ῲ': 'ῲ', - 'ῳ': 'ῼ', - 'ῴ': 'ῴ', - 'ῶ': 'ῶ', - 'ῷ': 'ῷ', - 'ῼ': 'ῼ', - 'ⅰ': 'ⅰ', - 'ⅱ': 'ⅱ', - 'ⅲ': 'ⅲ', - 'ⅳ': 'ⅳ', - 'ⅴ': 'ⅴ', - 'ⅵ': 'ⅵ', - 'ⅶ': 'ⅶ', - 'ⅷ': 'ⅷ', - 'ⅸ': 'ⅸ', - 'ⅹ': 'ⅹ', - 'ⅺ': 'ⅺ', - 'ⅻ': 'ⅻ', - 'ⅼ': 'ⅼ', - 'ⅽ': 'ⅽ', - 'ⅾ': 'ⅾ', - 'ⅿ': 'ⅿ', - 'ⓐ': 'ⓐ', - 'ⓑ': 'ⓑ', - 'ⓒ': 'ⓒ', - 'ⓓ': 'ⓓ', - 'ⓔ': 'ⓔ', - 'ⓕ': 'ⓕ', - 'ⓖ': 'ⓖ', - 'ⓗ': 'ⓗ', - 'ⓘ': 'ⓘ', - 'ⓙ': 'ⓙ', - 'ⓚ': 'ⓚ', - 'ⓛ': 'ⓛ', - 'ⓜ': 'ⓜ', - 'ⓝ': 'ⓝ', - 'ⓞ': 'ⓞ', - 'ⓟ': 'ⓟ', - 'ⓠ': 'ⓠ', - 'ⓡ': 'ⓡ', - 'ⓢ': 'ⓢ', - 'ⓣ': 'ⓣ', - 'ⓤ': 'ⓤ', - 'ⓥ': 'ⓥ', - 'ⓦ': 'ⓦ', - 'ⓧ': 'ⓧ', - 'ⓨ': 'ⓨ', - 'ⓩ': 'ⓩ', - 'ꞵ': 'Ꞵ', - 'ꞷ': 'Ꞷ', - 'ꭓ': 'Ꭓ', - 'ꭰ': 'Ꭰ', - 'ꭱ': 'Ꭱ', - 'ꭲ': 'Ꭲ', - 'ꭳ': 'Ꭳ', - 'ꭴ': 'Ꭴ', - 'ꭵ': 'Ꭵ', - 'ꭶ': 'Ꭶ', - 'ꭷ': 'Ꭷ', - 'ꭸ': 'Ꭸ', - 'ꭹ': 'Ꭹ', - 'ꭺ': 'Ꭺ', - 'ꭻ': 'Ꭻ', - 'ꭼ': 'Ꭼ', - 'ꭽ': 'Ꭽ', - 'ꭾ': 'Ꭾ', - 'ꭿ': 'Ꭿ', - 'ꮀ': 'Ꮀ', - 'ꮁ': 'Ꮁ', - 'ꮂ': 'Ꮂ', - 'ꮃ': 'Ꮃ', - 'ꮄ': 'Ꮄ', - 'ꮅ': 'Ꮅ', - 'ꮆ': 'Ꮆ', - 'ꮇ': 'Ꮇ', - 'ꮈ': 'Ꮈ', - 'ꮉ': 'Ꮉ', - 'ꮊ': 'Ꮊ', - 'ꮋ': 'Ꮋ', - 'ꮌ': 'Ꮌ', - 'ꮍ': 'Ꮍ', - 'ꮎ': 'Ꮎ', - 'ꮏ': 'Ꮏ', - 'ꮐ': 'Ꮐ', - 'ꮑ': 'Ꮑ', - 'ꮒ': 'Ꮒ', - 'ꮓ': 'Ꮓ', - 'ꮔ': 'Ꮔ', - 'ꮕ': 'Ꮕ', - 'ꮖ': 'Ꮖ', - 'ꮗ': 'Ꮗ', - 'ꮘ': 'Ꮘ', - 'ꮙ': 'Ꮙ', - 'ꮚ': 'Ꮚ', - 'ꮛ': 'Ꮛ', - 'ꮜ': 'Ꮜ', - 'ꮝ': 'Ꮝ', - 'ꮞ': 'Ꮞ', - 'ꮟ': 'Ꮟ', - 'ꮠ': 'Ꮠ', - 'ꮡ': 'Ꮡ', - 'ꮢ': 'Ꮢ', - 'ꮣ': 'Ꮣ', - 'ꮤ': 'Ꮤ', - 'ꮥ': 'Ꮥ', - 'ꮦ': 'Ꮦ', - 'ꮧ': 'Ꮧ', - 'ꮨ': 'Ꮨ', - 'ꮩ': 'Ꮩ', - 'ꮪ': 'Ꮪ', - 'ꮫ': 'Ꮫ', - 'ꮬ': 'Ꮬ', - 'ꮭ': 'Ꮭ', - 'ꮮ': 'Ꮮ', - 'ꮯ': 'Ꮯ', - 'ꮰ': 'Ꮰ', - 'ꮱ': 'Ꮱ', - 'ꮲ': 'Ꮲ', - 'ꮳ': 'Ꮳ', - 'ꮴ': 'Ꮴ', - 'ꮵ': 'Ꮵ', - 'ꮶ': 'Ꮶ', - 'ꮷ': 'Ꮷ', - 'ꮸ': 'Ꮸ', - 'ꮹ': 'Ꮹ', - 'ꮺ': 'Ꮺ', - 'ꮻ': 'Ꮻ', - 'ꮼ': 'Ꮼ', - 'ꮽ': 'Ꮽ', - 'ꮾ': 'Ꮾ', - 'ꮿ': 'Ꮿ', - 'ff': 'ff', - 'fi': 'fi', - 'fl': 'fl', - 'ffi': 'ffi', - 'ffl': 'ffl', - 'ſt': 'ſt', - 'st': 'st', - 'ﬓ': 'ﬓ', - 'ﬔ': 'ﬔ', - 'ﬕ': 'ﬕ', - 'ﬖ': 'ﬖ', - 'ﬗ': 'ﬗ' - }; - mw.Title.phpCharToUpper = function ( chr ) { - var mapped = toUpperMapping[ chr ]; - return mapped || chr.toUpperCase(); - }; -}() ); diff --git a/resources/src/mediawiki.Title/phpCharToUpper.json b/resources/src/mediawiki.Title/phpCharToUpper.json new file mode 100644 index 0000000000..b0887fa36a --- /dev/null +++ b/resources/src/mediawiki.Title/phpCharToUpper.json @@ -0,0 +1,245 @@ +{ + "ß": "ß", + "ʼn": "ʼn", + "Dž": "Dž", + "dž": "Dž", + "Lj": "Lj", + "lj": "Lj", + "Nj": "Nj", + "nj": "Nj", + "ǰ": "ǰ", + "Dz": "Dz", + "dz": "Dz", + "ʝ": "Ʝ", + "ͅ": "ͅ", + "ΐ": "ΐ", + "ΰ": "ΰ", + "և": "և", + "ᏸ": "Ᏸ", + "ᏹ": "Ᏹ", + "ᏺ": "Ᏺ", + "ᏻ": "Ᏻ", + "ᏼ": "Ᏼ", + "ᏽ": "Ᏽ", + "ẖ": "ẖ", + "ẗ": "ẗ", + "ẘ": "ẘ", + "ẙ": "ẙ", + "ẚ": "ẚ", + "ὐ": "ὐ", + "ὒ": "ὒ", + "ὔ": "ὔ", + "ὖ": "ὖ", + "ᾀ": "ᾈ", + "ᾁ": "ᾉ", + "ᾂ": "ᾊ", + "ᾃ": "ᾋ", + "ᾄ": "ᾌ", + "ᾅ": "ᾍ", + "ᾆ": "ᾎ", + "ᾇ": "ᾏ", + "ᾈ": "ᾈ", + "ᾉ": "ᾉ", + "ᾊ": "ᾊ", + "ᾋ": "ᾋ", + "ᾌ": "ᾌ", + "ᾍ": "ᾍ", + "ᾎ": "ᾎ", + "ᾏ": "ᾏ", + "ᾐ": "ᾘ", + "ᾑ": "ᾙ", + "ᾒ": "ᾚ", + "ᾓ": "ᾛ", + "ᾔ": "ᾜ", + "ᾕ": "ᾝ", + "ᾖ": "ᾞ", + "ᾗ": "ᾟ", + "ᾘ": "ᾘ", + "ᾙ": "ᾙ", + "ᾚ": "ᾚ", + "ᾛ": "ᾛ", + "ᾜ": "ᾜ", + "ᾝ": "ᾝ", + "ᾞ": "ᾞ", + "ᾟ": "ᾟ", + "ᾠ": "ᾨ", + "ᾡ": "ᾩ", + "ᾢ": "ᾪ", + "ᾣ": "ᾫ", + "ᾤ": "ᾬ", + "ᾥ": "ᾭ", + "ᾦ": "ᾮ", + "ᾧ": "ᾯ", + "ᾨ": "ᾨ", + "ᾩ": "ᾩ", + "ᾪ": "ᾪ", + "ᾫ": "ᾫ", + "ᾬ": "ᾬ", + "ᾭ": "ᾭ", + "ᾮ": "ᾮ", + "ᾯ": "ᾯ", + "ᾲ": "ᾲ", + "ᾳ": "ᾼ", + "ᾴ": "ᾴ", + "ᾶ": "ᾶ", + "ᾷ": "ᾷ", + "ᾼ": "ᾼ", + "ῂ": "ῂ", + "ῃ": "ῌ", + "ῄ": "ῄ", + "ῆ": "ῆ", + "ῇ": "ῇ", + "ῌ": "ῌ", + "ῒ": "ῒ", + "ΐ": "ΐ", + "ῖ": "ῖ", + "ῗ": "ῗ", + "ῢ": "ῢ", + "ΰ": "ΰ", + "ῤ": "ῤ", + "ῦ": "ῦ", + "ῧ": "ῧ", + "ῲ": "ῲ", + "ῳ": "ῼ", + "ῴ": "ῴ", + "ῶ": "ῶ", + "ῷ": "ῷ", + "ῼ": "ῼ", + "ⅰ": "ⅰ", + "ⅱ": "ⅱ", + "ⅲ": "ⅲ", + "ⅳ": "ⅳ", + "ⅴ": "ⅴ", + "ⅵ": "ⅵ", + "ⅶ": "ⅶ", + "ⅷ": "ⅷ", + "ⅸ": "ⅸ", + "ⅹ": "ⅹ", + "ⅺ": "ⅺ", + "ⅻ": "ⅻ", + "ⅼ": "ⅼ", + "ⅽ": "ⅽ", + "ⅾ": "ⅾ", + "ⅿ": "ⅿ", + "ⓐ": "ⓐ", + "ⓑ": "ⓑ", + "ⓒ": "ⓒ", + "ⓓ": "ⓓ", + "ⓔ": "ⓔ", + "ⓕ": "ⓕ", + "ⓖ": "ⓖ", + "ⓗ": "ⓗ", + "ⓘ": "ⓘ", + "ⓙ": "ⓙ", + "ⓚ": "ⓚ", + "ⓛ": "ⓛ", + "ⓜ": "ⓜ", + "ⓝ": "ⓝ", + "ⓞ": "ⓞ", + "ⓟ": "ⓟ", + "ⓠ": "ⓠ", + "ⓡ": "ⓡ", + "ⓢ": "ⓢ", + "ⓣ": "ⓣ", + "ⓤ": "ⓤ", + "ⓥ": "ⓥ", + "ⓦ": "ⓦ", + "ⓧ": "ⓧ", + "ⓨ": "ⓨ", + "ⓩ": "ⓩ", + "ꞵ": "Ꞵ", + "ꞷ": "Ꞷ", + "ꭓ": "Ꭓ", + "ꭰ": "Ꭰ", + "ꭱ": "Ꭱ", + "ꭲ": "Ꭲ", + "ꭳ": "Ꭳ", + "ꭴ": "Ꭴ", + "ꭵ": "Ꭵ", + "ꭶ": "Ꭶ", + "ꭷ": "Ꭷ", + "ꭸ": "Ꭸ", + "ꭹ": "Ꭹ", + "ꭺ": "Ꭺ", + "ꭻ": "Ꭻ", + "ꭼ": "Ꭼ", + "ꭽ": "Ꭽ", + "ꭾ": "Ꭾ", + "ꭿ": "Ꭿ", + "ꮀ": "Ꮀ", + "ꮁ": "Ꮁ", + "ꮂ": "Ꮂ", + "ꮃ": "Ꮃ", + "ꮄ": "Ꮄ", + "ꮅ": "Ꮅ", + "ꮆ": "Ꮆ", + "ꮇ": "Ꮇ", + "ꮈ": "Ꮈ", + "ꮉ": "Ꮉ", + "ꮊ": "Ꮊ", + "ꮋ": "Ꮋ", + "ꮌ": "Ꮌ", + "ꮍ": "Ꮍ", + "ꮎ": "Ꮎ", + "ꮏ": "Ꮏ", + "ꮐ": "Ꮐ", + "ꮑ": "Ꮑ", + "ꮒ": "Ꮒ", + "ꮓ": "Ꮓ", + "ꮔ": "Ꮔ", + "ꮕ": "Ꮕ", + "ꮖ": "Ꮖ", + "ꮗ": "Ꮗ", + "ꮘ": "Ꮘ", + "ꮙ": "Ꮙ", + "ꮚ": "Ꮚ", + "ꮛ": "Ꮛ", + "ꮜ": "Ꮜ", + "ꮝ": "Ꮝ", + "ꮞ": "Ꮞ", + "ꮟ": "Ꮟ", + "ꮠ": "Ꮠ", + "ꮡ": "Ꮡ", + "ꮢ": "Ꮢ", + "ꮣ": "Ꮣ", + "ꮤ": "Ꮤ", + "ꮥ": "Ꮥ", + "ꮦ": "Ꮦ", + "ꮧ": "Ꮧ", + "ꮨ": "Ꮨ", + "ꮩ": "Ꮩ", + "ꮪ": "Ꮪ", + "ꮫ": "Ꮫ", + "ꮬ": "Ꮬ", + "ꮭ": "Ꮭ", + "ꮮ": "Ꮮ", + "ꮯ": "Ꮯ", + "ꮰ": "Ꮰ", + "ꮱ": "Ꮱ", + "ꮲ": "Ꮲ", + "ꮳ": "Ꮳ", + "ꮴ": "Ꮴ", + "ꮵ": "Ꮵ", + "ꮶ": "Ꮶ", + "ꮷ": "Ꮷ", + "ꮸ": "Ꮸ", + "ꮹ": "Ꮹ", + "ꮺ": "Ꮺ", + "ꮻ": "Ꮻ", + "ꮼ": "Ꮼ", + "ꮽ": "Ꮽ", + "ꮾ": "Ꮾ", + "ꮿ": "Ꮿ", + "ff": "ff", + "fi": "fi", + "fl": "fl", + "ffi": "ffi", + "ffl": "ffl", + "ſt": "ſt", + "st": "st", + "ﬓ": "ﬓ", + "ﬔ": "ﬔ", + "ﬕ": "ﬕ", + "ﬖ": "ﬖ", + "ﬗ": "ﬗ" +} diff --git a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js index 3a717605c3..c25db2f405 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js @@ -168,6 +168,7 @@ focusout: this.onBlur.bind( this ) } ); this.calendar.$element.on( { + focusout: this.onBlur.bind( this ), click: this.onCalendarClick.bind( this ), keypress: this.onCalendarKeyPress.bind( this ) } ); diff --git a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php index 97ffd9413b..c210061191 100644 --- a/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php +++ b/tests/phpunit/includes/resourceloader/DerivativeResourceLoaderContextTest.php @@ -8,10 +8,10 @@ class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase { use MediaWikiCoversValidator; - protected static function getContext() { + protected static function makeContext() { $request = new FauxRequest( [ - 'lang' => 'zh', - 'modules' => 'test.context', + 'lang' => 'qqx', + 'modules' => 'test.default', 'only' => 'scripts', 'skin' => 'fallback', 'target' => 'test', @@ -19,123 +19,114 @@ class DerivativeResourceLoaderContextTest extends PHPUnit\Framework\TestCase { return new ResourceLoaderContext( new ResourceLoader(), $request ); } - public function testGetInherited() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); - - // Request parameters - $this->assertEquals( $derived->getDebug(), false ); - $this->assertEquals( $derived->getLanguage(), 'zh' ); - $this->assertEquals( $derived->getModules(), [ 'test.context' ] ); - $this->assertEquals( $derived->getOnly(), 'scripts' ); - $this->assertEquals( $derived->getSkin(), 'fallback' ); - $this->assertEquals( $derived->getUser(), null ); - - // Misc - $this->assertEquals( $derived->getDirection(), 'ltr' ); - $this->assertEquals( $derived->getHash(), 'zh|fallback|||scripts|||||' ); - } - - public function testModules() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeModules() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getModules(), [ 'test.default' ], 'inherit from parent' ); $derived->setModules( [ 'test.override' ] ); - $this->assertEquals( $derived->getModules(), [ 'test.override' ] ); - } - - public function testLanguage() { - $context = self::getContext(); - $derived = new DerivativeResourceLoaderContext( $context ); - - $derived->setLanguage( 'nl' ); - $this->assertEquals( $derived->getLanguage(), 'nl' ); + $this->assertSame( $derived->getModules(), [ 'test.override' ] ); } - public function testDirection() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeLanguageAndDirection() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getLanguage(), 'qqx', 'inherit from parent' ); $derived->setLanguage( 'nl' ); - $this->assertEquals( $derived->getDirection(), 'ltr' ); + $this->assertSame( $derived->getLanguage(), 'nl' ); + $this->assertSame( $derived->getDirection(), 'ltr' ); + // Changing the language must clear cache of computed direction $derived->setLanguage( 'he' ); - $this->assertEquals( $derived->getDirection(), 'rtl' ); + $this->assertSame( $derived->getDirection(), 'rtl' ); + $this->assertSame( $derived->getLanguage(), 'he' ); + // Overriding the direction explicitly is allowed $derived->setDirection( 'ltr' ); - $this->assertEquals( $derived->getDirection(), 'ltr' ); + $this->assertSame( $derived->getDirection(), 'ltr' ); + $this->assertSame( $derived->getLanguage(), 'he' ); } - public function testSkin() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeSkin() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getSkin(), 'fallback', 'inherit from parent' ); - $derived->setSkin( 'override' ); - $this->assertEquals( $derived->getSkin(), 'override' ); + $derived->setSkin( 'myskin' ); + $this->assertSame( $derived->getSkin(), 'myskin' ); } - public function testUser() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeUser() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getUser(), null, 'inherit from parent' ); - $derived->setUser( 'Example' ); - $this->assertEquals( $derived->getUser(), 'Example' ); + $derived->setUser( 'MyUser' ); + $this->assertSame( $derived->getUser(), 'MyUser' ); } - public function testDebug() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeDebug() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getDebug(), false, 'inherit from parent' ); $derived->setDebug( true ); - $this->assertEquals( $derived->getDebug(), true ); + $this->assertSame( $derived->getDebug(), true ); } - public function testOnly() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeOnly() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getOnly(), 'scripts', 'inherit from parent' ); $derived->setOnly( 'styles' ); - $this->assertEquals( $derived->getOnly(), 'styles' ); + $this->assertSame( $derived->getOnly(), 'styles' ); $derived->setOnly( null ); - $this->assertEquals( $derived->getOnly(), null ); + $this->assertSame( $derived->getOnly(), null ); } - public function testVersion() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeVersion() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getVersion(), null ); $derived->setVersion( 'hw1' ); - $this->assertEquals( $derived->getVersion(), 'hw1' ); + $this->assertSame( $derived->getVersion(), 'hw1' ); } - public function testRaw() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); + public function testChangeRaw() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getRaw(), false, 'inherit from parent' ); $derived->setRaw( true ); - $this->assertEquals( $derived->getRaw(), true ); + $this->assertSame( $derived->getRaw(), true ); } - public function testGetHash() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); - - $this->assertEquals( $derived->getHash(), 'zh|fallback|||scripts|||||' ); + public function testChangeHash() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertSame( $derived->getHash(), 'qqx|fallback|||scripts|||||', 'inherit' ); $derived->setLanguage( 'nl' ); $derived->setUser( 'Example' ); // Assert that subclass is able to clear parent class "hash" member - $this->assertEquals( $derived->getHash(), 'nl|fallback||Example|scripts|||||' ); + $this->assertSame( $derived->getHash(), 'nl|fallback||Example|scripts|||||' ); } - public function testContentOverrides() { - $derived = new DerivativeResourceLoaderContext( self::getContext() ); - - $this->assertNull( $derived->getContentOverrideCallback() ); + public function testChangeContentOverrides() { + $derived = new DerivativeResourceLoaderContext( self::makeContext() ); + $this->assertNull( $derived->getContentOverrideCallback(), 'default' ); $override = function ( Title $t ) { return null; }; $derived->setContentOverrideCallback( $override ); - $this->assertSame( $override, $derived->getContentOverrideCallback() ); + $this->assertSame( $override, $derived->getContentOverrideCallback(), 'changed' ); $derived2 = new DerivativeResourceLoaderContext( $derived ); - $this->assertSame( $override, $derived2->getContentOverrideCallback() ); + $this->assertSame( + $override, + $derived2->getContentOverrideCallback(), + 'change via a second derivative layer' + ); } - public function testAccessors() { - $context = self::getContext(); + public function testImmutableAccessors() { + $context = self::makeContext(); $derived = new DerivativeResourceLoaderContext( $context ); $this->assertSame( $derived->getRequest(), $context->getRequest() ); $this->assertSame( $derived->getResourceLoader(), $context->getResourceLoader() ); diff --git a/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php b/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php index 70bf39f75d..e57764306e 100644 --- a/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php +++ b/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php @@ -3,7 +3,7 @@ use Wikimedia\TestingAccessWrapper; /** - * @group Cache + * @group ResourceLoader * @covers MessageBlobStore */ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase { @@ -13,64 +13,17 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase { protected function setUp() { parent::setUp(); - // MediaWiki tests defaults $wgMainWANCache to CACHE_NONE. - // Use hash instead so that caching is observed - $this->wanCache = $this->getMockBuilder( WANObjectCache::class ) - ->setConstructorArgs( [ [ - 'cache' => new HashBagOStuff(), - 'pool' => 'test', - 'relayer' => new EventRelayerNull( [] ) - ] ] ) - ->setMethods( [ 'makePurgeValue' ] ) - ->getMock(); - - $this->wanCache->expects( $this->any() ) - ->method( 'makePurgeValue' ) - ->will( $this->returnCallback( function ( $timestamp, $holdoff ) { - // Disable holdoff as it messes with testing. Aside from a 0-second holdoff, - // make sure that "time" passes between getMulti() check init and the set() - // in recacheMessageBlob(). This especially matters for Windows clocks. - $ts = (float)$timestamp - 0.0001; - - return WANObjectCache::PURGE_VAL_PREFIX . $ts . ':0'; - } ) ); - } - - protected function makeBlobStore( $methods = null, $rl = null ) { - $blobStore = $this->getMockBuilder( MessageBlobStore::class ) - ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] ) - ->setMethods( $methods ) - ->getMock(); - - $access = TestingAccessWrapper::newFromObject( $blobStore ); - $access->wanCache = $this->wanCache; - return $blobStore; - } - - protected function makeModule( array $messages ) { - $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] ); - $module->setName( 'test.blobstore' ); - return $module; - } - - /** @covers MessageBlobStore::setLogger */ - public function testSetLogger() { - $blobStore = $this->makeBlobStore(); - $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) ); + // MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE. + // Use HashBagOStuff here so that we can observe caching. + $this->wanCache = new WANObjectCache( [ + 'cache' => new HashBagOStuff() + ] ); + + $this->clock = 1301655600.000; + $this->wanCache->setMockTime( $this->clock ); } - /** @covers MessageBlobStore::getResourceLoader */ - public function testGetResourceLoader() { - // Call protected method - $blobStore = TestingAccessWrapper::newFromObject( $this->makeBlobStore() ); - $this->assertInstanceOf( - ResourceLoader::class, - $blobStore->getResourceLoader() - ); - } - - /** @covers MessageBlobStore::fetchMessage */ - public function testFetchMessage() { + public function testBlobCreation() { $module = $this->makeModule( [ 'mainpage' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); @@ -81,140 +34,153 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase { $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' ); } - /** @covers MessageBlobStore::fetchMessage */ - public function testFetchMessageFail() { + public function testBlobCreation_unknownMessage() { $module = $this->makeModule( [ 'i-dont-exist' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( null, $rl ); - $blob = $blobStore->getBlob( $module, 'en' ); + // Generating a blob should succeed without errors, + // even if a message is unknown. + $blob = $blobStore->getBlob( $module, 'en' ); $this->assertEquals( '{"i-dont-exist":"\u29fci-dont-exist\u29fd"}', $blob, 'Generated blob' ); } - public function testGetBlob() { - $module = $this->makeModule( [ 'foo' ] ); + public function testMessageCachingAndPurging() { + $module = $this->makeModule( [ 'example' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); + + // Advance this new WANObjectCache instance to a normal state, + // by doing one "get" and letting its hold off period expire. + // Without this, the first real "get" would lazy-initialise the + // checkKey and thus reject the first "set". + $blobStore->getBlob( $module, 'en' ); + $this->clock += 20; + + // Arrange version 1 of a message $blobStore->expects( $this->once() ) ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'Example' ) ); + ->will( $this->returnValue( 'First version' ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); + $this->assertEquals( '{"example":"First version"}', $blob, 'Blob for v1' ); - $this->assertEquals( '{"foo":"Example"}', $blob, 'Generated blob' ); - } - - public function testGetBlobCached() { - $module = $this->makeModule( [ 'example' ] ); - $rl = new ResourceLoader(); - $rl->register( $module->getName(), $module ); - + // Arrange version 2 $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); $blobStore->expects( $this->once() ) ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'First' ) ); + ->will( $this->returnValue( 'Second version' ) ); + $this->clock += 20; - $module = $this->makeModule( [ 'example' ] ); + // Assert + // We do not validate whether a cached message is up-to-date. + // Instead, changes to messages will send us a purge. + // When cache is not purged or expired, it must be used. $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' ); + $this->assertEquals( '{"example":"First version"}', $blob, 'Reuse cached v1 blob' ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->never() ) - ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'Second' ) ); + // Purge cache + $blobStore->updateMessage( 'example' ); + $this->clock += 20; - $module = $this->makeModule( [ 'example' ] ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Cache hit' ); + $this->assertEquals( '{"example":"Second version"}', $blob, 'Updated blob for v2' ); } - public function testUpdateMessage() { + public function testPurgeEverything() { $module = $this->makeModule( [ 'example' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->once() ) + // Advance this new WANObjectCache instance to a normal state. + $blobStore->getBlob( $module, 'en' ); + $this->clock += 20; + + // Arrange version 1 and 2 + $blobStore->expects( $this->exactly( 2 ) ) ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'First' ) ); + ->will( $this->onConsecutiveCalls( 'First', 'Second' ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' ); + $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1' ); - $blobStore->updateMessage( 'example' ); + $this->clock += 20; - $module = $this->makeModule( [ 'example' ] ); - $rl = new ResourceLoader(); - $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->once() ) - ->method( 'fetchMessage' ) - ->will( $this->returnValue( 'Second' ) ); + // Assert + $blob = $blobStore->getBlob( $module, 'en' ); + $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1 again' ); + + // Purge everything + $blobStore->clear(); + $this->clock += 20; + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' ); + $this->assertEquals( '{"example":"Second"}', $blob, 'Blob for v2' ); } - public function testValidation() { + public function testValidateAgainstModuleRegistry() { + // Arrange version 1 of a module $module = $this->makeModule( [ 'foo' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); $blobStore->expects( $this->once() ) ->method( 'fetchMessage' ) ->will( $this->returnValueMap( [ + // message key, language code, message value [ 'foo', 'en', 'Hello' ], ] ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"foo":"Hello"}', $blob, 'Generated blob' ); + $this->assertEquals( '{"foo":"Hello"}', $blob, 'Blob for v1' ); - // Now, imagine a change to the module is deployed. The module now contains - // message 'foo' and 'bar'. While updateMessage() was not called (since no - // message values were changed) it should detect the change in list of - // message keys. + // Arrange version 2 of module + // While message values may be out of date, the set of messages returned + // must always match the set of message keys required by the module. + // We do not receive purges for this because no messages were changed. $module = $this->makeModule( [ 'foo', 'bar' ] ); $rl = new ResourceLoader(); $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); $blobStore->expects( $this->exactly( 2 ) ) ->method( 'fetchMessage' ) ->will( $this->returnValueMap( [ + // message key, language code, message value [ 'foo', 'en', 'Hello' ], [ 'bar', 'en', 'World' ], ] ) ); + // Assert $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Updated blob' ); + $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Blob for v2' ); } - public function testClear() { - $module = $this->makeModule( [ 'example' ] ); - $rl = new ResourceLoader(); - $rl->register( $module->getName(), $module ); - $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl ); - $blobStore->expects( $this->exactly( 2 ) ) - ->method( 'fetchMessage' ) - ->will( $this->onConsecutiveCalls( 'First', 'Second' ) ); - - $now = microtime( true ); - $this->wanCache->setMockTime( $now ); - - $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' ); + public function testSetLoggedIsVoid() { + $blobStore = $this->makeBlobStore(); + $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) ); + } - $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"First"}', $blob, 'Cache-hit' ); + private function makeBlobStore( $methods = null, $rl = null ) { + $blobStore = $this->getMockBuilder( MessageBlobStore::class ) + ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] ) + ->setMethods( $methods ) + ->getMock(); - $now += 1; - $blobStore->clear(); + $access = TestingAccessWrapper::newFromObject( $blobStore ); + $access->wanCache = $this->wanCache; + return $blobStore; + } - $blob = $blobStore->getBlob( $module, 'en' ); - $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' ); + private function makeModule( array $messages ) { + $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] ); + $module->setName( 'test.blobstore' ); + return $module; } }