Merge "Api method documentation tweaks"
authorDemon <chadh@wikimedia.org>
Wed, 18 Jul 2012 18:24:43 +0000 (18:24 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 18 Jul 2012 18:24:43 +0000 (18:24 +0000)
119 files changed:
.jshintignore
.jshintrc
RELEASE-NOTES-1.20
docs/hooks.txt
includes/AutoLoader.php
includes/CacheHelper.php
includes/DataUpdate.php
includes/DefaultSettings.php
includes/DeferredUpdates.php
includes/EditPage.php
includes/Exception.php
includes/Export.php
includes/GlobalFunctions.php
includes/HTMLForm.php
includes/ImagePage.php
includes/Linker.php
includes/OutputPage.php
includes/Pager.php
includes/RecentChange.php
includes/SkinTemplate.php
includes/SqlDataUpdate.php
includes/WikiMap.php
includes/WikiPage.php
includes/actions/HistoryAction.php
includes/actions/InfoAction.php
includes/api/ApiBlock.php
includes/api/ApiDelete.php
includes/api/ApiMove.php
includes/api/ApiProtect.php
includes/api/ApiQueryAllCategories.php
includes/api/ApiQueryAllImages.php
includes/api/ApiQueryAllLinks.php
includes/api/ApiQueryAllPages.php
includes/api/ApiQueryCategories.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryDuplicateFiles.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryIWBacklinks.php
includes/api/ApiQueryIWLinks.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryImages.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryLangBacklinks.php
includes/api/ApiQueryLinks.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryStashImageInfo.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiQueryWatchlistRaw.php
includes/api/ApiRollback.php
includes/api/ApiUnblock.php
includes/api/ApiUndelete.php
includes/cache/ProcessCacheLRU.php [new file with mode: 0644]
includes/db/Database.php
includes/db/DatabaseOracle.php
includes/db/DatabasePostgres.php
includes/filerepo/LocalRepo.php
includes/filerepo/RepoGroup.php
includes/filerepo/backend/FileBackend.php
includes/filerepo/backend/FileBackendMultiWrite.php
includes/filerepo/backend/FileBackendStore.php
includes/filerepo/backend/lockmanager/MemcLockManager.php
includes/filerepo/file/File.php
includes/installer/Installer.php
includes/logging/LogFormatter.php
includes/logging/LogPage.php
includes/media/Generic.php
includes/media/MediaTransformOutput.php
includes/objectcache/MemcachedClient.php
includes/parser/LinkHolderArray.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/search/SearchOracle.php
includes/specials/SpecialContributions.php
includes/specials/SpecialFileDuplicateSearch.php
includes/specials/SpecialListusers.php
includes/specials/SpecialNewimages.php
includes/specials/SpecialProtectedpages.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialVersion.php
languages/Language.php
languages/messages/MessagesBa.php
languages/messages/MessagesEn.php
languages/messages/MessagesShi.php
maintenance/copyFileBackend.php
maintenance/language/messages.inc
maintenance/mergeMessageFileList.php
maintenance/syncFileBackend.php
maintenance/updateCollation.php
resources/jquery/jquery.arrowSteps.css
resources/jquery/jquery.arrowSteps.js
resources/jquery/jquery.autoEllipsis.js
resources/jquery/jquery.byteLength.js
resources/jquery/jquery.byteLimit.js
resources/jquery/jquery.checkboxShiftClick.js
resources/jquery/jquery.client.js
resources/jquery/jquery.collapsibleTabs.js
resources/jquery/jquery.color.js
resources/jquery/jquery.colorUtil.js
resources/jquery/jquery.delayedBind.js
resources/jquery/jquery.expandableField.js
resources/jquery/jquery.getAttrs.js
resources/jquery/jquery.highlightText.js
resources/jquery/jquery.localize.js
resources/jquery/jquery.makeCollapsible.js
resources/jquery/jquery.messageBox.js
resources/jquery/jquery.mwExtension.js
resources/jquery/jquery.placeholder.js
resources/jquery/jquery.qunit.completenessTest.js
resources/jquery/jquery.spinner.js
resources/jquery/jquery.suggestions.js
resources/jquery/jquery.tabIndex.js
resources/jquery/jquery.tablesorter.js
resources/jquery/jquery.textSelection.js
resources/mediawiki.action/mediawiki.action.edit.js
tests/phpunit/MediaWikiLangTestCase.php
tests/phpunit/docs/ExportDemoTest.php [new file with mode: 0644]
tests/phpunit/includes/cache/ProcessCacheLRUTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/languages/LanguageTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js

index fb2a42f..8ba7fc3 100644 (file)
@@ -5,6 +5,7 @@ resources/jquery/jquery.cycle.all.js
 resources/jquery/jquery.cookie.js
 resources/jquery/jquery.farbtastic.js
 resources/jquery/jquery.form.js
+resources/jquery/jquery.hoverIntent.js
 resources/jquery/jquery.js
 resources/jquery/jquery.json.js
 resources/jquery/jquery.mockjax.js
@@ -12,7 +13,9 @@ resources/jquery/jquery.qunit.js
 resources/jquery/jquery.validate.js
 resources/jquery/jquery.xmldom.js
 resources/jquery.effects
+resources/jquery.tipsy
 resources/jquery.ui
+resources/mediawiki.libs/mediawiki.libs.jpegmeta.js
 tests/jasmine/lib/jasmine-1.0.1/jasmine-html.js
 tests/jasmine/lib/jasmine-1.0.1/jasmine.js
 
index 4e82155..3c801c2 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -4,7 +4,19 @@
                "jQuery",
                "QUnit"
        ],
-       "browser": true,
+
+       "bitwise": true,
+       "curly": true,
+       "eqeqeq": true,
+       "immed": true,
+       "latedef": true,
+       "newcap": true,
+       "noempty": true,
+       "undef": true,
+       "trailing": true,
+
+       "laxbreak": true,
        "smarttabs": true,
-       "laxbreak": true
+
+       "browser": true
 }
index e56cf60..8bebaaa 100644 (file)
@@ -23,6 +23,8 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
 * The user right 'upload_by_url' is no longer given to sysops by default.
   This only affects installations which have $wgAllowCopyUploads set to true.
 * Removed f-prot support from $wgAntivirusSetup.
+* $wgDBerrorLogInUTC to log error in $wgDBerrorLog using an UTC date instead
+  of the wiki timezone set by $wgLocalTimezone.
 
 === New features in 1.20 ===
 * Added TitleIsAlwaysKnown hook which gets called when determining if a page exists.
@@ -93,6 +95,8 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
 * (bug 34678) Added InternalParseBeforeSanitize hook which gets called during Parser's
   internalParse method just before the parser removes unwanted/dangerous HTML tags.
 * (bug 36783) Implement jQuery Promise interface in mediawiki.api module.
+* Make dates in sortable tables sort according to the page content language
+  instead of the site content language
 
 === Bug fixes in 1.20 ===
 * (bug 30245) Use the correct way to construct a log page title.
@@ -161,6 +165,7 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
 * (bug 38152) jquery.tablesorter: Use .data() instead of .attr(), so that live
   values are used instead of just the fixed values from when the tablesorter
   was initialized.
+* (bug 38093) Gender of changed user groups missing in Special:Log/rights
 
 === API changes in 1.20 ===
 * (bug 34316) Add ability to retrieve maximum upload size from MediaWiki API.
@@ -185,8 +190,9 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
 * (bug 32381) Allow descending order for list=backlinks, list=embeddedin and list=imageusage.
 * (bug 32383) Allow descending order for list=langbacklinks.
 * API meta=siteinfo can now return the list of known variable IDs.
-* (bug 30836) siteinfo prop=specialpagealiases will no longer return nonexistent special pages.
 * (bug 35980) list=deletedrevs now honors drdir correctly in "all" mode (mode #3).
+* (bug 29290) API avoids mangling fields in continuation parameters
+* (bug 36987) API avoids mangling fields in continuation parameters
 
 === Languages updated in 1.20 ===
 
index 47654ce..5a836ae 100644 (file)
@@ -245,6 +245,10 @@ $block: The block from which the autoblock is coming.
 'AbortDiffCache': Can be used to cancel the caching of a diff
 &$diffEngine: DifferenceEngine object
 
+'AbortEmailNotification': Can be used to cancel email notifications for an edit.
+$editor: The User who made the change.
+$title: The Title of the page that was edited.
+
 'AbortLogin': Return false to cancel account login.
 $user: the User object being authenticated against
 $password: the password being submitted, not yet checked for validity
index 11cf616..6c37d1a 100644 (file)
@@ -419,6 +419,7 @@ $wgAutoloadLocalClasses = array(
        'LinkCache' => 'includes/cache/LinkCache.php',
        'MessageCache' => 'includes/cache/MessageCache.php',
        'ObjectFileCache' => 'includes/cache/ObjectFileCache.php',
+       'ProcessCacheLRU' => 'includes/cache/ProcessCacheLRU.php',
        'ResourceFileCache' => 'includes/cache/ResourceFileCache.php',
        'SquidUpdate' => 'includes/cache/SquidUpdate.php',
        'TitleDependency' => 'includes/cache/CacheDependency.php',
index 5209857..8199cb4 100644 (file)
@@ -73,7 +73,8 @@ interface ICacheHelper {
        function saveCache();
 
        /**
-        * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry..
+        * Sets the time to live for the cache, in seconds or a unix timestamp
+        * indicating the point of expiry...
         *
         * @since 1.20
         *
@@ -319,7 +320,8 @@ class CacheHelper implements ICacheHelper {
        }
 
        /**
-        * Sets the time to live for the cache, in seconds or a unix timestamp indicating the point of expiry..
+        * Sets the time to live for the cache, in seconds or a unix timestamp
+        * indicating the point of expiry...
         *
         * @since 1.20
         *
index 7203c3b..793d335 100644 (file)
 /**
  * Abstract base class for update jobs that do something with some secondary
  * data extracted from article.
+ *
+ * @note: subclasses should NOT start or commit transactions in their doUpdate() method,
+ *        a transaction will automatically be wrapped around the update. If need be,
+ *        subclasses can override the beginTransaction() and commitTransaction() methods.
  */
 abstract class DataUpdate implements DeferrableUpdate {
 
index eed7d58..690d39e 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * Default values for configuration settings.
+ * Default values for MediaWiki configuration settings.
  *
  *
  *                 NEVER EDIT THIS FILE
@@ -17,6 +17,9 @@
  * Documentation is in the source and on:
  * http://www.mediawiki.org/wiki/Manual:Configuration_settings
  *
+ * @warning  Note: this (and other things) will break if the autoloader is not
+ * enabled. Please include includes/AutoLoader.php before including this file.
+ *
  * 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
  * @file
  */
 
+/**
+ * @defgroup Globalsettings Global settings
+ */
+
 /**
  * @cond file_level_code
- * This is not a valid entry point, perform no further processing unless MEDIAWIKI is defined
+ * This is not a valid entry point, perform no further processing unless
+ * MEDIAWIKI is defined
  */
 if( !defined( 'MEDIAWIKI' ) ) {
        echo "This file is part of MediaWiki and is not a valid entry point\n";
        die( 1 );
 }
 
-# Create a site configuration object. Not used for much in a default install.
-# Note: this (and other things) will break if the autoloader is not enabled.
-# Please include includes/AutoLoader.php before including this file.
+/**
+ * wgConf hold the site configuration.
+ * Not used for much in a default install.
+ */
 $wgConf = new SiteConfiguration;
-/** @endcond */
 
 /** MediaWiki version number */
 $wgVersion = '1.20alpha';
@@ -59,10 +67,10 @@ $wgSitename = 'MediaWiki';
 /**
  * URL of the server.
  *
- * Example:
- * <code>
+ * @par Example:
+ * @code
  * $wgServer = 'http://example.com';
- * </code>
+ * @endcode
  *
  * This is usually detected correctly by MediaWiki. If MediaWiki detects the
  * wrong server, it will redirect incorrectly after you save a page. In that
@@ -273,11 +281,16 @@ $wgUploadStashScalerBaseUrl = false;
 
 /**
  * To set 'pretty' URL paths for actions other than
- * plain page views, add to this array. For instance:
+ * plain page views, add to this array.
+ *
+ * @par Example:
+ * Set pretty URL for the edit action:
+ * @code
  *   'edit' => "$wgScriptPath/edit/$1"
+ * @endcode
  *
- * There must be an appropriate script or rewrite rule
- * in place to handle these URLs.
+ * There must be an appropriate script or rewrite rule in place to handle these
+ * URLs.
  */
 $wgActionPaths = array();
 
@@ -300,7 +313,7 @@ $wgUploadStashMaxAge = 6 * 3600; // 6 hours
 $wgAllowImageMoving = true;
 
 /**
- * These are additional characters that should be replaced with '-' in file names
+ * These are additional characters that should be replaced with '-' in filenames
  */
 $wgIllegalFileChars = ":";
 
@@ -310,9 +323,10 @@ $wgIllegalFileChars = ":";
 $wgFileStore = array();
 
 /**
- * What directory to place deleted uploads in
+ * What directory to place deleted uploads in.
+ * Defaults to "{$wgUploadDirectory}/deleted".
  */
-$wgDeletedDirectory = false; //  Defaults to $wgUploadDirectory/deleted
+$wgDeletedDirectory = false;
 
 /**
  * Set this to true if you use img_auth and want the user to see details on why access failed.
@@ -418,10 +432,11 @@ $wgUseInstantCommons = false;
  * File backend structure configuration.
  * This is an array of file backend configuration arrays.
  * Each backend configuration has the following parameters:
- *     'name'        : A unique name for the backend
- *     'class'       : The file backend class to use
- *     'wikiId'      : A unique string that identifies the wiki (container prefix)
- *     'lockManager' : The name of a lock manager (see $wgLockManagers)
+ *  - 'name'        : A unique name for the backend
+ *  - 'class'       : The file backend class to use
+ *  - 'wikiId'      : A unique string that identifies the wiki (container prefix)
+ *  - 'lockManager' : The name of a lock manager (see $wgLockManagers)
+ *
  * Additional parameters are specific to the class used.
  */
 $wgFileBackends = array();
@@ -429,8 +444,8 @@ $wgFileBackends = array();
 /**
  * Array of configuration arrays for each lock manager.
  * Each backend configuration has the following parameters:
- *     'name'        : A unique name for the lock manger
- *     'class'       : The lock manger class to use
+ *  - 'name'        : A unique name for the lock manager
+ *  - 'class'       : The lock manger class to use
  * Additional parameters are specific to the class used.
  */
 $wgLockManagers = array();
@@ -439,12 +454,13 @@ $wgLockManagers = array();
  * Show EXIF data, on by default if available.
  * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php
  *
- * NOTE FOR WINDOWS USERS:
- * To enable EXIF functions, add the following lines to the
- * "Windows extensions" section of php.ini:
- *
+ * @note FOR WINDOWS USERS:
+ * To enable EXIF functions, add the following lines to the "Windows
+ * extensions" section of php.ini:
+ * @code{.ini}
  * extension=extensions/php_mbstring.dll
  * extension=extensions/php_exif.dll
+ * @endcode
  */
 $wgShowEXIF = function_exists( 'exif_read_data' );
 
@@ -512,11 +528,13 @@ $wgCopyUploadsDomains = array();
  * file and url keys. If the * key is set this value will be used as maximum
  * for non-specified types.
  *
- * For example:
+ * @par Example:
+ * @code
  * $wgMaxUploadSize = array(
  *     '*' => 250 * 1024,
  *     'url' => 500 * 1024,
  * );
+ * @endcode
  * Sets the maximum for all uploads to 250 kB except for upload-by-url, which
  * will have a maximum of 500 kB.
  *
@@ -526,8 +544,12 @@ $wgMaxUploadSize = 1024*1024*100; # 100MB
 /**
  * Point the upload navigation link to an external URL
  * Useful if you want to use a shared repository by default
- * without disabling local uploads (use $wgEnableUploads = false for that)
- * e.g. $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload';
+ * without disabling local uploads (use $wgEnableUploads = false for that).
+ *
+ * @par Example:
+ * @code
+ * $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload';
+ * @endcode
  */
 $wgUploadNavigationUrl = false;
 
@@ -539,14 +561,20 @@ $wgUploadNavigationUrl = false;
 $wgUploadMissingFileUrl = false;
 
 /**
- * Give a path here to use thumb.php for thumbnail generation on client request, instead of
- * generating them on render and outputting a static URL. This is necessary if some of your
- * apache servers don't have read/write access to the thumbnail path.
+ * Give a path here to use thumb.php for thumbnail generation on client
+ * request, instead of generating them on render and outputting a static URL.
+ * This is necessary if some of your apache servers don't have read/write
+ * access to the thumbnail path.
  *
- * Example:
+ * @par Example:
+ * @code
  *   $wgThumbnailScriptPath = "{$wgScriptPath}/thumb{$wgScriptExtension}";
+ * @endcode
  */
 $wgThumbnailScriptPath = false;
+/**
+ * @see $wgThumbnailScriptPath
+ */
 $wgSharedThumbnailScriptPath = false;
 
 /**
@@ -559,7 +587,8 @@ $wgSharedThumbnailScriptPath = false;
  * maintenance/rebuildImages.php to register them in the database. This is no
  * longer recommended, use maintenance/importImages.php instead.
  *
- * Note that this variable may be ignored if $wgLocalFileRepo is set.
+ * @note That this variable may be ignored if $wgLocalFileRepo is set.
+ * @todo Deprecate the setting and ultimately remove it from Core.
  */
 $wgHashedUploadDirectory = true;
 
@@ -584,13 +613,17 @@ $wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/File:";
  * This is the list of preferred extensions for uploading files. Uploading files
  * with extensions not in this list will trigger a warning.
  *
- * WARNING: If you add any OpenOffice or Microsoft Office file formats here,
+ * @warning If you add any OpenOffice or Microsoft Office file formats here,
  * such as odt or doc, and untrusted users are allowed to upload files, then
  * your wiki will be vulnerable to cross-site request forgery (CSRF).
  */
 $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' );
 
-/** Files with these extensions will never be allowed as uploads. */
+/**
+ * Files with these extensions will never be allowed as uploads.
+ * An array of file extensions to blacklist. You should append to this array
+ * if you want to blacklist additional files.
+ * */
 $wgFileBlacklist = array(
        # HTML may contain cookie-stealing JavaScript and web bugs
        'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht',
@@ -628,7 +661,7 @@ $wgAllowJavaUploads = false;
 /**
  * This is a flag to determine whether or not to check file extensions on upload.
  *
- * WARNING: setting this to false is insecure for public wikis.
+ * @warning Setting this to false is insecure for public wikis.
  */
 $wgCheckFileExtensions = true;
 
@@ -636,18 +669,21 @@ $wgCheckFileExtensions = true;
  * If this is turned off, users may override the warning for files not covered
  * by $wgFileExtensions.
  *
- * WARNING: setting this to false is insecure for public wikis.
+ * @warning Setting this to false is insecure for public wikis.
  */
 $wgStrictFileExtensions = true;
 
 /**
  * Setting this to true will disable the upload system's checks for HTML/JavaScript.
- * THIS IS VERY DANGEROUS on a publicly editable site, so USE wgGroupPermissions
- * TO RESTRICT UPLOADING to only those that you trust
+ *
+ * @warning THIS IS VERY DANGEROUS on a publicly editable site, so USE
+ * $wgGroupPermissions TO RESTRICT UPLOADING to only those that you trust
  */
 $wgDisableUploadScriptChecks = false;
 
-/** Warn if uploaded files are larger than this (in bytes), or false to disable*/
+/**
+ * Warn if uploaded files are larger than this (in bytes), or false to disable
+ */
 $wgUploadSizeWarning = false;
 
 /**
@@ -674,18 +710,18 @@ $wgTrustedMediaFormats = array(
  * Each entry in the array maps a MIME type to a class name
  */
 $wgMediaHandlers = array(
-       'image/jpeg' => 'JpegHandler',
-       'image/png' => 'PNGHandler',
-       'image/gif' => 'GIFHandler',
-       'image/tiff' => 'TiffHandler',
+       'image/jpeg'     => 'JpegHandler',
+       'image/png'      => 'PNGHandler',
+       'image/gif'      => 'GIFHandler',
+       'image/tiff'     => 'TiffHandler',
        'image/x-ms-bmp' => 'BmpHandler',
-       'image/x-bmp' => 'BmpHandler',
-       'image/x-xcf' => 'XCFHandler',
-       'image/svg+xml' => 'SvgHandler', // official
-       'image/svg' => 'SvgHandler', // compat
+       'image/x-bmp'    => 'BmpHandler',
+       'image/x-xcf'    => 'XCFHandler',
+       'image/svg+xml'  => 'SvgHandler', // official
+       'image/svg'      => 'SvgHandler', // compat
        'image/vnd.djvu' => 'DjVuHandler', // official
-       'image/x.djvu' => 'DjVuHandler', // compat
-       'image/x-djvu' => 'DjVuHandler', // compat
+       'image/x.djvu'   => 'DjVuHandler', // compat
+       'image/x-djvu'   => 'DjVuHandler', // compat
 );
 
 /**
@@ -719,17 +755,18 @@ $wgImageMagickTempDir = false;
  * %s will be replaced with the source path, %d with the destination
  * %w and %h will be replaced with the width and height.
  *
- * Example for GraphicMagick:
- * <code>
+ * @par Example for GraphicMagick:
+ * @code
  * $wgCustomConvertCommand = "gm convert %s -resize %wx%h %d"
- * </code>
+ * @endcode
  *
  * Leave as false to skip this.
  */
 $wgCustomConvertCommand = false;
 
 /**
- * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some image formats.
+ * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some
+ * image formats.
  */
 $wgExiv2Command = '/usr/bin/exiv2';
 
@@ -767,11 +804,15 @@ $wgSVGMaxSize = 2048;
 $wgSVGMetadataCutoff = 262144;
 
 /**
- * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't
- * perform basic stuff like MIME detection and which are vulnerable to further idiots uploading
- * crap files as images. When this directive is on, "<title>" will be allowed in files with
- * an "image/svg+xml" MIME type. You should leave this disabled if your web server is misconfigured
- * and doesn't send appropriate MIME types for SVG images.
+ * Disallow <title> element in SVG files.
+ *
+ * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic
+ * browsers which can not perform basic stuff like MIME detection and which are
+ * vulnerable to further idiots uploading crap files as images.
+ *
+ * When this directive is on, "<title>" will be allowed in files with an
+ * "image/svg+xml" MIME type. You should leave this disabled if your web server
+ * is misconfigured and doesn't send appropriate MIME types for SVG images.
  */
 $wgAllowTitlesInSVG = false;
 
@@ -801,13 +842,13 @@ $wgMaxAnimatedGifArea = 1.25e7;
  * For inline display, we need to convert to PNG or JPEG.
  * Note scaling should work with ImageMagick, but may not with GD scaling.
  *
- * Example:
- * <code>
+ * @par Example:
+ * @code
  *  // PNG is lossless, but inefficient for photos
  *  $wgTiffThumbnailType = array( 'png', 'image/png' );
  *  // JPEG is good for photos, but has no transparency support. Bad for diagrams.
  *  $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' );
- * </code>
+ * @endcode
  */
  $wgTiffThumbnailType = false;
 
@@ -941,10 +982,11 @@ $wgLoadFileinfoExtension = false;
  * the mime type to standard output.
  * The name of the file to process will be appended to the command given here.
  * If not set or NULL, mime_content_type will be used if available.
- * Example:
- * <code>
+ *
+ * @par Example:
+ * @code
  * #$wgMimeDetectorCommand = "file -bi"; # use external mime detector (Linux)
- * </code>
+ * @endcode
  */
 $wgMimeDetectorCommand = null;
 
@@ -1021,7 +1063,10 @@ $wgThumbUpright = 0.75;
 $wgDirectoryMode = 0777;
 
 /**
- * DJVU settings
+ * @name DJVU settings
+ * @{
+ */
+/**
  * Path of the djvudump executable
  * Enable this and $wgDjvuRenderer to enable djvu rendering
  */
@@ -1046,15 +1091,18 @@ $wgDjvuTxt = null;
  * Path of the djvutoxml executable
  * This works like djvudump except much, much slower as of version 3.5.
  *
- * For now I recommend you use djvudump instead. The djvuxml output is
+ * For now we  recommend you use djvudump instead. The djvuxml output is
  * probably more stable, so we'll switch back to it as soon as they fix
  * the efficiency problem.
  * http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583
+ *
+ * @par Example:
+ * @code
+ * $wgDjvuToXML = 'djvutoxml';
+ * @endcode
  */
-# $wgDjvuToXML = 'djvutoxml';
 $wgDjvuToXML = null;
 
-
 /**
  * Shell command for the DJVU post processor
  * Default: pnmtopng, since ddjvu generates ppm output
@@ -1065,6 +1113,7 @@ $wgDjvuPostProcessor = 'pnmtojpeg';
  * File extension for the DJVU post processor output
  */
 $wgDjvuOutputExtension = 'jpg';
+/** @} */ # end of DJvu }
 
 /** @} */ # end of file uploads }
 
@@ -1141,17 +1190,21 @@ $wgNewPasswordExpiry = 3600 * 24 * 7;
 $wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60;
 
 /**
- * SMTP Mode
+ * SMTP Mode.
+ *
  * For using a direct (authenticated) SMTP server connection.
  * Default to false or fill an array :
- * <code>
- * "host" => 'SMTP domain',
- * "IDHost" => 'domain for MessageID',
- * "port" => "25",
- * "auth" => true/false,
- * "username" => user,
- * "password" => password
- * </code>
+ *
+ * @code
+ * $wgSMTP = array(
+ *     'host'     => 'SMTP domain',
+ *     'IDHost'   => 'domain for MessageID',
+ *     'port'     => '25',
+ *     'auth'     => [true|false],
+ *     'username' => [SMTP username],
+ *     'password' => [SMTP password],
+ * );
+ * @endcode
  */
 $wgSMTP = false;
 
@@ -1173,9 +1226,9 @@ $wgEnotifFromEditor = false;
 # It call this to be a "user-preferences-option (UPO)"
 
 /**
- * Require email authentication before sending mail to an email addres. This is
- * highly recommended. It prevents MediaWiki from being used as an open spam
- * relay.
+ * Require email authentication before sending mail to an email address.
+ * This is highly recommended. It prevents MediaWiki from being used as an open
+ * spam relay.
  */
 $wgEmailAuthentication = true;
 
@@ -1354,9 +1407,9 @@ $wgSharedTables = array( 'user', 'user_properties' );
  * accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your
  * slaves in my.cnf. You can set read_only mode at runtime using:
  *
- * <code>
+ * @code
  *     SET @@read_only=1;
- * </code>
+ * @endcode
  *
  * Since the effect of writing to a slave is so damaging and difficult to clean
  * up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even
@@ -1381,13 +1434,19 @@ $wgMasterWaitTimeout = 10;
 
 /** File to log database errors to */
 $wgDBerrorLog = false;
+/**
+ * Override wiki timezone to UTC for wgDBerrorLog
+ * @since 1.20
+ */
+$wgDBerrorLogInUTC = false;
 
 /** When to give an error message */
 $wgDBClusterTimeout = 10;
 
 /**
- * Scale load balancer polling time so that under overload conditions, the database server
- * receives a SHOW STATUS query at an average interval of this many microseconds
+ * Scale load balancer polling time so that under overload conditions, the
+ * database server receives a SHOW STATUS query at an average interval of this
+ * many microseconds
  */
 $wgDBAvgStatusPoll = 2000;
 
@@ -1395,7 +1454,7 @@ $wgDBAvgStatusPoll = 2000;
  * Set to true to engage MySQL 4.1/5.0 charset-related features;
  * for now will just cause sending of 'SET NAMES=utf8' on connect.
  *
- * WARNING: THIS IS EXPERIMENTAL!
+ * @warning THIS IS EXPERIMENTAL!
  *
  * May break if you're not using the table defs from mysql5/tables.sql.
  * May break if you're upgrading an existing wiki if set differently.
@@ -1448,19 +1507,30 @@ $wgCompressRevisions = false;
 
 /**
  * External stores allow including content
- * from non database sources following URL links
+ * from non database sources following URL links.
  *
  * Short names of ExternalStore classes may be specified in an array here:
+ * @code
  * $wgExternalStores = array("http","file","custom")...
+ * @endcode
  *
  * CAUTION: Access to database might lead to code execution
  */
 $wgExternalStores = false;
 
 /**
- * An array of external mysql servers, e.g.
- * $wgExternalServers = array( 'cluster1' => array( 'srv28', 'srv29', 'srv30' ) );
- * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to another class.
+ * An array of external MySQL servers.
+ *
+ * @par Example:
+ * Create a cluster named 'cluster1' containing three servers:
+ * @code
+ * $wgExternalServers = array(
+ *     'cluster1' => array( 'srv28', 'srv29', 'srv30' )
+ * );
+ * @endcode
+ *
+ * Used by LBFactory_Simple, may be ignored if $wgLBFactoryConf is set to
+ * another class.
  */
 $wgExternalServers = array();
 
@@ -1469,9 +1539,12 @@ $wgExternalServers = array();
  * Part of a URL, e.g. DB://cluster1
  *
  * Can be an array instead of a single string, to enable data distribution. Keys
- * must be consecutive integers, starting at zero. Example:
+ * must be consecutive integers, starting at zero.
  *
+ * @par Example:
+ * @code
  * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' );
+ * @endcode
  *
  * @var array
  */
@@ -1634,8 +1707,8 @@ $wgObjectCaches = array(
 );
 
 /**
- * The expiry time for the parser cache, in seconds. The default is 86.4k
- * seconds, otherwise known as a day.
+ * The expiry time for the parser cache, in seconds.
+ * The default is 86400 (one day).
  */
 $wgParserCacheExpireTime = 86400;
 
@@ -1684,9 +1757,9 @@ $wgMemCachedTimeout = 100000;
 $wgUseLocalMessageCache = false;
 
 /**
- * Defines format of local cache
- * true - Serialized object
- * false - PHP source file (Warning - security risk)
+ * Defines format of local cache.
+ *  - true: Serialized object
+ *  - false: PHP source file (Warning - security risk)
  */
 $wgLocalMessageCacheSerialized = true;
 
@@ -1699,23 +1772,23 @@ $wgAdaptiveMessageCache = false;
 
 /**
  * Localisation cache configuration. Associative array with keys:
- *     class:       The class to use. May be overridden by extensions.
+ * class:       The class to use. May be overridden by extensions.
  *
- *     store:       The location to store cache data. May be 'files', 'db' or
- *                  'detect'. If set to "files", data will be in CDB files. If set
- *                  to "db", data will be stored to the database. If set to
- *                  "detect", files will be used if $wgCacheDirectory is set,
- *                  otherwise the database will be used.
+ * store:       The location to store cache data. May be 'files', 'db' or
+ *              'detect'. If set to "files", data will be in CDB files. If set
+ *              to "db", data will be stored to the database. If set to
+ *              "detect", files will be used if $wgCacheDirectory is set,
+ *              otherwise the database will be used.
  *
- *     storeClass:  The class name for the underlying storage. If set to a class
- *                  name, it overrides the "store" setting.
+ * storeClass:  The class name for the underlying storage. If set to a class
+ *              name, it overrides the "store" setting.
  *
- *     storeDirectory:  If the store class puts its data in files, this is the
- *                      directory it will use. If this is false, $wgCacheDirectory
- *                      will be used.
+ * storeDirectory:  If the store class puts its data in files, this is the
+ *                  directory it will use. If this is false, $wgCacheDirectory
+ *                  will be used.
  *
- *     manualRecache:   Set this to true to disable cache updates on web requests.
- *                      Use maintenance/rebuildLocalisationCache.php instead.
+ * manualRecache:   Set this to true to disable cache updates on web requests.
+ *                  Use maintenance/rebuildLocalisationCache.php instead.
  */
 $wgLocalisationCacheConf = array(
        'class' => 'LocalisationCache',
@@ -1730,14 +1803,17 @@ $wgCachePages = true;
 
 /**
  * Set this to current time to invalidate all prior cached pages. Affects both
- * client- and server-side caching.
+ * client-side and server-side caching.
  * You can get the current date on your server by using the command:
+ * @verbatim
  *   date +%Y%m%d%H%M%S
+ * @endverbatim
  */
 $wgCacheEpoch = '20030516000000';
 
 /**
  * Bump this number when changing the global style sheets and JavaScript.
+ *
  * It should be appended in the query string of static CSS and JS includes,
  * to ensure that client-side caches do not keep obsolete copies of global
  * styles.
@@ -1863,10 +1939,12 @@ $wgUseXVO = false;
 $wgVaryOnXFP = false;
 
 /**
- * Internal server name as known to Squid, if different. Example:
- * <code>
+ * Internal server name as known to Squid, if different.
+ *
+ * @par Example:
+ * @code
  * $wgInternalServer = 'http://yourinternal.tld:8000';
- * </code>
+ * @endcode
  */
 $wgInternalServer = false;
 
@@ -1914,6 +1992,7 @@ $wgMaxSquidPurgeTitles = 400;
  *
  * Example configuration to send purges for upload.wikimedia.org to one
  * multicast group and all other purges to another:
+ * @code
  * $wgHTCPMulticastRouting = array(
  *         '|^https?://upload\.wikimedia\.org|' => array(
  *                 'host' => '239.128.0.113',
@@ -1924,6 +2003,7 @@ $wgMaxSquidPurgeTitles = 400;
  *                 'port' => 4827,
  *         ),
  * );
+ * @endcode
  *
  * @see $wgHTCPMulticastTTL
  */
@@ -1972,11 +2052,12 @@ $wgLanguageCode = 'en';
 
 /**
  * Some languages need different word forms, usually for different cases.
- * Used in Language::convertGrammar(). Example:
+ * Used in Language::convertGrammar().
  *
- * <code>
+ * @par Example:
+ * @code
  * $wgGrammarForms['en']['genitive']['car'] = 'car\'s';
- * </code>
+ * @endcode
  */
 $wgGrammarForms = array();
 
@@ -2059,7 +2140,7 @@ $wgAllUnicodeFixes = false;
  * converting a wiki from MediaWiki 1.4 or earlier to UTF-8 without the
  * burdensome mass conversion of old text data.
  *
- * NOTE! This DOES NOT touch any fields other than old_text.Titles, comments,
+ * @note This DOES NOT touch any fields other than old_text. Titles, comments,
  * user names, etc still must be converted en masse in the database before
  * continuing as a UTF-8 wiki.
  */
@@ -2168,28 +2249,27 @@ $wgCanonicalLanguageLinks = true;
 $wgDefaultLanguageVariant = false;
 
 /**
- * Disabled variants array of language variant conversion. Example:
- * <code>
+ * Disabled variants array of language variant conversion.
+ *
+ * @par Example:
+ * @code
  *  $wgDisabledVariants[] = 'zh-mo';
  *  $wgDisabledVariants[] = 'zh-my';
- * </code>
- *
- * or:
- *
- * <code>
- *  $wgDisabledVariants = array('zh-mo', 'zh-my');
- * </code>
+ * @endcode
  */
 $wgDisabledVariants = array();
 
 /**
  * Like $wgArticlePath, but on multi-variant wikis, this provides a
  * path format that describes which parts of the URL contain the
- * language variant.  For Example:
+ * language variant.
  *
- *   $wgLanguageCode = 'sr';
- *   $wgVariantArticlePath = '/$2/$1';
- *   $wgArticlePath = '/wiki/$1';
+ * @par Example:
+ * @code
+ *     $wgLanguageCode = 'sr';
+ *     $wgVariantArticlePath = '/$2/$1';
+ *     $wgArticlePath = '/wiki/$1';
+ * @endcode
  *
  * A link to /wiki/ would be redirected to /sr/Главна_страна
  *
@@ -2215,10 +2295,14 @@ $wgLoginLanguageSelector = false;
  * wfMsg(). The code behaves this way by default. However, sites like the
  * Wikimedia Commons do offer different versions of 'mainpage' and the like for
  * different languages. This array provides a way to override the default
- * behavior. For example, to allow language-specific main page and community
- * portal, set
+ * behavior.
  *
- * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
+ * @par Example:
+ * To allow language-specific main page and community
+ * portal:
+ * @code
+ *     $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' );
+ * @endcode
  */
 $wgForceUIMsgAsContentMsg = array();
 
@@ -2233,13 +2317,13 @@ $wgForceUIMsgAsContentMsg = array();
  * Timezones can be translated by editing MediaWiki messages of type
  * timezone-nameinlowercase like timezone-utc.
  *
- * Examples:
- * <code>
+ * @par Examples:
+ * @code
  * $wgLocaltimezone = 'GMT';
  * $wgLocaltimezone = 'PST8PDT';
  * $wgLocaltimezone = 'Europe/Sweden';
  * $wgLocaltimezone = 'CET';
- * </code>
+ * @endcode
  */
 $wgLocaltimezone = null;
 
@@ -2260,7 +2344,7 @@ $wgLocalTZoffset = null;
  * language variant conversion is disabled in interface messages. Setting this
  * to true re-enables it.
  *
- * This variable should be removed (implicitly false) in 1.20 or earlier.
+ * @todo This variable should be removed (implicitly false) in 1.20 or earlier.
  */
 $wgBug34832TransitionalRollback = true;
 
@@ -2357,9 +2441,13 @@ $wgWellFormedXml = true;
 
 /**
  * Permit other namespaces in addition to the w3.org default.
- * Use the prefix for the key and the namespace for the value. For
- * example:
+ *
+ * Use the prefix for the key and the namespace for the value.
+ *
+ * @par Example:
+ * @code
  * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg';
+ * @endCode
  * Normally we wouldn't have to define this in the root "<html>"
  * element, but IE needs it there in some circumstances.
  *
@@ -2371,7 +2459,7 @@ $wgXhtmlNamespaces = array();
 /**
  * Show IP address, for non-logged in users. It's necessary to switch this off
  * for some forms of caching.
- * Will disable file cache.
+ * @warning Will disable file cache.
  */
 $wgShowIPinHeader = true;
 
@@ -2530,15 +2618,16 @@ $wgExperimentalHtmlIds = false;
  * The value should be either a string or an array. If it is a string it will be output
  * directly as html, however some skins may choose to ignore it. An array is the preferred format
  * for the icon, the following keys are used:
- *   src: An absolute url to the image to use for the icon, this is recommended
+ *  - src: An absolute url to the image to use for the icon, this is recommended
  *        but not required, however some skins will ignore icons without an image
- *   url: The url to use in the <a> arround the text or icon, if not set an <a> will not be outputted
- *   alt: This is the text form of the icon, it will be displayed without an image in
+ * - url: The url to use in the a element arround the text or icon, if not set an a element will not be outputted
+ * - alt: This is the text form of the icon, it will be displayed without an image in
  *        skins like Modern or if src is not set, and will otherwise be used as
  *        the alt="" for the image. This key is required.
- *   width and height: If the icon specified by src is not of the standard size
+ * - width and height: If the icon specified by src is not of the standard size
  *                     you can specify the size of image to use with these keys.
  *                     Otherwise they will default to the standard 88x31.
+ * @todo Reformat documentation.
  */
 $wgFooterIcons = array(
        "copyright" => array(
@@ -2554,23 +2643,24 @@ $wgFooterIcons = array(
 );
 
 /**
- * Login / create account link behavior when it's possible for anonymous users to create an account
- * true = use a combined login / create account link
- * false = split login and create account into two separate links
+ * Login / create account link behavior when it's possible for anonymous users
+ * to create an account.
+ *  - true = use a combined login / create account link
+ *  - false = split login and create account into two separate links
  */
 $wgUseCombinedLoginLink = true;
 
 /**
- * Search form behavior for Vector skin only
- * true = use an icon search button
- * false = use Go & Search buttons
+ * Search form behavior for Vector skin only.
+ *  - true = use an icon search button
+ *  - false = use Go & Search buttons
  */
 $wgVectorUseSimpleSearch = false;
 
 /**
- * Watch and unwatch as an icon rather than a link for Vector skin only
- * true = use an icon watch/unwatch button
- * false = use watch/unwatch text link
+ * Watch and unwatch as an icon rather than a link for Vector skin only.
+ *  - true = use an icon watch/unwatch button
+ *  - false = use watch/unwatch text link
  */
 $wgVectorUseIconWatch = false;
 
@@ -2608,10 +2698,13 @@ $wgSend404Code = true;
  */
 
 /**
- * Client-side resource modules. Extensions should add their module definitions
- * here.
+ * Client-side resource modules.
  *
- * Example:
+ * Extensions should add their resource loader module definitions
+ * to the $wgResourceModules variable.
+ *
+ * @par Example:
+ * @code
  *   $wgResourceModules['ext.myExtension'] = array(
  *      'scripts' => 'myExtension.js',
  *      'styles' => 'myExtension.css',
@@ -2619,6 +2712,7 @@ $wgSend404Code = true;
  *      'localBasePath' => dirname( __FILE__ ),
  *      'remoteExtPath' => 'MyExtension',
  *   );
+ * @endcode
  */
 $wgResourceModules = array();
 
@@ -2627,11 +2721,13 @@ $wgResourceModules = array();
  * built-in source that is not in this array, but defined by
  * ResourceLoader::__construct() so that it cannot be unset.
  *
- * Example:
+ * @par Example:
+ * @code
  *   $wgResourceLoaderSources['foo'] = array(
  *       'loadScript' => 'http://example.org/w/load.php',
  *       'apiScript' => 'http://example.org/w/api.php'
  *   );
+ * @endcode
  */
 $wgResourceLoaderSources = array();
 
@@ -2642,7 +2738,9 @@ $wgResourceLoaderSources = array();
 $wgResourceBasePath = null;
 
 /**
- * Maximum time in seconds to cache resources served by the resource loader
+ * Maximum time in seconds to cache resources served by the resource loader.
+ *
+ * @todo Document array structure
  */
 $wgResourceLoaderMaxage = array(
        'versioned' => array(
@@ -2658,8 +2756,9 @@ $wgResourceLoaderMaxage = array(
 );
 
 /**
- * The default debug mode (on/off) for of ResourceLoader requests. This will still
- * be overridden when the debug URL parameter is used.
+ * The default debug mode (on/off) for of ResourceLoader requests.
+ *
+ * This will still be overridden when the debug URL parameter is used.
  */
 $wgResourceLoaderDebug = false;
 
@@ -2685,33 +2784,54 @@ $wgResourceLoaderMinifierMaxLineLength = 1000;
 
 /**
  * Whether to include the mediawiki.legacy JS library (old wikibits.js), and its
- * dependencies
+ * dependencies.
  */
 $wgIncludeLegacyJavaScript = true;
 
 /**
- * Whether to preload the mediawiki.util module as blocking module in the top queue.
- * Before MediaWiki 1.19, modules used to load slower/less asynchronous which allowed
- * modules to lack dependencies on 'popular' modules that were likely loaded already.
+ * Whether to preload the mediawiki.util module as blocking module in the top
+ * queue.
+ *
+ * Before MediaWiki 1.19, modules used to load slower/less asynchronous which
+ * allowed modules to lack dependencies on 'popular' modules that were likely
+ * loaded already.
+ *
  * This setting is to aid scripts during migration by providing mediawiki.util
  * unconditionally (which was the most commonly missed dependency).
- * It doesn't cover all missing dependencies obviously but should fix most of them.
+ * It doesn't cover all missing dependencies obviously but should fix most of
+ * them.
+ *
  * This should be removed at some point after site/user scripts have been fixed.
- * Enable this if your wiki has a large amount of user/site scripts that are lacking
- * dependencies.
+ * Enable this if your wiki has a large amount of user/site scripts that are
+ * lacking dependencies.
+ * @todo Deprecate
  */
 $wgPreloadJavaScriptMwUtil = false;
 
 /**
- * Whether or not to assing configuration variables to the global window object.
- * If this is set to false, old code using deprecated variables like:
- * " if ( window.wgRestrictionEdit ) ..."
+ * Whether or not to assign configuration variables to the global window object.
+ *
+ * If this is set to false, old code using deprecated variables will no longer
+ * work.
+ *
+ * @par Example of legacy code:
+ * @code{,js}
+ *     if ( window.wgRestrictionEdit ) { ... }
+ * @endcode
  * or:
- * " if ( wgIsArticle ) ..."
- * will no longer work and needs to use mw.config instead. For example:
- * " if ( mw.config.exists('wgRestrictionEdit') )"
- * or
- * " if ( mw.config.get('wgIsArticle') )".
+ * @code{,js}
+ *     if ( wgIsArticle ) { ... }
+ * @endcode
+ *
+ * Instead, one needs to use mw.config.
+ * @par Example using mw.config global configuration:
+ * @code{,js}
+ *     if ( mw.config.exists('wgRestrictionEdit') ) { ... }
+ * @endcode
+ * or:
+ * @code{,js}
+ *     if ( mw.config.get('wgIsArticle') ) { ... }
+ * @endcode
  */
 $wgLegacyJavaScriptGlobals = true;
 
@@ -2729,8 +2849,8 @@ $wgLegacyJavaScriptGlobals = true;
 $wgResourceLoaderMaxQueryLength = -1;
 
 /**
- * If set to true, JavaScript modules loaded from wiki pages will be parsed prior
- * to minification to validate it.
+ * If set to true, JavaScript modules loaded from wiki pages will be parsed
+ * prior to minification to validate it.
  *
  * Parse errors will result in a JS exception being thrown during module load,
  * which avoids breaking other modules loaded in the same request.
@@ -2784,19 +2904,25 @@ $wgMetaNamespaceTalk = false;
  * names of existing namespaces. Extensions developers should use
  * $wgCanonicalNamespaceNames.
  *
- * PLEASE  NOTE: Once you delete a namespace, the pages in that namespace will
+ * @warning Once you delete a namespace, the pages in that namespace will
  * no longer be accessible. If you rename it, then you can access them through
  * the new namespace name.
  *
  * Custom namespaces should start at 100 to avoid conflicting with standard
  * namespaces, and should always follow the even/odd main/talk pattern.
+ *
+ * @par Example:
+ * @code
+ * $wgExtraNamespaces = array(
+ *    100 => "Hilfe",
+ *    101 => "Hilfe_Diskussion",
+ *    102 => "Aide",
+ *    103 => "Discussion_Aide"
+ * );
+ * @endcode
+ *
+ * @todo Add a note about maintenance/namespaceDupes.php
  */
-# $wgExtraNamespaces = array(
-#     100 => "Hilfe",
-#     101 => "Hilfe_Diskussion",
-#     102 => "Aide",
-#     103 => "Discussion_Aide"
-# );
 $wgExtraNamespaces = array();
 
 /**
@@ -2808,18 +2934,22 @@ $wgExtraNamespaces = array();
 $wgExtraGenderNamespaces = array();
 
 /**
- * Namespace aliases
+ * Namespace aliases.
+ *
  * These are alternate names for the primary localised namespace names, which
  * are defined by $wgExtraNamespaces and the language file. If a page is
  * requested with such a prefix, the request will be redirected to the primary
  * name.
  *
  * Set this to a map from namespace names to IDs.
- * Example:
+ *
+ * @par Example:
+ * @code
  *    $wgNamespaceAliases = array(
  *        'Wikipedian' => NS_USER,
  *        'Help' => 100,
  *    );
+ * @endcode
  */
 $wgNamespaceAliases = array();
 
@@ -2834,8 +2964,8 @@ $wgNamespaceAliases = array();
  *   -  +         Enabled by default, but doesn't work with path to query rewrite rules, corrupted by apache
  *   -  ?         Enabled by default, but doesn't work with path to PATH_INFO rewrites
  *
- * All three of these punctuation problems can be avoided by using an alias, instead of a
- * rewrite rule of either variety.
+ * All three of these punctuation problems can be avoided by using an alias,
+ * instead of a rewrite rule of either variety.
  *
  * The problem with % is that when using a path to query rewrite rule, URLs are
  * double-unescaped: once by Apache's path conversion code, and again by PHP. So
@@ -2861,33 +2991,47 @@ $wgLocalInterwiki = false;
  */
 $wgInterwikiExpiry = 10800;
 
-/** Interwiki caching settings.
-       $wgInterwikiCache specifies path to constant database file
-               This cdb database is generated by dumpInterwiki from maintenance
-               and has such key formats:
-                       dbname:key - a simple key (e.g. enwiki:meta)
-                       _sitename:key - site-scope key (e.g. wiktionary:meta)
-                       __global:key - global-scope key (e.g. __global:meta)
-                       __sites:dbname - site mapping (e.g. __sites:enwiki)
-               Sites mapping just specifies site name, other keys provide
-                       "local url" data layout.
-       $wgInterwikiScopes specify number of domains to check for messages:
-               1 - Just wiki(db)-level
-               2 - wiki and global levels
-               3 - site levels
-       $wgInterwikiFallbackSite - if unable to resolve from cache
+/**
+ * @name Interwiki caching settings.
+ * @{
+ */
+/**
+ *$wgInterwikiCache specifies path to constant database file.
+ *
+ * This cdb database is generated by dumpInterwiki from maintenance and has
+ * such key formats:
+ *  - dbname:key - a simple key (e.g. enwiki:meta)
+ *  - _sitename:key - site-scope key (e.g. wiktionary:meta)
+ *  - __global:key - global-scope key (e.g. __global:meta)
+ *  - __sites:dbname - site mapping (e.g. __sites:enwiki)
+ *
+ * Sites mapping just specifies site name, other keys provide "local url"
+ * data layout.
  */
 $wgInterwikiCache = false;
+/**
+ * Specify number of domains to check for messages.
+ *     - 1: Just wiki(db)-level
+ *     - 2: wiki and global levels
+ *     - 3: site levels
+ */
 $wgInterwikiScopes = 3;
+/**
+ *     $wgInterwikiFallbackSite - if unable to resolve from cache
+ */
 $wgInterwikiFallbackSite = 'wiki';
+/** @} */ # end of Interwiki caching settings.
 
 /**
  * If local interwikis are set up which allow redirects,
  * set this regexp to restrict URLs which will be displayed
  * as 'redirected from' links.
  *
+ * @par Example:
  * It might look something like this:
+ * @code
  * $wgRedirectSources = '!^https?://[a-z-]+\.wikipedia\.org/!';
+ * @endcode
  *
  * Leave at false to avoid displaying any incoming redirect markers.
  * This does not affect intra-wiki redirects, which don't change
@@ -2897,7 +3041,8 @@ $wgRedirectSources = false;
 
 /**
  * Set this to false to avoid forcing the first letter of links to capitals.
- * WARNING: may break links! This makes links COMPLETELY case-sensitive. Links
+ *
+ * @warning may break links! This makes links COMPLETELY case-sensitive. Links
  * appearing with a capital at the beginning of a sentence will *not* go to the
  * same place as links in the middle of a sentence using a lowercase initial.
  */
@@ -2911,7 +3056,11 @@ $wgCapitalLinks = true;
  * associated content namespaces, the values for those are ignored in favor of the
  * subject namespace's setting. Setting for NS_MEDIA is taken automatically from
  * NS_FILE.
- * EX: $wgCapitalLinkOverrides[ NS_FILE ] = false;
+ *
+ * @par Example:
+ * @code
+ *     $wgCapitalLinkOverrides[ NS_FILE ] = false;
+ * @endcode
  */
 $wgCapitalLinkOverrides = array();
 
@@ -3044,11 +3193,11 @@ $wgAllowExternalImages = false;
  * You can use this to set up a trusted, simple repository of images.
  * You may also specify an array of strings to allow multiple sites
  *
- * Examples:
- * <code>
+ * @par Examples:
+ * @code
  * $wgAllowExternalImagesFrom = 'http://127.0.0.1/';
  * $wgAllowExternalImagesFrom = array( 'http://127.0.0.1/', 'http://example.com' );
- * </code>
+ * @endcode
  */
 $wgAllowExternalImagesFrom = '';
 
@@ -3495,18 +3644,19 @@ $wgBlockCIDRLimit = array(
 $wgBlockDisablesLogin = false;
 
 /**
- * Pages anonymous user may see as an array, e.g.
+ * Pages anonymous user may see, set as an array of pages titles.
  *
- * <code>
+ * @par Example:
+ * @code
  * $wgWhitelistRead = array ( "Main Page", "Wikipedia:Help");
- * </code>
+ * @endcode
  *
  * Special:Userlogin and Special:ChangePassword are always whitelisted.
  *
- * NOTE: This will only work if $wgGroupPermissions['*']['read'] is false --
+ * @note This will only work if $wgGroupPermissions['*']['read'] is false --
  * see below. Otherwise, ALL pages are accessible, regardless of this setting.
  *
- * Also note that this will only protect _pages in the wiki_. Uploaded files
+ * @note Also that this will only protect _pages in the wiki_. Uploaded files
  * will remain readable. You can use img_auth.php to protect uploaded files,
  * see http://www.mediawiki.org/wiki/Manual:Image_Authorization
  */
@@ -3520,6 +3670,7 @@ $wgEmailConfirmToEdit = false;
 
 /**
  * Permission keys given to users in each group.
+ *
  * This is an array where the keys are all groups and each value is an
  * array of the format (right => boolean).
  *
@@ -3650,6 +3801,7 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true;
 
 /**
  * Permission keys revoked from users in each group.
+ *
  * This acts the same way as wgGroupPermissions above, except that
  * if the user is in a group here, the permission will be removed from them.
  *
@@ -3667,16 +3819,20 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' );
  * A map of group names that the user is in, to group names that those users
  * are allowed to add or revoke.
  *
- * Setting the list of groups to add or revoke to true is equivalent to "any group".
- *
- * For example, to allow sysops to add themselves to the "bot" group:
+ * Setting the list of groups to add or revoke to true is equivalent to "any
+ * group".
  *
+ * @par Example:
+ * To allow sysops to add themselves to the "bot" group:
+ * @code
  *    $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) );
+ * @endcode
  *
+ * @par Example:
  * Implicit groups may be used for the source group, for instance:
- *
+ * @code
  *    $wgGroupsRemoveFromSelf = array( '*' => true );
- *
+ * @endcode
  * This allows users in the '*' group (i.e. any user) to remove themselves from
  * any group that they happen to be in.
  *
@@ -3712,13 +3868,16 @@ $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' );
  * namespace.  If you list more than one permission, a user must
  * have all of them to edit pages in that namespace.
  *
- * Note: NS_MEDIAWIKI is implicitly restricted to editinterface.
+ * @note NS_MEDIAWIKI is implicitly restricted to 'editinterface'.
  */
 $wgNamespaceProtection = array();
 
 /**
  * Pages in namespaces in this array can not be used as templates.
- * Elements must be numeric namespace ids.
+ *
+ * Elements MUST be numeric namespace ids, you can safely use the MediaWiki
+ * namespaces constants (NS_USER, NS_MAIN...).
+ *
  * Among other things, this may be useful to enforce read-restrictions
  * which may otherwise be bypassed by using the template machanism.
  */
@@ -3734,11 +3893,15 @@ $wgNonincludableNamespaces = array();
  *
  * When left at 0, all registered accounts will pass.
  *
- * Example:
- * <code>
+ * @par Example:
+ * Set automatic confirmation to 10 minutes (which is 600 seconds):
+ * @code
  *  $wgAutoConfirmAge = 600;     // ten minutes
+ * @endcode
+ * Set age to one day:
+ * @code
  *  $wgAutoConfirmAge = 3600*24; // one day
- * </code>
+ * @endcode
  */
 $wgAutoConfirmAge = 0;
 
@@ -3746,14 +3909,18 @@ $wgAutoConfirmAge = 0;
  * Number of edits an account requires before it is autoconfirmed.
  * Passing both this AND the time requirement is needed. Example:
  *
- * <code>
+ * @par Example:
+ * @code
  * $wgAutoConfirmCount = 50;
- * </code>
+ * @endcode
  */
 $wgAutoConfirmCount = 0;
 
 /**
  * Automatically add a usergroup to any user who matches certain conditions.
+ *
+ * @todo Redocument $wgAutopromote
+ *
  * The format is
  *   array( '&' or '|' or '^' or '!', cond1, cond2, ... )
  * where cond1, cond2, ... are themselves conditions; *OR*
@@ -3781,14 +3948,19 @@ $wgAutopromote = array(
 
 /**
  * Automatically add a usergroup to any user who matches certain conditions.
+ *
  * Does not add the user to the group again if it has been removed.
  * Also, does not remove the group if the user no longer meets the criteria.
  *
- * The format is
+ * The format is:
+ * @code
  *     array( event => criteria, ... )
- * where event is
- *     'onEdit' (when user edits) or 'onView' (when user views the wiki)
- * and criteria has the same format as $wgAutopromote
+ * @endcode
+ * Where event is either:
+ *     - 'onEdit' (when user edits)
+ *     - 'onView' (when user views the wiki)
+ *
+ * Criteria has the same format as $wgAutopromote
  *
  * @see $wgAutopromote
  * @since 1.18
@@ -3806,16 +3978,23 @@ $wgAutopromoteOnceLogInRC = true;
 
 /**
  * $wgAddGroups and $wgRemoveGroups can be used to give finer control over who
- * can assign which groups at Special:Userrights.  Example configuration:
+ * can assign which groups at Special:Userrights.
  *
+ * @par Example:
+ * Bureaucrats can add any group:
  * @code
- * // Bureaucrat can add any group
  * $wgAddGroups['bureaucrat'] = true;
- * // Bureaucrats can only remove bots and sysops
+ * @endcode
+ * Bureaucrats can only remove bots and sysops:
+ * @code
  * $wgRemoveGroups['bureaucrat'] = array( 'bot', 'sysop' );
- * // Sysops can make bots
+ * @endcode
+ * Sysops can make bots:
+ * @code
  * $wgAddGroups['sysop'] = array( 'bot' );
- * // Sysops can disable other sysops in an emergency, and disable bots
+ * @endcode
+ * Sysops can disable other sysops in an emergency, and disable bots:
+ * @code
  * $wgRemoveGroups['sysop'] = array( 'sysop', 'bot' );
  * @endcode
  */
@@ -3835,8 +4014,10 @@ $wgAvailableRights = array();
  */
 $wgDeleteRevisionsLimit = 0;
 
-/** Number of accounts each IP address may create, 0 to disable.
- * Requires memcached */
+/**
+ * Number of accounts each IP address may create, 0 to disable.
+ *
+ * @warning Requires memcached */
 $wgAccountCreationThrottle = 0;
 
 /**
@@ -3846,8 +4027,9 @@ $wgAccountCreationThrottle = 0;
  * There's no administrator override on-wiki, so be careful what you set. :)
  * May be an array of regexes or a single string for backwards compatibility.
  *
- * See http://en.wikipedia.org/wiki/Regular_expression
- * Note that each regex needs a beginning/end delimiter, eg: # or /
+ * @see http://en.wikipedia.org/wiki/Regular_expression
+ *
+ * @note Each regex needs a beginning/end delimiter, eg: # or /
  */
 $wgSpamRegex = array();
 
@@ -3855,39 +4037,46 @@ $wgSpamRegex = array();
 $wgSummarySpamRegex = array();
 
 /**
- * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies
+ * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open
+ * proxies
  * @since 1.16
  */
 $wgEnableDnsBlacklist = false;
 
 /**
- * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for backward
- *  compatibility
+ * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for
+ * backward compatibility.
  */
 $wgEnableSorbs = false;
 
 /**
- * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true. This is an
- * array of either a URL or an array with the URL and a key (should the blacklist
- * require a key). For example:
+ * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true.
+ *
+ * This is an array of either a URL or an array with the URL and a key (should
+ * the blacklist require a key).
+ *
+ * @par Example:
  * @code
  * $wgDnsBlacklistUrls = array(
  *   // String containing URL
- *   'http.dnsbl.sorbs.net',
+ *   'http.dnsbl.sorbs.net.',
  *   // Array with URL and key, for services that require a key
- *   array( 'dnsbl.httpbl.net', 'mykey' ),
+ *   array( 'dnsbl.httpbl.net.', 'mykey' ),
  *   // Array with just the URL. While this works, it is recommended that you
  *   // just use a string as shown above
- *   array( 'opm.tornevall.org' )
+ *   array( 'opm.tornevall.org.' )
  * );
  * @endcode
+ *
+ * @note You should end the domain name with a . to avoid searching your
+ * eventual domain search suffixes.
  * @since 1.16
  */
 $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' );
 
 /**
- * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for backward
- *  compatibility
+ * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for
+ * backward compatibility.
  */
 $wgSorbsUrl = array();
 
@@ -3898,13 +4087,24 @@ $wgSorbsUrl = array();
 $wgProxyWhitelist = array();
 
 /**
- * Simple rate limiter options to brake edit floods.  Maximum number actions
- * allowed in the given number of seconds; after that the violating client re-
- * ceives HTTP 500 error pages until the period elapses.
+ * Simple rate limiter options to brake edit floods.
+ *
+ * Maximum number actions allowed in the given number of seconds; after that
+ * the violating client receives HTTP 500 error pages until the period
+ * elapses.
+ *
+ * @par Example:
+ * To set a generic maximum of 4 hits in 60 seconds:
+ * @code
+ * $wgRateLimits = array( 4, 60 );
+ * @endcode
  *
- * array( 4, 60 ) for a maximum of 4 hits in 60 seconds.
+ * You could also limit per action and then type of users. See the inline
+ * code for a template to use.
  *
- * This option set is experimental and likely to change. Requires memcached.
+ * This option set is experimental and likely to change.
+ *
+ * @warning Requires memcached.
  */
 $wgRateLimits = array(
        'edit' => array(
@@ -3953,7 +4153,8 @@ $wgQueryPageDefaultLimit = 50;
 
 /**
  * Limit password attempts to X attempts per Y seconds per IP per account.
- * Requires memcached.
+ *
+ * @warning Requires memcached.
  */
 $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
 
@@ -3968,10 +4169,10 @@ $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 );
  * If you enable this, every editor's IP address will be scanned for open HTTP
  * proxies.
  *
- * Don't enable this. Many sysops will report "hostile TCP port scans" to your
- * ISP and ask for your server to be shut down.
- *
+ * @warning Don't enable this. Many sysops will report "hostile TCP port scans"
+ * to your ISP and ask for your server to be shut down.
  * You have been warned.
+ *
  */
 $wgBlockOpenProxies = false;
 /** Port we want to scan for a proxy */
@@ -4105,18 +4306,18 @@ $wgDebugRedirects = false;
 
 /**
  * If true, log debugging data from action=raw and load.php.
- * This is normally false to avoid overlapping debug entries due to gen=css and
- * gen=js requests.
+ * This is normally false to avoid overlapping debug entries due to gen=css
+ * and gen=js requests.
  */
 $wgDebugRawPage = false;
 
 /**
  * Send debug data to an HTML comment in the output.
  *
- * This may occasionally be useful when supporting a non-technical end-user. It's
- * more secure than exposing the debug log file to the web, since the output only
- * contains private data for the current user. But it's not ideal for development
- * use since data is lost on fatal errors and redirects.
+ * This may occasionally be useful when supporting a non-technical end-user.
+ * It's more secure than exposing the debug log file to the web, since the
+ * output only contains private data for the current user. But it's not ideal
+ * for development use since data is lost on fatal errors and redirects.
  */
 $wgDebugComments = false;
 
@@ -4432,12 +4633,13 @@ $wgMWSuggestTemplate = false;
 $wgDisableSearchUpdate = false;
 
 /**
- * List of namespaces which are searched by default. Example:
+ * List of namespaces which are searched by default.
  *
- * <code>
+ * @par Example:
+ * @code
  * $wgNamespacesToBeSearchedDefault[NS_MAIN] = true;
  * $wgNamespacesToBeSearchedDefault[NS_PROJECT] = true;
- * </code>
+ * @endcode
  */
 $wgNamespacesToBeSearchedDefault = array(
        NS_MAIN => true,
@@ -4445,9 +4647,9 @@ $wgNamespacesToBeSearchedDefault = array(
 
 /**
  * Namespaces to be searched when user clicks the "Help" tab
- * on Special:Search
+ * on Special:Search.
  *
- * Same format as $wgNamespacesToBeSearchedDefault
+ * Same format as $wgNamespacesToBeSearchedDefault.
  */
 $wgNamespacesToBeSearchedHelp = array(
        NS_PROJECT => true,
@@ -4455,8 +4657,10 @@ $wgNamespacesToBeSearchedHelp = array(
 );
 
 /**
- * If set to true the 'searcheverything' preference will be effective only for logged-in users.
- * Useful for big wikis to maintain different search profiles for anonymous and logged-in users.
+ * If set to true the 'searcheverything' preference will be effective only for
+ * logged-in users.
+ * Useful for big wikis to maintain different search profiles for anonymous and
+ * logged-in users.
  *
  */
 $wgSearchEverythingOnlyLoggedIn = false;
@@ -4472,18 +4676,22 @@ $wgDisableInternalSearch = false;
  * If the URL includes '$1', this will be replaced with the URL-encoded
  * search term.
  *
- * For example, to forward to Google you'd have something like:
- * $wgSearchForwardUrl = 'http://www.google.com/search?q=$1' .
- *                       '&domains=http://example.com' .
- *                       '&sitesearch=http://example.com' .
- *                       '&ie=utf-8&oe=utf-8';
+ * @par Example:
+ * To forward to Google you'd have something like:
+ * @code
+ * $wgSearchForwardUrl =
+ *     'http://www.google.com/search?q=$1' .
+ *     '&domains=http://example.com' .
+ *     '&sitesearch=http://example.com' .
+ *     '&ie=utf-8&oe=utf-8';
+ * @endcode
  */
 $wgSearchForwardUrl = null;
 
 /**
- * Search form behavior
- * true = use Go & Search buttons
- * false = use Go button & Advanced search link
+ * Search form behavior.
+ * true = use Go & Search buttons
+ * false = use Go button & Advanced search link
  */
 $wgUseTwoButtonsSearchForm = true;
 
@@ -4500,11 +4708,13 @@ $wgSitemapNamespaces = false;
  * maintenance/generateSitemap.php script.
  *
  * This should be a map of namespace IDs to priority
- * Example:
+ * @par Example:
+ * @code
  *  $wgSitemapNamespacesPriorities = array(
  *      NS_USER => '0.9',
  *      NS_HELP => '0.0',
  *  );
+ * @endcode
  */
 $wgSitemapNamespacesPriorities = false;
 
@@ -4733,18 +4943,23 @@ $wgFeedDiffCutoff = 32768;
 /** Override the site's default RSS/ATOM feed for recentchanges that appears on
  * every page. Some sites might have a different feed they'd like to promote
  * instead of the RC feed (maybe like a "Recent New Articles" or "Breaking news" one).
- * Ex: $wgSiteFeed['format'] = "http://example.com/somefeed.xml"; Format can be one
- * of either 'rss' or 'atom'.
+ * Should be a format as key (either 'rss' or 'atom') and an URL to the feed
+ * as value.
+ * @par Example:
+ * Configure the 'atom' feed to http://example.com/somefeed.xml
+ * @code
+ * $wgSiteFeed['atom'] = "http://example.com/somefeed.xml";
+ * @endcode
  */
 $wgOverrideSiteFeed = array();
 
 /**
- * Available feeds objects
+ * Available feeds objects.
  * Should probably only be defined when a page is syndicated ie when
- * $wgOut->isSyndicated() is true
+ * $wgOut->isSyndicated() is true.
  */
 $wgFeedClasses = array(
-       'rss' => 'RSSFeed',
+       'rss'  => 'RSSFeed',
        'atom' => 'AtomFeed',
 );
 
@@ -4903,9 +5118,9 @@ $wgExportAllowListContributors = false;
  * can become *insanely large* and could easily break your wiki,
  * it's disabled by default for now.
  *
- * There's a HARD CODED limit of 5 levels of recursion to prevent a
- * crazy-big export from being done by someone setting the depth
- * number too high. In other words, last resort safety net.
+ * @warning There's a HARD CODED limit of 5 levels of recursion to prevent a
+ * crazy-big export from being done by someone setting the depth number too
+ * high. In other words, last resort safety net.
  */
 $wgExportMaxLinkDepth = 0;
 
@@ -4927,7 +5142,8 @@ $wgExportAllowAll = false;
  */
 
 /**
- * A list of callback functions which are called once MediaWiki is fully initialised
+ * A list of callback functions which are called once MediaWiki is fully
+ * initialised
  */
 $wgExtensionFunctions = array();
 
@@ -4942,9 +5158,10 @@ $wgExtensionFunctions = array();
  * Variables defined in extensions will override conflicting variables defined
  * in the core.
  *
- * Example:
+ * @par Example:
+ * @code
  *    $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php';
- *
+ * @endcode
  */
 $wgExtensionMessagesFiles = array();
 
@@ -4958,7 +5175,9 @@ $wgExtensionMessagesFiles = array();
  * Registration is done with $pout->addOutputHook( $tag, $data ).
  *
  * The callback has the form:
+ * @code
  *    function outputHook( $outputPage, $parserOutput, $data ) { ... }
+ * @endcode
  */
 $wgParserOutputHooks = array();
 
@@ -4989,7 +5208,7 @@ $wgAutoloadClasses = array();
  * urls, descriptions and pointers to localized description msgs. Note that
  * the version, url, description and descriptionmsg key can be omitted.
  *
- * <code>
+ * @code
  * $wgExtensionCredits[$type][] = array(
  *     'name' => 'Example extension',
  *     'version' => 1.9,
@@ -4999,7 +5218,7 @@ $wgAutoloadClasses = array();
  *     'description' => 'An example extension',
  *     'descriptionmsg' => 'exampleextension-desc',
  * );
- * </code>
+ * @endcode
  *
  * Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'.
  * Where 'descriptionmsg' can be an array with message key and parameters:
@@ -5015,12 +5234,30 @@ $wgAuth = null;
 
 /**
  * Global list of hooks.
- * Add a hook by doing:
+ *
+ * The key is one of the events made available by MediaWiki, you can find
+ * a description for most of them in docs/hooks.txt. The array is used
+ * internally by Hook:run().
+ *
+ * The value can be one of:
+ *
+ * - A function name:
+ * @code
  *     $wgHooks['event_name'][] = $function;
- * or:
+ * @endcode
+ * - A function with some data:
+ * @code
  *     $wgHooks['event_name'][] = array($function, $data);
- * or:
+ * @endcode
+ * - A an object method:
+ * @code
  *     $wgHooks['event_name'][] = array($object, 'method');
+ * @endcode
+ *
+ * @warning You should always append to an event array or you will end up
+ * deleting a previous registered hook.
+ *
+ * @todo Does it support PHP closures?
  */
 $wgHooks = array();
 
@@ -5174,11 +5411,13 @@ $wgLogRestrictions = array(
  *
  * See $wgLogTypes for a list of available log types.
  *
- * For example:
+ * @par Example:
+ * @code
  *   $wgFilterLogTypes => array(
  *      'move' => true,
  *      'import' => false,
  *   );
+ * @endcode
  *
  * Will display show/hide links for the move and import logs. Move logs will be
  * hidden by default unless the link is clicked. Import logs will be shown by
@@ -5197,7 +5436,7 @@ $wgFilterLogTypes = array(
  *
  * Extensions with custom log types may add to this array.
  *
- * Since 1.19, if you follow the naming convention log-name-TYPE,
+ * @since 1.19, if you follow the naming convention log-name-TYPE,
  * where TYPE is your log type, yoy don't need to use this array.
  */
 $wgLogNames = array(
@@ -5220,7 +5459,7 @@ $wgLogNames = array(
  *
  * Extensions with custom log types may add to this array.
  *
- * Since 1.19, if you follow the naming convention log-description-TYPE,
+ * @since 1.19, if you follow the naming convention log-description-TYPE,
  * where TYPE is your log type, yoy don't need to use this array.
  */
 $wgLogHeaders = array(
@@ -5499,8 +5738,10 @@ $wgDefaultRobotPolicy = 'index,follow';
  * URLs, so search engine spiders risk getting lost in a maze of twisty special
  * pages, all alike, and never reaching your actual content.
  *
- * Example:
+ * @par Example:
+ * @code
  *   $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' );
+ * @endcode
  */
 $wgNamespaceRobotPolicies = array();
 
@@ -5508,10 +5749,18 @@ $wgNamespaceRobotPolicies = array();
  * Robot policies per article. These override the per-namespace robot policies.
  * Must be in the form of an array where the key part is a properly canonical-
  * ised text form title and the value is a robot policy.
- * Example:
- *   $wgArticleRobotPolicies = array( 'Main Page' => 'noindex,follow',
- *     'User:Bob' => 'index,follow' );
- * Example that DOES NOT WORK because the names are not canonical text forms:
+ *
+ * @par Example:
+ * @code
+ * $wgArticleRobotPolicies = array(
+ *             'Main Page' => 'noindex,follow',
+ *             'User:Bob' => 'index,follow',
+ * );
+ * @endcode
+ *
+ * @par Example that DOES NOT WORK because the names are not canonical text
+ * forms:
+ * @code
  *   $wgArticleRobotPolicies = array(
  *     # Underscore, not space!
  *     'Main_Page' => 'noindex,follow',
@@ -5520,6 +5769,7 @@ $wgNamespaceRobotPolicies = array();
  *     # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false for that namespace)!
  *     'abc' => 'noindex,nofollow'
  *   );
+ * @endcode
  */
 $wgArticleRobotPolicies = array();
 
@@ -5527,8 +5777,11 @@ $wgArticleRobotPolicies = array();
  * An array of namespace keys in which the __INDEX__/__NOINDEX__ magic words
  * will not function, so users can't decide whether pages in that namespace are
  * indexed by search engines.  If set to null, default to $wgContentNamespaces.
- * Example:
+ *
+ * @par Example:
+ * @code
  *   $wgExemptFromUserRobotsControl = array( NS_MAIN, NS_TALK, NS_PROJECT );
+ * @endcode
  */
 $wgExemptFromUserRobotsControl = null;
 
@@ -5558,9 +5811,10 @@ $wgEnableAPI = true;
 $wgEnableWriteAPI = true;
 
 /**
- * API module extensions
+ * API module extensions.
  * Associative array mapping module name to class name.
  * Extension modules may override the core modules.
+ * @todo Describe each of the variables, group them and add examples
  */
 $wgAPIModules = array();
 $wgAPIMetaModules = array();
@@ -5575,7 +5829,7 @@ $wgAPIMaxDBRows = 5000;
 
 /**
  * The maximum size (in bytes) of an API result.
- * Don't set this lower than $wgMaxArticleSize*1024
+ * @warning Do not set this lower than $wgMaxArticleSize*1024
  */
 $wgAPIMaxResultSize = 8388608;
 
@@ -5630,17 +5884,18 @@ $wgAjaxLicensePreview = true;
  * This is currently only used by the API (requests to api.php)
  * $wgCrossSiteAJAXdomains can be set using a wildcard syntax:
  *
- * '*' matches any number of characters
- * '?' matches any 1 character
- *
- * Example:
- $wgCrossSiteAJAXdomains = array(
-  'www.mediawiki.org',
-  '*.wikipedia.org',
-  '*.wikimedia.org',
-  '*.wiktionary.org',
- );
+ * - '*' matches any number of characters
+ * - '?' matches any 1 character
  *
+ * @par Example:
+ * @code
+ * $wgCrossSiteAJAXdomains = array(
+ *     'www.mediawiki.org',
+ *     '*.wikipedia.org',
+ *     '*.wikimedia.org',
+ *     '*.wiktionary.org',
+ * );
+ * @endcode
  */
 $wgCrossSiteAJAXdomains = array();
 
@@ -5744,7 +5999,7 @@ $wgUpdateRowsPerQuery = 100;
 
 /**
  * The build directory for HipHop compilation.
- * Defaults to $IP/maintenance/hiphop/build.
+ * Defaults to '$IP/maintenance/hiphop/build'.
  */
 $wgHipHopBuildDirectory = false;
 
@@ -5764,8 +6019,9 @@ $wgHipHopCompilerProcs = 'detect';
  *
  * To compile extensions with HipHop, set $wgExtensionsDirectory correctly,
  * and use code like:
- *
+ * @code
  *    require( MWInit::extensionSetupPath( 'Extension/Extension.php' ) );
+ * @endcode
  *
  * to include the extension setup file from LocalSettings.php. It is not
  * necessary to set this variable unless you use MWInit::extensionSetupPath().
@@ -5810,9 +6066,11 @@ $wgExternalDiffEngine = false;
 
 /**
  * Disable redirects to special pages and interwiki redirects, which use a 302
- * and have no "redirected from" link. Note this is only for articles with #Redirect
- * in them. URL's containing a local interwiki prefix (or a non-canonical special
- * page name) are still hard redirected regardless of this setting.
+ * and have no "redirected from" link.
+ *
+ * @note This is only for articles with #REDIRECT in them. URL's containing a
+ * local interwiki prefix (or a non-canonical special page name) are still hard
+ * redirected regardless of this setting.
  */
 $wgDisableHardRedirects = false;
 
@@ -5823,8 +6081,8 @@ $wgDisableHardRedirects = false;
 $wgLinkHolderBatchSize = 1000;
 
 /**
- * By default MediaWiki does not register links pointing to same server in externallinks dataset,
- * use this value to override:
+ * By default MediaWiki does not register links pointing to same server in
+ * externallinks dataset, use this value to override:
  */
 $wgRegisterInternalExternals = false;
 
@@ -5852,8 +6110,10 @@ $wgRedirectOnLogin = null;
  * This configuration array maps pool types to an associative array. The only
  * defined key in the associative array is "class", which gives the class name.
  * The remaining elements are passed through to the class as constructor
- * parameters. Example:
+ * parameters.
  *
+ * @par Example:
+ * @code
  *   $wgPoolCounterConf = array( 'ArticleView' => array(
  *     'class' => 'PoolCounter_Client',
  *     'timeout' => 15, // wait timeout in seconds
@@ -5861,6 +6121,7 @@ $wgRedirectOnLogin = null;
  *     'maxqueue' => 50, // maximum number of total threads in each pool
  *     ... any extension-specific options...
  *   );
+ * @endcode
  */
 $wgPoolCounterConf = null;
 
index 00af974..b4989a6 100644 (file)
@@ -88,10 +88,19 @@ class DeferredUpdates {
                }
 
                foreach ( $updates as $update ) {
-                       $update->doUpdate();
+                       try {
+                               $update->doUpdate();
 
-                       if ( $doCommit && $dbw->trxLevel() ) {
-                               $dbw->commit( __METHOD__ );
+                               if ( $doCommit && $dbw->trxLevel() ) {
+                                       $dbw->commit( __METHOD__ );
+                               }
+                       } catch ( MWException $e ) {
+                               // We don't want exceptions thrown during deferred updates to
+                               // be reported to the user since the output is already sent.
+                               // Instead we just log them.
+                               if ( !$e instanceof ErrorPageError ) {
+                                       wfDebugLog( 'exception', $e->getLogMessage() );
+                               }
                        }
                }
 
index 5ce703e..f034153 100644 (file)
@@ -591,8 +591,8 @@ class EditPage {
                                wfProfileOut( get_class( $this ) . "::importContentFormData" );
                        }
 
-                       # Truncate for whole multibyte characters. +5 bytes for ellipsis
-                       $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 );
+                       # Truncate for whole multibyte characters
+                       $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 255 );
 
                        # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
                        # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
@@ -604,7 +604,7 @@ class EditPage {
                        # currently doing double duty as both edit summary and section title. Right now this
                        # is just to allow API edits to work around this limitation, but this should be
                        # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
-                       $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 250 );
+                       $this->sectiontitle = $wgLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
                        $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
 
                        $this->edittime = $request->getVal( 'wpEdittime' );
@@ -2109,7 +2109,7 @@ class EditPage {
         * @return array An array in the format array( $label, $input )
         */
        function getSummaryInput( $summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null ) {
-               // Note: the maxlength is overriden in JS to 250 and to make it use UTF-8 bytes, not characters.
+               // Note: the maxlength is overriden in JS to 255 and to make it use UTF-8 bytes, not characters.
                $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : array() ) + array(
                        'id' => 'wpSummary',
                        'maxlength' => '200',
index 6e1325f..0fc5cd7 100644 (file)
@@ -33,7 +33,8 @@ class MWException extends Exception {
        var $logId;
 
        /**
-        * Should the exception use $wgOut to output the error ?
+        * Should the exception use $wgOut to output the error?
+        *
         * @return bool
         */
        function useOutputPage() {
@@ -44,7 +45,8 @@ class MWException extends Exception {
        }
 
        /**
-        * Can the extension use wfMsg() to get i18n messages ?
+        * Can the extension use wfMsg() to get i18n messages?
+        *
         * @return bool
         */
        function useMessageCache() {
@@ -62,9 +64,9 @@ class MWException extends Exception {
        /**
         * Run hook to allow extensions to modify the text of the exception
         *
-        * @param $name String: class name of the exception
-        * @param $args Array: arguments to pass to the callback functions
-        * @return Mixed: string to output or null if any hook has been called
+        * @param $name string: class name of the exception
+        * @param $args array: arguments to pass to the callback functions
+        * @return string|null string to output or null if any hook has been called
         */
        function runHooks( $name, $args = array() ) {
                global $wgExceptionHooks;
@@ -97,11 +99,11 @@ class MWException extends Exception {
        /**
         * Get a message from i18n
         *
-        * @param $key String: message name
-        * @param $fallback String: default message if the message cache can't be
+        * @param $key string: message name
+        * @param $fallback string: default message if the message cache can't be
         *                  called by the exception
         * The function also has other parameters that are arguments for the message
-        * @return String message with arguments replaced
+        * @return string message with arguments replaced
         */
        function msg( $key, $fallback /*[, params...] */ ) {
                $args = array_slice( func_get_args(), 2 );
@@ -118,7 +120,7 @@ class MWException extends Exception {
         * backtrace to the error, otherwise show a message to ask to set it to true
         * to show that information.
         *
-        * @return String html to output
+        * @return string html to output
         */
        function getHTML() {
                global $wgShowExceptionDetails;
@@ -140,8 +142,10 @@ class MWException extends Exception {
        }
 
        /**
+        * Get the text to display when reporting the error on the command line.
         * If $wgShowExceptionDetails is true, return a text message with a
         * backtrace to the error.
+        *
         * @return string
         */
        function getText() {
@@ -157,13 +161,21 @@ class MWException extends Exception {
        }
 
        /**
-        * Return titles of this error page
-        * @return String
+        * Return the title of the page when reporting this error in a HTTP response.
+        *
+        * @return string
         */
        function getPageTitle() {
                return $this->msg( 'internalerror', "Internal error" );
        }
 
+       /**
+        * Get a random ID for this error.
+        * This allows to link the exception to its correspoding log entry when
+        * $wgShowExceptionDetails is set to false.
+        *
+        * @return string
+        */
        function getLogId() {
                if ( $this->logId === null ) {
                        $this->logId = wfRandomString( 8 );
@@ -175,7 +187,7 @@ class MWException extends Exception {
         * Return the requested URL and point to file and line number from which the
         * exception occured
         *
-        * @return String
+        * @return string
         */
        function getLogMessage() {
                global $wgRequest;
@@ -197,7 +209,9 @@ class MWException extends Exception {
                return "[$id] $url   Exception from line $line of $file: $message";
        }
 
-       /** Output the exception report using HTML */
+       /**
+        * Output the exception report using HTML.
+        */
        function reportHTML() {
                global $wgOut;
                if ( $this->useOutputPage() ) {
@@ -261,7 +275,9 @@ class MWException extends Exception {
        }
 
        /**
-        * @static
+        * Check whether we are in command line mode or not to report the exception
+        * in the correct format.
+        *
         * @return bool
         */
        static function isCommandLine() {
@@ -272,6 +288,7 @@ class MWException extends Exception {
 /**
  * Exception class which takes an HTML error message, and does not
  * produce a backtrace. Replacement for OutputPage::fatalError().
+ *
  * @ingroup Exception
  */
 class FatalError extends MWException {
@@ -292,7 +309,8 @@ class FatalError extends MWException {
 }
 
 /**
- * An error page which can definitely be safely rendered using the OutputPage
+ * An error page which can definitely be safely rendered using the OutputPage.
+ *
  * @ingroup Exception
  */
 class ErrorPageError extends MWException {
@@ -331,6 +349,8 @@ class ErrorPageError extends MWException {
  * Show an error page on a badtitle.
  * Similar to ErrorPage, but emit a 400 HTTP error code to let mobile
  * browser it is not really a valid content.
+ *
+ * @ingroup Exception
  */
 class BadTitleError extends ErrorPageError {
 
@@ -360,6 +380,7 @@ class BadTitleError extends ErrorPageError {
 /**
  * Show an error when a user tries to do something they do not have the necessary
  * permissions for.
+ *
  * @ingroup Exception
  */
 class PermissionsError extends ErrorPageError {
@@ -396,7 +417,8 @@ class PermissionsError extends ErrorPageError {
 
 /**
  * Show an error when the wiki is locked/read-only and the user tries to do
- * something that requires write access
+ * something that requires write access.
+ *
  * @ingroup Exception
  */
 class ReadOnlyError extends ErrorPageError {
@@ -410,7 +432,8 @@ class ReadOnlyError extends ErrorPageError {
 }
 
 /**
- * Show an error when the user hits a rate limit
+ * Show an error when the user hits a rate limit.
+ *
  * @ingroup Exception
  */
 class ThrottledError extends ErrorPageError {
@@ -429,7 +452,8 @@ class ThrottledError extends ErrorPageError {
 }
 
 /**
- * Show an error when the user tries to do something whilst blocked
+ * Show an error when the user tries to do something whilst blocked.
+ *
  * @ingroup Exception
  */
 class UserBlockedError extends ErrorPageError {
@@ -495,15 +519,18 @@ class UserBlockedError extends ErrorPageError {
  * }
  * @endcode
  *
- * @param $reasonMsg A message key containing the reason for the error.
- *        Optional, default: 'exception-nologin-text'
- * @param $titleMsg A message key to set the page title.
- *        Optional, default: 'exception-nologin'
- * @param $params Parameters to wfMsg().
- *        Optiona, default: null
+ * @ingroup Exception
  */
 class UserNotLoggedIn extends ErrorPageError {
 
+       /**
+        * @param $reasonMsg A message key containing the reason for the error.
+        *        Optional, default: 'exception-nologin-text'
+        * @param $titleMsg A message key to set the page title.
+        *        Optional, default: 'exception-nologin'
+        * @param $params Parameters to wfMsg().
+        *        Optiona, default: null
+        */
        public function __construct(
                $reasonMsg = 'exception-nologin-text',
                $titleMsg  = 'exception-nologin',
@@ -628,7 +655,8 @@ class MWExceptionHandler {
        /**
         * Print a message, if possible to STDERR.
         * Use this in command line mode only (see isCommandLine)
-        * @param $message String Failure text
+        *
+        * @param $message string Failure text
         */
        public static function printError( $message ) {
                # NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602).
@@ -642,8 +670,9 @@ class MWExceptionHandler {
 
        /**
         * Print a message after escaping it and converting newlines to <br>
-        * Use this for non-command line failures
-        * @param $message String Failure text
+        * Use this for non-command line failures.
+        *
+        * @param $message string Failure text
         */
        private static function escapeEchoAndDie( $message ) {
                echo nl2br( htmlspecialchars( $message ) ) . "\n";
index 2bef114..f01fb23 100644 (file)
@@ -58,6 +58,14 @@ class WikiExporter {
         */
        var $sink;
 
+       /**
+        * Returns the export schema version.
+        * @return string
+        */
+       public static function schemaVersion() {
+               return "0.7";
+       }
+
        /**
         * If using WikiExporter::STREAM to stream a large amount of data,
         * provide a database connection which is not managed by
@@ -465,10 +473,12 @@ class WikiExporter {
 class XmlDumpWriter {
        /**
         * Returns the export schema version.
+        * @deprecated in 1.20; use WikiExporter::schemaVersion() instead
         * @return string
         */
        function schemaVersion() {
-               return "0.7";
+               wfDeprecated( __METHOD__, '1.20' );
+               return WikiExporter::schemaVersion();
        }
 
        /**
@@ -483,7 +493,7 @@ class XmlDumpWriter {
         */
        function openStream() {
                global $wgLanguageCode;
-               $ver = $this->schemaVersion();
+               $ver = WikiExporter::schemaVersion();
                return Xml::element( 'mediawiki', array(
                        'xmlns'              => "http://www.mediawiki.org/xml/export-$ver/",
                        'xmlns:xsi'          => "http://www.w3.org/2001/XMLSchema-instance",
index 1a21e20..35887a4 100644 (file)
@@ -1053,11 +1053,22 @@ function wfDebugLog( $logGroup, $text, $public = true ) {
  * @param $text String: database error message.
  */
 function wfLogDBError( $text ) {
-       global $wgDBerrorLog;
+       global $wgDBerrorLog, $wgDBerrorLogInUTC;
        if ( $wgDBerrorLog ) {
                $host = wfHostname();
                $wiki = wfWikiID();
-               $text = date( 'D M j G:i:s T Y' ) . "\t$host\t$wiki\t$text";
+
+               if( $wgDBerrorLogInUTC ) {
+                       $wikiTimezone = date_default_timezone_get();
+                       date_default_timezone_set( 'UTC' );
+               }
+               $date = date( 'D M j G:i:s T Y' );
+               if( $wgDBerrorLogInUTC ) {
+                       // Restore timezone
+                       date_default_timezone_set( $wikiTimezone );
+               }
+
+               $text = "$date\t$host\t$wiki\t$text";
                wfErrorLog( $text, $wgDBerrorLog );
        }
 }
@@ -2874,9 +2885,11 @@ function wfEscapeShellArg( ) {
  *                 (non-zero is usually failure)
  * @param $environ Array optional environment variables which should be
  *                 added to the executed command environment.
+ * @param $limits Array optional array with limits(filesize, memory, time)
+ *                 this overwrites the global wgShellMax* limits.
  * @return string collected stdout as a string (trailing newlines stripped)
  */
-function wfShellExec( $cmd, &$retval = null, $environ = array() ) {
+function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array() ) {
        global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime;
 
        static $disabled;
@@ -2924,9 +2937,9 @@ function wfShellExec( $cmd, &$retval = null, $environ = array() ) {
        $cmd = $envcmd . $cmd;
 
        if ( php_uname( 's' ) == 'Linux' ) {
-               $time = intval( $wgMaxShellTime );
-               $mem = intval( $wgMaxShellMemory );
-               $filesize = intval( $wgMaxShellFileSize );
+               $time = intval ( isset($limits['time']) ? $limits['time'] : $wgMaxShellTime );
+               $mem = intval ( isset($limits['memory']) ? $limits['memory'] : $wgMaxShellMemory );
+               $filesize = intval ( isset($limits['filesize']) ? $limits['filesize'] : $wgMaxShellFileSize );
 
                if ( $time > 0 && $mem > 0 ) {
                        $script = "$IP/bin/ulimit4.sh";
@@ -3196,7 +3209,7 @@ function wfUseMW( $req_ver ) {
  * http://bugs.php.net/bug.php?id=33898
  *
  * PHP's basename() only considers '\' a pathchar on Windows and Netware.
- * We'll consider it so always, as we don't want \s in our Unix paths either.
+ * We'll consider it so always, as we don't want '\s' in our Unix paths either.
  *
  * @param $path String
  * @param $suffix String: to remove if present
index 9ced19e..38b15b9 100644 (file)
@@ -688,7 +688,7 @@ class HTMLForm extends ContextSource {
        /**
         * Format a stack of error messages into a single HTML string
         * @param $errors Array of message keys/values
-        * @return String HTML, a <ul> list of errors
+        * @return String HTML, a "<ul>" list of errors
         */
        public static function formatErrors( $errors ) {
                $errorstr = '';
@@ -1991,7 +1991,7 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
 
        /**
         * @param  $request WebRequest
-        * @return Array( <overall message>, <select value>, <text field value> )
+        * @return Array("<overall message>","<select value>","<text field value>")
         */
        function loadDataFromRequest( $request ) {
                if ( $request->getCheck( $this->mName ) ) {
index c76a5de..fead0f5 100644 (file)
@@ -155,7 +155,7 @@ class ImagePage extends Article {
                        # should be in page content language
                        $pageLang = $this->getTitle()->getPageLanguage();
                        $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
-                               'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
+                               'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
                        parent::view();
                        $out->addHTML( Xml::closeElement( 'div' ) );
index 87cdd3f..5355140 100644 (file)
@@ -1728,7 +1728,7 @@ class Linker {
                        }
                        $outText .= "</div><ul>\n";
 
-                       usort( $templates, array( 'Title', 'compare' ) );
+                       usort( $templates, 'Title::compare' );
                        foreach ( $templates as $titleObj ) {
                                $r = $titleObj->getRestrictions( 'edit' );
                                if ( in_array( 'sysop', $r ) ) {
index ac90e67..1b4bc49 100644 (file)
@@ -2963,6 +2963,9 @@ $templates
                        'wgPageContentLanguage' => $lang->getCode(),
                        'wgSeparatorTransformTable' => $compactSeparatorTransTable,
                        'wgDigitTransformTable' => $compactDigitTransTable,
+                       'wgDefaultDateFormat' => $lang->getDefaultDateFormat(),
+                       'wgMonthNames' => $lang->getMonthNamesArray(),
+                       'wgMonthNamesShort' => $lang->getMonthAbbreviationsArray(),
                        'wgRelevantPageName' => $relevantTitle->getPrefixedDBKey(),
                );
                if ( $wgContLang->hasVariants() ) {
index f80e05d..d82f957 100644 (file)
@@ -175,6 +175,15 @@ abstract class IndexPager extends ContextSource implements Pager {
                }
        }
 
+       /**
+        * Get the Database object in use
+        *
+        * @return DatabaseBase
+        */
+       public function getDatabase() {
+               return $this->mDb;
+       }
+
        /**
         * Do the query, using information from the object context. This function
         * has been kept minimal to make it overridable if necessary, to allow for
index fb8e022..20e7909 100644 (file)
@@ -231,13 +231,15 @@ class RecentChange {
                        }
                        $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
 
-                       # @todo FIXME: This would be better as an extension hook
-                       $enotif = new EmailNotification();
-                       $status = $enotif->notifyOnPageChange( $editor, $title,
-                               $this->mAttribs['rc_timestamp'],
-                               $this->mAttribs['rc_comment'],
-                               $this->mAttribs['rc_minor'],
-                               $this->mAttribs['rc_last_oldid'] );
+                       if ( wfRunHooks( 'AbortEmailNotification', array($editor, $title) ) ) {
+                               # @todo FIXME: This would be better as an extension hook
+                               $enotif = new EmailNotification();
+                               $status = $enotif->notifyOnPageChange( $editor, $title,
+                                       $this->mAttribs['rc_timestamp'],
+                                       $this->mAttribs['rc_comment'],
+                                       $this->mAttribs['rc_minor'],
+                                       $this->mAttribs['rc_last_oldid'] );
+                       }
                }
        }
 
index 6ad374d..3f07dd6 100644 (file)
@@ -1699,17 +1699,22 @@ abstract class BaseTemplate extends QuickTemplate {
        }
 
        /**
-        * Generates a list item for a navigation, portlet, portal, sidebar... etc list
-        * $key is a string, usually a key from the list you are generating this link from
-        * $item is an array of list item data containing some of a specific set of keys.
+        * Generates a list item for a navigation, portlet, portal, sidebar... list
+        *
+        * @param $key string, usually a key from the list you are generating this link from.
+        * @param $item array, of list item data containing some of a specific set of keys.
         * The "id" and "class" keys will be used as attributes for the list item,
         * if "active" contains a value of true a "active" class will also be appended to class.
-        * If you want something other than a <li> you can pass a tag name such as
+        *
+        * @param $options array
+        *
+        * If you want something other than a "<li>" you can pass a tag name such as
         * "tag" => "span" in the $options array to change the tag used.
         * link/content data for the list item may come in one of two forms
         * A "links" key may be used, in which case it should contain an array with
-        * a list of links to include inside the list item, see makeLink for the format
-        * of individual links array items.
+        * a list of links to include inside the list item, see makeLink for the
+        * format of individual links array items.
+        *
         * Otherwise the relevant keys from the list item $item array will be passed
         * to makeLink instead. Note however that "id" and "class" are used by the
         * list item directly so they will not be passed to makeLink
@@ -1717,6 +1722,7 @@ abstract class BaseTemplate extends QuickTemplate {
         * If you need an id or class on a single link you should include a "links"
         * array with just one link item inside of it.
         * $options is also passed on to makeLink calls
+        *
         * @return string
         */
        function makeListItem( $key, $item, $options = array() ) {
index 6d36a43..aeb9ba4 100644 (file)
 /**
  * Abstract base class for update jobs that put some secondary data extracted
  * from article content into the database.
+ *
+ * @note: subclasses should NOT start or commit transactions in their doUpdate() method,
+ *        a transaction will automatically be wrapped around the update. Starting another
+ *        one would break the outer transaction bracket. If need be, subclasses can override
+ *        the beginTransaction() and commitTransaction() methods.
  */
 abstract class SqlDataUpdate extends DataUpdate {
 
index 1190172..7dd85b6 100644 (file)
@@ -126,6 +126,13 @@ class WikiReference {
        private $mServer; ///< server URL, may be protocol-relative, e.g. '//www.mediawiki.org'
        private $mPath;   ///< path, '/wiki/$1'
 
+       /**
+        * @param $major string
+        * @param $minor string
+        * @param $canonicalServer string
+        * @param $path string
+        * @param $server null|string
+        */
        public function __construct( $major, $minor, $canonicalServer, $path, $server = null ) {
                $this->mMajor = $major;
                $this->mMinor = $minor;
@@ -186,8 +193,17 @@ class WikiReference {
                return $this->mCanonicalServer . $this->getLocalUrl( $page );
        }
 
+       /**
+        * Get a canonical server URL
+        * @return string
+        */
+       public function getCanonicalServer() {
+               return $this->mCanonicalServer;
+       }
+
        /**
         * Alias for getCanonicalUrl(), for backwards compatibility.
+        * @param $page string
         * @return String
         */
        public function getUrl( $page ) {
index 8995f3f..6d9170a 100644 (file)
@@ -2606,7 +2606,7 @@ class WikiPage extends Page {
                if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
                        $truncatedtext = $wgContLang->truncate(
                                str_replace( "\n", ' ', $newtext ),
-                               max( 0, 250
+                               max( 0, 255
                                        - strlen( wfMsgForContent( 'autoredircomment' ) )
                                        - strlen( $rt->getFullText() )
                                ) );
index 7393315..1dacabc 100644 (file)
@@ -618,10 +618,8 @@ class HistoryPager extends ReverseChronologicalPager {
                $tools = array();
 
                # Rollback and undo links
-               if ( $prevRev &&
-                       !count( $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getUser() ) ) )
-               {
-                       if ( $latest && !count( $this->getTitle()->getUserPermissionsErrors( 'rollback', $this->getUser() ) ) ) {
+               if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) {
+                       if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) {
                                $this->preventClickjacking();
                                $tools[] = '<span class="mw-rollback-link">' .
                                        Linker::buildRollbackLink( $rev, $this->getContext() ) . '</span>';
index 543beb7..89e2cfe 100644 (file)
@@ -107,6 +107,7 @@ class InfoAction extends FormlessAction {
         * @return mixed array or boolean false
         */
        public static function pageCountInfo( $title ) {
+               wfProfileIn( __METHOD__ );
                $id = $title->getArticleID();
                $dbr = wfGetDB( DB_SLAVE );
 
@@ -114,8 +115,8 @@ class InfoAction extends FormlessAction {
                        'watchlist',
                        'COUNT(*)',
                        array(
+                               'wl_namespace' => $title->getNamespace(),
                                'wl_title'     => $title->getDBkey(),
-                               'wl_namespace' => $title->getNamespace()
                        ),
                        __METHOD__
                );
@@ -133,15 +134,21 @@ class InfoAction extends FormlessAction {
                        array( 'rev_page' => $id ),
                        __METHOD__
                );
+               $result = array( 'watchers' => $watchers, 'edits' => $edits,
+                       'authors' => $authors );
 
-               $views = (int)$dbr->selectField(
-                       'page',
-                       'page_counter',
-                       array( 'page_id' => $id ),
-                       __METHOD__
-               );
-
-               return array( 'watchers' => $watchers, 'edits' => $edits,
-                       'authors' => $authors, 'views' => $views );
+               global $wgDisableCounters;
+               if ( !$wgDisableCounters ) {
+                       $views = (int)$dbr->selectField(
+                               'page',
+                               'page_counter',
+                               array( 'page_id' => $id ),
+                               __METHOD__
+                       );
+                       $result['views'] = $views;
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $result;
        }
 }
index 6cd31ae..183d978 100644 (file)
@@ -72,9 +72,9 @@ class ApiBlock extends ApiBase {
                $data = array(
                        'Target' => $params['user'],
                        'Reason' => array(
-                               is_null( $params['reason'] ) ? '' : $params['reason'],
+                               $params['reason'],
                                'other',
-                               is_null( $params['reason'] ) ? '' : $params['reason']
+                               $params['reason']
                        ),
                        'Expiry' => $params['expiry'] == 'never' ? 'infinite' : $params['expiry'],
                        'HardBlock' => !$params['anononly'],
@@ -156,7 +156,7 @@ class ApiBlock extends ApiBase {
                                ApiBase::PARAM_DEPRECATED => true,
                        ),
                        'expiry' => 'never',
-                       'reason' => null,
+                       'reason' => '',
                        'anononly' => false,
                        'nocreate' => false,
                        'autoblock' => false,
@@ -174,7 +174,7 @@ class ApiBlock extends ApiBase {
                        'token' => 'A block token previously obtained through prop=info',
                        'gettoken' => 'If set, a block token will be returned, and no other action will be taken',
                        'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.',
-                       'reason' => 'Reason for block (optional)',
+                       'reason' => 'Reason for block',
                        'anononly' => 'Block anonymous users only (i.e. disable anonymous edits for this IP)',
                        'nocreate' => 'Prevent account creation',
                        'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from',
index 91406af..518e64c 100644 (file)
@@ -52,7 +52,7 @@ class ApiDelete extends ApiBase {
                }
 
                $titleObj = $pageObj->getTitle();
-               $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
+               $reason = $params['reason'];
                $user = $this->getUser();
 
                if ( $titleObj->getNamespace() == NS_FILE ) {
@@ -62,7 +62,7 @@ class ApiDelete extends ApiBase {
                }
 
                if ( !$status->isGood() ) {
-                       $errors = $this->getErrorsArray();
+                       $errors = $status->getErrorsArray();
                        $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them
                }
 
index c89f59b..acaa8b0 100644 (file)
@@ -37,9 +37,6 @@ class ApiMove extends ApiBase {
        public function execute() {
                $user = $this->getUser();
                $params = $this->extractRequestParams();
-               if ( is_null( $params['reason'] ) ) {
-                       $params['reason'] = '';
-               }
 
                $this->requireOnlyOneParameter( $params, 'from', 'fromid' );
 
@@ -181,7 +178,7 @@ class ApiMove extends ApiBase {
                                ApiBase::PARAM_REQUIRED => true
                        ),
                        'token' => null,
-                       'reason' => null,
+                       'reason' => '',
                        'movetalk' => false,
                        'movesubpages' => false,
                        'noredirect' => false,
@@ -213,7 +210,7 @@ class ApiMove extends ApiBase {
                        'fromid' => "Page ID of the page you want to move. Cannot be used together with {$p}from",
                        'to' => 'Title you want to rename the page to',
                        'token' => 'A move token previously retrieved through prop=info',
-                       'reason' => 'Reason for the move (optional)',
+                       'reason' => 'Reason for the move',
                        'movetalk' => 'Move the talk page, if it exists',
                        'movesubpages' => 'Move subpages, if applicable',
                        'noredirect' => 'Don\'t create a redirect',
index 0fcaf42..42d9f65 100644 (file)
@@ -176,7 +176,7 @@ class ApiProtect extends ApiBase {
                        'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)',
                        'expiry' => array( 'Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.',
                                        'Use \'infinite\', \'indefinite\' or \'never\', for a neverexpiring protection.' ),
-                       'reason' => 'Reason for (un)protecting (optional)',
+                       'reason' => 'Reason for (un)protecting',
                        'cascade' => array( 'Enable cascading protection (i.e. protect pages included in this page)',
                                        'Ignored if not all protection levels are \'sysop\' or \'protect\'' ),
                        'watch' => 'If set, add the page being (un)protected to your watchlist',
index 60b57bf..c81222a 100644 (file)
@@ -57,6 +57,18 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
 
                $this->addTables( 'category' );
                $this->addFields( 'cat_title' );
+               $this->addWhere( 'cat_pages > 0' );
+
+               if ( !is_null( $params['continue'] ) ) {
+                       $cont = explode( '|', $params['continue'] );
+                       if ( count( $cont ) != 1 ) {
+                               $this->dieUsage( "Invalid continue param. You should pass the " .
+                                       "original value returned by the previous query", "_badcontinue" );
+                       }
+                       $op = $params['dir'] == 'descending' ? '<' : '>';
+                       $cont_from = $db->addQuotes( $cont[0] );
+                       $this->addWhere( "cat_title $op= $cont_from" );
+               }
 
                $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
                $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
@@ -104,8 +116,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
                foreach ( $res as $row ) {
                        if ( ++ $count > $params['limit'] ) {
                                // We've reached the one extra which shows that there are additional cats to be had. Stop here...
-                               // TODO: Security issue - if the user has no right to view next title, it will still be shown
-                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) );
+                               $this->setContinueEnumParameter( 'continue', $row->cat_title );
                                break;
                        }
 
@@ -127,7 +138,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
                                }
                                $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $item );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->cat_title );
                                        break;
                                }
                        }
@@ -143,6 +154,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
        public function getAllowedParams() {
                return array(
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'dir' => array(
@@ -178,6 +190,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
        public function getParamDescription() {
                return array(
                        'from' => 'The category to start enumerating from',
+                       'continue' => 'When more results are available, use this to continue',
                        'to' => 'The category to stop enumerating at',
                        'prefix' => 'Search for all category titles that begin with this value',
                        'dir' => 'Direction to sort in',
@@ -213,6 +226,12 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
                return 'Enumerate all categories';
        }
 
+       public function getPossibleErrors() {
+               return array_merge( parent::getPossibleErrors(), array(
+                       array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+               ) );
+       }
+
        public function getExamples() {
                return array(
                        'api.php?action=query&list=allcategories&acprop=size',
index 4ab4d72..45cc404 100644 (file)
@@ -85,6 +85,17 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
 
                $params = $this->extractRequestParams();
 
+               if ( !is_null( $params['continue'] ) ) {
+                       $cont = explode( '|', $params['continue'] );
+                       if ( count( $cont ) != 1 ) {
+                               $this->dieUsage( "Invalid continue param. You should pass the " .
+                                       "original value returned by the previous query", "_badcontinue" );
+                       }
+                       $op = $params['dir'] == 'descending' ? '<' : '>';
+                       $cont_from = $db->addQuotes( $cont[0] );
+                       $this->addWhere( "img_name $op= $cont_from" );
+               }
+
                // Image filters
                $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
                $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
@@ -148,8 +159,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                foreach ( $res as $row ) {
                        if ( ++ $count > $limit ) {
                                // We've reached the one extra which shows that there are additional pages to be had. Stop here...
-                               // TODO: Security issue - if the user has no right to view next title, it will still be shown
-                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
+                               $this->setContinueEnumParameter( 'continue', $row->img_name );
                                break;
                        }
 
@@ -161,7 +171,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
 
                                $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->img_name );
                                        break;
                                }
                        } else {
@@ -179,6 +189,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
        public function getAllowedParams() {
                return array (
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'minsize' => array(
@@ -215,6 +226,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
        public function getParamDescription() {
                return array(
                        'from' => 'The image title to start enumerating from',
+                       'continue' => 'When more results are available, use this to continue',
                        'to' => 'The image title to stop enumerating at',
                        'prefix' => 'Search for all image titles that begin with this value',
                        'dir' => 'The direction in which to list',
@@ -228,7 +240,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                );
        }
 
-       private $propertyFilter = array( 'archivename' );
+       private $propertyFilter = array( 'archivename', 'thumbmime' );
 
        public function getResultProperties() {
                return array_merge(
@@ -254,6 +266,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
                        array( 'code' => 'mimesearchdisabled', 'info' => 'MIME search disabled in Miser Mode' ),
                        array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ),
                        array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ),
+                       array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
                ) );
        }
 
index 70b6656..759b7a0 100644 (file)
@@ -77,17 +77,25 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
                }
                if ( !is_null( $params['continue'] ) ) {
                        $continueArr = explode( '|', $params['continue'] );
-                       if ( count( $continueArr ) != 2 ) {
-                               $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
-                       }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
-                       $continueTitle = $db->addQuotes( $this->titleToKey( $continueArr[0] ) );
-                       $continueFrom = intval( $continueArr[1] );
-                       $this->addWhere(
-                               "pl_title $op $continueTitle OR " .
-                               "(pl_title = $continueTitle AND " .
-                               "pl_from $op= $continueFrom)"
-                       );
+                       if ( $params['unique'] ) {
+                               if ( count( $continueArr ) != 1 ) {
+                                       $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
+                               }
+                               $continueTitle = $db->addQuotes( $continueArr[0] );
+                               $this->addWhere( "pl_title $op= $continueTitle" );
+                       } else {
+                               if ( count( $continueArr ) != 2 ) {
+                                       $this->dieUsage( 'Invalid continue parameter', 'badcontinue' );
+                               }
+                               $continueTitle = $db->addQuotes( $continueArr[0] );
+                               $continueFrom = intval( $continueArr[1] );
+                               $this->addWhere(
+                                       "pl_title $op $continueTitle OR " .
+                                       "(pl_title = $continueTitle AND " .
+                                       "pl_from $op= $continueFrom)"
+                               );
+                       }
                }
 
                $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
@@ -121,11 +129,10 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
                foreach ( $res as $row ) {
                        if ( ++ $count > $limit ) {
                                // We've reached the one extra which shows that there are additional pages to be had. Stop here...
-                               // TODO: Security issue - if the user has no right to view next title, it will still be shown
                                if ( $params['unique'] ) {
-                                       $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->pl_title );
                                } else {
-                                       $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from );
+                                       $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from );
                                }
                                break;
                        }
@@ -142,9 +149,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
                                $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
                                if ( !$fit ) {
                                        if ( $params['unique'] ) {
-                                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) );
+                                               $this->setContinueEnumParameter( 'continue', $row->pl_title );
                                        } else {
-                                               $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from );
+                                               $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from );
                                        }
                                        break;
                                }
index cfc22ff..58fa4c7 100644 (file)
@@ -67,6 +67,17 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
                // Page filters
                $this->addTables( 'page' );
 
+               if ( !is_null( $params['continue'] ) ) {
+                       $cont = explode( '|', $params['continue'] );
+                       if ( count( $cont ) != 1 ) {
+                               $this->dieUsage( "Invalid continue param. You should pass the " .
+                                       "original value returned by the previous query", "_badcontinue" );
+                       }
+                       $op = $params['dir'] == 'descending' ? '<' : '>';
+                       $cont_from = $db->addQuotes( $cont[0] );
+                       $this->addWhere( "page_title $op= $cont_from" );
+               }
+
                if ( $params['filterredir'] == 'redirects' ) {
                        $this->addWhereFld( 'page_is_redirect', 1 );
                } elseif ( $params['filterredir'] == 'nonredirects' ) {
@@ -165,13 +176,22 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
                $this->addOption( 'LIMIT', $limit + 1 );
                $res = $this->select( __METHOD__ );
 
+               //Get gender information
+               if( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) {
+                       $users = array();
+                       foreach ( $res as $row ) {
+                               $users[] = $row->page_title;
+                       }
+                       GenderCache::singleton()->doQuery( $users, __METHOD__ );
+                       $res->rewind(); //reset
+               }
+
                $count = 0;
                $result = $this->getResult();
                foreach ( $res as $row ) {
                        if ( ++ $count > $limit ) {
                                // We've reached the one extra which shows that there are additional pages to be had. Stop here...
-                               // TODO: Security issue - if the user has no right to view next title, it will still be shown
-                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) );
+                               $this->setContinueEnumParameter( 'continue', $row->page_title );
                                break;
                        }
 
@@ -184,7 +204,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
                                );
                                $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->page_title );
                                        break;
                                }
                        } else {
@@ -202,6 +222,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
 
                return array(
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'namespace' => array(
@@ -275,6 +296,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
                $p = $this->getModulePrefix();
                return array(
                        'from' => 'The page title to start enumerating from',
+                       'continue' => 'When more results are available, use this to continue',
                        'to' => 'The page title to stop enumerating at',
                        'prefix' => 'Search for all page titles that begin with this value',
                        'namespace' => 'The namespace to enumerate',
@@ -314,6 +336,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
                return array_merge( parent::getPossibleErrors(), array(
                        array( 'code' => 'params', 'info' => 'Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator' ),
                        array( 'code' => 'params', 'info' => 'prlevel may not be used without prtype' ),
+                       array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
                ) );
        }
 
index 283eb13..30901ab 100644 (file)
@@ -91,7 +91,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
                        }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $clfrom = intval( $cont[0] );
-                       $clto = $this->getDB()->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $clto = $this->getDB()->addQuotes( $cont[1] );
                        $this->addWhere(
                                "cl_from $op $clfrom OR " .
                                "(cl_from = $clfrom AND " .
@@ -143,8 +143,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
                                if ( ++$count > $params['limit'] ) {
                                        // We've reached the one extra which shows that
                                        // there are additional pages to be had. Stop here...
-                                       $this->setContinueEnumParameter( 'continue', $row->cl_from .
-                                                       '|' . $this->keyToTitle( $row->cl_to ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to );
                                        break;
                                }
 
@@ -164,8 +163,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
 
                                $fit = $this->addPageSubItem( $row->cl_from, $vals );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'continue', $row->cl_from .
-                                                       '|' . $this->keyToTitle( $row->cl_to ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to );
                                        break;
                                }
                        }
@@ -175,8 +173,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
                                if ( ++$count > $params['limit'] ) {
                                        // We've reached the one extra which shows that
                                        // there are additional pages to be had. Stop here...
-                                       $this->setContinueEnumParameter( 'continue', $row->cl_from .
-                                                       '|' . $this->keyToTitle( $row->cl_to ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to );
                                        break;
                                }
 
index df5e303..449d24a 100644 (file)
@@ -164,7 +164,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', 'badcontinue' );
                        }
                        $ns = intval( $cont[0] );
-                       $title = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $db->addQuotes( $cont[1] );
                        $ts = $db->addQuotes( $db->timestamp( $cont[2] ) );
                        $op = ( $dir == 'newer' ? '>' : '<' );
                        $this->addWhere( "ar_namespace $op $ns OR " .
@@ -203,7 +203,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                // We've had enough
                                if ( $mode == 'all' || $mode == 'revs' ) {
                                        $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' .
-                                               $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp );
+                                               $row->ar_title . '|' . $row->ar_timestamp );
                                } else {
                                        $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) );
                                }
@@ -269,7 +269,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        if ( !$fit ) {
                                if ( $mode == 'all' || $mode == 'revs' ) {
                                        $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' .
-                                               $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp );
+                                               $row->ar_title . '|' . $row->ar_timestamp );
                                } else {
                                        $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) );
                                }
index 856d0fd..bc7a86f 100644 (file)
@@ -82,8 +82,8 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                        }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $db = $this->getDB();
-                       $orig = $db->addQuotes( $this->titleTokey( $cont[0] ) );
-                       $dup = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $orig = $db->addQuotes( $cont[0] );
+                       $dup = $db->addQuotes( $cont[1] );
                        $this->addWhere(
                                "i1.img_name $op $orig OR " .
                                "(i1.img_name = $orig AND " .
@@ -110,9 +110,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                        if ( ++$count > $params['limit'] ) {
                                // We've reached the one extra which shows that
                                // there are additional pages to be had. Stop here...
-                               $this->setContinueEnumParameter( 'continue',
-                                       $this->keyToTitle( $row->orig_name ) . '|' .
-                                       $this->keyToTitle( $row->dup_name ) );
+                               $this->setContinueEnumParameter( 'continue', $row->orig_name . '|' . $row->dup_name );
                                break;
                        }
                        if ( !is_null( $resultPageSet ) ) {
@@ -125,9 +123,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                                );
                                $fit = $this->addPageSubItem( $images[$row->orig_name], $r );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'continue',
-                                                       $this->keyToTitle( $row->orig_name ) . '|' .
-                                                       $this->keyToTitle( $row->dup_name ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->orig_name . '|' . $row->dup_name );
                                        break;
                                }
                        }
index 07d5b41..eed9d7c 100644 (file)
@@ -73,9 +73,23 @@ class ApiQueryFilearchive extends ApiQueryBase {
                $this->addFieldsIf( 'fa_metadata', $fld_metadata );
                $this->addFieldsIf( 'fa_bits', $fld_bitdepth );
 
+               if ( !is_null( $params['continue'] ) ) {
+                       $cont = explode( '|', $params['continue'] );
+                       if ( count( $cont ) != 1 ) {
+                               $this->dieUsage( "Invalid continue param. You should pass the " .
+                                       "original value returned by the previous query", "_badcontinue" );
+                       }
+                       $op = $params['dir'] == 'descending' ? '<' : '>';
+                       $cont_from = $db->addQuotes( $cont[0] );
+                       $this->addWhere( "fa_name $op= $cont_from" );
+               }
+
                // Image filters
                $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
                $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
+               if ( !is_null( $params['continue'] ) ) {
+                       $from = $params['continue'];
+               }
                $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) );
                $this->addWhereRange( 'fa_name', $dir, $from, $to );
                if ( isset( $params['prefix'] ) ) {
@@ -129,8 +143,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
                foreach ( $res as $row ) {
                        if ( ++$count > $limit ) {
                                // We've reached the one extra which shows that there are additional pages to be had. Stop here...
-                               // TODO: Security issue - if the user has no right to view next title, it will still be shown
-                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) );
+                               $this->setContinueEnumParameter( 'continue', $row->fa_name );
                                break;
                        }
 
@@ -199,7 +212,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
 
                        $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file );
                        if ( !$fit ) {
-                               $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) );
+                               $this->setContinueEnumParameter( 'continue', $row->fa_name );
                                break;
                        }
                }
@@ -210,6 +223,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
        public function getAllowedParams() {
                return array (
                        'from' => null,
+                       'continue' => null,
                        'to' => null,
                        'prefix' => null,
                        'limit' => array(
@@ -251,6 +265,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
        public function getParamDescription() {
                return array(
                        'from' => 'The image title to start enumerating from',
+                       'continue' => 'When more results are available, use this to continue',
                        'to' => 'The image title to stop enumerating at',
                        'prefix' => 'Search for all image titles that begin with this value',
                        'dir' => 'The direction in which to list',
@@ -345,6 +360,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
                        array( 'code' => 'hashsearchdisabled', 'info' => 'Search by hash disabled in Miser Mode' ),
                        array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ),
                        array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ),
+                       array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
                ) );
        }
 
index 37f347d..5361727 100644 (file)
@@ -64,7 +64,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
                        $db = $this->getDB();
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $prefix = $db->addQuotes( $cont[0] );
-                       $title = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $db->addQuotes( $cont[1] );
                        $from = intval( $cont[2] );
                        $this->addWhere(
                                "iwl_prefix $op $prefix OR " .
index 7e69513..9788e4e 100644 (file)
@@ -66,7 +66,7 @@ class ApiQueryIWLinks extends ApiQueryBase {
                        $db = $this->getDB();
                        $iwlfrom = intval( $cont[0] );
                        $iwlprefix = $db->addQuotes( $cont[1] );
-                       $iwltitle = $db->addQuotes( $this->titleToKey( $cont[2] ) );
+                       $iwltitle = $db->addQuotes( $cont[2] );
                        $this->addWhere(
                                "iwl_from $op $iwlfrom OR " .
                                "(iwl_from = $iwlfrom AND " .
index 4d2d04b..2caf499 100644 (file)
@@ -356,8 +356,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
 
                                        if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) {
                                                list( $ext, $mime ) = $file->getHandler()->getThumbType(
-                                                       substr( $mto->getPath(), strrpos( $mto->getPath(), '.' ) + 1 ),
-                                                       $file->getMimeType(), $thumbParams );
+                                                       $mto->getExtension(), $file->getMimeType(), $thumbParams );
                                                $vals['thumbmime'] = $mime;
                                        }
                                } elseif ( $mto && $mto->isError() ) {
@@ -491,7 +490,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
         *
         * @return array
         */
-       private static function getProperties() {
+       private static function getProperties( $modulePrefix = '' ) {
                return array(
                        'timestamp' =>      ' timestamp     - Adds timestamp for the uploaded version',
                        'user' =>           ' user          - Adds the user who uploaded the image version',
@@ -503,7 +502,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        'dimensions' =>     ' dimensions    - Alias for size', // For backwards compatibility with Allimages
                        'sha1' =>           ' sha1          - Adds SHA-1 hash for the image',
                        'mime' =>           ' mime          - Adds MIME type of the image',
-                       'thumbmime' =>      ' thumbmime     - Adds MIME type of the image thumbnail (requires url)',
+                       'thumbmime' =>      ' thumbmime     - Adds MIME type of the image thumbnail' .
+                               ' (requires url and param ' . $modulePrefix . 'urlwidth)',
                        'mediatype' =>      ' mediatype     - Adds the media type of the image',
                        'metadata' =>       ' metadata      - Lists EXIF metadata for the version of the image',
                        'archivename' =>    ' archivename   - Adds the file name of the archive version for non-latest versions',
@@ -518,10 +518,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
         *
         * @return array
         */
-       public static function getPropertyDescriptions( $filter = array() ) {
+       public static function getPropertyDescriptions( $filter = array(), $modulePrefix = '' ) {
                return array_merge(
                        array( 'What image information to get:' ),
-                       array_values( array_diff_key( self::getProperties(), array_flip( $filter ) ) )
+                       array_values( array_diff_key( self::getProperties( $modulePrefix ), array_flip( $filter ) ) )
                );
        }
 
@@ -532,7 +532,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
        public function getParamDescription() {
                $p = $this->getModulePrefix();
                return array(
-                       'prop' => self::getPropertyDescriptions(),
+                       'prop' => self::getPropertyDescriptions( array(), $p ),
                        'urlwidth' => array( "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.",
                                            'Only the current version of the image can be scaled' ),
                        'urlheight' => "Similar to {$p}urlwidth. Cannot be used without {$p}urlwidth",
index 3779123..2ef6fa5 100644 (file)
@@ -68,7 +68,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
                        }
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $ilfrom = intval( $cont[0] );
-                       $ilto = $this->getDB()->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $ilto = $this->getDB()->addQuotes( $cont[1] );
                        $this->addWhere(
                                "il_from $op $ilfrom OR " .
                                "(il_from = $ilfrom AND " .
@@ -109,16 +109,14 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
                                if ( ++$count > $params['limit'] ) {
                                        // We've reached the one extra which shows that
                                        // there are additional pages to be had. Stop here...
-                                       $this->setContinueEnumParameter( 'continue', $row->il_from .
-                                                       '|' . $this->keyToTitle( $row->il_to ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to );
                                        break;
                                }
                                $vals = array();
                                ApiQueryBase::addTitleInfo( $vals, Title::makeTitle( NS_FILE, $row->il_to ) );
                                $fit = $this->addPageSubItem( $row->il_from, $vals );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'continue', $row->il_from .
-                                                       '|' . $this->keyToTitle( $row->il_to ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to );
                                        break;
                                }
                        }
@@ -129,8 +127,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
                                if ( ++$count > $params['limit'] ) {
                                        // We've reached the one extra which shows that
                                        // there are additional pages to be had. Stop here...
-                                       $this->setContinueEnumParameter( 'continue', $row->il_from .
-                                                       '|' . $this->keyToTitle( $row->il_to ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to );
                                        break;
                                }
                                $titles[] = Title::makeTitle( NS_FILE, $row->il_to );
index 87fd58b..99175b3 100644 (file)
@@ -57,7 +57,10 @@ class ApiQueryInfo extends ApiQueryBase {
                global $wgDisableCounters;
 
                $pageSet->requestField( 'page_restrictions' );
-               $pageSet->requestField( 'page_is_redirect' );
+               // when resolving redirects, no page will have this field
+               if( !$pageSet->isResolvingRedirects() ) {
+                       $pageSet->requestField( 'page_is_redirect' );
+               }
                $pageSet->requestField( 'page_is_new' );
                if ( !$wgDisableCounters ) {
                        $pageSet->requestField( 'page_counter' );
@@ -280,7 +283,10 @@ class ApiQueryInfo extends ApiQueryBase {
                }
 
                $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
-               $this->pageIsRedir = $pageSet->getCustomField( 'page_is_redirect' );
+               // when resolving redirects, no page will have this field
+               $this->pageIsRedir = !$pageSet->isResolvingRedirects()
+                       ? $pageSet->getCustomField( 'page_is_redirect' )
+                       : array();
                $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
 
                global $wgDisableCounters;
@@ -346,7 +352,7 @@ class ApiQueryInfo extends ApiQueryBase {
                                : intval( $this->pageCounter[$pageid] );
                        $pageInfo['length'] = intval( $this->pageLength[$pageid] );
 
-                       if ( $this->pageIsRedir[$pageid] ) {
+                       if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
                                $pageInfo['redirect'] = '';
                        }
                        if ( $this->pageIsNew[$pageid] ) {
index c7639ee..6248c35 100644 (file)
@@ -64,7 +64,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
                        $db = $this->getDB();
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $prefix = $db->addQuotes( $cont[0] );
-                       $title = $db->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $db->addQuotes( $cont[1] );
                        $from = intval( $cont[2] );
                        $this->addWhere(
                                "ll_lang $op $prefix OR " .
index 5cf9068..aa2633d 100644 (file)
@@ -119,7 +119,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
                        $op = $params['dir'] == 'descending' ? '<' : '>';
                        $plfrom = intval( $cont[0] );
                        $plns = intval( $cont[1] );
-                       $pltitle = $this->getDB()->addQuotes( $this->titleToKey( $cont[2] ) );
+                       $pltitle = $this->getDB()->addQuotes( $cont[2] );
                        $this->addWhere(
                                "{$this->prefix}_from $op $plfrom OR " .
                                "({$this->prefix}_from = $plfrom AND " .
@@ -157,8 +157,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
                                        // We've reached the one extra which shows that
                                        // there are additional pages to be had. Stop here...
                                        $this->setContinueEnumParameter( 'continue',
-                                               "{$row->pl_from}|{$row->pl_namespace}|" .
-                                               $this->keyToTitle( $row->pl_title ) );
+                                               "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
                                        break;
                                }
                                $vals = array();
@@ -166,8 +165,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
                                $fit = $this->addPageSubItem( $row->pl_from, $vals );
                                if ( !$fit ) {
                                        $this->setContinueEnumParameter( 'continue',
-                                               "{$row->pl_from}|{$row->pl_namespace}|" .
-                                               $this->keyToTitle( $row->pl_title ) );
+                                               "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
                                        break;
                                }
                        }
@@ -179,8 +177,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
                                        // We've reached the one extra which shows that
                                        // there are additional pages to be had. Stop here...
                                        $this->setContinueEnumParameter( 'continue',
-                                               "{$row->pl_from}|{$row->pl_namespace}|" .
-                                               $this->keyToTitle( $row->pl_title ) );
+                                               "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" );
                                        break;
                                }
                                $titles[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
index 511cbe4..5316006 100644 (file)
@@ -257,9 +257,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
        protected function appendSpecialPageAliases( $property ) {
                global $wgContLang;
                $data = array();
-               $aliases = $wgContLang->getSpecialPageAliases();
-               foreach ( SpecialPageFactory::getList() as $specialpage => $stuff ) {
-                       $arr = array( 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] );
+               foreach ( $wgContLang->getSpecialPageAliases() as $specialpage => $aliases ) {
+                       $arr = array( 'realname' => $specialpage, 'aliases' => $aliases );
                        $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
                        $data[] = $arr;
                }
index 02484ae..a310d10 100644 (file)
@@ -113,7 +113,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo {
        public function getParamDescription() {
                $p = $this->getModulePrefix();
                return array(
-                       'prop' => self::getPropertyDescriptions( $this->propertyFilter ),
+                       'prop' => self::getPropertyDescriptions( $this->propertyFilter, $p ),
                        'filekey' => 'Key that identifies a previous upload that was stashed temporarily.',
                        'sessionkey' => 'Alias for filekey, for backward compatibility.',
                        'urlwidth' => "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.",
index f531980..4161dd7 100644 (file)
@@ -256,7 +256,7 @@ class ApiQueryContributions extends ApiQueryBase {
                $this->addFieldsIf( 'page_latest', $this->fld_flags );
                // $this->addFieldsIf( 'rev_text_id', $this->fld_ids ); // Should this field be exposed?
                $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment );
-               $this->addFieldsIf( 'rev_len', $this->fld_size );
+               $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff );
                $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
                $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff );
                $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
index 1b1eee0..38b17fc 100644 (file)
@@ -76,7 +76,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
                                        "original value returned by the previous query", "_badcontinue" );
                        }
                        $ns = intval( $cont[0] );
-                       $title = $this->getDB()->addQuotes( $this->titleToKey( $cont[1] ) );
+                       $title = $this->getDB()->addQuotes( $cont[1] );
                        $op = $params['dir'] == 'ascending' ? '>' : '<';
                        $this->addWhere(
                                "wl_namespace $op $ns OR " .
@@ -103,8 +103,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
                foreach ( $res as $row ) {
                        if ( ++$count > $params['limit'] ) {
                                // We've reached the one extra which shows that there are additional pages to be had. Stop here...
-                               $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' .
-                                                                       $this->keyToTitle( $row->wl_title ) );
+                               $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title );
                                break;
                        }
                        $t = Title::makeTitle( $row->wl_namespace, $row->wl_title );
@@ -118,8 +117,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
                                }
                                $fit = $this->getResult()->addValue( $this->getModuleName(), null, $vals );
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' .
-                                                                       $this->keyToTitle( $row->wl_title ) );
+                                       $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title );
                                        break;
                                }
                        } else {
index 4de49ea..76de5fb 100644 (file)
@@ -49,7 +49,7 @@ class ApiRollback extends ApiBase {
                // User and title already validated in call to getTokenSalt from Main
                $titleObj = $this->getRbTitle();
                $pageObj = WikiPage::factory( $titleObj );
-               $summary = ( isset( $params['summary'] ) ? $params['summary'] : '' );
+               $summary = $params['summary'];
                $details = array();
                $retval = $pageObj->doRollback( $this->getRbUser(), $summary, $params['token'], $params['markbot'], $details, $this->getUser() );
 
@@ -91,7 +91,7 @@ class ApiRollback extends ApiBase {
                                ApiBase::PARAM_REQUIRED => true
                        ),
                        'token' => null,
-                       'summary' => null,
+                       'summary' => '',
                        'markbot' => false,
                        'watchlist' => array(
                                ApiBase::PARAM_DFLT => 'preferences',
@@ -110,7 +110,7 @@ class ApiRollback extends ApiBase {
                        'title' => 'Title of the page you want to rollback.',
                        'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.',
                        'token' => "A rollback token previously retrieved through {$this->getModulePrefix()}prop=revisions",
-                       'summary' => 'Custom edit summary. If not set, default summary will be used',
+                       'summary' => 'Custom edit summary. If empty, default summary will be used',
                        'markbot' => 'Mark the reverted edits and the revert as bot edits',
                        'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch',
                );
index 40a6d44..9bb8200 100644 (file)
@@ -69,7 +69,7 @@ class ApiUnblock extends ApiBase {
 
                $data = array(
                        'Target' => is_null( $params['id'] ) ? $params['user'] : "#{$params['id']}",
-                       'Reason' => is_null( $params['reason'] ) ? '' : $params['reason']
+                       'Reason' => $params['reason']
                );
                $block = Block::newFromTarget( $data['Target'] );
                $retval = SpecialUnblock::processUnblock( $data, $this->getContext() );
@@ -104,7 +104,7 @@ class ApiUnblock extends ApiBase {
                                ApiBase::PARAM_DFLT => false,
                                ApiBase::PARAM_DEPRECATED => true,
                        ),
-                       'reason' => null,
+                       'reason' => '',
                );
        }
 
@@ -115,7 +115,7 @@ class ApiUnblock extends ApiBase {
                        'user' => "Username, IP address or IP range you want to unblock. Cannot be used together with {$p}id",
                        'token' => "An unblock token previously obtained through prop=info",
                        'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken',
-                       'reason' => 'Reason for unblock (optional)',
+                       'reason' => 'Reason for unblock',
                );
        }
 
index c89f49a..a1a68c3 100644 (file)
@@ -116,7 +116,7 @@ class ApiUndelete extends ApiBase {
                return array(
                        'title' => 'Title of the page you want to restore',
                        'token' => 'An undelete token previously retrieved through list=deletedrevs',
-                       'reason' => 'Reason for restoring (optional)',
+                       'reason' => 'Reason for restoring',
                        'timestamps' => 'Timestamps of the revisions to restore. If not set, all revisions will be restored.',
                        'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch',
                );
diff --git a/includes/cache/ProcessCacheLRU.php b/includes/cache/ProcessCacheLRU.php
new file mode 100644 (file)
index 0000000..f215ebd
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Per-process memory cache for storing items.
+ *
+ * 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 Cache
+ */
+
+/**
+ * Handles per process caching of items
+ * @ingroup Cache
+ */
+class ProcessCacheLRU {
+       /** @var Array */
+       protected $cache = array(); // (key => prop => value)
+
+       protected $maxCacheKeys; // integer; max entries
+
+       /**
+        * @param $maxKeys integer Maximum number of entries allowed (min 1).
+        * @throws MWException When $maxCacheKeys is not an int or =< 0.
+        */
+       public function __construct( $maxKeys ) {
+               if ( !is_int( $maxKeys ) || $maxKeys < 1 ) {
+                       throw new MWException( __METHOD__ . " must be given an integer and >= 1" );
+               }
+               $this->maxCacheKeys = $maxKeys;
+       }
+
+       /**
+        * Set a property field for a cache entry.
+        * This will prune the cache if it gets too large.
+        * If the item is already set, it will be pushed to the top of the cache.
+        *
+        * @param $key string
+        * @param $prop string
+        * @param $value mixed
+        * @return void
+        */
+       public function set( $key, $prop, $value ) {
+               if ( isset( $this->cache[$key] ) ) {
+                       $this->ping( $key ); // push to top
+               } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
+                       reset( $this->cache );
+                       unset( $this->cache[key( $this->cache )] );
+               }
+               $this->cache[$key][$prop] = $value;
+       }
+
+       /**
+        * Check if a property field exists for a cache entry.
+        *
+        * @param $key string
+        * @param $prop string
+        * @return bool
+        */
+       public function has( $key, $prop ) {
+               return isset( $this->cache[$key][$prop] );
+       }
+
+       /**
+        * Get a property field for a cache entry.
+        * This returns null if the property is not set.
+        * If the item is already set, it will be pushed to the top of the cache.
+        *
+        * @param $key string
+        * @param $prop string
+        * @return mixed
+        */
+       public function get( $key, $prop ) {
+               if ( isset( $this->cache[$key][$prop] ) ) {
+                       $this->ping( $key ); // push to top
+                       return $this->cache[$key][$prop];
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Clear one or several cache entries, or all cache entries
+        *
+        * @param $keys string|Array
+        * @return void
+        */
+       public function clear( $keys = null ) {
+               if ( $keys === null ) {
+                       $this->cache = array();
+               } else {
+                       foreach ( (array)$keys as $key ) {
+                               unset( $this->cache[$key] );
+                       }
+               }
+       }
+
+       /**
+        * Push an entry to the top of the cache
+        *
+        * @param $key string
+        */
+       protected function ping( $key ) {
+               $item = $this->cache[$key];
+               unset( $this->cache[$key] );
+               $this->cache[$key] = $item;
+       }
+}
index a3faebf..cc4ffaf 100644 (file)
@@ -271,7 +271,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool|null previous value of the flag
         */
-       function debug( $debug = null ) {
+       public function debug( $debug = null ) {
                return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
        }
 
@@ -297,7 +297,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return null|bool The previous value of the flag
         */
-       function bufferResults( $buffer = null ) {
+       public function bufferResults( $buffer = null ) {
                if ( is_null( $buffer ) ) {
                        return !(bool)( $this->mFlags & DBO_NOBUFFER );
                } else {
@@ -316,7 +316,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool The previous value of the flag.
         */
-       function ignoreErrors( $ignoreErrors = null ) {
+       public function ignoreErrors( $ignoreErrors = null ) {
                return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
        }
 
@@ -329,7 +329,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $level int An integer (0 or 1), or omitted to leave it unchanged.
         * @return int The previous value
         */
-       function trxLevel( $level = null ) {
+       public function trxLevel( $level = null ) {
                return wfSetVar( $this->mTrxLevel, $level );
        }
 
@@ -338,7 +338,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $count int The count to set, or omitted to leave it unchanged.
         * @return int The error count
         */
-       function errorCount( $count = null ) {
+       public function errorCount( $count = null ) {
                return wfSetVar( $this->mErrorCount, $count );
        }
 
@@ -347,7 +347,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $prefix string The table prefix to set, or omitted to leave it unchanged.
         * @return string The previous table prefix.
         */
-       function tablePrefix( $prefix = null ) {
+       public function tablePrefix( $prefix = null ) {
                return wfSetVar( $this->mTablePrefix, $prefix );
        }
 
@@ -360,7 +360,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return LoadBalancer|null
         */
-       function getLBInfo( $name = null ) {
+       public function getLBInfo( $name = null ) {
                if ( is_null( $name ) ) {
                        return $this->mLBInfo;
                } else {
@@ -380,7 +380,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $name
         * @param $value
         */
-       function setLBInfo( $name, $value = null ) {
+       public function setLBInfo( $name, $value = null ) {
                if ( is_null( $value ) ) {
                        $this->mLBInfo = $name;
                } else {
@@ -393,7 +393,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $lag int
         */
-       function setFakeSlaveLag( $lag ) {
+       public function setFakeSlaveLag( $lag ) {
                $this->mFakeSlaveLag = $lag;
        }
 
@@ -402,7 +402,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $enabled bool
         */
-       function setFakeMaster( $enabled = true ) {
+       public function setFakeMaster( $enabled = true ) {
                $this->mFakeMaster = $enabled;
        }
 
@@ -411,7 +411,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function cascadingDeletes() {
+       public function cascadingDeletes() {
                return false;
        }
 
@@ -420,7 +420,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function cleanupTriggers() {
+       public function cleanupTriggers() {
                return false;
        }
 
@@ -430,7 +430,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function strictIPs() {
+       public function strictIPs() {
                return false;
        }
 
@@ -439,7 +439,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
        */
-       function realTimestamps() {
+       public function realTimestamps() {
                return false;
        }
 
@@ -448,7 +448,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function implicitGroupby() {
+       public function implicitGroupby() {
                return true;
        }
 
@@ -458,7 +458,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function implicitOrderby() {
+       public function implicitOrderby() {
                return true;
        }
 
@@ -478,7 +478,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function searchableIPs() {
+       public function searchableIPs() {
                return false;
        }
 
@@ -487,7 +487,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function functionalIndexes() {
+       public function functionalIndexes() {
                return false;
        }
 
@@ -495,7 +495,7 @@ abstract class DatabaseBase implements DatabaseType {
         * Return the last query that went through DatabaseBase::query()
         * @return String
         */
-       function lastQuery() {
+       public function lastQuery() {
                return $this->mLastQuery;
        }
 
@@ -505,7 +505,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function doneWrites() {
+       public function doneWrites() {
                return $this->mDoneWrites;
        }
 
@@ -513,7 +513,7 @@ abstract class DatabaseBase implements DatabaseType {
         * Is a connection to the database open?
         * @return Boolean
         */
-       function isOpen() {
+       public function isOpen() {
                return $this->mOpened;
        }
 
@@ -529,7 +529,7 @@ abstract class DatabaseBase implements DatabaseType {
         *       and removes it in command line mode
         *   - DBO_PERSISTENT: use persistant database connection
         */
-       function setFlag( $flag ) {
+       public function setFlag( $flag ) {
                global $wgDebugDBTransactions;
                $this->mFlags |= $flag;
                if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) {
@@ -542,7 +542,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $flag: same as setFlag()'s $flag param
         */
-       function clearFlag( $flag ) {
+       public function clearFlag( $flag ) {
                global $wgDebugDBTransactions;
                $this->mFlags &= ~$flag;
                if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) {
@@ -556,7 +556,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $flag: same as setFlag()'s $flag param
         * @return Boolean
         */
-       function getFlag( $flag ) {
+       public function getFlag( $flag ) {
                return !!( $this->mFlags & $flag );
        }
 
@@ -567,14 +567,14 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function getProperty( $name ) {
+       public function getProperty( $name ) {
                return $this->$name;
        }
 
        /**
         * @return string
         */
-       function getWikiID() {
+       public function getWikiID() {
                if ( $this->mTablePrefix ) {
                        return "{$this->mDBname}-{$this->mTablePrefix}";
                } else {
@@ -811,7 +811,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function isWriteQuery( $sql ) {
+       public function isWriteQuery( $sql ) {
                return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
        }
 
@@ -957,7 +957,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $fname String
         * @param $tempIgnore Boolean
         */
-       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+       public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
                # Ignore errors during error handling to avoid infinite recursion
                $ignore = $this->ignoreErrors( true );
                ++$this->mErrorCount;
@@ -1013,7 +1013,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return ResultWrapper
         */
-       function execute( $prepared, $args = null ) {
+       public function execute( $prepared, $args = null ) {
                if ( !is_array( $args ) ) {
                        # Pull the var args
                        $args = func_get_args();
@@ -1060,7 +1060,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $args Array of arguments to fill it with
         * @return string executable SQL
         */
-       function fillPrepared( $preparedQuery, $args ) {
+       public function fillPrepared( $preparedQuery, $args ) {
                reset( $args );
                $this->preparedArgs =& $args;
 
@@ -1147,7 +1147,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool|mixed The value from the field, or false on failure.
         */
-       function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField',
+       public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField',
                $options = array() )
        {
                if ( !is_array( $options ) ) {
@@ -1180,7 +1180,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @return Array
         * @see DatabaseBase::select()
         */
-       function makeSelectOptions( $options ) {
+       public function makeSelectOptions( $options ) {
                $preLimitTail = $postLimitTail = '';
                $startOpts = '';
 
@@ -1405,7 +1405,7 @@ abstract class DatabaseBase implements DatabaseType {
         *   DBQueryError exception will be thrown, except if the "ignore errors"
         *   option was set, in which case false will be returned.
         */
-       function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+       public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
                $options = array(), $join_conds = array() ) {
                $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
 
@@ -1426,7 +1426,9 @@ abstract class DatabaseBase implements DatabaseType {
         * @return string SQL query string.
         * @see DatabaseBase::select()
         */
-       function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) {
+       public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+               $options = array(), $join_conds = array() )
+       {
                if ( is_array( $vars ) ) {
                        $vars = implode( ',', $vars );
                }
@@ -1491,7 +1493,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return object|bool
         */
-       function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow',
+       public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow',
                $options = array(), $join_conds = array() )
        {
                $options = (array)$options;
@@ -1581,7 +1583,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $fname String: calling function name (optional)
         * @return Boolean: whether $table has filed $field
         */
-       function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) {
+       public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) {
                $info = $this->fieldInfo( $table, $field );
 
                return (bool)$info;
@@ -1598,7 +1600,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool|null
         */
-       function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
+       public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
                $info = $this->indexInfo( $table, $index, $fname );
                if ( is_null( $info ) ) {
                        return null;
@@ -1615,7 +1617,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function tableExists( $table, $fname = __METHOD__ ) {
+       public function tableExists( $table, $fname = __METHOD__ ) {
                $table = $this->tableName( $table );
                $old = $this->ignoreErrors( true );
                $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
@@ -1630,7 +1632,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $index
         * @return string
         */
-       function fieldType( $res, $index ) {
+       public function fieldType( $res, $index ) {
                if ( $res instanceof ResultWrapper ) {
                        $res = $res->result;
                }
@@ -1646,7 +1648,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function indexUnique( $table, $index ) {
+       public function indexUnique( $table, $index ) {
                $indexInfo = $this->indexInfo( $table, $index );
 
                if ( !$indexInfo ) {
@@ -1699,7 +1701,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) {
+       public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) {
                # No rows to insert, easy just return now
                if ( !count( $a ) ) {
                        return true;
@@ -1813,7 +1815,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function makeList( $a, $mode = LIST_COMMA ) {
+       public function makeList( $a, $mode = LIST_COMMA ) {
                if ( !is_array( $a ) ) {
                        throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
                }
@@ -1878,7 +1880,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $subKey String: field name to match the sub-level keys to (eg 'pl_title')
         * @return Mixed: string SQL fragment, or false if no items in array.
         */
-       function makeWhereFrom2d( $data, $baseKey, $subKey ) {
+       public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
                $conds = array();
 
                foreach ( $data as $base => $sub ) {
@@ -1905,7 +1907,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $field
         * @return string
         */
-       function bitNot( $field ) {
+       public function bitNot( $field ) {
                return "(~$field)";
        }
 
@@ -1914,7 +1916,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $fieldRight
         * @return string
         */
-       function bitAnd( $fieldLeft, $fieldRight ) {
+       public function bitAnd( $fieldLeft, $fieldRight ) {
                return "($fieldLeft & $fieldRight)";
        }
 
@@ -1923,7 +1925,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param  $fieldRight
         * @return string
         */
-       function bitOr( $fieldLeft, $fieldRight ) {
+       public function bitOr( $fieldLeft, $fieldRight ) {
                return "($fieldLeft | $fieldRight)";
        }
 
@@ -1936,7 +1938,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool Success or failure
         */
-       function selectDB( $db ) {
+       public function selectDB( $db ) {
                # Stub.  Shouldn't cause serious problems if it's not overridden, but
                # if your database engine supports a concept similar to MySQL's
                # databases you may as well.
@@ -1947,14 +1949,14 @@ abstract class DatabaseBase implements DatabaseType {
        /**
         * Get the current DB name
         */
-       function getDBname() {
+       public function getDBname() {
                return $this->mDBname;
        }
 
        /**
         * Get the server hostname or IP address
         */
-       function getServer() {
+       public function getServer() {
                return $this->mServer;
        }
 
@@ -1975,7 +1977,7 @@ abstract class DatabaseBase implements DatabaseType {
         *   raw - Do not add identifier quotes to the table name
         * @return String: full database name
         */
-       function tableName( $name, $format = 'quoted' ) {
+       public function tableName( $name, $format = 'quoted' ) {
                global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
                # Skip the entire process when we have a string quoted on both ends.
                # Note that we check the end so that we will still quote any use of
@@ -2209,7 +2211,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function addQuotes( $s ) {
+       public function addQuotes( $s ) {
                if ( $s === null ) {
                        return 'NULL';
                } else {
@@ -2301,7 +2303,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @since 1.16
         * @return String: fully built LIKE statement
         */
-       function buildLike() {
+       public function buildLike() {
                $params = func_get_args();
 
                if ( count( $params ) > 0 && is_array( $params[0] ) ) {
@@ -2326,7 +2328,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return LikeMatch
         */
-       function anyChar() {
+       public function anyChar() {
                return new LikeMatch( '_' );
        }
 
@@ -2335,7 +2337,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return LikeMatch
         */
-       function anyString() {
+       public function anyString() {
                return new LikeMatch( '%' );
        }
 
@@ -2350,7 +2352,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $seqName string
         * @return null
         */
-       function nextSequenceValue( $seqName ) {
+       public function nextSequenceValue( $seqName ) {
                return null;
        }
 
@@ -2364,7 +2366,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $index
         * @return string
         */
-       function useIndexClause( $index ) {
+       public function useIndexClause( $index ) {
                return '';
        }
 
@@ -2390,7 +2392,7 @@ abstract class DatabaseBase implements DatabaseType {
         *    a field name or an array of field names
         * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
         */
-       function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) {
+       public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) {
                $quotedTable = $this->tableName( $table );
 
                if ( count( $rows ) == 0 ) {
@@ -2491,7 +2493,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $fname      String: Calling function name (use __METHOD__) for
         *                    logs/profiling
         */
-       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
+       public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
                $fname = 'DatabaseBase::deleteJoin' )
        {
                if ( !$conds ) {
@@ -2518,7 +2520,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return int
         */
-       function textFieldSize( $table, $field ) {
+       public function textFieldSize( $table, $field ) {
                $table = $this->tableName( $table );
                $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
                $res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
@@ -2543,7 +2545,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @return string Returns the text of the low priority option if it is
         *   supported, or a blank string otherwise
         */
-       function lowPriorityOption() {
+       public function lowPriorityOption() {
                return '';
        }
 
@@ -2557,7 +2559,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) {
+       public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) {
                if ( !$conds ) {
                        throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
                }
@@ -2598,7 +2600,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return ResultWrapper
         */
-       function insertSelect( $destTable, $srcTable, $varMap, $conds,
+       public function insertSelect( $destTable, $srcTable, $varMap, $conds,
                $fname = 'DatabaseBase::insertSelect',
                $insertOptions = array(), $selectOptions = array() )
        {
@@ -2656,7 +2658,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function limitResult( $sql, $limit, $offset = false ) {
+       public function limitResult( $sql, $limit, $offset = false ) {
                if ( !is_numeric( $limit ) ) {
                        throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
                }
@@ -2680,7 +2682,7 @@ abstract class DatabaseBase implements DatabaseType {
         * within the UNION construct.
         * @return Boolean
         */
-       function unionSupportsOrderAndLimit() {
+       public function unionSupportsOrderAndLimit() {
                return true; // True for almost every DB supported
        }
 
@@ -2692,7 +2694,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $all Boolean: use UNION ALL
         * @return String: SQL fragment
         */
-       function unionQueries( $sqls, $all ) {
+       public function unionQueries( $sqls, $all ) {
                $glue = $all ? ') UNION ALL (' : ') UNION (';
                return '(' . implode( $glue, $sqls ) . ')';
        }
@@ -2706,7 +2708,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $falseVal String: SQL expression to return if false
         * @return String: SQL fragment
         */
-       function conditional( $cond, $trueVal, $falseVal ) {
+       public function conditional( $cond, $trueVal, $falseVal ) {
                return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
        }
 
@@ -2720,7 +2722,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function strreplace( $orig, $old, $new ) {
+       public function strreplace( $orig, $old, $new ) {
                return "REPLACE({$orig}, {$old}, {$new})";
        }
 
@@ -2730,7 +2732,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return int
         */
-       function getServerUptime() {
+       public function getServerUptime() {
                return 0;
        }
 
@@ -2740,7 +2742,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasDeadlock() {
+       public function wasDeadlock() {
                return false;
        }
 
@@ -2750,7 +2752,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasLockTimeout() {
+       public function wasLockTimeout() {
                return false;
        }
 
@@ -2761,7 +2763,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasErrorReissuable() {
+       public function wasErrorReissuable() {
                return false;
        }
 
@@ -2771,7 +2773,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function wasReadOnlyError() {
+       public function wasReadOnlyError() {
                return false;
        }
 
@@ -2793,8 +2795,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool
         */
-       function deadlockLoop() {
-
+       public function deadlockLoop() {
                $this->begin( __METHOD__ );
                $args = func_get_args();
                $function = array_shift( $args );
@@ -2846,7 +2847,7 @@ abstract class DatabaseBase implements DatabaseType {
         *   greater than zero if we waited for some period of time, less than
         *   zero if we timed out.
         */
-       function masterPosWait( DBMasterPos $pos, $timeout ) {
+       public function masterPosWait( DBMasterPos $pos, $timeout ) {
                wfProfileIn( __METHOD__ );
 
                if ( !is_null( $this->mFakeSlaveLag ) ) {
@@ -2879,7 +2880,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return DBMasterPos, or false if this is not a slave.
         */
-       function getSlavePos() {
+       public function getSlavePos() {
                if ( !is_null( $this->mFakeSlaveLag ) ) {
                        $pos = new MySQLMasterPos( 'fake', microtime( true ) - $this->mFakeSlaveLag );
                        wfDebug( __METHOD__ . ": fake slave pos = $pos\n" );
@@ -2895,7 +2896,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return DBMasterPos, or false if this is not a master
         */
-       function getMasterPos() {
+       public function getMasterPos() {
                if ( $this->mFakeMaster ) {
                        return new MySQLMasterPos( 'fake', microtime( true ) );
                } else {
@@ -2908,7 +2909,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $fname string
         */
-       function begin( $fname = 'DatabaseBase::begin' ) {
+       public function begin( $fname = 'DatabaseBase::begin' ) {
                $this->query( 'BEGIN', $fname );
                $this->mTrxLevel = 1;
        }
@@ -2918,7 +2919,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $fname string
         */
-       function commit( $fname = 'DatabaseBase::commit' ) {
+       public function commit( $fname = 'DatabaseBase::commit' ) {
                if ( $this->mTrxLevel ) {
                        $this->query( 'COMMIT', $fname );
                        $this->mTrxLevel = 0;
@@ -2931,7 +2932,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $fname string
         */
-       function rollback( $fname = 'DatabaseBase::rollback' ) {
+       public function rollback( $fname = 'DatabaseBase::rollback' ) {
                if ( $this->mTrxLevel ) {
                        $this->query( 'ROLLBACK', $fname, true );
                        $this->mTrxLevel = 0;
@@ -2952,7 +2953,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $fname String: calling function name
         * @return Boolean: true if operation was successful
         */
-       function duplicateTableStructure( $oldName, $newName, $temporary = false,
+       public function duplicateTableStructure( $oldName, $newName, $temporary = false,
                $fname = 'DatabaseBase::duplicateTableStructure' )
        {
                throw new MWException(
@@ -2980,7 +2981,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function timestamp( $ts = 0 ) {
+       public function timestamp( $ts = 0 ) {
                return wfTimestamp( TS_MW, $ts );
        }
 
@@ -2997,7 +2998,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return string
         */
-       function timestampOrNull( $ts = null ) {
+       public function timestampOrNull( $ts = null ) {
                if ( is_null( $ts ) ) {
                        return null;
                } else {
@@ -3020,7 +3021,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool|ResultWrapper
         */
-       function resultObject( $result ) {
+       public function resultObject( $result ) {
                if ( empty( $result ) ) {
                        return false;
                } elseif ( $result instanceof ResultWrapper ) {
@@ -3050,7 +3051,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return bool Success or failure
         */
-       function ping() {
+       public function ping() {
                # Stub.  Not essential to override.
                return true;
        }
@@ -3064,7 +3065,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @return int Database replication lag in seconds
         */
-       function getLag() {
+       public function getLag() {
                return intval( $this->mFakeSlaveLag );
        }
 
@@ -3085,7 +3086,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $b string
         * @return string
         */
-       function encodeBlob( $b ) {
+       public function encodeBlob( $b ) {
                return $b;
        }
 
@@ -3096,7 +3097,7 @@ abstract class DatabaseBase implements DatabaseType {
         * @param $b string
         * @return string
         */
-       function decodeBlob( $b ) {
+       public function decodeBlob( $b ) {
                return $b;
        }
 
@@ -3137,7 +3138,9 @@ abstract class DatabaseBase implements DatabaseType {
         *      generated dynamically using $filename
         * @return bool|string
         */
-       function sourceFile( $filename, $lineCallback = false, $resultCallback = false, $fname = false ) {
+       public function sourceFile(
+               $filename, $lineCallback = false, $resultCallback = false, $fname = false
+       ) {
                wfSuppressWarnings();
                $fp = fopen( $filename, 'r' );
                wfRestoreWarnings();
@@ -3189,7 +3192,7 @@ abstract class DatabaseBase implements DatabaseType {
         *
         * @param $vars bool|array mapping variable name to value.
         */
-       function setSchemaVars( $vars ) {
+       public function setSchemaVars( $vars ) {
                $this->mSchemaVars = $vars;
        }
 
index 8ce6e70..e1b3a9a 100644 (file)
@@ -810,7 +810,7 @@ class DatabaseOracle extends DatabaseBase {
        /**
         * Return aggregated value function call
         */
-       function aggregateValue ( $valuedata, $valuename = 'value' ) {
+       public function aggregateValue( $valuedata, $valuename = 'value' ) {
                return $valuedata;
        }
 
index 763ca52..2b7521f 100644 (file)
@@ -1037,7 +1037,7 @@ __INDEXATTR__;
        /**
         * Return aggregated value function call
         */
-       function aggregateValue( $valuedata, $valuename = 'value' ) {
+       public function aggregateValue( $valuedata, $valuename = 'value' ) {
                return $valuedata;
        }
 
index c616e16..dd0c947 100644 (file)
@@ -235,7 +235,8 @@ class LocalRepo extends FileRepo {
                        'image',
                        LocalFile::selectFields(),
                        array( 'img_sha1' => $hash ),
-                       __METHOD__
+                       __METHOD__,
+                       array( 'ORDER BY' => 'img_name' )
                );
                
                $result = array();
index 709655a..6b31b7e 100644 (file)
@@ -259,6 +259,7 @@ class RepoGroup {
                foreach ( $this->foreignRepos as $repo ) {
                        $result = array_merge( $result, $repo->findBySha1( $hash ) );
                }
+               usort( $result, 'File::compare' );
                return $result;
        }
 
index 9cf8f94..1b2fbf0 100644 (file)
@@ -74,19 +74,19 @@ abstract class FileBackend {
         * This should only be called from within FileBackendGroup.
         *
         * $config includes:
-        *     'name'        : The unique name of this backend.
-        *                     This should consist of alphanumberic, '-', and '_' characters.
-        *                     This name should not be changed after use.
-        *     'wikiId'      : Prefix to container names that is unique to this wiki.
-        *                     It should only consist of alphanumberic, '-', and '_' characters.
-        *     'lockManager' : Registered name of a file lock manager to use.
-        *     'fileJournal' : File journal configuration; see FileJournal::factory().
-        *                     Journals simply log changes to files stored in the backend.
-        *     'readOnly'    : Write operations are disallowed if this is a non-empty string.
-        *                     It should be an explanation for the backend being read-only.
-        *     'parallelize' : When to do file operations in parallel (when possible).
-        *                     Allowed values are "implicit", "explicit" and "off".
-        *     'concurrency' : How many file operations can be done in parallel.
+        *   - name        : The unique name of this backend.
+        *                   This should consist of alphanumberic, '-', and '_' characters.
+        *                   This name should not be changed after use.
+        *   - wikiId      : Prefix to container names that is unique to this wiki.
+        *                   It should only consist of alphanumberic, '-', and '_' characters.
+        *   - lockManager : Registered name of a file lock manager to use.
+        *   - fileJournal : File journal configuration; see FileJournal::factory().
+        *                   Journals simply log changes to files stored in the backend.
+        *   - readOnly    : Write operations are disallowed if this is a non-empty string.
+        *                   It should be an explanation for the backend being read-only.
+        *   - parallelize : When to do file operations in parallel (when possible).
+        *                   Allowed values are "implicit", "explicit" and "off".
+        *   - concurrency : How many file operations can be done in parallel.
         *
         * @param $config Array
         * @throws MWException
@@ -103,7 +103,9 @@ abstract class FileBackend {
                        ? $config['lockManager']
                        : LockManagerGroup::singleton()->get( $config['lockManager'] );
                $this->fileJournal = isset( $config['fileJournal'] )
-                       ? FileJournal::factory( $config['fileJournal'], $this->name )
+                       ? ( ( $config['fileJournal'] instanceof FileJournal )
+                               ? $config['fileJournal']
+                               : FileJournal::factory( $config['fileJournal'], $this->name ) )
                        : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name );
                $this->readOnly = isset( $config['readOnly'] )
                        ? (string)$config['readOnly']
@@ -223,26 +225,27 @@ abstract class FileBackend {
         * @endcode
         *
         * Boolean flags for operations (operation-specific):
-        * 'ignoreMissingSource' : The operation will simply succeed and do
-        *                         nothing if the source file does not exist.
-        * 'overwrite'           : Any destination file will be overwritten.
-        * 'overwriteSame'       : An error will not be given if a file already
-        *                         exists at the destination that has the same
-        *                         contents as the new contents to be written there.
+        *   - ignoreMissingSource : The operation will simply succeed and do
+        *                           nothing if the source file does not exist.
+        *   - overwrite           : Any destination file will be overwritten.
+        *   - overwriteSame       : An error will not be given if a file already
+        *                           exists at the destination that has the same
+        *                           contents as the new contents to be written there.
         *
         * $opts is an associative of boolean flags, including:
-        * 'force'               : Operation precondition errors no longer trigger an abort.
-        *                         Any remaining operations are still attempted. Unexpected
-        *                         failures may still cause remaning operations to be aborted.
-        * 'nonLocking'          : No locks are acquired for the operations.
-        *                         This can increase performance for non-critical writes.
-        *                         This has no effect unless the 'force' flag is set.
-        * 'allowStale'          : Don't require the latest available data.
-        *                         This can increase performance for non-critical writes.
-        *                         This has no effect unless the 'force' flag is set.
-        * 'nonJournaled'        : Don't log this operation batch in the file journal.
-        *                         This limits the ability of recovery scripts.
-        * 'parallelize'         : Try to do operations in parallel when possible.
+        *   - force               : Operation precondition errors no longer trigger an abort.
+        *                           Any remaining operations are still attempted. Unexpected
+        *                           failures may still cause remaning operations to be aborted.
+        *   - nonLocking          : No locks are acquired for the operations.
+        *                           This can increase performance for non-critical writes.
+        *                           This has no effect unless the 'force' flag is set.
+        *   - allowStale          : Don't require the latest available data.
+        *                           This can increase performance for non-critical writes.
+        *                           This has no effect unless the 'force' flag is set.
+        *   - nonJournaled        : Don't log this operation batch in the file journal.
+        *                           This limits the ability of recovery scripts.
+        *   - parallelize         : Try to do operations in parallel when possible.
+        *   - bypassReadOnly      : Allow writes in read-only mode (since 1.20).
         *
         * @remarks Remarks on locking:
         * File system paths given to operations should refer to files that are
@@ -264,7 +267,7 @@ abstract class FileBackend {
         * @return Status
         */
        final public function doOperations( array $ops, array $opts = array() ) {
-               if ( $this->isReadOnly() ) {
+               if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                if ( empty( $opts['force'] ) ) { // sanity
@@ -388,6 +391,7 @@ abstract class FileBackend {
         *  - move
         *  - delete
         *  - null
+        *
         * a) Create a new file in storage with the contents of a string
         * @code
         *     array(
@@ -436,8 +440,11 @@ abstract class FileBackend {
         * @endcode
         *
         * @par Boolean flags for operations (operation-specific):
-        * 'ignoreMissingSource' : The operation will simply succeed and do
-        *                         nothing if the source file does not exist.
+        *   - ignoreMissingSource : The operation will simply succeed and do
+        *                           nothing if the source file does not exist.
+        *
+        * $opts is an associative of boolean flags, including:
+        *   - bypassReadOnly      : Allow writes in read-only mode (since 1.20)
         *
         * @par Return value:
         * This returns a Status, which contains all warnings and fatals that occured
@@ -446,11 +453,12 @@ abstract class FileBackend {
         * considered "OK" as long as no fatal errors occured.
         *
         * @param $ops Array Set of operations to execute
+        * @param $opts Array Batch operation options
         * @return Status
         * @since 1.20
         */
-       final public function doQuickOperations( array $ops ) {
-               if ( $this->isReadOnly() ) {
+       final public function doQuickOperations( array $ops, array $opts = array() ) {
+               if ( empty( $opts['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                foreach ( $ops as &$op ) {
@@ -573,15 +581,16 @@ abstract class FileBackend {
         * These flags should always be set for directories that have private files.
         *
         * $params include:
-        *     dir       : storage directory
-        *     noAccess  : try to deny file access (@since 1.20)
-        *     noListing : try to deny file listing (@since 1.20)
+        *   - dir            : storage directory
+        *   - noAccess       : try to deny file access (since 1.20)
+        *   - noListing      : try to deny file listing (since 1.20)
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
         *
         * @param $params Array
         * @return Status
         */
        final public function prepare( array $params ) {
-               if ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doPrepare( $params );
@@ -601,13 +610,14 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *   - dir       : storage directory
-        *   - noAccess  : try to deny file access
-        *   - noListing : try to deny file listing
+        *   - dir            : storage directory
+        *   - noAccess       : try to deny file access
+        *   - noListing      : try to deny file listing
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
         * @return Status
         */
        final public function secure( array $params ) {
-               if ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doSecure( $params );
@@ -626,16 +636,17 @@ abstract class FileBackend {
         * This essentially can undo the result of secure() calls.
         *
         * $params include:
-        *     dir     : storage directory
-        *     access  : try to allow file access
-        *     listing : try to allow file listing
+        *   - dir            : storage directory
+        *   - access         : try to allow file access
+        *   - listing        : try to allow file listing
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
         *
         * @param $params Array
         * @return Status
         * @since 1.20
         */
        final public function publish( array $params ) {
-               if ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doPublish( $params );
@@ -653,12 +664,13 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     dir       : storage directory
-        *     recursive : recursively delete empty subdirectories first (@since 1.20)
+        *   - dir            : storage directory
+        *   - recursive      : recursively delete empty subdirectories first (since 1.20)
+        *   - bypassReadOnly : allow writes in read-only mode (since 1.20)
         * @return Status
         */
        final public function clean( array $params ) {
-               if ( $this->isReadOnly() ) {
+               if ( empty( $params['bypassReadOnly'] ) && $this->isReadOnly() ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
                return $this->doClean( $params );
@@ -675,8 +687,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return bool|null Returns null on failure
         */
        abstract public function fileExists( array $params );
@@ -686,8 +698,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return string|bool TS_MW timestamp or false on failure
         */
        abstract public function getFileTimestamp( array $params );
@@ -698,8 +710,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return string|bool Returns false on failure
         */
        abstract public function getFileContents( array $params );
@@ -709,8 +721,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return integer|bool Returns false on failure
         */
        abstract public function getFileSize( array $params );
@@ -719,14 +731,14 @@ abstract class FileBackend {
         * Get quick information about a file at a storage path in the backend.
         * If the file does not exist, then this returns false.
         * Otherwise, the result is an associative array that includes:
-        *     mtime  : the last-modified timestamp (TS_MW)
-        *     size   : the file size (bytes)
+        *   - mtime  : the last-modified timestamp (TS_MW)
+        *   - size   : the file size (bytes)
         * Additional values may be included for internal use only.
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return Array|bool|null Returns null on failure
         */
        abstract public function getFileStat( array $params );
@@ -736,8 +748,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return string|bool Hash string or false on failure
         */
        abstract public function getFileSha1Base36( array $params );
@@ -748,8 +760,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return Array
         */
        abstract public function getFileProps( array $params );
@@ -763,9 +775,9 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src     : source storage path
-        *     headers : additional HTTP headers to send on success
-        *     latest  : use the latest available data
+        *   - src     : source storage path
+        *   - headers : additional HTTP headers to send on success
+        *   - latest  : use the latest available data
         * @return Status
         */
        abstract public function streamFile( array $params );
@@ -785,8 +797,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return FSFile|null Returns null on failure
         */
        abstract public function getLocalReference( array $params );
@@ -798,8 +810,8 @@ abstract class FileBackend {
         *
         * @param $params Array
         * $params include:
-        *     src    : source storage path
-        *     latest : use the latest available data
+        *   - src    : source storage path
+        *   - latest : use the latest available data
         * @return TempFSFile|null Returns null on failure
         */
        abstract public function getLocalCopy( array $params );
@@ -813,7 +825,7 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir : storage directory
+        *   - dir : storage directory
         * @return bool|null Returns null on failure
         * @since 1.20
         */
@@ -831,8 +843,8 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir     : storage directory
-        *     topOnly : only return direct child dirs of the directory
+        *   - dir     : storage directory
+        *   - topOnly : only return direct child dirs of the directory
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
@@ -846,7 +858,7 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir : storage directory
+        *   - dir : storage directory
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
@@ -866,8 +878,8 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir     : storage directory
-        *     topOnly : only return direct child files of the directory (@since 1.20)
+        *   - dir     : storage directory
+        *   - topOnly : only return direct child files of the directory (since 1.20)
         * @return Traversable|Array|null Returns null on failure
         */
        abstract public function getFileList( array $params );
@@ -880,7 +892,7 @@ abstract class FileBackend {
         *
         * @param $params array
         * $params include:
-        *     dir : storage directory
+        *   - dir : storage directory
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
index 2fc9d8e..c307759 100644 (file)
@@ -48,9 +48,12 @@ class FileBackendMultiWrite extends FileBackend {
        /* Possible internal backend consistency checks */
        const CHECK_SIZE = 1;
        const CHECK_TIME = 2;
+       const CHECK_SHA1 = 4;
 
        /**
         * Construct a proxy backend that consists of several internal backends.
+        * Locking, journaling, and read-only checks are handled by the proxy backend.
+        *
         * Additional $config params include:
         *     'backends'    : Array of backend config and multi-backend settings.
         *                     Each value is the config used in the constructor of a
@@ -61,9 +64,11 @@ class FileBackendMultiWrite extends FileBackend {
         *                                           the config of that backend as a template.
         *                                           Values specified here take precedence.
         *     'syncChecks'  : Integer bitfield of internal backend sync checks to perform.
-        *                     Possible bits include self::CHECK_SIZE and self::CHECK_TIME.
+        *                     Possible bits include the FileBackendMultiWrite::CHECK_* constants.
+        *                     There are constants for SIZE, TIME, and SHA1.
         *                     The checks are done before allowing any file operations.
         * @param $config Array
+        * @throws MWException
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
@@ -81,17 +86,23 @@ class FileBackendMultiWrite extends FileBackend {
                                throw new MWException( "Two or more backends defined with the name $name." );
                        }
                        $namesUsed[$name] = 1;
-                       if ( !isset( $config['class'] ) ) {
-                               throw new MWException( 'No class given for a backend config.' );
-                       }
-                       $class = $config['class'];
-                       $this->backends[$index] = new $class( $config );
+                       // Alter certain sub-backend settings for sanity
+                       unset( $config['readOnly'] ); // use proxy backend setting
+                       unset( $config['fileJournal'] ); // use proxy backend journal
+                       $config['lockManager'] = 'nullLockManager'; // lock under proxy backend
                        if ( !empty( $config['isMultiMaster'] ) ) {
                                if ( $this->masterIndex >= 0 ) {
                                        throw new MWException( 'More than one master backend defined.' );
                                }
-                               $this->masterIndex = $index;
+                               $this->masterIndex = $index; // this is the "master"
+                               $config['fileJournal'] = $this->fileJournal; // log under proxy backend
                        }
+                       // Create sub-backend object
+                       if ( !isset( $config['class'] ) ) {
+                               throw new MWException( 'No class given for a backend config.' );
+                       }
+                       $class = $config['class'];
+                       $this->backends[$index] = new $class( $config );
                }
                if ( $this->masterIndex < 0 ) { // need backends and must have a master
                        throw new MWException( 'No master backend defined.' );
@@ -108,27 +119,14 @@ class FileBackendMultiWrite extends FileBackend {
        final protected function doOperationsInternal( array $ops, array $opts ) {
                $status = Status::newGood();
 
-               $performOps = array(); // list of FileOp objects
-               $paths = array(); // storage paths read from or written to
-               // Build up a list of FileOps. The list will have all the ops
-               // for one backend, then all the ops for the next, and so on.
-               // These batches of ops are all part of a continuous array.
-               // Also build up a list of files read/changed...
-               foreach ( $this->backends as $index => $backend ) {
-                       $backendOps = $this->substOpBatchPaths( $ops, $backend );
-                       // Add on the operation batch for this backend
-                       $performOps = array_merge( $performOps,
-                               $backend->getOperationsInternal( $backendOps ) );
-                       if ( $index == 0 ) { // first batch
-                               // Get the files used for these operations. Each backend has a batch of
-                               // the same operations, so we only need to get them from the first batch.
-                               $paths = $backend->getPathsToLockForOpsInternal( $performOps );
-                               // Get the paths under the proxy backend's name
-                               $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
-                               $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
-                       }
-               }
+               $mbe = $this->backends[$this->masterIndex]; // convenience
 
+               // Get the paths to lock from the master backend
+               $realOps = $this->substOpBatchPaths( $ops, $mbe );
+               $paths = $mbe->getPathsToLockForOpsInternal( $mbe->getOperationsInternal( $realOps ) );
+               // Get the paths under the proxy backend's name
+               $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
+               $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
                // Try to lock those files for the scope of this function...
                if ( empty( $opts['nonLocking'] ) ) {
                        // Try to lock those files for the scope of this function...
@@ -138,46 +136,29 @@ class FileBackendMultiWrite extends FileBackend {
                                return $status; // abort
                        }
                }
-
                // Clear any cache entries (after locks acquired)
                $this->clearCache();
-
                // Do a consistency check to see if the backends agree
-               if ( count( $this->backends ) > 1 ) {
-                       $status->merge( $this->consistencyCheck( array_merge( $paths['sh'], $paths['ex'] ) ) );
-                       if ( !$status->isOK() ) {
-                               return $status; // abort
+               $status->merge( $this->consistencyCheck( array_merge( $paths['sh'], $paths['ex'] ) ) );
+               if ( !$status->isOK() ) {
+                       return $status; // abort
+               }
+               // Actually attempt the operation batch on the master backend...
+               $masterStatus = $mbe->doOperations( $realOps, $opts );
+               $status->merge( $masterStatus );
+               // Propagate the operations to the clone backends...
+               foreach ( $this->backends as $index => $backend ) {
+                       if ( $index !== $this->masterIndex ) { // not done already
+                               $realOps = $this->substOpBatchPaths( $ops, $backend );
+                               $status->merge( $backend->doOperations( $realOps, $opts ) );
                        }
                }
-
-               // Actually attempt the operation batch...
-               $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
-
-               $success = array();
-               $failCount = 0;
-               $successCount = 0;
                // Make 'success', 'successCount', and 'failCount' fields reflect
                // the overall operation, rather than all the batches for each backend.
                // Do this by only using success values from the master backend's batch.
-               $batchStart = $this->masterIndex * count( $ops );
-               $batchEnd = $batchStart + count( $ops ) - 1;
-               for ( $i = $batchStart; $i <= $batchEnd; $i++ ) {
-                       if ( !isset( $subStatus->success[$i] ) ) {
-                               break; // failed out before trying this op
-                       } elseif ( $subStatus->success[$i] ) {
-                               ++$successCount;
-                       } else {
-                               ++$failCount;
-                       }
-                       $success[] = $subStatus->success[$i];
-               }
-               $subStatus->success = $success;
-               $subStatus->successCount = $successCount;
-               $subStatus->failCount = $failCount;
-
-               // Merge errors into status fields
-               $status->merge( $subStatus );
-               $status->success = $subStatus->success; // not done in merge()
+               $status->success = $masterStatus->success;
+               $status->successCount = $masterStatus->successCount;
+               $status->failCount = $masterStatus->failCount;
 
                return $status;
        }
@@ -190,21 +171,29 @@ class FileBackendMultiWrite extends FileBackend {
         */
        public function consistencyCheck( array $paths ) {
                $status = Status::newGood();
-               if ( $this->syncChecks == 0 ) {
+               if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
                        return $status; // skip checks
                }
 
                $mBackend = $this->backends[$this->masterIndex];
                foreach ( array_unique( $paths ) as $path ) {
                        $params = array( 'src' => $path, 'latest' => true );
+                       $mParams = $this->substOpPaths( $params, $mBackend );
                        // Stat the file on the 'master' backend
-                       $mStat = $mBackend->getFileStat( $this->substOpPaths( $params, $mBackend ) );
+                       $mStat = $mBackend->getFileStat( $mParams );
+                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
+                               $mSha1 = $mBackend->getFileSha1( $mParams );
+                       } else {
+                               $mSha1 = false;
+                       }
+                       $mUsable = $mBackend->isPathUsableInternal( $mParams['src'] );
                        // Check of all clone backends agree with the master...
                        foreach ( $this->backends as $index => $cBackend ) {
                                if ( $index === $this->masterIndex ) {
                                        continue; // master
                                }
-                               $cStat = $cBackend->getFileStat( $this->substOpPaths( $params, $cBackend ) );
+                               $cParams = $this->substOpPaths( $params, $cBackend );
+                               $cStat = $cBackend->getFileStat( $cParams );
                                if ( $mStat ) { // file is in master
                                        if ( !$cStat ) { // file should exist
                                                $status->fatal( 'backend-fail-synced', $path );
@@ -224,11 +213,20 @@ class FileBackendMultiWrite extends FileBackend {
                                                        continue;
                                                }
                                        }
+                                       if ( $this->syncChecks & self::CHECK_SHA1 ) {
+                                               if ( $cBackend->getFileSha1( $cParams ) !== $mSha1 ) { // wrong SHA1
+                                                       $status->fatal( 'backend-fail-synced', $path );
+                                                       continue;
+                                               }
+                                       }
                                } else { // file is not in master
                                        if ( $cStat ) { // file should not exist
                                                $status->fatal( 'backend-fail-synced', $path );
                                        }
                                }
+                               if ( $mUsable !== $cBackend->isPathUsableInternal( $cParams['src'] ) ) {
+                                       $status->fatal( 'backend-fail-synced', $path );
+                               }
                        }
                }
 
@@ -302,10 +300,12 @@ class FileBackendMultiWrite extends FileBackend {
         * @see FileBackend::doQuickOperationsInternal()
         * @return Status
         */
-       public function doQuickOperationsInternal( array $ops ) {
+       protected function doQuickOperationsInternal( array $ops ) {
+               $status = Status::newGood();
                // Do the operations on the master backend; setting Status fields...
                $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
-               $status = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
+               $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
+               $status->merge( $masterStatus );
                // Propagate the operations to the clone backends...
                foreach ( $this->backends as $index => $backend ) {
                        if ( $index !== $this->masterIndex ) { // not done already
@@ -313,6 +313,12 @@ class FileBackendMultiWrite extends FileBackend {
                                $status->merge( $backend->doQuickOperations( $realOps ) );
                        }
                }
+               // Make 'success', 'successCount', and 'failCount' fields reflect
+               // the overall operation, rather than all the batches for each backend.
+               // Do this by only using success values from the master backend's batch.
+               $status->success = $masterStatus->success;
+               $status->successCount = $masterStatus->successCount;
+               $status->failCount = $masterStatus->failCount;
                return $status;
        }
 
index 9c713d3..4a9cff2 100644 (file)
 abstract class FileBackendStore extends FileBackend {
        /** @var BagOStuff */
        protected $memCache;
-
-       /** @var Array Map of paths to small (RAM/disk) cache items */
-       protected $cache = array(); // (storage path => key => value)
-       protected $maxCacheSize = 300; // integer; max paths with entries
-       /** @var Array Map of paths to large (RAM/disk) cache items */
-       protected $expensiveCache = array(); // (storage path => key => value)
-       protected $maxExpensiveCacheSize = 5; // integer; max paths with entries
+       /** @var ProcessCacheLRU */
+       protected $cheapCache; // Map of paths to small (RAM/disk) cache items
+       /** @var ProcessCacheLRU */
+       protected $expensiveCache; // Map of paths to large (RAM/disk) cache items
 
        /** @var Array Map of container names to sharding settings */
        protected $shardViaHashLevels = array(); // (container name => config array)
@@ -58,7 +55,9 @@ abstract class FileBackendStore extends FileBackend {
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
-               $this->memCache = new EmptyBagOStuff(); // disabled by default
+               $this->memCache       = new EmptyBagOStuff(); // disabled by default
+               $this->cheapCache     = new ProcessCacheLRU( 300 );
+               $this->expensiveCache = new ProcessCacheLRU( 5 );
        }
 
        /**
@@ -579,17 +578,17 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( !isset( $this->cache[$path]['stat'] ) ) {
+               if ( !$this->cheapCache->has( $path, 'stat' ) ) {
                        $this->primeFileCache( array( $path ) ); // check persistent cache
                }
-               if ( isset( $this->cache[$path]['stat'] ) ) {
+               if ( $this->cheapCache->has( $path, 'stat' ) ) {
+                       $stat = $this->cheapCache->get( $path, 'stat' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
-                       if ( !$latest || $this->cache[$path]['stat']['latest'] ) {
-                               $this->pingCache( $path ); // LRU
+                       if ( !$latest || $stat['latest'] ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
-                               return $this->cache[$path]['stat'];
+                               return $stat;
                        }
                }
                wfProfileIn( __METHOD__ . '-miss' );
@@ -599,13 +598,11 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileOut( __METHOD__ . '-miss' );
                if ( is_array( $stat ) ) { // don't cache negatives
                        $stat['latest'] = $latest;
-                       $this->trimCache(); // limit memory
-                       $this->cache[$path]['stat'] = $stat;
+                       $this->cheapCache->set( $path, 'stat', $stat );
                        $this->setFileCache( $path, $stat ); // update persistent cache
                        if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                               $this->trimCache(); // limit memory
-                               $this->cache[$path]['sha1'] =
-                                       array( 'hash' => $stat['sha1'], 'latest' => $latest );
+                               $this->cheapCache->set( $path, 'sha1',
+                                       array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
                        }
                } else {
                        wfDebug( __METHOD__ . ": File $path does not exist.\n" );
@@ -653,14 +650,14 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( isset( $this->cache[$path]['sha1'] ) ) {
+               if ( $this->cheapCache->has( $path, 'sha1' ) ) {
+                       $stat = $this->cheapCache->get( $path, 'sha1' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
-                       if ( !$latest || $this->cache[$path]['sha1']['latest'] ) {
-                               $this->pingCache( $path ); // LRU
+                       if ( !$latest || $stat['latest'] ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
-                               return $this->cache[$path]['sha1']['hash'];
+                               return $stat['hash'];
                        }
                }
                wfProfileIn( __METHOD__ . '-miss' );
@@ -669,8 +666,8 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
                if ( $hash ) { // don't cache negatives
-                       $this->trimCache(); // limit memory
-                       $this->cache[$path]['sha1'] = array( 'hash' => $hash, 'latest' => $latest );
+                       $this->cheapCache->set( $path, 'sha1',
+                               array( 'hash' => $hash, 'latest' => $latest ) );
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -716,21 +713,20 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( isset( $this->expensiveCache[$path]['localRef'] ) ) {
+               if ( $this->expensiveCache->has( $path, 'localRef' ) ) {
+                       $val = $this->expensiveCache->get( $path, 'localRef' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
-                       if ( !$latest || $this->expensiveCache[$path]['localRef']['latest'] ) {
-                               $this->pingExpensiveCache( $path );
+                       if ( !$latest || $val['latest'] ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
-                               return $this->expensiveCache[$path]['localRef']['object'];
+                               return $val['object'];
                        }
                }
                $tmpFile = $this->getLocalCopy( $params );
                if ( $tmpFile ) { // don't cache negatives
-                       $this->trimExpensiveCache(); // limit memory
-                       $this->expensiveCache[$path]['localRef'] =
-                               array( 'object' => $tmpFile, 'latest' => $latest );
+                       $this->expensiveCache->set( $path, 'localRef',
+                               array( 'object' => $tmpFile, 'latest' => $latest ) );
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -970,7 +966,7 @@ abstract class FileBackendStore extends FileBackend {
         * @see FileBackend::doOperationsInternal()
         * @return Status
         */
-       protected function doOperationsInternal( array $ops, array $opts ) {
+       final protected function doOperationsInternal( array $ops, array $opts ) {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
@@ -1119,12 +1115,12 @@ abstract class FileBackendStore extends FileBackend {
                        $paths = array_filter( $paths, 'strlen' ); // remove nulls
                }
                if ( $paths === null ) {
-                       $this->cache = array();
-                       $this->expensiveCache = array();
+                       $this->cheapCache->clear();
+                       $this->expensiveCache->clear();
                } else {
                        foreach ( $paths as $path ) {
-                               unset( $this->cache[$path] );
-                               unset( $this->expensiveCache[$path] );
+                               $this->cheapCache->clear( $path );
+                               $this->expensiveCache->clear( $path );
                        }
                }
                $this->doClearCache( $paths );
@@ -1149,58 +1145,6 @@ abstract class FileBackendStore extends FileBackend {
         */
        abstract protected function directoriesAreVirtual();
 
-       /**
-        * Move a cache entry to the top (such as when accessed)
-        *
-        * @param $path string Storage path
-        * @return void
-        */
-       protected function pingCache( $path ) {
-               if ( isset( $this->cache[$path] ) ) {
-                       $tmp = $this->cache[$path];
-                       unset( $this->cache[$path] );
-                       $this->cache[$path] = $tmp;
-               }
-       }
-
-       /**
-        * Prune the inexpensive cache if it is too big to add an item
-        *
-        * @return void
-        */
-       protected function trimCache() {
-               if ( count( $this->cache ) >= $this->maxCacheSize ) {
-                       reset( $this->cache );
-                       unset( $this->cache[key( $this->cache )] );
-               }
-       }
-
-       /**
-        * Move a cache entry to the top (such as when accessed)
-        *
-        * @param $path string Storage path
-        * @return void
-        */
-       protected function pingExpensiveCache( $path ) {
-               if ( isset( $this->expensiveCache[$path] ) ) {
-                       $tmp = $this->expensiveCache[$path];
-                       unset( $this->expensiveCache[$path] );
-                       $this->expensiveCache[$path] = $tmp;
-               }
-       }
-
-       /**
-        * Prune the expensive cache if it is too big to add an item
-        *
-        * @return void
-        */
-       protected function trimExpensiveCache() {
-               if ( count( $this->expensiveCache ) >= $this->maxExpensiveCacheSize ) {
-                       reset( $this->expensiveCache );
-                       unset( $this->expensiveCache[key( $this->expensiveCache )] );
-               }
-       }
-
        /**
         * Check if a container name is valid.
         * This checks for for length and illegal characters.
@@ -1555,12 +1499,10 @@ abstract class FileBackendStore extends FileBackend {
                foreach ( $values as $cacheKey => $val ) {
                        if ( is_array( $val ) ) {
                                $path = $pathNames[$cacheKey];
-                               $this->trimCache(); // limit memory
-                               $this->cache[$path]['stat'] = $val;
+                               $this->cheapCache->set( $path, 'stat', $val );
                                if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->trimCache(); // limit memory
-                                       $this->cache[$path]['sha1'] =
-                                               array( 'hash' => $val['sha1'], 'latest' => $val['latest'] );
+                                       $this->cheapCache->set( $path, 'sha1',
+                                               array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) );
                                }
                        }
                }
index 79696b2..9e81dbf 100644 (file)
@@ -92,7 +92,7 @@ class MemcLockManager extends QuorumLockManager {
                $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode
                $this->lockExpiry = $met ? 2*(int)$met : 2*3600;
 
-               $this->session = wfRandomString( 31 );
+               $this->session = wfRandomString( 32 );
        }
 
        /**
@@ -264,11 +264,24 @@ class MemcLockManager extends QuorumLockManager {
        protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
                $lockedKeys = array();
 
+               // Acquire the keys in lexicographical order, to avoid deadlock problems.
+               // If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
+               sort( $keys );
+
+               // Try to quickly loop to acquire the keys, but back off after a few rounds.
+               // This reduces memcached spam, especially in the rare case where a server acquires
+               // some lock keys and dies without releasing them. Lock keys expire after a few minutes.
+               $rounds = 0;
                $start = microtime( true );
                do {
+                       if ( ( ++$rounds % 4 ) == 0 ) {
+                               usleep( 1000*50 ); // 50 ms
+                       }
                        foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
                                if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
                                        $lockedKeys[] = $key;
+                               } else {
+                                       continue; // acquire in order
                                }
                        }
                } while ( count( $lockedKeys ) < count( $keys ) && ( microtime( true ) - $start ) <= 6 );
index 81536b5..3fa8166 100644 (file)
@@ -245,6 +245,18 @@ abstract class File {
                }
        }
 
+       /**
+        * Callback for usort() to do file sorts by title
+        *
+        * @param $a File
+        * @param $b File
+        *
+        * @return Integer: result of title comparison
+        */
+       public static function compare( File $a, File $b ) {
+               return Title::compare( $a->getTitle(), $b->getTitle() );
+       }
+
        /**
         * Return the name of this file
         *
index 12a84a1..d87f294 100644 (file)
@@ -558,7 +558,7 @@ abstract class Installer {
         * write your messages. This appears to work well enough. Basic formatting and
         * external links work just fine.
         *
-        * But in case a translator decides to throw in a #ifexist or internal link or
+        * But in case a translator decides to throw in a "#ifexist" or internal link or
         * whatever, this function is guarded to catch the attempted DB access and to present
         * some fallback text.
         *
index 93f3f83..a7c803d 100644 (file)
@@ -509,11 +509,9 @@ class LogFormatter {
                        );
 
                        if ( $this->linkFlood ) {
-                               $element .= Linker::userToolLinks(
+                               $element .= Linker::userToolLinksRedContribs(
                                        $user->getId(),
                                        $user->getName(),
-                                       true, // Red if no edits
-                                       0, // Flags
                                        $user->getEditCount()
                                );
                        }
index 9bb7eb1..cf921b0 100644 (file)
@@ -255,9 +255,12 @@ class LogPage {
                                        $rightsnone = wfMsgExt( 'rightsnone', array( 'parsemag', 'language' => $langObj ) );
 
                                        if( $skin ) {
+                                               $username = $title->getText();
                                                foreach ( $params as &$param ) {
                                                        $groupArray = array_map( 'trim', explode( ',', $param ) );
-                                                       $groupArray = array_map( array( 'User', 'getGroupMember' ), $groupArray );
+                                                       foreach( $groupArray as &$group ) {
+                                                               $group = User::getGroupMember( $group, $username );
+                                                       }
                                                        $param = $wgLang->listToText( $groupArray );
                                                }
                                        }
index d0e7650..bd4d865 100644 (file)
@@ -537,7 +537,7 @@ abstract class MediaHandler {
 
        /**
         * Remove files from the purge list
-        * 
+        *
         * @param array $files
         * @param array $options
         */
@@ -717,7 +717,8 @@ abstract class ImageHandler extends MediaHandler {
                $page = isset( $params['page'] ) ? $params['page'] : false;
 
                if( $image->mustRender() || $params['width'] < $image->getWidth() ) {
-                       return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page );
+                       return new ThumbnailImage( $image,
+                               $url, $params['width'], $params['height'], false, $page );
                }
        }
 
index fc1f834..d0a7339 100644 (file)
@@ -36,21 +36,30 @@ abstract class MediaTransformOutput {
        protected $storagePath = false;
 
        /**
-        * Get the width of the output box
+        * @return integer Width of the output box
         */
        public function getWidth() {
                return $this->width;
        }
 
        /**
-        * Get the height of the output box
+        * @return integer Height of the output box
         */
        public function getHeight() {
                return $this->height;
        }
 
        /**
-        * @return string The thumbnail URL
+        * Get the final extension of the thumbnail.
+        * Returns false for scripted transformations.
+        * @return string|false
+        */
+       public function getExtension() {
+               return $this->path ? FileBackend::extensionFromPath( $this->path ) : false;
+       }
+
+       /**
+        * @return string|false The thumbnail URL
         */
        public function getUrl() {
                return $this->url;
@@ -106,7 +115,7 @@ abstract class MediaTransformOutput {
         * This will return false if there was an error, the
         * thumbnail is to be handled client-side only, or if
         * transformation was deferred via TRANSFORM_LATER.
-        * 
+        *
         * @return Bool
         */
        public function hasFile() {
@@ -198,7 +207,7 @@ class ThumbnailImage extends MediaTransformOutput {
         * Get a thumbnail object from a file and parameters.
         * If $path is set to null, the output file is treated as a source copy.
         * If $path is set to false, no output file will be created.
-        * 
+        *
         * @param $file File object
         * @param $url String: URL path to the thumb
         * @param $width Integer: file's width
index ec67a39..eda57c0 100644 (file)
@@ -895,7 +895,10 @@ class MWMemcached {
        function _load_items( $sock, &$ret ) {
                while ( 1 ) {
                        $decl = fgets( $sock );
-                       if ( $decl == "END\r\n" ) {
+                       if( $decl === false ) {
+                               $this->_debugprint( "Error reading socket for a memcached response\n" );
+                               return 0;
+                       } elseif ( $decl == "END\r\n" ) {
                                return true;
                        } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) {
                                list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] );
@@ -939,7 +942,12 @@ class MWMemcached {
                                }
 
                        } else {
-                               $this->_debugprint( "Error parsing memcached response\n" );
+                               $peer = $peerAddress = $peerPort = '';
+                               $gotPeer = socket_getpeername( $sock, $peerAddress, $peerPort );
+                               if( $gotPeer ) {
+                                       $peer = " from [$peerAddress:$peerPort";
+                               }
+                               $this->_debugprint( "Error parsing memcached response{$peer}\n" );
                                return 0;
                        }
                }
index 02950f0..d9356b4 100644 (file)
@@ -457,7 +457,7 @@ class LinkHolderArray {
                        foreach ( $entries as $index => $entry ) {
                                $pdbk = $entry['pdbk'];
                                // we only deal with new links (in its first query)
-                               if ( !isset( $colours[$pdbk] ) ) {
+                               if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
                                        $title = $entry['title'];
                                        $titleText = $title->getText();
                                        $titlesAttrs[] = array(
@@ -473,7 +473,7 @@ class LinkHolderArray {
                }
 
                // Now do the conversion and explode string to text of titles
-               $titlesAllVariants = $wgContLang->autoConvertToAllVariants( $titlesToBeConverted );
+               $titlesAllVariants = $wgContLang->autoConvertToAllVariants( rtrim( $titlesToBeConverted, "\0" ) );
                $allVariantsName = array_keys( $titlesAllVariants );
                foreach ( $titlesAllVariants as &$titlesVariant ) {
                        $titlesVariant = explode( "\0", $titlesVariant );
@@ -541,7 +541,7 @@ class LinkHolderArray {
                                        $entry =& $this->internals[$ns][$index];
                                        $pdbk = $entry['pdbk'];
 
-                                       if(!isset($colours[$pdbk])){
+                                       if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] === 'new' ) {
                                                // found link in some of the variants, replace the link holder data
                                                $entry['title'] = $variantTitle;
                                                $entry['pdbk'] = $varPdbk;
index 467a1ac..c86ed1d 100644 (file)
@@ -79,9 +79,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        'wgVersion' => $wgVersion,
                        'wgEnableAPI' => $wgEnableAPI,
                        'wgEnableWriteAPI' => $wgEnableWriteAPI,
-                       'wgDefaultDateFormat' => $wgContLang->getDefaultDateFormat(),
-                       'wgMonthNames' => $wgContLang->getMonthNamesArray(),
-                       'wgMonthNamesShort' => $wgContLang->getMonthAbbreviationsArray(),
                        'wgMainPageTitle' => $mainPage->getPrefixedText(),
                        'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
                        'wgNamespaceIds' => $namespaceIds,
index 1c45dc4..a2db52f 100644 (file)
@@ -124,7 +124,7 @@ class SearchOracle extends SearchEngine {
        /**
         * Return a LIMIT clause to limit results on the query.
         *
-        * @param string
+        * @param $sql string
         *
         * @return String
         */
index e8223e8..fb94ff7 100644 (file)
@@ -704,13 +704,22 @@ class ContribsPager extends ReverseChronologicalPager {
                $join_conds = array();
                $tables = array( 'revision', 'page', 'user' );
                if ( $this->contribs == 'newbie' ) {
-                       $tables[] = 'user_groups';
                        $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
                        $condition[] = 'rev_user >' . (int)( $max - $max / 100 );
-                       $condition[] = 'ug_group IS NULL';
                        $index = 'user_timestamp';
-                       # @todo FIXME: Other groups may have 'bot' rights
-                       $join_conds['user_groups'] = array( 'LEFT JOIN', "ug_user = rev_user AND ug_group = 'bot'" );
+                       # ignore local groups with the bot right
+                       # @todo FIXME: Global groups may have 'bot' rights
+                       $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
+                       if( count( $groupsWithBotPermission ) ) {
+                               $tables[] = 'user_groups';
+                               $condition[] = 'ug_group IS NULL';
+                               $join_conds['user_groups'] = array(
+                                       'LEFT JOIN', array(
+                                               'ug_user = rev_user',
+                                               'ug_group' => $groupsWithBotPermission
+                                       )
+                               );
+                       }
                } else {
                        $uid = User::idFromName( $this->target );
                        if ( $uid ) {
@@ -759,21 +768,15 @@ class ContribsPager extends ReverseChronologicalPager {
        }
 
        function doBatchLookups() {
-               $this->mResult->rewind();
-               $revIds = array();
-               foreach ( $this->mResult as $row ) {
-                       if( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
-                               $revIds[] = $row->rev_parent_id;
-                       }
-               }
-               $this->mParentLens = Revision::getParentLengths( $this->getDatabase(), $revIds );
-               $this->mResult->rewind(); // reset
-
                # Do a link batch query
                $this->mResult->seek( 0 );
+               $revIds = array();
                $batch = new LinkBatch();
                # Give some pointers to make (last) links
                foreach ( $this->mResult as $row ) {
+                       if( isset( $row->rev_parent_id ) && $row->rev_parent_id ) {
+                               $revIds[] = $row->rev_parent_id;
+                       }
                        if ( isset( $row->rev_id ) ) {
                                if ( $this->contribs === 'newbie' ) { // multiple users
                                        $batch->add( NS_USER, $row->user_name );
@@ -782,6 +785,7 @@ class ContribsPager extends ReverseChronologicalPager {
                                $batch->add( $row->page_namespace, $row->page_title );
                        }
                }
+               $this->mParentLens = Revision::getParentLengths( $this->getDatabase(), $revIds );
                $batch->execute();
                $this->mResult->seek( 0 );
        }
@@ -953,15 +957,6 @@ class ContribsPager extends ReverseChronologicalPager {
                return $ret;
        }
 
-       /**
-        * Get the Database object in use
-        *
-        * @return DatabaseBase
-        */
-       public function getDatabase() {
-               return $this->mDb;
-       }
-
        /**
         * Overwrite Pager function and return a helpful comment
         * @return string
index 18d19db..f8e40e0 100644 (file)
@@ -157,10 +157,24 @@ class FileDuplicateSearchPage extends QueryPage {
                                );
                        }
 
+                       $this->doBatchLookups( $dupes );
                        $this->showList( $dupes );
                }
        }
 
+       function doBatchLookups( $list ) {
+               $batch = new LinkBatch();
+               foreach( $list as $file ) {
+                       $batch->addObj( $file->getTitle() );
+                       if( $file->isLocal() ) {
+                               $userName = $file->getUser( 'text' );
+                               $batch->add( NS_USER, $userName );
+                               $batch->add( NS_USER_TALK, $userName );
+                       }
+               }
+               $batch->execute();
+       }
+
        /**
         *
         * @param Skin $skin
@@ -178,7 +192,17 @@ class FileDuplicateSearchPage extends QueryPage {
                );
 
                $userText = $result->getUser( 'text' );
-               $user = Linker::link( Title::makeTitle( NS_USER, $userText ), $userText );
+               if ( $result->isLocal() ) {
+                       $userId = $result->getUser( 'id' );
+                       $user = Linker::userLink( $userId, $userText );
+                       $user .= $this->getContext()->msg( 'word-separator' )->plain();
+                       $user .= '<span style="white-space: nowrap;">';
+                       $user .= Linker::userToolLinks( $userId, $userText );
+                       $user .= '</span>';
+               } else {
+                       $user = htmlspecialchars( $userText );
+               }
+
                $time = $this->getLanguage()->userTimeAndDate( $result->getTimestamp(), $this->getUser() );
 
                return "$plink . . $user . . $time";
index 75be397..ebcca73 100644 (file)
  */
 class UsersPager extends AlphabeticPager {
 
+       /**
+        * @param $context IContextSource
+        * @param $par null|array
+        */
        function __construct( IContextSource $context = null, $par = null ) {
                if ( $context ) {
                        $this->setContext( $context );
@@ -69,10 +73,16 @@ class UsersPager extends AlphabeticPager {
                parent::__construct();
        }
 
+       /**
+        * @return string
+        */
        function getIndexField() {
                return $this->creationSort ? 'user_id' : 'user_name';
        }
 
+       /**
+        * @return Array
+        */
        function getQueryInfo() {
                $dbr = wfGetDB( DB_SLAVE );
                $conds = array();
@@ -125,18 +135,20 @@ class UsersPager extends AlphabeticPager {
                return $query;
        }
 
+       /**
+        * @param $row Object
+        * @return String
+        */
        function formatRow( $row ) {
-               if ($row->user_id == 0) #Bug 16487
+               if ( $row->user_id == 0 ) { #Bug 16487
                        return '';
+               }
 
                $userName = $row->user_name;
 
                $ulinks = Linker::userLink( $row->user_id, $userName );
                $ulinks .= Linker::userToolLinks( $row->user_id, $userName );
 
-               $userPage = Title::makeTitle( NS_USER, $row->user_name );
-               $name = Linker::link( $userPage, htmlspecialchars( $userPage->getText() ) );
-
                $lang = $this->getLanguage();
 
                $groups_list = self::getGroups( $row->user_id );
@@ -186,9 +198,12 @@ class UsersPager extends AlphabeticPager {
                $this->mResult->rewind();
        }
 
+       /**
+        * @return string
+        */
        function getPageHeader( ) {
                global $wgScript;
-               // @todo Add a PrefixedBaseDBKey
+
                list( $self ) = explode( '/', $this->getTitle()->getPrefixedDBkey() );
 
                # Form tag
@@ -243,10 +258,12 @@ class UsersPager extends AlphabeticPager {
         */
        function getDefaultQuery() {
                $query = parent::getDefaultQuery();
-               if( $this->requestedGroup != '' )
+               if( $this->requestedGroup != '' ) {
                        $query['group'] = $this->requestedGroup;
-               if( $this->requestedUser != '' )
+               }
+               if( $this->requestedUser != '' ) {
                        $query['username'] = $this->requestedUser;
+               }
                wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) );
                return $query;
        }
index 45dbd36..35f39ce 100644 (file)
@@ -68,15 +68,18 @@ class NewFilesPager extends ReverseChronologicalPager {
                $tables = array( 'image' );
 
                if( !$this->showbots ) {
-                       $tables[] = 'user_groups';
-                       $conds[] = 'ug_group IS NULL';
-                       $jconds['user_groups'] = array(
-                               'LEFT JOIN',
-                               array(
-                                       'ug_group' => User::getGroupsWithPermission( 'bot' ),
-                                       'ug_user = img_user'
-                               )
-                       );
+                       $groupsWithBotPermission = User::getGroupsWithPermission( 'bot' );
+                       if( count( $groupsWithBotPermission ) ) {
+                               $tables[] = 'user_groups';
+                               $conds[] = 'ug_group IS NULL';
+                               $jconds['user_groups'] = array(
+                                       'LEFT JOIN',
+                                       array(
+                                               'ug_group' => $groupsWithBotPermission,
+                                               'ug_user = img_user'
+                                       )
+                               );
+                       }
                }
 
                if( !$wgMiserMode && $this->like !== null ){
index 5aa2b49..74ed537 100644 (file)
@@ -71,7 +71,7 @@ class SpecialProtectedpages extends SpecialPage {
        /**
         * Callback function to output a restriction
         * @param Title $row Protected title
-        * @return string Formatted <li> element
+        * @return string Formatted "<li>" element
         */
        public function formatRow( $row ) {
                wfProfileIn( __METHOD__ );
index 6f59135..611b3b9 100644 (file)
@@ -921,7 +921,8 @@ class SpecialUndelete extends SpecialPage {
                                "</td>\n" .
                        "</tr>" .
                        $diffEngine->generateDiffBody(
-                               $previousRev->getText(), $currentRev->getText() ) .
+                               $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ),
+                               $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) .
                        "</table>" .
                        "</div>\n"
                );
index 5a71ccb..51c2d0f 100644 (file)
@@ -108,6 +108,7 @@ class SpecialVersion extends SpecialPage {
                        'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
                        'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
                        'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
+                       'Timo Tijhof',
                        wfMsg( 'version-poweredby-others' )
                );
 
index 3ee959e..430209d 100644 (file)
@@ -246,7 +246,11 @@ class Language {
         */
        public static function isValidCode( $code ) {
                return
-                       strcspn( $code, ":/\\\000" ) === strlen( $code )
+                       // People think language codes are html safe, so enforce it.
+                       // Ideally we should only allow a-zA-Z0-9-
+                       // but, .+ and other chars are often used for {{int:}} hacks
+                       // see bugs 37564, 37587, 36938
+                       strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
                        && !preg_match( Title::getTitleInvalidRegex(), $code );
        }
 
index 6346c7a..00687eb 100644 (file)
@@ -322,7 +322,6 @@ $1',
 'versionrequiredtext' => 'Был бит менән эшләү өсөн MediaWiki-ның $1 версияһы кәрәк. [[Special:Version|Ҡулланылған версия тураһында мәғлүмәт битен]] ҡара.',
 
 'ok' => 'Тамам',
-'pagetitle' => '{{SITENAME}} проектынан',
 'retrievedfrom' => 'Сығанағы — «$1»',
 'youhavenewmessages' => 'Яңы $1 бар ($2).',
 'newmessageslink' => 'яңы хәбәр',
index ffef8a1..9578ed5 100644 (file)
@@ -760,7 +760,7 @@ XHTML id names.
 'index-category'                 => 'Indexed pages',
 'noindex-category'               => 'Noindexed pages',
 'broken-file-category'           => 'Pages with broken file links',
-'categoryviewer-pagedlinks'      => '($1) ($2)',
+'categoryviewer-pagedlinks'      => '($1) ($2)', # only translate this message to other languages if you have to change it
 
 'linkprefix' => '/^(.*?)([a-zA-Z\\x80-\\xff]+)$/sD', # only translate this message to other languages if you have to change it
 
@@ -964,88 +964,90 @@ This might also indicate a bug in the software used by {{SITENAME}}.',
 A list of valid special pages can be found at [[Special:SpecialPages|{{int:specialpages}}]].',
 
 # General errors
-'error'                => 'Error',
-'databaseerror'        => 'Database error',
-'dberrortext'          => 'A database query syntax error has occurred.
+'error'                         => 'Error',
+'databaseerror'                 => 'Database error',
+'dberrortext'                   => 'A database query syntax error has occurred.
 This may indicate a bug in the software.
 The last attempted database query was:
 <blockquote><tt>$1</tt></blockquote>
 from within function "<tt>$2</tt>".
 Database returned error "<tt>$3: $4</tt>".',
-'dberrortextcl'        => 'A database query syntax error has occurred.
+'dberrortextcl'                 => 'A database query syntax error has occurred.
 The last attempted database query was:
 "$1"
 from within function "$2".
 Database returned error "$3: $4"',
-'laggedslavemode'      => "'''Warning:''' Page may not contain recent updates.",
-'readonly'             => 'Database locked',
-'enterlockreason'      => 'Enter a reason for the lock, including an estimate of when the lock will be released',
-'readonlytext'         => 'The database is currently locked to new entries and other modifications, probably for routine database maintenance, after which it will be back to normal.
+'laggedslavemode'               => "'''Warning:''' Page may not contain recent updates.",
+'readonly'                      => 'Database locked',
+'enterlockreason'               => 'Enter a reason for the lock, including an estimate of when the lock will be released',
+'readonlytext'                  => 'The database is currently locked to new entries and other modifications, probably for routine database maintenance, after which it will be back to normal.
 
 The administrator who locked it offered this explanation: $1',
-'missing-article'      => 'The database did not find the text of a page that it should have found, named "$1" $2.
+'missing-article'               => 'The database did not find the text of a page that it should have found, named "$1" $2.
 
 This is usually caused by following an outdated diff or history link to a page that has been deleted.
 
 If this is not the case, you may have found a bug in the software.
 Please report this to an [[Special:ListUsers/sysop|administrator]], making note of the URL.',
-'missingarticle-rev'   => '(revision#: $1)',
-'missingarticle-diff'  => '(Diff: $1, $2)',
-'readonly_lag'         => 'The database has been automatically locked while the slave database servers catch up to the master',
-'internalerror'        => 'Internal error',
-'internalerror_info'   => 'Internal error: $1',
-'fileappenderrorread'  => 'Could not read "$1" during append.',
-'fileappenderror'      => 'Could not append "$1" to "$2".',
-'filecopyerror'        => 'Could not copy file "$1" to "$2".',
-'filerenameerror'      => 'Could not rename file "$1" to "$2".',
-'filedeleteerror'      => 'Could not delete file "$1".',
-'directorycreateerror' => 'Could not create directory "$1".',
-'filenotfound'         => 'Could not find file "$1".',
-'fileexistserror'      => 'Unable to write to file "$1": File exists.',
-'unexpected'           => 'Unexpected value: "$1"="$2".',
-'formerror'            => 'Error: Could not submit form.',
-'badarticleerror'      => 'This action cannot be performed on this page.',
-'cannotdelete'         => 'The page or file "$1" could not be deleted.
+'missingarticle-rev'            => '(revision#: $1)',
+'missingarticle-diff'           => '(Diff: $1, $2)',
+'readonly_lag'                  => 'The database has been automatically locked while the slave database servers catch up to the master',
+'internalerror'                 => 'Internal error',
+'internalerror_info'            => 'Internal error: $1',
+'fileappenderrorread'           => 'Could not read "$1" during append.',
+'fileappenderror'               => 'Could not append "$1" to "$2".',
+'filecopyerror'                 => 'Could not copy file "$1" to "$2".',
+'filerenameerror'               => 'Could not rename file "$1" to "$2".',
+'filedeleteerror'               => 'Could not delete file "$1".',
+'directorycreateerror'          => 'Could not create directory "$1".',
+'filenotfound'                  => 'Could not find file "$1".',
+'fileexistserror'               => 'Unable to write to file "$1": File exists.',
+'unexpected'                    => 'Unexpected value: "$1"="$2".',
+'formerror'                     => 'Error: Could not submit form.',
+'badarticleerror'               => 'This action cannot be performed on this page.',
+'cannotdelete'                  => 'The page or file "$1" could not be deleted.
 It may have already been deleted by someone else.',
-'cannotdelete-title'   => 'Cannot delete page "$1"',
-'delete-hook-aborted'  => 'Deletion aborted by hook.
+'cannotdelete-title'            => 'Cannot delete page "$1"',
+'delete-hook-aborted'           => 'Deletion aborted by hook.
 It gave no explanation.',
-'badtitle'             => 'Bad title',
-'badtitletext'         => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title.
+'badtitle'                      => 'Bad title',
+'badtitletext'                  => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title.
 It may contain one or more characters which cannot be used in titles.',
-'perfcached'           => 'The following data is cached and may not be up to date. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.',
-'perfcachedts'         => 'The following data is cached, and was last updated $1. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.',
-'querypage-no-updates' => 'Updates for this page are currently disabled.
+'perfcached'                    => 'The following data is cached and may not be up to date. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.',
+'perfcachedts'                  => 'The following data is cached, and was last updated $1. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.',
+'querypage-no-updates'          => 'Updates for this page are currently disabled.
 Data here will not presently be refreshed.',
-'wrong_wfQuery_params' => 'Incorrect parameters to wfQuery()<br />
+'wrong_wfQuery_params'          => 'Incorrect parameters to wfQuery()<br />
 Function: $1<br />
 Query: $2',
-'viewsource'           => 'View source',
-'viewsource-title'     => 'View source for $1',
-'actionthrottled'      => 'Action throttled',
-'actionthrottledtext'  => 'As an anti-spam measure, you are limited from performing this action too many times in a short space of time, and you have exceeded this limit.
+'viewsource'                    => 'View source',
+'viewsource-title'              => 'View source for $1',
+'actionthrottled'               => 'Action throttled',
+'actionthrottledtext'           => 'As an anti-spam measure, you are limited from performing this action too many times in a short space of time, and you have exceeded this limit.
 Please try again in a few minutes.',
-'protectedpagetext'    => 'This page has been protected to prevent editing.',
-'viewsourcetext'       => 'You can view and copy the source of this page:',
-'viewyourtext'         => "You can view and copy the source of '''your edits''' to this page:",
-'protectedinterface'   => 'This page provides interface text for the software, and is protected to prevent abuse.',
-'editinginterface'     => "'''Warning:''' You are editing a page which is used to provide interface text for the software.
+'protectedpagetext'             => 'This page has been protected to prevent editing.',
+'viewsourcetext'                => 'You can view and copy the source of this page:',
+'viewyourtext'                  => "You can view and copy the source of '''your edits''' to this page:",
+'protectedinterface'            => 'This page provides interface text for the software, and is protected to prevent abuse.',
+'editinginterface'              => "'''Warning:''' You are editing a page which is used to provide interface text for the software.
 Changes to this page will affect the appearance of the user interface for other users.
 For translations, please consider using [//translatewiki.net/wiki/Main_Page?setlang=en translatewiki.net], the MediaWiki localisation project.",
-'sqlhidden'            => '(SQL query hidden)',
-'cascadeprotected'     => 'This page has been protected from editing, because it is included in the following {{PLURAL:$1|page, which is|pages, which are}} protected with the "cascading" option turned on:
+'sqlhidden'                     => '(SQL query hidden)',
+'cascadeprotected'              => 'This page has been protected from editing, because it is included in the following {{PLURAL:$1|page, which is|pages, which are}} protected with the "cascading" option turned on:
 $2',
-'namespaceprotected'   => "You do not have permission to edit pages in the '''$1''' namespace.",
-'customcssprotected'   => "You do not have permission to edit this CSS page, because it contains another user's personal settings.",
-'customjsprotected'    => "You do not have permission to edit this JavaScript page, because it contains another user's personal settings.",
-'ns-specialprotected'  => 'Special pages cannot be edited.',
-'titleprotected'       => 'This title has been protected from creation by [[User:$1|$1]].
+'namespaceprotected'            => "You do not have permission to edit pages in the '''$1''' namespace.",
+'customcssprotected'            => "You do not have permission to edit this CSS page, because it contains another user's personal settings.",
+'customjsprotected'             => "You do not have permission to edit this JavaScript page, because it contains another user's personal settings.",
+'ns-specialprotected'           => 'Special pages cannot be edited.',
+'titleprotected'                => 'This title has been protected from creation by [[User:$1|$1]].
 The reason given is "\'\'$2\'\'".',
-'filereadonlyerror'    => 'Unable to modify the file "$1" because the file repository "$2" is in read-only mode.
+'filereadonlyerror'             => 'Unable to modify the file "$1" because the file repository "$2" is in read-only mode.
 
 The administrator who locked it offered this explanation: "$3".',
 'invalidtitle-knownnamespace'   => 'Invalid title with namespace "$2" and text "$3"',
 'invalidtitle-unknownnamespace' => 'Invalid title with unknown namespace number $1 and text "$2"',
+'exception-nologin'             => 'Not logged in',
+'exception-nologin-text'        => 'This page or action requires you to be logged in on this wiki.',
 
 # Virus scanner
 'virus-badscanner'     => "Bad configuration: Unknown virus scanner: ''$1''",
@@ -1089,8 +1091,6 @@ Do not forget to change your [[Special:Preferences|{{SITENAME}} preferences]].',
 Please choose a different name.',
 'loginerror'                 => 'Login error',
 'createaccounterror'         => 'Could not create account: $1',
-'exception-nologin'          => 'Not logged in',
-'exception-nologin-text'     => 'This page or action requires you to be logged in on this wiki.',
 'nocookiesnew'               => 'The user account was created, but you are not logged in.
 {{SITENAME}} uses cookies to log in users.
 You have cookies disabled.
@@ -1382,7 +1382,7 @@ Custom .css and .js pages use a lowercase title, e.g. {{ns:user}}:Foo/vector.css
 'note'                             => "'''Note:'''",
 'previewnote'                      => "'''Remember that this is only a preview.'''
 Your changes have not yet been saved!",
-'continue-editing'                 => "Continue editing",
+'continue-editing'                 => 'Continue editing',
 'previewconflict'                  => 'This preview reflects the text in the upper text editing area as it will appear if you choose to save.',
 'session_fail_preview'             => "'''Sorry! We could not process your edit due to a loss of session data.'''
 Please try again.
@@ -1668,7 +1668,7 @@ Note that using the navigation links will reset this column.',
 'mergehistory-comment'             => 'Merged [[:$1]] into [[:$2]]: $3',
 'mergehistory-same-destination'    => 'Source and destination pages cannot be the same',
 'mergehistory-reason'              => 'Reason:',
-'mergehistory-revisionrow'         => '$1 ($2) $3 . . $4 $5 $6',
+'mergehistory-revisionrow'         => '$1 ($2) $3 . . $4 $5 $6', # only translate this message to other languages if you have to change it
 
 # Merge log
 'mergelog'           => 'Merge log',
@@ -2295,14 +2295,15 @@ If the problem persists, contact an [[Special:ListUsers/sysop|administrator]].',
 'backend-fail-read'          => 'Could not read file $1.',
 'backend-fail-create'        => 'Could not write file $1.',
 'backend-fail-maxsize'       => 'Could not write file $1 because it is larger than {{PLURAL:$2|one byte|$2 bytes}}.',
-'backend-fail-usable'        => 'Could not write file $1 due to insufficient permissions or missing directories/containers.',
 'backend-fail-readonly'      => 'The storage backend "$1" is currently read-only. The reason given is: "\'\'$2\'\'"',
 'backend-fail-synced'        => 'The file "$1" is in an inconsistent state within the internal storage backends',
 'backend-fail-connect'       => 'Could not connect to storage backend "$1".',
 'backend-fail-internal'      => 'An unknown error occurred in storage backend "$1".',
 'backend-fail-contenttype'   => 'Could not determine the content type of the file to store at "$1".',
 'backend-fail-batchsize'     => 'Storage backend given a batch of $1 file {{PLURAL:$1|operation|operations}}; the limit is $2 {{PLURAL:$2|operation|operations}}.',
+'backend-fail-usable'        => 'Could not write file $1 due to insufficient permissions or missing directories/containers.',
 
+# File journal errors
 'filejournal-fail-dbconnect' => 'Could not connect to the journal database for storage backend "$1".',
 'filejournal-fail-dbquery'   => 'Could not update the journal database for storage backend "$1".',
 
@@ -2696,8 +2697,8 @@ It may contain one or more characters which cannot be used in titles.',
 
 # SpecialCachedPage
 'cachedspecial-viewing-cached-ttl' => 'You are viewing a cached version of this page, which can be up to $1 old.',
-'cachedspecial-viewing-cached-ts' => 'You are viewing a cached version of this page, which might not be completely actual.',
-'cachedspecial-refresh-now' => 'View latest.',
+'cachedspecial-viewing-cached-ts'  => 'You are viewing a cached version of this page, which might not be completely actual.',
+'cachedspecial-refresh-now'        => 'View latest.',
 
 # Special:Categories
 'categories'                    => 'Categories',
@@ -3062,7 +3063,7 @@ It may have already been undeleted.',
 $1',
 'undelete-show-file-confirm'   => 'Are you sure you want to view the deleted revision of the file "<nowiki>$1</nowiki>" from $2 at $3?',
 'undelete-show-file-submit'    => 'Yes',
-'undelete-revisionrow'        => "$1 $2 ($3) $4 . . $5 $6 $7",
+'undelete-revisionrow'         => '$1 $2 ($3) $4 . . $5 $6 $7', # only translate this message to other languages if you have to change it
 
 # Namespace form on various pages
 'namespace'                     => 'Namespace:',
@@ -4618,44 +4619,44 @@ You can also [[Special:EditWatchlist|use the standard editor]].',
 'duplicate-defaultsort' => '\'\'\'Warning:\'\'\' Default sort key "$2" overrides earlier default sort key "$1".',
 
 # Special:Version
-'version'                       => 'Version',
-'version-summary'               => '', # do not translate or duplicate this message to other languages
-'version-extensions'            => 'Installed extensions',
-'version-specialpages'          => 'Special pages',
-'version-parserhooks'           => 'Parser hooks',
-'version-variables'             => 'Variables',
-'version-antispam'              => 'Spam prevention',
-'version-skins'                 => 'Skins',
-'version-api'                   => 'API', # only translate this message to other languages if you have to change it
-'version-other'                 => 'Other',
-'version-mediahandlers'         => 'Media handlers',
-'version-hooks'                 => 'Hooks',
-'version-extension-functions'   => 'Extension functions',
-'version-parser-extensiontags'  => 'Parser extension tags',
-'version-parser-function-hooks' => 'Parser function hooks',
-'version-hook-name'             => 'Hook name',
-'version-hook-subscribedby'     => 'Subscribed by',
-'version-version'               => '(Version $1)',
-'version-svn-revision'          => '(r$2)', # only translate this message to other languages if you have to change it
-'version-license'               => 'License',
-'version-poweredby-credits'     => "This wiki is powered by '''[//www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
-'version-poweredby-others'      => '[{{SERVER}}{{SCRIPTPATH}}/CREDITS others]',
-'version-license-info'          => 'MediaWiki 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.
+'version'                               => 'Version',
+'version-summary'                       => '', # do not translate or duplicate this message to other languages
+'version-extensions'                    => 'Installed extensions',
+'version-specialpages'                  => 'Special pages',
+'version-parserhooks'                   => 'Parser hooks',
+'version-variables'                     => 'Variables',
+'version-antispam'                      => 'Spam prevention',
+'version-skins'                         => 'Skins',
+'version-api'                           => 'API', # only translate this message to other languages if you have to change it
+'version-other'                         => 'Other',
+'version-mediahandlers'                 => 'Media handlers',
+'version-hooks'                         => 'Hooks',
+'version-extension-functions'           => 'Extension functions',
+'version-parser-extensiontags'          => 'Parser extension tags',
+'version-parser-function-hooks'         => 'Parser function hooks',
+'version-hook-name'                     => 'Hook name',
+'version-hook-subscribedby'             => 'Subscribed by',
+'version-version'                       => '(Version $1)',
+'version-svn-revision'                  => '(r$2)', # only translate this message to other languages if you have to change it
+'version-license'                       => 'License',
+'version-poweredby-credits'             => "This wiki is powered by '''[//www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
+'version-poweredby-others'              => '[{{SERVER}}{{SCRIPTPATH}}/CREDITS others]',
+'version-license-info'                  => 'MediaWiki 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.
 
 MediaWiki 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 [{{SERVER}}{{SCRIPTPATH}}/COPYING 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 or [//www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].',
-'version-software'              => 'Installed software',
-'version-software-product'      => 'Product',
-'version-software-version'      => 'Version',
-'version-entrypoints'           => 'Entry point URLs',
+'version-software'                      => 'Installed software',
+'version-software-product'              => 'Product',
+'version-software-version'              => 'Version',
+'version-entrypoints'                   => 'Entry point URLs',
 'version-entrypoints-header-entrypoint' => 'Entry point',
-'version-entrypoints-header-url' => 'URL',
-'version-entrypoints-articlepath' => '[https://www.mediawiki.org/wiki/Manual:$wgArticlePath Article path]',
-'version-entrypoints-scriptpath' => '[https://www.mediawiki.org/wiki/Manual:$wgScriptPath Script path]',
-'version-entrypoints-index-php' => '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
-'version-entrypoints-api-php'   => '[https://www.mediawiki.org/wiki/Manual:api.php api.php]',
-'version-entrypoints-load-php'  => '[https://www.mediawiki.org/wiki/Manual:load.php load.php]',
+'version-entrypoints-header-url'        => 'URL',
+'version-entrypoints-articlepath'       => '[https://www.mediawiki.org/wiki/Manual:$wgArticlePath Article path]', # only translate this message to other languages if you have to change it
+'version-entrypoints-scriptpath'        => '[https://www.mediawiki.org/wiki/Manual:$wgScriptPath Script path]', # only translate this message to other languages if you have to change it
+'version-entrypoints-index-php'         => '[https://www.mediawiki.org/wiki/Manual:index.php index.php]', # do not translate or duplicate this message to other languages
+'version-entrypoints-api-php'           => '[https://www.mediawiki.org/wiki/Manual:api.php api.php]', # do not translate or duplicate this message to other languages
+'version-entrypoints-load-php'          => '[https://www.mediawiki.org/wiki/Manual:load.php load.php]', # do not translate or duplicate this message to other languages
 
 # Special:FilePath
 'filepath'         => 'File path',
@@ -4843,9 +4844,9 @@ Otherwise, you can use the easy form below. Your comment will be added to the pa
 'api-error-empty-file'                    => 'The file you submitted was empty.',
 'api-error-emptypage'                     => 'Creating new, empty pages is not allowed.',
 'api-error-fetchfileerror'                => 'Internal error: Something went wrong while fetching the file.',
-'api-error-file-too-large'                => 'The file you submitted was too large.',
 'api-error-fileexists-forbidden'          => 'A file with name "$1" already exists, and cannot be overwritten.',
 'api-error-fileexists-shared-forbidden'   => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.',
+'api-error-file-too-large'                => 'The file you submitted was too large.',
 'api-error-filename-tooshort'             => 'The filename is too short.',
 'api-error-filetype-banned'               => 'This type of file is banned.',
 'api-error-filetype-missing'              => 'The filename is missing an extension.',
index 6cd98ab..cc05670 100644 (file)
@@ -269,8 +269,6 @@ $1',
 Ẓr [[Special:Version|ayyaw tasna]].',
 
 'ok' => 'Waxxa',
-'pagetitle' => '(MediaWiki)$1 - {{SITENAME}}',
-'pagetitle-view-mainpage' => '{{SITENAME}}',
 'retrievedfrom' => 'Yurrid z "$1"',
 'youhavenewmessages' => 'Illa dark $1 ($2).',
 'newmessageslink' => 'Tibratin timaynutin',
index 8db0a7f..544f48b 100644 (file)
@@ -44,6 +44,7 @@ class CopyFileBackend extends Maintenance {
                $this->addOption( 'subdir', 'Only do items in this child directory', false, true );
                $this->addOption( 'ratefile', 'File to check periodically for batch size', false, true );
                $this->addOption( 'skiphash', 'Skip SHA-1 sync checks for files' );
+               $this->addOption( 'missingonly', 'Only copy files missing from destination listing' );
                $this->setBatchSize( 50 );
        }
 
@@ -65,12 +66,32 @@ class CopyFileBackend extends Maintenance {
                                $this->output( "Doing container '$container'...\n" );
                        }
 
-                       $dir = $src->getRootStoragePath() . "/$backendRel";
-                       $srcPathsRel = $src->getFileList( array( 'dir' => $dir ) );
+                       $srcPathsRel = $src->getFileList( array(
+                               'dir' => $src->getRootStoragePath() . "/$backendRel" ) );
                        if ( $srcPathsRel === null ) {
                                $this->error( "Could not list files in $container.", 1 ); // die
                        }
 
+                       // Do a listing comparison if specified
+                       if ( $this->hasOption( 'missingonly' ) ) {
+                               $relFilesSrc = array();
+                               $relFilesDst = array();
+                               foreach ( $srcPathsRel as $srcPathRel ) {
+                                       $relFilesSrc[] = $srcPathRel;
+                               }
+                               $dstPathsRel = $dst->getFileList( array(
+                                       'dir' => $dst->getRootStoragePath() . "/$backendRel" ) );
+                               if ( $dstPathsRel === null ) {
+                                       $this->error( "Could not list files in $container.", 1 ); // die
+                               }
+                               foreach ( $dstPathsRel as $dstPathRel ) {
+                                       $relFilesDst[] = $dstPathRel;
+                               }
+                               // Only copy the missing files over in the next loop
+                               $srcPathsRel = array_diff( $relFilesSrc, $relFilesDst );
+                               $this->output( count( $srcPathsRel ) . " file(s) need to be copied.\n" );
+                       }
+
                        $batchPaths = array();
                        foreach ( $srcPathsRel as $srcPathRel ) {
                                // Check up on the rate file periodically to adjust the concurrency
@@ -120,7 +141,7 @@ class CopyFileBackend extends Maintenance {
                        }
                        $fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
                        // Note: prepare() is usually fast for key/value backends
-                       $status = $dst->prepare( array( 'dir' => dirname( $dstPath ) ) );
+                       $status = $dst->prepare( array( 'dir' => dirname( $dstPath ), 'bypassReadOnly' => 1 ) );
                        if ( !$status->isOK() ) {
                                $this->error( print_r( $status->getErrorsArray(), true ) );
                                $this->error( "Could not copy $srcPath to $dstPath.", 1 ); // die
@@ -131,7 +152,11 @@ class CopyFileBackend extends Maintenance {
                }
 
                $t_start = microtime( true );
-               $status = $dst->doOperations( $ops, array( 'nonJournaled' => 1 ) );
+               $status = $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) );
+               if ( !$status->isOK() ) {
+                       sleep( 10 ); // wait and retry copy again
+                       $status = $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) );
+               }
                $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( !$status->isOK() ) {
                        $this->error( print_r( $status->getErrorsArray(), true ) );
index 2e381aa..d15647e 100644 (file)
@@ -1414,6 +1414,7 @@ $wgMessageStructure = array(
                'lockmanager-fail-releaselock',
                'lockmanager-fail-db-bucket',
                'lockmanager-fail-db-release',
+               'lockmanager-fail-svr-acquire',
                'lockmanager-fail-svr-release'
        ),
 
index 8669fe3..6a9baa8 100644 (file)
@@ -33,6 +33,7 @@ class MergeMessageFileList extends Maintenance {
        function __construct() {
                parent::__construct();
                $this->addOption( 'list-file', 'A file containing a list of extension setup files, one per line.', true, true );
+               $this->addOption( 'extensions-dir', 'Path where extensions can be found.', false, true );
                $this->addOption( 'output', 'Send output to this file (omit for stdout)', false, true );
                $this->mDescription = 'Merge $wgExtensionMessagesFiles from various extensions to produce a ' .
                        'single array containing all message files.';
@@ -41,11 +42,36 @@ class MergeMessageFileList extends Maintenance {
        public function execute() {
                global $mmfl;
 
+               # Add setup files contained in file passed to --list-file
                $lines = file( $this->getOption( 'list-file' ) );
                if ( $lines === false ) {
                        $this->error( 'Unable to open list file.' );
                }
                $mmfl = array( 'setupFiles' => array_map( 'trim', $lines ) );
+
+               # Now find out files in a directory
+               $hasError = false;
+               if ( $this->hasOption( 'extensions-dir' ) ) {
+                       $extdir = $this->getOption( 'extensions-dir' );
+                       $entries = scandir( $extdir );
+                       foreach( $entries as $extname ) {
+                               if ( $extname == '.' || $extname == '..' || !is_dir( "$extdir/$extname" ) ) {
+                                       continue;
+                               }
+                               $extfile = "{$extdir}/{$extname}/{$extname}.php";
+                               if ( file_exists( $extfile ) ) {
+                                       $mmfl['setupFiles'][] = $extfile;
+                               } else {
+                                       $hasError = true;
+                                       $this->error( "Extension {$extname} in {$extdir} lacks expected {$extname}.php" );
+                               }
+                       }
+               }
+
+               if ( $hasError ) {
+                       $this->error( "Some files are missing (see above). Giving up.", 1 );
+               }
+
                if ( $this->hasOption( 'output' ) ) {
                        $mmfl['output'] = $this->getOption( 'output' );
                }
index 1af33e6..c4ba66e 100644 (file)
@@ -185,7 +185,8 @@ class SyncFileBackend extends Maintenance {
                                }
                                $fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
                                // Note: prepare() is usually fast for key/value backends
-                               $status->merge( $dst->prepare( array( 'dir' => dirname( $dPath ) ) ) );
+                               $status->merge( $dst->prepare( array(
+                                       'dir' => dirname( $dPath ), 'bypassReadOnly' => 1 ) ) );
                                if ( !$status->isOK() ) {
                                        return $status;
                                }
@@ -201,8 +202,7 @@ class SyncFileBackend extends Maintenance {
                }
 
                $t_start = microtime( true );
-               $status->merge( $dst->doOperations( $ops,
-                       array( 'nonLocking' => 1, 'nonJournaled' => 1 ) ) );
+               $status->merge( $dst->doQuickOperations( $ops, array( 'bypassReadOnly' => 1 ) ) );
                $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( $status->isOK() && $this->getOption( 'verbose' ) ) {
                        $this->output( "Synchronized these file(s) [{$ellapsed_ms}ms]:\n" .
index fd425fe..2363665 100644 (file)
@@ -32,6 +32,8 @@ class UpdateCollation extends Maintenance {
        const BATCH_SIZE = 50; // Number of rows to process in one batch
        const SYNC_INTERVAL = 20; // Wait for slaves after this many batches
 
+       var $sizeHistogram = array();
+
        public function __construct() {
                parent::__construct();
 
@@ -50,6 +52,13 @@ TEXT;
                        'categorylinks table is large. This will only update rows with that ' .
                        'collation, though, so it may miss out-of-date rows with a different, ' .
                        'even older collation.', false, true );
+               $this->addOption( 'target-collation', 'Set this to the new collation type to ' .
+                       'use instead of $wgCategoryCollation. Usually you should not use this, ' . 
+                       'you should just update $wgCategoryCollation in LocalSettings.php.', 
+                       false, true );
+               $this->addOption( 'dry-run', 'Don\'t actually change the collations, just ' .
+                       'compile statistics.' );
+               $this->addOption( 'verbose-stats', 'Show more statistics.' );
        }
 
        public function execute() {
@@ -57,10 +66,19 @@ TEXT;
 
                $dbw = $this->getDB( DB_MASTER );
                $force = $this->getOption( 'force' );
+               $dryRun = $this->getOption( 'dry-run' );
+               $verboseStats = $this->getOption( 'verbose-stats' );
+               if ( $this->hasOption( 'target-collation' ) ) {
+                       $collationName = $this->getOption( 'target-collation' );
+                       $collation = Collation::factory( $collationName );
+               } else {
+                       $collationName = $wgCategoryCollation;
+                       $collation = Collation::singleton();
+               }
 
                $options = array( 'LIMIT' => self::BATCH_SIZE, 'STRAIGHT_JOIN' );
 
-               if ( $force ) {
+               if ( $force || $dryRun ) {
                        $options['ORDER BY'] = 'cl_from, cl_to';
                        $collationConds = array();
                } else {
@@ -68,7 +86,7 @@ TEXT;
                                $collationConds['cl_collation'] = $this->getOption( 'previous-collation' );
                        } else {
                                $collationConds = array( 0 =>
-                                       'cl_collation != ' . $dbw->addQuotes( $wgCategoryCollation )
+                                       'cl_collation != ' . $dbw->addQuotes( $collationName )
                                );
                        }
 
@@ -82,7 +100,7 @@ TEXT;
                        } else {
                                $count = $dbw->estimateRowCount(
                                        'categorylinks',
-                                       '',
+                                       '*',
                                        $collationConds,
                                        __METHOD__
                                );
@@ -110,7 +128,9 @@ TEXT;
                        );
                        $this->output( " processing..." );
 
-                       $dbw->begin( __METHOD__ );
+                       if ( !$dryRun ) {
+                               $dbw->begin( __METHOD__ );
+                       }
                        foreach ( $res as $row ) {
                                $title = Title::newFromRow( $row );
                                if ( !$row->cl_collation ) {
@@ -135,23 +155,32 @@ TEXT;
                                } else {
                                        $type = 'page';
                                }
-                               $dbw->update(
-                                       'categorylinks',
-                                       array(
-                                               'cl_sortkey' => Collation::singleton()->getSortKey(
-                                                       $title->getCategorySortkey( $prefix ) ),
-                                               'cl_sortkey_prefix' => $prefix,
-                                               'cl_collation' => $wgCategoryCollation,
-                                               'cl_type' => $type,
-                                               'cl_timestamp = cl_timestamp',
-                                       ),
-                                       array( 'cl_from' => $row->cl_from, 'cl_to' => $row->cl_to ),
-                                       __METHOD__
-                               );
+                               $newSortKey = $collation->getSortKey(
+                                       $title->getCategorySortkey( $prefix ) );
+                               if ( $verboseStats ) {
+                                       $this->updateSortKeySizeHistogram( $newSortKey );
+                               }
+
+                               if ( !$dryRun ) {
+                                       $dbw->update(
+                                               'categorylinks',
+                                               array(
+                                                       'cl_sortkey' => $newSortKey,
+                                                       'cl_sortkey_prefix' => $prefix,
+                                                       'cl_collation' => $collationName,
+                                                       'cl_type' => $type,
+                                                       'cl_timestamp = cl_timestamp',
+                                               ),
+                                               array( 'cl_from' => $row->cl_from, 'cl_to' => $row->cl_to ),
+                                               __METHOD__
+                                       );
+                               }
+                       }
+                       if ( !$dryRun ) {
+                               $dbw->commit( __METHOD__ );
                        }
-                       $dbw->commit( __METHOD__ );
 
-                       if ( $force && $row ) {
+                       if ( ( $force || $dryRun ) && $row ) {
                                $encFrom = $dbw->addQuotes( $row->cl_from );
                                $encTo = $dbw->addQuotes( $row->cl_to );
                                $batchConds = array(
@@ -162,12 +191,83 @@ TEXT;
                        $count += $res->numRows();
                        $this->output( "$count done.\n" );
 
-                       if ( ++$batchCount % self::SYNC_INTERVAL == 0 ) {
+                       if ( !$dryRun && ++$batchCount % self::SYNC_INTERVAL == 0 ) {
                                $this->output( "Waiting for slaves ... " );
                                wfWaitForSlaves();
                                $this->output( "done\n" );
                        }
                } while ( $res->numRows() == self::BATCH_SIZE );
+
+               $this->output( "$count rows processed\n" );
+
+               if ( $verboseStats ) {
+                       $this->output( "\n" );
+                       $this->showSortKeySizeHistogram();
+               }
+       }
+
+       function updateSortKeySizeHistogram( $key ) {
+               $length = strlen( $key );
+               if ( !isset( $this->sizeHistogram[$length] ) ) {
+                       $this->sizeHistogram[$length] = 0;
+               }
+               $this->sizeHistogram[$length]++;
+       }
+
+       function showSortKeySizeHistogram() {
+               $maxLength = max( array_keys( $this->sizeHistogram ) );
+               if ( $maxLength == 0 ) {
+                       return;
+               }
+               $numBins = 20;
+               $coarseHistogram = array_fill( 0, $numBins, 0 );
+               $coarseBoundaries = array();
+               $boundary = 0;
+               for ( $i = 0; $i < $numBins - 1; $i++ ) {
+                       $boundary += $maxLength / $numBins;
+                       $coarseBoundaries[$i] = round( $boundary );
+               }
+               $coarseBoundaries[$numBins - 1] = $maxLength + 1;
+               $raw = '';
+               for ( $i = 0; $i <= $maxLength; $i++ ) {
+                       if ( $raw !== '' ) {
+                               $raw .= ', ';
+                       }
+                       if ( !isset( $this->sizeHistogram[$i] ) ) {
+                               $val = 0;
+                       } else {
+                               $val = $this->sizeHistogram[$i];
+                       }
+                       for ( $coarseIndex = 0; $coarseIndex < $numBins - 1; $coarseIndex++ ) {
+                               if ( $coarseBoundaries[$coarseIndex] > $i ) {
+                                       $coarseHistogram[$coarseIndex] += $val;
+                                       break;
+                               }
+                       }
+                       if ( $coarseIndex == $numBins - 1 ) {
+                               $coarseHistogram[$coarseIndex] += $val;
+                       }
+                       $raw .= $val;
+               }
+
+               $this->output( "Sort key size histogram\nRaw data: $raw\n\n" );
+
+               $maxBinVal = max( $coarseHistogram );
+               $scale = 60 / $maxBinVal;
+               $prevBoundary = 0;
+               for ( $coarseIndex = 0; $coarseIndex < $numBins; $coarseIndex++ ) {
+                       if ( !isset( $coarseHistogram[$coarseIndex] ) ) {
+                               $val = 0;
+                       } else {
+                               $val = $coarseHistogram[$coarseIndex];
+                       }
+                       $boundary = $coarseBoundaries[$coarseIndex];
+                       $this->output( sprintf( "%-10s %-10d |%s\n",
+                               $prevBoundary . '-' . ( $boundary - 1 ) . ': ',
+                               $val,
+                               str_repeat( '*', $scale * $val ) ) );
+                       $prevBoundary = $boundary;
+               }
        }
 }
 
index 6046103..f8f6e95 100644 (file)
@@ -42,4 +42,4 @@
        /* TODO: eliminate duplication of jquery.arrowSteps.head.png embedding */
        /* @embed */
        background: url(images/jquery.arrowSteps.head-ltr.png) no-repeat right center;
-}
\ No newline at end of file
+}
index f963754..1b414dd 100644 (file)
@@ -1,21 +1,21 @@
 /**
  * jQuery arrowSteps plugin
  * Copyright Neil Kandalgaonkar, 2010
- * 
- * This work is licensed under the terms of the GNU General Public License, 
- * version 2 or later. 
- * (see http://www.fsf.org/licensing/licenses/gpl.html). 
- * Derivative works and later versions of the code must be free software 
+ *
+ * This work is licensed under the terms of the GNU General Public License,
+ * version 2 or later.
+ * (see http://www.fsf.org/licensing/licenses/gpl.html).
+ * Derivative works and later versions of the code must be free software
  * licensed under the same or a compatible license.
  *
  *
  * DESCRIPTION
  *
- * Show users their progress through a series of steps, via a row of items that fit 
+ * Show users their progress through a series of steps, via a row of items that fit
  * together like arrows. One item can be highlighted at a time.
  *
  *
- * SYNOPSIS 
+ * SYNOPSIS
  *
  * <ul id="robin-hood-daffy">
  *   <li id="guard"><div>Guard!</div></li>
  *   <li id="thrust"><div>Thrust!</div></li>
  * </ul>
  *
- * <script language="javascript"><!-- 
+ * <script>
  *   $( '#robin-hood-daffy' ).arrowSteps();
  *
  *   $( '#robin-hood-daffy' ).arrowStepsHighlight( '#guard' );
  *   // 'Guard!' is highlighted.
  *
  *   // ... user completes the 'guard' step ...
- * 
+ *
  *   $( '#robin-hood-daffy' ).arrowStepsHighlight( '#turn' );
  *   // 'Turn!' is highlighted.
- *
- *   //-->
  * </script>
  *
  */
 
-( function( $j ) { 
-       $j.fn.arrowSteps = function() {
+( function ( $ ) {
+       $.fn.arrowSteps = function () {
                this.addClass( 'arrowSteps' );
                var $steps = this.find( 'li' );
 
                var width = parseInt( 100 / $steps.length, 10 );
                $steps.css( 'width', width + '%' );
 
-               // every step except the last one has an arrow at the right hand side. Also add in the padding 
+               // every step except the last one has an arrow at the right hand side. Also add in the padding
                // for the calculated arrow width.
                var arrowWidth = parseInt( this.outerHeight(), 10 );
                $steps.filter( ':not(:last-child)' ).addClass( 'arrow' )
                this.data( 'arrowSteps', $steps );
                return this;
        };
-       
-       $j.fn.arrowStepsHighlight = function( selector ) {
+
+       $.fn.arrowStepsHighlight = function ( selector ) {
                var $steps = this.data( 'arrowSteps' );
                var $previous;
-               $j.each( $steps, function( i, step ) {
-                       var $step = $j( step );
+               $.each( $steps, function ( i, step ) {
+                       var $step = $( step );
                        if ( $step.is( selector ) ) {
                                if ($previous) {
                                        $previous.addClass( 'tail' );
@@ -75,7 +73,7 @@
                                $step.removeClass( 'head tail lasthead' );
                        }
                        $previous = $step;
-               } ); 
+               } );
        };
 
-} )( jQuery );
+}( jQuery ) );
index 9a5fcc9..23ba074 100644 (file)
@@ -1,23 +1,26 @@
 /**
  * Plugin that automatically truncates the plain text contents of an element and adds an ellipsis
  */
-( function( $ ) {
+( function ( $ ) {
 
 // Cache ellipsed substrings for every string-width-position combination
 var cache = { };
 // Use a separate cache when match highlighting is enabled
 var matchTextCache = { };
 
-$.fn.autoEllipsis = function( options ) {
+$.fn.autoEllipsis = function ( options ) {
        options = $.extend( {
-               'position': 'center',
-               'tooltip': false,
-               'restoreText': false,
-               'hasSpan': false,
-               'matchText': null
+               position: 'center',
+               tooltip: false,
+               restoreText: false,
+               hasSpan: false,
+               matchText: null
        }, options );
-       $(this).each( function() {
-               var $el = $(this);
+       $(this).each( function () {
+               var $container, $trimmableText,
+                       text, trimmableText, w, pw,
+                       l, r, i, side,
+                       $el = $(this);
                if ( options.restoreText ) {
                        if ( !$el.data( 'autoEllipsis.originalText' ) ) {
                                $el.data( 'autoEllipsis.originalText', $el.text() );
@@ -27,16 +30,13 @@ $.fn.autoEllipsis = function( options ) {
                }
 
                // container element - used for measuring against
-               var $container = $el;
-               // trimmable text element - only the text within this element will be trimmed
-               var $trimmableText = null;
-               // protected text element - the width of this element is counted, but next is never trimmed from it
-               var $protectedText = null;
+               $container = $el;
 
+               // trimmable text element - only the text within this element will be trimmed
                if ( options.hasSpan ) {
                        $trimmableText = $el.children( options.selector );
                } else {
-                       $trimmableText = $( '<span />' )
+                       $trimmableText = $( '<span>' )
                                .css( 'whiteSpace', 'nowrap' )
                                .text( $el.text() );
                        $el
@@ -44,10 +44,11 @@ $.fn.autoEllipsis = function( options ) {
                                .append( $trimmableText );
                }
 
-               var text = $container.text();
-               var trimmableText = $trimmableText.text();
-               var w = $container.width();
-               var pw = $protectedText ? $protectedText.width() : 0;
+               text = $container.text();
+               trimmableText = $trimmableText.text();
+               w = $container.width();
+               pw = 0;
+
                // Try cache
                if ( options.matchText ) {
                        if ( !( text in matchTextCache ) ) {
@@ -86,7 +87,8 @@ $.fn.autoEllipsis = function( options ) {
                        switch ( options.position ) {
                                case 'right':
                                        // Use binary search-like technique for efficiency
-                                       var l = 0, r = trimmableText.length;
+                                       l = 0;
+                                       r = trimmableText.length;
                                        do {
                                                var m = Math.ceil( ( l + r ) / 2 );
                                                $trimmableText.text( trimmableText.substr( 0, m ) + '...' );
@@ -101,9 +103,10 @@ $.fn.autoEllipsis = function( options ) {
                                        break;
                                case 'center':
                                        // TODO: Use binary search like for 'right'
-                                       var i = [Math.round( trimmableText.length / 2 ), Math.round( trimmableText.length / 2 )];
-                                       var side = 1; // Begin with making the end shorter
-                                       while ( $trimmableText.outerWidth() + pw > w  && i[0] > 0 ) {
+                                       i = [Math.round( trimmableText.length / 2 ), Math.round( trimmableText.length / 2 )];
+                                       // Begin with making the end shorter
+                                       side = 1;
+                                       while ( $trimmableText.outerWidth() + pw > w && i[0] > 0 ) {
                                                $trimmableText.text( trimmableText.substr( 0, i[0] ) + '...' + trimmableText.substr( i[1] ) );
                                                // Alternate between trimming the end and begining
                                                if ( side === 0 ) {
@@ -119,7 +122,7 @@ $.fn.autoEllipsis = function( options ) {
                                        break;
                                case 'left':
                                        // TODO: Use binary search like for 'right'
-                                       var r = 0;
+                                       r = 0;
                                        while ( $trimmableText.outerWidth() + pw > w && r < trimmableText.length ) {
                                                $trimmableText.text( '...' + trimmableText.substr( r ) );
                                                r++;
@@ -140,4 +143,4 @@ $.fn.autoEllipsis = function( options ) {
        } );
 };
 
-} )( jQuery );
\ No newline at end of file
+}( jQuery ) );
\ No newline at end of file
index 20fa5c8..3d5b720 100644 (file)
@@ -3,9 +3,9 @@
  *
  * Calculate the byte length of a string (accounting for UTF-8).
  *
- * @author Jan Paul Posma
+ * @author Jan Paul Posma, 2011
  */
-jQuery.byteLength = function( str ) {
+jQuery.byteLength = function ( str ) {
 
        // This basically figures out how many bytes a UTF-16 string (which is what js sees)
        // will take in UTF-8 by replacing a 2 byte character with 2 *'s, etc, and counting that.
@@ -16,4 +16,4 @@ jQuery.byteLength = function( str ) {
                .replace( /[\u0080-\u07FF\uD800-\uDFFF]/g, '**' )
                .replace( /[\u0800-\uD7FF\uE000-\uFFFF]/g, '***' )
                .length;
-}
+};
index d8f4bfc..484651e 100644 (file)
@@ -4,7 +4,7 @@
  * @author Jan Paul Posma, 2011
  * @author Timo Tijhof, 2011-2012
  */
-( function ( $, undefined ) {
+( function ( $ ) {
 
        /**
         * Enforces a byte limit to a textbox, so that UTF-8 entries are counted as well, when, for example,
@@ -61,7 +61,7 @@
                        }
        
                        // Save function for reference
-                       $el.data( 'byteLimit-callback', fn );
+                       $el.data( 'byteLimitCallback', fn );
        
                        // We've got something, go for it:
                        $el.keypress( function ( e ) {
index 0a1d7d7..3d7f94d 100644 (file)
@@ -7,22 +7,22 @@
  * @license GPL v2
  */
 ( function( $ ) {
-$.fn.checkboxShiftClick = function( text ) {
-       var prevCheckbox = null;
-       var $box = this;
-       // When our boxes are clicked..
-       $box.click( function( e ) {
-               // And one has been clicked before...
-               if ( prevCheckbox !== null && e.shiftKey ) {
-                       // Check or uncheck this one and all in-between checkboxes
-                       $box.slice(
-                               Math.min( $box.index( prevCheckbox ), $box.index( e.target ) ),
-                               Math.max( $box.index( prevCheckbox ), $box.index( e.target ) ) + 1
-                       ).prop( 'checked', e.target.checked ? true : false );
-               }
-               // Either way, update the prevCheckbox variable to the one clicked now
-               prevCheckbox = e.target;
-       } );
-       return $box;
-};
-} )( jQuery );
\ No newline at end of file
+       $.fn.checkboxShiftClick = function ( text ) {
+               var prevCheckbox = null;
+               var $box = this;
+               // When our boxes are clicked..
+               $box.click( function ( e ) {
+                       // And one has been clicked before...
+                       if ( prevCheckbox !== null && e.shiftKey ) {
+                               // Check or uncheck this one and all in-between checkboxes
+                               $box.slice(
+                                       Math.min( $box.index( prevCheckbox ), $box.index( e.target ) ),
+                                       Math.max( $box.index( prevCheckbox ), $box.index( e.target ) ) + 1
+                               ).prop( 'checked', e.target.checked ? true : false );
+                       }
+                       // Either way, update the prevCheckbox variable to the one clicked now
+                       prevCheckbox = e.target;
+               } );
+               return $box;
+       };
+}( jQuery ) );
index 2c6e257..26eea96 100644 (file)
@@ -32,6 +32,8 @@
                 *  }
                 */
                profile: function ( nav ) {
+                       /*jshint boss:true */
+
                        if ( nav === undefined ) {
                                nav = window.navigator;
                        }
                 * @return Boolean true if browser known or assumed to be supported, false if blacklisted
                 */
                test: function ( map, profile ) {
+                       /*jshint evil:true */
+
                        var conditions, dir, i, op, val;
                        profile = $.isPlainObject( profile ) ? profile : $.client.profile();
 
index 1784f86..cb25796 100644 (file)
@@ -1,30 +1,34 @@
-/*
+/**
  * Collapsible tabs jQuery Plugin
  */
-( function( $ ) {
-       $.fn.collapsibleTabs = function( options ) {
+( function ( $ ) {
+       $.fn.collapsibleTabs = function ( options ) {
                // return if the function is called on an empty jquery object
-               if( !this.length ) return this;
-               //merge options into the defaults
+               if ( !this.length ) {
+                       return this;
+               }
+               // Merge options into the defaults
                var $settings = $.extend( {}, $.collapsibleTabs.defaults, options );
 
-               this.each( function() {
-                       var $this = $( this );
+               this.each( function () {
+                       var $el = $( this );
                        // add the element to our array of collapsible managers
-                       $.collapsibleTabs.instances = ( $.collapsibleTabs.instances.length == 0 ?
-                               $this : $.collapsibleTabs.instances.add( $this ) );
+                       $.collapsibleTabs.instances = ( $.collapsibleTabs.instances.length === 0 ?
+                               $el : $.collapsibleTabs.instances.add( $el ) );
                        // attach the settings to the elements
-                       $this.data( 'collapsibleTabsSettings', $settings );
+                       $el.data( 'collapsibleTabsSettings', $settings );
                        // attach data to our collapsible elements
-                       $this.children( $settings.collapsible ).each( function() {
+                       $el.children( $settings.collapsible ).each( function () {
                                $.collapsibleTabs.addData( $( this ) );
                        } );
                } );
 
                // if we haven't already bound our resize hanlder, bind it now
-               if( !$.collapsibleTabs.boundEvent ) {
+               if ( !$.collapsibleTabs.boundEvent ) {
                        $( window )
-                               .delayedBind( '500', 'resize', function( ) { $.collapsibleTabs.handleResize(); } );
+                               .delayedBind( '500', 'resize', function ( ) {
+                                       $.collapsibleTabs.handleResize();
+                               } );
                }
                // call our resize handler to setup the page
                $.collapsibleTabs.handleResize();
                        collapsedContainer: '#p-cactions ul',
                        collapsible: 'li.collapsible',
                        shifting: false,
-                       expandCondition: function( eleWidth ) {
+                       expandCondition: function ( eleWidth ) {
                                return ( $( '#left-navigation' ).position().left + $( '#left-navigation' ).width() )
                                        < ( $( '#right-navigation' ).position().left - eleWidth );
                        },
-                       collapseCondition: function() {
+                       collapseCondition: function () {
                                return ( $( '#left-navigation' ).position().left + $( '#left-navigation' ).width() )
                                        > $( '#right-navigation' ).position().left;
                        }
                },
-               addData: function( $collapsible ) {
+               addData: function ( $collapsible ) {
                        var $settings = $collapsible.parent().data( 'collapsibleTabsSettings' );
-                       if ( $settings != null ) {
+                       if ( $settings !== null ) {
                                $collapsible.data( 'collapsibleTabsSettings', {
-                                       'expandedContainer': $settings.expandedContainer,
-                                       'collapsedContainer': $settings.collapsedContainer,
-                                       'expandedWidth': $collapsible.width(),
-                                       'prevElement': $collapsible.prev()
+                                       expandedContainer: $settings.expandedContainer,
+                                       collapsedContainer: $settings.collapsedContainer,
+                                       expandedWidth: $collapsible.width(),
+                                       prevElement: $collapsible.prev()
                                } );
                        }
                },
-               getSettings: function( $collapsible ) {
+               getSettings: function ( $collapsible ) {
                        var $settings = $collapsible.data( 'collapsibleTabsSettings' );
-                       if ( typeof $settings == 'undefined' ) {
+                       if ( $settings === undefined ) {
                                $.collapsibleTabs.addData( $collapsible );
                                $settings = $collapsible.data( 'collapsibleTabsSettings' );
                        }
                        return $settings;
                },
-               handleResize: function( e ){
-                       $.collapsibleTabs.instances.each( function() {
-                               var $this = $( this ), data = $.collapsibleTabs.getSettings( $this );
-                               if( data.shifting ) return;
+               handleResize: function ( e ) {
+                       $.collapsibleTabs.instances.each( function () {
+                               var $el = $( this ),
+                                       data = $.collapsibleTabs.getSettings( $el );
+
+                               if ( data.shifting ) {
+                                       return;
+                               }
 
                                // if the two navigations are colliding
-                               if( $this.children( data.collapsible ).length > 0 && data.collapseCondition() ) {
+                               if ( $el.children( data.collapsible ).length > 0 && data.collapseCondition() ) {
 
-                                       $this.trigger( "beforeTabCollapse" );
+                                       $el.trigger( 'beforeTabCollapse' );
                                        // move the element to the dropdown menu
-                                       $.collapsibleTabs.moveToCollapsed( $this.children( data.collapsible + ':last' ) );
+                                       $.collapsibleTabs.moveToCollapsed( $el.children( data.collapsible + ':last' ) );
                                }
 
                                // if there are still moveable items in the dropdown menu,
                                // and there is sufficient space to place them in the tab container
-                               if( $( data.collapsedContainer + ' ' + data.collapsible ).length > 0
+                               if ( $( data.collapsedContainer + ' ' + data.collapsible ).length > 0
                                                && data.expandCondition( $.collapsibleTabs.getSettings( $( data.collapsedContainer ).children(
-                                                               data.collapsible+":first" ) ).expandedWidth ) ) {
+                                                               data.collapsible + ':first' ) ).expandedWidth ) ) {
                                        //move the element from the dropdown to the tab
-                                       $this.trigger( "beforeTabExpand" );
+                                       $el.trigger( 'beforeTabExpand' );
                                        $.collapsibleTabs
-                                               .moveToExpanded( data.collapsedContainer + " " + data.collapsible + ':first' );
+                                               .moveToExpanded( data.collapsedContainer + ' ' + data.collapsible + ':first' );
                                }
                        });
                },
-               moveToCollapsed: function( ele ) {
-                       var $moving = $( ele );
-                       var data = $.collapsibleTabs.getSettings( $moving );
-                       var dataExp = $.collapsibleTabs.getSettings( data.expandedContainer );
+               moveToCollapsed: function ( ele ) {
+                       var $moving = $( ele ),
+                               data = $.collapsibleTabs.getSettings( $moving ),
+                               dataExp = $.collapsibleTabs.getSettings( data.expandedContainer );
                        dataExp.shifting = true;
                        $moving
                                .detach()
                        dataExp.shifting = false;
                        $.collapsibleTabs.handleResize();
                },
-               moveToExpanded: function( ele ) {
-                       var $moving = $( ele );
-                       var data = $.collapsibleTabs.getSettings( $moving );
-                       var dataExp = $.collapsibleTabs.getSettings( data.expandedContainer );
+               moveToExpanded: function ( ele ) {
+                       var $moving = $( ele ),
+                               data = $.collapsibleTabs.getSettings( $moving ),
+                               dataExp = $.collapsibleTabs.getSettings( data.expandedContainer );
                        dataExp.shifting = true;
                        // remove this element from where it's at and put it in the dropdown menu
                        $moving.detach().insertAfter( data.prevElement ).data( 'collapsibleTabsSettings', data );
                        $.collapsibleTabs.handleResize();
                }
        };
-} )( jQuery );
+
+}( jQuery ) );
index 8a619b5..8bc45c9 100644 (file)
@@ -1,44 +1,54 @@
 /**
  * jQuery Color Animations
- * Copyright 2007 John Resig
+ *
+ * @author John Resig, 2007
+ * @author Krinkle, 2011
  * Released under the MIT and GPL licenses.
  *
- * - 2011-01-05: Modified by Krinkle to use the jQuery.colorUtil plugin (which has to be loaded first!)
+ * - 2011-01-05: Forked for MediaWiki. See also jQuery.colorUtil plugin
  */
-(function( $ ) {
+( function ( $ ) {
 
-       // We override the animation for all of these color styles
-       $.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor', 'color', 'outlineColor'],
-               function( i, attr ) {
-                       $.fx.step[attr] = function( fx ) {
-                               if ( fx.state == 0 ) {
-                                       fx.start = getColor( fx.elem, attr );
-                                       fx.end = $.colorUtil.getRGB( fx.end );
-                               }
-
-                               fx.elem.style[attr] = 'rgb(' + [
-                                       Math.max(Math.min( parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0]), 255), 0),
-                                       Math.max(Math.min( parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1]), 255), 0),
-                                       Math.max(Math.min( parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2]), 255), 0)
-                               ].join( ',' ) + ')';
-                       }
-               }
-       );
-
-       function getColor(elem, attr) {
+       function getColor( elem, attr ) {
+               /*jshint boss:true */
                var color;
 
                do {
-                       color = $.curCSS(elem, attr);
+                       color = $.curCSS( elem, attr );
 
                        // Keep going until we find an element that has color, or we hit the body
-                       if ( color != '' && color != 'transparent' || $.nodeName(elem, 'body') )
+                       if ( color !== '' && color !== 'transparent' || $.nodeName( elem, 'body' ) ) {
                                break;
+                       }
 
                        attr = 'backgroundColor';
                } while ( elem = elem.parentNode );
 
-               return $.colorUtil.getRGB(color);
-       };
+               return $.colorUtil.getRGB( color );
+       }
+
+       // We override the animation for all of these color styles
+       $.each([
+               'backgroundColor',
+               'borderBottomColor',
+               'borderLeftColor',
+               'borderRightColor',
+               'borderTopColor',
+               'color',
+               'outlineColor'
+       ], function ( i, attr ) {
+               $.fx.step[attr] = function ( fx ) {
+                       if ( fx.state === 0 ) {
+                               fx.start = getColor( fx.elem, attr );
+                               fx.end = $.colorUtil.getRGB( fx.end );
+                       }
+
+                       fx.elem.style[attr] = 'rgb(' + [
+                               Math.max( Math.min( parseInt( (fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0], 10 ), 255 ), 0 ),
+                               Math.max( Math.min( parseInt( (fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1], 10 ), 255 ), 0 ),
+                               Math.max( Math.min( parseInt( (fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2], 10 ), 255 ), 0 )
+                       ].join( ',' ) + ')';
+               };
+       } );
 
-} )( jQuery );
+}( jQuery ) );
index 1116aec..c1fe7fe 100644 (file)
  * jQuery Color Utilities
  * Written by Krinkle in 2011
  * Released under the MIT and GPL licenses.
- * Mostly based on other plugins and functions (taken through JSLint and optimized a little).
- * Sources cited locally.
+ * Mostly based on other plugins and functions (linted and optimized a little).
+ * Sources cited inline.
  */
-( function( $ ) {
-$.colorUtil = {
-
-       // Color Conversion function from highlightFade
-       // By Blair Mitchelmore
-       // http://jquery.offput.ca/highlightFade/
-       // Parse strings looking for color tuples [255,255,255]
-       getRGB : function( color ) {
-               var result;
-
-               // Check if we're already dealing with an array of colors
-               if ( color && color.constructor == Array && color.length == 3 ){
-                       return color;
-               }
+( function ( $ ) {
+       $.colorUtil = {
 
-               // Look for rgb(num,num,num)
-               if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) {
-                       return [parseInt(result[1],10), parseInt(result[2],10), parseInt(result[3],10)];
-               }
+               // Color Conversion function from highlightFade
+               // By Blair Mitchelmore
+               // http://jquery.offput.ca/highlightFade/
+               // Parse strings looking for color tuples [255,255,255]
+               getRGB : function ( color ) {
+                       /*jshint boss:true */
+                       var result;
 
-               // Look for rgb(num%,num%,num%)
-               if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) {
-                       return [parseFloat(result[1],10)*2.55, parseFloat(result[2],10)*2.55, parseFloat(result[3])*2.55];
-               }
+                       // Check if we're already dealing with an array of colors
+                       if ( color && $.isArray( color ) && color.length === 3 ) {
+                               return color;
+                       }
 
-               // Look for #a0b1c2
-               if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) {
-                       return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)];
-               }
+                       // Look for rgb(num,num,num)
+                       if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) {
+                               return [parseInt(result[1],10), parseInt(result[2],10), parseInt(result[3],10)];
+                       }
 
-               // Look for #fff
-               if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) {
-                       return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)];
-               }
+                       // Look for rgb(num%,num%,num%)
+                       if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) {
+                               return [parseFloat(result[1],10)*2.55, parseFloat(result[2],10)*2.55, parseFloat(result[3])*2.55];
+                       }
 
-               // Look for rgba(0, 0, 0, 0) == transparent in Safari 3
-               if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) {
-                       return $.colorUtil.colors.transparent;
-               }
+                       // Look for #a0b1c2
+                       if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) {
+                               return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)];
+                       }
 
-               // Otherwise, we're most likely dealing with a named color
-               return $.colorUtil.colors[$.trim(color).toLowerCase()];
-       },
-
-       // Some named colors to work with
-       // From Interface by Stefan Petre
-       // http://interface.eyecon.ro/
-       colors: {
-               aqua:[0,255,255],
-               azure:[240,255,255],
-               beige:[245,245,220],
-               black:[0,0,0],
-               blue:[0,0,255],
-               brown:[165,42,42],
-               cyan:[0,255,255],
-               darkblue:[0,0,139],
-               darkcyan:[0,139,139],
-               darkgrey:[169,169,169],
-               darkgreen:[0,100,0],
-               darkkhaki:[189,183,107],
-               darkmagenta:[139,0,139],
-               darkolivegreen:[85,107,47],
-               darkorange:[255,140,0],
-               darkorchid:[153,50,204],
-               darkred:[139,0,0],
-               darksalmon:[233,150,122],
-               darkviolet:[148,0,211],
-               fuchsia:[255,0,255],
-               gold:[255,215,0],
-               green:[0,128,0],
-               indigo:[75,0,130],
-               khaki:[240,230,140],
-               lightblue:[173,216,230],
-               lightcyan:[224,255,255],
-               lightgreen:[144,238,144],
-               lightgrey:[211,211,211],
-               lightpink:[255,182,193],
-               lightyellow:[255,255,224],
-               lime:[0,255,0],
-               magenta:[255,0,255],
-               maroon:[128,0,0],
-               navy:[0,0,128],
-               olive:[128,128,0],
-               orange:[255,165,0],
-               pink:[255,192,203],
-               purple:[128,0,128],
-               violet:[128,0,128],
-               red:[255,0,0],
-               silver:[192,192,192],
-               white:[255,255,255],
-               yellow:[255,255,0],
-               transparent: [255,255,255]
-       },
-       /**
-        * http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
-        * Converts an RGB color value to HSL. Conversion formula
-        * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
-        * Assumes r, g, and b are contained in the set [0, 255] and
-        * returns h, s, and l in the set [0, 1].
-        *
-        * @param       Number  R               The red color value
-        * @param       Number  G               The green color value
-        * @param       Number  B               The blue color value
-        * @return      Array                   The HSL representation
-        */
-       rgbToHsl: function( R, G, B ) {
-               var     r = R / 255,
-                       g = G / 255,
-                       b = B / 255;
-               var max = Math.max(r, g, b), min = Math.min(r, g, b);
-               var h, s, l = (max + min) / 2;
-
-               if(max == min){
-                       h = s = 0; // achromatic
-               }else{
-                       var d = max - min;
-                       s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
-                       switch(max){
-                               case r: h = (g - b) / d + (g < b ? 6 : 0); break;
-                               case g: h = (b - r) / d + 2; break;
-                               case b: h = (r - g) / d + 4; break;
+                       // Look for #fff
+                       if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) {
+                               return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)];
+                       }
+
+                       // Look for rgba(0, 0, 0, 0) == transparent in Safari 3
+                       if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) {
+                               return $.colorUtil.colors.transparent;
                        }
-                       h /= 6;
-               }
 
-               return [h, s, l];
-       },
-       /**
-        * http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
-        * Converts an HSL color value to RGB. Conversion formula
-        * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
-        * Assumes h, s, and l are contained in the set [0, 1] and
-        * returns r, g, and b in the set [0, 255].
-        *
-        * @param       Number  h               The hue
-        * @param       Number  s               The saturation
-        * @param       Number  l               The lightness
-        * @return      Array                   The RGB representation
-        */
-       hslToRgb: function( h, s, l ) {
-               var r, g, b;
-
-               if(s === 0){
-                       r = g = b = l; // achromatic
-               }else{
-                       var hue2rgb = function(p, q, t){
-                               if(t < 0){ t += 1; }
-                               if(t > 1){ t -= 1; }
-                               if(t < 1/6){ return p + (q - p) * 6 * t; }
-                               if(t < 1/2){ return q; }
-                               if(t < 2/3){ return p + (q - p) * (2/3 - t) * 6; }
-                               return p;
-                       };
-
-                       var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
-                       var p = 2 * l - q;
-                       r = hue2rgb(p, q, h + 1/3);
-                       g = hue2rgb(p, q, h);
-                       b = hue2rgb(p, q, h - 1/3);
+                       // Otherwise, we're most likely dealing with a named color
+                       return $.colorUtil.colors[$.trim(color).toLowerCase()];
+               },
+
+               // Some named colors to work with
+               // From Interface by Stefan Petre
+               // http://interface.eyecon.ro/
+               colors: {
+                       aqua: [0,255,255],
+                       azure: [240,255,255],
+                       beige: [245,245,220],
+                       black: [0,0,0],
+                       blue: [0,0,255],
+                       brown: [165,42,42],
+                       cyan: [0,255,255],
+                       darkblue: [0,0,139],
+                       darkcyan: [0,139,139],
+                       darkgrey: [169,169,169],
+                       darkgreen: [0,100,0],
+                       darkkhaki: [189,183,107],
+                       darkmagenta: [139,0,139],
+                       darkolivegreen: [85,107,47],
+                       darkorange: [255,140,0],
+                       darkorchid: [153,50,204],
+                       darkred: [139,0,0],
+                       darksalmon: [233,150,122],
+                       darkviolet: [148,0,211],
+                       fuchsia: [255,0,255],
+                       gold: [255,215,0],
+                       green: [0,128,0],
+                       indigo: [75,0,130],
+                       khaki: [240,230,140],
+                       lightblue: [173,216,230],
+                       lightcyan: [224,255,255],
+                       lightgreen: [144,238,144],
+                       lightgrey: [211,211,211],
+                       lightpink: [255,182,193],
+                       lightyellow: [255,255,224],
+                       lime: [0,255,0],
+                       magenta: [255,0,255],
+                       maroon: [128,0,0],
+                       navy: [0,0,128],
+                       olive: [128,128,0],
+                       orange: [255,165,0],
+                       pink: [255,192,203],
+                       purple: [128,0,128],
+                       violet: [128,0,128],
+                       red: [255,0,0],
+                       silver: [192,192,192],
+                       white: [255,255,255],
+                       yellow: [255,255,0],
+                       transparent: [255,255,255]
+               },
+
+               /**
+                * http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
+                * Converts an RGB color value to HSL. Conversion formula
+                * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+                * Assumes r, g, and b are contained in the set [0, 255] and
+                * returns h, s, and l in the set [0, 1].
+                *
+                * @param       Number  R               The red color value
+                * @param       Number  G               The green color value
+                * @param       Number  B               The blue color value
+                * @return      Array                   The HSL representation
+                */
+               rgbToHsl: function ( R, G, B ) {
+                       var r = R / 255,
+                               g = G / 255,
+                               b = B / 255;
+                       var max = Math.max( r, g, b ), min = Math.min( r, g, b );
+                       var h, s, l = (max + min) / 2;
+
+                       if ( max === min ) {
+                               // achromatic
+                               h = s = 0;
+                       } else {
+                               var d = max - min;
+                               s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+                               switch ( max ) {
+                                       case r:
+                                               h = (g - b) / d + (g < b ? 6 : 0);
+                                               break;
+                                       case g:
+                                               h = (b - r) / d + 2;
+                                               break;
+                                       case b:
+                                               h = (r - g) / d + 4;
+                                               break;
+                               }
+                               h /= 6;
+                       }
+
+                       return [h, s, l];
+               },
+
+               /**
+                * http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
+                * Converts an HSL color value to RGB. Conversion formula
+                * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+                * Assumes h, s, and l are contained in the set [0, 1] and
+                * returns r, g, and b in the set [0, 255].
+                *
+                * @param       Number  h               The hue
+                * @param       Number  s               The saturation
+                * @param       Number  l               The lightness
+                * @return      Array                   The RGB representation
+                */
+               hslToRgb: function ( h, s, l ) {
+                       var r, g, b;
+
+                       if ( s === 0 ) {
+                               r = g = b = l; // achromatic
+                       } else {
+                               var hue2rgb = function ( p, q, t ) {
+                                       if ( t < 0 ) {
+                                               t += 1;
+                                       }
+                                       if ( t > 1 ) {
+                                               t -= 1;
+                                       }
+                                       if ( t < 1/6 ) {
+                                               return p + (q - p) * 6 * t;
+                                       }
+                                       if ( t < 1/2 ) {
+                                               return q;
+                                       }
+                                       if ( t < 2/3 ) {
+                                               return p + (q - p) * (2/3 - t) * 6;
+                                       }
+                                       return p;
+                               };
+
+                               var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+                               var p = 2 * l - q;
+                               r = hue2rgb( p, q, h + 1/3 );
+                               g = hue2rgb( p, q, h );
+                               b = hue2rgb( p, q, h - 1/3 );
+                       }
+
+                       return [r * 255, g * 255, b * 255];
+               },
+
+               /**
+                * Get's a brighter or darker rgb() value string.
+                *
+                * @author Krinkle
+                *
+                * @example     getCSSColorMod( 'red', +0.1 )
+                * @example     getCSSColorMod( 'rgb(200,50,50)', -0.2 )
+                *
+                * @param       Mixed   currentColor current value in css
+                * @param       Number  mod wanted brightness modification between -1 and 1
+                * @return      String 'rgb(r,g,b)'
+                */
+               getColorBrightness: function ( currentColor, mod ) {
+                       var rgbArr = $.colorUtil.getRGB( currentColor ),
+                               hslArr = $.colorUtil.rgbToHsl(rgbArr[0], rgbArr[1], rgbArr[2] );
+                       rgbArr = $.colorUtil.hslToRgb(hslArr[0], hslArr[1], hslArr[2]+mod);
+
+                       return 'rgb(' +
+                               [parseInt( rgbArr[0], 10), parseInt( rgbArr[1], 10 ), parseInt( rgbArr[2], 10 )].join( ',' ) +
+                               ')';
                }
 
-               return [r * 255, g * 255, b * 255];
-       },
-       /**
-        * Get's a brighter or darker rgb() value string.
-        *
-        * @author Krinkle
-        *
-        * @example     getCSSColorMod( 'red', +0.1 )
-        * @example     getCSSColorMod( 'rgb(200,50,50)', -0.2 )
-        *
-        * @param       Mixed   currentColor current value in css
-        * @param       Number  mod wanted brightness modification between -1 and 1
-        * @return      String 'rgb(r,g,b)'
-        */
-       getColorBrightness: function( currentColor, mod ) {
-               var     rgbArr = $.colorUtil.getRGB( currentColor ),
-                       hslArr = $.colorUtil.rgbToHsl(rgbArr[0], rgbArr[1], rgbArr[2] );
-               rgbArr = $.colorUtil.hslToRgb(hslArr[0], hslArr[1], hslArr[2]+mod);
-               return 'rgb(' +
-                       [parseInt( rgbArr[0], 10), parseInt( rgbArr[1], 10 ), parseInt( rgbArr[2], 10 )].join( ',' ) +
-                       ')';
-       }
-
-};
-} )( jQuery );
\ No newline at end of file
+       };
+
+}( jQuery ) );
index d84ee26..5d32b6b 100644 (file)
@@ -1,4 +1,4 @@
-(function( $ ) {
+( function ( $ ) {
 /**
  * Function that escapes spaces in event names. This is needed because
  * "_delayedBind-foo bar-1000" refers to two events
@@ -18,25 +18,26 @@ $.fn.extend( {
         * @param data Data to pass to the event handler (optional)
         * @param callback Function to call
         */
-       delayedBind: function( timeout, event, data, callback ) {
-               if ( arguments.length == 3 ) {
+       delayedBind: function ( timeout, event, data, callback ) {
+               if ( arguments.length === 3 ) {
                        // Shift optional parameter down
                        callback = data;
                        data = undefined;
                }
                var encEvent = encodeEvent( event );
-               return this.each( function() {
+               return this.each( function () {
                        var that = this;
                        // Bind the top half
                        // Do this only once for every (event, timeout) pair
                        if (  !( $(this).data( '_delayedBindBound-' + encEvent + '-' + timeout ) ) ) {
                                $(this).data( '_delayedBindBound-' + encEvent + '-' + timeout, true );
-                               $(this).bind( event, function() {
+                               $(this).bind( event, function () {
                                        var timerID = $(this).data( '_delayedBindTimerID-' + encEvent + '-' + timeout );
                                        // Cancel the running timer
-                                       if ( typeof timerID != 'undefined' )
+                                       if ( timerID !== null ) {
                                                clearTimeout( timerID );
-                                       timerID = setTimeout( function() {
+                                       }
+                                       timerID = setTimeout( function () {
                                                $(that).trigger( '_delayedBind-' + encEvent + '-' + timeout );
                                        }, timeout );
                                        $(this).data( '_delayedBindTimerID-' + encEvent + '-' + timeout, timerID );
@@ -51,23 +52,25 @@ $.fn.extend( {
        /**
         * Cancel the timers for delayed events on the selected elements.
         */
-       delayedBindCancel: function( timeout, event ) {
+       delayedBindCancel: function ( timeout, event ) {
                var encEvent = encodeEvent( event );
-               return this.each( function() {
+               return this.each( function () {
                        var timerID = $(this).data( '_delayedBindTimerID-' + encEvent + '-' + timeout );
-                       if ( typeof timerID != 'undefined' )
+                       if ( timerID !== null ) {
                                clearTimeout( timerID );
+                       }
                } );
        },
        
        /**
         * Unbind an event bound with delayedBind()
         */
-       delayedBindUnbind: function( timeout, event, callback ) {
+       delayedBindUnbind: function ( timeout, event, callback ) {
                var encEvent = encodeEvent( event );
-               return this.each( function() {
+               return this.each( function () {
                        $(this).unbind( '_delayedBind-' + encEvent + '-' + timeout, callback );
                } );
        }
 } );
-} )( jQuery );
\ No newline at end of file
+
+}( jQuery ) );
index 3c65d01..a3396a2 100644 (file)
  * Options:
  *
  */
-( function( $ ) {
-
-$.expandableField = {
-       /**
-        * Expand the field, make the callback
-        */
-       expandField: function( e, context ) {
-               context.config.beforeExpand.call( context.data.$field, context );
-               context.data.$field
-                       .animate( { 'width': context.data.expandedWidth }, 'fast', function() {
-                               context.config.afterExpand.call( this, context );
-                       } );
-       },
-       /**
-        * Condense the field, make the callback
-        */
-       condenseField: function( e, context ) {
-               context.config.beforeCondense.call( context.data.$field, context );
-               context.data.$field
-                       .animate( { 'width': context.data.condensedWidth }, 'fast', function() {
-                               context.config.afterCondense.call( this, context );
-                       } );
-       },
-       /**
-        * Sets the value of a property, and updates the widget accordingly
-        * @param property String Name of property
-        * @param value Mixed Value to set property with
-        */
-       configure: function( context, property, value ) {
-               // Validate creation using fallback values
-               switch( property ) {
-                       default:
-                               context.config[property] = value;
-                               break;
-               }
-       }
-
-};
-$.fn.expandableField = function() {
-       
-       // Multi-context fields
-       var returnValue = null;
-       var args = arguments;
-       
-       $( this ).each( function() {
-
-               /* Construction / Loading */
-               
-               var context = $( this ).data( 'expandableField-context' );
-               if ( context == null ) {
-                       context = {
-                               config: {
-                                       // callback function for before collapse
-                                       'beforeCondense': function( context ) {},
-                                       // callback function for before expand
-                                       'beforeExpand': function( context ) {},
-                                       // callback function for after collapse
-                                       'afterCondense': function( context ) {},
-                                       // callback function for after expand
-                                       'afterExpand': function( context ) {},
-                                       // Whether the field should expand to the left or the right -- defaults to left
-                                       'expandToLeft': true
-                               }
-                       };
+( function ( $ ) {
+
+       $.expandableField = {
+               /**
+                * Expand the field, make the callback
+                */
+               expandField: function ( e, context ) {
+                       context.config.beforeExpand.call( context.data.$field, context );
+                       context.data.$field
+                               .animate( { 'width': context.data.expandedWidth }, 'fast', function () {
+                                       context.config.afterExpand.call( this, context );
+                               } );
+               },
+               /**
+                * Condense the field, make the callback
+                */
+               condenseField: function ( e, context ) {
+                       context.config.beforeCondense.call( context.data.$field, context );
+                       context.data.$field
+                               .animate( { 'width': context.data.condensedWidth }, 'fast', function () {
+                                       context.config.afterCondense.call( this, context );
+                               } );
+               },
+               /**
+                * Sets the value of a property, and updates the widget accordingly
+                * @param property String Name of property
+                * @param value Mixed Value to set property with
+                */
+               configure: function ( context, property, value ) {
+                       // TODO: Validate creation using fallback values
+                       context.config[property] = value;
                }
-               
-               /* API */
-               // Handle various calling styles
-               if ( args.length > 0 ) {
-                       if ( typeof args[0] == 'object' ) {
-                               // Apply set of properties
-                               for ( var key in args[0] ) {
-                                       $.expandableField.configure( context, key, args[0][key] );
-                               }
-                       } else if ( typeof args[0] == 'string' ) {
-                               if ( args.length > 1 ) {
-                                       // Set property values
-                                       $.expandableField.configure( context, args[0], args[1] );
-                               } else if ( returnValue == null ) {
-                                       // Get property values, but don't give access to internal data - returns only the first
-                                       returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
+
+       };
+
+       $.fn.expandableField = function () {
+
+               // Multi-context fields
+               var returnValue;
+               var args = arguments;
+
+               $( this ).each( function () {
+                       var key;
+
+                       /* Construction / Loading */
+
+                       var context = $( this ).data( 'expandableField-context' );
+
+                       // TODO: Do we need to check both null and undefined?
+                       if ( context === undefined || context === null ) {
+                               context = {
+                                       config: {
+                                               // callback function for before collapse
+                                               beforeCondense: function ( context ) {},
+
+                                               // callback function for before expand
+                                               beforeExpand: function ( context ) {},
+
+                                               // callback function for after collapse
+                                               afterCondense: function ( context ) {},
+
+                                               // callback function for after expand
+                                               afterExpand: function ( context ) {},
+
+                                               // Whether the field should expand to the left or the right -- defaults to left
+                                               expandToLeft: true
+                                       }
+                               };
+                       }
+
+                       /* API */
+                       // Handle various calling styles
+                       if ( args.length > 0 ) {
+                               if ( typeof args[0] === 'object' ) {
+                                       // Apply set of properties
+                                       for ( key in args[0] ) {
+                                               $.expandableField.configure( context, key, args[0][key] );
+                                       }
+                               } else if ( typeof args[0] === 'string' ) {
+                                       if ( args.length > 1 ) {
+                                               // Set property values
+                                               $.expandableField.configure( context, args[0], args[1] );
+
+                                       // TODO: Do we need to check both null and undefined?
+                                       } else if ( returnValue === null || returnValue === undefined ) {
+                                               // Get property values, but don't give access to internal data - returns only the first
+                                               returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
+                                       }
                                }
                        }
-               }
-               
-               /* Initialization */
-               
-               if ( typeof context.data == 'undefined' ) {
-                       context.data = {
-                               // The width of the field in it's condensed state
-                               'condensedWidth': $( this ).width(),
-                               // The width of the field in it's expanded state
-                               'expandedWidth': $( this ).width() * 2,
-                               // Reference to the field
-                               '$field': $( this )
-                       };
-                       
-                       $( this )
-                               .addClass( 'expandableField' )
-                               .focus( function( e ) {
-                                       $.expandableField.expandField( e, context );
-                               } )
-                               .delayedBind( 250, 'blur', function( e ) {
-                                       $.expandableField.condenseField( e, context );
-                               } );
-               }
-               // Store the context for next time
-               $( this ).data( 'expandableField-context', context );
-       } );
-       return returnValue !== null ? returnValue : $(this);
-};
 
-} )( jQuery );
+                       /* Initialization */
+
+                       if ( context.data === undefined ) {
+                               context.data = {
+                                       // The width of the field in it's condensed state
+                                       condensedWidth: $( this ).width(),
+
+                                       // The width of the field in it's expanded state
+                                       expandedWidth: $( this ).width() * 2,
+
+                                       // Reference to the field
+                                       $field: $( this )
+                               };
+
+                               $( this )
+                                       .addClass( 'expandableField' )
+                                       .focus( function ( e ) {
+                                               $.expandableField.expandField( e, context );
+                                       } )
+                                       .delayedBind( 250, 'blur', function ( e ) {
+                                               $.expandableField.condenseField( e, context );
+                                       } );
+                       }
+                       // Store the context for next time
+                       $( this ).data( 'expandableField-context', context );
+               } );
+               return returnValue !== undefined ? returnValue : $(this);
+       };
+
+}( jQuery ) );
index c05012d..25b806b 100644 (file)
@@ -3,8 +3,8 @@
  *
  * @author Timo Tijhof, 2011
  */
-jQuery.fn.getAttrs = function( all ) {
-       var     map = this[0].attributes,
+jQuery.fn.getAttrs = function ( all ) {
+       var map = this[0].attributes,
                attrs = {},
                len = map.length,
                i, v;
index 42b1194..fa4416c 100644 (file)
@@ -1,64 +1,67 @@
 /**
- * Plugin that highlights matched word partials in a given element
- * TODO: add a function for restoring the previous text
- * TODO: accept mappings for converting shortcuts like WP: to Wikipedia: 
+ * Plugin that highlights matched word partials in a given element.
+ * TODO: Add a function for restoring the previous text.
+ * TODO: Accept mappings for converting shortcuts like WP: to Wikipedia:.
  */
-( function( $ ) {
+( function ( $ ) {
 
-$.highlightText = {
-       
-       // Split our pattern string at spaces and run our highlight function on the results
-       splitAndHighlight: function( node, pat ) {
-               var patArray = pat.split(" ");
-               for ( var i = 0; i < patArray.length; i++ ) {
-                       if ( patArray[i].length == 0 ) continue;
-                       $.highlightText.innerHighlight( node, patArray[i] );
-               }
-               return node;
-       },
-       // scans a node looking for the pattern and wraps a span around each match 
-       innerHighlight: function( node, pat ) {
-               // if this is a text node
-               if ( node.nodeType == 3 ) {
-                       // TODO - need to be smarter about the character matching here. 
-                       // non latin characters can make regex think a new word has begun: do not use \b
-                       // http://stackoverflow.com/questions/3787072/regex-wordwrap-with-utf8-characters-in-js
-                       // look for an occurrence of our pattern and store the starting position
-                       var match = node.data.match( new RegExp( "(^|\\s)" + $.escapeRE( pat ), "i" ) );
-                       if ( match ) {
-                               var pos = match.index + match[1].length; // include length of any matched spaces
-                               // create the span wrapper for the matched text
-                               var spannode = document.createElement( 'span' );
-                               spannode.className = 'highlight';
-                               // shave off the characters preceding the matched text
-                               var middlebit = node.splitText( pos );
-                               // shave off any unmatched text off the end
-                               middlebit.splitText( pat.length );
-                               // clone for appending to our span
-                               var middleclone = middlebit.cloneNode( true );
-                               // append the matched text node to the span
-                               spannode.appendChild( middleclone );
-                               // replace the matched node, with our span-wrapped clone of the matched node
-                               middlebit.parentNode.replaceChild( spannode, middlebit );
+       $.highlightText = {
+
+               // Split our pattern string at spaces and run our highlight function on the results
+               splitAndHighlight: function ( node, pat ) {
+                       var patArray = pat.split( ' ' );
+                       for ( var i = 0; i < patArray.length; i++ ) {
+                               if ( patArray[i].length === 0 ) {
+                                       continue;
+                               }
+                               $.highlightText.innerHighlight( node, patArray[i] );
                        }
-               // if this is an element with childnodes, and not a script, style or an element we created
-               } else if ( node.nodeType == 1 && node.childNodes && !/(script|style)/i.test( node.tagName )
-                               && !( node.tagName.toLowerCase() == 'span' && node.className.match( /\bhighlight/ ) ) ) {
-                       for ( var i = 0; i < node.childNodes.length; ++i ) {
-                               // call the highlight function for each child node
-                               $.highlightText.innerHighlight( node.childNodes[i], pat );
+                       return node;
+               },
+
+               // scans a node looking for the pattern and wraps a span around each match
+               innerHighlight: function ( node, pat ) {
+                       // if this is a text node
+                       if ( node.nodeType === 3 ) {
+                               // TODO - need to be smarter about the character matching here.
+                               // non latin characters can make regex think a new word has begun: do not use \b
+                               // http://stackoverflow.com/questions/3787072/regex-wordwrap-with-utf8-characters-in-js
+                               // look for an occurrence of our pattern and store the starting position
+                               var match = node.data.match( new RegExp( "(^|\\s)" + $.escapeRE( pat ), "i" ) );
+                               if ( match ) {
+                                       var pos = match.index + match[1].length; // include length of any matched spaces
+                                       // create the span wrapper for the matched text
+                                       var spannode = document.createElement( 'span' );
+                                       spannode.className = 'highlight';
+                                       // shave off the characters preceding the matched text
+                                       var middlebit = node.splitText( pos );
+                                       // shave off any unmatched text off the end
+                                       middlebit.splitText( pat.length );
+                                       // clone for appending to our span
+                                       var middleclone = middlebit.cloneNode( true );
+                                       // append the matched text node to the span
+                                       spannode.appendChild( middleclone );
+                                       // replace the matched node, with our span-wrapped clone of the matched node
+                                       middlebit.parentNode.replaceChild( spannode, middlebit );
+                               }
+                       // if this is an element with childnodes, and not a script, style or an element we created
+                       } else if ( node.nodeType === 1 && node.childNodes && !/(script|style)/i.test( node.tagName )
+                                       && !( node.tagName.toLowerCase() === 'span' && node.className.match( /\bhighlight/ ) ) ) {
+                               for ( var i = 0; i < node.childNodes.length; ++i ) {
+                                       // call the highlight function for each child node
+                                       $.highlightText.innerHighlight( node.childNodes[i], pat );
+                               }
                        }
                }
-       }
-};
+       };
 
-$.fn.highlightText = function( matchString ) {
-       return $( this ).each( function() {
-               var $this = $( this );
-               $this.data( 'highlightText', { originalText: $this.text() } );
-               $.highlightText.splitAndHighlight( this, matchString );
-       } );
-};
+       $.fn.highlightText = function ( matchString ) {
+               return $( this ).each( function () {
+                       var $el = $( this );
+                       $el.data( 'highlightText', { originalText: $el.text() } );
+                       $.highlightText.splitAndHighlight( this, matchString );
+               } );
+       };
 
-} )( jQuery );
+}( jQuery ) );
 
index 42554e0..27cae29 100644 (file)
@@ -18,7 +18,7 @@
  *                     <img src="something.jpg" title="My Title Message" alt="My Alt Message" />
  *             </p>
  */
-( function( $ ) {
+( function ( $, mw ) {
 /**
  * Localizes a DOM selection by replacing <html:msg /> elements with localized text and adding
  * localized title and alt attributes to elements with title-msg and alt-msg attributes
  *  * prefix: Message prefix to use when localizing elements and attributes
  */
 
-$.fn.localize = function( options ) {
-       options = $.extend( { 'prefix': '', 'keys': {}, 'params': {} }, options );
+$.fn.localize = function ( options ) {
+       options = $.extend( {
+               prefix: '',
+               keys: {},
+               params: {}
+       }, options );
+
        function msg( key ) {
                var args = key in options.params ? options.params[key] : [];
                // Format: mw.msg( key [, p1, p2, ...] )
                args.unshift( options.prefix + ( key in options.keys ? options.keys[key] : key ) );
                return mw.msg.apply( mw, args );
-       };
+       }
+
        return $(this)
                // Ok, so here's the story on this selector.
                // In IE 6/7, searching for 'msg' turns up the 'html:msg', but searching for 'html:msg' does not.
@@ -43,11 +49,11 @@ $.fn.localize = function( options ) {
                // So searching for both 'msg' and 'html:msg' seems to get the job done.
                // This feels pretty icky, though.
                .find( 'msg,html\\:msg' )
-                       .each( function() {
+                       .each( function () {
                                var $el = $(this);
                                var msgText = msg( $el.attr( 'key' ) );
 
-                               if ( $el.attr('raw') ) {
+                               if ( $el.attr( 'raw' ) ) {
                                        $el.html(msgText);
                                } else {
                                        $el.text(msgText);
@@ -58,7 +64,7 @@ $.fn.localize = function( options ) {
                        } )
                        .end()
                .find( '[title-msg]' )
-                       .each( function() {
+                       .each( function () {
                                var $el = $(this);
                                $el
                                        .attr( 'title', msg( $el.attr( 'title-msg' ) ) )
@@ -66,7 +72,7 @@ $.fn.localize = function( options ) {
                        } )
                        .end()
                .find( '[alt-msg]' )
-                       .each( function() {
+                       .each( function () {
                                var $el = $(this);
                                $el
                                        .attr( 'alt', msg( $el.attr( 'alt-msg' ) ) )
@@ -77,4 +83,5 @@ $.fn.localize = function( options ) {
 
 // Let IE know about the msg tag before it's used...
 document.createElement( 'msg' );
-} )( jQuery );
+
+}( jQuery, mediaWiki ) );
index 7a1897c..77f2639 100644 (file)
  * @license CC-BY 3.0 <http://creativecommons.org/licenses/by/3.0>
  * @license GPL2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
  */
-( function( $, mw ) {
+( function ( $, mw ) {
 
-$.fn.makeCollapsible = function() {
+$.fn.makeCollapsible = function () {
 
-       return this.each(function() {
+       return this.each(function () {
                var _fn = 'jquery.makeCollapsible> ';
 
                // Define reused variables and functions
-               var     $that = $(this).addClass( 'mw-collapsible' ), // case: $( '#myAJAXelement' ).makeCollapsible()
+               var $toggle,
+                       $that = $(this).addClass( 'mw-collapsible' ), // case: $( '#myAJAXelement' ).makeCollapsible()
                        that = this,
                        collapsetext = $(this).attr( 'data-collapsetext' ),
                        expandtext = $(this).attr( 'data-expandtext' ),
-                       toggleElement = function( $collapsible, action, $defaultToggle, instantHide ) {
+                       toggleElement = function ( $collapsible, action, $defaultToggle, instantHide ) {
+                               var $collapsibleContent, $containers;
+
                                // Validate parameters
                                if ( !$collapsible.jquery ) { // $collapsible must be an instance of jQuery
                                        return;
                                }
-                               if ( action != 'expand' && action != 'collapse' ) {
+                               if ( action !== 'expand' && action !== 'collapse' ) {
                                        // action must be string with 'expand' or 'collapse'
                                        return;
                                }
-                               if ( typeof $defaultToggle == 'undefined' ) {
+                               if ( $defaultToggle === undefined ) {
                                        $defaultToggle = null;
                                }
                                if ( $defaultToggle !== null && !($defaultToggle instanceof $) ) {
@@ -45,9 +48,7 @@ $.fn.makeCollapsible = function() {
                                        return;
                                }
 
-                               var $containers = null;
-
-                               if ( action == 'collapse' ) {
+                               if ( action === 'collapse' ) {
 
                                        // Collapse the element
                                        if ( $collapsible.is( 'table' ) ) {
@@ -80,7 +81,7 @@ $.fn.makeCollapsible = function() {
                                                }
 
                                        } else { // <div>, <p> etc.
-                                               var $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
+                                               $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
 
                                                // If a collapsible-content is defined, collapse it
                                                if ( $collapsibleContent.length ) {
@@ -123,7 +124,7 @@ $.fn.makeCollapsible = function() {
                                                }
 
                                        } else { // <div>, <p> etc.
-                                               var $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
+                                               $collapsibleContent = $collapsible.find( '> .mw-collapsible-content' );
 
                                                // If a collapsible-content is defined, collapse it
                                                if ( $collapsibleContent.length ) {
@@ -142,8 +143,8 @@ $.fn.makeCollapsible = function() {
                                }
                        },
                        // Toggles collapsible and togglelink class and updates text label
-                       toggleLinkDefault = function( that, e ) {
-                               var     $that = $(that),
+                       toggleLinkDefault = function ( that, e ) {
+                               var $that = $(that),
                                        $collapsible = $that.closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' );
                                e.preventDefault();
                                e.stopPropagation();
@@ -175,9 +176,9 @@ $.fn.makeCollapsible = function() {
                                return;
                        },
                        // Toggles collapsible and togglelink class
-                       toggleLinkPremade = function( $that, e ) {
-                               var     $collapsible = $that.eq(0).closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' );
-                               if ( $(e.target).is('a') ) {
+                       toggleLinkPremade = function ( $that, e ) {
+                               var $collapsible = $that.eq(0).closest( '.mw-collapsible.mw-made-collapsible' ).toggleClass( 'mw-collapsed' );
+                               if ( $(e.target).is( 'a' ) ) {
                                        return true;
                                }
                                e.preventDefault();
@@ -200,11 +201,11 @@ $.fn.makeCollapsible = function() {
                                return;
                        },
                        // Toggles customcollapsible
-                       toggleLinkCustom = function( $that, e, $collapsible ) {
+                       toggleLinkCustom = function ( $that, e, $collapsible ) {
                                // For the initial state call of customtogglers there is no event passed
                                if (e) {
                                        e.preventDefault();
-                               e.stopPropagation();
+                                       e.stopPropagation();
                                }
                                // Get current state and toggle to the opposite
                                var action = $collapsible.hasClass( 'mw-collapsed' ) ? 'expand' : 'collapse';
@@ -214,7 +215,7 @@ $.fn.makeCollapsible = function() {
                        };
 
                // Use custom text or default ?
-               if( !collapsetext ) {
+               if ( !collapsetext ) {
                        collapsetext = mw.msg( 'collapsible-collapse' );
                }
                if ( !expandtext ) {
@@ -229,7 +230,7 @@ $.fn.makeCollapsible = function() {
                                .parent()
                                .prepend( '&nbsp;[' )
                                .append( ']&nbsp;' )
-                               .bind( 'click.mw-collapse', function(e) {
+                               .bind( 'click.mw-collapse', function (e) {
                                        toggleLinkDefault( this, e );
                                } );
 
@@ -251,7 +252,7 @@ $.fn.makeCollapsible = function() {
 
                        // Double check that there is actually a customtoggle link
                        if ( $customTogglers.length ) {
-                               $customTogglers.bind( 'click.mw-collapse', function( e ) {
+                               $customTogglers.bind( 'click.mw-collapse', function ( e ) {
                                        toggleLinkCustom( $(this), e, $that );
                                } );
                        } else {
@@ -271,22 +272,22 @@ $.fn.makeCollapsible = function() {
                        // Elements are treated differently
                        if ( $that.is( 'table' ) ) {
                                // The toggle-link will be in one the the cells (td or th) of the first row
-                               var     $firstRowCells = $( 'tr:first th, tr:first td', that ),
-                                       $toggle = $firstRowCells.find( '> .mw-collapsible-toggle' );
+                               var $firstRowCells = $that.find( 'tr:first th, tr:first td' );
+                               $toggle = $firstRowCells.find( '> .mw-collapsible-toggle' );
 
                                // If theres no toggle link, add it to the last cell
                                if ( !$toggle.length ) {
                                        $firstRowCells.eq(-1).prepend( $toggleLink );
                                } else {
-                                       $toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
+                                       $toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function ( e ) {
                                                toggleLinkPremade( $toggle, e );
                                        } );
                                }
 
                        } else if ( $that.is( 'ul' ) || $that.is( 'ol' ) ) {
                                // The toggle-link will be in the first list-item
-                               var     $firstItem = $( 'li:first', $that),
-                                       $toggle = $firstItem.find( '> .mw-collapsible-toggle' );
+                               var $firstItem = $that.find( 'li:first' );
+                               $toggle = $firstItem.find( '> .mw-collapsible-toggle' );
 
                                // If theres no toggle link, add it
                                if ( !$toggle.length ) {
@@ -294,12 +295,12 @@ $.fn.makeCollapsible = function() {
                                        // to be "1". Except if the value-attribute is already used.
                                        // If no value was set WebKit returns "", Mozilla returns '-1', others return null or undefined.
                                        var firstval = $firstItem.attr( 'value' );
-                                       if ( firstval === undefined || !firstval || firstval == '-1' ) {
+                                       if ( firstval === undefined || !firstval || firstval === '-1' || firstval === -1 ) {
                                                $firstItem.attr( 'value', '1' );
                                        }
                                        $that.prepend( $toggleLink.wrap( '<li class="mw-collapsible-toggle-li"></li>' ).parent() );
                                } else {
-                                       $toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
+                                       $toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function ( e ) {
                                                toggleLinkPremade( $toggle, e );
                                        } );
                                }
@@ -307,7 +308,7 @@ $.fn.makeCollapsible = function() {
                        } else { // <div>, <p> etc.
 
                                // The toggle-link will be the first child of the element
-                               var $toggle = $that.find( '> .mw-collapsible-toggle' );
+                               $toggle = $that.find( '> .mw-collapsible-toggle' );
 
                                // If a direct child .content-wrapper does not exists, create it
                                if ( !$that.find( '> .mw-collapsible-content' ).length ) {
@@ -318,7 +319,7 @@ $.fn.makeCollapsible = function() {
                                if ( !$toggle.length ) {
                                        $that.prepend( $toggleLink );
                                } else {
-                                       $toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function( e ) {
+                                       $toggleLink = $toggle.unbind( 'click.mw-collapse' ).bind( 'click.mw-collapse', function ( e ) {
                                                toggleLinkPremade( $toggle, e );
                                        } );
                                }
@@ -336,4 +337,5 @@ $.fn.makeCollapsible = function() {
                }
        } );
 };
-} )( jQuery, mediaWiki );
+
+}( jQuery, mediaWiki ) );
index 690fedd..c088bc4 100644 (file)
  * @license CC-BY 3.0 <http://creativecommons.org/licenses/by/3.0>
  * @license GPL2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
  */
-( function( $ ) {
-// @return jQuery object of the message box
-$.messageBoxNew = function( options ) {
+( function ( $ ) {
+
+/** @return jQuery object of the message box */
+$.messageBoxNew = function ( options ) {
        options = $.extend( {
-               'id': 'js-messagebox', // unique identifier for this message box
-               'parent': 'body', // jQuery/CSS selector
-               'insert': 'prepend' // 'prepend' or 'append'
+               // unique identifier for this message box
+               id: 'js-messagebox',
+
+               // jQuery/CSS selector
+               parent: 'body',
+
+               // 'prepend' or 'append'
+               insert: 'prepend'
        }, options );
        var $curBox = $( '#' + options.id );
        // Only create a new box if it doesn't exist already
@@ -47,17 +53,21 @@ $.messageBoxNew = function( options ) {
                }
        }
 };
-// Calling with no message or message set to empty string or null will hide the group,
-// setting 'replace' to true as well will reset and hide the group entirely.
-// If there are no visible groups the main message box is hidden automatically,
-// and shown again once there are messages
-// @return jQuery object of message group
-$.messageBox = function( options ) {
+
+/**
+ * Calling with no message or message set to empty string or null will hide the group,
+ * setting 'replace' to true as well will reset and hide the group entirely.
+ * If there are no visible groups the main message box is hidden automatically,
+ * and shown again once there are messages
+ * @return {jQuery}: jQuery object of message group.
+ */
+$.messageBox = function ( options ) {
        options = $.extend( {
-               'message': '',
-               'group': 'default',
-               'replace': false, // if true replaces any previous message in this group
-               'target': 'js-messagebox'
+               message: '',
+               group: 'default',
+               // if replace=true, it replaces any previous message in this group
+               replace: false,
+               target: 'js-messagebox'
        }, options );
        var $target = $.messageBoxNew( { id: options.target } );
        var groupID = options.target + '-' + options.group;
@@ -95,4 +105,5 @@ $.messageBox = function( options ) {
        }
        return $group;
 };
-} )( jQuery );
+
+}( jQuery ) );
index 023ea7f..3757393 100644 (file)
@@ -1,26 +1,26 @@
 /*
  * JavaScript backwards-compatibility alternatives and other convenience functions
  */
-( function( $ ) {
+( function ( $ ) {
 
        $.extend({
-               trimLeft: function( str ) {
+               trimLeft: function ( str ) {
                        return str === null ? '' : str.toString().replace( /^\s+/, '' );
                },
-               trimRight: function( str ) {
+               trimRight: function ( str ) {
                        return str === null ?
                                        '' : str.toString().replace( /\s+$/, '' );
                },
-               ucFirst: function( str ) {
+               ucFirst: function ( str ) {
                        return str.charAt( 0 ).toUpperCase() + str.substr( 1 );
                },
-               escapeRE: function( str ) {
-                       return str.replace ( /([\\{}()|.?*+\-^$\[\]])/g, "\\$1" );
+               escapeRE: function ( str ) {
+                       return str.replace ( /([\\{}()|.?*+\-\^$\[\]])/g, "\\$1" );
                },
-               isDomElement: function( el ) {
+               isDomElement: function ( el ) {
                        return !!el && !!el.nodeType;
                },
-               isEmpty: function( v ) {
+               isEmpty: function ( v ) {
                        if ( v === '' || v === 0 || v === '0' || v === null
                                || v === false || v === undefined )
                        {
@@ -39,8 +39,8 @@
                        }
                        return false;
                },
-               compareArray: function( arrThis, arrAgainst ) {
-                       if ( arrThis.length != arrAgainst.length ) {
+               compareArray: function ( arrThis, arrAgainst ) {
+                       if ( arrThis.length !== arrAgainst.length ) {
                                return false;
                        }
                        for ( var i = 0; i < arrThis.length; i++ ) {
                        }
                        return true;
                },
-               compareObject: function( objectA, objectB ) {
+               compareObject: function ( objectA, objectB ) {
 
                        // Do a simple check if the types match
-                       if ( typeof objectA == typeof objectB ) {
+                       if ( typeof objectA === typeof objectB ) {
 
                                // Only loop over the contents if it really is an object
-                               if ( typeof objectA == 'object' ) {
+                               if ( typeof objectA === 'object' ) {
                                        // If they are aliases of the same object (ie. mw and mediaWiki) return now
                                        if ( objectA === objectB ) {
                                                return true;
@@ -72,7 +72,7 @@
                                                        if ( prop in objectB ) {
                                                                // Compare the types of the properties
                                                                var type = typeof objectA[prop];
-                                                               if ( type == typeof objectB[prop] ) {
+                                                               if ( type === typeof objectB[prop] ) {
                                                                        // Recursively check objects inside this one
                                                                        switch ( type ) {
                                                                                case 'object' :
                }
        });
 
-} )( jQuery );
+}( jQuery ) );
index 8310cef..5ec05f2 100644 (file)
@@ -3,85 +3,86 @@
  *
  * This will automatically use the HTML5 placeholder attribute if supported, or emulate this behavior if not.
  *
- * @author Trevor Parscal <tparscal@wikimedia.org>
- * @author Krinkle <krinklemail@gmail.com>
+ * @author Trevor Parscal <tparscal@wikimedia.org>, 2012
+ * @author Krinkle <krinklemail@gmail.com>, 2012
  * @version 0.2.0
  * @license GPL v2
  */
-( function( $ ) {
+( function ( $ ) {
 
-$.fn.placeholder = function() {
+       $.fn.placeholder = function () {
 
-       return this.each( function() {
+               return this.each( function () {
 
-               // If the HTML5 placeholder attribute is supported, use it
-               if ( this.placeholder && 'placeholder' in document.createElement( this.tagName ) ) {
-                       return;
-               }
+                       // If the HTML5 placeholder attribute is supported, use it
+                       if ( this.placeholder && 'placeholder' in document.createElement( this.tagName ) ) {
+                               return;
+                       }
 
-               var placeholder = this.getAttribute( 'placeholder' );
-               var $input = $(this);
+                       var placeholder = this.getAttribute( 'placeholder' );
+                       var $input = $(this);
 
-               // Show initially, if empty
-               if ( this.value === '' || this.value === placeholder ) {
-                       $input.addClass( 'placeholder' ).val( placeholder );
-               }
+                       // Show initially, if empty
+                       if ( this.value === '' || this.value === placeholder ) {
+                               $input.addClass( 'placeholder' ).val( placeholder );
+                       }
 
-               $input
-                       // Show on blur if empty
-                       .blur( function() {
-                               if ( this.value === '' ) {
-                                       this.value = placeholder;
-                                       $input.addClass( 'placeholder' );
-                               }
-                       } )
+                       $input
+                               // Show on blur if empty
+                               .blur( function () {
+                                       if ( this.value === '' ) {
+                                               this.value = placeholder;
+                                               $input.addClass( 'placeholder' );
+                                       }
+                               } )
+
+                               // Hide on focus
+                               // Also listen for other events in case $input was
+                               // already focused when the events were bound
+                               .bind( 'focus drop keydown paste', function ( e ) {
+                                       if ( $input.hasClass( 'placeholder' ) ) {
+                                               if ( e.type === 'drop' && e.originalEvent.dataTransfer ) {
+                                                       // Support for drag&drop. Instead of inserting the dropped
+                                                       // text somewhere in the middle of the placeholder string,
+                                                       // we want to set the contents of the search box to the
+                                                       // dropped text.
 
-                       // Hide on focus
-                       // Also listen for other events in case $input was
-                       // already focused when the events were bound
-                       .bind( 'focus drop keydown paste', function( e ) {
-                               if ( $input.hasClass( 'placeholder' ) ) {
-                                       if ( e.type == 'drop' && e.originalEvent.dataTransfer ) {
-                                               // Support for drag&drop. Instead of inserting the dropped
-                                               // text somewhere in the middle of the placeholder string,
-                                               // we want to set the contents of the search box to the
-                                               // dropped text.
+                                                       // IE wants getData( 'text' ) but Firefox wants getData( 'text/plain' )
+                                                       // Firefox fails gracefully with an empty string, IE barfs with an error
+                                                       try {
+                                                               // Try the Firefox way
+                                                               this.value = e.originalEvent.dataTransfer.getData( 'text/plain' );
+                                                       } catch ( exception ) {
+                                                               // Got an exception, so use the IE way
+                                                               this.value = e.originalEvent.dataTransfer.getData( 'text' );
+                                                       }
 
-                                               // IE wants getData( 'text' ) but Firefox wants getData( 'text/plain' )
-                                               // Firefox fails gracefully with an empty string, IE barfs with an error
-                                               try {
-                                                       // Try the Firefox way
-                                                       this.value = e.originalEvent.dataTransfer.getData( 'text/plain' );
-                                               } catch ( exception ) {
-                                                       // Got an exception, so use the IE way
-                                                       this.value = e.originalEvent.dataTransfer.getData( 'text' );
+                                                       // On Firefox, drop fires after the dropped text has been inserted,
+                                                       // but on IE it fires before. If we don't prevent the default action,
+                                                       // IE will insert the dropped text twice.
+                                                       e.preventDefault();
+                                               } else {
+                                                       this.value = '';
                                                }
+                                               $input.removeClass( 'placeholder' );
+                                       }
+                               } );
 
-                                               // On Firefox, drop fires after the dropped text has been inserted,
-                                               // but on IE it fires before. If we don't prevent the default action,
-                                               // IE will insert the dropped text twice.
-                                               e.preventDefault();
-                                       } else {
-                                               this.value = '';
+                       // Blank on submit -- prevents submitting with unintended value
+                       if ( this.form ) {
+                               $( this.form ).submit( function () {
+                                       // $input.trigger( 'focus' ); would be problematic
+                                       // because it actually focuses $input, leading
+                                       // to nasty behavior in mobile browsers
+                                       if ( $input.hasClass( 'placeholder' ) ) {
+                                               $input
+                                                       .val( '' )
+                                                       .removeClass( 'placeholder' );
                                        }
-                                       $input.removeClass( 'placeholder' );
-                               }
-                       } );
+                               });
+                       }
 
-               // Blank on submit -- prevents submitting with unintended value
-               if ( this.form ) {
-                       $( this.form ).submit( function() {
-                               // $input.trigger( 'focus' ); would be problematic
-                               // because it actually focuses $input, leading
-                               // to nasty behavior in mobile browsers
-                               if ( $input.hasClass( 'placeholder' ) ) {
-                                       $input
-                                               .val( '' )
-                                               .removeClass( 'placeholder' );
-                               }
-                       });
-               }
+               });
+       };
 
-       });
-};
-} )( jQuery );
+}( jQuery ) );
index 2db0629..d54e66c 100644 (file)
@@ -43,8 +43,9 @@
                                length = arguments.length;
 
                        for ( ; i < length; i++ ) {
+                               options = arguments[ i ];
                                // Only deal with non-null/undefined values
-                               if ( (options = arguments[ i ]) != null ) {
+                               if ( options !== null && options !== undefined ) {
                                        // Extend the base object
                                        for ( name in options ) {
                                                src = target[ name ];
        };
 
 
-/**
- * CompletenessTest
- * @constructor
- *
- * @example
- *  var myTester = new CompletenessTest( myLib );
- * @param masterVariable {Object} The root variable that contains all object
- *  members. CompletenessTest will recursively traverse objects and keep track
- *  of all methods.
- * @param ignoreFn {Function} Optionally pass a function to filter out certain
- *  methods. Example: You may want to filter out instances of jQuery or some
- *  other constructor. Otherwise "missingTests" will include all methods that
- *  were not called from that instance.
- */
-var CompletenessTest = function ( masterVariable, ignoreFn ) {
-
-       // Keep track in these objects. Keyed by strings with the
-       // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
-       this.injectionTracker = {};
-       this.methodCallTracker = {};
-       this.missingTests = {};
-
-       this.ignoreFn = undefined === ignoreFn ? function () { return false; } : ignoreFn;
+       /**
+        * CompletenessTest
+        * @constructor
+        *
+        * @example
+        *  var myTester = new CompletenessTest( myLib );
+        * @param masterVariable {Object} The root variable that contains all object
+        *  members. CompletenessTest will recursively traverse objects and keep track
+        *  of all methods.
+        * @param ignoreFn {Function} Optionally pass a function to filter out certain
+        *  methods. Example: You may want to filter out instances of jQuery or some
+        *  other constructor. Otherwise "missingTests" will include all methods that
+        *  were not called from that instance.
+        */
+       var CompletenessTest = function ( masterVariable, ignoreFn ) {
 
-       // Lazy limit in case something weird happends (like recurse (part of) ourself).
-       this.lazyLimit = 2000;
-       this.lazyCounter = 0;
+               // Keep track in these objects. Keyed by strings with the
+               // method names (ie. 'my.foo', 'my.bar', etc.) values are boolean true.
+               this.injectionTracker = {};
+               this.methodCallTracker = {};
+               this.missingTests = {};
 
-       var that = this;
+               this.ignoreFn = undefined === ignoreFn ? function () { return false; } : ignoreFn;
 
-       // Bind begin and end to QUnit.
-       QUnit.begin( function () {
-               that.walkTheObject( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_INJECT );
-               log( 'CompletenessTest/walkTheObject/ACTION_INJECT', that );
-       });
+               // Lazy limit in case something weird happends (like recurse (part of) ourself).
+               this.lazyLimit = 2000;
+               this.lazyCounter = 0;
 
-       QUnit.done( function () {
-               that.populateMissingTests();
-               log( 'CompletenessTest/populateMissingTests', that );
+               var that = this;
 
-               var toolbar, testResults, cntTotal, cntCalled, cntMissing;
+               // Bind begin and end to QUnit.
+               QUnit.begin( function () {
+                       that.walkTheObject( null, masterVariable, masterVariable, [], CompletenessTest.ACTION_INJECT );
+                       log( 'CompletenessTest/walkTheObject/ACTION_INJECT', that );
+               });
 
-               cntTotal = util.keys( that.injectionTracker ).length;
-               cntCalled = util.keys( that.methodCallTracker ).length;
-               cntMissing = util.keys( that.missingTests ).length;
+               QUnit.done( function () {
+                       that.populateMissingTests();
+                       log( 'CompletenessTest/populateMissingTests', that );
 
-               function makeTestResults( blob, title, style ) {
-                       var elOutputWrapper, elTitle, elContainer, elList, elFoot;
+                       var toolbar, testResults, cntTotal, cntCalled, cntMissing;
 
-                       elTitle = document.createElement( 'strong' );
-                       elTitle.textContent = title || 'Values';
+                       cntTotal = util.keys( that.injectionTracker ).length;
+                       cntCalled = util.keys( that.methodCallTracker ).length;
+                       cntMissing = util.keys( that.missingTests ).length;
 
-                       elList = document.createElement( 'ul' );
-                       util.each( blob, function ( key ) {
-                               var elItem = document.createElement( 'li' );
-                               elItem.textContent = key;
-                               elList.appendChild( elItem );
-                       });
+                       function makeTestResults( blob, title, style ) {
+                               var elOutputWrapper, elTitle, elContainer, elList, elFoot;
 
-                       elFoot = document.createElement( 'p' );
-                       elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
+                               elTitle = document.createElement( 'strong' );
+                               elTitle.textContent = title || 'Values';
 
-                       elContainer = document.createElement( 'div' );
-                       elContainer.appendChild( elTitle );
-                       elContainer.appendChild( elList );
-                       elContainer.appendChild( elFoot );
+                               elList = document.createElement( 'ul' );
+                               util.each( blob, function ( key ) {
+                                       var elItem = document.createElement( 'li' );
+                                       elItem.textContent = key;
+                                       elList.appendChild( elItem );
+                               });
 
-                       elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
-                       if ( !elOutputWrapper ) {
-                               elOutputWrapper = document.createElement( 'div' );
-                               elOutputWrapper.id = 'qunit-completenesstest';
-                       }
-                       elOutputWrapper.appendChild( elContainer );
+                               elFoot = document.createElement( 'p' );
+                               elFoot.innerHTML = '<em>&mdash; CompletenessTest</em>';
 
-                       util.each( style, function ( key, value ) {
-                               elOutputWrapper.style[key] = value;
-                       });
-                       return elOutputWrapper;
-               }
+                               elContainer = document.createElement( 'div' );
+                               elContainer.appendChild( elTitle );
+                               elContainer.appendChild( elList );
+                               elContainer.appendChild( elFoot );
 
-               if ( cntMissing === 0 ) {
-                       // Good
-                       testResults = makeTestResults(
-                               {},
-                               'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
-                               {
-                                       backgroundColor: '#D2E0E6',
-                                       color: '#366097',
-                                       paddingTop: '1em',
-                                       paddingRight: '1em',
-                                       paddingBottom: '1em',
-                                       paddingLeft: '1em'
-                               }
-                       );
-               } else {
-                       // Bad
-                       testResults = makeTestResults(
-                               that.missingTests,
-                               'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
-                               {
-                                       backgroundColor: '#EE5757',
-                                       color: 'black',
-                                       paddingTop: '1em',
-                                       paddingRight: '1em',
-                                       paddingBottom: '1em',
-                                       paddingLeft: '1em'
+                               elOutputWrapper = document.getElementById( 'qunit-completenesstest' );
+                               if ( !elOutputWrapper ) {
+                                       elOutputWrapper = document.createElement( 'div' );
+                                       elOutputWrapper.id = 'qunit-completenesstest';
                                }
-                       );
-               }
+                               elOutputWrapper.appendChild( elContainer );
 
-               toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
-               if ( toolbar ) {
-                       toolbar.insertBefore( testResults, toolbar.firstChild );
-               }
-       });
+                               util.each( style, function ( key, value ) {
+                                       elOutputWrapper.style[key] = value;
+                               });
+                               return elOutputWrapper;
+                       }
 
-       return this;
-};
+                       if ( cntMissing === 0 ) {
+                               // Good
+                               testResults = makeTestResults(
+                                       {},
+                                       'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. No missing tests!',
+                                       {
+                                               backgroundColor: '#D2E0E6',
+                                               color: '#366097',
+                                               paddingTop: '1em',
+                                               paddingRight: '1em',
+                                               paddingBottom: '1em',
+                                               paddingLeft: '1em'
+                                       }
+                               );
+                       } else {
+                               // Bad
+                               testResults = makeTestResults(
+                                       that.missingTests,
+                                       'Detected calls to ' + cntCalled + '/' + cntTotal + ' methods. ' + cntMissing + ' methods not covered:',
+                                       {
+                                               backgroundColor: '#EE5757',
+                                               color: 'black',
+                                               paddingTop: '1em',
+                                               paddingRight: '1em',
+                                               paddingBottom: '1em',
+                                               paddingLeft: '1em'
+                                       }
+                               );
+                       }
 
-/* Static members */
-CompletenessTest.ACTION_INJECT = 500;
-CompletenessTest.ACTION_CHECK = 501;
+                       toolbar = document.getElementById( 'qunit-testrunner-toolbar' );
+                       if ( toolbar ) {
+                               toolbar.insertBefore( testResults, toolbar.firstChild );
+                       }
+               });
 
-/* Public methods */
-CompletenessTest.fn = CompletenessTest.prototype = {
+               return this;
+       };
 
-       /**
-        * CompletenessTest.fn.walkTheObject
-        *
-        * This function recursively walks through the given object, calling itself as it goes.
-        * Depending on the action it either injects our listener into the methods, or
-        * reads from our tracker and records which methods have not been called by the test suite.
-        *
-        * @param currName {String|Null} Name of the given object member (Initially this is null).
-        * @param currVar {mixed} The variable to check (initially an object,
-        *  further down it could be anything).
-        * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
-        *  Initially this is the same as currVar.
-        * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
-        *  masterVariable. Not including currName.
-        * @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK)
-        */
-       walkTheObject: function ( currName, currVar, masterVariable, parentPathArray, action ) {
+       /* Static members */
+       CompletenessTest.ACTION_INJECT = 500;
+       CompletenessTest.ACTION_CHECK = 501;
+
+       /* Public methods */
+       CompletenessTest.fn = CompletenessTest.prototype = {
+
+               /**
+                * CompletenessTest.fn.walkTheObject
+                *
+                * This function recursively walks through the given object, calling itself as it goes.
+                * Depending on the action it either injects our listener into the methods, or
+                * reads from our tracker and records which methods have not been called by the test suite.
+                *
+                * @param currName {String|Null} Name of the given object member (Initially this is null).
+                * @param currVar {mixed} The variable to check (initially an object,
+                *  further down it could be anything).
+                * @param masterVariable {Object} Throughout our interation, always keep track of the master/root.
+                *  Initially this is the same as currVar.
+                * @param parentPathArray {Array} Array of names that indicate our breadcrumb path starting at
+                *  masterVariable. Not including currName.
+                * @param action {Number} What is this function supposed to do (ACTION_INJECT or ACTION_CHECK)
+                */
+               walkTheObject: function ( currName, currVar, masterVariable, parentPathArray, action ) {
+
+                       var key, value, tmpPathArray,
+                               type = util.type( currVar ),
+                               that = this;
+
+                       // Hard ignores
+                       if ( this.ignoreFn( currVar, that, parentPathArray ) ) {
+                               return null;
+                       }
 
-               var key, value, tmpPathArray,
-                       type = util.type( currVar ),
-                       that = this;
+                       // Handle the lazy limit
+                       this.lazyCounter++;
+                       if ( this.lazyCounter > this.lazyLimit ) {
+                               log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, parentPathArray );
+                               return null;
+                       }
 
-               // Hard ignores
-               if ( this.ignoreFn( currVar, that, parentPathArray ) ) {
-                       return null;
-               }
+                       // Functions
+                       if ( type === 'function' ) {
 
-               // Handle the lazy limit
-               this.lazyCounter++;
-               if ( this.lazyCounter > this.lazyLimit ) {
-                       log( 'CompletenessTest.fn.walkTheObject> Limit reached: ' + this.lazyCounter, parentPathArray );
-                       return null;
-               }
+                               if ( !currVar.prototype || util.isEmptyObject( currVar.prototype ) ) {
 
-               // Functions
-               if ( type === 'function' ) {
+                                       if ( action === CompletenessTest.ACTION_INJECT ) {
 
-                       if ( !currVar.prototype || util.isEmptyObject( currVar.prototype ) ) {
+                                               that.injectionTracker[ parentPathArray.join( '.' ) ] = true;
+                                               that.injectCheck( masterVariable, parentPathArray, function () {
+                                                       that.methodCallTracker[ parentPathArray.join( '.' ) ] = true;
+                                               } );
+                                       }
 
-                               if ( action === CompletenessTest.ACTION_INJECT ) {
+                               // We don't support checking object constructors yet...
+                               // ...we can check the prototypes fine, though.
+                               } else {
+                                       if ( action === CompletenessTest.ACTION_INJECT ) {
 
-                                       that.injectionTracker[ parentPathArray.join( '.' ) ] = true;
-                                       that.injectCheck( masterVariable, parentPathArray, function () {
-                                               that.methodCallTracker[ parentPathArray.join( '.' ) ] = true;
-                                       } );
-                               }
+                                               for ( key in currVar.prototype ) {
+                                                       if ( hasOwn.call( currVar.prototype, key ) ) {
+                                                               value = currVar.prototype[key];
+                                                               if ( key === 'constructor' ) {
+                                                                       continue;
+                                                               }
 
-                       // We don't support checking object constructors yet...
-                       // ...we can check the prototypes fine, though.
-                       } else {
-                               if ( action === CompletenessTest.ACTION_INJECT ) {
+                                                               // Clone and break reference to parentPathArray
+                                                               tmpPathArray = util.extend( [], parentPathArray );
+                                                               tmpPathArray.push( 'prototype' );
+                                                               tmpPathArray.push( key );
 
-                                       for ( key in currVar.prototype ) {
-                                               if ( hasOwn.call( currVar.prototype, key ) ) {
-                                                       value = currVar.prototype[key];
-                                                       if ( key === 'constructor' ) {
-                                                               continue;
+                                                               that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
                                                        }
-
-                                                       // Clone and break reference to parentPathArray
-                                                       tmpPathArray = util.extend( [], parentPathArray );
-                                                       tmpPathArray.push( 'prototype' );
-                                                       tmpPathArray.push( key );
-
-                                                       that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
                                                }
-                                       }
 
+                                       }
                                }
-                       }
 
-               }
+                       }
 
-               // Recursively. After all, this is the *completeness* test
-               if ( type === 'function' || type === 'object' ) {
-                       for ( key in currVar ) {
-                               if ( hasOwn.call( currVar, key ) ) {
-                                       value = currVar[key];
+                       // Recursively. After all, this is the *completeness* test
+                       if ( type === 'function' || type === 'object' ) {
+                               for ( key in currVar ) {
+                                       if ( hasOwn.call( currVar, key ) ) {
+                                               value = currVar[key];
 
-                                       // Clone and break reference to parentPathArray
-                                       tmpPathArray = util.extend( [], parentPathArray );
-                                       tmpPathArray.push( key );
+                                               // Clone and break reference to parentPathArray
+                                               tmpPathArray = util.extend( [], parentPathArray );
+                                               tmpPathArray.push( key );
 
-                                       that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
+                                               that.walkTheObject( key, value, masterVariable, tmpPathArray, action );
+                                       }
                                }
                        }
-               }
-       },
+               },
 
-       populateMissingTests: function () {
-               var ct = this;
-               util.each( ct.injectionTracker, function ( key ) {
-                       ct.hasTest( key );
-               });
-       },
+               populateMissingTests: function () {
+                       var ct = this;
+                       util.each( ct.injectionTracker, function ( key ) {
+                               ct.hasTest( key );
+                       });
+               },
 
-       /**
-        * CompletenessTest.fn.hasTest
-        *
-        * Checks if the given method name (ie. 'my.foo.bar')
-        * was called during the test suite (as far as the tracker knows).
-        * If not it adds it to missingTests.
-        *
-        * @param fnName {String}
-        * @return {Boolean}
-        */
-       hasTest: function ( fnName ) {
-               if ( !( fnName in this.methodCallTracker ) ) {
-                       this.missingTests[fnName] = true;
-                       return false;
-               }
-               return true;
-       },
+               /**
+                * CompletenessTest.fn.hasTest
+                *
+                * Checks if the given method name (ie. 'my.foo.bar')
+                * was called during the test suite (as far as the tracker knows).
+                * If not it adds it to missingTests.
+                *
+                * @param fnName {String}
+                * @return {Boolean}
+                */
+               hasTest: function ( fnName ) {
+                       if ( !( fnName in this.methodCallTracker ) ) {
+                               this.missingTests[fnName] = true;
+                               return false;
+                       }
+                       return true;
+               },
 
-       /**
-        * CompletenessTest.fn.injectCheck
-        *
-        * Injects a function (such as a spy that updates methodCallTracker when
-        * it's called) inside another function.
-        *
-        * @param masterVariable {Object}
-        * @param objectPathArray {Array}
-        * @param injectFn {Function}
-        */
-       injectCheck: function ( masterVariable, objectPathArray, injectFn ) {
-               var i, len, prev, memberName, lastMember,
-                       curr = masterVariable;
-
-               // Get the object in question through the path from the master variable,
-               // We can't pass the value directly because we need to re-define the object
-               // member and keep references to the parent object, member name and member
-               // value at all times.
-               for ( i = 0, len = objectPathArray.length; i < len; i++ ) {
-                       memberName = objectPathArray[i];
-
-                       prev = curr;
-                       curr = prev[memberName];
-                       lastMember = memberName;
-               }
+               /**
+                * CompletenessTest.fn.injectCheck
+                *
+                * Injects a function (such as a spy that updates methodCallTracker when
+                * it's called) inside another function.
+                *
+                * @param masterVariable {Object}
+                * @param objectPathArray {Array}
+                * @param injectFn {Function}
+                */
+               injectCheck: function ( masterVariable, objectPathArray, injectFn ) {
+                       var i, len, prev, memberName, lastMember,
+                               curr = masterVariable;
+
+                       // Get the object in question through the path from the master variable,
+                       // We can't pass the value directly because we need to re-define the object
+                       // member and keep references to the parent object, member name and member
+                       // value at all times.
+                       for ( i = 0, len = objectPathArray.length; i < len; i++ ) {
+                               memberName = objectPathArray[i];
+
+                               prev = curr;
+                               curr = prev[memberName];
+                               lastMember = memberName;
+                       }
 
-               // Objects are by reference, members (unless objects) are not.
-               prev[lastMember] = function () {
-                       injectFn();
-                       return curr.apply( this, arguments );
-               };
-       }
-};
+                       // Objects are by reference, members (unless objects) are not.
+                       prev[lastMember] = function () {
+                               injectFn();
+                               return curr.apply( this, arguments );
+                       };
+               }
+       };
 
-window.CompletenessTest = CompletenessTest;
+       /* Expose */
+       window.CompletenessTest = CompletenessTest;
 
-} )( jQuery );
+}( jQuery ) );
index 87e4538..e8b683e 100644 (file)
@@ -3,42 +3,42 @@
  *
  * Simple jQuery plugin to create, inject and remove spinners.
  */
-( function( $ ) {
+( function ( $ ) {
 
-$.extend( {
-       /**
-        * Creates a spinner element.
-        *
-        * @param id {String} id of the spinner
-        * @return {jQuery} spinner
-        */
-       createSpinner: function( id ) {
-               return $( '<div>' ).attr( {
-                       id: 'mw-spinner-' + id,
-                       'class': 'mw-spinner',
-                       title: '...'
-               } );
-       },
+       $.extend( {
+               /**
+                * Creates a spinner element.
+                *
+                * @param id {String} id of the spinner
+                * @return {jQuery} spinner
+                */
+               createSpinner: function ( id ) {
+                       return $( '<div>' ).attr( {
+                               id: 'mw-spinner-' + id,
+                               'class': 'mw-spinner',
+                               title: '...'
+                       } );
+               },
+
+               /**
+                * Removes a spinner element.
+                *
+                * @param id {String}
+                * @return {jQuery} spinner
+                */
+               removeSpinner: function ( id ) {
+                       return $( '#mw-spinner-' + id ).remove();
+               }
+       } );
 
        /**
-        * Removes a spinner element.
+        * Injects a spinner after the elements in the jQuery collection.
         *
-        * @param id {String}
-        * @return {jQuery} spinner
+        * @param id String id of the spinner
+        * @return {jQuery}
         */
-       removeSpinner: function( id ) {
-               return $( '#mw-spinner-' + id ).remove();
-       }
-} );
-
-/**
- * Injects a spinner after the elements in the jQuery collection.
- *
- * @param id String id of the spinner
- * @return {jQuery}
- */
-$.fn.injectSpinner = function( id ) {
-       return this.after( $.createSpinner( id ) );
-};
+       $.fn.injectSpinner = function ( id ) {
+               return this.after( $.createSpinner( id ) );
+       };
 
-} )( jQuery );
+}( jQuery ) );
index 466c551..dff5535 100644 (file)
  * highlightInput: Whether to hightlight matched portions of the input or not
  *             Type: Boolean, Default: false
  */
-( function( $ ) {
+( function ( $ ) {
 
 $.suggestions = {
        /**
         * Cancel any delayed updateSuggestions() call and inform the user so
         * they can cancel their result fetching if they use AJAX or something
         */
-       cancel: function( context ) {
-               if ( context.data.timerID != null ) {
+       cancel: function ( context ) {
+               if ( context.data.timerID !== null ) {
                        clearTimeout( context.data.timerID );
                }
                if ( $.isFunction( context.config.cancel ) ) {
@@ -61,7 +61,7 @@ $.suggestions = {
         * restores the value the currently displayed suggestions are based on, rather than the value just before
         * highlight() overwrote it; the former is arguably slightly more sensible.
         */
-       restore: function( context ) {
+       restore: function ( context ) {
                context.data.$textbox.val( context.data.prevText );
        },
        /**
@@ -70,7 +70,7 @@ $.suggestions = {
         * function does nothing.
         * @param {Boolean} delayed Whether or not to delay this by the currently configured amount of time
         */
-       update: function( context, delayed ) {
+       update: function ( context, delayed ) {
                // Only fetch if the value in the textbox changed and is not empty
                // if the textbox is empty then clear the result div, but leave other settings intouched
                function maybeFetch() {
@@ -86,7 +86,7 @@ $.suggestions = {
                }
 
                // Cancel previous call
-               if ( context.data.timerID != null ) {
+               if ( context.data.timerID !== null ) {
                        clearTimeout( context.data.timerID );
                }
                if ( delayed ) {
@@ -97,11 +97,11 @@ $.suggestions = {
                }
                $.suggestions.special( context );
        },
-       special: function( context ) {
+       special: function ( context ) {
                // Allow custom rendering - but otherwise don't do any rendering
                if ( typeof context.config.special.render === 'function' ) {
                        // Wait for the browser to update the value
-                       setTimeout( function() {
+                       setTimeout( function () {
                                // Render special
                                var $special = context.data.$container.find( '.suggestions-special' );
                                context.config.special.render.call( $special, context.data.$textbox.val() );
@@ -113,7 +113,7 @@ $.suggestions = {
         * @param property String Name of property
         * @param value Mixed Value to set property with
         */
-       configure: function( context, property, value ) {
+       configure: function ( context, property, value ) {
                // Validate creation using fallback values
                switch( property ) {
                        case 'fetch':
@@ -154,12 +154,13 @@ $.suggestions = {
                                                var $autoEllipseMe = $( [] );
                                                var matchedText = null;
                                                for ( var i = 0; i < context.config.suggestions.length; i++ ) {
+                                                       /*jshint loopfunc:true */
                                                        var text = context.config.suggestions[i];
                                                        var $result = $( '<div>' )
                                                                .addClass( 'suggestions-result' )
                                                                .attr( 'rel', i )
                                                                .data( 'text', context.config.suggestions[i] )
-                                                               .mousemove( function( e ) {
+                                                               .mousemove( function ( e ) {
                                                                        context.data.selectedWithMouse = true;
                                                                        $.suggestions.highlight(
                                                                                context, $(this).closest( '.suggestions-results div' ), false
@@ -220,9 +221,9 @@ $.suggestions = {
         * @param result <tr> to highlight: jQuery object, or 'prev' or 'next'
         * @param updateTextbox If true, put the suggestion in the textbox
         */
-       highlight: function( context, result, updateTextbox ) {
+       highlight: function ( context, result, updateTextbox ) {
                var selected = context.data.$container.find( '.suggestions-result-current' );
-               if ( !result.get || selected.get( 0 ) != result.get( 0 ) ) {
+               if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) {
                        if ( result === 'prev' ) {
                                if( selected.is( '.suggestions-special' ) ) {
                                        result = context.data.$container.find( '.suggestions-result:last' );
@@ -277,8 +278,8 @@ $.suggestions = {
         * Respond to keypress event
         * @param key Integer Code of key pressed
         */
-       keypress: function( e, context, key ) {
-               var     wasVisible = context.data.$container.is( ':visible' ),
+       keypress: function ( e, context, key ) {
+               var wasVisible = context.data.$container.is( ':visible' ),
                        preventDefault = false;
                switch ( key ) {
                        // Arrow down
@@ -340,13 +341,13 @@ $.suggestions = {
                }
        }
 };
-$.fn.suggestions = function() {
+$.fn.suggestions = function () {
 
        // Multi-context fields
-       var returnValue = null;
+       var returnValue;
        var args = arguments;
 
-       $(this).each( function() {
+       $(this).each( function () {
 
                /* Construction / Loading */
 
@@ -354,8 +355,8 @@ $.fn.suggestions = function() {
                if ( context === undefined || context === null ) {
                        context = {
                                config: {
-                                       'fetch' : function() {},
-                                       'cancel': function() {},
+                                       'fetch' : function () {},
+                                       'cancel': function () {},
                                        'special': {},
                                        'result': {},
                                        '$region': $(this),
@@ -383,7 +384,7 @@ $.fn.suggestions = function() {
                                if ( args.length > 1 ) {
                                        // Set property values
                                        $.suggestions.configure( context, args[0], args[1] );
-                               } else if ( returnValue == null ) {
+                               } else if ( returnValue === null || returnValue === undefined ) {
                                        // Get property values, but don't give access to internal data - returns only the first
                                        returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
                                }
@@ -395,15 +396,18 @@ $.fn.suggestions = function() {
                if ( context.data === undefined ) {
                        context.data = {
                                // ID of running timer
-                               'timerID': null,
+                               timerID: null,
+
                                // Text in textbox when suggestions were last fetched
-                               'prevText': null,
+                               prevText: null,
+
                                // Number of results visible without scrolling
-                               'visibleResults': 0,
+                               visibleResults: 0,
+
                                // Suggestion the last mousedown event occured on
-                               'mouseDownOn': $( [] ),
-                               '$textbox': $(this),
-                               'selectedWithMouse': false
+                               mouseDownOn: $( [] ),
+                               $textbox: $(this),
+                               selectedWithMouse: false
                        };
                        // Setup the css for positioning the results box
                        var newCSS = {
@@ -426,14 +430,14 @@ $.fn.suggestions = function() {
                                        $( '<div>' ).addClass( 'suggestions-results' )
                                                // Can't use click() because the container div is hidden when the textbox loses focus. Instead,
                                                // listen for a mousedown followed by a mouseup on the same div
-                                               .mousedown( function( e ) {
+                                               .mousedown( function ( e ) {
                                                        context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results div' );
                                                } )
-                                               .mouseup( function( e ) {
+                                               .mouseup( function ( e ) {
                                                        var $result = $( e.target ).closest( '.suggestions-results div' );
                                                        var $other = context.data.mouseDownOn;
                                                        context.data.mouseDownOn = $( [] );
-                                                       if ( $result.get( 0 ) != $other.get( 0 ) ) {
+                                                       if ( $result.get( 0 ) !== $other.get( 0 ) ) {
                                                                return;
                                                        }
                                                        $.suggestions.highlight( context, $result, true );
@@ -448,14 +452,14 @@ $.fn.suggestions = function() {
                                        $( '<div>' ).addClass( 'suggestions-special' )
                                                // Can't use click() because the container div is hidden when the textbox loses focus. Instead,
                                                // listen for a mousedown followed by a mouseup on the same div
-                                               .mousedown( function( e ) {
+                                               .mousedown( function ( e ) {
                                                        context.data.mouseDownOn = $( e.target ).closest( '.suggestions-special' );
                                                } )
-                                               .mouseup( function( e ) {
+                                               .mouseup( function ( e ) {
                                                        var $special = $( e.target ).closest( '.suggestions-special' );
                                                        var $other = context.data.mouseDownOn;
                                                        context.data.mouseDownOn = $( [] );
-                                                       if ( $special.get( 0 ) != $other.get( 0 ) ) {
+                                                       if ( $special.get( 0 ) !== $other.get( 0 ) ) {
                                                                return;
                                                        }
                                                        context.data.$container.hide();
@@ -464,7 +468,7 @@ $.fn.suggestions = function() {
                                                        }
                                                        context.data.$textbox.focus();
                                                } )
-                                               .mousemove( function( e ) {
+                                               .mousemove( function ( e ) {
                                                        context.data.selectedWithMouse = true;
                                                        $.suggestions.highlight(
                                                                context, $( e.target ).closest( '.suggestions-special' ), false
@@ -475,9 +479,9 @@ $.fn.suggestions = function() {
                        $(this)
                                // Stop browser autocomplete from interfering
                                .attr( 'autocomplete', 'off')
-                               .keydown( function( e ) {
+                               .keydown( function ( e ) {
                                        // Store key pressed to handle later
-                                       context.data.keypressed = ( e.keyCode === undefined ) ? e.which : e.keyCode;
+                                       context.data.keypressed = e.which;
                                        context.data.keypressedCount = 0;
 
                                        switch ( context.data.keypressed ) {
@@ -496,18 +500,18 @@ $.fn.suggestions = function() {
                                                        }
                                        }
                                } )
-                               .keypress( function( e ) {
+                               .keypress( function ( e ) {
                                        context.data.keypressedCount++;
                                        $.suggestions.keypress( e, context, context.data.keypressed );
                                } )
-                               .keyup( function( e ) {
+                               .keyup( function ( e ) {
                                        // Some browsers won't throw keypress() for arrow keys. If we got a keydown and a keyup without a
                                        // keypress in between, solve it
                                        if ( context.data.keypressedCount === 0 ) {
                                                $.suggestions.keypress( e, context, context.data.keypressed );
                                        }
                                } )
-                               .blur( function() {
+                               .blur( function () {
                                        // When losing focus because of a mousedown
                                        // on a suggestion, don't hide the suggestions
                                        if ( context.data.mouseDownOn.length > 0 ) {
@@ -520,6 +524,7 @@ $.fn.suggestions = function() {
                // Store the context for next time
                $(this).data( 'suggestions-context', context );
        } );
-       return returnValue !== null ? returnValue : $(this);
+       return returnValue !== undefined ? returnValue : $(this);
 };
-} )( jQuery );
\ No newline at end of file
+
+}( jQuery ) );
index 75731d7..cdae0ba 100644 (file)
@@ -1,50 +1,52 @@
 /**
  * jQuery tabIndex
  */
-( function( $ ) {
-/**
- * Finds the lowerst tabindex in use within a selection
- *
- * @return number Lowest tabindex on the page
- */
-$.fn.firstTabIndex = function() {
-       var minTabIndex = null;
-       $(this).find( '[tabindex]' ).each( function() {
-               var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
-               // In IE6/IE7 the above jQuery selector returns all elements,
-               // becuase it has a default value for tabIndex in IE6/IE7 of 0
-               // (rather than null/undefined). Therefore check "> 0" as well.
-               // Under IE7 under Windows NT 5.2 is also capable of returning NaN.
-               if ( tabIndex > 0 && !isNaN( tabIndex ) ) {
-                       // Initial value
-                       if ( minTabIndex === null ) {
-                               minTabIndex = tabIndex;
-                       } else if ( tabIndex < minTabIndex ) {
-                               minTabIndex = tabIndex;
+( function ( $ ) {
+
+       /**
+        * Finds the lowerst tabindex in use within a selection
+        *
+        * @return number Lowest tabindex on the page
+        */
+       $.fn.firstTabIndex = function () {
+               var minTabIndex = null;
+               $(this).find( '[tabindex]' ).each( function () {
+                       var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
+                       // In IE6/IE7 the above jQuery selector returns all elements,
+                       // becuase it has a default value for tabIndex in IE6/IE7 of 0
+                       // (rather than null/undefined). Therefore check "> 0" as well.
+                       // Under IE7 under Windows NT 5.2 is also capable of returning NaN.
+                       if ( tabIndex > 0 && !isNaN( tabIndex ) ) {
+                               // Initial value
+                               if ( minTabIndex === null ) {
+                                       minTabIndex = tabIndex;
+                               } else if ( tabIndex < minTabIndex ) {
+                                       minTabIndex = tabIndex;
+                               }
                        }
-               }
-       } );
-       return minTabIndex;
-};
+               } );
+               return minTabIndex;
+       };
 
-/**
- * Finds the highest tabindex in use within a selection
- *
- * @return number Highest tabindex on the page
- */
-$.fn.lastTabIndex = function() {
-       var maxTabIndex = null;
-       $(this).find( '[tabindex]' ).each( function() {
-               var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
-               if ( tabIndex > 0 && !isNaN( tabIndex ) ) {
-                       // Initial value
-                       if ( maxTabIndex === null ) {
-                               maxTabIndex = tabIndex;
-                       } else if ( tabIndex > maxTabIndex ) {
-                               maxTabIndex = tabIndex;
+       /**
+        * Finds the highest tabindex in use within a selection
+        *
+        * @return number Highest tabindex on the page
+        */
+       $.fn.lastTabIndex = function () {
+               var maxTabIndex = null;
+               $(this).find( '[tabindex]' ).each( function () {
+                       var tabIndex = parseInt( $(this).prop( 'tabindex' ), 10 );
+                       if ( tabIndex > 0 && !isNaN( tabIndex ) ) {
+                               // Initial value
+                               if ( maxTabIndex === null ) {
+                                       maxTabIndex = tabIndex;
+                               } else if ( tabIndex > maxTabIndex ) {
+                                       maxTabIndex = tabIndex;
+                               }
                        }
-               }
-       } );
-       return maxTabIndex;
-};
-} )( jQuery );
+               } );
+               return maxTabIndex;
+       };
+
+}( jQuery ) );
index 08272a5..0edc8ee 100644 (file)
@@ -56,7 +56,7 @@
  * @author Christian Bach/christian.bach@polyester.se
  */
 
-( function( $ ) {
+( function ( $, mw ) {
 
        /* Local scope */
 
        }
 
        function detectParserForColumn( table, rows, cellIndex ) {
-               var     l = parsers.length,
+               var l = parsers.length,
                        nodeValue,
                        // Start with 1 because 0 is the fallback parser
                        i = 1,
        }
 
        function buildParserCache( table, $headers ) {
-               var     rows = table.tBodies[0].rows,
+               var rows = table.tBodies[0].rows,
                        sortType,
                        parsers = [];
 
                if ( rows[0] ) {
 
-                       var     cells = rows[0].cells,
+                       var cells = rows[0].cells,
                                len = cells.length,
                                i, parser;
 
        /* Other utility functions */
 
        function buildCache( table ) {
-               var     totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length ) || 0,
+               var totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length ) || 0,
                        totalCells = ( table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length ) || 0,
                        parsers = table.config.parsers,
                        cache = {
                for ( var i = 0; i < totalRows; ++i ) {
 
                        // Add the table data to main data array
-                       var     $row = $( table.tBodies[0].rows[i] ),
+                       var $row = $( table.tBodies[0].rows[i] ),
                                cols = [];
 
                        // if this is a child row, add it to the last row's children and
        }
 
        function appendToTable( table, cache ) {
-               var     row = cache.row,
+               var row = cache.row,
                        normalized = cache.normalized,
                        totalRows = normalized.length,
                        checkCell = ( normalized[0].length - 1 ),
                }
                table.tBodies[0].appendChild( fragment );
        }
-       
+
        /**
         * Find all header rows in a thead-less table and put them in a <thead> tag.
         * This only treats a row as a header row if it contains only <th>s (no <td>s)
         * and if it is preceded entirely by header rows. The algorithm stops when
         * it encounters the first non-header row.
-        * 
+        *
         * After this, it will look at all rows at the bottom for footer rows
         * And place these in a tfoot using similar rules.
         * @param $table jQuery object for a <table>
-        */ 
+        */
        function emulateTHeadAndFoot( $table ) {
                var $rows = $table.find( '> tbody > tr' );
                if( !$table.get(0).tHead ) {
                        var $thead = $( '<thead>' );
-                       $rows.each( function() {
+                       $rows.each( function () {
                                if ( $(this).children( 'td' ).length > 0 ) {
                                        // This row contains a <td>, so it's not a header row
                                        // Stop here
                                        break;
                                }
                                $tfoot.prepend( $( $rows[i] ));
-                       } 
+                       }
                        $table.append( $tfoot );
                }
        }
 
        function buildHeaders( table, msg ) {
-               var     maxSeen = 0,
+               var maxSeen = 0,
                        longest,
                        realCellIndex = 0,
                        $tableHeaders = $( 'thead:eq(0) > tr', table );
                if ( $tableHeaders.length > 1 ) {
-                       $tableHeaders.each( function() {
+                       $tableHeaders.each( function () {
                                if ( this.cells.length > maxSeen ) {
                                        maxSeen = this.cells.length;
                                        longest = this;
                        });
                        $tableHeaders = $( longest );
                }
-               $tableHeaders = $tableHeaders.children( 'th' ).each( function( index ) {
+               $tableHeaders = $tableHeaders.children( 'th' ).each( function ( index ) {
                        this.column = realCellIndex;
 
                        var colspan = this.colspan;
        function isValueInArray( v, a ) {
                var l = a.length;
                for ( var i = 0; i < l; i++ ) {
-                       if ( a[i][0] == v ) {
+                       if ( a[i][0] === v ) {
                                return true;
                        }
                }
                $headers.removeClass( css[0] ).removeClass( css[1] );
 
                var h = [];
-               $headers.each( function( offset ) {
+               $headers.each( function ( offset ) {
                        if ( !this.sortDisabled ) {
                                h[this.column] = $( this );
                        }
 
        function explodeRowspans( $table ) {
                // Split multi row cells into multiple cells with the same content
-               $table.find( '> tbody > tr > [rowspan]' ).each(function() {
+               $table.find( '> tbody > tr > [rowspan]' ).each(function () {
                        var rowSpan = this.rowSpan;
                        this.rowSpan = 1;
                        var cell = $( this );
                         * @param $tables {jQuery}
                         * @param settings {Object} (optional)
                         */
-                       construct: function( $tables, settings ) {
-                               return $tables.each( function( i, table ) {
+                       construct: function ( $tables, settings ) {
+                               return $tables.each( function ( i, table ) {
                                        // Declare and cache.
-                                       var     $document, $headers, cache, config, sortOrder,
+                                       var $document, $headers, cache, config, sortOrder,
                                                $table = $( table ),
                                                shiftDown = 0,
                                                firstTime = true;
                                                // No thead found. Look for rows with <th>s and
                                                // move them into a <thead> tag or a <tfoot> tag
                                                emulateTHeadAndFoot( $table );
-                                               
+
                                                // Still no thead? Then quit
                                                if ( !table.tHead ) {
                                                        return;
 
                                        // Apply event handling to headers
                                        // this is too big, perhaps break it out?
-                                       $headers.click( function( e ) {
-                                               if ( e.target.nodeName.toLowerCase() == 'a' ) {
+                                       $headers.click( function ( e ) {
+                                               if ( e.target.nodeName.toLowerCase() === 'a' ) {
                                                        // The user clicked on a link inside a table header
                                                        // Do nothing and let the default link click action continue
                                                        return true;
                                                                        for ( var j = 0; j < config.sortList.length; j++ ) {
                                                                                var s = config.sortList[j],
                                                                                        o = config.headerList[s[0]];
-                                                                               if ( s[0] == i ) {
+                                                                               if ( s[0] === i ) {
                                                                                        o.count = s[1];
                                                                                        o.count++;
                                                                                        s[1] = o.count % 2;
                                                }
 
                                        // Cancel selection
-                                       } ).mousedown( function() {
+                                       } ).mousedown( function () {
                                                if ( config.cancelSelection ) {
-                                                       this.onselectstart = function() {
+                                                       this.onselectstart = function () {
                                                                return false;
                                                        };
                                                        return false;
                                } );
                        },
 
-                       addParser: function( parser ) {
-                               var     l = parsers.length,
+                       addParser: function ( parser ) {
+                               var l = parsers.length,
                                        a = true;
                                for ( var i = 0; i < l; i++ ) {
-                                       if ( parsers[i].id.toLowerCase() == parser.id.toLowerCase() ) {
+                                       if ( parsers[i].id.toLowerCase() === parser.id.toLowerCase() ) {
                                                a = false;
                                        }
                                }
                                }
                        },
 
-                       formatDigit: function( s ) {
+                       formatDigit: function ( s ) {
                                if ( ts.transformTable !== false ) {
-                                       var     out = '',
+                                       var out = '',
                                                c;
                                        for ( var p = 0; p < s.length; p++ ) {
                                                c = s.charAt(p);
                                return ( isNaN(i)) ? 0 : i;
                        },
 
-                       formatFloat: function( s ) {
+                       formatFloat: function ( s ) {
                                var i = parseFloat(s);
                                return ( isNaN(i)) ? 0 : i;
                        },
 
-                       formatInt: function( s ) {
+                       formatInt: function ( s ) {
                                var i = parseInt( s, 10 );
                                return ( isNaN(i)) ? 0 : i;
                        },
 
-                       clearTableBody: function( table ) {
+                       clearTableBody: function ( table ) {
                                if ( $.browser.msie ) {
-                                       var empty = function( el ) {
+                                       var empty = function ( el ) {
                                                while ( el.firstChild ) {
                                                        el.removeChild( el.firstChild );
                                                }
        ts = $.tablesorter;
 
        // Register as jQuery prototype method
-       $.fn.tablesorter = function( settings ) {
+       $.fn.tablesorter = function ( settings ) {
                return ts.construct( this, settings );
        };
 
        // Add default parsers
        ts.addParser( {
                id: 'text',
-               is: function( s ) {
+               is: function ( s ) {
                        return true;
                },
-               format: function( s ) {
+               format: function ( s ) {
                        s = $.trim( s.toLowerCase() );
                        if ( ts.collationRegex ) {
                                var tsc = ts.collationTable;
-                               s = s.replace( ts.collationRegex, function( match ) {
+                               s = s.replace( ts.collationRegex, function ( match ) {
                                        var r = tsc[match] ? tsc[match] : tsc[match.toUpperCase()];
                                        return r.toLowerCase();
                                } );
 
        ts.addParser( {
                id: 'IPAddress',
-               is: function( s ) {
+               is: function ( s ) {
                        return ts.rgx.IPAddress[0].test(s);
                },
-               format: function( s ) {
-                       var     a = s.split( '.' ),
+               format: function ( s ) {
+                       var a = s.split( '.' ),
                                r = '',
                                l = a.length;
                        for ( var i = 0; i < l; i++ ) {
                                var item = a[i];
-                               if ( item.length == 1 ) {
+                               if ( item.length === 1 ) {
                                        r += '00' + item;
-                               } else if ( item.length == 2 ) {
+                               } else if ( item.length === 2 ) {
                                        r += '0' + item;
                                } else {
                                        r += item;
 
        ts.addParser( {
                id: 'currency',
-               is: function( s ) {
+               is: function ( s ) {
                        return ts.rgx.currency[0].test(s);
                },
-               format: function( s ) {
+               format: function ( s ) {
                        return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[1], '' ) );
                },
                type: 'numeric'
 
        ts.addParser( {
                id: 'url',
-               is: function( s ) {
+               is: function ( s ) {
                        return ts.rgx.url[0].test(s);
                },
-               format: function( s ) {
+               format: function ( s ) {
                        return $.trim( s.replace( ts.rgx.url[1], '' ) );
                },
                type: 'text'
 
        ts.addParser( {
                id: 'isoDate',
-               is: function( s ) {
+               is: function ( s ) {
                        return ts.rgx.isoDate[0].test(s);
                },
-               format: function( s ) {
+               format: function ( s ) {
                        return $.tablesorter.formatFloat((s !== '') ? new Date(s.replace(
                        new RegExp( /-/g), '/')).getTime() : '0' );
                },
 
        ts.addParser( {
                id: 'usLongDate',
-               is: function( s ) {
+               is: function ( s ) {
                        return ts.rgx.usLongDate[0].test(s);
                },
-               format: function( s ) {
+               format: function ( s ) {
                        return $.tablesorter.formatFloat( new Date(s).getTime() );
                },
                type: 'numeric'
 
        ts.addParser( {
                id: 'date',
-               is: function( s ) {
+               is: function ( s ) {
                        return ( ts.dateRegex[0].test(s) || ts.dateRegex[1].test(s) || ts.dateRegex[2].test(s ));
                },
-               format: function( s, table ) {
+               format: function ( s, table ) {
+                       var match;
                        s = $.trim( s.toLowerCase() );
 
-                       var match;
                        if ( ( match = s.match( ts.dateRegex[0] ) ) !== null ) {
-                               if ( mw.config.get( 'wgDefaultDateFormat' ) == 'mdy' || mw.config.get( 'wgContentLanguage' ) == 'en' ) {
+                               if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgContentLanguage' ) === 'en' ) {
                                        s = [ match[3], match[1], match[2] ];
-                               } else if ( mw.config.get( 'wgDefaultDateFormat' ) == 'dmy' ) {
+                               } else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) {
                                        s = [ match[3], match[2], match[1] ];
                                }
                        } else if ( ( match = s.match( ts.dateRegex[1] ) ) !== null ) {
                        }
 
                        // Pad Month and Day
-                       if ( s[1].length == 1 ) {
+                       if ( s[1].length === 1 ) {
                                s[1] = '0' + s[1];
                        }
-                       if ( s[2].length == 1 ) {
+                       if ( s[2].length === 1 ) {
                                s[2] = '0' + s[2];
                        }
 
 
        ts.addParser( {
                id: 'time',
-               is: function( s ) {
+               is: function ( s ) {
                        return ts.rgx.time[0].test(s);
                },
-               format: function( s ) {
+               format: function ( s ) {
                        return $.tablesorter.formatFloat( new Date( '2000/01/01 ' + s ).getTime() );
                },
                type: 'numeric'
 
        ts.addParser( {
                id: 'number',
-               is: function( s, table ) {
+               is: function ( s, table ) {
                        return $.tablesorter.numberRegex.test( $.trim( s ));
                },
-               format: function( s ) {
+               format: function ( s ) {
                        return $.tablesorter.formatDigit(s);
                },
                type: 'numeric'
        } );
 
-} )( jQuery );
+}( jQuery, mediaWiki ) );
index 91b6e75..9f1f3a4 100644 (file)
 /**
  * These plugins provide extra functionality for interaction with textareas.
  */
-( function( $ ) {
+( function ( $ ) {
+       /*jshint noempty:false */
 
-if (document.selection && document.selection.createRange) {
-       // On IE, patch the focus() method to restore the windows' scroll position
-       // (bug 32241)
-       $.fn.extend({
-               focus : (function ( _focus ) {
-                       return function () {
-                               if ( arguments.length == 0 ) {
-                                       var $w = $( window );
-                                       var state = {top: $w.scrollTop(), left: $w.scrollLeft()};
-                                       var result = _focus.apply( this, arguments );
-                                       window.scrollTo( state.top, state.left );
-                                       return result;
-                               }
-                               return _focus.apply( this, arguments );
-                       };
-               })( $.fn.focus )
-       });
-}
-
-$.fn.textSelection = function( command, options ) {
-
-/**
- * Helper function to get an IE TextRange object for an element
- */
-function rangeForElementIE( e ) {
-       if ( e.nodeName.toLowerCase() == 'input' ) {
-               return e.createTextRange();
-       } else {
-               var sel = document.body.createTextRange();
-               sel.moveToElementText( e );
-               return sel;
+       if ( document.selection && document.selection.createRange ) {
+               // On IE, patch the focus() method to restore the windows' scroll position
+               // (bug 32241)
+               $.fn.extend({
+                       focus : ( function ( _focus ) {
+                               return function () {
+                                       if ( arguments.length === 0 ) {
+                                               var $w = $( window );
+                                               var state = {top: $w.scrollTop(), left: $w.scrollLeft()};
+                                               var result = _focus.apply( this, arguments );
+                                               window.scrollTo( state.top, state.left );
+                                               return result;
+                                       }
+                                       return _focus.apply( this, arguments );
+                               };
+                       }( $.fn.focus ) )
+               });
        }
-}
 
-/**
- * Helper function for IE for activating the textarea. Called only in the
- * IE-specific code paths below; makes use of IE-specific non-standard
- * function setActive() if possible to avoid screen flicker.
- */
-function activateElementOnIE( element ) {
-       if ( element.setActive ) {
-               element.setActive(); // bug 32241: doesn't scroll
-       } else {
-               $( element ).focus(); // may scroll (but we patched it above)
-       }
-}
-
-var fn = {
-/**
- * Get the contents of the textarea
- */
-getContents: function() {
-       return this.val();
-},
-/**
- * Get the currently selected text in this textarea. Will focus the textarea
- * in some browsers (IE/Opera)
- */
-getSelection: function() {
-       var e = this.get( 0 );
-       var retval = '';
-       if ( $(e).is( ':hidden' ) ) {
-               // Do nothing
-       } else if ( document.selection && document.selection.createRange ) {
-               activateElementOnIE( e );
-               var range = document.selection.createRange();
-               retval = range.text;
-       } else if ( e.selectionStart || e.selectionStart == '0' ) {
-               retval = e.value.substring( e.selectionStart, e.selectionEnd );
-       }
-       return retval;
-},
-/**
- * Ported from skins/common/edit.js by Trevor Parscal
- * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
- *
- * Inserts text at the begining and end of a text selection, optionally
- * inserting text at the caret when selection is empty.
- *
- * @fixme document the options parameters
- */
-encapsulateSelection: function( options ) {
-       return this.each( function() {
-               var pre = options.pre, post = options.post;
+       $.fn.textSelection = function ( command, options ) {
 
                /**
-                * Check if the selected text is the same as the insert text
+                * Helper function to get an IE TextRange object for an element
                 */
-               function checkSelectedText() {
-                       if ( !selText ) {
-                               selText = options.peri;
-                               isSample = true;
-                       } else if ( options.replace ) {
-                               selText = options.peri;
+               function rangeForElementIE( e ) {
+                       if ( e.nodeName.toLowerCase() === 'input' ) {
+                               return e.createTextRange();
                        } else {
-                               while ( selText.charAt( selText.length - 1 ) == ' ' ) {
-                                       // Exclude ending space char
-                                       selText = selText.substring( 0, selText.length - 1 );
-                                       post += ' ';
-                               }
-                               while ( selText.charAt( 0 ) == ' ' ) {
-                                       // Exclude prepending space char
-                                       selText = selText.substring( 1, selText.length );
-                                       pre = ' ' + pre;
-                               }
+                               var sel = document.body.createTextRange();
+                               sel.moveToElementText( e );
+                               return sel;
                        }
                }
 
                /**
-                * Do the splitlines stuff.
-                *
-                * Wrap each line of the selected text with pre and post
+                * Helper function for IE for activating the textarea. Called only in the
+                * IE-specific code paths below; makes use of IE-specific non-standard
+                * function setActive() if possible to avoid screen flicker.
                 */
-               function doSplitLines( selText, pre, post ) {
-                       var insertText = '';
-                       var selTextArr = selText.split( '\n' );
-                       for ( var i = 0; i < selTextArr.length; i++ ) {
-                               insertText += pre + selTextArr[i] + post;
-                               if ( i != selTextArr.length - 1 ) {
-                                       insertText += '\n';
-                               }
+               function activateElementOnIE( element ) {
+                       if ( element.setActive ) {
+                               element.setActive(); // bug 32241: doesn't scroll
+                       } else {
+                               $( element ).focus(); // may scroll (but we patched it above)
                        }
-                       return insertText;
                }
 
-               var isSample = false;
-               if ( this.style.display == 'none' ) {
-                       // Do nothing
-               } else if ( document.selection && document.selection.createRange ) {
-                       // IE
+               var fn = {
+                       /**
+                        * Get the contents of the textarea
+                        */
+                       getContents: function () {
+                               return this.val();
+                       },
+                       /**
+                        * Get the currently selected text in this textarea. Will focus the textarea
+                        * in some browsers (IE/Opera)
+                        */
+                       getSelection: function () {
+                               var e = this.get( 0 );
+                               var retval = '';
+                               if ( $(e).is( ':hidden' ) ) {
+                                       // Do nothing
+                               } else if ( document.selection && document.selection.createRange ) {
+                                       activateElementOnIE( e );
+                                       var range = document.selection.createRange();
+                                       retval = range.text;
+                               } else if ( e.selectionStart || e.selectionStart === 0 ) {
+                                       retval = e.value.substring( e.selectionStart, e.selectionEnd );
+                               }
+                               return retval;
+                       },
+                       /**
+                        * Ported from skins/common/edit.js by Trevor Parscal
+                        * (c) 2009 Wikimedia Foundation (GPLv2) - http://www.wikimedia.org
+                        *
+                        * Inserts text at the begining and end of a text selection, optionally
+                        * inserting text at the caret when selection is empty.
+                        *
+                        * @fixme document the options parameters
+                        */
+                       encapsulateSelection: function ( options ) {
+                               return this.each( function () {
+                                       var selText, scrollTop, insertText,
+                                               pre = options.pre,
+                                               post = options.post;
 
-                       // Note that IE9 will trigger the next section unless we check this first.
-                       // See bug 35201.
+                                       /**
+                                        * Check if the selected text is the same as the insert text
+                                        */
+                                       function checkSelectedText() {
+                                               if ( !selText ) {
+                                                       selText = options.peri;
+                                                       isSample = true;
+                                               } else if ( options.replace ) {
+                                                       selText = options.peri;
+                                               } else {
+                                                       while ( selText.charAt( selText.length - 1 ) === ' ' ) {
+                                                               // Exclude ending space char
+                                                               selText = selText.substring( 0, selText.length - 1 );
+                                                               post += ' ';
+                                                       }
+                                                       while ( selText.charAt( 0 ) === ' ' ) {
+                                                               // Exclude prepending space char
+                                                               selText = selText.substring( 1, selText.length );
+                                                               pre = ' ' + pre;
+                                                       }
+                                               }
+                                       }
 
-                       activateElementOnIE( this );
-                       if ( context ) {
-                               context.fn.restoreCursorAndScrollTop();
-                       }
-                       if ( options.selectionStart !== undefined ) {
-                               $(this).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
-                       }
+                                       /**
+                                        * Do the splitlines stuff.
+                                        *
+                                        * Wrap each line of the selected text with pre and post
+                                        */
+                                       function doSplitLines( selText, pre, post ) {
+                                               var insertText = '';
+                                               var selTextArr = selText.split( '\n' );
+                                               for ( var i = 0; i < selTextArr.length; i++ ) {
+                                                       insertText += pre + selTextArr[i] + post;
+                                                       if ( i !== selTextArr.length - 1 ) {
+                                                               insertText += '\n';
+                                                       }
+                                               }
+                                               return insertText;
+                                       }
 
-                       var selText = $(this).textSelection( 'getSelection' );
-                       var scrollTop = this.scrollTop;
-                       var range = document.selection.createRange();
+                                       var isSample = false;
+                                       if ( this.style.display === 'none' ) {
+                                               // Do nothing
+                                       } else if ( document.selection && document.selection.createRange ) {
+                                               // IE
 
-                       checkSelectedText();
-                       var insertText = pre + selText + post;
-                       if ( options.splitlines ) {
-                               insertText = doSplitLines( selText, pre, post );
-                       }
-                       if ( options.ownline && range.moveStart ) {
-                               var range2 = document.selection.createRange();
-                               range2.collapse();
-                               range2.moveStart( 'character', -1 );
-                               // FIXME: Which check is correct?
-                               if ( range2.text != "\r" && range2.text != "\n" && range2.text != "" ) {
-                                       insertText = "\n" + insertText;
-                                       pre += "\n";
-                               }
-                               var range3 = document.selection.createRange();
-                               range3.collapse( false );
-                               range3.moveEnd( 'character', 1 );
-                               if ( range3.text != "\r" && range3.text != "\n" && range3.text != "" ) {
-                                       insertText += "\n";
-                                       post += "\n";
-                               }
-                       }
+                                               // Note that IE9 will trigger the next section unless we check this first.
+                                               // See bug 35201.
 
-                       range.text = insertText;
-                       if ( isSample && options.selectPeri && range.moveStart ) {
-                               range.moveStart( 'character', - post.length - selText.length );
-                               range.moveEnd( 'character', - post.length );
-                       }
-                       range.select();
-                       // Restore the scroll position
-                       this.scrollTop = scrollTop;
-               } else if ( this.selectionStart || this.selectionStart == '0' ) {
-                       // Mozilla/Opera
+                                               activateElementOnIE( this );
+                                               if ( context ) {
+                                                       context.fn.restoreCursorAndScrollTop();
+                                               }
+                                               if ( options.selectionStart !== undefined ) {
+                                                       $(this).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
+                                               }
 
-                       $(this).focus();
-                       if ( options.selectionStart !== undefined ) {
-                               $(this).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
-                       }
-                       
-                       var selText = $(this).textSelection( 'getSelection' );
-                       var startPos = this.selectionStart;
-                       var endPos = this.selectionEnd;
-                       var scrollTop = this.scrollTop;
-                       checkSelectedText();
-                       if ( options.selectionStart !== undefined
-                                       && endPos - startPos != options.selectionEnd - options.selectionStart )
-                       {
-                               // This means there is a difference in the selection range returned by browser and what we passed.
-                               // This happens for Chrome in the case of composite characters. Ref bug #30130
-                               // Set the startPos to the correct position.
-                               startPos = options.selectionStart;
-                       }
+                                               selText = $(this).textSelection( 'getSelection' );
+                                               scrollTop = this.scrollTop;
+                                               var range = document.selection.createRange();
 
-                       var insertText = pre + selText + post;
-                       if ( options.splitlines ) {
-                               insertText = doSplitLines( selText, pre, post );
-                       }
-                       if ( options.ownline ) {
-                               if ( startPos != 0 && this.value.charAt( startPos - 1 ) != "\n" && this.value.charAt( startPos - 1 ) != "\r" ) {
-                                       insertText = "\n" + insertText;
-                                       pre += "\n";
-                               }
-                               if ( this.value.charAt( endPos ) != "\n" && this.value.charAt( endPos ) != "\r" ) {
-                                       insertText += "\n";
-                                       post += "\n";
-                               }
-                       }
-                       this.value = this.value.substring( 0, startPos ) + insertText +
-                               this.value.substring( endPos, this.value.length );
-                       // Setting this.value scrolls the textarea to the top, restore the scroll position
-                       this.scrollTop = scrollTop;
-                       if ( window.opera ) {
-                               pre = pre.replace( /\r?\n/g, "\r\n" );
-                               selText = selText.replace( /\r?\n/g, "\r\n" );
-                               post = post.replace( /\r?\n/g, "\r\n" );
-                       }
-                       if ( isSample && options.selectPeri && !options.splitlines ) {
-                               this.selectionStart = startPos + pre.length;
-                               this.selectionEnd = startPos + pre.length + selText.length;
-                       } else {
-                               this.selectionStart = startPos + insertText.length;
-                               this.selectionEnd = this.selectionStart;
-                       }
-               }
-               $(this).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
-                       options.replace, options.spitlines ] );
-       });
-},
-/**
- * Ported from Wikia's LinkSuggest extension
- * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
- * Some code copied from
- * http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
- *
- * Get the position (in resolution of bytes not nessecarily characters)
- * in a textarea
- *
- * Will focus the textarea in some browsers (IE/Opera)
- *
- * @fixme document the options parameters
- */
- getCaretPosition: function( options ) {
-       function getCaret( e ) {
-               var caretPos = 0, endPos = 0;
-               if ( document.selection && document.selection.createRange ) {
-                       // IE doesn't properly report non-selected caret position through
-                       // the selection ranges when textarea isn't focused. This can
-                       // lead to saving a bogus empty selection, which then screws up
-                       // whatever we do later (bug 31847).
-                       activateElementOnIE( e );
+                                               checkSelectedText();
+                                               insertText = pre + selText + post;
+                                               if ( options.splitlines ) {
+                                                       insertText = doSplitLines( selText, pre, post );
+                                               }
+                                               if ( options.ownline && range.moveStart ) {
+                                                       var range2 = document.selection.createRange();
+                                                       range2.collapse();
+                                                       range2.moveStart( 'character', -1 );
+                                                       // FIXME: Which check is correct?
+                                                       if ( range2.text !== "\r" && range2.text !== "\n" && range2.text !== "" ) {
+                                                               insertText = "\n" + insertText;
+                                                               pre += "\n";
+                                                       }
+                                                       var range3 = document.selection.createRange();
+                                                       range3.collapse( false );
+                                                       range3.moveEnd( 'character', 1 );
+                                                       if ( range3.text !== "\r" && range3.text !== "\n" && range3.text !== "" ) {
+                                                               insertText += "\n";
+                                                               post += "\n";
+                                                       }
+                                               }
 
-                       // IE Support
-                       var preFinished = false;
-                       var periFinished = false;
-                       var postFinished = false;
-                       var preText, rawPreText, periText;
-                       var rawPeriText, postText, rawPostText;
-                       // Create range containing text in the selection
-                       var periRange = document.selection.createRange().duplicate();
-                       // Create range containing text before the selection
-                       var preRange = rangeForElementIE( e );
-                       // Move the end where we need it
-                       preRange.setEndPoint("EndToStart", periRange);
-                       // Create range containing text after the selection
-                       var postRange = rangeForElementIE( e );
-                       // Move the start where we need it
-                       postRange.setEndPoint("StartToEnd", periRange);
-                       // Load the text values we need to compare
-                       preText = rawPreText = preRange.text;
-                       periText = rawPeriText = periRange.text;
-                       postText = rawPostText = postRange.text;
-                       /*
-                        * Check each range for trimmed newlines by shrinking the range by 1
-                        * character and seeing if the text property has changed. If it has
-                        * not changed then we know that IE has trimmed a \r\n from the end.
-                        */
-                       do {
-                               if ( !preFinished ) {
-                                       if ( preRange.compareEndPoints( "StartToEnd", preRange ) == 0 ) {
-                                               preFinished = true;
-                                       } else {
-                                               preRange.moveEnd( "character", -1 );
-                                               if ( preRange.text == preText ) {
-                                                       rawPreText += "\r\n";
+                                               range.text = insertText;
+                                               if ( isSample && options.selectPeri && range.moveStart ) {
+                                                       range.moveStart( 'character', - post.length - selText.length );
+                                                       range.moveEnd( 'character', - post.length );
+                                               }
+                                               range.select();
+                                               // Restore the scroll position
+                                               this.scrollTop = scrollTop;
+                                       } else if ( this.selectionStart || this.selectionStart === 0 ) {
+                                               // Mozilla/Opera
+
+                                               $(this).focus();
+                                               if ( options.selectionStart !== undefined ) {
+                                                       $(this).textSelection( 'setSelection', { 'start': options.selectionStart, 'end': options.selectionEnd } );
+                                               }
+
+                                               selText = $(this).textSelection( 'getSelection' );
+                                               var startPos = this.selectionStart;
+                                               var endPos = this.selectionEnd;
+                                               scrollTop = this.scrollTop;
+                                               checkSelectedText();
+                                               if ( options.selectionStart !== undefined
+                                                               && endPos - startPos !== options.selectionEnd - options.selectionStart )
+                                               {
+                                                       // This means there is a difference in the selection range returned by browser and what we passed.
+                                                       // This happens for Chrome in the case of composite characters. Ref bug #30130
+                                                       // Set the startPos to the correct position.
+                                                       startPos = options.selectionStart;
+                                               }
+
+                                               insertText = pre + selText + post;
+                                               if ( options.splitlines ) {
+                                                       insertText = doSplitLines( selText, pre, post );
+                                               }
+                                               if ( options.ownline ) {
+                                                       if ( startPos !== 0 && this.value.charAt( startPos - 1 ) !== "\n" && this.value.charAt( startPos - 1 ) !== "\r" ) {
+                                                               insertText = "\n" + insertText;
+                                                               pre += "\n";
+                                                       }
+                                                       if ( this.value.charAt( endPos ) !== "\n" && this.value.charAt( endPos ) !== "\r" ) {
+                                                               insertText += "\n";
+                                                               post += "\n";
+                                                       }
+                                               }
+                                               this.value = this.value.substring( 0, startPos ) + insertText +
+                                                       this.value.substring( endPos, this.value.length );
+                                               // Setting this.value scrolls the textarea to the top, restore the scroll position
+                                               this.scrollTop = scrollTop;
+                                               if ( window.opera ) {
+                                                       pre = pre.replace( /\r?\n/g, "\r\n" );
+                                                       selText = selText.replace( /\r?\n/g, "\r\n" );
+                                                       post = post.replace( /\r?\n/g, "\r\n" );
+                                               }
+                                               if ( isSample && options.selectPeri && !options.splitlines ) {
+                                                       this.selectionStart = startPos + pre.length;
+                                                       this.selectionEnd = startPos + pre.length + selText.length;
                                                } else {
-                                                       preFinished = true;
+                                                       this.selectionStart = startPos + insertText.length;
+                                                       this.selectionEnd = this.selectionStart;
                                                }
                                        }
+                                       $(this).trigger( 'encapsulateSelection', [ options.pre, options.peri, options.post, options.ownline,
+                                               options.replace, options.spitlines ] );
+                               });
+                       },
+                       /**
+                        * Ported from Wikia's LinkSuggest extension
+                        * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
+                        * Some code copied from
+                        * http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
+                        *
+                        * Get the position (in resolution of bytes not nessecarily characters)
+                        * in a textarea
+                        *
+                        * Will focus the textarea in some browsers (IE/Opera)
+                        *
+                        * @fixme document the options parameters
+                        */
+                        getCaretPosition: function ( options ) {
+                               function getCaret( e ) {
+                                       var caretPos = 0, endPos = 0;
+                                       if ( document.selection && document.selection.createRange ) {
+                                               // IE doesn't properly report non-selected caret position through
+                                               // the selection ranges when textarea isn't focused. This can
+                                               // lead to saving a bogus empty selection, which then screws up
+                                               // whatever we do later (bug 31847).
+                                               activateElementOnIE( e );
+
+                                               // IE Support
+                                               var preFinished = false;
+                                               var periFinished = false;
+                                               var postFinished = false;
+                                               var preText, rawPreText, periText;
+                                               var rawPeriText, postText, rawPostText;
+                                               // Create range containing text in the selection
+                                               var periRange = document.selection.createRange().duplicate();
+                                               // Create range containing text before the selection
+                                               var preRange = rangeForElementIE( e );
+                                               // Move the end where we need it
+                                               preRange.setEndPoint( 'EndToStart', periRange );
+                                               // Create range containing text after the selection
+                                               var postRange = rangeForElementIE( e );
+                                               // Move the start where we need it
+                                               postRange.setEndPoint( 'StartToEnd', periRange );
+                                               // Load the text values we need to compare
+                                               preText = rawPreText = preRange.text;
+                                               periText = rawPeriText = periRange.text;
+                                               postText = rawPostText = postRange.text;
+                                               /*
+                                                * Check each range for trimmed newlines by shrinking the range by 1
+                                                * character and seeing if the text property has changed. If it has
+                                                * not changed then we know that IE has trimmed a \r\n from the end.
+                                                */
+                                               do {
+                                                       if ( !preFinished ) {
+                                                               if ( preRange.compareEndPoints( 'StartToEnd', preRange ) === 0 ) {
+                                                                       preFinished = true;
+                                                               } else {
+                                                                       preRange.moveEnd( 'character', -1 );
+                                                                       if ( preRange.text === preText ) {
+                                                                               rawPreText += "\r\n";
+                                                                       } else {
+                                                                               preFinished = true;
+                                                                       }
+                                                               }
+                                                       }
+                                                       if ( !periFinished ) {
+                                                               if ( periRange.compareEndPoints( 'StartToEnd', periRange ) === 0 ) {
+                                                                       periFinished = true;
+                                                               } else {
+                                                                       periRange.moveEnd( 'character', -1 );
+                                                                       if ( periRange.text === periText ) {
+                                                                               rawPeriText += "\r\n";
+                                                                       } else {
+                                                                               periFinished = true;
+                                                                       }
+                                                               }
+                                                       }
+                                                       if ( !postFinished ) {
+                                                               if ( postRange.compareEndPoints( 'StartToEnd', postRange ) === 0 ) {
+                                                                       postFinished = true;
+                                                               } else {
+                                                                       postRange.moveEnd( 'character', -1 );
+                                                                       if ( postRange.text === postText ) {
+                                                                               rawPostText += "\r\n";
+                                                                       } else {
+                                                                               postFinished = true;
+                                                                       }
+                                                               }
+                                                       }
+                                               } while ( ( !preFinished || !periFinished || !postFinished ) );
+                                               caretPos = rawPreText.replace( /\r\n/g, "\n" ).length;
+                                               endPos = caretPos + rawPeriText.replace( /\r\n/g, "\n" ).length;
+                                       } else if ( e.selectionStart || e.selectionStart === 0 ) {
+                                               // Firefox support
+                                               caretPos = e.selectionStart;
+                                               endPos = e.selectionEnd;
+                                       }
+                                       return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
                                }
-                               if ( !periFinished ) {
-                                       if ( periRange.compareEndPoints( "StartToEnd", periRange ) == 0 ) {
-                                               periFinished = true;
-                                       } else {
-                                               periRange.moveEnd( "character", -1 );
-                                               if ( periRange.text == periText ) {
-                                                       rawPeriText += "\r\n";
+                               return getCaret( this.get( 0 ) );
+                       },
+                       /**
+                        * @fixme document the options parameters
+                        */
+                       setSelection: function ( options ) {
+                               return this.each( function () {
+                                       if ( $(this).is( ':hidden' ) ) {
+                                               // Do nothing
+                                       } else if ( this.selectionStart || this.selectionStart === 0 ) {
+                                               // Opera 9.0 doesn't allow setting selectionStart past
+                                               // selectionEnd; any attempts to do that will be ignored
+                                               // Make sure to set them in the right order
+                                               if ( options.start > this.selectionEnd ) {
+                                                       this.selectionEnd = options.end;
+                                                       this.selectionStart = options.start;
                                                } else {
-                                                       periFinished = true;
+                                                       this.selectionStart = options.start;
+                                                       this.selectionEnd = options.end;
+                                               }
+                                       } else if ( document.body.createTextRange ) {
+                                               var selection = rangeForElementIE( this );
+                                               var length = this.value.length;
+                                               // IE doesn't count \n when computing the offset, so we won't either
+                                               var newLines = this.value.match( /\n/g );
+                                               if ( newLines ) {
+                                                       length = length - newLines.length;
                                                }
+                                               selection.moveStart( 'character', options.start );
+                                               selection.moveEnd( 'character', -length + options.end );
+
+                                               // This line can cause an error under certain circumstances (textarea empty, no selection)
+                                               // Silence that error
+                                               try {
+                                                       selection.select();
+                                               } catch( e ) { }
                                        }
+                               });
+                       },
+                       /**
+                        * Ported from Wikia's LinkSuggest extension
+                        * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
+                        *
+                        * Scroll a textarea to the current cursor position. You can set the cursor
+                        * position with setSelection()
+                        * @param options boolean Whether to force a scroll even if the caret position
+                        *  is already visible. Defaults to false
+                        *
+                        * @fixme document the options parameters (function body suggests options.force is a boolean, not options itself)
+                        */
+                       scrollToCaretPosition: function ( options ) {
+                               function getLineLength( e ) {
+                                       return Math.floor( e.scrollWidth / ( $.client.profile().platform === 'linux' ? 7 : 8 ) );
                                }
-                               if ( !postFinished ) {
-                                       if ( postRange.compareEndPoints("StartToEnd", postRange) == 0 ) {
-                                               postFinished = true;
-                                       } else {
-                                               postRange.moveEnd( "character", -1 );
-                                               if ( postRange.text == postText ) {
-                                                       rawPostText += "\r\n";
-                                               } else {
-                                                       postFinished = true;
+                               function getCaretScrollPosition( e ) {
+                                       var i, j;
+                                       // FIXME: This functions sucks and is off by a few lines most
+                                       // of the time. It should be replaced by something decent.
+                                       var text = e.value.replace( /\r/g, '' );
+                                       var caret = $( e ).textSelection( 'getCaretPosition' );
+                                       var lineLength = getLineLength( e );
+                                       var row = 0;
+                                       var charInLine = 0;
+                                       var lastSpaceInLine = 0;
+                                       for ( i = 0; i < caret; i++ ) {
+                                               charInLine++;
+                                               if ( text.charAt( i ) === ' ' ) {
+                                                       lastSpaceInLine = charInLine;
+                                               } else if ( text.charAt( i ) === "\n" ) {
+                                                       lastSpaceInLine = 0;
+                                                       charInLine = 0;
+                                                       row++;
+                                               }
+                                               if ( charInLine > lineLength ) {
+                                                       if ( lastSpaceInLine > 0 ) {
+                                                               charInLine = charInLine - lastSpaceInLine;
+                                                               lastSpaceInLine = 0;
+                                                               row++;
+                                                       }
                                                }
                                        }
+                                       var nextSpace = 0;
+                                       for ( j = caret; j < caret + lineLength; j++ ) {
+                                               if (
+                                                       text.charAt( j ) === ' ' ||
+                                                       text.charAt( j ) === "\n" ||
+                                                       caret === text.length
+                                               ) {
+                                                       nextSpace = j;
+                                                       break;
+                                               }
+                                       }
+                                       if ( nextSpace > lineLength && caret <= lineLength ) {
+                                               charInLine = caret - lastSpaceInLine;
+                                               row++;
+                                       }
+                                       return ( $.client.profile().platform === 'mac' ? 13 : ( $.client.profile().platform === 'linux' ? 15 : 16 ) ) * row;
                                }
-                       } while ( ( !preFinished || !periFinished || !postFinished ) );
-                       caretPos = rawPreText.replace( /\r\n/g, "\n" ).length;
-                       endPos = caretPos + rawPeriText.replace( /\r\n/g, "\n" ).length;
-               } else if ( e.selectionStart || e.selectionStart == '0' ) {
-                       // Firefox support
-                       caretPos = e.selectionStart;
-                       endPos = e.selectionEnd;
-               }
-               return options.startAndEnd ? [ caretPos, endPos ] : caretPos;
-       }
-       return getCaret( this.get( 0 ) );
-},
-/**
- * @fixme document the options parameters
- */
-setSelection: function( options ) {
-       return this.each( function() {
-               if ( $(this).is( ':hidden' ) ) {
-                       // Do nothing
-               } else if ( this.selectionStart || this.selectionStart == '0' ) {
-                       // Opera 9.0 doesn't allow setting selectionStart past
-                       // selectionEnd; any attempts to do that will be ignored
-                       // Make sure to set them in the right order
-                       if ( options.start > this.selectionEnd ) {
-                               this.selectionEnd = options.end;
-                               this.selectionStart = options.start;
-                       } else {
-                               this.selectionStart = options.start;
-                               this.selectionEnd = options.end;
+                               return this.each(function () {
+                                       if ( $(this).is( ':hidden' ) ) {
+                                               // Do nothing
+                                       } else if ( this.selectionStart || this.selectionStart === 0 ) {
+                                               // Mozilla
+                                               var scroll = getCaretScrollPosition( this );
+                                               if ( options.force || scroll < $(this).scrollTop() ||
+                                                               scroll > $(this).scrollTop() + $(this).height() ) {
+                                                       $(this).scrollTop( scroll );
+                                               }
+                                       } else if ( document.selection && document.selection.createRange ) {
+                                               // IE / Opera
+                                               /*
+                                                * IE automatically scrolls the selected text to the
+                                                * bottom of the textarea at range.select() time, except
+                                                * if it was already in view and the cursor position
+                                                * wasn't changed, in which case it does nothing. To
+                                                * cover that case, we'll force it to act by moving one
+                                                * character back and forth.
+                                                */
+                                               var range = document.body.createTextRange();
+                                               var savedRange = document.selection.createRange();
+                                               var pos = $(this).textSelection( 'getCaretPosition' );
+                                               var oldScrollTop = this.scrollTop;
+                                               range.moveToElementText( this );
+                                               range.collapse();
+                                               range.move( 'character', pos + 1);
+                                               range.select();
+                                               if ( this.scrollTop !== oldScrollTop ) {
+                                                       this.scrollTop += range.offsetTop;
+                                               } else if ( options.force ) {
+                                                       range.move( 'character', -1 );
+                                                       range.select();
+                                               }
+                                               savedRange.select();
+                                       }
+                                       $(this).trigger( 'scrollToPosition' );
+                               } );
                        }
-               } else if ( document.body.createTextRange ) {
-                       var selection = rangeForElementIE( this );
-                       var length = this.value.length;
-                       // IE doesn't count \n when computing the offset, so we won't either
-                       var newLines = this.value.match( /\n/g );
-                       if ( newLines ) length = length - newLines.length;
-                       selection.moveStart( 'character', options.start );
-                       selection.moveEnd( 'character', -length + options.end );
+               };
 
-                       // This line can cause an error under certain circumstances (textarea empty, no selection)
-                       // Silence that error
-                       try {
-                               selection.select();
-                       } catch( e ) { }
-               }
-       });
-},
-/**
- * Ported from Wikia's LinkSuggest extension
- * https://svn.wikia-code.com/wikia/trunk/extensions/wikia/LinkSuggest
- *
- * Scroll a textarea to the current cursor position. You can set the cursor
- * position with setSelection()
- * @param options boolean Whether to force a scroll even if the caret position
- *  is already visible. Defaults to false
- *
- * @fixme document the options parameters (function body suggests options.force is a boolean, not options itself)
- */
-scrollToCaretPosition: function( options ) {
-       function getLineLength( e ) {
-               return Math.floor( e.scrollWidth / ( $.client.profile().platform == 'linux' ? 7 : 8 ) );
-       }
-       function getCaretScrollPosition( e ) {
-               // FIXME: This functions sucks and is off by a few lines most
-               // of the time. It should be replaced by something decent.
-               var text = e.value.replace( /\r/g, "" );
-               var caret = $( e ).textSelection( 'getCaretPosition' );
-               var lineLength = getLineLength( e );
-               var row = 0;
-               var charInLine = 0;
-               var lastSpaceInLine = 0;
-               for ( i = 0; i < caret; i++ ) {
-                       charInLine++;
-                       if ( text.charAt( i ) == " " ) {
-                               lastSpaceInLine = charInLine;
-                       } else if ( text.charAt( i ) == "\n" ) {
-                               lastSpaceInLine = 0;
-                               charInLine = 0;
-                               row++;
-                       }
-                       if ( charInLine > lineLength ) {
-                               if ( lastSpaceInLine > 0 ) {
-                                       charInLine = charInLine - lastSpaceInLine;
-                                       lastSpaceInLine = 0;
-                                       row++;
+               // Apply defaults
+               switch ( command ) {
+                       //case 'getContents': // no params
+                       //case 'setContents': // no params with defaults
+                       //case 'getSelection': // no params
+                       case 'encapsulateSelection':
+                               options = $.extend( {
+                                       pre: '', // Text to insert before the cursor/selection
+                                       peri: '', // Text to insert between pre and post and select afterwards
+                                       post: '', // Text to insert after the cursor/selection
+                                       ownline: false, // Put the inserted text on a line of its own
+                                       replace: false, // If there is a selection, replace it with peri instead of leaving it alone
+                                       selectPeri: true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
+                                       splitlines: false, // If multiple lines are selected, encapsulate each line individually
+                                       selectionStart: undefined, // Position to start selection at
+                                       selectionEnd: undefined // Position to end selection at. Defaults to start
+                               }, options );
+                               break;
+                       case 'getCaretPosition':
+                               options = $.extend( {
+                                       // Return [start, end] instead of just start
+                                       startAndEnd: false
+                               }, options );
+                               // FIXME: We may not need character position-based functions if we insert markers in the right places
+                               break;
+                       case 'setSelection':
+                               options = $.extend( {
+                                       // Position to start selection at
+                                       start: undefined,
+                                       // Position to end selection at. Defaults to start
+                                       end: undefined,
+                                       // Element to start selection in (iframe only)
+                                       startContainer: undefined,
+                                       // Element to end selection in (iframe only). Defaults to startContainer
+                                       endContainer: undefined
+                               }, options );
+
+                               if ( options.end === undefined ) {
+                                       options.end = options.start;
                                }
-                       }
-               }
-               var nextSpace = 0;
-               for ( j = caret; j < caret + lineLength; j++ ) {
-                       if (
-                               text.charAt( j ) == " " ||
-                               text.charAt( j ) == "\n" ||
-                               caret == text.length
-                       ) {
-                               nextSpace = j;
+                               if ( options.endContainer === undefined ) {
+                                       options.endContainer = options.startContainer;
+                               }
+                               // FIXME: We may not need character position-based functions if we insert markers in the right places
+                               break;
+                       case 'scrollToCaretPosition':
+                               options = $.extend( {
+                                       force: false // Force a scroll even if the caret position is already visible
+                               }, options );
                                break;
-                       }
                }
-               if ( nextSpace > lineLength && caret <= lineLength ) {
-                       charInLine = caret - lastSpaceInLine;
-                       row++;
+
+               var context = $(this).data( 'wikiEditor-context' );
+               var hasIframe = typeof context !== 'undefined' && context && typeof context.$iframe !== 'undefined';
+
+               // IE selection restore voodoo
+               var needSave = false;
+               if ( hasIframe && context.savedSelection !== null ) {
+                       context.fn.restoreSelection();
+                       needSave = true;
                }
-               return ( $.client.profile().platform == 'mac' ? 13 : ( $.client.profile().platform == 'linux' ? 15 : 16 ) ) * row;
-       }
-       return this.each(function() {
-               if ( $(this).is( ':hidden' ) ) {
-                       // Do nothing
-               } else if ( this.selectionStart || this.selectionStart == '0' ) {
-                       // Mozilla
-                       var scroll = getCaretScrollPosition( this );
-                       if ( options.force || scroll < $(this).scrollTop() ||
-                                       scroll > $(this).scrollTop() + $(this).height() )
-                               $(this).scrollTop( scroll );
-               } else if ( document.selection && document.selection.createRange ) {
-                       // IE / Opera
-                       /*
-                        * IE automatically scrolls the selected text to the
-                        * bottom of the textarea at range.select() time, except
-                        * if it was already in view and the cursor position
-                        * wasn't changed, in which case it does nothing. To
-                        * cover that case, we'll force it to act by moving one
-                        * character back and forth.
-                        */
-                       var range = document.body.createTextRange();
-                       var savedRange = document.selection.createRange();
-                       var pos = $(this).textSelection( 'getCaretPosition' );
-                       var oldScrollTop = this.scrollTop;
-                       range.moveToElementText( this );
-                       range.collapse();
-                       range.move( 'character', pos + 1);
-                       range.select();
-                       if ( this.scrollTop != oldScrollTop )
-                               this.scrollTop += range.offsetTop;
-                       else if ( options.force ) {
-                               range.move( 'character', -1 );
-                               range.select();
-                       }
-                       savedRange.select();
+               var retval = ( hasIframe ? context.fn : fn )[command].call( this, options );
+               if ( hasIframe && needSave ) {
+                       context.fn.saveSelection();
                }
-               $(this).trigger( 'scrollToPosition' );
-       } );
-}
-};
-       // Apply defaults
-       switch ( command ) {
-               //case 'getContents': // no params
-               //case 'setContents': // no params with defaults
-               //case 'getSelection': // no params
-               case 'encapsulateSelection':
-                       options = $.extend( {
-                               'pre': '', // Text to insert before the cursor/selection
-                               'peri': '', // Text to insert between pre and post and select afterwards
-                               'post': '', // Text to insert after the cursor/selection
-                               'ownline': false, // Put the inserted text on a line of its own
-                               'replace': false, // If there is a selection, replace it with peri instead of leaving it alone
-                               'selectPeri': true, // Select the peri text if it was inserted (but not if there was a selection and replace==false, or if splitlines==true)
-                               'splitlines': false, // If multiple lines are selected, encapsulate each line individually
-                               'selectionStart': undefined, // Position to start selection at
-                               'selectionEnd': undefined // Position to end selection at. Defaults to start
-                       }, options );
-                       break;
-               case 'getCaretPosition':
-                       options = $.extend( {
-                               'startAndEnd': false // Return [start, end] instead of just start
-                       }, options );
-                       // FIXME: We may not need character position-based functions if we insert markers in the right places
-                       break;
-               case 'setSelection':
-                       options = $.extend( {
-                               'start': undefined, // Position to start selection at
-                               'end': undefined, // Position to end selection at. Defaults to start
-                               'startContainer': undefined, // Element to start selection in (iframe only)
-                               'endContainer': undefined // Element to end selection in (iframe only). Defaults to startContainer
-                       }, options );
-                       if ( options.end === undefined )
-                               options.end = options.start;
-                       if ( options.endContainer == undefined )
-                               options.endContainer = options.startContainer;
-                       // FIXME: We may not need character position-based functions if we insert markers in the right places
-                       break;
-               case 'scrollToCaretPosition':
-                       options = $.extend( {
-                               'force': false // Force a scroll even if the caret position is already visible
-                       }, options );
-                       break;
-       }
-       var context = $(this).data( 'wikiEditor-context' );
-       var hasIframe = typeof context !== 'undefined' && context && typeof context.$iframe !== 'undefined';
 
-       // IE selection restore voodoo
-       var needSave = false;
-       if ( hasIframe && context.savedSelection !== null ) {
-               context.fn.restoreSelection();
-               needSave = true;
-       }
-       var retval = ( hasIframe ? context.fn : fn )[command].call( this, options );
-       if ( hasIframe && needSave ) {
-               context.fn.saveSelection();
-       }
-       return retval;
-};
-} )( jQuery );
+               return retval;
+       };
+
+}( jQuery ) );
index e685ca9..14b845d 100644 (file)
@@ -92,7 +92,7 @@
                isReady = true;
 
                // Make sure edit summary does not exceed byte limit
-               $( '#wpSummary' ).byteLimit( 250 );
+               $( '#wpSummary' ).byteLimit( 255 );
 
                /**
                 * Restore the edit box scroll state following a preview operation,
index 783f031..6dd8ea3 100644 (file)
@@ -10,6 +10,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase {
        public function setUp() {
                global $wgLanguageCode, $wgLang, $wgContLang;
 
+               parent::setUp();
+
                self::$oldLang = $wgLang;
                self::$oldContLang = $wgContLang;
 
@@ -23,6 +25,7 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase {
 
                $wgContLang = $wgLang = Language::factory( $wgLanguageCode );
                MessageCache::singleton()->disable();
+
        }
 
        public function tearDown() {
@@ -32,6 +35,8 @@ abstract class MediaWikiLangTestCase extends MediaWikiTestCase {
                $wgContLang = self::$oldContLang;
                $wgLanguageCode = $wgContLang->getCode();
                self::$oldContLang = self::$oldLang = null;
+
+               parent::tearDown();
        }
 
 }
diff --git a/tests/phpunit/docs/ExportDemoTest.php b/tests/phpunit/docs/ExportDemoTest.php
new file mode 100644 (file)
index 0000000..ce65d49
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Test for the demo xml
+ *
+ * @group Dump
+ */
+class ExportDemoTest extends DumpTestCase {
+
+       /**
+        * @group large
+        */
+       function testExportDemo() {
+               $this->validateXmlFileAgainstXsd( "../../docs/export-demo.xml" );
+       }
+
+       /**
+        * Validates a xml file against the xsd.
+        *
+        * The validation is slow, because php has to read the xsd on each call.
+        *
+        * @param $fname string: name of file to validate
+        */
+       protected function validateXmlFileAgainstXsd( $fname ) {
+               $version = WikiExporter::schemaVersion();
+
+               $dom = new DomDocument();
+               $dom->load( $fname );
+
+               try {
+                       $this->assertTrue( $dom->schemaValidate( "../../docs/export-" . $version . ".xsd" ),
+                               "schemaValidate has found an error" );
+               } catch( Exception $e ) {
+                       $this->fail( "xml not valid against xsd: " . $e->getMessage() );
+               }
+       }
+}
diff --git a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php
new file mode 100644 (file)
index 0000000..30bfb12
--- /dev/null
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * Test for ProcessCacheLRU class.
+ *
+ * Note that it uses the ProcessCacheLRUTestable class which extends some
+ * properties and methods visibility. That class is defined at the end of the
+ * file containing this class.
+ *
+ * @group Cache
+ */
+class ProcessCacheLRUTest extends MediaWikiTestCase {
+
+       /**
+        * Helper to verify emptiness of a cache object.
+        * Compare against an array so we get the cache content difference.
+        */
+       function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) {
+               $this->assertAttributeEquals( array(), 'cache', $cache, $msg );
+       }
+
+       /**
+        * Helper to fill a cache object passed by reference
+        */
+       function fillCache( &$cache, $numEntries ) {
+               // Fill cache with three values
+               for( $i=1; $i<=$numEntries; $i++) {
+                       $cache->set( "cache-key-$i", "prop-$i", "value-$i" );
+               }
+       }
+
+       /**
+        * Generates an array of what would be expected in cache for a given cache
+        * size and a number of entries filled in sequentially
+        */
+       function getExpectedCache( $cacheMaxEntries, $entryToFill ) {
+               $expected = array();
+
+               if( $entryToFill === 0 ) {
+                       # The cache is empty!
+                       return array();
+               } elseif( $entryToFill <= $cacheMaxEntries ) {
+                       # Cache is not fully filled
+                       $firstKey = 1;
+               } else {
+                       # Cache overflowed
+                       $firstKey = 1 + $entryToFill - $cacheMaxEntries;
+               }
+
+               $lastKey  = $entryToFill;
+
+               for( $i=$firstKey; $i<=$lastKey; $i++ ) {
+                       $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" );
+               }
+               return $expected;
+       }
+
+       /**
+        * Highlight diff between assertEquals and assertNotSame
+        */
+       function testPhpUnitArrayEquality() {
+               $one = array( 'A' => 1, 'B' => 2 );
+               $two = array( 'B' => 2, 'A' => 1 );
+               $this->assertEquals( $one, $two );  // ==
+               $this->assertNotSame( $one, $two ); // ===
+       }
+
+       /**
+        * @dataProvider provideInvalidConstructorArg
+        * @expectedException MWException
+        */
+       function testConstructorGivenInvalidValue( $maxSize ) {
+               $c = new ProcessCacheLRUTestable( $maxSize );
+       }
+
+       /**
+        * Value which are forbidden by the constructor
+        */
+       function provideInvalidConstructorArg() {
+               return array(
+                       array( null ),
+                       array( array() ),
+                       array( new stdClass() ),
+                       array( 0 ),
+                       array( '5' ),
+                       array( -1 ),
+               );
+       }
+
+       function testAddAndGetAKey() {
+               $oneCache = new ProcessCacheLRUTestable( 1 );
+               $this->assertCacheEmpty( $oneCache );
+
+               // First set just one value
+               $oneCache->set( 'cache-key', 'prop1', 'value1' );
+               $this->assertEquals( 1, $oneCache->getEntriesCount() );
+               $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) );
+               $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) );
+       }
+
+       function testDeleteOldKey() {
+               $oneCache = new ProcessCacheLRUTestable( 1 );
+               $this->assertCacheEmpty( $oneCache );
+
+               $oneCache->set( 'cache-key', 'prop1', 'value1' );
+               $oneCache->set( 'cache-key', 'prop1', 'value2' );
+               $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) );
+       }
+
+       /**
+        * This test that we properly overflow when filling a cache with
+        * a sequence of always different cache-keys. Meant to verify we correclty
+        * delete the older key.
+        *
+        * @dataProvider provideCacheFilling
+        * @param $cacheMaxEntries Maximum entry the created cache will hold
+        * @param $entryToFill Number of entries to insert in the created cache.
+        */
+       function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) {
+               $cache = new ProcessCacheLRUTestable( $cacheMaxEntries );
+               $this->fillCache( $cache, $entryToFill);
+
+               $this->assertSame(
+                       $this->getExpectedCache( $cacheMaxEntries, $entryToFill ),
+                       $cache->getCache(),
+                       "Filling a $cacheMaxEntries entries cache with $entryToFill entries"
+               );
+
+       }
+
+       /**
+        * Provider for testFillingCache
+        */
+       function provideCacheFilling() {
+               // ($cacheMaxEntries, $entryToFill, $msg='')
+               return array(
+                       array( 1,  0 ),
+                       array( 1,  1 ),
+                       array( 1,  2 ), # overflow
+                       array( 5, 33 ), # overflow
+               );
+
+       }
+
+       /**
+        * Create a cache with only one remaining entry then update
+        * the first inserted entry. Should bump it to the top.
+        */
+       function testReplaceExistingKeyShouldBumpEntryToTop() {
+               $maxEntries = 3;
+
+               $cache = new ProcessCacheLRUTestable( $maxEntries );
+               // Fill cache leaving just one remaining slot
+               $this->fillCache( $cache, $maxEntries - 1 );
+
+               // Set an existing cache key
+               $cache->set( "cache-key-1", "prop-1", "new-value-for-1" );
+
+               $this->assertSame(
+                       array(
+                               'cache-key-2' => array( 'prop-2' => 'value-2' ),
+                               'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ),
+                       ),
+                       $cache->getCache()
+               );
+       }
+
+       function testRecentlyAccessedKeyStickIn() {
+               $cache = new ProcessCacheLRUTestable( 2 );
+               $cache->set( 'first' , 'prop1', 'value1' );
+               $cache->set( 'second', 'prop2', 'value2' );
+
+               // Get first
+               $cache->get( 'first', 'prop1' );
+               // Cache a third value, should invalidate the least used one
+               $cache->set( 'third', 'prop3', 'value3' );
+
+               $this->assertFalse( $cache->has( 'second', 'prop2' ) );
+       }
+
+       /**
+        * This first create a full cache then update the value for the 2nd
+        * filled entry.
+        * Given a cache having 1,2,3 as key, updating 2 should bump 2 to
+        * the top of the queue with the new value: 1,3,2* (* = updated).
+        */
+       function testReplaceExistingKeyInAFullCacheShouldBumpToTop() {
+               $maxEntries = 3;
+
+               $cache = new ProcessCacheLRUTestable( $maxEntries );
+               $this->fillCache( $cache, $maxEntries );
+
+               // Set an existing cache key
+               $cache->set( "cache-key-2", "prop-2", "new-value-for-2" );
+               $this->assertSame(
+                       array(
+                               'cache-key-1' => array( 'prop-1' => 'value-1' ),
+                               'cache-key-3' => array( 'prop-3' => 'value-3' ),
+                               'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ),
+                       ),
+                       $cache->getCache()
+               );
+               $this->assertEquals( 'new-value-for-2',
+                       $cache->get( 'cache-key-2', 'prop-2' )
+               );
+       }
+
+       function testBumpExistingKeyToTop() {
+               $cache = new ProcessCacheLRUTestable( 3 );
+               $this->fillCache( $cache, 3 );
+
+               // Set the very first cache key to a new value
+               $cache->set( "cache-key-1", "prop-1", "new value for 1" );
+               $this->assertEquals(
+                       array(
+                               'cache-key-2' => array( 'prop-2' => 'value-2' ),
+                               'cache-key-3' => array( 'prop-3' => 'value-3' ),
+                               'cache-key-1' => array( 'prop-1' => 'new value for 1' ),
+                       ),
+                       $cache->getCache()
+               );
+
+       }
+
+}
+
+/**
+ * Overrides some ProcessCacheLRU methods and properties accessibility.
+ */
+class ProcessCacheLRUTestable extends ProcessCacheLRU {
+       public $cache = array();
+
+       public function getCache() {
+               return $this->cache;
+       }
+       public function getEntriesCount() {
+               return count( $this->cache );
+       }
+}
index 61507f5..6fb7ace 100644 (file)
@@ -243,6 +243,8 @@ class FileBackendTest extends MediaWikiTestCase {
                $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $dest ) );
        }
 
        public function provider_testStore() {
@@ -330,6 +332,8 @@ class FileBackendTest extends MediaWikiTestCase {
                $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
                $this->assertEquals( $props1, $props2,
                        "Source and destination have the same props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $source, $dest ) );
        }
 
        public function provider_testCopy() {
@@ -419,6 +423,8 @@ class FileBackendTest extends MediaWikiTestCase {
                        "Source file does not exist accourding to props ($backendName)." );
                $this->assertEquals( true, $props2['fileExists'],
                        "Destination file exists accourding to props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $source, $dest ) );
        }
 
        public function provider_testMove() {
@@ -504,6 +510,8 @@ class FileBackendTest extends MediaWikiTestCase {
                $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
                $this->assertFalse( $props1['fileExists'],
                        "Source file $source does not exist according to props ($backendName)." );
+
+               $this->assertBackendPathsConsistent( array( $source ) );
        }
 
        public function provider_testDelete() {
@@ -595,6 +603,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                $this->backend->getFileSize( array( 'src' => $dest ) ),
                                "Destination file $dest has original size according to props ($backendName)." );
                }
+
+               $this->assertBackendPathsConsistent( array( $dest ) );
        }
 
        /**
@@ -1822,6 +1832,13 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) );
        }
 
+       function assertBackendPathsConsistent( array $paths ) {
+               if ( $this->backend instanceof FileBackendMultiWrite ) {
+                       $status = $this->backend->consistencyCheck( $paths );
+                       $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
+               }
+       }
+
        function assertGoodStatus( $status, $msg ) {
                $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg );
        }
index a1a6d59..bef7677 100644 (file)
@@ -24,43 +24,195 @@ class LanguageTest extends MediaWikiTestCase {
                );
        }
 
-       /** @dataProvider provideFormattableTimes */
+       /**
+        * @dataProvider provideFormattableTimes
+        */
        function testFormatTimePeriod( $seconds, $format, $expected, $desc ) {
                $this->assertEquals( $expected, $this->lang->formatTimePeriod( $seconds, $format ), $desc );
        }
 
        function provideFormattableTimes() {
                return array(
-                       array( 9.45, array(), '9.5s', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 9.45, array( 'noabbrevs' => true ), '9.5 seconds', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 9.95, array(), '10s', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 9.95, array( 'noabbrevs' => true ), '10 seconds', 'formatTimePeriod() rounding (<10s)' ),
-                       array( 59.55, array(), '1m 0s', 'formatTimePeriod() rounding (<60s)' ),
-                       array( 59.55, array( 'noabbrevs' => true ), '1 minute 0 seconds', 'formatTimePeriod() rounding (<60s)' ),
-                       array( 119.55, array(), '2m 0s', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 119.55, array( 'noabbrevs' => true ), '2 minutes 0 seconds', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 3599.55, array(), '1h 0m 0s', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 3599.55, array( 'noabbrevs' => true ), '1 hour 0 minutes 0 seconds', 'formatTimePeriod() rounding (<1h)' ),
-                       array( 7199.55, array(), '2h 0m 0s', 'formatTimePeriod() rounding (>=1h)' ),
-                       array( 7199.55, array( 'noabbrevs' => true ), '2 hours 0 minutes 0 seconds', 'formatTimePeriod() rounding (>=1h)' ),
-                       array( 7199.55, 'avoidseconds', '2h 0m', 'formatTimePeriod() rounding (>=1h), avoidseconds' ),
-                       array( 7199.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 hours 0 minutes', 'formatTimePeriod() rounding (>=1h), avoidseconds' ),
-                       array( 7199.55, 'avoidminutes', '2h 0m', 'formatTimePeriod() rounding (>=1h), avoidminutes' ),
-                       array( 7199.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '2 hours 0 minutes', 'formatTimePeriod() rounding (>=1h), avoidminutes' ),
-                       array( 172799.55, 'avoidseconds', '48h 0m', 'formatTimePeriod() rounding (=48h), avoidseconds' ),
-                       array( 172799.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '48 hours 0 minutes', 'formatTimePeriod() rounding (=48h), avoidseconds' ),
-                       array( 259199.55, 'avoidminutes', '3d 0h', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 259199.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '3 days 0 hours', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 176399.55, 'avoidseconds', '2d 1h 0m', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 176399.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 days 1 hour 0 minutes', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 176399.55, 'avoidminutes', '2d 1h', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 176399.55, array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ), '2 days 1 hour', 'formatTimePeriod() rounding (>48h), avoidminutes' ),
-                       array( 259199.55, 'avoidseconds', '3d 0h 0m', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 259199.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '3 days 0 hours 0 minutes', 'formatTimePeriod() rounding (>48h), avoidseconds' ),
-                       array( 172801.55, 'avoidseconds', '2d 0h 0m', 'formatTimePeriod() rounding, (>48h), avoidseconds' ),
-                       array( 172801.55, array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ), '2 days 0 hours 0 minutes', 'formatTimePeriod() rounding, (>48h), avoidseconds' ),
-                       array( 176460.55, array(), '2d 1h 1m 1s', 'formatTimePeriod() rounding, recursion, (>48h)' ),
-                       array( 176460.55, array( 'noabbrevs' => true ), '2 days 1 hour 1 minute 1 second', 'formatTimePeriod() rounding, recursion, (>48h)' ),
+                       array(
+                               9.45,
+                               array(),
+                               '9.5s',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               9.45,
+                               array( 'noabbrevs' => true ),
+                               '9.5 seconds',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               9.95,
+                               array(),
+                               '10s',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               9.95,
+                               array( 'noabbrevs' => true ),
+                               '10 seconds',
+                               'formatTimePeriod() rounding (<10s)'
+                       ),
+                       array(
+                               59.55,
+                               array(),
+                               '1m 0s',
+                               'formatTimePeriod() rounding (<60s)'
+                       ),
+                       array(
+                               59.55,
+                               array( 'noabbrevs' => true ),
+                               '1 minute 0 seconds',
+                               'formatTimePeriod() rounding (<60s)'
+                       ),
+                       array(
+                               119.55,
+                               array(),
+                               '2m 0s',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               119.55,
+                               array( 'noabbrevs' => true ),
+                               '2 minutes 0 seconds',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               3599.55,
+                               array(),
+                               '1h 0m 0s',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               3599.55,
+                               array( 'noabbrevs' => true ),
+                               '1 hour 0 minutes 0 seconds',
+                               'formatTimePeriod() rounding (<1h)'
+                       ),
+                       array(
+                               7199.55,
+                               array(),
+                               '2h 0m 0s',
+                               'formatTimePeriod() rounding (>=1h)'
+                       ),
+                       array(
+                               7199.55,
+                               array( 'noabbrevs' => true ),
+                               '2 hours 0 minutes 0 seconds',
+                               'formatTimePeriod() rounding (>=1h)'
+                       ),
+                       array(
+                               7199.55,
+                               'avoidseconds',
+                               '2h 0m',
+                               'formatTimePeriod() rounding (>=1h), avoidseconds'
+                       ),
+                       array(
+                               7199.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '2 hours 0 minutes',
+                               'formatTimePeriod() rounding (>=1h), avoidseconds'
+                       ),
+                       array(
+                               7199.55,
+                               'avoidminutes',
+                               '2h 0m',
+                               'formatTimePeriod() rounding (>=1h), avoidminutes'
+                       ),
+                       array(
+                               7199.55,
+                               array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+                               '2 hours 0 minutes',
+                               'formatTimePeriod() rounding (>=1h), avoidminutes'
+                       ),
+                       array(
+                               172799.55,
+                               'avoidseconds',
+                               '48h 0m',
+                               'formatTimePeriod() rounding (=48h), avoidseconds'
+                       ),
+                       array(
+                               172799.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '48 hours 0 minutes',
+                               'formatTimePeriod() rounding (=48h), avoidseconds'
+                       ),
+                       array(
+                               259199.55,
+                               'avoidminutes',
+                               '3d 0h',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               259199.55,
+                               array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+                               '3 days 0 hours',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               176399.55,
+                               'avoidseconds',
+                               '2d 1h 0m',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               176399.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '2 days 1 hour 0 minutes',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               176399.55,
+                               'avoidminutes',
+                               '2d 1h',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               176399.55,
+                               array( 'avoid' => 'avoidminutes', 'noabbrevs' => true ),
+                               '2 days 1 hour',
+                               'formatTimePeriod() rounding (>48h), avoidminutes'
+                       ),
+                       array(
+                               259199.55,
+                               'avoidseconds',
+                               '3d 0h 0m',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               259199.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '3 days 0 hours 0 minutes',
+                               'formatTimePeriod() rounding (>48h), avoidseconds'
+                       ),
+                       array(
+                               172801.55,
+                               'avoidseconds',
+                               '2d 0h 0m',
+                               'formatTimePeriod() rounding, (>48h), avoidseconds'
+                       ),
+                       array(
+                               172801.55,
+                               array( 'avoid' => 'avoidseconds', 'noabbrevs' => true ),
+                               '2 days 0 hours 0 minutes',
+                               'formatTimePeriod() rounding, (>48h), avoidseconds'
+                       ),
+                       array(
+                               176460.55,
+                               array(),
+                               '2d 1h 1m 1s',
+                               'formatTimePeriod() rounding, recursion, (>48h)'
+                       ),
+                       array(
+                               176460.55,
+                               array( 'noabbrevs' => true ),
+                               '2 days 1 hour 1 minute 1 second',
+                               'formatTimePeriod() rounding, recursion, (>48h)'
+                       ),
                );
 
        }
@@ -98,8 +250,8 @@ class LanguageTest extends MediaWikiTestCase {
        }
 
        /**
-       * @dataProvider provideHTMLTruncateData()
-       */
+        * @dataProvider provideHTMLTruncateData()
+        */
        function testTruncateHtml( $len, $ellipsis, $input, $expected ) {
                // Actual HTML...
                $this->assertEquals(
@@ -792,16 +944,16 @@ class LanguageTest extends MediaWikiTestCase {
                );
        }
 
-       /**\r
-        * @dataProvider provideCheckTitleEncodingData\r
-        */\r
-       function testCheckTitleEncoding( $s ) {\r
-               $this->assertEquals(\r
-                       $s,\r
-                       $this->lang->checkTitleEncoding($s),\r
-                       "checkTitleEncoding('$s')"\r
-               );\r
-       }\r
+       /**
+        * @dataProvider provideCheckTitleEncodingData
+        */
+       function testCheckTitleEncoding( $s ) {
+               $this->assertEquals(
+                       $s,
+                       $this->lang->checkTitleEncoding($s),
+                       "checkTitleEncoding('$s')"
+               );
+       }
 
        function provideCheckTitleEncodingData() {
                return array (
@@ -815,42 +967,42 @@ class LanguageTest extends MediaWikiTestCase {
                        ),
                        // The following two data sets come from bug 36839. They fail if checkTitleEncoding uses a regexp to test for
                        // valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding
-                   // uses mb_check_encoding for its test.
+                       // uses mb_check_encoding for its test.
                        array(
                                rawurldecode(
                                        "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C"
-                                       . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C"
-                                       . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C"
-                                       . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C"
-                                       . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C"
-                                       . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C"
-                                       . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C"
-                                       . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C"
-                                       . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C"
-                                       . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C"
-                                       . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C"
-                                       . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C"
-                                       . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C"
-                                       . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis"
+                                               . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C"
+                                               . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C"
+                                               . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C"
+                                               . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C"
+                                               . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C"
+                                               . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C"
+                                               . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C"
+                                               . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C"
+                                               . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C"
+                                               . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C"
+                                               . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C"
+                                               . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C"
+                                               . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis"
                                ),
                        ),
                        array(
                                rawurldecode(
                                        "Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C"
-                                       . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C"
-                                       . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C"
-                                       . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou"
-                                       . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C"
-                                       . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C"
-                                       . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C"
-                                       . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C"
-                                       . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C"
-                                       . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C"
-                                       . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes"
+                                               . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C"
+                                               . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C"
+                                               . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou"
+                                               . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C"
+                                               . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C"
+                                               . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C"
+                                               . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C"
+                                               . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C"
+                                               . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C"
+                                               . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes"
                                )
                        )
                );
index 86beb5f..0e700ef 100644 (file)
@@ -141,7 +141,7 @@ test( '$content', function() {
  * one element can have a given id. 
  */
 test( 'addPortletLink', function () {
-       var pTestTb, vectorTabs, tbRL, cuQuux, $cuQuux, tbMW, $tbMW, tbRLDM, caFoo;
+       var pTestTb, pCustom, vectorTabs, tbRL, cuQuux, $cuQuux, tbMW, $tbMW, tbRLDM, caFoo;
        expect( 8 );
 
        pTestTb = '\