Merge "Minor bug fixes to Balancer."
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 21 Jul 2016 02:11:45 +0000 (02:11 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 21 Jul 2016 02:11:45 +0000 (02:11 +0000)
76 files changed:
RELEASE-NOTES-1.28
autoload.php
docs/distributors.txt
docs/hooks.txt
img_auth.php
includes/Category.php
includes/DefaultSettings.php
includes/Html.php
includes/HttpFunctions.php
includes/SiteConfiguration.php
includes/StreamFile.php
includes/Title.php
includes/WatchedItemStore.php
includes/api/ApiLogin.php
includes/api/ApiPurge.php
includes/api/i18n/it.json
includes/collation/IcuCollation.php
includes/db/DatabaseMysqlBase.php
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/deferred/SiteStatsUpdate.php
includes/filebackend/FileBackend.php
includes/filebackend/FileBackendStore.php
includes/filebackend/MemoryFileBackend.php
includes/filebackend/SwiftFileBackend.php
includes/filerepo/FileRepo.php
includes/gallery/TraditionalImageGallery.php
includes/installer/DatabaseUpdater.php
includes/installer/i18n/ko.json
includes/installer/i18n/wuu.json
includes/jobqueue/jobs/DeleteLinksJob.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/libs/MultiHttpClient.php
includes/linker/LinkRenderer.php
includes/media/MediaTransformOutput.php
includes/page/WikiPage.php
includes/profiler/ProfilerStub.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderModule.php
includes/revisiondelete/RevDelArchivedFileItem.php
includes/revisiondelete/RevDelFileItem.php
includes/specials/SpecialCreateAccount.php
includes/specials/SpecialUserLogin.php
includes/specials/pagers/ContribsPager.php
includes/specials/pre-authmanager/SpecialUserlogin.php
includes/utils/BatchRowIterator.php
languages/i18n/aeb-arab.json
languages/i18n/ar.json
languages/i18n/de.json
languages/i18n/es.json
languages/i18n/fr.json
languages/i18n/ia.json
languages/i18n/ja.json
languages/i18n/ko.json
languages/i18n/lij.json
languages/i18n/nn.json
languages/i18n/ru.json
languages/i18n/sr-ec.json
languages/i18n/ur.json
languages/i18n/wuu.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/cleanupEmptyCategories.php [new file with mode: 0644]
maintenance/mssql/tables.sql
maintenance/tables.sql
resources/Resources.php
resources/src/mediawiki/mediawiki.content.json.css [deleted file]
resources/src/mediawiki/mediawiki.content.json.less [new file with mode: 0644]
resources/src/mediawiki/page/gallery.css
tests/parser/parserTests.txt
tests/phpunit/includes/HtmlTest.php
tests/phpunit/includes/WatchedItemStoreUnitTest.php
tests/phpunit/includes/db/DatabaseMysqlBaseTest.php
tests/phpunit/includes/filebackend/FileBackendTest.php
tests/phpunit/includes/libs/XmlTypeCheckTest.php
tests/phpunit/includes/registration/ExtensionProcessorTest.php

index ff8e038..429c5fe 100644 (file)
@@ -6,6 +6,9 @@ MediaWiki 1.28 is an alpha-quality branch and is not recommended for use in
 production.
 
 === Configuration changes in 1.28 ===
+* BREAKING CHANGE: $wgHTTPProxy is now *required* for all external requests
+  made by MediaWiki via a proxy. Relying on the http_proxy environment
+  variable is no longer supported.
 * The load.php entry point now enforces the existing policy of not allowing
   access to session data, which includes the session user and the session
   user's language. If such access is attempted, an exception will be thrown.
@@ -64,6 +67,10 @@ changes to languages because of Phabricator reports.
   use or update a custom session provider if needed.
 * Deprecated APIEditBeforeSave hook in favor of EditFilterMergedContent.
 * The 'UploadVerification' hook is deprecated. Use 'UploadVerifyFile' instead.
+* SiteConfiguration::isLocalVHost() was removed (deprecated since 1.25).
+* The 'UserLoginComplete' hook has a new parameter to differentiate between actual
+  login and visiting the login page while already logged in.
+* ResourceLoader::makeLoaderURL() was removed (deprecated since 1.24).
 
 == Compatibility ==
 
index 61c97c6..ecbc9b3 100644 (file)
@@ -245,6 +245,7 @@ $wgAutoloadLocalClasses = [
        'ClassCollector' => __DIR__ . '/includes/utils/AutoloadGenerator.php',
        'CleanupAncientTables' => __DIR__ . '/maintenance/cleanupAncientTables.php',
        'CleanupBlocks' => __DIR__ . '/maintenance/cleanupBlocks.php',
+       'CleanupEmptyCategories' => __DIR__ . '/maintenance/cleanupEmptyCategories.php',
        'CleanupPreferences' => __DIR__ . '/maintenance/cleanupPreferences.php',
        'CleanupRemovedModules' => __DIR__ . '/maintenance/cleanupRemovedModules.php',
        'CleanupSpam' => __DIR__ . '/maintenance/cleanupSpam.php',
@@ -1438,6 +1439,7 @@ $wgAutoloadLocalClasses = [
        'UpdateArticleCount' => __DIR__ . '/maintenance/updateArticleCount.php',
        'UpdateCollation' => __DIR__ . '/maintenance/updateCollation.php',
        'UpdateDoubleWidthSearch' => __DIR__ . '/maintenance/updateDoubleWidthSearch.php',
+       'UpdateExtensionJsonSchema' => __DIR__ . '/maintenance/updateExtensionJsonSchema.php',
        'UpdateLogging' => __DIR__ . '/maintenance/archives/upgradeLogging.php',
        'UpdateMediaWiki' => __DIR__ . '/maintenance/update.php',
        'UpdateRestrictions' => __DIR__ . '/maintenance/updateRestrictions.php',
index efa573d..f19574c 100644 (file)
@@ -1,23 +1,23 @@
 This document is intended to provide useful advice for parties seeking to
-redistribute MediaWiki to end users.  It's targeted particularly at maintainers
+redistribute MediaWiki to end users. It's targeted particularly at maintainers
 for Linux distributions, since it's been observed that distribution packages of
-MediaWiki often break.  We've consistently had to recommend that users seeking
+MediaWiki often break. We've consistently had to recommend that users seeking
 support use official tarballs instead of their distribution's packages, and
-this often solves whatever problem the user is having.  It would be nice if
+this often solves whatever problem the user is having. It would be nice if
 this could change.
 
 == Background: why web applications are different ==
 
 MediaWiki is intended to be usable on any web host that provides support for
-PHP and a database.  Many users of low-end shared hosting have very limited
+PHP and a database. Many users of low-end shared hosting have very limited
 access to their machine: often only FTP access to some subdirectory of the web
-root.  Support for these users entails several restrictions, such as:
+root. Support for these users entails several restrictions, such as:
 
-  1) We cannot require installation of any files outside the web root.  Few of
+  1) We cannot require installation of any files outside the web root. Few of
   our users have access to directories like /usr or /etc.
   2) We cannot require the ability to run any utility on the command line.
   Many shared hosts have exec() and similar PHP functions disabled.
-  3) We cannot assume that the software has write access anywhere useful.  The
+  3) We cannot assume that the software has write access anywhere useful. The
   user account that MediaWiki (including its installer) runs under is often
   different from the account the user used to upload the files, and we might be
   restricted by PHP settings such as safe mode or open_basedir.
@@ -30,28 +30,28 @@ root.  Support for these users entails several restrictions, such as:
 
 Since anything that works on cheap shared hosting will work if you have shell
 or root access too, MediaWiki's design is based around catering to the lowest
-common denominator.  Although we support higher-end setups as well (like
+common denominator. Although we support higher-end setups as well (like
 Wikipedia!), the way many things work by default is tailored toward shared
-hosting.  These defaults are unconventional from the point of view of normal
+hosting. These defaults are unconventional from the point of view of normal
 (non-web) applications -- they might conflict with distributors' policies, and
 they certainly aren't ideal for someone who's installing MediaWiki as root.
 
 == Directory structure ==
 
 Because of constraint (1) above, MediaWiki does not conform to normal
-Unix filesystem layout.  Hopefully we'll offer direct support for standard
+Unix filesystem layout. Hopefully we'll offer direct support for standard
 layouts in the future, but for now *any change to the location of files is
-unsupported*.  Moving things and leaving symlinks will *probably* not break
+unsupported*. Moving things and leaving symlinks will *probably* not break
 anything, but it is *strongly* advised not to try any more intrusive changes to
-get MediaWiki to conform more closely to your filesystem hierarchy.  Any such
+get MediaWiki to conform more closely to your filesystem hierarchy. Any such
 attempt will almost certainly result in unnecessary bugs.
 
 The standard recommended location to install MediaWiki, relative to the web
-root, is /w (so, e.g., /var/www/w).  Rewrite rules can then be used to enable
-"pretty URLs" like /wiki/Article instead of /w/index.php?title=Article.  (This
+root, is /w (so, e.g., /var/www/w). Rewrite rules can then be used to enable
+"pretty URLs" like /wiki/Article instead of /w/index.php?title=Article. (This
 is the convention Wikipedia uses.)  In theory, it should be possible to enable
 the appropriate rewrite rules by default, if you can reconfigure the web
-server, but you'd need to alter LocalSettings.php too.  See
+server, but you'd need to alter LocalSettings.php too. See
 <https://www.mediawiki.org/wiki/Manual:Short_URL> for details on short URLs.
 
 If you really must mess around with the directory structure, note that the
@@ -59,37 +59,38 @@ following files *must* all be web-accessible for MediaWiki to function
 correctly:
 
   * api.php, img_auth.php, index.php, load.php, opensearch_desc.php, thumb.php,
-  profileinfo.php, redirect.php, trackback.php.  These are the entry points for
-  normal usage.  This list may be incomplete and is subject to change.
+  profileinfo.php. These are the entry points for normal usage. This list may be
+  incomplete and is subject to change.
   * mw-config/index.php: Used for web-based installation (sets up the database,
   prompts for the name of the wiki, etc.).
-  * images/: Used for uploaded files.  This could be somewhere else if
+  * images/: Used for uploaded files. This could be somewhere else if
   $wgUploadDirectory and $wgUploadPath are changed appropriately.
   * skins/*/: Subdirectories of skins/ contain CSS and JavaScript files that
-  must be accessible to web browsers.  The PHP files and Skin.sample in skins/
-  don't need to be accessible.  This could be somewhere else if
+  must be accessible to web browsers. The PHP files and Skin.sample in skins/
+  don't need to be accessible. This could be somewhere else if
   $wgStyleDirectory and $wgStylePath are changed appropriately.
   * extensions/: Many extensions include CSS and JavaScript files in their
-  extensions directory, and will break if they aren't web-accessible.  Some
+  extensions directory, and will break if they aren't web-accessible. Some
   extensions might theoretically provide additional entry points as well, at
   least in principle.
 
 But all files should keep their position relative to the web-visible
-installation directory no matter what.  If you must move includes/ somewhere in
-/usr/share, provide a symlink from /var/www/w.  If you don't, you *will* break
-something.  You have been warned.
+installation directory no matter what. If you must move includes/ somewhere in
+/usr/share, provide a symlink from /var/www/w. If you don't, you *will* break
+something. You have been warned.
 
 == Configuration ==
 
-MediaWiki is configured using LocalSettings.php.  This is a PHP file that's
+MediaWiki is configured using LocalSettings.php. This is a PHP file that's
 generated when the user visits mw-config/index.php to install the software, and
-which the user can edit by hand thereafter.  It's just a plain old PHP file,
-and can contain any PHP statements.  It usually sets global variables that are
+which the user can edit by hand thereafter. It's just a plain old PHP file,
+and can contain any PHP statements. It usually sets global variables that are
 used for configuration, and includes files used by any extensions.
 
-Distributors can easily add extra statements to the autogenerated
-LocalSettings.php by changing mw-config/overrides.php (see that file for details
-and examples).
+Distributors can easily change the installer behavior, including LocalSettings
+generated, by placing their overrides into mw-config/overrides directory. Doing
+that is highly preferred to modifying MediaWiki code directly. See
+mw-config/overrides/README for more details and examples.
 
 There's a new maintenance/install.php script which could be used for performing
 an install through the command line.
@@ -98,7 +99,7 @@ Some configuration options that distributors might be in a position to set
 intelligently:
 
   * $wgEmergencyContact: An e-mail address that can be used to contact the wiki
-  administrator.  By default, "wikiadmin@ServerName".
+  administrator. By default, "wikiadmin@ServerName".
   * $wgPasswordSender: The e-mail address to use when sending password e-mails.
   By default, "MediaWiki Mail <apache@ServerName>".
        (with ServerName guessed from the http request)
@@ -115,16 +116,16 @@ Any package manager which replaces the files but doesn't update the db is leavin
 an inconsistent wiki that may produce blank pages (php errors) when new features 
 using the changed schema would be used.
 
-Since MediaWiki 1.17 it is possible to upgrade using the installer by providing 
+Since MediaWiki 1.17 it is possible to upgrade using the web installer by providing
 an arbitrary secret value stored as $wgUpgradeKey in LocalSettings (older versions 
 needed to rename LocalSettings.php in order to upgrade using the installer).
 
 == Documentation ==
 
 MediaWiki's official documentation is split between two places: the source
-code, and <https://www.mediawiki.org/>.  The source code documentation is written
+code, and <https://www.mediawiki.org/>. The source code documentation is written
 exclusively by developers, and so is likely to be reliable (at worst,
-outdated).  However, it can be pretty sparse.  mediawiki.org documentation is
+outdated). However, it can be pretty sparse. mediawiki.org documentation is
 often much more thorough, but it's maintained by a wiki that's open to
 anonymous edits, so its quality is sometimes sketchy -- don't assume that
 anything there is officially endorsed!
@@ -132,31 +133,27 @@ anything there is officially endorsed!
 == Upstream ==
 
 MediaWiki is a project hosted and led by the Wikimedia Foundation, the
-not-for-profit charity that operates Wikipedia.  Wikimedia employs the lead
+not-for-profit charity that operates Wikipedia. Wikimedia employs the lead
 developer and several other paid developers, but commit access is given out
-liberally and there are multiple very active volunteer developers as well.  A
+liberally and there are multiple very active volunteer developers as well. A
 list of developers can be found at <https://www.mediawiki.org/wiki/Developers>.
 
-MediaWiki's bug tracker is at <https://bugzilla.wikimedia.org>.  However, most
-developers follow the bug tracker little or not at all.  The best place to
-post if you want to get developers' attention is the wikitech-l mailing list
-<https://lists.wikimedia.org/mailman/listinfo/wikitech-l>.  Posts to wikitech-l
-will inevitably be read by multiple experienced MediaWiki developers.  There's
+MediaWiki's bug tracker is at <https://phabricator.wikimedia.org>. However, you
+might find that the best place to post if you want to get developers' attention
+is the wikitech-l mailing list
+<https://lists.wikimedia.org/mailman/listinfo/wikitech-l>. Posts to wikitech-l
+will inevitably be read by multiple experienced MediaWiki developers. There's
 also an active IRC chat at <irc://irc.freenode.net/mediawiki>, where there are
 usually several developers at reasonably busy times of day.
 
-Unfortunately, we don't have a very good system for patch review.  Patches
-should be submitted on Bugzilla (as unified diffs produced with "svn diff"
-against the latest trunk revision), but many patches languish without review
-until they bitrot into uselessness.  You might want to get a developer to
-commit to reviewing your patch before you put too much effort into it.
-Reasonably straightforward patches shouldn't be too hard to get accepted if
-there's an interested developer, however -- posting to Bugzilla and then
-dropping a note on wikitech-l if nobody responds is a good tactic.
+Our Git repositories are hosted at <https://gerrit.wikimedia.org>, see
+<https://www.mediawiki.org/wiki/Gerrit> for more information. Patches should
+be submitted there. If you know which developers are best suited to review your
+patch, add them to it, otherwise ask on IRC to get better review time.
 
 All redistributors of MediaWiki should be subscribed to mediawiki-announce
-<https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce>.  It's
-extremely low-traffic, with an average of less than one post per month.  All
+<https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce>. It's
+extremely low-traffic, with an average of less than one post per month. All
 new releases are announced here, including critical security updates.
 
 == Useful software to install ==
@@ -167,41 +164,40 @@ perhaps configure it to use them (see Configuration section of this document):
 
   * APC (Alternative PHP Cache), XCache, or similar: Will greatly speed up the
   execution of MediaWiki, and all other PHP applications, at some cost in
-  memory usage.  Will be used automatically for the most part.
-  * clamav: Can be used for virus scanning of uploaded files.  Enable with
+  memory usage. Will be used automatically for the most part.
+  * clamav: Can be used for virus scanning of uploaded files. Enable with
   "$wgAntivirus = 'clamav';".
-  * DjVuLibre: Allows processing of DjVu files.  To enable this, set
+  * DjVuLibre: Allows processing of DjVu files. To enable this, set
   "$wgDjvuDump = 'djvudump'; $wgDjvuRenderer = 'ddjvu'; $wgDjvuTxt = 'djvutxt';".
-  * HTML Tidy: Fixes errors in HTML at runtime.  Can be enabled with 
+  * HTML Tidy: Fixes errors in HTML at runtime. Can be enabled with 
        "$wgUseTidy = true;".
-  * ImageMagick: For resizing images.  "$wgUseImageMagick = true;" will enable
-  it.  PHP's GD can also be used, but ImageMagick is preferable.
-  * Squid: Can provide a drastic speedup and a major cut in resource
-  consumption, but enabling it may interfere with other applications.  It might
-  be suitable for a separate mediawiki-squid package.  For setup details, see:
-  <https://www.mediawiki.org/wiki/Manual:Squid_caching>
+  * ImageMagick: For resizing images. "$wgUseImageMagick = true;" will enable
+  it. PHP's GD can also be used, but ImageMagick is preferable.
+  * HTTP cache such as Varnish or Squid: can provide a drastic speedup and a
+  major cut in resource consumption, but enabling it may interfere with other
+  applications. It might be suitable for a separate package. For setup details, see:
+  - <https://www.mediawiki.org/wiki/Manual:Varnish_caching>
+  - <https://www.mediawiki.org/wiki/Manual:Squid_caching>
   * rsvg or other SVG rasterizer: ImageMagick can be used for SVG support, but
-  is not ideal.  Wikipedia (as of the time of this writing) uses rsvg.  To
+  is not ideal. Wikipedia (as of the time of this writing) uses rsvg. To
   enable, set "$wgSVGConverter = 'rsvg';" (or other as appropriate).
-  * texvc: Included with MediaWiki.  Instructions for compiling and
-  installing it are in the math/ directory.
 
-MediaWiki uses some standard GNU utilities as well, such as diff and diff3.  If
+MediaWiki uses some standard GNU utilities as well, such as diff and diff3. If
 these are present in /usr/bin or some other reasonable location, they will be
 configured automatically on install.
 
-MediaWiki also has a "job queue" that handles background processing.  Because
+MediaWiki also has a "job queue" that handles background processing. Because
 shared hosts often don't provide access to cron, the job queue is run on every
-page view by default.  This means the background tasks aren't really done in
-the background.  Busy wikis can set $wgJobRunRate to 0 and run
-maintenance/runJobs.php periodically out of cron.  Distributors probably
+page view by default. This means the background tasks aren't really done in
+the background. Busy wikis can set $wgJobRunRate to 0 and run
+maintenance/runJobs.php periodically out of cron. Distributors probably
 shouldn't set this up as a default, however, since the extra cron job is
 unnecessary overhead for a little-used wiki.
 
 == Web server configuration ==
 
 MediaWiki includes several .htaccess files to restrict access to some
-directories.  If the web server is not configured to support these files, and
+directories. If the web server is not configured to support these files, and
 the relevant directories haven't been moved someplace inaccessible anyway (e.g.
 symlinked in /usr/share with the web server configured to not follow symlinks),
 then it might be useful to deny web access to those directories in the web
index 2b3116d..e1b3974 100644 (file)
@@ -3467,6 +3467,9 @@ $user: User object for the logged-in user
 For functionality that needs to run after any login (API or web) use UserLoggedIn.
 &$user: the user object that was created on login
 &$inject_html: Any HTML to inject after the "logged in" message.
+$direct: (bool) The hook is called directly after a successful login. This will only happen once
+  per login. A UserLoginComplete call with direct=false can happen when the user visits the login
+  page while already logged in.
 
 'UserLoginForm': DEPRECATED! Create an AuthenticationProvider instead.
 Manipulate the login form.
index d636188..fa1609f 100644 (file)
@@ -162,13 +162,21 @@ function wfImageAuthMain() {
                }
        }
 
+       $options = []; // HTTP header options
+       if ( isset( $_SERVER['HTTP_RANGE'] ) ) {
+               $options['range'] = $_SERVER['HTTP_RANGE'];
+       }
+       if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
+               $options['if-modified-since'] = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+       }
+
        if ( $request->getCheck( 'download' ) ) {
                $headers[] = 'Content-Disposition: attachment';
        }
 
        // Stream the requested file
        wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." );
-       $repo->streamFile( $filename, $headers );
+       $repo->streamFile( $filename, $headers, $options );
 }
 
 /**
index 28b566a..531e0be 100644 (file)
@@ -79,6 +79,11 @@ class Category {
                                $this->mSubcats = 0;
                                $this->mFiles = 0;
 
+                               # If the title exists, call refreshCounts to add a row for it.
+                               if ( $this->mTitle->exists() ) {
+                                       DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
+                               }
+
                                return true;
                        } else {
                                return false; # Fail
@@ -331,21 +336,35 @@ class Category {
                        [ 'LOCK IN SHARE MODE' ]
                );
 
+               $shouldExist = $result->pages > 0 || $this->getTitle()->exists();
+
                if ( $this->mID ) {
-                       # The category row already exists, so do a plain UPDATE instead
-                       # of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap
-                       # in the cat_id sequence. The row may or may not be "affected".
-                       $dbw->update(
-                               'category',
-                               [
-                                       'cat_pages' => $result->pages,
-                                       'cat_subcats' => $result->subcats,
-                                       'cat_files' => $result->files
-                               ],
-                               [ 'cat_title' => $this->mName ],
-                               __METHOD__
-                       );
-               } else {
+                       if ( $shouldExist ) {
+                               # The category row already exists, so do a plain UPDATE instead
+                               # of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap
+                               # in the cat_id sequence. The row may or may not be "affected".
+                               $dbw->update(
+                                       'category',
+                                       [
+                                               'cat_pages' => $result->pages,
+                                               'cat_subcats' => $result->subcats,
+                                               'cat_files' => $result->files
+                                       ],
+                                       [ 'cat_title' => $this->mName ],
+                                       __METHOD__
+                               );
+                       } else {
+                               # The category is empty and has no description page, delete it
+                               $dbw->delete(
+                                       'category',
+                                       [ 'cat_title' => $this->mName ],
+                                       __METHOD__
+                               );
+                               $this->mID = false;
+                       }
+               } elseif ( $shouldExist ) {
+                       # The category row doesn't exist but should, so create it. Use
+                       # upsert in case of races.
                        $dbw->upsert(
                                'category',
                                [
@@ -362,6 +381,8 @@ class Category {
                                ],
                                __METHOD__
                        );
+                       // @todo: Should we update $this->mID here? Or not since Category
+                       // objects tend to be short lived enough to not matter?
                }
 
                $dbw->endAtomic( __METHOD__ );
index 88110bb..16c335c 100644 (file)
@@ -1444,7 +1444,10 @@ $wgGalleryOptions = [
        'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize
        'imageWidth' => 120, // Width of the cells containing images in galleries (in "px")
        'imageHeight' => 120, // Height of the cells containing images in galleries (in "px")
-       'captionLength' => 25, // Length to truncate filename to in caption when using "showfilename"
+       'captionLength' => true, // Deprecated @since 1.28
+                                // Length to truncate filename to in caption when using "showfilename".
+                                // A value of 'true' will truncate the filename to one line using CSS
+                                // and will be the behaviour after deprecation.
        'showBytes' => true, // Show the filesize in bytes in categories
        'mode' => 'traditional',
 ];
index e5128d1..a5567fc 100644 (file)
@@ -1020,9 +1020,21 @@ class Html {
        static function srcSet( array $urls ) {
                $candidates = [];
                foreach ( $urls as $density => $url ) {
-                       // Cast density to float to strip 'x'.
-                       $candidates[] = $url . ' ' . (float)$density . 'x';
+                       // Cast density to float to strip 'x', then back to string to serve
+                       // as array index.
+                       $density = (string)(float)$density;
+                       $candidates[$density] = $url;
                }
+
+               // Remove duplicates that are the same as a smaller value
+               ksort( $candidates, SORT_NUMERIC );
+               $candidates = array_unique( $candidates );
+
+               // Append density info to the url
+               foreach ( $candidates as $density => $url ) {
+                       $candidates[$density] = $url . ' ' . $density . 'x';
+               }
+
                return implode( ", ", $candidates );
        }
 }
index b12f49f..54b057a 100644 (file)
@@ -124,47 +124,6 @@ class Http {
                return Http::request( 'POST', $url, $options, $caller );
        }
 
-       /**
-        * Check if the URL can be served by localhost
-        *
-        * @param string $url Full url to check
-        * @return bool
-        */
-       public static function isLocalURL( $url ) {
-               global $wgCommandLineMode, $wgLocalVirtualHosts;
-
-               if ( $wgCommandLineMode ) {
-                       return false;
-               }
-
-               // Extract host part
-               $matches = [];
-               if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) {
-                       $host = $matches[1];
-                       // Split up dotwise
-                       $domainParts = explode( '.', $host );
-                       // Check if this domain or any superdomain is listed as a local virtual host
-                       $domainParts = array_reverse( $domainParts );
-
-                       $domain = '';
-                       $countParts = count( $domainParts );
-                       for ( $i = 0; $i < $countParts; $i++ ) {
-                               $domainPart = $domainParts[$i];
-                               if ( $i == 0 ) {
-                                       $domain = $domainPart;
-                               } else {
-                                       $domain = $domainPart . '.' . $domain;
-                               }
-
-                               if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
-                                       return true;
-                               }
-                       }
-               }
-
-               return false;
-       }
-
        /**
         * A standard user-agent we can use for external requests.
         * @return string
@@ -194,7 +153,7 @@ class Http {
        }
 
        /**
-        * Gets the relevant proxy from $wgHTTPProxy/http_proxy (when set).
+        * Gets the relevant proxy from $wgHTTPProxy
         *
         * @return mixed The proxy address or an empty string if not set.
         */
@@ -205,11 +164,6 @@ class Http {
                        return $wgHTTPProxy;
                }
 
-               $envHttpProxy = getenv( "http_proxy" );
-               if ( $envHttpProxy ) {
-                       return $envHttpProxy;
-               }
-
                return "";
        }
 }
@@ -393,15 +347,56 @@ class MWHttpRequest {
                        return;
                }
 
-               // Otherwise, fallback to $wgHTTPProxy/http_proxy (when set) if this is not a machine
+               // Otherwise, fallback to $wgHTTPProxy if this is not a machine
                // local URL and proxies are not disabled
-               if ( Http::isLocalURL( $this->url ) || $this->noProxy ) {
+               if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
                        $this->proxy = '';
                } else {
                        $this->proxy = Http::getProxy();
                }
        }
 
+       /**
+        * Check if the URL can be served by localhost
+        *
+        * @param string $url Full url to check
+        * @return bool
+        */
+       private static function isLocalURL( $url ) {
+               global $wgCommandLineMode, $wgLocalVirtualHosts;
+
+               if ( $wgCommandLineMode ) {
+                       return false;
+               }
+
+               // Extract host part
+               $matches = [];
+               if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
+                       $host = $matches[1];
+                       // Split up dotwise
+                       $domainParts = explode( '.', $host );
+                       // Check if this domain or any superdomain is listed as a local virtual host
+                       $domainParts = array_reverse( $domainParts );
+
+                       $domain = '';
+                       $countParts = count( $domainParts );
+                       for ( $i = 0; $i < $countParts; $i++ ) {
+                               $domainPart = $domainParts[$i];
+                               if ( $i == 0 ) {
+                                       $domain = $domainPart;
+                               } else {
+                                       $domain = $domainPart . '.' . $domain;
+                               }
+
+                               if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
+                                       return true;
+                               }
+                       }
+               }
+
+               return false;
+       }
+
        /**
         * Set the user agent
         * @param string $UA
index 1a92fb2..5b9bdfa 100644 (file)
@@ -565,17 +565,6 @@ class SiteConfiguration {
                return $multi ? $res : current( $res );
        }
 
-       /**
-        * Returns true if the given vhost is handled locally.
-        *
-        * @deprecated since 1.25; check if the host is in $wgLocalVirtualHosts instead.
-        * @param string $vhost
-        * @return bool
-        */
-       public function isLocalVHost( $vhost ) {
-               return in_array( $vhost, $this->localVHosts );
-       }
-
        /**
         * Merge multiple arrays together.
         * On encountering duplicate keys, merge the two, but ONLY if they're arrays.
index 8d0b8f1..0fc7980 100644 (file)
  * Functions related to the output of file content
  */
 class StreamFile {
-       const READY_STREAM = 1;
-       const NOT_MODIFIED = 2;
+       // Do not send any HTTP headers unless requested by caller (e.g. body only)
+       const STREAM_HEADLESS = 1;
+       // Do not try to tear down any PHP output buffers
+       const STREAM_ALLOW_OB = 2;
 
        /**
         * Stream a file to the browser, adding all the headings and fun stuff.
@@ -33,107 +35,183 @@ class StreamFile {
         * and Content-Disposition.
         *
         * @param string $fname Full name and path of the file to stream
-        * @param array $headers Any additional headers to send
+        * @param array $headers Any additional headers to send if the file exists
         * @param bool $sendErrors Send error messages if errors occur (like 404)
+        * @param array $optHeaders HTTP request header map (e.g. "range") (use lowercase keys)
+        * @param integer $flags Bitfield of STREAM_* constants
         * @throws MWException
         * @return bool Success
         */
-       public static function stream( $fname, $headers = [], $sendErrors = true ) {
+       public static function stream(
+               $fname, $headers = [], $sendErrors = true, $optHeaders = [], $flags = 0
+       ) {
+               $section = new ProfileSection( __METHOD__ );
 
                if ( FileBackend::isStoragePath( $fname ) ) { // sanity
                        throw new MWException( __FUNCTION__ . " given storage path '$fname'." );
                }
 
-               MediaWiki\suppressWarnings();
-               $stat = stat( $fname );
-               MediaWiki\restoreWarnings();
-
-               $res = self::prepareForStream( $fname, $stat, $headers, $sendErrors );
-               if ( $res == self::NOT_MODIFIED ) {
-                       $ok = true; // use client cache
-               } elseif ( $res == self::READY_STREAM ) {
-                       $ok = readfile( $fname );
-               } else {
-                       $ok = false; // failed
+               // Don't stream it out as text/html if there was a PHP error
+               if ( ( ( $flags & self::STREAM_HEADLESS ) == 0 || $headers ) && headers_sent() ) {
+                       echo "Headers already sent, terminating.\n";
+                       return false;
                }
 
-               return $ok;
-       }
+               $headerFunc = ( $flags & self::STREAM_HEADLESS )
+                       ? function ( $header ) {
+                                // no-op
+                       }
+                       : function ( $header ) {
+                               is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
+                       };
+
+               MediaWiki\suppressWarnings();
+               $info = stat( $fname );
+               MediaWiki\restoreWarnings();
 
-       /**
-        * Call this function used in preparation before streaming a file.
-        * This function does the following:
-        * (a) sends Last-Modified, Content-type, and Content-Disposition headers
-        * (b) cancels any PHP output buffering and automatic gzipping of output
-        * (c) sends Content-Length header based on HTTP_IF_MODIFIED_SINCE check
-        *
-        * @param string $path Storage path or file system path
-        * @param array|bool $info File stat info with 'mtime' and 'size' fields
-        * @param array $headers Additional headers to send
-        * @param bool $sendErrors Send error messages if errors occur (like 404)
-        * @return int|bool READY_STREAM, NOT_MODIFIED, or false on failure
-        */
-       public static function prepareForStream(
-               $path, $info, $headers = [], $sendErrors = true
-       ) {
                if ( !is_array( $info ) ) {
                        if ( $sendErrors ) {
-                               HttpStatus::header( 404 );
-                               header( 'Cache-Control: no-cache' );
-                               header( 'Content-Type: text/html; charset=utf-8' );
-                               $encFile = htmlspecialchars( $path );
-                               $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
-                               echo "<html><body>
-                                       <h1>File not found</h1>
-                                       <p>Although this PHP script ($encScript) exists, the file requested for output
-                                       ($encFile) does not.</p>
-                                       </body></html>
-                                       ";
+                               self::send404Message( $fname, $flags );
                        }
                        return false;
                }
 
-               // Sent Last-Modified HTTP header for client-side caching
-               header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $info['mtime'] ) );
+               // Send Last-Modified HTTP header for client-side caching
+               $headerFunc( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $info['mtime'] ) );
 
-               // Cancel output buffering and gzipping if set
-               wfResetOutputBuffers();
+               if ( ( $flags & self::STREAM_ALLOW_OB ) == 0 ) {
+                       // Cancel output buffering and gzipping if set
+                       wfResetOutputBuffers();
+               }
 
-               $type = self::contentTypeFromPath( $path );
+               $type = self::contentTypeFromPath( $fname );
                if ( $type && $type != 'unknown/unknown' ) {
-                       header( "Content-type: $type" );
+                       $headerFunc( "Content-type: $type" );
                } else {
                        // Send a content type which is not known to Internet Explorer, to
                        // avoid triggering IE's content type detection. Sending a standard
                        // unknown content type here essentially gives IE license to apply
                        // whatever content type it likes.
-                       header( 'Content-type: application/x-wiki' );
+                       $headerFunc( 'Content-type: application/x-wiki' );
                }
 
-               // Don't stream it out as text/html if there was a PHP error
-               if ( headers_sent() ) {
-                       echo "Headers already sent, terminating.\n";
-                       return false;
+               // Don't send if client has up to date cache
+               if ( isset( $optHeaders['if-modified-since'] ) ) {
+                       $modsince = preg_replace( '/;.*$/', '', $optHeaders['if-modified-since'] );
+                       if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) {
+                               ini_set( 'zlib.output_compression', 0 );
+                               $headerFunc( 304 );
+                               return true; // ok
+                       }
                }
 
                // Send additional headers
                foreach ( $headers as $header ) {
-                       header( $header );
+                       header( $header ); // always use header(); specifically requested
                }
 
-               // Don't send if client has up to date cache
-               if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
-                       $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
-                       if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) {
-                               ini_set( 'zlib.output_compression', 0 );
-                               HttpStatus::header( 304 );
-                               return self::NOT_MODIFIED; // ok
+               if ( isset( $optHeaders['range'] ) ) {
+                       $range = self::parseRange( $optHeaders['range'], $info['size'] );
+                       if ( is_array( $range ) ) {
+                               $headerFunc( 206 );
+                               $headerFunc( 'Content-Length: ' . $range[2] );
+                               $headerFunc( "Content-Range: bytes {$range[0]}-{$range[1]}/{$info['size']}" );
+                       } elseif ( $range === 'invalid' ) {
+                               if ( $sendErrors ) {
+                                       $headerFunc( 416 );
+                                       $headerFunc( 'Cache-Control: no-cache' );
+                                       $headerFunc( 'Content-Type: text/html; charset=utf-8' );
+                                       $headerFunc( 'Content-Range: bytes */' . $info['size'] );
+                               }
+                               return false;
+                       } else { // unsupported Range request (e.g. multiple ranges)
+                               $range = null;
+                               $headerFunc( 'Content-Length: ' . $info['size'] );
+                       }
+               } else {
+                       $range = null;
+                       $headerFunc( 'Content-Length: ' . $info['size'] );
+               }
+
+               if ( is_array( $range ) ) {
+                       $handle = fopen( $fname, 'rb' );
+                       if ( $handle ) {
+                               $ok = true;
+                               fseek( $handle, $range[0] );
+                               $remaining = $range[2];
+                               while ( $remaining > 0 && $ok ) {
+                                       $bytes = min( $remaining, 8 * 1024 );
+                                       $data = fread( $handle, $bytes );
+                                       $remaining -= $bytes;
+                                       $ok = ( $data !== false );
+                                       print $data;
+                               }
+                       } else {
+                               return false;
                        }
+               } else {
+                       return readfile( $fname ) !== false; // faster
                }
 
-               header( 'Content-Length: ' . $info['size'] );
+               return true;
+       }
+
+       /**
+        * Send out a standard 404 message for a file
+        *
+        * @param string $fname Full name and path of the file to stream
+        * @param integer $flags Bitfield of STREAM_* constants
+        * @since 1.24
+        */
+       public static function send404Message( $fname, $flags = 0 ) {
+               if ( ( $flags & self::STREAM_HEADLESS ) == 0 ) {
+                       HttpStatus::header( 404 );
+                       header( 'Cache-Control: no-cache' );
+                       header( 'Content-Type: text/html; charset=utf-8' );
+               }
+               $encFile = htmlspecialchars( $fname );
+               $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
+               echo "<!DOCTYPE html><html><body>
+                       <h1>File not found</h1>
+                       <p>Although this PHP script ($encScript) exists, the file requested for output
+                       ($encFile) does not.</p>
+                       </body></html>
+                       ";
+       }
 
-               return self::READY_STREAM; // ok
+       /**
+        * Convert a Range header value to an absolute (start, end) range tuple
+        *
+        * @param string $range Range header value
+        * @param integer $size File size
+        * @return array|string Returns error string on failure (start, end, length)
+        * @since 1.24
+        */
+       public static function parseRange( $range, $size ) {
+               $m = [];
+               if ( preg_match( '#^bytes=(\d*)-(\d*)$#', $range, $m ) ) {
+                       list( , $start, $end ) = $m;
+                       if ( $start === '' && $end === '' ) {
+                               $absRange = [ 0, $size - 1 ];
+                       } elseif ( $start === '' ) {
+                               $absRange = [ $size - $end, $size - 1 ];
+                       } elseif ( $end === '' ) {
+                               $absRange = [ $start, $size - 1 ];
+                       } else {
+                               $absRange = [ $start, $end ];
+                       }
+                       if ( $absRange[0] >= 0 && $absRange[1] >= $absRange[0] ) {
+                               if ( $absRange[0] < $size ) {
+                                       $absRange[1] = min( $absRange[1], $size - 1 ); // stop at EOF
+                                       $absRange[2] = $absRange[1] - $absRange[0] + 1;
+                                       return $absRange;
+                               } elseif ( $absRange[0] == 0 && $size == 0 ) {
+                                       return 'unrecognized'; // the whole file should just be sent
+                               }
+                       }
+                       return 'invalid';
+               }
+               return 'unrecognized';
        }
 
        /**
index 3dd37d7..62f4060 100644 (file)
@@ -1773,12 +1773,13 @@ class Title implements LinkTarget {
         *
         * @param array $query
         * @param bool $query2
-        * @param string $proto Protocol to use; setting this will cause a full URL to be used
+        * @param string|int|bool $proto A PROTO_* constant on how the URL should be expanded,
+        *                               or false (default) for no expansion
         * @see self::getLocalURL for the arguments.
         * @return string The URL
         */
-       public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
-               if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
+       public function getLinkURL( $query = '', $query2 = false, $proto = false ) {
+               if ( $this->isExternal() || $proto !== false ) {
                        $ret = $this->getFullURL( $query, $query2, $proto );
                } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
                        $ret = $this->getFragmentForURL();
index 515fbfc..89ca50c 100644 (file)
@@ -719,28 +719,29 @@ class WatchedItemStore implements StatsdAwareInterface {
         */
        public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
                $dbw = $this->getConnection( DB_MASTER );
-               $res = $dbw->select( [ 'watchlist' ],
-                       [ 'wl_user' ],
+               $uids = $dbw->selectFieldValues(
+                       'watchlist',
+                       'wl_user',
                        [
                                'wl_user != ' . intval( $editor->getId() ),
                                'wl_namespace' => $target->getNamespace(),
                                'wl_title' => $target->getDBkey(),
                                'wl_notificationtimestamp IS NULL',
-                       ], __METHOD__
+                       ],
+                       __METHOD__
                );
+               $this->reuseConnection( $dbw );
 
-               $watchers = [];
-               foreach ( $res as $row ) {
-                       $watchers[] = intval( $row->wl_user );
-               }
-
+               $watchers = array_map( 'intval', $uids );
                if ( $watchers ) {
                        // Update wl_notificationtimestamp for all watching users except the editor
                        $fname = __METHOD__;
-                       $dbw->onTransactionIdle(
-                               function () use ( $dbw, $timestamp, $watchers, $target, $fname ) {
+                       DeferredUpdates::addCallableUpdate(
+                               function () use ( $timestamp, $watchers, $target, $fname ) {
                                        global $wgUpdateRowsPerQuery;
 
+                                       $dbw = $this->getConnection( DB_MASTER );
+
                                        $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery );
                                        foreach ( $watchersChunks as $watchersChunk ) {
                                                $dbw->update( 'watchlist',
@@ -758,12 +759,12 @@ class WatchedItemStore implements StatsdAwareInterface {
                                                }
                                        }
                                        $this->uncacheLinkTarget( $target );
+
+                                       $this->reuseConnection( $dbw );
                                }
                        );
                }
 
-               $this->reuseConnection( $dbw );
-
                return $watchers;
        }
 
index 3572229..0e4c6e0 100644 (file)
@@ -208,7 +208,7 @@ class ApiLogin extends ApiBase {
 
                                // Deprecated hook
                                $injected_html = '';
-                               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
+                               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html, true ] );
 
                                $result['lguserid'] = intval( $user->getId() );
                                $result['lgusername'] = $user->getName();
index 64bb9ba..822369a 100644 (file)
@@ -68,35 +68,37 @@ class ApiPurge extends ApiBase {
 
                                        # Parse content; note that HTML generation is only needed if we want to cache the result.
                                        $content = $page->getContent( Revision::RAW );
-                                       $enableParserCache = $this->getConfig()->get( 'EnableParserCache' );
-                                       $p_result = $content->getParserOutput(
-                                               $title,
-                                               $page->getLatest(),
-                                               $popts,
-                                               $enableParserCache
-                                       );
-
-                                       # Logging to better see expensive usage patterns
-                                       if ( $forceRecursiveLinkUpdate ) {
-                                               LoggerFactory::getInstance( 'RecursiveLinkPurge' )->info(
-                                                       "Recursive link purge enqueued for {title}",
-                                                       [
-                                                               'user' => $this->getUser()->getName(),
-                                                               'title' => $title->getPrefixedText()
-                                                       ]
+                                       if ( $content ) {
+                                               $enableParserCache = $this->getConfig()->get( 'EnableParserCache' );
+                                               $p_result = $content->getParserOutput(
+                                                       $title,
+                                                       $page->getLatest(),
+                                                       $popts,
+                                                       $enableParserCache
                                                );
-                                       }
-
-                                       # Update the links tables
-                                       $updates = $content->getSecondaryDataUpdates(
-                                               $title, null, $forceRecursiveLinkUpdate, $p_result );
-                                       DataUpdate::runUpdates( $updates );
-
-                                       $r['linkupdate'] = true;
 
-                                       if ( $enableParserCache ) {
-                                               $pcache = ParserCache::singleton();
-                                               $pcache->save( $p_result, $page, $popts );
+                                               # Logging to better see expensive usage patterns
+                                               if ( $forceRecursiveLinkUpdate ) {
+                                                       LoggerFactory::getInstance( 'RecursiveLinkPurge' )->info(
+                                                               "Recursive link purge enqueued for {title}",
+                                                               [
+                                                                       'user' => $this->getUser()->getName(),
+                                                                       'title' => $title->getPrefixedText()
+                                                               ]
+                                                       );
+                                               }
+
+                                               # Update the links tables
+                                               $updates = $content->getSecondaryDataUpdates(
+                                                       $title, null, $forceRecursiveLinkUpdate, $p_result );
+                                               DataUpdate::runUpdates( $updates );
+
+                                               $r['linkupdate'] = true;
+
+                                               if ( $enableParserCache ) {
+                                                       $pcache = ParserCache::singleton();
+                                                       $pcache->save( $p_result, $page, $popts );
+                                               }
                                        }
                                } else {
                                        $error = $this->parseMsg( [ 'actionthrottledtext' ] );
index 6a2f836..57e124c 100644 (file)
        "api-help-examples": "{{PLURAL:$1|Esempio|Esempi}}:",
        "api-help-permissions": "{{PLURAL:$1|Permesso|Permessi}}:",
        "api-help-open-in-apisandbox": "<small>[apri in una sandbox]</small>",
-       "api-help-authmanager-general-usage": "La procedura generale per usare questo modulo Ã©:\n# Ottenere i campi disponibili da <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> con <kbd>amirequestsfor=$4</kbd>, e un token <kbd>$5</kbd> da <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.\n# Mostra i campi all'utente e ottieni i dati che invia.\n# Esegui un post a questo modulo, fornendo <var>$1returnurl</var> e ogni campo rilevante.\n# Controlla <samp>status</samp> nella response.\n#* Se hai ricevuto <samp>PASS</samp> o <samp>FAIL</samp>, hai finito. L'operazione nel primo caso è andata a buon fine, nel secondo no.\n#* Se hai ricevuto <samp>UI</samp>, mostra i nuovi campi all'utente e ottieni i dati che invia. Esegui un post a questo modulo con <var>$1continue</var> e i campi rilevanti settati, quindi ripeti il punto 4.\n#* Se hai ricevuto <samp>REDIRECT</samp>, dirigi l'utente a <samp>redirecttarget</samp> e aspetta che ritorni a <var>$1returnurl</var>. A quel punto esegui un post a questo modulo con <var>$1continue</var> e ogni campo passato all'URL di ritorno, e ripeti il punto 4.\n#* Se hai ricevuto <samp>RESTART</samp>, vuol dire che l'autenticazione ha funzionato ma non abbiamo un account collegato. Potresti considerare questo caso come <samp>UI</samp> o come <samp>FAIL</samp>.",
+       "api-help-authmanager-general-usage": "La procedura generale per usare questo modulo Ã¨:\n# Ottenere i campi disponibili da <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> con <kbd>amirequestsfor=$4</kbd>, e un token <kbd>$5</kbd> da <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.\n# Mostra i campi all'utente e ottieni i dati che invia.\n# Esegui un post a questo modulo, fornendo <var>$1returnurl</var> e ogni campo rilevante.\n# Controlla <samp>status</samp> nella response.\n#* Se hai ricevuto <samp>PASS</samp> o <samp>FAIL</samp>, hai finito. L'operazione nel primo caso è andata a buon fine, nel secondo no.\n#* Se hai ricevuto <samp>UI</samp>, mostra i nuovi campi all'utente e ottieni i dati che invia. Esegui un post a questo modulo con <var>$1continue</var> e i campi rilevanti settati, quindi ripeti il punto 4.\n#* Se hai ricevuto <samp>REDIRECT</samp>, dirigi l'utente a <samp>redirecttarget</samp> e aspetta che ritorni a <var>$1returnurl</var>. A quel punto esegui un post a questo modulo con <var>$1continue</var> e ogni campo passato all'URL di ritorno, e ripeti il punto 4.\n#* Se hai ricevuto <samp>RESTART</samp>, vuol dire che l'autenticazione ha funzionato ma non abbiamo un account collegato. Potresti considerare questo caso come <samp>UI</samp> o come <samp>FAIL</samp>.",
        "api-help-authmanagerhelper-messageformat": "Formato da utilizzare per per la restituzione dei messaggi.",
        "api-help-authmanagerhelper-preservestate": "Conserva lo stato da un precedente tentativo di accesso non riuscito, se possibile.",
        "api-help-authmanagerhelper-returnurl": "URL di ritorno per i flussi di autenticazione di terze parti, deve essere assoluto. E' necessario fornirlo, oppure va fornito <var>$1continue</var>.\n\nAlla ricezione di una risposta <samp>REDIRECT</samp>, in genere si apre un browser o una vista web all'URL specificato <samp>redirecttarget</samp> per un flusso di autenticazione di terze parti. Quando questo è completato, la terza parte invierà il browser o la vista web a questo URL. Dovresti estrarre qualsiasi parametro POST o della richiesta dall'URL e passarli come un request <var>$1continue</var> a questo modulo API.",
index 27f917b..b956d4b 100644 (file)
@@ -93,7 +93,14 @@ class IcuCollation extends Collation {
                'be-tarask' => [ "Ё" ],
                'cy' => [ "Ch", "Dd", "Ff", "Ng", "Ll", "Ph", "Rh", "Th" ],
                'en' => [],
-               'fa' => [ "آ", "ء", "ه" ],
+               // RTL, let's put each letter on a new line
+               'fa' => [
+                       "آ",
+                       "ء",
+                       "ه",
+                       "ا",
+                       "و"
+               ],
                'fi' => [ "Å", "Ä", "Ö" ],
                'fr' => [],
                'hu' => [ "Cs", "Dz", "Dzs", "Gy", "Ly", "Ny", "Ö", "Sz", "Ty", "Ü", "Zs" ],
index 3ebc3ec..02a8d30 100644 (file)
@@ -36,6 +36,8 @@ abstract class DatabaseMysqlBase extends Database {
        protected $lagDetectionMethod;
        /** @var array Method to detect slave lag */
        protected $lagDetectionOptions = [];
+       /** @var bool bool Whether to use GTID methods */
+       protected $useGTIDs = false;
 
        /** @var string|null */
        private $serverVersion = null;
@@ -43,13 +45,14 @@ abstract class DatabaseMysqlBase extends Database {
        /**
         * Additional $params include:
         *   - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
-        *                          pt-heartbeat assumes the table is at heartbeat.heartbeat
-        *                          and uses UTC timestamps in the heartbeat.ts column.
-        *                          (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
+        *       pt-heartbeat assumes the table is at heartbeat.heartbeat
+        *       and uses UTC timestamps in the heartbeat.ts column.
+        *       (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
         *   - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
-        *                           the default behavior. Normally, the heartbeat row with the server
-        *                           ID of this server's master will be used. Set the "conds" field to
-        *                           override the query conditions, e.g. ['shard' => 's1'].
+        *       the default behavior. Normally, the heartbeat row with the server
+        *       ID of this server's master will be used. Set the "conds" field to
+        *       override the query conditions, e.g. ['shard' => 's1'].
+        *   - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
         * @param array $params
         */
        function __construct( array $params ) {
@@ -61,6 +64,7 @@ abstract class DatabaseMysqlBase extends Database {
                $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
                        ? $params['lagDetectionOptions']
                        : [];
+               $this->useGTIDs = !empty( $params['useGTIDs' ] );
        }
 
        /**
@@ -788,13 +792,20 @@ abstract class DatabaseMysqlBase extends Database {
                        return 0; // already reached this point for sure
                }
 
-               # Commit any open transactions
+               // Commit any open transactions
                $this->commit( __METHOD__, 'flush' );
 
-               # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
-               $encFile = $this->addQuotes( $pos->file );
-               $encPos = intval( $pos->pos );
-               $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
+               // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+               if ( $this->useGTIDs && $pos->gtids ) {
+                       // Wait on the GTID set (MariaDB only)
+                       $gtidArg = implode( ',', $pos->gtids );
+                       $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+               } else {
+                       // Wait on the binlog coordinates
+                       $encFile = $this->addQuotes( $pos->file );
+                       $encPos = intval( $pos->pos );
+                       $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
+               }
 
                $row = $res ? $this->fetchRow( $res ) : false;
                if ( !$row ) {
@@ -827,15 +838,23 @@ abstract class DatabaseMysqlBase extends Database {
         * @return MySQLMasterPos|bool
         */
        function getSlavePos() {
-               $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' );
+               $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
                $row = $this->fetchObject( $res );
 
                if ( $row ) {
                        $pos = isset( $row->Exec_master_log_pos )
                                ? $row->Exec_master_log_pos
                                : $row->Exec_Master_Log_Pos;
+                       // Also fetch the last-applied GTID set (MariaDB)
+                       if ( $this->useGTIDs ) {
+                               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
+                               $gtidRow = $this->fetchObject( $res );
+                               $gtidSet = $gtidRow ? $gtidRow->Value : '';
+                       } else {
+                               $gtidSet = '';
+                       }
 
-                       return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
+                       return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
                } else {
                        return false;
                }
@@ -847,11 +866,20 @@ abstract class DatabaseMysqlBase extends Database {
         * @return MySQLMasterPos|bool
         */
        function getMasterPos() {
-               $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' );
+               $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
                $row = $this->fetchObject( $res );
 
                if ( $row ) {
-                       return new MySQLMasterPos( $row->File, $row->Position );
+                       // Also fetch the last-written GTID set (MariaDB)
+                       if ( $this->useGTIDs ) {
+                               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
+                               $gtidRow = $this->fetchObject( $res );
+                               $gtidSet = $gtidRow ? $gtidRow->Value : '';
+                       } else {
+                               $gtidSet = '';
+                       }
+
+                       return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
                } else {
                        return false;
                }
@@ -1443,20 +1471,43 @@ class MySQLField implements Field {
        }
 }
 
+/**
+ * DBMasterPos class for MySQL/MariaDB
+ *
+ * Note that master positions and sync logic here make some assumptions:
+ *  - Binlog-based usage assumes single-source replication and non-hierarchical replication.
+ *  - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
+ *    that GTID sets are complete (e.g. include all domains on the server).
+ */
 class MySQLMasterPos implements DBMasterPos {
-       /** @var string */
+       /** @var string Binlog file */
        public $file;
-       /** @var int Position */
+       /** @var int Binglog file position */
        public $pos;
+       /** @var string[] GTID list */
+       public $gtids = [];
        /** @var float UNIX timestamp */
        public $asOfTime = 0.0;
 
-       function __construct( $file, $pos ) {
+       /**
+        * @param string $file Binlog file name
+        * @param integer $pos Binlog position
+        * @param string $gtid Comma separated GTID set [optional]
+        */
+       function __construct( $file, $pos, $gtid = '' ) {
                $this->file = $file;
                $this->pos = $pos;
+               $this->gtids = array_map( 'trim', explode( ',', $gtid ) );
                $this->asOfTime = microtime( true );
        }
 
+       /**
+        * @return string <binlog file>/<position>, e.g db1034-bin.000976/843431247
+        */
+       function __toString() {
+               return "{$this->file}/{$this->pos}";
+       }
+
        function asOfTime() {
                return $this->asOfTime;
        }
@@ -1466,10 +1517,29 @@ class MySQLMasterPos implements DBMasterPos {
                        throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
                }
 
-               $thisPos = $this->getCoordinates();
-               $thatPos = $pos->getCoordinates();
+               // Prefer GTID comparisons, which work with multi-tier replication
+               $thisPosByDomain = $this->getGtidCoordinates();
+               $thatPosByDomain = $pos->getGtidCoordinates();
+               if ( $thisPosByDomain && $thatPosByDomain ) {
+                       $reached = true;
+                       // Check that this has positions GTE all of those in $pos for all domains in $pos
+                       foreach ( $thatPosByDomain as $domain => $thatPos ) {
+                               $thisPos = isset( $thisPosByDomain[$domain] ) ? $thisPosByDomain[$domain] : -1;
+                               $reached = $reached && ( $thatPos <= $thisPos );
+                       }
 
-               return ( $thisPos && $thatPos && $thisPos >= $thatPos );
+                       return $reached;
+               }
+
+               // Fallback to the binlog file comparisons
+               $thisBinPos = $this->getBinlogCoordinates();
+               $thatBinPos = $pos->getBinlogCoordinates();
+               if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) {
+                       return ( $thisBinPos['pos'] >= $thatBinPos['pos'] );
+               }
+
+               // Comparing totally different binlogs does not make sense
+               return false;
        }
 
        function channelsMatch( DBMasterPos $pos ) {
@@ -1477,36 +1547,56 @@ class MySQLMasterPos implements DBMasterPos {
                        throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
                }
 
-               $thisBinlog = $this->getBinlogName();
-               $thatBinlog = $pos->getBinlogName();
+               // Prefer GTID comparisons, which work with multi-tier replication
+               $thisPosDomains = array_keys( $this->getGtidCoordinates() );
+               $thatPosDomains = array_keys( $pos->getGtidCoordinates() );
+               if ( $thisPosDomains && $thatPosDomains ) {
+                       // Check that this has GTIDs for all domains in $pos
+                       return !array_diff( $thatPosDomains, $thisPosDomains );
+               }
 
-               return ( $thisBinlog !== false && $thisBinlog === $thatBinlog );
-       }
+               // Fallback to the binlog file comparisons
+               $thisBinPos = $this->getBinlogCoordinates();
+               $thatBinPos = $pos->getBinlogCoordinates();
 
-       function __toString() {
-               // e.g db1034-bin.000976/843431247
-               return "{$this->file}/{$this->pos}";
+               return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] );
        }
 
        /**
-        * @return string|bool
+        * @note: this returns false for multi-source replication GTID sets
+        * @see https://mariadb.com/kb/en/mariadb/gtid
+        * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
+        * @return array Map of (domain => integer position) or false
         */
-       protected function getBinlogName() {
-               $m = [];
-               if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
-                       return $m[1];
+       protected function getGtidCoordinates() {
+               $gtidInfos = [];
+               foreach ( $this->gtids as $gtid ) {
+                       $m = [];
+                       // MariaDB style: <domain>-<server id>-<sequence number>
+                       if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
+                               $gtidInfos[(int)$m[1]] = (int)$m[2];
+                       // MySQL style: <UUID domain>:<sequence number>
+                       } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
+                               $gtidInfos[$m[1]] = (int)$m[2];
+                       } else {
+                               $gtidInfos = [];
+                               break; // unrecognized GTID
+                       }
+
                }
 
-               return false;
+               return $gtidInfos;
        }
 
        /**
-        * @return array|bool (int, int)
+        * @see http://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
+        * @see http://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
+        * @return array|bool (binlog, (integer file number, integer position)) or false
         */
-       protected function getCoordinates() {
+       protected function getBinlogCoordinates() {
                $m = [];
-               if ( preg_match( '!\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
-                       return [ (int)$m[1], (int)$m[2] ];
+               if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
+                       return [ 'binlog' => $m[1], 'pos' => [ (int)$m[2], (int)$m[3] ] ];
                }
 
                return false;
index a7c39ca..0009781 100644 (file)
@@ -61,6 +61,8 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                // This handles the case when updates have to batched into several COMMITs.
                $scopedLock = LinksUpdate::acquirePageLock( $this->mDb, $id );
 
+               $title = $this->page->getTitle();
+
                // Delete restrictions for it
                $this->mDb->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
 
@@ -80,6 +82,20 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                        }
                }
 
+               // Refresh the category table entry if it seems to have no pages. Check
+               // master for the most up-to-date cat_pages count.
+               if ( $title->getNamespace() === NS_CATEGORY ) {
+                       $row = $this->mDb->selectRow(
+                               'category',
+                               [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
+                               [ 'cat_title' => $title->getDBkey(), 'cat_pages <= 0' ],
+                               __METHOD__
+                       );
+                       if ( $row ) {
+                               $cat = Category::newFromRow( $row, $title )->refreshCounts();
+                       }
+               }
+
                // If using cascading deletes, we can skip some explicit deletes
                if ( !$this->mDb->cascadingDeletes() ) {
                        // Delete outgoing links
@@ -132,7 +148,6 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
 
                // If using cleanup triggers, we can skip some manual deletes
                if ( !$this->mDb->cleanupTriggers() ) {
-                       $title = $this->page->getTitle();
                        // Find recentchanges entries to clean up...
                        $rcIdsForTitle = $this->mDb->selectFieldValues(
                                'recentchanges',
@@ -164,10 +179,8 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate
                        }
                }
 
-               $this->mDb->onTransactionIdle( function() use ( &$scopedLock ) {
-                       // Release the lock *after* the final COMMIT for correctness
-                       ScopedCallback::consume( $scopedLock );
-               } );
+               // Commit and release the lock
+               ScopedCallback::consume( $scopedLock );
        }
 
        private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) {
index d4a61fa..22944eb 100644 (file)
@@ -168,18 +168,16 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
         *
         * @param IDatabase $dbw
         * @param integer $pageId
-        * @return ScopedCallback|null Returns null on failure
+        * @param string $why One of (job, atomicity)
+        * @return ScopedCallback
         * @throws RuntimeException
         * @since 1.27
         */
-       public static function acquirePageLock( IDatabase $dbw, $pageId ) {
-               $scopedLock = $dbw->getScopedLockAndFlush(
-                       "LinksUpdate:pageid:$pageId",
-                       __METHOD__,
-                       15
-               );
+       public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) {
+               $key = "LinksUpdate:$why:pageid:$pageId";
+               $scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 );
                if ( !$scopedLock ) {
-                       throw new RuntimeException( "Could not acquire lock on page #$pageId." );
+                       throw new RuntimeException( "Could not acquire lock '$key'." );
                }
 
                return $scopedLock;
index 5a62185..b8e2726 100644 (file)
@@ -74,7 +74,7 @@ class SiteStatsUpdate implements DeferrableUpdate {
                        $this->doUpdatePendingDeltas();
                } else {
                        // Need a separate transaction because this a global lock
-                       wfGetDB( DB_MASTER )->onTransactionIdle( [ $this, 'tryDBUpdateInternal' ] );
+                       DeferredUpdates::addCallableUpdate( [ $this, 'tryDBUpdateInternal' ] );
                }
        }
 
index 03974f7..10183f4 100644 (file)
@@ -1005,15 +1005,21 @@ abstract class FileBackend {
 
        /**
         * Stream the file at a storage path in the backend.
+        *
         * If the file does not exists, an HTTP 404 error will be given.
         * Appropriate HTTP headers (Status, Content-Type, Content-Length)
         * will be sent if streaming began, while none will be sent otherwise.
         * Implementations should flush the output buffer before sending data.
         *
         * @param array $params Parameters include:
-        *   - src     : source storage path
-        *   - headers : list of additional HTTP headers to send on success
-        *   - latest  : use the latest available data
+        *   - src      : source storage path
+        *   - headers  : list of additional HTTP headers to send if the file exists
+        *   - options  : HTTP request header map with lower case keys (since 1.28). Supports:
+        *                range             : format is "bytes=(\d*-\d*)"
+        *                if-modified-since : format is an HTTP date
+        *   - headless : only include the body (and headers from "headers") (since 1.28)
+        *   - latest   : use the latest available data
+        *   - allowOB  : preserve any output buffers (since 1.28)
         * @return Status
         */
        abstract public function streamFile( array $params );
index 4d9587e..a29119c 100644 (file)
@@ -844,30 +844,19 @@ abstract class FileBackendStore extends FileBackend {
                $ps = Profiler::instance()->scopedProfileIn( __METHOD__ . "-{$this->name}" );
                $status = Status::newGood();
 
-               $info = $this->getFileStat( $params );
-               if ( !$info ) { // let StreamFile handle the 404
-                       $status->fatal( 'backend-fail-notexists', $params['src'] );
-               }
-
-               // Set output buffer and HTTP headers for stream
-               $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : [];
-               $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
-               if ( $res == StreamFile::NOT_MODIFIED ) {
-                       // do nothing; client cache is up to date
-               } elseif ( $res == StreamFile::READY_STREAM ) {
-                       $status = $this->doStreamFile( $params );
-                       if ( !$status->isOK() ) {
-                               // Per bug 41113, nasty things can happen if bad cache entries get
-                               // stuck in cache. It's also possible that this error can come up
-                               // with simple race conditions. Clear out the stat cache to be safe.
-                               $this->clearCache( [ $params['src'] ] );
-                               $this->deleteFileCache( $params['src'] );
-                               trigger_error( "Bad stat cache or race condition for file {$params['src']}." );
-                       }
-               } else {
+               // Always set some fields for subclass convenience
+               $params['options'] = isset( $params['options'] ) ? $params['options'] : [];
+               $params['headers'] = isset( $params['headers'] ) ? $params['headers'] : [];
+
+               // Don't stream it out as text/html if there was a PHP error
+               if ( ( empty( $params['headless'] ) || $params['headers'] ) && headers_sent() ) {
+                       print "Headers already sent, terminating.\n";
                        $status->fatal( 'backend-fail-stream', $params['src'] );
+                       return $status;
                }
 
+               $status->merge( $this->doStreamFile( $params ) );
+
                return $status;
        }
 
@@ -879,10 +868,21 @@ abstract class FileBackendStore extends FileBackend {
        protected function doStreamFile( array $params ) {
                $status = Status::newGood();
 
+               $flags = 0;
+               $flags |= !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
+               $flags |= !empty( $params['allowOB'] ) ? StreamFile::STREAM_ALLOW_OB : 0;
+
                $fsFile = $this->getLocalReference( $params );
-               if ( !$fsFile ) {
-                       $status->fatal( 'backend-fail-stream', $params['src'] );
-               } elseif ( !readfile( $fsFile->getPath() ) ) {
+
+               if ( $fsFile ) {
+                       $res = StreamFile::stream( $fsFile->getPath(),
+                               $params['headers'], true, $params['options'], $flags );
+               } else {
+                       $res = false;
+                       StreamFile::send404Message( $params['src'], $flags );
+               }
+
+               if ( !$res ) {
                        $status->fatal( 'backend-fail-stream', $params['src'] );
                }
 
index 6e32c62..e2c1ede 100644 (file)
@@ -183,21 +183,6 @@ class MemoryFileBackend extends FileBackendStore {
                return $tmpFiles;
        }
 
-       protected function doStreamFile( array $params ) {
-               $status = Status::newGood();
-
-               $src = $this->resolveHashKey( $params['src'] );
-               if ( $src === null || !isset( $this->files[$src] ) ) {
-                       $status->fatal( 'backend-fail-stream', $params['src'] );
-
-                       return $status;
-               }
-
-               print $this->files[$src]['data'];
-
-               return $status;
-       }
-
        protected function doDirectoryExists( $container, $dir, array $params ) {
                $prefix = rtrim( "$container/$dir", '/' ) . '/';
                foreach ( $this->files as $path => $data ) {
index 0f7e4b5..2adf934 100644 (file)
@@ -1045,32 +1045,62 @@ class SwiftFileBackend extends FileBackendStore {
        protected function doStreamFile( array $params ) {
                $status = Status::newGood();
 
+               $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
+
                list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
                if ( $srcRel === null ) {
+                       StreamFile::send404Message( $params['src'], $flags );
                        $status->fatal( 'backend-fail-invalidpath', $params['src'] );
+
+                       return $status;
                }
 
                $auth = $this->getAuthentication();
                if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
+                       StreamFile::send404Message( $params['src'], $flags );
                        $status->fatal( 'backend-fail-stream', $params['src'] );
 
                        return $status;
                }
 
-               $handle = fopen( 'php://output', 'wb' );
+               // If "headers" is set, we only want to send them if the file is there.
+               // Do not bother checking if the file exists if headers are not set though.
+               if ( $params['headers'] && !$this->fileExists( $params ) ) {
+                       StreamFile::send404Message( $params['src'], $flags );
+                       $status->fatal( 'backend-fail-stream', $params['src'] );
 
+                       return $status;
+               }
+
+               // Send the requested additional headers
+               foreach ( $params['headers'] as $header ) {
+                       header( $header ); // aways send
+               }
+
+               if ( empty( $params['allowOB'] ) ) {
+                       // Cancel output buffering and gzipping if set
+                       wfResetOutputBuffers();
+               }
+
+               $handle = fopen( 'php://output', 'wb' );
                list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
                        'method' => 'GET',
                        'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
                        'headers' => $this->authTokenHeaders( $auth )
-                               + $this->headersFromParams( $params ),
+                               + $this->headersFromParams( $params ) + $params['options'],
                        'stream' => $handle,
+                       'flags'  => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
                ] );
 
                if ( $rcode >= 200 && $rcode <= 299 ) {
                        // good
                } elseif ( $rcode === 404 ) {
                        $status->fatal( 'backend-fail-stream', $params['src'] );
+                       // Per bug 41113, nasty things can happen if bad cache entries get
+                       // stuck in cache. It's also possible that this error can come up
+                       // with simple race conditions. Clear out the stat cache to be safe.
+                       $this->clearCache( [ $params['src'] ] );
+                       $this->deleteFileCache( $params['src'] );
                } else {
                        $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
                }
index d7559d0..4ab913d 100644 (file)
@@ -1585,12 +1585,13 @@ class FileRepo {
         *
         * @param string $virtualUrl
         * @param array $headers Additional HTTP headers to send on success
+        * @param array $optHeaders HTTP request headers (if-modified-since, range, ...)
         * @return Status
         * @since 1.27
         */
-       public function streamFileWithStatus( $virtualUrl, $headers = [] ) {
+       public function streamFileWithStatus( $virtualUrl, $headers = [], $optHeaders = [] ) {
                $path = $this->resolveToStoragePath( $virtualUrl );
-               $params = [ 'src' => $path, 'headers' => $headers ];
+               $params = [ 'src' => $path, 'headers' => $headers, 'options' => $optHeaders ];
 
                return $this->backend->streamFile( $params );
        }
index 2fb2281..f6527b8 100644 (file)
@@ -189,8 +189,16 @@ class TraditionalImageGallery extends ImageGalleryBase {
                                // Preloaded into LinkCache above
                                Linker::linkKnown(
                                        $nt,
-                                       htmlspecialchars( $lang->truncate( $nt->getText(), $this->mCaptionLength ) )
-                               ) . "<br />\n" :
+                                       htmlspecialchars(
+                                               $this->mCaptionLength !== true ?
+                                                       $lang->truncate( $nt->getText(), $this->mCaptionLength ) :
+                                                       $nt->getText()
+                                       ),
+                                       [
+                                               'class' => 'galleryfilename' .
+                                                       ( $this->mCaptionLength === true ? ' galleryfilename-truncate' : '' )
+                                       ]
+                               ) . "\n" :
                                '';
 
                        $galleryText = $textlink . $text . $fileSize;
index 6a20abc..0d8137c 100644 (file)
@@ -75,6 +75,7 @@ abstract class DatabaseUpdater {
                PopulateFilearchiveSha1::class,
                PopulateBacklinkNamespace::class,
                FixDefaultJsonContentPages::class,
+               CleanupEmptyCategories::class,
        ];
 
        /**
index 5e9c34d..f23fbed 100644 (file)
@@ -80,8 +80,8 @@
        "config-no-scaling": "GD 라이브러리나 ImageMagick를 찾을 수 없습니다.\n그림 섬네일이 비활성화됩니다.",
        "config-no-uri": "<strong>오류:</strong> 현재 URI를 확인할 수 없습니다.\n설치가 중단되었습니다.",
        "config-no-cli-uri": "<strong>경고:</strong> 기본값을 사용하여 <code>--scriptpath</code>를 지정하지 않았습니다: <code>$1</code>.",
-       "config-using-server": "\"<nowiki>$1</nowiki>\"(을)를 서버 이름으로 사용합니다.",
-       "config-using-uri": "\"<nowiki>$1$2</nowiki>\"(을)를 서버 URL로 사용합니다.",
+       "config-using-server": "\"<nowiki>$1</nowiki>\" 서버 이름을 사용 중입니다.",
+       "config-using-uri": "\"<nowiki>$1$2</nowiki>\" 서버 URL을 사용 중입니다.",
        "config-uploads-not-safe": "<strong>경고:</strong> 올리기에 대한 기본 디렉터리(<code>$1</code>)는 임의의 스크립트 실행에 취약합니다.\n미디어위키는 보안 위협 때문에 모든 올려진 파일을 검사하지만, 올리기를 활성화하기 전에 [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Security#Upload_security 이 보안 취약점을 해결할 것]을 매우 권장합니다.",
        "config-no-cli-uploads-check": "<strong>경고:</strong> 올리기를 위한 기본 디렉터리(<code>$1</code>)는 CLI를 설치하는 동안 임의의 스크립트 실행에 대한 취약점에 대해 검사되지 않습니다.",
        "config-brokenlibxml": "시스템에 버그가 있는 PHP와 libxml2의 조합이 있으며 미디어위키나 다른 웹 애플리케이션에 숨겨진 데이터 손상을 일으킬 수 있습니다.\nlibxml2 2.7.3 이후 버전으로 업그레이드하세요. ([https://bugs.php.net/bug.php?id=45996 PHP에 제기한 버그])\n설치가 중단되었습니다.",
index 8b8d02d..6b5bfe0 100644 (file)
@@ -7,6 +7,7 @@
                ]
        },
        "config-information": "信息",
+       "config-back": "← 转去",
        "config-page-language": "闲话",
        "mainpagetext": "<strong>MediaWiki安装好哉。</strong>",
        "mainpagedocfooter": "请访问[https://meta.wikimedia.org/wiki/Help:Contents 用户手册]以获得使用此维基软件个信息!\n\n== 入门 ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings MediaWiki 配置设置列表]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki 常见问题解答]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki 发布邮件列表]"
index ca5d534..f39f8fd 100644 (file)
@@ -42,6 +42,10 @@ class DeleteLinksJob extends Job {
                }
 
                $pageId = $this->params['pageId'];
+
+               // Serialize links updates by page ID so they see each others' changes
+               $scopedLock = LinksUpdate::acquirePageLock( wfGetDB( DB_MASTER ), $pageId, 'job' );
+
                if ( WikiPage::newFromID( $pageId, WikiPage::READ_LATEST ) ) {
                        // The page was restored somehow or something went wrong
                        $this->setLastError( "deleteLinks: Page #$pageId exists" );
index c76ea4f..8fba728 100644 (file)
@@ -128,8 +128,18 @@ class RefreshLinksJob extends Job {
         * @return bool
         */
        protected function runForTitle( Title $title ) {
+               $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+
                $page = WikiPage::factory( $title );
                $page->loadPageData( WikiPage::READ_LATEST );
+
+               // Serialize links updates by page ID so they see each others' changes
+               $scopedLock = LinksUpdate::acquirePageLock( wfGetDB( DB_MASTER ), $page->getId(), 'job' );
+               // Get the latest ID *after* acquirePageLock() flushed the transaction.
+               // This is used to detect edits/moves after loadPageData() but before the scope lock.
+               // The works around the chicken/egg problem of determining the scope lock key.
+               $latest = $title->getLatestRevID( Title::GAID_FOR_UPDATE );
+
                if ( !empty( $this->params['triggeringRevisionId'] ) ) {
                        // Fetch the specified revision; lockAndGetLatest() below detects if the page
                        // was edited since and aborts in order to avoid corrupting the link tables
@@ -142,15 +152,15 @@ class RefreshLinksJob extends Job {
                        $revision = Revision::newFromTitle( $title, false, Revision::READ_LATEST );
                }
 
-               $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
-
                if ( !$revision ) {
                        $stats->increment( 'refreshlinks.rev_not_found' );
                        $this->setLastError( "Revision not found for {$title->getPrefixedDBkey()}" );
                        return false; // just deleted?
-               } elseif ( !$revision->isCurrent() || $revision->getPage() != $page->getId() ) {
-                       // If the revision isn't current, there's no point in doing a bunch
-                       // of work just to fail at the lockAndGetLatest() check later.
+               } elseif ( $revision->getId() != $latest || $revision->getPage() !== $page->getId() ) {
+                       // Do not clobber over newer updates with older ones. If all jobs where FIFO and
+                       // serialized, it would be OK to update links based on older revisions since it
+                       // would eventually get to the latest. Since that is not the case (by design),
+                       // only update the link tables to a state matching the current revision's output.
                        $stats->increment( 'refreshlinks.rev_not_current' );
                        $this->setLastError( "Revision {$revision->getId()} is not current" );
                        return false;
@@ -250,17 +260,6 @@ class RefreshLinksJob extends Job {
                        }
                }
 
-               $latestNow = $page->lockAndGetLatest();
-               if ( !$latestNow || $revision->getId() != $latestNow ) {
-                       // Do not clobber over newer updates with older ones. If all jobs where FIFO and
-                       // serialized, it would be OK to update links based on older revisions since it
-                       // would eventually get to the latest. Since that is not the case (by design),
-                       // only update the link tables to a state matching the current revision's output.
-                       $stats->increment( 'refreshlinks.rev_cas_failure' );
-                       $this->setLastError( "page_latest changed from {$revision->getId()} to $latestNow" );
-                       return false;
-               }
-
                DataUpdate::runUpdates( $updates );
 
                InfoAction::invalidateCache( $title );
index 0371f24..320a0b6 100644 (file)
@@ -35,6 +35,8 @@
  *                use application/x-www-form-urlencoded (headers sent automatically)
  *   - stream   : resource to stream the HTTP response body to
  *   - proxy    : HTTP proxy to use
+ *   - flags    : map of boolean flags which supports:
+ *                  - relayResponseHeaders : write out header via header()
  * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'.
  *
  * @author Aaron Schulz
@@ -172,6 +174,7 @@ class MultiHttpClient {
                                $req['body'] = '';
                                $req['headers']['content-length'] = 0;
                        }
+                       $req['flags'] = isset( $req['flags'] ) ? $req['flags'] : [];
                        $handles[$index] = $this->getCurlHandle( $req, $opts );
                        if ( count( $reqs ) > 1 ) {
                                // https://github.com/guzzle/guzzle/issues/349
@@ -373,6 +376,9 @@ class MultiHttpClient {
 
                curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
                        function ( $ch, $header ) use ( &$req ) {
+                               if ( !empty( $req['flags']['relayResponseHeaders'] ) ) {
+                                       header( $header );
+                               }
                                $length = strlen( $header );
                                $matches = [];
                                if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) {
index 7ab9cd3..2d67b87 100644 (file)
@@ -400,16 +400,13 @@ class LinkRenderer {
        private function getLinkURL( LinkTarget $target, array $query = [] ) {
                // TODO: Use a LinkTargetResolver service instead of Title
                $title = Title::newFromLinkTarget( $target );
-               $proto = $this->expandUrls !== false
-                       ? $this->expandUrls
-                       : PROTO_RELATIVE;
                if ( $this->forceArticlePath ) {
                        $realQuery = $query;
                        $query = [];
                } else {
                        $realQuery = [];
                }
-               $url = $title->getLinkURL( $query, false, $proto );
+               $url = $title->getLinkURL( $query, false, $this->expandUrls );
 
                if ( $this->forceArticlePath && $realQuery ) {
                        $url = wfAppendQuery( $url, $realQuery );
index 9176b54..b3a555a 100644 (file)
@@ -421,8 +421,10 @@ class ThumbnailImage extends MediaTransformOutput {
                }
 
                // Additional densities for responsive images, if specified.
-               if ( !empty( $this->responsiveUrls ) ) {
-                       $attribs['srcset'] = Html::srcSet( $this->responsiveUrls );
+               // If any of these urls is the same as src url, it'll be excluded.
+               $responsiveUrls = array_diff( $this->responsiveUrls, [ $this->url ] );
+               if ( !empty( $responsiveUrls ) ) {
+                       $attribs['srcset'] = Html::srcSet( $responsiveUrls );
                }
 
                Hooks::run( 'ThumbnailBeforeProduceHTML', [ $this, &$attribs, &$linkAttribs ] );
index b06b519..dbc27a9 100644 (file)
@@ -1103,15 +1103,10 @@ class WikiPage implements Page, IDBAccessObject {
                        return false;
                }
 
-               $title = $this->mTitle;
-               wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
-                       // Invalidate the cache in auto-commit mode
-                       $title->invalidateCache();
-               } );
-
+               $this->mTitle->invalidateCache();
                // Send purge after above page_touched update was committed
                DeferredUpdates::addUpdate(
-                       new CdnCacheUpdate( $title->getCdnUrls() ),
+                       new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
                        DeferredUpdates::PRESEND
                );
 
@@ -3279,6 +3274,14 @@ class WikiPage implements Page, IDBAccessObject {
                $title->touchLinks();
                $title->purgeSquid();
                $title->deleteTitleProtection();
+
+               if ( $title->getNamespace() == NS_CATEGORY ) {
+                       // Load the Category object, which will schedule a job to create
+                       // the category table row if necessary. Checking a slave is ok
+                       // here, in the worst case it'll run an unnecessary recount job on
+                       // a category that probably doesn't have many members.
+                       Category::newFromTitle( $title )->getID();
+               }
        }
 
        /**
@@ -3525,6 +3528,22 @@ class WikiPage implements Page, IDBAccessObject {
                                        $cat = Category::newFromName( $catName );
                                        Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
                                }
+
+                               // Refresh counts on categories that should be empty now, to
+                               // trigger possible deletion. Check master for the most
+                               // up-to-date cat_pages.
+                               if ( count( $deleted ) ) {
+                                       $rows = $dbw->select(
+                                               'category',
+                                               [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
+                                               [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
+                                               $method
+                                       );
+                                       foreach ( $rows as $row ) {
+                                               $cat = Category::newFromRow( $row );
+                                               $cat->refreshCounts();
+                                       }
+                               }
                        }
                );
        }
index 3fe9cdd..1017e44 100644 (file)
@@ -40,10 +40,6 @@ class ProfilerStub extends Profiler {
        public function close() {
        }
 
-       public function getCurrentSection() {
-               return '';
-       }
-
        public function logData() {
        }
 
index ccf764b..6596112 100644 (file)
@@ -1468,34 +1468,6 @@ MESSAGE;
                return wfAppendQuery( $script, $query );
        }
 
-       /**
-        * Build a load.php URL
-        * @deprecated since 1.24 Use createLoaderURL() instead
-        * @param array $modules Array of module names (strings)
-        * @param string $lang Language code
-        * @param string $skin Skin name
-        * @param string|null $user User name. If null, the &user= parameter is omitted
-        * @param string|null $version Versioning timestamp
-        * @param bool $debug Whether the request should be in debug mode
-        * @param string|null $only &only= parameter
-        * @param bool $printable Printable mode
-        * @param bool $handheld Handheld mode
-        * @param array $extraQuery Extra query parameters to add
-        * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too.
-        */
-       public static function makeLoaderURL( $modules, $lang, $skin, $user = null,
-               $version = null, $debug = false, $only = null, $printable = false,
-               $handheld = false, $extraQuery = []
-       ) {
-               global $wgLoadScript;
-
-               $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug,
-                       $only, $printable, $handheld, $extraQuery
-               );
-
-               return wfAppendQuery( $wgLoadScript, $query );
-       }
-
        /**
         * Helper for createLoaderURL()
         *
@@ -1505,7 +1477,7 @@ MESSAGE;
         * @param array $extraQuery
         * @return array
         */
-       public static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) {
+       protected static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) {
                return self::makeLoaderQuery(
                        $context->getModules(),
                        $context->getLanguage(),
@@ -1522,7 +1494,7 @@ MESSAGE;
 
        /**
         * Build a query array (array representation of query string) for load.php. Helper
-        * function for makeLoaderURL().
+        * function for createLoaderURL().
         *
         * @param array $modules
         * @param string $lang
index 3adadff..59f9a63 100644 (file)
@@ -459,7 +459,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
                                        ]
                                );
 
-                               $dbw->onTransactionIdle( function () use ( &$scopeLock ) {
+                               $dbw->onTransactionResolution( function () use ( &$scopeLock ) {
                                        ScopedCallback::consume( $scopeLock ); // release after commit
                                } );
                        }
index e2bb516..f47a70b 100644 (file)
@@ -107,8 +107,7 @@ class RevDelArchivedFileItem extends RevDelFileItem {
                                                'target' => $this->list->title->getPrefixedText(),
                                                'file' => $file->getKey(),
                                                'token' => $user->getEditToken( $file->getKey() )
-                                       ],
-                                       false, PROTO_RELATIVE
+                                       ]
                                ),
                        ];
                }
index 921fe5a..dca4d5a 100644 (file)
@@ -215,8 +215,7 @@ class RevDelFileItem extends RevDelItem {
                                                'target' => $this->list->title->getPrefixedText(),
                                                'file' => $file->getArchiveName(),
                                                'token' => $user->getEditToken( $file->getArchiveName() )
-                                       ],
-                                       false, PROTO_RELATIVE
+                                       ]
                                ),
                        ];
                }
index d01751e..f06a192 100644 (file)
@@ -129,7 +129,7 @@ class SpecialCreateAccount extends LoginSignupSpecialPage {
                # Run any hooks; display injected HTML
                $injected_html = '';
                $welcome_creation_msg = 'welcomecreation-msg';
-               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
+               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html, $direct ] );
 
                /**
                 * Let any extensions change what message is shown.
index db20d87..21f5659 100644 (file)
@@ -124,7 +124,7 @@ class SpecialUserLogin extends LoginSignupSpecialPage {
 
                # Run any hooks; display injected HTML if any, else redirect
                $injected_html = '';
-               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
+               Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html, $direct ] );
 
                if ( $injected_html !== '' || $extraMessages ) {
                        $this->showSuccessPage( 'success', $this->msg( 'loginsuccesstitle' ),
index fe0b4fe..f8eba9a 100644 (file)
@@ -224,6 +224,11 @@ class ContribsPager extends ReverseChronologicalPager {
                                        ]
                                ];
                        }
+                       // (T140537) Disallow looking too far in the past for 'newbies' queries. If the user requested
+                       // a timestamp offset far in the past such that there are no edits by users with user_ids in
+                       // the range, we would end up scanning all revisions from that offset until start of time.
+                       $condition[] = 'rev_timestamp > ' .
+                               $this->mDb->addQuotes( $this->mDb->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) );
                } else {
                        $uid = User::idFromName( $this->target );
                        if ( $uid ) {
index 951cb52..09132f2 100644 (file)
@@ -1262,7 +1262,8 @@ class LoginFormPreAuthManager extends SpecialPage {
                # Run any hooks; display injected HTML if any, else redirect
                $currentUser = $this->getUser();
                $injected_html = '';
-               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html ] );
+               $direct = RequestContext::getMain()->getRequest()->wasPosted();
+               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html, $direct ] );
 
                if ( $injected_html !== '' ) {
                        $this->displaySuccessfulAction( 'success', $this->msg( 'loginsuccesstitle' ),
@@ -1283,8 +1284,9 @@ class LoginFormPreAuthManager extends SpecialPage {
                $currentUser = $this->getUser();
                $injected_html = '';
                $welcome_creation_msg = 'welcomecreation-msg';
+               $direct = RequestContext::getMain()->getRequest()->wasPosted();
 
-               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html ] );
+               Hooks::run( 'UserLoginComplete', [ &$currentUser, &$injected_html, $direct ] );
 
                /**
                 * Let any extensions change what message is shown.
index 419ee47..c7bd395 100644 (file)
@@ -31,7 +31,7 @@ class BatchRowIterator implements RecursiveIterator {
        protected $db;
 
        /**
-        * @var string $table The name of the table to read from
+        * @var string|array $table The name or names of the table to read from
         */
        protected $table;
 
@@ -79,7 +79,7 @@ class BatchRowIterator implements RecursiveIterator {
 
        /**
         * @param IDatabase $db The database to read from
-        * @param string       $table      The name of the table to read from
+        * @param string|array $table      The name or names of the table to read from
         * @param string|array $primaryKey The name or names of the primary key columns
         * @param integer      $batchSize  The number of rows to fetch per iteration
         * @throws MWException
index f63f7c3..97a8233 100644 (file)
@@ -10,7 +10,8 @@
                        "GeekEmad",
                        "Nemo bis",
                        "Shbib Al-Subaie",
-                       "Macofe"
+                       "Macofe",
+                       "علاء"
                ]
        },
        "tog-usenewrc": ")جمّع التعديلات حسب الصفحة في أحدث التغييرات وقائمة المراقبة (يتطلب جافاسكربت",
        "rc-enhanced-hide": "أخفِ التفاصيل",
        "recentchangeslinked": "تغييرات ذات علاقة",
        "recentchangeslinked-title": "التغييرات المرتبطة ب \"$1\"",
-       "recentchangeslinked-summary": "هذه قائمة بالتغييرات التي تمت حديثا للصفحات الموصولة من صفحة معينة (أو إلى الأعضاء ضمن تصنيف معين).\nالصفحات في [[Special:Watchlist|قائمة مراقبتك]] '''عريضة'''",
+       "recentchangeslinked-summary": "هذه قائمة بالتغييرات التي تمت حديثاً للصفحات الموصولة من صفحة معينة (أو إلى الأعضاء ضمن تصنيف معين).\nالصفحات في [[Special:Watchlist|قائمة مراقبتك]] '''عريضة'''",
        "recentchangeslinked-page": "اسم الصفحة:",
-       "recentchangeslinked-to": "أظهر التغييرات للصفحات الموصولة للصفحة المعطاة عوضا عن ذلك",
+       "recentchangeslinked-to": "أظهر التغييرات للصفحات الموصولة للصفحة المعطاة عوضاً عن ذلك",
        "uploadlogpage": "سجل الرفع",
        "filedesc": "ملخص:",
        "license": "ترخيص:",
index 06a8abc..738cd5e 100644 (file)
        "category_header": "صفحات تصنيف «$1»",
        "subcategories": "تصنيفات فرعية",
        "category-media-header": "ملفات تصنيف \"$1\"",
-       "category-empty": "هذا التصنيف لا يحتوي حاليا على صفحات أو ملفات.",
+       "category-empty": "هذا التصنيف لا يحتوي حالياً على أي صفحات أو ملفات.",
        "hidden-categories": "{{PLURAL:$1|لا تصنيفات مخفية|تصنيف مخفي|تصنيفان مخفيان|تصنيفات مخفية}}",
        "hidden-category-category": "تصنيفات مخفية",
        "category-subcat-count": "{{PLURAL:$2|هذا التصنيف يحوي التصنيف الفرعي التالي|هذا التصنيف يحوي {{PLURAL:$1||التصنيف الفرعي|تصنيفين فرعيين|$1 تصنيفات فرعية}}، من إجمالي $2.}}",
        "recentchangeslinked-feed": "تغييرات ذات علاقة",
        "recentchangeslinked-toolbox": "تغييرات ذات علاقة",
        "recentchangeslinked-title": "التغييرات المرتبطة بصفحة «$1»",
-       "recentchangeslinked-summary": "هذه قائمة بالتغييرات التي تمت حديثا للصفحات الموصولة من صفحة معينة (أو إلى الأعضاء ضمن تصنيف معين).\nالصفحات في [[Special:Watchlist|قائمة مراقبتك]] '''مغلظة'''",
+       "recentchangeslinked-summary": "هذه قائمة بالتغييرات التي تمت حديثاً للصفحات الموصولة من صفحة معينة (أو إلى الأعضاء ضمن تصنيف معين).\nالصفحات في [[Special:Watchlist|قائمة مراقبتك]] '''عريضة'''",
        "recentchangeslinked-page": "اسم الصفحة:",
-       "recentchangeslinked-to": "أظهر التغييرات للصفحات الموصولة للصفحة المعطاة عوضا عن ذلك",
+       "recentchangeslinked-to": "أظهر التغييرات للصفحات الموصولة للصفحة المعطاة عوضاً عن ذلك",
        "recentchanges-page-added-to-category": "[[:$1]] أضيفت إلى التصنيف",
        "recentchanges-page-added-to-category-bundled": "أضيفت [[:$1]] و{{PLURAL:$2|صفحة واحدة|صفحتان|$2 صفحات}} إلى التصنيف",
        "recentchanges-page-removed-from-category": "أزيلت [[:$1]] من التصنيف",
index 3f186c9..f01e18d 100644 (file)
        "prefs-namespaces": "Namensräume",
        "default": "Voreinstellung",
        "prefs-files": "Dateien",
-       "prefs-custom-css": "Benutzerdefinierte CSS",
+       "prefs-custom-css": "Benutzerdefiniertes CSS",
        "prefs-custom-js": "Benutzerdefiniertes JavaScript",
        "prefs-common-css-js": "Gemeinsames CSS/JavaScript aller Benutzeroberflächen:",
        "prefs-reset-intro": "Du kannst diese Seite verwenden, um die Einstellungen auf die Standards zurückzusetzen.\nDies kann nicht mehr rückgängig gemacht werden.",
index acfe82e..9215593 100644 (file)
        "undelete_short": "Restaurar {{PLURAL:$1|una edición|$1 ediciones}}",
        "viewdeleted_short": "Ver {{PLURAL:$1|una edición borrada|$1 ediciones borradas}}",
        "protect": "Proteger",
-       "protect_change": "Cambiar",
+       "protect_change": "cambiar",
        "protectthispage": "Proteger esta página",
        "unprotect": "Cambiar protección",
        "unprotectthispage": "Cambiar la protección de esta página",
        "newpage": "Página nueva",
        "talkpage": "Discutir esta página",
-       "talkpagelinktext": "Discusión",
+       "talkpagelinktext": "discusión",
        "specialpage": "Página especial",
        "personaltools": "Herramientas personales",
        "articlepage": "Ver artículo",
index df11d65..c47c118 100644 (file)
        "pt-userlogout": "Se déconnecter",
        "php-mail-error-unknown": "Erreur inconnue dans la fonction <code>mail()</code> de PHP.",
        "user-mail-no-addy": "Impossible d’envoyer un courriel sans adresse de courriel.",
-       "user-mail-no-body": "Essai d’envoi d’un courriel avec un corps vide ou déraisonnablement court.",
+       "user-mail-no-body": "Essai d’envoi d’un courriel avec un corps vide ou anormalement court.",
        "changepassword": "Changer de mot de passe",
        "resetpass_announce": "Pour terminer votre inscription, vous devez fournir un nouveau mot de passe.",
        "resetpass_text": "<!-- Ajoutez le texte ici -->",
        "botpasswords-label-delete": "Supprimer",
        "botpasswords-label-resetpassword": "Réinitialiser le mot de passe",
        "botpasswords-label-grants": "Droits applicables :",
-       "botpasswords-help-grants": "Chaque droit accordé donne accès aux droits utilisateurs listés dont dispose déjà un compte. Voyez le [[Special:ListGrants|tableau des droits]] pour plus d’information.",
+       "botpasswords-help-grants": "Chaque droit accordé donne accès à la liste des droits utilisateurs dont l’utilisateur dispose déjà. Voyez le [[Special:ListGrants|tableau des droits]] pour plus d’informations.",
        "botpasswords-label-restrictions": "Restrictions d’utilisation :",
        "botpasswords-label-grants-column": "Accordé",
        "botpasswords-bad-appid": "Le nom de robot « $1 » n’est pas valide.",
        "showpreview": "Prévisualiser",
        "showdiff": "Voir les modifications",
        "blankarticle": "<strong>Attention :</strong> la page que vous créez est vide.\nSi vous cliquez de nouveau sur « {{int:savearticle}} », la page sera créée sans aucun contenu.",
-       "anoneditwarning": "<strong>Attention :</strong> vous n’êtes pas connecté. Votre adresse IP sera visible de tout le monde si vous faites des modifications. Si vous <strong>[$1 vous connectez]</strong> ou <strong>[$2 créez un compte]</strong>, vos modifications seront attribuées à votre nom d’utilisateur, entre autres avantages.",
-       "anonpreviewwarning": "<em>Vous n’êtes pas identifié{{GENDER:||e}}. Sauvegarder enregistrera votre adresse IP dans l’historique des modifications de la page.</em>",
-       "missingsummary": "<strong>Rappel :</strong> vous n’avez pas encore fourni le résumé de votre modification.\nSi vous cliquez de nouveau sur le bouton « {{int:savearticle}} », la publication sera faite sans nouvel avertissement.",
+       "anoneditwarning": "<strong>Attention :</strong> vous n’êtes pas connecté. Votre adresse IP sera visible de tout le monde si vous faites des modifications. Si vous <strong>[$1 vous connectez]</strong> ou <strong>[$2 créez un compte]</strong>, vos modifications seront attribuées à votre nom d’utilisateur, avec d'autres avantages.",
+       "anonpreviewwarning": "<em>Vous n’êtes pas connecté{{GENDER:||e}}. Sauvegarder enregistrera votre adresse IP dans l’historique des modifications de la page.</em>",
+       "missingsummary": "<strong>Rappel :</strong> vous n’avez pas encore fourni le résumé de votre modification.\nSi vous cliquez de nouveau sur le bouton « {{int:savearticle}} », vos modifications seront sauvegardées sans résumé.",
        "selfredirect": "<strong>Attention :</strong> vous êtes en train de rediriger la page vers elle-même.\nVous pouvez avoir spécifié la mauvaise cible pour la redirection, ou vous modifiez peut-être la mauvaise page.\nSi vous cliquez de nouveau sur « {{int:savearticle}} », la redirection sera tout de même créée.",
        "missingcommenttext": "Veuillez entrer un commentaire ci-dessous.",
        "missingcommentheader": "<strong>Rappel :</strong> vous n’avez pas fourni de sujet pour ce commentaire.\nSi vous cliquez de nouveau sur « {{int:Savearticle}} », votre modification sera enregistrée sans sujet.",
        "whitelistedittext": "Vous devez vous $1 pour avoir la permission de modifier le contenu.",
        "confirmedittext": "Vous devez confirmer votre adresse de courriel avant de modifier les pages.\nVeuillez entrer et valider votre adresse de courriel dans vos [[Special:Preferences|préférences]].",
        "nosuchsectiontitle": "Impossible de trouver la section",
-       "nosuchsectiontext": "Vous avez essayé de modifier une section qui n’existe pas.\nElle a peut-être été déplacée ou supprimée depuis que vous avez lu cette page.",
+       "nosuchsectiontext": "Vous avez essayé de modifier une section qui n’existe pas.\nElle a peut-être été déplacée ou supprimée pendant que vous la consultiez.",
        "loginreqtitle": "Connexion nécessaire",
        "loginreqlink": "connecter",
        "loginreqpagetext": "Vous devez vous $1 pour voir les autres pages.",
        "newarticletext": "Vous avez suivi un lien vers une page qui n’existe pas encore. \nAfin de créer cette page, entrez votre texte dans la boîte ci-après (vous pouvez consulter [$1 la page d’aide] pour plus d’informations). \nSi vous êtes arrivé{{GENDER:||e}} ici par erreur, cliquez sur le bouton <strong>Retour</strong> de votre navigateur.",
        "anontalkpagetext": "----\n<em>Vous êtes sur la page de discussion d’un utilisateur anonyme qui n’a pas encore créé de compte ou qui n’en utilise pas</em>.\nPour cette raison, nous devons utiliser son adresse IP pour l’identifier.\nUne adresse IP peut être partagée par plusieurs utilisateurs.\nSi vous êtes un{{GENDER:||e|}} utilisat{{GENDER:|eur|rice|eur}} anonyme et si vous constatez que des commentaires qui ne vous concernent pas vous ont été adressés, vous pouvez [[Special:CreateAccount|créer un compte]] ou [[Special:UserLogin|vous connecter]] afin d’éviter toute confusion future avec d’autres contributeurs anonymes.",
        "noarticletext": "Il n’y a pour l’instant aucun texte sur cette page.\nVous pouvez [[Special:Search/{{PAGENAME}}|lancer une recherche sur ce titre]] dans les autres pages,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechercher dans les opérations liées]\nou [{{fullurl:{{FULLPAGENAME}}|action=edit}} créer cette page]</span>.",
-       "noarticletext-nopermission": "Il n'y a pour l'instant aucun texte sur cette page.\nVous pouvez [[Special:Search/{{PAGENAME}}|faire une recherche sur ce titre]] dans les autres pages,\nou <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechercher dans les journaux associés]</span>.",
+       "noarticletext-nopermission": "Il n'y a pour l'instant aucun texte sur cette page.\nVous pouvez [[Special:Search/{{PAGENAME}}|faire une recherche sur ce titre]] dans les autres pages,\nou <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} rechercher dans les journaux associés]</span>, mais vous n'avez pas la permission de créer cette page.",
        "missing-revision": "La révision nº $1 de la page intitulée « {{FULLPAGENAME}} » n’existe pas.\n\nCela survient en général en suivant un lien historique obsolète vers une page qui a été supprimée.\nVous pouvez trouver plus de détails dans le [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} journal des suppressions].",
        "userpage-userdoesnotexist": "Le compte utilisateur « <nowiki>$1</nowiki> » n’est pas enregistré. Veuillez vérifier que vous voulez créer cette page.",
        "userpage-userdoesnotexist-view": "Le compte utilisateur « $1 » n'est pas enregistré.",
        "nonunicodebrowser": "<strong>Attention : votre navigateur ne supporte pas l’Unicode.</strong>\nUn palliatif est en place vous permettant de modifier les pages en toute sécurité, faisant apparaître les caractères non-ASCII  sous forme hexadécimale dans la boîte de modification.",
        "editingold": "<strong>Attention : vous êtes en train de modifier une ancienne version de cette page.</strong>\nSi vous la publiez, toutes les modifications effectuées depuis cette version seront perdues.",
        "yourdiff": "Différences",
-       "copyrightwarning": "Toutes les contributions à {{SITENAME}} sont considérées comme publiées sous les termes de la $2 (voir $1 pour plus de détails). Si vous ne désirez pas que vos écrits soient modifiés et distribués à volonté, merci de ne pas les soumettre ici.<br /> Vous nous promettez aussi que vous avez écrit ceci vous-même, ou que vous l’avez copié d’une source provenant du domaine public ou d’une ressource libre similaire. <strong>N’UTILISEZ PAS DE TRAVAUX SOUS DROIT D’AUTEUR SANS AUTORISATION EXPRESSE !</strong>",
+       "copyrightwarning": "Toutes les contributions à {{SITENAME}} sont considérées comme publiées sous les termes de la $2 (voir $1 pour plus de détails). \nSi vous ne désirez pas que vos écrits soient modifiés et distribués à volonté, merci de ne pas les soumettre ici.<br /> \nVous nous promettez aussi que vous avez écrit ceci vous-même, ou que vous l’avez copié d’une source provenant du domaine public ou d’une ressource libre similaire. \n<strong>N’UTILISEZ PAS DE TRAVAUX SOUS DROIT D’AUTEUR SANS AUTORISATION EXPRESSE !</strong>",
        "copyrightwarning2": "Toutes les contributions à {{SITENAME}} peuvent être modifiées ou supprimées par d’autres utilisateurs. Si vous ne désirez pas que vos écrits soient modifiés et distribués à volonté, merci de ne pas les soumettre ici.<br \n/>Vous nous promettez aussi que vous avez écrit ceci vous-même, ou que vous l’avez copié d’une source provenant du domaine public, ou d’une ressource libre. (voir $1 pour plus de détails).\n<strong>N’UTILISEZ PAS DE TRAVAUX SOUS DROIT D’AUTEUR SANS AUTORISATION EXPRESSE !<strong>",
        "editpage-cannot-use-custom-model": "Le modèle de contenu de cette page ne peut pas être modifié.",
        "longpageerror": "<strong>Erreur : Le texte que vous avez soumis fait {{PLURAL:$1|un Kio|$1 Kio}}, ce qui dépasse la limite fixée à {{PLURAL:$2|un Kio|$2 Kio}}.</strong>\nIl ne peut pas être sauvegardé.",
        "permissionserrorstext": "Vous n'avez pas la permission d'effectuer l'opération demandée pour {{PLURAL:$1|la raison suivante|les raisons suivantes}} :",
        "permissionserrorstext-withaction": "Vous ne pouvez pas $2, pour {{PLURAL:$1|la raison suivante|les raisons suivantes}} :",
        "contentmodelediterror": "Vous ne pouvez pas modifier cette révision car son modèle de contenu est <code>$1</code>, ce qui diffère du modèle de contenu actuel de la page <code>$2</code>.",
-       "recreate-moveddeleted-warn": "'''Attention : vous êtes en train de recréer une page qui a été précédemment supprimée.'''\n\nAssurez-vous qu'il est pertinent de poursuivre les modifications sur cette page. Le journal des suppressions et des déplacements est affiché ci-dessous :",
-       "moveddeleted-notice": "Cette page a été supprimée. Le journal des suppressions et des déplacements est affiché ci-dessous pour référence.",
-       "moveddeleted-notice-recent": "Désolé, cette page a été récemment supprimée (dans les dernières 24 heures).\nLes journaux des suppressions et des renommages pour la page sont fournis ci-dessous à titre d’information.",
+       "recreate-moveddeleted-warn": "<strong>Attention : vous êtes en train de recréer une page qui a été précédemment supprimée.</strong>\n\nAssurez-vous qu'il est pertinent de poursuivre les modifications sur cette page. \nLe journal des suppressions et des déplacements pour cette page est affiché ci-dessous à titre d'information :",
+       "moveddeleted-notice": "Cette page a été supprimée. \nLe journal des suppressions et des déplacements de la page est affiché ci-dessous pour référence.",
+       "moveddeleted-notice-recent": "Désolé, cette page a été récemment supprimée (dans les dernières 24 heures).\nLes journaux des suppressions et des renommages pour la page sont fournis ci-dessous pour référence.",
        "log-fulllog": "Voir le journal complet",
        "edit-hook-aborted": "Échec de la modification par une extension.\nAucune explication n’a été retournée.",
        "edit-gone-missing": "N’a pas pu mettre à jour la page.\nIl semble qu’elle ait été supprimée.",
        "history-edit-tags": "Modifier les balises des révisions sélectionnées",
        "rev-deleted-comment": "(résumé de modification retiré)",
        "rev-deleted-user": "(nom d'utilisateur retiré)",
-       "rev-deleted-event": "(détails de l’entrée retirés)",
+       "rev-deleted-event": "(détails de l’historique retirés)",
        "rev-deleted-user-contribs": "[nom d’utilisateur ou adresse IP retiré – modification masquée dans les contributions]",
        "rev-deleted-text-permission": "Cette version de la page a été <strong>effacée</strong>.\nDes détails sont disponibles dans le [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} journal des suppressions].",
        "rev-suppressed-text-permission": "Cette version de la page a été <strong>masquée</strong>.\nLes détails se trouvent dans le [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} journal des masquages].",
        "logdelete-text": "Les évènements du journal supprimés continueront à apparaître dans les journaux, mais une partie de leur contenu sera indisponible au public.",
        "revdelete-text-others": "Les autres administrateurs seront toujours en mesure d'accéder au contenu caché et le restaurer, à moins que des restrictions supplémentaires soient fixées.",
        "revdelete-confirm": "Confirmez que vous voulez effectuer cette action, que vous en comprenez les conséquences, et que vous le faites en accord avec [[{{MediaWiki:Policy-url}}|les règles]].",
-       "revdelete-suppress-text": "La suppression ne doit être utilisée '''que''' dans les cas suivants :\n* Informations potentiellement diffamatoires\n* Informations personnelles inappropriées\n*: ''adresse, numéro de téléphone, numéro de sécurité sociale, …''",
-       "revdelete-legend": "Mettre en place des restrictions de visibilité :",
+       "revdelete-suppress-text": "La suppression ne doit être utilisée <strong>que</strong> dans les cas suivants :\n* informations potentiellement diffamatoires\n* informations personnelles inappropriées\n*: <em>adresse, numéro de téléphone, numéro de sécurité sociale, …</em>",
+       "revdelete-legend": "Mettre en place des restrictions de visibilité",
        "revdelete-hide-text": "Texte de la révision",
        "revdelete-hide-image": "Masquer le contenu du fichier",
        "revdelete-hide-name": "Masquer la cible et les paramètres",
index 655058d..2920310 100644 (file)
        "content-model-css": "CSS",
        "content-json-empty-object": "Objecto vacue",
        "content-json-empty-array": "Array vacue",
+       "deprecated-self-close-category": "Paginas que usa etiquettas HTML auto-claudite non valide",
+       "deprecated-self-close-category-desc": "Le pagina contine etiquettas HTML auto-claudite non valide, como <code>&lt;b/></code> o <code>&lt;span/></code>. Le comportamento de istes cambiara proximemente pro esser in accordo con le specification HTML5, dunque lor uso in wikitexto es obsolete.",
        "duplicate-args-warning": "<strong>Attention:</strong> [[:$1]] appella [[:$2]] con plure valores pro le parametro \"$3\". Solmente le ultime valor fornite essera usate.",
        "duplicate-args-category": "Paginas que usa parametros duplicate in appellos de patrono",
        "duplicate-args-category-desc": "Le pagina contine appellos de patrono que usa duplicatos de parametros, como per exemplo <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "mw-widgets-dateinput-placeholder-month": "AAAA-MM",
        "mw-widgets-titleinput-description-new-page": "pagina non existe ancora",
        "mw-widgets-titleinput-description-redirect": "redirection a $1",
-       "api-error-blacklisted": "Per favor elige un altere titulo, plus descriptive.",
        "sessionmanager-tie": "Impossibile combinar plure typos de authentication de requesta: $1.",
        "sessionprovider-generic": "sessiones $1",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "sessiones basate sur cookies",
        "log-action-filter-newusers": "Typo de creation de conto:",
        "log-action-filter-patrol": "Typo de patrulia:",
        "log-action-filter-protect": "Typo de protection:",
-       "log-action-filter-rights": "Typo de cambio de derecto",
-       "log-action-filter-suppress": "Typo de suppression",
+       "log-action-filter-rights": "Typo de cambio de derecto:",
+       "log-action-filter-suppress": "Typo de suppression:",
        "log-action-filter-upload": "Typo de incargamento:",
        "log-action-filter-all": "Toto",
        "log-action-filter-block-block": "Blocar",
index 2264e1d..fc76250 100644 (file)
        "sqlite-no-fts": "$1 (全文検索なし)",
        "logentry-delete-delete": "$1 がページ「$3」を{{GENDER:$2|削除しました}}",
        "logentry-delete-restore": "$1 がページ「$3」を{{GENDER:$2|復元しました}}",
-       "logentry-delete-event": "$1 が$3の{{PLURAL:$5|記録項目$5件}}の閲覧レベルを{{GENDER:$2|変更しました}}: $4",
-       "logentry-delete-revision": "$1 がページ「$3」の{{PLURAL:$5|$5版}}の閲覧レベルを{{GENDER:$2|変更しました}}: $4",
-       "logentry-delete-event-legacy": "$1 が「$3」の記録項目の閲覧レベルを{{GENDER:$2|変更しました}}",
+       "logentry-delete-event": "$1 が $3 の{{PLURAL:$5|記録項目|記録項目$5件}}の閲覧レベルを{{GENDER:$2|変更しました}}: $4",
+       "logentry-delete-revision": "$1 がページ「$3」の{{PLURAL:$5|版|$5件の版}}の閲覧レベルを{{GENDER:$2|変更しました}}: $4",
+       "logentry-delete-event-legacy": "$1 が $3 の記録項目の閲覧レベルを{{GENDER:$2|変更しました}}",
        "logentry-delete-revision-legacy": "$1 がページ「$3」の版の閲覧レベルを{{GENDER:$2|変更しました}}",
-       "logentry-suppress-delete": "$1 がページ「$3」を{{GENDER:$2|隠蔽しました}}",
-       "logentry-suppress-event": "$1 が$3の{{PLURAL:$5|記録項目$5件}}の閲覧レベルを見えない形で{{GENDER:$2|変更しました}}: $4",
-       "logentry-suppress-revision": "$1 がページ「$3」の{{PLURAL:$5|$5版}}の閲覧レベルを見えない形で{{GENDER:$2|変更しました}}: $4",
-       "logentry-suppress-event-legacy": "$1 が$3で記録項目の閲覧レベルを見えない形で{{GENDER:$2|変更しました}}",
+       "logentry-suppress-delete": "$1 がページ「$3」を{{GENDER:$2|秘匿しました}}",
+       "logentry-suppress-event": "$1 が $3 の{{PLURAL:$5|記録項目|$5件の記録項目}}の閲覧レベルを見えない形で{{GENDER:$2|変更しました}}: $4",
+       "logentry-suppress-revision": "$1 がページ「$3」の{{PLURAL:$5|版|$5件の版}}の閲覧レベルを見えない形で{{GENDER:$2|変更しました}}: $4",
+       "logentry-suppress-event-legacy": "$1 が $3 の記録項目の閲覧レベルを見えない形で{{GENDER:$2|変更しました}}",
        "logentry-suppress-revision-legacy": "$1 がページ「$3」の版の閲覧レベルを見えない形で{{GENDER:$2|変更しました}}",
        "revdelete-content-hid": "本文の不可視化",
        "revdelete-summary-hid": "編集要約の不可視化",
        "logentry-tag-update-add-logentry": "$1 がページ $3 の記録項目 $5 に{{PLURAL:$7|タグ}} $6 を{{GENDER:$2|追加しました}}",
        "logentry-tag-update-remove-revision": "$1 がページ $3 の版 $4 から{{PLURAL:$9|タグ}} $8 を{{GENDER:$2|除去しました}}",
        "logentry-tag-update-remove-logentry": "$1 がページ $3 の記録項目 $5 から{{PLURAL:$9|タグ}} $8 を{{GENDER:$2|除去しました}}",
-       "logentry-tag-update-revision": "$1 ã\81\8cã\83\9aã\83¼ã\82¸ã\80\8c$3ã\80\8dã\81®ç\89\88 $4 ã\81§ã\81®ã\82¿ã\82°ã\82\92{{GENDER:$2|æ\9b´æ\96°ã\81\97ã\81¾ã\81\97ã\81\9f}} ($6 ã\81\8c{{PLURAL:$7|追å\8a }}ã\80\81$8 ã\81\8c{{PLURAL:$9|削除}})",
+       "logentry-tag-update-revision": "$1 ã\81\8cã\83\9aã\83¼ã\82¸ã\80\8c$3ã\80\8dã\81®ç\89\88 $4 ã\81®ã\82¿ã\82°ã\82\92{{GENDER:$2|æ\9b´æ\96°ã\81\97ã\81¾ã\81\97ã\81\9f}} ($6 ã\82\92{{PLURAL:$7|追å\8a }}ã\80\81$8 ã\82\92{{PLURAL:$9|削除}})",
        "logentry-tag-update-logentry": "$1 がページ「$3」の記録項目 $5 のタグを{{GENDER:$2|更新しました}} ($6 を{{PLURAL:$7|追加}}、$8 を{{PLURAL:$9|削除}})",
        "rightsnone": "(なし)",
        "revdelete-summary": "編集内容の要約",
index 208652e..e3218de 100644 (file)
        "spamprotectiontext": "스팸 필터가 문서 저장을 막았습니다.\n바깥 사이트로 연결하는 링크 중에 블랙리스트에 포함된 사이트가 있을 것입니다.",
        "spamprotectionmatch": "문제가 되는 부분은 다음과 같습니다: $1",
        "spambot_username": "미디어위키 스팸 정리",
-       "spam_reverting": "$1(을)를 포함하지 않는 최신 버전으로 되돌림",
+       "spam_reverting": "$1에 대한 링크를 포함하지 않는 최신 버전으로 되돌림",
        "spam_blanking": "모든 버전에 $1 링크를 포함하고 있어 차단함",
        "spam_deleting": "모든 버전에 $1 링크를 포함하고 있어 삭제함",
        "simpleantispam-label": "스팸 방지 검사입니다.\n이것을 입력하지 <strong>마세요</strong>!",
index de75f5e..2907aa1 100644 (file)
@@ -30,6 +30,7 @@
        "tog-watchdefault": "Azonzi e paggine e i files che modiffico a-i mæ sotta oservaçion",
        "tog-watchmoves": "Azonzi e paggine e i file che mescio a-i mæ sotta oservaçion",
        "tog-watchdeletion": "Azonzi e paggine e i files che scancello a-i mæ sotta oservaçion",
+       "tog-watchuploads": "Azonzi i noeuvi file che metto in osservaçion",
        "tog-watchrollback": "Azonzi a-i sotta osservassion e paggine dovve ho fæto un rollback",
        "tog-minordefault": "Indica de longo comme menô e modiffiche",
        "tog-previewontop": "Veddi l'anteprimma de d'äto a-o spaçio pe cangiâ",
        "databaseerror-query": "Query: $1",
        "databaseerror-function": "Fonsion: $1",
        "databaseerror-error": "Errô: $1",
+       "transaction-duration-limit-exceeded": "Pe evitâ un ato ritardo de replica, questa opiaçion a l'è stæta interotta perché a duata do tempo de scrittua ($1) a l'ha superou o limmite de $2 {{PLURAL:$2|segondo|segondi}}.\nSe t'ê aproeuvo a cangiâ tante cose inte'na votta sola, proeuva invece a cangiâ poche cose in tante votte.",
        "laggedslavemode": "'''Attension:''' a paggina a porriæ no riportâ i aggiornamenti ciù reçenti.",
        "readonly": "Database bloccòu",
        "enterlockreason": "Scrivi o motivo do blocco, e 'na stimma de quande o saiâ rimosso",
        "mypreferencesprotected": "No ti g'hæ o permisso pe modificâ e teu preferense.",
        "ns-specialprotected": "No se pœu modificâ e paggine speciali",
        "titleprotected": "A creaçion de 'na paggina con sto tittolo a l'è stæta bloccâ da [[User:$1|$1]].\nA raxon a l'è: <em>$2</em>.",
-       "filereadonlyerror": "N'ho posciuo modificâ o file \"$1\" perché o repository de file \"$2\" o l'è in modalitæ de sola lettua.\n\nL'amministratô ch'o l'ha bloccòu o l'ha fornio sta motivaçion: \"$3\".",
+       "filereadonlyerror": "N'ho posciuo modificâ o file \"$1\" perché o repository de file \"$2\" o l'è in modalitæ de sola lettua.\n\nL'amministratô de scistema ch'o l'ha bloccòu o l'ha fornio sta motivaçion: \"$3\".",
        "invalidtitle-knownnamespace": "Tittolo non vallido con namespace \"$2\" e testo \"$3\"",
        "invalidtitle-unknownnamespace": "Tittolo non vallido con namespace sconosciuo \"$1\" e testo \"$2\"",
        "exception-nologin": "No t'ê introu",
        "virus-scanfailed": "scansion fallia (codice $1)",
        "virus-unknownscanner": "antivirus sconosciuo:",
        "logouttext": "'''Sciortîa effettuâ.'''\n\nDanni a mente che gh'è de paggine che porrieivan continuâ a pai comme se a sciortîa a no foise avegnua, pe scin che no ti nettezzi a cache do to navegatô.",
+       "cannotlogoutnow-title": "Aoa no se poeu sciortî",
+       "cannotlogoutnow-text": "Quande s'adoeuvia $1 no se poeu sciortî.",
        "welcomeuser": "Benvegnuo, $1!",
        "welcomecreation-msg": "L'utensa a l'è stæta creâ correttamente.\nSe ti veu ti peu personalizzâ e [[Special:Preferences|preferençe de {{SITENAME}}]].",
        "yourname": "Nomme",
        "remembermypassword": "Aregòrda a mæ login in sto navegatô (pe in mascimo de $1 {{PLURAL:$1|giórno|giórni}})",
        "userlogin-remembermypassword": "Mantegnime collegou",
        "userlogin-signwithsecure": "Adoeuvia una conescion segua",
+       "cannotloginnow-title": "Aoa no se poeu intrâ",
+       "cannotloginnow-text": "Quande s'adoeuvia $1 no se poeu intrâ.",
        "yourdomainname": "Indirisso do scito:",
        "password-change-forbidden": "No ti peu cangiâ poula segretta in questa wiki.",
        "externaldberror": "Gh'è stæto un aro co-ol server de autenticaçion esterno, oppû no ti g'hæ i aotorizzaçioin pe aggiornâ o to accesso esterno.",
        "login": "Intra",
+       "login-security": "Veifica a to identitæ",
        "nav-login-createaccount": "Intra / Registrate",
        "userlogin": "Intra / Registrite",
        "userloginnocreate": "Intra",
        "userlogin-resetpassword-link": "T'hæ miga ascordou a teu poula segretta?",
        "userlogin-helplink2": "Agiutto pe intrâ",
        "userlogin-loggedin": "Ti t'ê zà connesso comme {{GENDER:$1|$1}}.\nUsa o formulaio sottostante pe accede comme 'n atro utente.",
+       "userlogin-reauth": "Ti g'hæ da intrâ 'n'atra votta pe veificâ che ti t'ê {{GENDER:$1|$1}}.",
        "userlogin-createanother": "Crea 'n atra utensa",
        "createacct-emailrequired": "Addresso e-mail:",
        "createacct-emailoptional": "Adresso email (opsionale)",
        "createacct-email-ph": "Scrivi o teu adresso email",
        "createacct-another-email-ph": "Scrivi o teu adresso email",
        "createaccountmail": "Doeuvia una password temporanea abrettio e mandila a l'adresso de posta elettronica speçificou",
+       "createaccountmail-help": "O poeu ese doeuviou pe creâ un'utensa pe 'n'atra person-a sensa doveine conosce a password.",
        "createacct-realname": "Nomme reale (opçionâ)",
        "createaccountreason": "Raxon:",
        "createacct-reason": "Raxon",
        "createacct-reason-ph": "Perché t'ê apreuvo a creâ un'atra utensa",
+       "createacct-reason-help": "Messaggio vixualizou into registro da creaçion de l'utença",
        "createacct-submit": "Crea a to utensa",
        "createacct-another-submit": "Crea utensa",
+       "createacct-continue-submit": "Continnoa a creaçion de l'utença",
+       "createacct-another-continue-submit": "Continnoa a creaçion de l'utença",
        "createacct-benefit-heading": "{{SITENAME}} o l'è realizzou da de gente comme ti.",
        "createacct-benefit-body1": "{{PLURAL:$1|modiffica|modiffiche}}",
        "createacct-benefit-body2": "{{PLURAL:$1|paggina|paggine}}",
        "nocookiesnew": "L'utensa a l'è stæta creâ, ma ti no t'ê intròu. {{SITENAME}} o deuvia i cookie pe lasciâ intrâ i utenti e ti ti ghe l'hæ disattivæ.\nRipreuva a intrâ co-o to nomme utente e poula segretta apen-a creæ doppo avei attivòu i cookie.",
        "nocookieslogin": "Pe intrâ in {{SITENAME}} bezeugna aveighe i cookie attivæ. Ti ti ghe l'hæ disattivæ. Pe piaxei attîvili e preuva torna a intrâ.",
        "nocookiesfornew": "L'utensa a no l'è stæta creâ, perché n'emmo posciuo confermâ a so sorgente.\nAsseguite d'avei attivòu i cookie, recarrega sta paggina e ripreuva.",
+       "createacct-loginerror": "L'utença a l'è stæta creaa correttamente, ma no l'è stæto poscibbile fate accede in moddo aotomattico. Procedi co l'[[Special:UserLogin|accesso manoâ]].",
        "noname": "O nomme d'ûtente o l'è sballiòu.",
        "loginsuccesstitle": "Accesso effettuòu",
        "loginsuccess": "'''O collegamento a-o server de {{SITENAME}} co-o nomme d'ûtente \"$1\" o l'è attivo.'''",
        "noemail": "No gh'è nisciûn indirisso e-mail registròu pe l'ûtente \"$1\".",
        "noemailcreate": "Ti devi dâ un addresso e-mail vallido.",
        "passwordsent": "Ûnn-a nêuva paròlla d'ordine a l'è stæta inviâa a l'indirisso e-mail registròu pe l'ûtente \"$1\".\nPe piaxei, fa 'n accesso appenn-a ti a ghe reçeivi.",
-       "blocked-mailpassword": "O teu addresso IP o l'è bloccòu, e pe sta raxon non se peu usâ a funsion de recuppero da pòula segretta, pe prevegnî di abuxi.",
+       "blocked-mailpassword": "O teu addresso IP o l'è bloccòu a-e modiffiche, pe prevegnî di abuxi non se peu doeuviâ a fonçion de recuppero da pòula segretta da queseto addresso IP.",
        "eauthentsent": "Un messaggio e-mail de conferma o l'è stæto inviòu a l'addresso indicòu.\nPe abilitâ l'invîo de messaggi e-mail pe quest'utensa, se deve seguî e instrussioin indicæ, pe confermâ che ti t'ê o legittimo propietâio de l'utensa.",
        "throttled-mailpassword": "Un'e-mail de reimpostassione da poula segretta a l'è zà stæta inviâ da meno de {{PLURAL:$1|1 oa|$1 oe}}.\nPe prevegnî di abuxi, a fonsion de reimpostassion da poula segretta a peu vese deuviâ solo che 'na votta ogni {{PLURAL:$1|oa|$1 oe}}.",
        "mailerror": "Errô inte l'invio do messaggio: $1",
        "createacct-another-realname-tip": "O nomme veo o l'è opçionâ.\nSe ti çerni de inseilo, o saiâ deuviòu pe attribuî a l'aotô a paternitæ di contengnui inviæ.",
        "pt-login": "Intra",
        "pt-login-button": "Intra",
+       "pt-login-continue-button": "Continoa l'accesso",
        "pt-createaccount": "Registrite",
        "pt-userlogout": "Sciorti",
        "php-mail-error-unknown": "Errô sconosciuo intaa funçion PHP mail()",
        "newpassword": "Neuva poula segretta",
        "retypenew": "Ripette a nêuva paròlla d'ordine:",
        "resetpass_submit": "Çerni a poula segretta e intra",
-       "changepassword-success": "O cangio de password o l'é anæto ben!",
+       "changepassword-success": "A to password a l'è stæta cangiâ!",
        "changepassword-throttled": "T'hæ çercòu de intrâ troppe votte tutt'assemme.\nPe piaxei aspeta $1 primma de provâ torna.",
+       "botpasswords": "Password bot",
+       "botpasswords-summary": "<em>Password bot</em> o consente l'accesso a un'utença tramite API sença doeuviâ e credençiæ d'accesso prinçipæ de l'utença. I driti utente disponibili quande s'è effettuou l'accesso co-ina password bot poeuan ese limitæ.\n\nSe no ti conosci o motivo pe-o quæ ti porriesci voeilo fâ, alloa foscia no ti doviesci fâlo. Nisciun doviæ mai domandâte de generâ un de questi e dapoeu dâlo a liatri.",
+       "botpasswords-disabled": "E password bot son disabilitæ.",
+       "botpasswords-no-central-id": "Pe doeuviâ una password bot, l'è necessaio accede a un'utença çentralizzâ.",
+       "botpasswords-existing": "Password bot in existença",
+       "botpasswords-createnew": "Crea una noeuva password bot",
+       "botpasswords-editexisting": "Modifica una password bot existente",
+       "botpasswords-label-appid": "Nomme bot:",
+       "botpasswords-label-create": "Crea",
+       "botpasswords-label-update": "Aggiorna",
+       "botpasswords-label-cancel": "Anulla",
+       "botpasswords-label-delete": "Scassa",
+       "botpasswords-label-resetpassword": "Reimposta a poula segretta",
+       "botpasswords-label-grants": "Assegnaçioin applicabile:",
+       "botpasswords-help-grants": "Ogni assegnaçion a dà accesso a-i driti utente elencæ che un'utença a g'ha zà. Amia a [[Special:ListGrants|tabella d'e assegnaçioin]] pe de ulteioî informaçioin.",
+       "botpasswords-label-restrictions": "Restriçioin d'utilizzo:",
+       "botpasswords-label-grants-column": "Assegnaçioin",
+       "botpasswords-bad-appid": "O nomme bot \"$1\" o no l'è vallido.",
+       "botpasswords-insert-failed": "Imposcibile azonze o nomme bot \"$1\". O l'è za stæto azonto?",
+       "botpasswords-update-failed": "Imposcibile aggiornâ o nomme bot \"$1\". O l'è stæto scassou?",
+       "botpasswords-created-title": "Password bot creâ",
+       "botpasswords-created-body": "A password pe-o bot de nomme \"$1\" de l'utente \"$2\" a l'è stæta creâ.",
+       "botpasswords-updated-title": "Password bot aggiornâ",
+       "botpasswords-updated-body": "A password pe-o bot de nomme \"$1\" de l'utente \"$2\" a l'è stæta aggiornâ.",
+       "botpasswords-deleted-title": "Password bot scassâ",
+       "botpasswords-deleted-body": "A password pe-o bot de nomme \"$1\" de l'utente \"$2\" a l'è stæta scassâ.",
+       "botpasswords-newpassword": "A noeuva password pe accede con <strong>$1</strong> a l'è <strong>$2</strong>. <em>Marchitelo pe rifeimento futuo.</em>",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider o no l'è disponibbile.",
+       "botpasswords-restriction-failed": "E restriçioin de password bot impediscian questo accesso.",
+       "botpasswords-invalid-name": "O nomme utente indicou o no conten o separatô pe-o password bot (\"$1\").",
+       "botpasswords-not-exist": "L'utente \"$1\" o no dispon-e de 'na password bot ciamâ \"$2\".",
        "resetpass_forbidden": "No l'é poscìbile cangiâ e paròlle segrétte",
+       "resetpass_forbidden-reason": "No l'é poscìbile cangiâ e paole segrette: $1",
        "resetpass-no-info": "Pe anâ direttamente a sta paggina, primma ti g'hæ da intrâ .",
        "resetpass-submit-loggedin": "Cangia a password",
        "resetpass-submit-cancel": "Anulla",
        "passwordreset-email": "Addresso e-mail:",
        "passwordreset-emailtitle": "Dettaggi account sciu {{SITENAME}}",
        "passwordreset-emailtext-ip": "Quarcun (probabilmente ti, con adresso IP $1) o l'ha domandòu l'invio de 'na neuva poula segretta per l'accesso a {{SITENAME}} ($4). {{PLURAL:$3|L'utente associòu|I utenti associæ}} a sto addresso e-mail son:\n\n$2\n\n{{PLURAL:$3|Questa poula segretta temporannia a descazziâ|Queste poule segrette temporannie descazzian}} doppo {{PLURAL:$5|un giorno|$5 giorni}}.\nTi doviesci accede e çerne una neuva poula segretta oua. \n\nSe no t'ê stæto ti a fâ a domanda, ò se ti t'hæ aregordòu a poula segretta originale e no ti veu ciù cangiâla, ti peu ignorâ sto messaggio e continuâ a deuviâ a teu vegia poula segretta.",
+       "passwordreset-emailtext-user": "L'utente $1 de {{SITENAME}} o l'ha domandou l'inandio de 'na noeuva password pe l'accesso a {{SITENAME}} ($4). {{PLURAL:$3|L'utente associou|I utenti associæ}} a questo indriçço email son:\n\n$2\n\n{{PLURAL:$3|Questa password temporannia a descazziâ|Queste password temporannie descazian}} doppo {{PLURAL:$5|un giorno|$5 giorni}}.\nTi doviesci accede e çerne una noeuva password aoa. \n\nSe no t'ê stæto ti a fâ a recesta, o se ti t'ê aregordou a password originâ e no ti voeu ciu cangiala, ti poeu ignorâ sto messaggio e continoâ a doeuviâ a to vegia password.",
        "passwordreset-emailelement": "Nomme utente: \n$1\n\nPoula segretta temporannia: \n$2",
-       "passwordreset-emailsentemail": "Se questo o l'è un addresso de posta elettronnica registròu pe-a teu utensa, alloa saiâ inviâ un'e-mail pe reimpostâ a poula segretta.",
+       "passwordreset-emailsentemail": "Se questo addresso de posta elettronnica o l'è associou a-a teu utença, alloa saiâ inviou un'e-mail pe rempostâ a poula segretta.",
+       "passwordreset-emailsentusername": "Se gh'è un adreçço de posta elettronica associou con questo nomme utente, alloa saiâ inviou una email pe rempostâ a password.",
        "passwordreset-emailsent-capture": "L'è stæto inviòu un'e-mail de reimpostaçion da poula segretta, o contegnuo o l'è riportòu chì appreuvo.",
        "passwordreset-emailerror-capture": "L'è stæto generòu un'e-mail de reimpostaçion da poula segretta, riportà chì appreuvo. L'invio {{GENDER:$2|a l'utente}} o no l'è ariêscîo: $1",
+       "passwordreset-emailsent-capture2": "L'email de rempostaçion da password {{PLURAL:$1|a l'è stæta inviâ|son stæte inviæ}}. {{PLURAL:$1|O nomme|L'elenco di nommi}} utente e password o l'è mostrou chì de sotta.",
+       "passwordreset-emailerror-capture2": "Invio de email {{GENDER:$2|a l'utente}} non ariescio: $1. {{PLURAL:$3|O nomme|L'elenco di nommi}} utente e password o l'è mostrou chì de sotta.",
+       "passwordreset-nocaller": "Un chi ciamma ti g'hæ da dâlo",
+       "passwordreset-nosuchcaller": "O ciamante o no l'existe: $1",
+       "passwordreset-ignored": "A reimpostaçion da password a no l'è stæta gestia. Foscia n'è stæto configuou nisciun provider ?",
+       "passwordreset-invalideamil": "Addresso e-mail non vallido",
+       "passwordreset-nodata": "No è stæto fornio ni un nomme utente ni un adreçço de posta elettronica",
        "changeemail": "Cangia o elimmina l'adresso e-mail",
        "changeemail-header": "Completa sto formulaio pe cangiâ o to adresso e-mail. Se ti veu rimeuve l'associaçion de quasesegge addresso e-mail da-a teu utensa, lascia io neuvo addresso e-mail veuo quande ti invii o formulaio.",
        "changeemail-passwordrequired": "Saiâ necessaio insei a poula segretta pe confermâ a modiffica.",
        "minoredit": "Cangiamento minô (m)",
        "watchthis": "Metti sotta oservaçion",
        "savearticle": "Sarva a pàgina",
+       "savechanges": "Sarva e modiffiche",
+       "publishpage": "Pubbrica a paggina",
+       "publishchanges": "Pubbrica e modiffiche",
        "preview": "Anteprìmma",
        "showpreview": "Veddi l'anteprimma",
        "showdiff": "Veddi i cangiamenti",
        "accmailtext": "Una poula segretta generâ abrettio pe [[User talk:$1|$1]] a l'è stæta mandâ a $2.\n\nSta poula segretta a peu ese cangiâ inta paggina pe ''[[Special:ChangePassword|cangiâ a poula segretta]]'' subbito doppo l'accesso.",
        "newarticle": "(Nêuvo)",
        "newarticletext": "Sto colegaménto o corisponde a 'na pàgina ch'a no l'existe ancon.\n\nSe se vêu creâ a pàgina òua, se pêu comensâ a scrive into spàçio chì sotta.\n(amia e [$1 paggine d'agiûtto] pe ciû informaçioìn).\n\nSe t'ê intròu chì pe sballio,  sciacca '''Inderê''' into navegatô.",
-       "anontalkpagetext": "----\n''Sta chì a l'è a paggina de discuscion de un utente anonnimo, ch'o no l'ha ancon creou un'utensa o comunque o no a doeuvia oua. Pe identificâlo l'è quindi necessaio doeuviâ o nummero do so adresso IP. I adresci IP poeuan però ese condivixi da ciù utenti. Se t'ê un utente anonimo e ti ritegni che i commenti inte sta pagina no se riferiscian a ti, [[Special:CreateAccount|crea una noeuva utensa]] o donque [[Special:UserLogin|intra con quella che ti g'hæ za]] pe evitâ de chì avanti de ese confuzo con di atri utenti anonnimi .''",
-       "noarticletext": "Po-u momento a pagina çercâ a l'è vêua. L'è poscibbile [[Special:Search/{{PAGENAME}}|çercâ 'sto tittolo]] inte âtre pagine do scîto opû [{{fullurl:{{FULLPAGENAME}}|action=edit}} cangiâ a pagina òua].",
+       "anontalkpagetext": "----\n<em>Sta chì a l'è a paggina de discuscion de un utente anonnimo, ch'o no l'ha ancon creou un'utensa o comunque o no a doeuvia oua.</em> Pe identificâlo l'è quindi necessaio doeuviâ o nummero do so adresso IP. I adresci IP poeuan però ese condivixi da ciù utenti. Se t'ê un utente anonnimo e ti ritegni che i commenti inte sta pagina no se riferiscian a ti, [[Special:CreateAccount|crea una noeuva utensa]] o donque [[Special:UserLogin|intra con quella che ti g'hæ za]] pe evitâ de chì avanti de ese confuzo con di atri utenti anonnimi .",
+       "noarticletext": "Po-u momento a pagina çercâ a l'è vêua. Ti poeu [[Special:Search/{{PAGENAME}}|çercâ sto tittolo]] inti atre pagine do scito, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} çercâ inti registri correlæ] oppû [{{fullurl:{{FULLPAGENAME}}|action=edit}} creâ questa pagina]</span>.",
        "noarticletext-nopermission": "Òua a pàgina çercâ a l'è vêua. L'è poscìbile [[Special:Search/{{PAGENAME}}|çercâ sto tìtolo]] inte di âtre pàgine do scîto o <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} çercâ inti registri corelæ]</span>, ma no ti gh'hæ i outorizzaçioin pe creâ sta paggina.",
        "missing-revision": "La verscion #$1 da paggina \"{{FULLPAGENAME}}\" a no l'esiste.\n\nQuesto succede solitamente se inta stoia ti sciacchi un vegio ingancio a una paggina scassâ.\n\nI dettaggi peuan ese attrovæ into [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} registro de scançellaçioin].",
        "userpage-userdoesnotexist": "L'utensa \"$1\" a no corisponde a un utente registròu.\nTi veu davei creâ o modificâ sta paggina?",
        "previewnote": "'''Questa chì a l'è solo 'n'anteprimma; i cangiamenti no son ancon stæti sarvæ!'''",
        "continue-editing": "Vanni a l'area de modiffica",
        "previewconflict": "L'anteprimma a mostra o scrito presente inta casella de modiffica de d'ato coscì comme o l'apaiâ se ti çerni de sarvalo òua.",
-       "session_fail_preview": "'''N'emmo posciuo elaborâ a modiffica a caosa da pèrdia di dæti relativi a-a sescion.'''\nRitenta.\nSe-o problema o persciste, preuva a [[Special:UserLogout|sciortî]] e a intrâ torna.'''",
+       "session_fail_preview": "Spiaxenti. No è støto poscibile elaboâ a modifica perché son andæti persci i dæti relativi a-a sescion.\n\nFoscia t'ê stæto disconnesso. <strong>Verifica d'ese ancon collegou e riproeuva</strong>.\nSe o problema o persciste, ti poeu provâ a [[Special:UserLogout|scollegate]] e effettuâ un nuoeuvo accesso, controllando che o to browser o l'açette i cookie da questo scito.",
+       "session_fail_preview_html": "Spiaxenti. No è stæto poscibbile elaboâ a modifica perché son anæti persci i dæti relativi a-a sescion.\n\n<em>Scicomme {{SITENAME}} o g'ha de l'HTML sgroeuzzo attivou e gh'è stæto una perdia di dæti da sescion, l'anteprimma a l'è ascosa comme precaoçion contra i attacchi JavaScript.</em>\n\n<strong>Se se tratta de un normale tentativo d'anteprimma, riproeuva.</strong> \nSe o problema o persciste, ti poeu provâ a [[Special:UserLogout|scollegati]] e effettuâ un noeuvo accesso, controllando che o to browser o l'açette i cookie da questo scito.",
+       "token_suffix_mismatch": "'''A modiffica a no l'è stæta sarvâ perché o to client o l'ha mostrou de gestî in moddo errou i carattei de puntezatua into token associou a-a mæxima. Pe evitâ una poscibile corruçion do testo da pagina, l'è stæto refuou l'intrega modiffica. Questa scituaçion a poeu veificase, de votte, quande s'adoeuvia di serviççi de proxy anonnimi via web che presentan di bug.'''",
        "edit_form_incomplete": "'''De parte do formulaio de modiffica n'han razonto o server; controlla che e modiffiche seggian intatte e ripreuva.'''",
        "editing": "Modiffica de $1",
        "creating": "T'ê apreuvo a creâ $1",
        "editingold": "<strong>Attençion: t'ê apreuvo a modificâ una verscion non aggiornâ da paggina.</strong>\nSarvandola coscì, tutti i cangi fæti doppo sta verscion saian sorvescriti.",
        "yourdiff": "Differense",
        "copyrightwarning": "Nota: Tùtte e contribuçioìn a {{SITENAME}} van conscideræ comme rilasciæ drento a-i termini da licensa d'ûso $2 (veddi $1 pe savéine de ciù).\nSe no ti veu che i testi teu pêuan esse modificæ da quarchedùn sensa limitaçioìn, no mandâli a {{SITENAME}}.<br />\nInviando o testo ti diciâri, sott'a teu responsabilitæ, ch'o l'é stæto scrîto da ti personalmente oppure ch'o l'é stæto piggiòu da 'na fonte de pùbrico domìnio òu anàlogamente lìbea.<br />\n'''NO INVI MATERIÂLE COVERTO DA DRÎTI D'AUTÔ SENSA OUTORIZAÇION!'''",
+       "copyrightwarning2": "Pe piaxei tegni presente che tutti i contributi a {{SITENAME}} poeuan ese modificæ, alteræ ò scassæ da di atri contributoî.\nSe no ti voeu che se faççe ravaxo di to testi, alloa no stali manco a mette.<br />\nInviando o testo ti deciæi ascì, sotta a to responsabilitæ, ch'o l'è stæto scrito da ti personalmente oppù ch'o l'è stæto copiou da 'na fonte de pubbrico dominnio o scimilemente libera (vedi $1 pe dettaggi).\n'''No stanni a inviâ do mateiâ protetto da-o drito d'aotô sença aotorizzaçion!'''",
        "editpage-cannot-use-custom-model": "O modello do contegnuo de sta paggina o no peu ese modificòu.",
        "longpageerror": "'''Errô: o scrito inviou o l'è longo {{PLURAL:$1|1|$1}} kilobyte, ciu che-o mascimo consentio de ({{PLURAL:$2|1|$2}} kilobyte).'''\nNo se peu sarvâ.",
+       "readonlywarning": "<strong>Attençion</strong>: o database o l'è bloccou pe manutençion, no l'è momentaniamente poscibile sarvâ e modifiche effettuæ.\nPe no perdile, coppile in te'n file de testo e sarvilo in atteisa do sbrocco do database.\n\nL'amministratô de scistema ch'o l'ha misso l'abrocco o l'ha fornio questa spiegaçion: $1.",
        "protectedpagewarning": "'''Attençion: questa paggina a l'è stæta bloccâ de moddo che solo i utenti co-i privileggi d'amministratô possan modificala.'''\nL'urtimo elemento do registro o l'è riportou chì appreuvo pe referença:",
        "semiprotectedpagewarning": "'''Notta:''' Questa paggina a l'è stæta bloccä de moddo che solo i utenti registræ possan modificâla.\nL'urtimo elemento do registro o l'è riportou chì appreuvo pe referensa:",
        "cascadeprotectedwarning": "'''Attençion:''' Questa paggina a l'è stæta bloccâ de moddo che solo i utenti co-i privileggi d'amministratô possan modificala da-o momento ch'a l'é inclusa seleçionando a proteçion \"ricorsciva\" {{PLURAL:$1|inta paggina|inte paggine}}:",
        "permissionserrors": "No ti g'hæ o permisso",
        "permissionserrorstext": "No ti g'hæ i permissi pe fâlo, pe  {{PLURAL:$1|questa raxon|queste raxoin}}:",
        "permissionserrorstext-withaction": "No ti g'hæ i permìssi pe $2 pe {{PLURAL:$1|sta raxon|ste raxoìn}}:",
-       "contentmodelediterror": "No ti peu modificâ sta verscion da-o momento che o so modello de contegnuo o l'è <code>$1</code>, mentre o corrente modello de contegnuo da paggina o l'è <code>$2</code>.",
+       "contentmodelediterror": "No ti peu modificâ sta verscion da-o momento che o so modello de contegnuo o l'è <code>$1</code>, ch'o diffeisce da-o corrente modello de contegnuo da paggina <code>$2</code>.",
        "recreate-moveddeleted-warn": "Atençión: ti stæ pe ricreâ 'na pàgina zà scancelâ into passòu.'''\n\nConsciddera se l'è o caxo de continoâ  a cangiâ 'sta pàgina.\nPe comoditæ e cancellaçioìn e i stramui son pubricæ chì sotta:",
        "moveddeleted-notice": "Sta pàgina a l'é stæta scancelâ.\nA lista de scancelaçioìn e di stramui son riportæ chi de sotta pe informaçión.",
        "moveddeleted-notice-recent": "Spiaxenti, sta paggina a l'è stæta scassâ reçentemente (inte urtime 24 oe).\n\nE açioin de cançellaçion e spostamento pe questa paggina son disponibile chì appreuvo pe referença.",
        "content-model-javascript": "JavaScript",
        "content-json-empty-object": "Ogetto veuo",
        "content-json-empty-array": "Array veuo",
+       "deprecated-self-close-category": "Paggine ch'adoeuvian di tag HTML aoto-ciosi non vallidi",
+       "deprecated-self-close-category-desc": "A pagina a conten di tag HTML aoto-ciosi non vallidi, comme <code>&lt;b/></code> o <code>&lt;span/></code>. O comportamento de questi fito o cangiâ pe ese coerente co-e specifiche HTML5, pe questo o so utilizzo into wikitesto o l'è deprecou.",
        "duplicate-args-warning": "<strong>Avvertensa:</strong> [[:$1]] o ciamma [[:$2]] con ciù de un valô pe-o parammetro \"$3\".  Solo l'urtimo valô fornio o saiâ deuviou.",
        "duplicate-args-category": "Paggine che ciamman i template deuviando di parammetri duplicæ",
        "duplicate-args-category-desc": "A paggina a conten de ciamæ a di template ch'adeuvian di argomenti duplicæ, comme presempio <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> ò <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "revdelete-selected-text": "{{PLURAL:$1|Verscion seleçionâ|Verscioin seleçionæ}} de [[:$2]]:",
        "revdelete-selected-file": "{{PLURAL:$1|Verscion seleçionata|Verscioin seleçionæ}} do file [[:$2]]:",
        "logdelete-selected": "{{PLURAL:$1|Evento do registro seleçionou|Eventi do registro seleçionou}}:",
+       "revdelete-text-text": "E verscioin scassæ apparian ancon inta cronologia da pagina, ma parte do so contegnuo o saiâ inaccescibile a-o pubbrico.",
+       "revdelete-text-file": "E verscioin di file scassæ apparian ancon inta cronologia do file, ma parte do so contegnuo a saiâ inaccescibile a-o pubbrico.",
+       "logdelete-text": "I eventi scassæ apparian ancon inti registri, ma 'na parte do so contegnuo a saiâ inaccescibile a-o pubbrico.",
+       "revdelete-text-others": "Di atri amministratoî saian ancon in grou de accede a-i contegnui ascoxi e porian ripristinali, se no l'è stæto impostou de restriçioin azontive.",
+       "revdelete-confirm": "Pe piaxei conferma che questo l'è quante t'intendi fâ, che t'accapisci e conseguençe, e che ti o fæ into rispetto de [[{{MediaWiki:Policy-url}}|linnie guidda]].",
+       "revdelete-suppress-text": "A rimoçion a saiæ da doeuviâ '''solo''' che inti seguenti caxi:\n* informaçioin potençialmente diffamatoie\n* dæti personæ inoportun\n*: ''indiriççi, nummeri de teleffono, coddiçi fiscali, ecc.''",
        "revdelete-legend": "Imposta e limitaçioin de vixibilitæ:",
        "revdelete-hide-text": "Testo da verscion",
        "revdelete-hide-image": "Ascondi i contegnui do file",
        "revdelete-unsuppress": "Elimmina e limitaçioin in scê verscioin ripristinæ",
        "revdelete-log": "Raxon:",
        "revdelete-submit": "Applica {{PLURAL:$1|a-a verscion seleçionâ|a-e verscioin seleçionæ}}",
-       "revdelete-success": "'''Vixibilitæ da verscion agiornâ con successo.'''",
+       "revdelete-success": "'''Vixibilitæ da verscion agiornâ.'''",
        "revdelete-failure": "'''A vixibilitæ da verscion a no peu ese agiornâ:'''\n$1",
-       "logdelete-success": "'''Registro de vixibilitæ impostou con successo.'''",
+       "logdelete-success": "'''Registro de vixibilitæ impostou.'''",
        "logdelete-failure": "No s'è posciuo impostâ o registro de vixibilitæ: $1",
        "revdel-restore": "càngia a vixibilitæ",
        "pagehist": "Stoia da paggina",
        "revdelete-modify-no-access": "Imposcibbile modificâ l'ogetto con dæta $1 $2 in quanto o l'è stæto identificou comme \"riservou\" e no se dispon-e do relativo accesso.",
        "revdelete-modify-missing": "Imposcibbile modificâ l'ogetto con ID $1: into database o no gh'è!",
        "revdelete-no-change": "'''Attençion:''' l'ogetto con dæta $1 $2 o l'aveiva zà e impostaçioin de vixibilitæ domandæ.",
+       "revdelete-concurrent-change": "Imposcibile modificâ l'ogetto con dæta $1, $2: pâ che o so stato o segge stæto modificou da un atro utente dementre che ti ti çercavi de modificalo.",
+       "revdelete-only-restricted": "Errô inte l'asconde l'ogetto datou $1, $2: no ti poeu asconde i ogetti a-i amministratoî sença seleçionâ un-a di atre opçioin de vixibilitæ.",
        "revdelete-reason-dropdown": "* Raxoin ciù comun-e pe-o scassamento\n** Violaçion do drito d'outô\n** Commenti ò informaçioin personæ inappropiæ\n** Nomme utente inappropiou\n** Informaçion potençialmente diffamatoia",
        "revdelete-otherreason": "Un atro motivo:",
        "revdelete-reasonotherlist": "Un'atra raxon",
        "revdelete-edit-reasonlist": "Modiffica e raxoin do scassamento",
        "revdelete-offender": "Aotô da verscion:",
        "suppressionlog": "Registro de sopprescioin",
+       "suppressionlogtext": "De sotta gh'è 'na lista de scassatue e di brocchi con do contegnuo ascoso a-i amministratoî.\nAmia a [[Special:BlockList|lista di blocchi]] pe l'elenco di bandi e di blocchi attivi a-o momento.",
        "mergehistory": "Union de stoie da paggina",
+       "mergehistory-header": "Questa pagina a consente de unî e verscioin d'a cronologia de 'na pagina sorgente a 'na pagina ciù reçente.\nAsseguite che sto cangio o mantegne a continuitæ storica da pagina.",
        "mergehistory-box": "Unisci a stoia de doe paggine:",
        "mergehistory-from": "Paggina d'origgine:",
        "mergehistory-into": "Paggina de destinaçion:",
        "mergehistory-empty": "Nisciun-a verscion da unî.",
        "mergehistory-done": "{{PLURAL:$3|Una verscion de $1 a l'è stæta unia|$3 vercsioin de $1 son stæte unie}} a-a stoia de [[:$2]].",
        "mergehistory-fail": "Imposcibbile unî e stoie. Verificâ a paggina e i parammetri temporali.",
+       "mergehistory-fail-bad-timestamp": "O timestamp o no l'è vallido.",
+       "mergehistory-fail-invalid-source": "A pagina sorgente a no l'è vallida.",
+       "mergehistory-fail-invalid-dest": "A pagina de destinaçion a no l'è vallida.",
+       "mergehistory-fail-no-change": "L'union de cronologie a no l'ha unio arcun-a verscion. Ricontrolla e pagine e i parametri temporali.",
+       "mergehistory-fail-permission": "Aotorizzaçioin insuffixente pe unî e cronologie.",
+       "mergehistory-fail-self-merge": "A paggina d'origgine e quella de destinaçion son a mæxima",
+       "mergehistory-fail-timestamps-overlap": "E verscioin d'origine se sorvepon-an ò vegnan doppo e verscioin de destinaçion.",
        "mergehistory-fail-toobig": "Imposcibbile eseguî l'union da stoia essendoghe ciu che o limmite de $1 {{PLURAL:$1|verscion|verscioin}} da mesciâ.",
        "mergehistory-no-source": "A paggina d'origgine $1 a no l'existe.",
        "mergehistory-no-destination": "A paggina de destinaçion $1 a no l'existe.",
        "powersearch-togglenone": "Nisciun",
        "powersearch-remember": "Aregordite a seleçion pe-e proscime riçerche",
        "search-external": "Riçerca esterna",
-       "searchdisabled": "La riçerca de {{SITENAME}} a no l'è attiva. Into fra tempo ti peu çercâ in sce Google. \nNotta che i seu indexi di contegnui de {{SITENAME}} porrieivan no ese aggiornæ.)",
+       "searchdisabled": "A riçerca de {{SITENAME}} a no l'è attiva. Into fratempo ti peu çercâ in sce Google. \nNotta che i so indexi di contegnui de {{SITENAME}} porrieivan no ese aggiornæ.",
        "search-error": "S'è verificou 'n errô durante a riçerca: $1",
        "preferences": "Preferençe",
        "mypreferences": "Preferençe",
        "recentchangesdays-max": "Mascimo $1 {{PLURAL:$1|giorno|giorni}}",
        "recentchangescount": "Nummero de modiffiche da mostrâ pe difetto:",
        "prefs-help-recentchangescount": "Comprende i urtime modiffiche, paggine de stoie e registri.",
+       "prefs-help-watchlist-token2": "Questa a l'è a ciave segretta pe-o feed web di to oservæ.\nChiunque a conosce saiâ in graddo de leze i to oservæ, quindi no stanni a condividdila. [[Special:ResetTokens|Clicca chi se ti g'hæ de besoeugno de rempostâla]].",
        "savedprefs": "E teu preferençe son stæte sarvæ.",
        "savedrights": "I driti utente de {{GENDER:$1|$1}} son stæti sarvæ.",
        "timezonelegend": "Fuso oraio:",
        "prefs-custom-css": "CSS personalizzou",
        "prefs-custom-js": "JavaScript personalizzou",
        "prefs-common-css-js": "CSS/JavaScript condiviso pe tutte e pelle:",
+       "prefs-reset-intro": "L'è poscibile doeuviâ sta pagina pe rimpostâ e propie preferençe a quelle predefinie do scito.\nL'opiaçion a no poeu ese annullâ.",
        "prefs-emailconfirm-label": "Conferma de l'e-mail:",
        "youremail": "Indirìsso email:",
        "username": "{{GENDER:$1|Nomme utente}}:",
        "yourvariant": "Variante da lengoa do contegnuo:",
        "prefs-help-variant": "A variante o grafia co-a quæ ti prefeisci che e paggine do wiki seggian mostræ.",
        "yournick": "Nomiagio:",
+       "prefs-help-signature": "I commenti inte pagine de discuscion devan ese firmæ con \"<nowiki>~~~~</nowiki>\" ch'o saiâ convertio inta proppia firma seguia da-a dæta.",
        "badsig": "Errô in ta firma; controlla i comandi HTML.",
        "badsiglength": "A firma scelta a l'è troppo longa.\nA non deve passâ $1 {{PLURAL:$1|carattere|caratteri}}.",
+       "yourgender": "Comme rifeise a ti?",
+       "gender-unknown": "Into mençunâte, o software o l'adoeuviâ de paole de genere noeütro ogni votta che saiâ poscibile",
        "gender-male": "O l'è registrou insce {{SITENAME}}",
        "gender-female": "A l'è registrâ insce {{SITENAME}}",
        "prefs-help-gender": "L'impostassion de sta preferensa a l'è opsionâ.\nO software o deuvia sto valô pe addressâse a ti e mensunate a-i atri deuviando o gennere grammaticale apropiou.\nQuesta informassion a saiâ pubblica.",
        "userrights": "Manezzo di driti di utenti",
        "userrights-lookup-user": "Gestisci i gruppi di utenti",
        "userrights-user-editname": "Scrivi o teu nomme utente:",
-       "editusergroup": "Modiffica i gruppi di utenti",
+       "editusergroup": "Modiffica groppi {{GENDER:$1|utente}}",
        "editinguser": "Apreuvo a cangiâ i driti de l'{{GENDER:$1|utente}} <strong>[[User:$1|$1]]</strong> $2",
        "userrights-editusergroup": "Modiffica i gruppi di utenti",
-       "saveusergroups": "Sarva i gruppi di utenti",
+       "saveusergroups": "Sarva groppi {{GENDER:$1|utente}}",
        "userrights-groupsmember": "Membro de:",
        "userrights-groupsmember-auto": "Membro impliçito de:",
        "userrights-reason": "Raxon:",
        "userrights-changeable-col": "Gruppi che ti peu modificâ",
        "userrights-unchangeable-col": "Gruppi che no ti peu modificâ",
        "userrights-conflict": "Conflito de modiffica di driti utente! Pe piaxei controlla e conferma e teu modiffiche.",
-       "userrights-removed-self": "T'hæ rimosso con successo i teu driti. E quindi, no ti saiæ ciù in grou de accede a questa paggina.",
+       "userrights-removed-self": "T'hæ rimosso i teu driti. E quindi, no ti saiæ ciù in grou de accede a questa paggina.",
        "group": "Gruppo:",
        "group-user": "Ûtenti",
        "group-autoconfirmed": "Utenti aotoconfermæ",
        "grouppage-sysop": "{{ns:project}}:Amministratoî",
        "grouppage-bureaucrat": "{{ns:project}}:Buroccrati",
        "grouppage-suppress": "{{ns:project}}:Soppressoî",
+       "right-read": "Leze paggine",
+       "right-edit": "Modiffica paggine",
+       "right-createpage": "Crea paggine (escluse e pagine de discuscion)",
+       "right-createtalk": "Crea pagine de discuscion",
+       "right-createaccount": "Crea noeuve utençe",
+       "right-autocreateaccount": "Accede aotomaticamente con un'utença esterna",
+       "right-minoredit": "Marca e modifiche comme menoî",
        "right-move": "Mescia e paggine",
+       "right-move-subpages": "Mescia e pagine insemme a-e relative sottopagine",
+       "right-move-rootuserpages": "Mescia e pagine prinçipæ di utenti",
+       "right-move-categorypages": "Mescia e categorie",
+       "right-movefile": "Mescia i file",
+       "right-suppressredirect": "O no crea un redirect aotomatico quande se mescia una pagina",
+       "right-upload": "Carrega file",
+       "right-reupload": "Sorvescrive un file existente",
+       "right-reupload-own": "Sorvescrive un file existente caregou da-o mæximo utente",
+       "right-reupload-shared": "Sorvescrive localmente di file presenti inte l'archivio condiviso",
+       "right-upload_by_url": "Carega un file da un addreçço URL",
+       "right-purge": "Nettezza a cache do scito sença conferma",
+       "right-autoconfirmed": "Non soggetto a-o limmite de açioin pe IP",
+       "right-bot": "Da trattâ comme processo aotomatico",
+       "right-nominornewtalk": "Fa scì che e modiffiche menoî a-e pagine de discuscion no faççan comparî l'avviso de noeuvo messaggio",
+       "right-apihighlimits": "Doeuvia di limmiti ciù erti pe-e interrogaçioin API",
        "right-writeapi": "Deuvia l'API in scrittua",
+       "right-delete": "Scassa e paggine",
+       "right-bigdelete": "Scassa e pagine con cronologie longhe",
+       "right-deletelogentry": "Scassa e ripristina voxe de registro specifiche",
+       "right-deleterevision": "Scassa e ripristina de verscion specifiche de pagine",
+       "right-deletedhistory": "Vixualizza e verscioin da cronologia scassæ, sença o so testo associou",
+       "right-deletedtext": "Vixualizza o testo scassou e-e modifiche fra de verscioin scassæ",
+       "right-browsearchive": "Çerca inte pagine scassæ",
+       "right-undelete": "Recuppera una paggina",
+       "right-suppressrevision": "Vedde, asconde e ripristina de verscioin specifiche de pagine a quæ-se-segge utente",
+       "right-viewsuppressed": "Vedde de verscioin ascose a qua-se-sæ utente",
+       "right-suppressionlog": "Vixualizza i registri privæ",
+       "right-block": "Blocca e modifiche di atri utenti",
+       "right-blockemail": "Impedisce a un utente de mandâ de email",
+       "right-hideuser": "Blocca un nomme utente, ascondendolo a-o pubbrico",
+       "right-ipblock-exempt": "Ignora i blocchi di IP, i blocchi aotomatichi e i blocchi de range de IP",
+       "right-unblockself": "Sblocca lê mæximo",
+       "right-protect": "Camgia i livelli de proteçion e modifica e pagine protette ricorscivamente",
+       "right-editprotected": "Modifica e pagine protette con \"{{int:protect-level-sysop}}\"",
+       "right-editsemiprotected": "Modifica e pagine protette con \"{{int:protect-level-autoconfirmed}}\"",
+       "right-editcontentmodel": "Modifica o modello de contegnuo de 'na paggina",
+       "right-editinterface": "Modiffica l'interfaccia utente",
+       "right-editusercssjs": "Modifica i file CSS e JS di atri utenti",
+       "right-editusercss": "Modifica i file CSS di atri utenti",
+       "right-edituserjs": "Modiffica i file JS di atri utenti",
+       "right-editmyusercss": "Modifica o file CSS do proppio utente",
+       "right-editmyuserjs": "Modifica o file JavaScript do proppio utente",
+       "right-viewmywatchlist": "Vixualizza i proppi osservæ speçiali",
+       "right-editmywatchlist": "Modifica i to osservæ speçiali. Da notâ che arcun-e açioin porian ancon azonze de pagine anche sença questo drito.",
+       "right-viewmyprivateinfo": "Vixualizza i proppi dæti personæ (presempio: adreçço email, nomme veo)",
+       "right-editmyprivateinfo": "Modiffica i proppi dæti personæ (presempio: addreçço email, nomme veo)",
+       "right-editmyoptions": "Modiffica e proppie preferençe",
+       "right-rollback": "Annulla rapidamente e modifiche de l'urtimo utente ch'o l'ha modificou una pagina in particolâ",
+       "right-markbotedits": "Marca e modifiche soggette a rollback comme effettuæ da bot",
+       "right-noratelimit": "No soggetto a-o limmite de açioin",
+       "right-import": "Importa de pagine da di atri wiki",
+       "right-importupload": "Importa paggine da un upload de file",
+       "right-patrol": "Marca e modifiche di altri utenti comme controllæ",
+       "right-autopatrol": "Marca aotomaticamente e proppie modifiche comme controllæ",
+       "right-patrolmarks": "Doeuvia a fonçion de verifica di urtime modiffiche",
+       "right-unwatchedpages": "Vixualizza una lista de pagine non öservæ",
+       "right-mergehistory": "Unisce a cronologia de paggine",
+       "right-userrights": "Modiffica tutti i driti de l'utente",
+       "right-userrights-interwiki": "Modiffica i driti di utenti insce di atre wiki",
+       "right-siteadmin": "Abbrocca e sbrocca o database",
+       "right-override-export-depth": "Esporta e paggine includendo e pagine collegæ scin a 'na profonditæ de 5",
+       "right-sendemail": "Manda de email a di atri utenti",
+       "right-passwordreset": "Vedde i messaggi de rempostaçion da password",
+       "right-managechangetags": "Crea e attiva/disattiva i [[Special:Tags|etichette]]",
+       "right-applychangetags": "Apprica di [[Special:Tags|etichette]] a-e proppie modiffiche",
+       "right-changetags": "Azonze e leva de specifiche [[Special:Tags|etichette]] insce scingole verscioin o voxe de registro",
        "newuserlogpage": "Nêuvi utenti",
        "rightslog": "Diritti d'ûtente",
        "action-edit": "càngia sta pàgina",
        "whatlinkshere-next": "{{PLURAL:$1|sûccescivo|sûccescivi $1}}",
        "whatlinkshere-links": "← colegaménti",
        "whatlinkshere-hideredirs": "$1 i rendirissamenti",
-       "whatlinkshere-hidetrans": "$1 Incluxon",
+       "whatlinkshere-hidetrans": "$1 Incluxoin",
        "whatlinkshere-hidelinks": "$1 colegaménti",
        "whatlinkshere-hideimages": "$1 colegaménti di file",
        "whatlinkshere-filters": "Filtri",
        "thumbnail_error": "Errô inta creassion da miniatûa: $1",
        "thumbnail_invalid_params": "Parametri da a imàginetta non validi",
        "importlogpage": "Importassioîn",
-       "tooltip-pt-userpage": "A teu pagina utilizatô",
-       "tooltip-pt-mytalk": "E mæ discûscioîn",
-       "tooltip-pt-preferences": "E mæ preferense",
+       "tooltip-pt-userpage": "A {{GENDER:|to}} pagina utente",
+       "tooltip-pt-mytalk": "A {{GENDER:|to}} paggina de discuscion",
+       "tooltip-pt-preferences": "E {{GENDER:|to}} preferençe",
        "tooltip-pt-watchlist": "A lista de pagine che ti g'hæ sotta osservaçion",
-       "tooltip-pt-mycontris": "E mæ contribuçioìn",
+       "tooltip-pt-mycontris": "A lista de {{GENDER:|to}} contribuçioin",
        "tooltip-pt-login": "Consegemmo a registraçión, ma a no l'è obrigatoia.",
        "tooltip-pt-logout": "Sciorti",
        "tooltip-pt-createaccount": "Se conseggia de registrase e de intrâ, sciben che no segge obligatoio",
        "tooltip-t-whatlinkshere": "Lista de tùtte e pagine che son colegæ a sta chì.",
        "tooltip-t-recentchangeslinked": "Ùrtimi càngi de pàgine colegæ a quésta",
        "tooltip-feed-atom": "Feed Atom pe sta pàgina",
-       "tooltip-t-contributions": "Lista de contribûssioîn de quest'utente",
+       "tooltip-t-contributions": "Lista de contribûssioin de {{GENDER:$1|questo|questa}} utente",
        "tooltip-t-emailuser": "Invia 'n messaggio e-mail a quest'utente",
        "tooltip-t-upload": "Carrega di file murtimediali",
        "tooltip-t-specialpages": "Lista de tùtte e pagine speçiâli",
index 0909fd5..93bed08 100644 (file)
        "protect-otherreason": "Anna/ytterlegare årsak:",
        "protect-otherreason-op": "Anna årsak",
        "protect-dropdown": "*Vanlege verneårsaker\n** Gjenteke hærverk\n** Gjenteke spam\n** Endringskrig\n** Side med mange vitjande",
-       "protect-edit-reasonlist": "Endrar verneårsaker",
+       "protect-edit-reasonlist": "Endra verneårsaker",
        "protect-expiry-options": "1 time:1 hour,1 dag:1 day,1 veke:1 week,2 veker:2 weeks,1 månad:1 month,3 månader:3 months,6 månader:6 months,1 år:1 year,endelaus:infinite",
        "restriction-type": "Tilgang:",
        "restriction-level": "Avgrensingsnivå:",
index c710e22..50d1937 100644 (file)
        "notvisiblerev": "Версия была удалена",
        "watchlist-details": "В вашем списке наблюдения $1 {{PLURAL:$1|страница|страницы|страниц}}, не считая страниц обсуждений.",
        "wlheader-enotif": "Уведомления по эл. почте включены.",
-       "wlheader-showupdated": "Страницы, изменившиеся с вашего последнего их посещения, выделены '''жирным''' шрифтом.",
+       "wlheader-showupdated": "СÑ\82Ñ\80аниÑ\86Ñ\8b, Ð¸Ð·Ð¼ÐµÐ½Ð¸Ð²Ñ\88иеÑ\81Ñ\8f Ñ\81 Ð²Ð°Ñ\88его Ð¿Ð¾Ñ\81леднего Ð¸Ñ\85 Ð¿Ð¾Ñ\81еÑ\89ениÑ\8f, Ð²Ñ\8bделенÑ\8b '''полÑ\83жиÑ\80нÑ\8bм''' Ñ\88Ñ\80иÑ\84Ñ\82ом.",
        "wlnote": "Ниже {{PLURAL:$1|показано последнее изменение|показаны <strong>$1</strong> последние изменения|показаны <strong>$1</strong> последних изменений}} за {{PLURAL:$2|последний час|последние <strong>$2</strong> часа|последние <strong>$2</strong> часов}}, по состоянию на $3 $4.",
        "wlshowlast": "Показать за последние $1 часов $2 дней",
        "watchlist-hide": "Скрыть",
index df4b1a5..7c78a48 100644 (file)
        "grant-createeditmovepage": "Прављење, уређивање и премештање страница",
        "grant-delete": "Брисање страница, измена и уноса у дневницима",
        "grant-editinterface": "Уређивање Медијавики именског простора и корисничких CSS/JavaScript страница",
+       "grant-editmycssjs": "Уређивање вашег CSS/JavaScript-а",
        "grant-editmyoptions": "Уређивање ваших подешавања",
        "grant-editmywatchlist": "Уређивање вашег списка надгледања",
        "grant-editpage": "Уређивање постојећих страница",
        "grant-editprotected": "Уређивање заштићених страница",
+       "grant-patrol": "Патролирање измена",
        "grant-protect": "Закључавање и откључавање страница",
        "grant-rollback": "Враћање измена",
+       "grant-sendemail": "Слање имејлова другим корисницима",
        "grant-uploadeditmovefile": "Отпремање, замена и премештање датотека",
        "grant-uploadfile": "Слање нових датотека",
        "grant-basic": "Основна права",
index 630d964..a063d40 100644 (file)
        "group-user-member": "صارف",
        "group-autoconfirmed-member": "خودتوثیق شدہ صارف",
        "group-bot-member": "خودکار صارف",
-       "group-sysop-member": "منتظم",
+       "group-sysop-member": "{{GENDER:$1|منتظم}}",
        "group-bureaucrat-member": "{{GENDER:$1|مامور اداری}}",
        "group-suppress-member": "{{GENDER:$1|suppressor}}",
        "grouppage-user": "{{ns:project}}:صارفین",
        "grouppage-autoconfirmed": "{{ns:project}}:خود توثیق شدہ صارف",
        "grouppage-bot": "{{ns:project}}:روبہ جات",
        "grouppage-sysop": "{{ns:project}}:منتظمین",
-       "grouppage-bureaucrat": "بیورو کریٹ",
+       "grouppage-bureaucrat": "{{ns:project}}:مامورین اداری",
        "right-upload": "ملفات زبراثقال (اپ لوڈ) کریں",
        "right-writeapi": "اے پی آئی لکھائی کا استعمال",
        "right-delete": "صفحات حذف کریں",
        "logentry-move-move": "$1 نے صفحہ $3 کو بجانب $4 منتقل کیا",
        "logentry-newusers-create": "صارف کھاتہ $1 {{GENDER:$2|بنایا گیا}}",
        "logentry-protect-modify": "$1 نے $3 کا درجۂ حفاظت {{GENDER:$2|تبدیل کیا}} $4",
+       "logentry-rights-rights": "$1 نے {{GENDER:$6|$3}} کی گروہی رکنیت از $4 تا $5 {{GENDER:$2|تبدیل کی}}",
        "logentry-upload-upload": "$1 {{GENDER:$2|اپلوڈ}} $3",
        "rightsnone": "(کچھ نہیں)",
        "revdelete-summary": "خلاصۂ تدوین",
index 3c01f7e..4edc5d8 100644 (file)
        "enhancedrc-history": "歷史",
        "recentchanges": "近段辰光个改动",
        "recentchanges-legend": "近段辰光个改动选项",
-       "recentchanges-summary": "登该个页面浪跟踪最近对维基百科个改动。",
+       "recentchanges-summary": "登该个页面浪跟踪最近对本站个改动。",
        "recentchanges-feed-description": "跟踪此订阅垃拉 wiki 高头个最近更改。",
        "recentchanges-label-newpage": "箇编辑建立着新页",
        "recentchanges-label-minor": "箇是小编写",
index dddae77..e46859d 100644 (file)
        "youhavenewmessagesmanyusers": "您有来自多个用户的$1($2)。",
        "newmessageslinkplural": "{{PLURAL:$1|新信息|999=新消息}}",
        "newmessagesdifflinkplural": "最后{{PLURAL:$1|更改|999=更改}}",
-       "youhavenewmessagesmulti": "在$1有新信息",
+       "youhavenewmessagesmulti": "在$1有新信息",
        "editsection": "编辑",
        "editold": "编辑",
        "viewsourceold": "查看源代码",
        "password-login-forbidden": "这个用户名称及密码的使用是被禁止的。",
        "mailmypassword": "重置密码",
        "passwordremindertitle": "{{SITENAME}}的新临时密码",
-       "passwordremindertext": "有人(可能是您,来自IP地址$1)已请求{{SITENAME}}的新密码($4)。\n用户“$2”的一个新临时密码现在已被设置好为“$3”。\n如果这个动作是您所指示的,您便需要立即登录并选择一个新的密码。\n您的临时密码会于$5天内过期。\n\n如果是其他人发出了该请求,或者您已经记起了您的密码并不准备改变它,您可以忽略此消息并继续使用您的旧密码。",
+       "passwordremindertext": "有人(可能是您,来自IP地址$1)已请求{{SITENAME}}的新密码($4)。用户“$2”的一个新临时密码现在已被设置好为“$3”。如果这个动作是您所指示的,您便需要立即登录并选择一个新的密码。您的临时密码会于{{PLURAL:$5|一天|$5天}}内过期。\n\n如果是其他人发出了该请求,或者您已经记起了您的密码并不准备改变它,您可以忽略此消息并继续使用您的旧密码。",
        "noemail": "用户\"$1\"没有登记电子邮件地址。",
        "noemailcreate": "您需要提供一个有效的电子邮件地址",
        "passwordsent": "用户\"$1\"的新密码已经寄往所登记的电子邮件地址。\n请在收到后再登录。",
        "clearyourcache": "<strong>注意:</strong>在保存之后,您可能需要清除浏览器缓存才能看到所作出的变更的影响。\n* <strong>Firefox或Safari:</strong>按住<em>Shift</em>的同时单击<em>刷新</em>,或按<em>Ctrl-F5</em>或<em>Ctrl-R</em>(Mac为<em>⌘-R</em>)\n* <strong>Google Chrome:</strong>按<em>Ctrl-Shift-R</em>(Mac为<em>⌘-Shift-R</em>)\n* <strong>Internet Explorer:</strong>按住<em>Ctrl</em>的同时单击<em>刷新</em>,或按<em>Ctrl-F5</em>\n* <strong>Opera:</strong>前往<em>菜单 → 设置</em>(Mac为<em>Opera → Preferences</em>),然后<em>隐私和安全 → 清除浏览数据 → 缓存的图片和文件</em>。",
        "usercssyoucanpreview": "<strong>提示:</strong>在保存前请用“{{int:showpreview}}”按钮来测试您新的 CSS 。",
        "userjsyoucanpreview": "<strong>提示:</strong>在保存前请用“{{int:showpreview}}”按钮来测试您新的 JavaScript 。",
-       "usercsspreview": "<strong>请记住您现在只是在预览的用户CSS。它尚未保存!</strong>",
+       "usercsspreview": "<strong>请记住您现在只是在预览的用户CSS。它尚未保存!</strong>",
        "userjspreview": "<strong>请记住您现在只是在测试/预览您的用户JavaScript。它尚未保存!</strong>",
-       "sitecsspreview": "<strong>请记住现在只是在预览该CSS。它尚未保存!</strong>",
+       "sitecsspreview": "<strong>请记住现在只是在预览该CSS。它尚未保存!</strong>",
        "sitejspreview": "<strong>请记住您现在只是在预览该JavaScript代码。它尚未保存!</strong>",
        "userinvalidcssjstitle": "<strong>警告:</strong>不存在皮肤“$1”。注意自定义的 .css 和 .js 页要使用小写标题,例如,{{ns:user}}:Foo/vector.css 不同于 {{ns:user}}:Foo/Vector.css。",
        "updated": "(已更新)",
        "emailpagetext": "您可以使用下面的表格发送电子邮件信息至该{{GENDER:$1|用户}}。您在[[Special:Preferences|系统设置]]中输入的电子邮件地址将显示为邮件的“发件人”地址,所以该用户将可以直接回复您。",
        "defemailsubject": "来自{{SITENAME}}用户“$1”的电子邮件",
        "usermaildisabled": "用户电子邮件停用",
-       "usermaildisabledtext": "不能发送电子邮件至本wiki的其他用户",
+       "usermaildisabledtext": "不能发送电子邮件至本wiki的其他用户",
        "noemailtitle": "无电子邮件地址",
        "noemailtext": "该用户还没有指定一个有效的电子邮件地址。",
        "nowikiemailtext": "该用户已经选择不接收来自其他用户的电子邮件。",
        "rcpatroldisabled": "最近更改巡查已禁用",
        "rcpatroldisabledtext": "最近更改巡查功能目前已关闭。",
        "markedaspatrollederror": "不能标志为已检查",
-       "markedaspatrollederrortext": "需要指定一个版本以标记为已巡查。",
+       "markedaspatrollederrortext": "需要指定一个版本以标记为已巡查。",
        "markedaspatrollederror-noautopatrol": "你不能把自己的更改标记为已检查。",
        "markedaspatrollednotify": "$1的更改已被标记为已巡查。",
        "markedaspatrollederrornotify": "标记为已巡查失败。",
index dab710e..42f8f05 100644 (file)
        "content-model-css": "CSS",
        "content-json-empty-object": "空物件",
        "content-json-empty-array": "空陣列",
+       "deprecated-self-close-category": "使用無效 Self-closed HTML 標籤的頁面",
+       "deprecated-self-close-category-desc": "頁面包含無效的 Self-closed HTML 標籤,如 <code>&lt;b/></code> or <code>&lt;span/></code>。這些標籤的模式將會更改為與 HTML5 規格一致,因此 wikitext 的這種用法已停用。",
        "duplicate-args-warning": "<strong>警告:</strong> [[:$1]] 呼叫 [[:$2]] 的 \"$3\" 參數使用了超過一次,僅會使用提供的最後一個參數值。",
        "duplicate-args-category": "模板呼叫時使用重複的參數的頁面",
        "duplicate-args-category-desc": "該頁面包含重複使用參數的模板呼叫,如 <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> 或 <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>。",
diff --git a/maintenance/cleanupEmptyCategories.php b/maintenance/cleanupEmptyCategories.php
new file mode 100644 (file)
index 0000000..b8a246e
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+/**
+ * Clean up empty categories in the category table.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script to clean up empty categories in the category table.
+ *
+ * @ingroup Maintenance
+ * @since 1.28
+ */
+class CleanupEmptyCategories extends LoggedUpdateMaintenance {
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription(
+                       <<<TEXT
+This script will clean up the category table by removing entries for empty
+categories without a description page and adding entries for empty categories
+with a description page. It will print out progress indicators every batch. The
+script is perfectly safe to run on large, live wikis, and running it multiple
+times is harmless. You may want to use the throttling options if it's causing
+too much load; they will not affect correctness.
+
+If the script is stopped and later resumed, you can use the --mode and --begin
+options with the last printed progress indicator to pick up where you left off.
+
+When the script has finished, it will make a note of this in the database, and
+will not run again without the --force option.
+TEXT
+               );
+
+               $this->addOption(
+                       'mode',
+                       '"add" empty categories with description pages, "remove" empty categories '
+                       . 'without description pages, or "both"',
+                       false,
+                       true
+               );
+               $this->addOption(
+                       'begin',
+                       'Only do categories whose names are alphabetically after the provided name',
+                       false,
+                       true
+               );
+               $this->addOption(
+                       'throttle',
+                       'Wait this many milliseconds after each batch. Default: 0',
+                       false,
+                       true
+               );
+       }
+
+       protected function getUpdateKey() {
+               return 'cleanup empty categories';
+       }
+
+       protected function doDBUpdates() {
+               $mode = $this->getOption( 'mode', 'both' );
+               $begin = $this->getOption( 'begin', '' );
+               $throttle = $this->getOption( 'throttle', 0 );
+
+               if ( !in_array( $mode, [ 'add', 'remove', 'both' ] ) ) {
+                       $this->output( "--mode must be 'add', 'remove', or 'both'.\n" );
+                       return false;
+               }
+
+               $dbw = $this->getDB( DB_MASTER );
+
+               $throttle = intval( $throttle );
+
+               if ( $mode === 'add' || $mode === 'both' ) {
+                       if ( $begin !== '' ) {
+                               $where = [ 'page_title > ' . $dbw->addQuotes( $begin ) ];
+                       } else {
+                               $where = [];
+                       }
+
+                       $this->output( "Adding empty categories with description pages...\n" );
+                       while ( true ) {
+                               # Find which category to update
+                               $rows = $dbw->select(
+                                       [ 'page', 'category' ],
+                                       'page_title',
+                                       array_merge( $where, [
+                                               'page_namespace' => NS_CATEGORY,
+                                               'cat_title' => null,
+                                       ] ),
+                                       __METHOD__,
+                                       [
+                                               'ORDER BY' => 'page_title',
+                                               'LIMIT' => $this->mBatchSize,
+                                       ],
+                                       [
+                                               'category' => [ 'LEFT JOIN', 'page_title = cat_title' ],
+                                       ]
+                               );
+                               if ( !$rows || $rows->numRows() <= 0 ) {
+                                       # Done, hopefully.
+                                       break;
+                               }
+
+                               foreach ( $rows as $row ) {
+                                       $name = $row->page_title;
+                                       $where = [ 'page_title > ' . $dbw->addQuotes( $name ) ];
+
+                                       # Use the row to update the category count
+                                       $cat = Category::newFromName( $name );
+                                       if ( !is_object( $cat ) ) {
+                                               $this->output( "The category named $name is not valid?!\n" );
+                                       } else {
+                                               $cat->refreshCounts();
+                                       }
+                               }
+                               $this->output( "--mode=$mode --begin=$name\n" );
+
+                               wfWaitForSlaves();
+                               usleep( $throttle * 1000 );
+                       }
+
+                       $begin = '';
+               }
+
+               if ( $mode === 'remove' || $mode === 'both' ) {
+                       if ( $begin !== '' ) {
+                               $where = [ 'cat_title > ' . $dbw->addQuotes( $begin ) ];
+                       } else {
+                               $where = [];
+                       }
+                       $i = 0;
+
+                       $this->output( "Removing empty categories without description pages...\n" );
+                       while ( true ) {
+                               # Find which category to update
+                               $rows = $dbw->select(
+                                       [ 'category', 'page' ],
+                                       'cat_title',
+                                       array_merge( $where, [
+                                               'page_title' => null,
+                                               'cat_pages' => 0,
+                                       ] ),
+                                       __METHOD__,
+                                       [
+                                               'ORDER BY' => 'cat_title',
+                                               'LIMIT' => $this->mBatchSize,
+                                       ],
+                                       [
+                                               'page' => [ 'LEFT JOIN', [
+                                                       'page_namespace' => NS_CATEGORY, 'page_title = cat_title'
+                                               ] ],
+                                       ]
+                               );
+                               if ( !$rows || $rows->numRows() <= 0 ) {
+                                       # Done, hopefully.
+                                       break;
+                               }
+                               foreach ( $rows as $row ) {
+                                       $name = $row->cat_title;
+                                       $where = [ 'cat_title > ' . $dbw->addQuotes( $name ) ];
+
+                                       # Use the row to update the category count
+                                       $cat = Category::newFromName( $name );
+                                       if ( !is_object( $cat ) ) {
+                                               $this->output( "The category named $name is not valid?!\n" );
+                                       } else {
+                                               $cat->refreshCounts();
+                                       }
+                               }
+
+                               $this->output( "--mode=remove --begin=$name\n" );
+
+                               wfWaitForSlaves();
+                               usleep( $throttle * 1000 );
+                       }
+               }
+
+               $this->output( "Category cleanup complete.\n" );
+
+               return true;
+       }
+}
+
+$maintClass = 'CleanupEmptyCategories';
+require_once RUN_MAINTENANCE_IF_MAIN;
index 12cfed8..48b2250 100644 (file)
@@ -336,9 +336,9 @@ CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp);
 CREATE INDEX /*i*/cl_collation_ext ON /*_*/categorylinks (cl_collation, cl_to, cl_type, cl_from);
 
 --
--- Track all existing categories.  Something is a category if 1) it has an en-
--- try somewhere in categorylinks, or 2) it once did.  Categories might not
--- have corresponding pages, so they need to be tracked separately.
+-- Track all existing categories. Something is a category if 1) it has an entry
+-- somewhere in categorylinks, or 2) it has a description page. Categories
+-- might not have corresponding pages, so they need to be tracked separately.
 --
 CREATE TABLE /*_*/category (
   -- Primary key
index 89aeb9c..9c9bdfb 100644 (file)
@@ -623,9 +623,9 @@ CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp);
 CREATE INDEX /*i*/cl_collation_ext ON /*_*/categorylinks (cl_collation, cl_to, cl_type, cl_from);
 
 --
--- Track all existing categories.  Something is a category if 1) it has an en-
--- try somewhere in categorylinks, or 2) it once did.  Categories might not
--- have corresponding pages, so they need to be tracked separately.
+-- Track all existing categories. Something is a category if 1) it has an entry
+-- somewhere in categorylinks, or 2) it has a description page. Categories
+-- might not have corresponding pages, so they need to be tracked separately.
 --
 CREATE TABLE /*_*/category (
   -- Primary key
index e35c3d7..e838a53 100644 (file)
@@ -948,7 +948,7 @@ return [
        ],
        'mediawiki.content.json' => [
                'position' => 'top',
-               'styles' => 'resources/src/mediawiki/mediawiki.content.json.css',
+               'styles' => 'resources/src/mediawiki/mediawiki.content.json.less',
        ],
        'mediawiki.confirmCloseWindow' => [
                'scripts' => [
diff --git a/resources/src/mediawiki/mediawiki.content.json.css b/resources/src/mediawiki/mediawiki.content.json.css
deleted file mode 100644 (file)
index 91fa02a..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/*!
- * CSS for styling HTML-formatted JSON Schema objects
- *
- * @file
- * @author Munaf Assaf <massaf@wikimedia.org>
- */
-
-.mw-json {
-       border-collapse: collapse;
-       border-spacing: 0;
-       font-style: normal;
-}
-
-.mw-json th,
-.mw-json td {
-       border: 1px solid #808080;
-       font-size: 16px;
-       padding: 0.5em 1em;
-}
-
-.mw-json .value,
-.mw-json-single-value {
-       background-color: #dcfae3;
-       font-family: monospace, monospace;
-       white-space: pre-wrap;
-}
-
-.mw-json-single-value {
-       background-color: #eee;
-}
-
-.mw-json-empty {
-       background-color: #fff;
-       font-style: italic;
-}
-
-.mw-json tr {
-       margin-bottom: 0.5em;
-       background-color: #eee;
-}
-
-.mw-json th {
-       background-color: #fff;
-       font-weight: normal;
-}
-
-.mw-json caption {
-       /* For stylistic reasons, suppress the caption of the outermost table */
-       display: none;
-}
-
-.mw-json table caption {
-       color: #808080;
-       display: inline-block;
-       font-size: 10px;
-       font-style: italic;
-       margin-bottom: 0.5em;
-       text-align: left;
-}
diff --git a/resources/src/mediawiki/mediawiki.content.json.less b/resources/src/mediawiki/mediawiki.content.json.less
new file mode 100644 (file)
index 0000000..91fa02a
--- /dev/null
@@ -0,0 +1,59 @@
+/*!
+ * CSS for styling HTML-formatted JSON Schema objects
+ *
+ * @file
+ * @author Munaf Assaf <massaf@wikimedia.org>
+ */
+
+.mw-json {
+       border-collapse: collapse;
+       border-spacing: 0;
+       font-style: normal;
+}
+
+.mw-json th,
+.mw-json td {
+       border: 1px solid #808080;
+       font-size: 16px;
+       padding: 0.5em 1em;
+}
+
+.mw-json .value,
+.mw-json-single-value {
+       background-color: #dcfae3;
+       font-family: monospace, monospace;
+       white-space: pre-wrap;
+}
+
+.mw-json-single-value {
+       background-color: #eee;
+}
+
+.mw-json-empty {
+       background-color: #fff;
+       font-style: italic;
+}
+
+.mw-json tr {
+       margin-bottom: 0.5em;
+       background-color: #eee;
+}
+
+.mw-json th {
+       background-color: #fff;
+       font-weight: normal;
+}
+
+.mw-json caption {
+       /* For stylistic reasons, suppress the caption of the outermost table */
+       display: none;
+}
+
+.mw-json table caption {
+       color: #808080;
+       display: inline-block;
+       font-size: 10px;
+       font-style: italic;
+       margin-bottom: 0.5em;
+       text-align: left;
+}
index 26463fd..dabf475 100644 (file)
@@ -45,6 +45,16 @@ div.gallerytext {
        word-wrap: break-word;
 }
 
+.galleryfilename {
+       display: block;
+}
+
+.galleryfilename-truncate {
+       white-space: nowrap;
+       overflow: hidden;
+       text-overflow: ellipsis;
+}
+
 /* new gallery stuff */
 ul.mw-gallery-nolines li.gallerybox div.thumb {
        background-color: transparent;
@@ -169,4 +179,4 @@ ul.mw-gallery-slideshow li.gallerycarousel {
 
 .mw-gallery-slideshow-img-container a {
        display: block;
-}
\ No newline at end of file
+}
index dd50607..d6d2b29 100644 (file)
@@ -35,7 +35,7 @@
 #
 # You can also set the following parser properties via test options:
 #  wgEnableUploads, wgAllowExternalImages, wgMaxTocLevel,
-#  wgLinkHolderBatchSize, wgRawHtml
+#  wgLinkHolderBatchSize, wgRawHtml, wgInterwikiMagic
 #
 # For testing purposes, temporary articles can created:
 # !!article / NAMESPACE:TITLE / !!text / ARTICLE TEXT / !!endarticle
@@ -8361,6 +8361,7 @@ Blah blah blah
 <link rel="mw:PageProp/Language" href="http://zh.wikipedia.org/wiki/Chinese"/>
 !! end
 
+## parsoid html2wt will lose the space variations
 !! test
 Interlanguage link with spacing
 !! options
@@ -8391,6 +8392,7 @@ Blah blah blah
 <link rel="mw:PageProp/Language" href="http://zh.wikipedia.org/wiki/Chinese"/>
 !! end
 
+## parsoid html2wt will lose the space variations
 !! test
 Interlanguage link variations
 !! options
@@ -8410,6 +8412,7 @@ Blah blah blah
 <link rel="mw:PageProp/Language" href="http://es.wikipedia.org/wiki/Foo_bar" />
 !! end
 
+## parsoid html2wt will normalize the space to _
 !! test
 Space and question mark encoding in interlanguage links (T95473)
 !! options
@@ -8470,6 +8473,34 @@ Blah blah blah
 <link rel="mw:PageProp/Language" title="Multilingual" href="http://wikisource.org/wiki/Article"/>
 !! end
 
+## PHP parser tests script needs an update
+## Parsoid html2wt will normalize output to [[:zh:Chinese]]
+!! test
+Language links render as inline links if $wgInterwikiMagic=false
+!! options
+wgInterwikiMagic=false
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+Blah blah blah
+[[zh:Chinese]]
+!! html/parsoid
+<p>Blah blah blah <a rel="mw:ExtLink" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese">zh:Chinese</a></p>
+!! end
+
+## PHP parser tests script needs an update
+## Parsoid html2wt will normalize output to [[:zh:Chinese]]
+!! test
+Language links render as inline links in the Talk namespace
+!! options
+title=Talk:Foo
+parsoid=wt2html,wt2wt,html2html
+!! wikitext
+Blah blah blah
+[[zh:Chinese]]
+!! html/parsoid
+<p>Blah blah blah <a rel="mw:ExtLink" href="http://zh.wikipedia.org/wiki/Chinese" title="zh:Chinese">zh:Chinese</a></p>
+!! end
+
 !! test
 Parsoid-specific test: Wikilinks with &nbsp; should RT properly
 !! options
@@ -18827,7 +18858,7 @@ File:Foobar.jpg
                <li class="gallerybox" style="width: 155px"><div style="width: 155px">
                        <div class="thumb" style="height: 150px;">Nonexistent.jpg</div>
                        <div class="gallerytext">
-<p><a href="/wiki/File:Nonexistent.jpg" title="File:Nonexistent.jpg">Nonexistent.jpg</a><br />
+<p><a href="/wiki/File:Nonexistent.jpg" class="galleryfilename galleryfilename-truncate" title="File:Nonexistent.jpg">Nonexistent.jpg</a>
 caption
 </p>
                        </div>
@@ -18835,14 +18866,14 @@ caption
                <li class="gallerybox" style="width: 155px"><div style="width: 155px">
                        <div class="thumb" style="height: 150px;">Nonexistent.jpg</div>
                        <div class="gallerytext">
-<p><a href="/wiki/File:Nonexistent.jpg" title="File:Nonexistent.jpg">Nonexistent.jpg</a><br />
+<p><a href="/wiki/File:Nonexistent.jpg" class="galleryfilename galleryfilename-truncate" title="File:Nonexistent.jpg">Nonexistent.jpg</a>
 </p>
                        </div>
                </div></li>
                <li class="gallerybox" style="width: 155px"><div style="width: 155px">
                        <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
                        <div class="gallerytext">
-<p><a href="/wiki/File:Foobar.jpg" title="File:Foobar.jpg">Foobar.jpg</a><br />
+<p><a href="/wiki/File:Foobar.jpg" class="galleryfilename galleryfilename-truncate" title="File:Foobar.jpg">Foobar.jpg</a>
 some <b>caption</b> <a href="/wiki/Main_Page" title="Main Page">Main Page</a>
 </p>
                        </div>
@@ -18850,7 +18881,7 @@ some <b>caption</b> <a href="/wiki/Main_Page" title="Main Page">Main Page</a>
                <li class="gallerybox" style="width: 155px"><div style="width: 155px">
                        <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
                        <div class="gallerytext">
-<p><a href="/wiki/File:Foobar.jpg" title="File:Foobar.jpg">Foobar.jpg</a><br />
+<p><a href="/wiki/File:Foobar.jpg" class="galleryfilename galleryfilename-truncate" title="File:Foobar.jpg">Foobar.jpg</a>
 </p>
                        </div>
                </div></li>
@@ -27065,6 +27096,21 @@ B <ref group="X" name="b" />
 </references>
 !! end
 
+!! test
+DOMDiff: Edits to content nested in elements with templated attributes should not be lost (T139388)
+!! options
+parsoid={
+  "modes": ["selser"],
+  "changes": [
+    [ "div:first-child", "text", "bar" ]
+  ]
+}
+!! wikitext
+<div style="{{1x|color:red;}}%">foo</div>
+!! wikitext/edited
+<div style="{{1x|color:red;}}%">bar</div>
+!! end
+
 !! test
 Empty LI (T49673)
 !! wikitext
@@ -27080,3 +27126,17 @@ Empty LI (T49673)
 <li>b</li>
 </ul>
 !! end
+
+!! test
+Thumbnail output
+!! wikitext
+[[File:Thumb.png|thumb]]
+!! html/php+tidy
+<div class="thumb tright">
+<div class="thumbinner" style="width:137px;"><a href="/wiki/File:Thumb.png" class="image"><img alt="Thumb.png" src="http://example.com/images/e/ea/Thumb.png" width="135" height="135" class="thumbimage" /></a>
+<div class="thumbcaption">
+<div class="magnify"><a href="/wiki/File:Thumb.png" class="internal" title="Enlarge"></a></div>
+</div>
+</div>
+</div>
+!! end
index 4721793..e44db83 100644 (file)
@@ -738,6 +738,16 @@ class HtmlTest extends MediaWikiTestCase {
                                '1x.png 1x, 1_5x.png 1.5x, 2x.png 2x',
                                'pixel depth keys may omit a trailing "x"'
                        ],
+                       [
+                               [ '1'  => 'small.png', '1.5' => 'large.png', '2'  => 'large.png' ],
+                               'small.png 1x, large.png 1.5x',
+                               'omit larger duplicates'
+                       ],
+                       [
+                               [ '1'  => 'small.png', '2'  => 'large.png', '1.5' => 'large.png' ],
+                               'small.png 1x, large.png 1.5x',
+                               'omit larger duplicates in irregular order'
+                       ],
                ];
        }
 
index 2d2e726..030d9d5 100644 (file)
@@ -2444,10 +2444,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        public function testUpdateNotificationTimestamp_watchersExist() {
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
-                       ->method( 'select' )
+                       ->method( 'selectFieldValues' )
                        ->with(
-                               [ 'watchlist' ],
-                               [ 'wl_user' ],
+                               'watchlist',
+                               'wl_user',
                                [
                                        'wl_user != 1',
                                        'wl_namespace' => 0,
@@ -2455,18 +2455,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                        'wl_notificationtimestamp IS NULL'
                                ]
                        )
-                       ->will(
-                               $this->returnValue( [
-                                       $this->getFakeRow( [ 'wl_user' => '2' ] ),
-                                       $this->getFakeRow( [ 'wl_user' => '3' ] )
-                               ] )
-                       );
-               $mockDb->expects( $this->once() )
-                       ->method( 'onTransactionIdle' )
-                       ->with( $this->isType( 'callable' ) )
-                       ->will( $this->returnCallback( function( $callable ) {
-                               $callable();
-                       } ) );
+                       ->will( $this->returnValue( [ '2', '3' ] ) );
                $mockDb->expects( $this->once() )
                        ->method( 'update' )
                        ->with(
@@ -2502,10 +2491,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        public function testUpdateNotificationTimestamp_noWatchers() {
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
-                       ->method( 'select' )
+                       ->method( 'selectFieldValues' )
                        ->with(
-                               [ 'watchlist' ],
-                               [ 'wl_user' ],
+                               'watchlist',
+                               'wl_user',
                                [
                                        'wl_user != 1',
                                        'wl_namespace' => 0,
@@ -2516,8 +2505,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->will(
                                $this->returnValue( [] )
                        );
-               $mockDb->expects( $this->never() )
-                       ->method( 'onTransactionIdle' );
                $mockDb->expects( $this->never() )
                        ->method( 'update' );
 
@@ -2551,19 +2538,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
                        ) );
                $mockDb->expects( $this->once() )
-                       ->method( 'select' )
+                       ->method( 'selectFieldValues' )
                        ->will(
-                               $this->returnValue( [
-                                       $this->getFakeRow( [ 'wl_user' => '2' ] ),
-                                       $this->getFakeRow( [ 'wl_user' => '3' ] )
-                               ] )
+                               $this->returnValue( [ '2', '3' ] )
                        );
-               $mockDb->expects( $this->once() )
-                       ->method( 'onTransactionIdle' )
-                       ->with( $this->isType( 'callable' ) )
-                       ->will( $this->returnCallback( function( $callable ) {
-                               $callable();
-                       } ) );
                $mockDb->expects( $this->once() )
                        ->method( 'update' );
 
index bb747c7..bb7eb79 100644 (file)
@@ -250,26 +250,71 @@ class DatabaseMysqlBaseTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideComparePositions
         */
-       function testHasReached( MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos ) {
-               $this->assertTrue( $higherPos->hasReached( $lowerPos ) );
-               $this->assertTrue( $higherPos->hasReached( $higherPos ) );
-               $this->assertTrue( $lowerPos->hasReached( $lowerPos ) );
-               $this->assertFalse( $lowerPos->hasReached( $higherPos ) );
+       function testHasReached( MySQLMasterPos $lowerPos, MySQLMasterPos $higherPos, $match ) {
+               if ( $match ) {
+                       $this->assertTrue( $lowerPos->channelsMatch( $higherPos ) );
+
+                       $this->assertTrue( $higherPos->hasReached( $lowerPos ) );
+                       $this->assertTrue( $higherPos->hasReached( $higherPos ) );
+                       $this->assertTrue( $lowerPos->hasReached( $lowerPos ) );
+                       $this->assertFalse( $lowerPos->hasReached( $higherPos ) );
+               } else { // channels don't match
+                       $this->assertFalse( $lowerPos->channelsMatch( $higherPos ) );
+
+                       $this->assertFalse( $higherPos->hasReached( $lowerPos ) );
+                       $this->assertFalse( $lowerPos->hasReached( $higherPos ) );
+               }
        }
 
        function provideComparePositions() {
                return [
+                       // Binlog style
                        [
                                new MySQLMasterPos( 'db1034-bin.000976', '843431247' ),
-                               new MySQLMasterPos( 'db1034-bin.000976', '843431248' )
+                               new MySQLMasterPos( 'db1034-bin.000976', '843431248' ),
+                               true
                        ],
                        [
                                new MySQLMasterPos( 'db1034-bin.000976', '999' ),
-                               new MySQLMasterPos( 'db1034-bin.000976', '1000' )
+                               new MySQLMasterPos( 'db1034-bin.000976', '1000' ),
+                               true
                        ],
                        [
                                new MySQLMasterPos( 'db1034-bin.000976', '999' ),
-                               new MySQLMasterPos( 'db1035-bin.000976', '1000' )
+                               new MySQLMasterPos( 'db1035-bin.000976', '1000' ),
+                               false
+                       ],
+                       // MySQL GTID style
+                       [
+                               new MySQLMasterPos( 'db1-bin.2', '1', '3E11FA47-71CA-11E1-9E33-C80AA9429562:23' ),
+                               new MySQLMasterPos( 'db1-bin.2', '2', '3E11FA47-71CA-11E1-9E33-C80AA9429562:24' ),
+                               true
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1-bin.2', '1', '3E11FA47-71CA-11E1-9E33-C80AA9429562:99' ),
+                               new MySQLMasterPos( 'db1-bin.2', '2', '3E11FA47-71CA-11E1-9E33-C80AA9429562:100' ),
+                               true
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1-bin.2', '1', '3E11FA47-71CA-11E1-9E33-C80AA9429562:99' ),
+                               new MySQLMasterPos( 'db1-bin.2', '2', '1E11FA47-71CA-11E1-9E33-C80AA9429562:100' ),
+                               false
+                       ],
+                       // MariaDB GTID style
+                       [
+                               new MySQLMasterPos( 'db1-bin.2', '1', '255-11-23' ),
+                               new MySQLMasterPos( 'db1-bin.2', '2', '255-11-24' ),
+                               true
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1-bin.2', '1', '255-11-99' ),
+                               new MySQLMasterPos( 'db1-bin.2', '2', '255-11-100' ),
+                               true
+                       ],
+                       [
+                               new MySQLMasterPos( 'db1-bin.2', '1', '255-11-999' ),
+                               new MySQLMasterPos( 'db1-bin.2', '2', '254-11-1000' ),
+                               false
                        ],
                ];
        }
index af5d3ac..254cfbd 100644 (file)
@@ -1139,16 +1139,16 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->tearDownFiles();
                $this->doTestStreamFile( $path, $content, $alreadyExists );
                $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestStreamFile( $path, $content, $alreadyExists );
+               $this->tearDownFiles();
        }
 
        private function doTestStreamFile( $path, $content ) {
                $backendName = $this->backendClass();
 
-               // Test doStreamFile() directly to avoid header madness
-               $class = new ReflectionClass( $this->backend );
-               $method = $class->getMethod( 'doStreamFile' );
-               $method->setAccessible( true );
-
                if ( $content !== null ) {
                        $this->prepare( [ 'dir' => dirname( $path ) ] );
                        $status = $this->create( [ 'dst' => $path, 'content' => $content ] );
@@ -1156,18 +1156,19 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Creation of file at $path succeeded ($backendName)." );
 
                        ob_start();
-                       $method->invokeArgs( $this->backend, [ [ 'src' => $path ] ] );
+                       $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1 ] );
                        $data = ob_get_contents();
                        ob_end_clean();
 
                        $this->assertEquals( $content, $data, "Correct content streamed from '$path'" );
                } else { // 404 case
                        ob_start();
-                       $method->invokeArgs( $this->backend, [ [ 'src' => $path ] ] );
+                       $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1 ] );
                        $data = ob_get_contents();
                        ob_end_clean();
 
-                       $this->assertEquals( '', $data, "Correct content streamed from '$path' ($backendName)" );
+                       $this->assertRegExp( '#<h1>File not found</h1>#', $data,
+                               "Correct content streamed from '$path' ($backendName)" );
                }
        }
 
@@ -1181,6 +1182,53 @@ class FileBackendTest extends MediaWikiTestCase {
                return $cases;
        }
 
+       public function testStreamFileRange() {
+               $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
+               $this->doTestStreamFileRange();
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestStreamFileRange();
+               $this->tearDownFiles();
+       }
+
+       private function doTestStreamFileRange() {
+               $backendName = $this->backendClass();
+
+               $base = self::baseStorePath();
+               $path = "$base/unittest-cont1/e/b/z/range_file.txt";
+               $content = "0123456789ABCDEF";
+
+               $this->prepare( [ 'dir' => dirname( $path ) ] );
+               $status = $this->create( [ 'dst' => $path, 'content' => $content ] );
+               $this->assertGoodStatus( $status,
+                       "Creation of file at $path succeeded ($backendName)." );
+
+               static $ranges = [
+                       'bytes=0-0'   => '0',
+                       'bytes=0-3'   => '0123',
+                       'bytes=4-8'   => '45678',
+                       'bytes=15-15' => 'F',
+                       'bytes=14-15' => 'EF',
+                       'bytes=-5'    => 'BCDEF',
+                       'bytes=-1'    => 'F',
+                       'bytes=10-16' => 'ABCDEF',
+                       'bytes=10-99' => 'ABCDEF',
+               ];
+
+               foreach ( $ranges as $range => $chunk ) {
+                       ob_start();
+                       $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1,
+                               'options' => [ 'range' => $range ] ] );
+                       $data = ob_get_contents();
+                       ob_end_clean();
+
+                       $this->assertEquals( $chunk, $data, "Correct chunk streamed from '$path' for '$range'" );
+               }
+       }
+
        /**
         * @dataProvider provider_testGetFileContents
         * @covers FileBackend::getFileContents
index 80efcb3..7f9a772 100644 (file)
@@ -30,6 +30,32 @@ class XmlTypeCheckTest extends PHPUnit_Framework_TestCase {
                $this->assertFalse( $testXML->wellFormed );
        }
 
+       /**
+        * Verify we check for recursive entity DOS
+        *
+        * (If the DOS isn't properly handled, the test runner will probably go OOM...)
+        */
+       public function testRecursiveEntity() {
+               $xml = <<<'XML'
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE foo [
+       <!ENTITY test "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
+       <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
+       <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
+       <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
+       <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
+       <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;">
+       <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;">
+       <!ENTITY g "-00000000000000000000000000000000000000000000000000000000000000000000000-">
+]>
+<foo>
+<bar>&test;</bar>
+</foo>
+XML;
+               $check = XmlTypeCheck::newFromString( $xml );
+               $this->assertFalse( $check->wellFormed );
+       }
+
        /**
         * @covers XMLTypeCheck::processingInstructionHandler
         */
index ea86535..be7fe91 100644 (file)
@@ -108,7 +108,7 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers ExtensionProcessor::extractConfig
+        * @covers ExtensionProcessor::extractConfig1
         */
        public function testExtractConfig() {
                $processor = new ExtensionProcessor;