Merge "Convert UtfNormalTest into a PHPUnit test"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 20 Feb 2015 18:50:45 +0000 (18:50 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 20 Feb 2015 18:50:45 +0000 (18:50 +0000)
47 files changed:
.rubocop.yml
.rubocop_todo.yml
Gemfile
RELEASE-NOTES-1.25
autoload.php
docs/hooks.txt
docs/sitelist-1.0.xsd [new file with mode: 0644]
docs/sitelist.txt [new file with mode: 0644]
includes/CategoryViewer.php
includes/DefaultSettings.php
includes/Linker.php
includes/api/ApiQueryBase.php
includes/cache/BacklinkCache.php
includes/deferred/HTMLCacheUpdate.php
includes/deferred/LinksUpdate.php
includes/jobqueue/jobs/RefreshLinksJob.php
includes/page/Article.php
includes/page/ImagePage.php
includes/page/WikiPage.php
includes/parser/ParserOutput.php
includes/poolcounter/PoolWorkArticleView.php
includes/resourceloader/ResourceLoaderModule.php
includes/site/SiteExporter.php [new file with mode: 0644]
includes/site/SiteImporter.php [new file with mode: 0644]
includes/specials/SpecialFileDuplicateSearch.php
maintenance/dumpSisterSites.php [deleted file]
maintenance/exportSites.php [new file with mode: 0644]
maintenance/importSites.php [new file with mode: 0644]
maintenance/jsduck/CustomTags.rb
resources/Resources.php
resources/src/mediawiki.special/mediawiki.special.preferences.js
tests/browser/features/step_definitions/create_account_steps.rb
tests/browser/features/step_definitions/create_and_follow_wiki_link_steps.rb
tests/browser/features/step_definitions/edit_page_steps.rb
tests/browser/features/step_definitions/file_steps.rb
tests/browser/features/step_definitions/preferences_appearance_steps.rb
tests/browser/features/step_definitions/preferences_user_profile_steps.rb
tests/browser/features/step_definitions/view_history_steps.rb
tests/browser/features/support/pages/edit_page.rb
tests/browser/features/support/pages/login_error_page.rb
tests/browser/features/support/pages/main_page.rb
tests/browser/features/support/pages/preferences_appearance_page.rb
tests/browser/features/support/pages/view_history_page.rb
tests/browser/features/support/pages/ztargetpage.rb
tests/phpunit/includes/site/SiteExporterTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteImporterTest.php [new file with mode: 0644]
tests/phpunit/includes/site/SiteImporterTest.xml [new file with mode: 0644]

index 00479d1..c04818e 100644 (file)
@@ -6,3 +6,6 @@ AllCops:
         - 'skins/**/*'
         - 'tests/frontend/node_modules/**/*'
         - 'vendor/**/*'
+
+Metrics/LineLength:
+  Max: 100
index f0702ba..90671fa 100644 (file)
@@ -5,34 +5,15 @@
 # Note that changes in the inspected code, or installation of new
 # versions of RuboCop, may require this file to be generated again.
 
-# Offense count: 1
-Lint/AmbiguousRegexpLiteral:
-  Enabled: false
-
 # Offense count: 2
 # Cop supports --auto-correct.
 Lint/UnusedMethodArgument:
   Enabled: false
 
-# Offense count: 19
-# Configuration parameters: AllowURI, URISchemes.
-Metrics/LineLength:
-  Max: 94
-
 # Offense count: 10
 Style/Documentation:
   Enabled: false
 
-# Offense count: 1
-# Cop supports --auto-correct.
-Style/EmptyLines:
-  Enabled: false
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Style/EmptyLinesAroundBody:
-  Enabled: false
-
 # Offense count: 1
 # Configuration parameters: Exclude.
 Style/FileName:
@@ -44,41 +25,13 @@ Style/FileName:
 Style/HashSyntax:
   Enabled: false
 
-# Offense count: 2
-# Cop supports --auto-correct.
-Style/LeadingCommentSpace:
-  Enabled: false
-
 # Offense count: 4
 # Cop supports --auto-correct.
 Style/PerlBackrefs:
   Enabled: false
 
-# Offense count: 4
-# Cop supports --auto-correct.
-Style/SpaceAroundOperators:
-  Enabled: false
-
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
-Style/SpaceInsideBlockBraces:
-  Enabled: true
-
-# Offense count: 6
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SupportedStyles.
-Style/SpaceInsideHashLiteralBraces:
-  Enabled: false
-
 # Offense count: 89
 # Cop supports --auto-correct.
 # Configuration parameters: EnforcedStyle, SupportedStyles.
 Style/StringLiterals:
   Enabled: false
-
-# Offense count: 11
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-Style/TrailingBlankLines:
-  Enabled: false
diff --git a/Gemfile b/Gemfile
index 1559d0e..d3f8506 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -1,5 +1,5 @@
-#ruby=ruby-2.1.2
-#ruby-gemset=core
+# ruby=ruby-2.1.2
+# ruby-gemset=core
 
 source "https://rubygems.org"
 
index 67a32ec..cdef220 100644 (file)
@@ -103,6 +103,8 @@ production.
   dynamically-compiled Mustache templates (currently uses lightncandy library).
 * Clickable anchors for each section heading in the content are now generated
   and appear in the gutter on hovering over the heading.
+* Added 'CategoryViewer::doCategoryQuery' and 'CategoryViewer::generateLink' hooks
+  to allow extensions to override how links to pages are rendered within NS_CATEGORY
 
 ==== External libraries ====
 * MediaWiki now requires certain external libraries to be installed. In the past
@@ -360,6 +362,7 @@ changes to languages because of Bugzilla reports.
   The alphabet of the prior string returned was A-Za-z0-9 and now it is 0-9A-F
 * (T87504) Avoid serving SVG background-images in CSS for Opera 12, which
   renders them incorrectly when combined with border-radius or background-size.
+* Removed maintenance script dumpSisterSites.php.
 
 == Compatibility ==
 
index 2f72418..a115e75 100644 (file)
@@ -354,7 +354,6 @@ $wgAutoloadLocalClasses = array(
        'DumpPipeOutput' => __DIR__ . '/includes/Export.php',
        'DumpRenderer' => __DIR__ . '/maintenance/renderDump.php',
        'DumpRev' => __DIR__ . '/maintenance/storage/dumpRev.php',
-       'DumpSisterSites' => __DIR__ . '/maintenance/dumpSisterSites.php',
        'DuplicateJob' => __DIR__ . '/includes/jobqueue/jobs/DuplicateJob.php',
        'EditAction' => __DIR__ . '/includes/actions/EditAction.php',
        'EditCLI' => __DIR__ . '/maintenance/edit.php',
@@ -1052,6 +1051,8 @@ $wgAutoloadLocalClasses = array(
        'Site' => __DIR__ . '/includes/site/Site.php',
        'SiteArray' => __DIR__ . '/includes/site/SiteList.php',
        'SiteConfiguration' => __DIR__ . '/includes/SiteConfiguration.php',
+       'SiteExporter' => __DIR__ . '/includes/site/SiteExporter.php',
+       'SiteImporter' => __DIR__ . '/includes/site/SiteImporter.php',
        'SiteList' => __DIR__ . '/includes/site/SiteList.php',
        'SiteListFileCache' => __DIR__ . '/includes/site/SiteListFileCache.php',
        'SiteListFileCacheBuilder' => __DIR__ . '/includes/site/SiteListFileCacheBuilder.php',
index 78ac2ff..a88803b 100644 (file)
@@ -870,6 +870,20 @@ $wikiPage: WikiPage that was removed
 'CategoryPageView': Before viewing a categorypage in CategoryPage::view.
 $catpage: CategoryPage instance
 
+'CategoryViewer::doCategoryQuery': After querying for pages to be displayed
+in a Category page. Gives extensions the opportunity to batch load any
+related data about the pages.
+$type: The category type. Either 'page', 'file' or 'subcat'
+$res: Query result from DatabaseBase::select()
+
+'CategoryViewer::generateLink': Before generating an output link allow
+extensions opportunity to generate a more specific or relevant link.
+$type: The category type. Either 'page', 'img' or 'subcat'
+$title: Title object for the categorized page
+$html: Requested html content of anchor
+&$link: Returned value. When set to a non-null value by a hook subscriber
+this value will be used as the anchor instead of Linker::link
+
 'ChangePasswordForm': For extensions that need to add a field to the
 ChangePassword form via the Preferences form.
 &$extraFields: An array of arrays that hold fields like would be passed to the
diff --git a/docs/sitelist-1.0.xsd b/docs/sitelist-1.0.xsd
new file mode 100644 (file)
index 0000000..126cd03
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+       This is an XML Schema description of the format
+       used by MediaWiki's exportSites.php and importSites.php
+       scripts.
+-->
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+               xmlns:mwsl="http://www.mediawiki.org/xml/sitelist-1.0/"
+               targetNamespace="http://www.mediawiki.org/xml/sitelist-1.0/"
+               elementFormDefault="qualified">
+
+       <annotation>
+               <documentation xml:lang="en">
+                       MediaWiki's export format for site definitions.
+               </documentation>
+       </annotation>
+
+       <!-- Our root element -->
+       <element name="sites" type="mwsl:MediaWikiSiteListType">
+               <unique name="GlobalIDConstraint">
+                       <selector xpath="mwsl:Site" />
+                       <field xpath="mwsl:GlobalID" />
+               </unique>
+       </element>
+
+       <simpleType name="EmptyTagType">
+               <restriction base="string">
+                       <length value="0"/>
+               </restriction>
+       </simpleType>
+
+       <complexType name="TypedIDType">
+               <simpleContent>
+                       <extension base="NCName">
+                               <attribute name="type" use="required" type="NCName" />
+                       </extension>
+               </simpleContent>
+       </complexType>
+
+       <complexType name="TypedURIType">
+               <simpleContent>
+                       <extension base="anyURI">
+                               <attribute name="type" use="required" type="NCName" />
+                       </extension>
+               </simpleContent>
+       </complexType>
+
+       <complexType name="MediaWikiSiteListType">
+               <sequence>
+                       <element name="site" type="mwsl:SiteType"
+                                        minOccurs="0" maxOccurs="unbounded" />
+               </sequence>
+               <attribute name="version" type="string" use="optional" />
+       </complexType>
+
+       <complexType name="SiteType">
+               <choice maxOccurs="unbounded">
+                       <element name="globalid" type="ID" minOccurs="1" maxOccurs="1" />
+                       <element name="localid" type="mwsl:TypedIDType" minOccurs="0" />
+                       <element name="group" type="NCName" minOccurs="0" maxOccurs="1" />
+                       <element name="source" type="NCName" minOccurs="0" maxOccurs="1" />
+                       <element name="forward" type="mwsl:EmptyTagType" minOccurs="0" maxOccurs="1" />
+                       <element name="path" type="mwsl:TypedURIType" minOccurs="0" />
+               </choice>
+               <attribute name="type" use="optional" type="NCName" />
+       </complexType>
+
+</schema>
diff --git a/docs/sitelist.txt b/docs/sitelist.txt
new file mode 100644 (file)
index 0000000..48c7ce5
--- /dev/null
@@ -0,0 +1,47 @@
+This document describes the XML format used to represent information about external sites known
+to a MediaWiki installation. This information about external sites is used to allow "inter-wiki"
+links, cross-language navigation, as well as close integration via direct access to the other
+site's web API or even directly to their database.
+
+Lists of external sites can be imported and exported using the importSites.php and exportSites.php
+scripts. In the database, external sites are described by the sites and site_ids tables.
+
+The formal specification of the format used by importSites.php and exportSites.php can be found in
+the sitelist-1.0.xsd file. Below is an example and a brief description of what the individual XML
+elements and attributes mean:
+
+
+       <sites version="1.0">
+               <site>
+                       <globalid>acme.com</globalid>
+                       <localid type="interwiki">acme</localid>
+                       <group>Vendor</group>
+                       <path type="link">http://acme.com/</path>
+                       <source>meta.wikimedia.org</source>
+               </site>
+               <site type="mediawiki">
+                       <globalid>de.wikidik.example</globalid>
+                       <localid type="equivalent">de</localid>
+                       <group>Dictionary</group>
+                       <forward/>
+                       <path type="page_path">http://acme.com/</path>
+               </site>
+       </sites>
+
+
+The XML elements are used as follows:
+
+* sites: The root element, containing a set of site tags. May have a version attribute with the value 1.0.
+* site: A site entry, representing an external website. May have a type attribute with one of the following values:
+** ''unknown'': (default) any website
+** ''mediawiki'': A MediaWiki site
+* globalid: A unique identifier for the site. For a given site, the same unique global ID must be used across all wikis in a wiki farm (aka wiki family).
+* localid: An identifier for the site, for use on the local wiki. Multiple local IDs may be assigned to a given site. The same local ID can be used to refer to different sites by different wikis on the same farm/family. The localid element may have a type attribute with one of the following values:
+** interwiki: Used as an "interwiki" link prefix, for creating cross-wiki links.
+** equivalent: Used as a "language" link prefix, for cross-linking equivalent content in different languages.
+* group: The site group (e.g. wiki family) the site belongs to.
+* path: A URL template for accessing resources on the site. Several paths may be defined for a given site, for accessing different kinds of resources, identified by the type attribute, using one of the following values:
+** link: Generic URL template, often the document root.
+** page_path: (for mediawiki sites) URL template for wiki pages (corresponds to the target wiki's $wgArticlePath setting)
+** file_path: (for mediawiki sites) URL pattern for application entry points and resources (corresponds to the target wiki's $wgScriptPath setting).
+* forward: Whether using a prefix defined by a localid tag in the URL will cause the request to be redirected to the corresponding page on the target wiki (currently unused). E.g. whether http://wiki.acme.com/wiki/foo:Buzz should be forwarded to http://wiki.foo.com/read/Buzz. (CAVEAT: not yet implement, can be specified but has no effect)
\ No newline at end of file
index 6b86853..1e0bf16 100644 (file)
@@ -174,19 +174,30 @@ class CategoryViewer extends ContextSource {
                // Subcategory; strip the 'Category' namespace from the link text.
                $title = $cat->getTitle();
 
-               $link = Linker::link( $title, htmlspecialchars( $title->getText() ) );
-               if ( $title->isRedirect() ) {
-                       // This didn't used to add redirect-in-category, but might
-                       // as well be consistent with the rest of the sections
-                       // on a category page.
-                       $link = '<span class="redirect-in-category">' . $link . '</span>';
-               }
-               $this->children[] = $link;
+               $this->children[] = $this->generateLink(
+                       'subcat',
+                       $title,
+                       $title->isRedirect(),
+                       htmlspecialchars( $title->getText() )
+               );
 
                $this->children_start_char[] =
                        $this->getSubcategorySortChar( $cat->getTitle(), $sortkey );
        }
 
+       function generateLink( $type, Title $title, $isRedirect, $html = null ) {
+               $link = null;
+               Hooks::run( 'CategoryViewer::generateLink', array( $type, $title, $html, &$link ) );
+               if ( $link === null ) {
+                       $link = Linker::link( $title, $html );
+               }
+               if ( $isRedirect ) {
+                       $link = '<span class="redirect-in-category">' . $link . '</span>';
+               }
+
+               return $link;
+       }
+
        /**
         * Get the character to be used for sorting subcategories.
         * If there's a link from Category:A to Category:B, the sortkey of the resulting
@@ -229,13 +240,7 @@ class CategoryViewer extends ContextSource {
                                $this->gallery->add( $title );
                        }
                } else {
-                       $link = Linker::link( $title );
-                       if ( $isRedirect ) {
-                               // This seems kind of pointless given 'mw-redirect' class,
-                               // but keeping for back-compatibility with user css.
-                               $link = '<span class="redirect-in-category">' . $link . '</span>';
-                       }
-                       $this->imgsNoGallery[] = $link;
+                       $this->imgsNoGallery[] = $this->generateLink( 'image', $title, $isRedirect );
 
                        $this->imgsNoGallery_start_char[] = $wgContLang->convert(
                                $this->collation->getFirstLetter( $sortkey ) );
@@ -252,13 +257,7 @@ class CategoryViewer extends ContextSource {
        function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
                global $wgContLang;
 
-               $link = Linker::link( $title );
-               if ( $isRedirect ) {
-                       // This seems kind of pointless given 'mw-redirect' class,
-                       // but keeping for back-compatibility with user css.
-                       $link = '<span class="redirect-in-category">' . $link . '</span>';
-               }
-               $this->articles[] = $link;
+               $this->articles[] = $this->generateLink( 'page', $title, $isRedirect );
 
                $this->articles_start_char[] = $wgContLang->convert(
                        $this->collation->getFirstLetter( $sortkey ) );
@@ -331,6 +330,8 @@ class CategoryViewer extends ContextSource {
                                )
                        );
 
+                       Hooks::run( 'CategoryViewer::doCategoryQuery', array( $type, $res ) );
+
                        $count = 0;
                        foreach ( $res as $row ) {
                                $title = Title::newFromRow( $row );
index bddccec..d4cdf9e 100644 (file)
@@ -6422,7 +6422,6 @@ $wgJobClasses = array(
        'PublishStashedFile' => 'PublishStashedFileJob',
        'ThumbnailRender' => 'ThumbnailRenderJob',
        'recentChangesUpdate' => 'RecentChangesUpdateJob',
-       'refreshLinksPrioritized' => 'RefreshLinksJob', // for cascading protection
        'null' => 'NullJob'
 );
 
index ae8695b..dd224ef 100644 (file)
@@ -1251,7 +1251,6 @@ class Linker {
                        $userId = $rev->getUser( Revision::FOR_THIS_USER );
                        $userText = $rev->getUserText( Revision::FOR_THIS_USER );
                        $link = self::userLink( $userId, $userText )
-                               . wfMessage( 'word-separator' )->escaped()
                                . self::userToolLinks( $userId, $userText );
                } else {
                        $link = wfMessage( 'rev-deleted-user' )->escaped();
index 998cc91..7414913 100644 (file)
@@ -513,7 +513,8 @@ abstract class ApiQueryBase extends ApiBase {
         */
        public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) {
                $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' );
-               if ( !$t ) {
+               if ( !$t || $t->hasFragment() ) {
+                       // Invalid title (e.g. bad chars) or contained a '#'.
                        $this->dieUsageMsg( array( 'invalidtitle', $titlePart ) );
                }
                if ( $namespace != $t->getNamespace() || $t->isExternal() ) {
index c3aefc5..c6d9a18 100644 (file)
@@ -487,45 +487,4 @@ class BacklinkCache {
 
                return array( 'numRows' => $numRows, 'batches' => $batches );
        }
-
-       /**
-        * Get a Title iterator for cascade-protected template/file use backlinks
-        *
-        * @return TitleArray
-        * @since 1.25
-        */
-       public function getCascadeProtectedLinks() {
-               // This method is used to make redudant jobs anyway, so its OK to use
-               // a slave. Also, the set of cascade protected pages tends to be stable.
-               $dbr = $this->getDB();
-
-               $queries = array();
-               // @note: UNION filters any duplicate pages
-               $queries[] = $dbr->selectSQLText(
-                       array( 'templatelinks', 'page_restrictions', 'page' ),
-                       array( 'page_namespace', 'page_title', 'page_id' ),
-                       array(
-                               'tl_namespace' => $this->title->getNamespace(),
-                               'tl_title' => $this->title->getDBkey(),
-                               'tl_from = pr_page',
-                               'pr_cascade' => 1,
-                               'page_id = tl_from'
-                       )
-               );
-               $queries[] = $dbr->selectSQLText(
-                       array( 'imagelinks', 'page_restrictions', 'page' ),
-                       array( 'page_namespace', 'page_title', 'page_id' ),
-                       array(
-                               'il_to' => $this->title->getDBkey(),
-                               'il_from = pr_page',
-                               'pr_cascade' => 1,
-                               'page_id = il_from'
-                       )
-               );
-
-               return TitleArray::newFromResult( $dbr->query(
-                       $dbr->unionQueries( $queries, false ),
-                       __METHOD__
-               ) );
-       }
 }
index 862ac27..e02cfbc 100644 (file)
@@ -43,6 +43,7 @@ class HTMLCacheUpdate implements DeferrableUpdate {
        }
 
        public function doUpdate() {
+
                $job = new HTMLCacheUpdateJob(
                        $this->mTitle,
                        array(
@@ -62,5 +63,6 @@ class HTMLCacheUpdate implements DeferrableUpdate {
                                $job->run(); // just do the purge query now
                        } );
                }
+
        }
 }
index e4f00e7..9c377df 100644 (file)
@@ -228,24 +228,12 @@ class LinksUpdate extends SqlDataUpdate {
         * Which means do LinksUpdate on all pages that include the current page,
         * using the job queue.
         */
-       protected function queueRecursiveJobs() {
+       function queueRecursiveJobs() {
                self::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
                if ( $this->mTitle->getNamespace() == NS_FILE ) {
                        // Process imagelinks in case the title is or was a redirect
                        self::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
                }
-
-               $bc = $this->mTitle->getBacklinkCache();
-               // Get jobs for cascade-protected backlinks for a high priority queue.
-               // If meta-templates change to using a new template, the new template
-               // should be implicitly protected as soon as possible, if applicable.
-               // These jobs duplicate a subset of the above ones, but can run sooner.
-               // Which ever runs first generally no-ops the other one.
-               $jobs = array();
-               foreach ( $bc->getCascadeProtectedLinks() as $title ) {
-                       $jobs[] = new RefreshLinksJob( $title, array( 'prioritize' => true ) );
-               }
-               JobQueueGroup::singleton()->push( $jobs );
        }
 
        /**
@@ -265,7 +253,6 @@ class LinksUpdate extends SqlDataUpdate {
                                        "refreshlinks:{$table}:{$title->getPrefixedText()}"
                                )
                        );
-
                        JobQueueGroup::singleton()->push( $job );
                        JobQueueGroup::singleton()->deduplicateRootJob( $job );
                }
index 1252b0b..5d95792 100644 (file)
@@ -39,10 +39,6 @@ class RefreshLinksJob extends Job {
 
        function __construct( $title, $params = '' ) {
                parent::__construct( 'refreshLinks', $title, $params );
-               // A separate type is used just for cascade-protected backlinks
-               if ( !empty( $this->params['prioritize'] ) ) {
-                       $this->command .= 'Prioritized';
-               }
                // Base backlink update jobs and per-title update jobs can be de-duplicated.
                // If template A changes twice before any jobs run, a clean queue will have:
                //              (A base, A base)
@@ -104,10 +100,6 @@ class RefreshLinksJob extends Job {
                return true;
        }
 
-       /**
-        * @param Title $title
-        * @return bool
-        */
        protected function runForTitle( Title $title = null ) {
                $linkCache = LinkCache::singleton();
                $linkCache->clear();
index 83c3241..59f2ae7 100644 (file)
@@ -707,7 +707,7 @@ class Article implements Page {
                }
 
                # Get the ParserOutput actually *displayed* here.
-               # Note that $this->mParserOutput is the *current*/oldid version output.
+               # Note that $this->mParserOutput is the *current* version output.
                $pOutput = ( $outputDone instanceof ParserOutput )
                        ? $outputDone // object fetched by hook
                        : $this->mParserOutput;
index b8f67c2..855f233 100644 (file)
@@ -1365,7 +1365,6 @@ class ImageHistoryList extends ContextSource {
                } else {
                        if ( $local ) {
                                $row .= Linker::userLink( $userId, $userText );
-                               $row .= $this->msg( 'word-separator' )->escaped();
                                $row .= '<span style="white-space: nowrap;">';
                                $row .= Linker::userToolLinks( $userId, $userText );
                                $row .= '</span>';
index fe61f6f..d30f589 100644 (file)
@@ -3378,35 +3378,70 @@ class WikiPage implements Page, IDBAccessObject {
        }
 
        /**
-        * Opportunistically enqueue link update jobs given fresh parser output if useful
+        * Updates cascading protections
         *
-        * @param ParserOutput $parserOutput Current version page output
-        * @return bool Whether a job was pushed
-        * @since 1.25
+        * @param ParserOutput $parserOutput ParserOutput object for the current version
         */
-       public function triggerOpportunisticLinksUpdate( ParserOutput $parserOutput ) {
-               if ( wfReadOnly() ) {
-                       return false;
+       public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) {
+               if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) {
+                       return;
                }
 
-               if ( $this->mTitle->areRestrictionsCascading() ) {
-                       // If the page is cascade protecting, the links should really be up-to-date
-                       $params = array( 'prioritize' => true );
-               } elseif ( $parserOutput->hasDynamicContent() ) {
-                       // Assume the output contains time/random based magic words
-                       $params = array();
-               } else {
-                       // If the inclusions are deterministic, the edit-triggered link jobs are enough
-                       return false;
+               // templatelinks or imagelinks tables may have become out of sync,
+               // especially if using variable-based transclusions.
+               // For paranoia, check if things have changed and if
+               // so apply updates to the database. This will ensure
+               // that cascaded protections apply as soon as the changes
+               // are visible.
+
+               // Get templates from templatelinks and images from imagelinks
+               $id = $this->getId();
+
+               $dbLinks = array();
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( array( 'templatelinks' ),
+                       array( 'tl_namespace', 'tl_title' ),
+                       array( 'tl_from' => $id ),
+                       __METHOD__
+               );
+
+               foreach ( $res as $row ) {
+                       $dbLinks["{$row->tl_namespace}:{$row->tl_title}"] = true;
                }
 
-               // Check if the last link refresh was before page_touched
-               if ( $this->getLinksTimestamp() < $this->getTouched() ) {
-                       JobQueueGroup::singleton()->push( new RefreshLinksJob( $this->mTitle, $params ) );
-                       return true;
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( array( 'imagelinks' ),
+                       array( 'il_to' ),
+                       array( 'il_from' => $id ),
+                       __METHOD__
+               );
+
+               foreach ( $res as $row ) {
+                       $dbLinks[NS_FILE . ":{$row->il_to}"] = true;
                }
 
-               return false;
+               // Get templates and images from parser output.
+               $poLinks = array();
+               foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
+                       foreach ( $templates as $dbk => $id ) {
+                               $poLinks["$ns:$dbk"] = true;
+                       }
+               }
+               foreach ( $parserOutput->getImages() as $dbk => $id ) {
+                       $poLinks[NS_FILE . ":$dbk"] = true;
+               }
+
+               // Get the diff
+               $links_diff = array_diff_key( $poLinks, $dbLinks );
+
+               if ( count( $links_diff ) > 0 ) {
+                       // Whee, link updates time.
+                       // Note: we are only interested in links here. We don't need to get
+                       // other DataUpdate items from the parser output.
+                       $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
+                       $u->doUpdate();
+               }
        }
 
        /**
index da7842a..e9e72be 100644 (file)
@@ -879,22 +879,6 @@ class ParserOutput extends CacheTime {
                $this->mLimitReportData[$key] = $value;
        }
 
-       /**
-        * Check whether the cache TTL was lowered due to dynamic content
-        *
-        * When content is determined by more than hard state (e.g. page edits),
-        * such as template/file transclusions based on the current timestamp or
-        * extension tags that generate lists based on queries, this return true.
-        *
-        * @return bool
-        * @since 1.25
-        */
-       public function hasDynamicContent() {
-               global $wgParserCacheExpireTime;
-
-               return $this->getCacheExpiry() < $wgParserCacheExpireTime;
-       }
-
        /**
         * Get or set the prevent-clickjacking flag
         *
index 54cbb27..da20f94 100644 (file)
@@ -159,7 +159,7 @@ class PoolWorkArticleView extends PoolCounterWork {
                }
 
                if ( $isCurrent ) {
-                       $this->page->triggerOpportunisticLinksUpdate( $this->parserOutput );
+                       $this->page->doCascadeProtectionUpdates( $this->parserOutput );
                }
 
                return true;
index 4c2c2b2..d689044 100644 (file)
@@ -524,8 +524,8 @@ abstract class ResourceLoaderModule {
                        return $data;
                }
 
-               wfDebugLog( 'resourceloader', __METHOD__ . ": New definition hash for module "
-                       . "{$this->getName()} in context {$context->getHash()}: $hash." );
+               wfDebugLog( 'resourceloader', __METHOD__ . ": New definition for module "
+                       . "{$this->getName()} in context \"{$context->getHash()}\"" );
 
                $timestamp = time();
                $cache->set( $key, $timestamp );
diff --git a/includes/site/SiteExporter.php b/includes/site/SiteExporter.php
new file mode 100644 (file)
index 0000000..62f6ca3
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Utility for exporting site entries to XML.
+ * For the output file format, see docs/sitelist.txt and docs/sitelist-1.0.xsd.
+ *
+ * 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
+ *
+ * @since 1.25
+ *
+ * @file
+ * @ingroup Site
+ *
+ * @license GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+class SiteExporter {
+
+       /**
+        * @var resource
+        */
+       private $sink;
+
+       /**
+        * @param resource $sink A file handle open for writing
+        */
+       public function __construct( $sink ) {
+               if ( !is_resource( $sink ) || get_resource_type( $sink ) !== 'stream' ) {
+                       throw new InvalidArgumentException( '$sink must be a file handle' );
+               }
+
+               $this->sink = $sink;
+       }
+
+       /**
+        * Writes a <site> tag for each Site object in $sites, and encloses the entire list
+        * between <sites> tags.
+        *
+        * @param Site[]|SiteList $sites
+        */
+       public function exportSites( $sites ) {
+               $attributes = array(
+                       'version' => '1.0',
+                       'xmlns' => 'http://www.mediawiki.org/xml/sitelist-1.0/',
+               );
+
+               fwrite( $this->sink, XML::openElement( 'sites', $attributes ) . "\n" );
+
+               foreach ( $sites as $site ) {
+                       $this->exportSite( $site );
+               }
+
+               fwrite( $this->sink, XML::closeElement( 'sites' ) . "\n" );
+               fflush( $this->sink );
+       }
+
+       /**
+        * Writes a <site> tag representing the given Site object.
+        *
+        * @param Site $site
+        */
+       private function exportSite( Site $site ) {
+               if ( $site->getType() !== Site::TYPE_UNKNOWN ) {
+                       $siteAttr = array( 'type' => $site->getType() );
+               } else {
+                       $siteAttr = null;
+               }
+
+               fwrite( $this->sink, "\t" . XML::openElement( 'site', $siteAttr ) . "\n" );
+
+               fwrite( $this->sink, "\t\t" . XML::element( 'globalid', null, $site->getGlobalId() ) . "\n" );
+
+               if ( $site->getGroup() !== Site::GROUP_NONE ) {
+                       fwrite( $this->sink, "\t\t" . XML::element( 'group', null, $site->getGroup() ) . "\n" );
+               }
+
+               if ( $site->getSource() !== Site::SOURCE_LOCAL ) {
+                       fwrite( $this->sink, "\t\t" . XML::element( 'source', null, $site->getSource() ) . "\n" );
+               }
+
+               if ( $site->shouldForward() ) {
+                       fwrite( $this->sink, "\t\t" . XML::element( 'forward', null, '' ) . "\n" );
+               }
+
+               foreach ( $site->getAllPaths() as $type => $path ) {
+                       fwrite( $this->sink, "\t\t" . XML::element( 'path', array( 'type' => $type ), $path ) . "\n" );
+               }
+
+               foreach ( $site->getLocalIds() as $type => $ids ) {
+                       foreach ( $ids as $id ) {
+                               fwrite( $this->sink, "\t\t" . XML::element( 'localid', array( 'type' => $type ), $id ) . "\n" );
+                       }
+               }
+
+               //@todo: export <data>
+               //@todo: export <config>
+
+               fwrite( $this->sink, "\t" . XML::closeElement( 'site' ) . "\n" );
+       }
+
+}
diff --git a/includes/site/SiteImporter.php b/includes/site/SiteImporter.php
new file mode 100644 (file)
index 0000000..02c3ca4
--- /dev/null
@@ -0,0 +1,257 @@
+<?php
+
+/**
+ * Utility for importing site entries from XML.
+ * For the expected format of the input, see docs/sitelist.txt and docs/sitelist-1.0.xsd.
+ *
+ * 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
+ *
+ * @since 1.25
+ *
+ * @file
+ * @ingroup Site
+ *
+ * @license GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+class SiteImporter {
+
+       /**
+        * @var SiteStore
+        */
+       private $store;
+
+       /**
+        * @var callable|null
+        */
+       private $exceptionCallback;
+
+       /**
+        * @param SiteStore $store
+        */
+       public function __construct( SiteStore $store ) {
+               $this->store = $store;
+       }
+
+       /**
+        * @return callable
+        */
+       public function getExceptionCallback() {
+               return $this->exceptionCallback;
+       }
+
+       /**
+        * @param callable $exceptionCallback
+        */
+       public function setExceptionCallback( $exceptionCallback ) {
+               $this->exceptionCallback = $exceptionCallback;
+       }
+
+       /**
+        * @param string $file
+        */
+       public function importFromFile( $file ) {
+               $xml = file_get_contents( $file );
+
+               if ( $xml === false ) {
+                       throw new RuntimeException( 'Failed to read ' . $file . '!' );
+               }
+
+               $this->importFromXML( $xml );
+       }
+
+       /**
+        * @param string $xml
+        *
+        * @throws InvalidArgumentException
+        */
+       public function importFromXML( $xml ) {
+               $document = new DOMDocument();
+
+               $oldLibXmlErrors = libxml_use_internal_errors( true );
+               $ok = $document->loadXML( $xml, LIBXML_NONET );
+
+               if ( !$ok ) {
+                       $errors = libxml_get_errors();
+                       libxml_use_internal_errors( $oldLibXmlErrors );
+
+                       foreach ( $errors as $error ) {
+                               /** @var LibXMLError $error */
+                               throw new InvalidArgumentException( 'Malformed XML: ' . $error->message . ' in line ' . $error->line );
+                       }
+
+                       throw new InvalidArgumentException( 'Malformed XML!' );
+               }
+
+               libxml_use_internal_errors( $oldLibXmlErrors );
+               $this->importFromDOM( $document->documentElement );
+       }
+
+       /**
+        * @param DOMElement $root
+        */
+       private function importFromDOM( DOMElement $root ) {
+               $sites = $this->makeSiteList( $root );
+               $this->store->saveSites( $sites );
+       }
+
+       /**
+        * @param DOMElement $root
+        *
+        * @return Site[]
+        */
+       private function makeSiteList( DOMElement $root ) {
+               $sites = array();
+
+               // Old sites, to get the row IDs that correspond to the global site IDs.
+               // TODO: Get rid of internal row IDs, they just get in the way. Get rid of ORMRow, too.
+               $oldSites = $this->store->getSites();
+
+               $current = $root->firstChild;
+               while ( $current ) {
+                       if ( $current instanceof DOMElement && $current->tagName === 'site' ) {
+                               try {
+                                       $site = $this->makeSite( $current );
+                                       $key = $site->getGlobalId();
+
+                                       if ( $oldSites->hasSite( $key ) ) {
+                                               $oldSite = $oldSites->getSite( $key );
+                                               $site->setInternalId( $oldSite->getInternalId() );
+                                       }
+
+                                       $sites[$key] = $site;
+                               } catch ( Exception $ex ) {
+                                       $this->handleException( $ex );
+                               }
+                       }
+
+                       $current = $current->nextSibling;
+               }
+
+               return $sites;
+       }
+
+       /**
+        * @param DOMElement $siteElement
+        *
+        * @return Site
+        * @throws InvalidArgumentException
+        */
+       public function makeSite( DOMElement $siteElement ) {
+               if ( $siteElement->tagName !== 'site' ) {
+                       throw new InvalidArgumentException( 'Expected <site> tag, found ' . $siteElement->tagName );
+               }
+
+               $type = $this->getAttributeValue( $siteElement, 'type', Site::TYPE_UNKNOWN );
+               $site = Site::newForType( $type );
+
+               $site->setForward( $this->hasChild( $siteElement, 'forward' ) );
+               $site->setGlobalId( $this->getChildText( $siteElement, 'globalid' ) );
+               $site->setGroup( $this->getChildText( $siteElement, 'group', Site::GROUP_NONE ) );
+               $site->setSource( $this->getChildText( $siteElement, 'source', Site::SOURCE_LOCAL ) );
+
+               $pathTags = $siteElement->getElementsByTagName( 'path' );
+               for ( $i = 0; $i < $pathTags->length; $i++ ) {
+                       $pathElement = $pathTags->item( $i );
+                       $pathType = $this->getAttributeValue( $pathElement, 'type' );
+                       $path = $pathElement->textContent;
+
+                       $site->setPath( $pathType, $path );
+               }
+
+               $idTags = $siteElement->getElementsByTagName( 'localid' );
+               for ( $i = 0; $i < $idTags->length; $i++ ) {
+                       $idElement = $idTags->item( $i );
+                       $idType = $this->getAttributeValue( $idElement, 'type' );
+                       $id = $idElement->textContent;
+
+                       $site->addLocalId( $idType, $id );
+               }
+
+               //@todo: import <data>
+               //@todo: import <config>
+
+               return $site;
+       }
+
+       /**
+        * @param DOMElement $element
+        * @param $name
+        * @param string|null|bool $default
+        *
+        * @return null|string
+        * @throws MWException If the attribute is not found and no default is provided
+        */
+       private function getAttributeValue( DOMElement $element, $name, $default = false ) {
+               $node = $element->getAttributeNode( $name );
+
+               if ( !$node ) {
+                       if ( $default !== false ) {
+                               return $default;
+                       } else {
+                               throw new MWException( 'Required ' . $name . ' attribute not found in <' . $element->tagName . '> tag' );
+                       }
+               }
+
+               return $node->textContent;
+       }
+
+       /**
+        * @param DOMElement $element
+        * @param string $name
+        * @param string|null|bool $default
+        *
+        * @return null|string
+        * @throws MWException If the child element is not found and no default is provided
+        */
+       private function getChildText( DOMElement $element, $name, $default = false ) {
+               $elements = $element->getElementsByTagName( $name );
+
+               if ( $elements->length < 1 ) {
+                       if ( $default !== false ) {
+                               return $default;
+                       } else {
+                               throw new MWException( 'Required <' . $name . '> tag not found inside <' . $element->tagName . '> tag' );
+                       }
+               }
+
+               $node = $elements->item( 0 );
+               return $node->textContent;
+       }
+
+       /**
+        * @param DOMElement $element
+        * @param string $name
+        *
+        * @return bool
+        * @throws MWException
+        */
+       private function hasChild( DOMElement $element, $name ) {
+               return $this->getChildText( $element, $name, null ) !== null;
+       }
+
+       /**
+        * @param Exception $ex
+        */
+       private function handleException( Exception $ex ) {
+               if ( $this->exceptionCallback ) {
+                       call_user_func( $this->exceptionCallback, $ex );
+               } else {
+                       wfLogWarning( $ex->getMessage() );
+               }
+       }
+
+}
index 607b4f6..606f837 100644 (file)
@@ -212,7 +212,6 @@ class FileDuplicateSearchPage extends QueryPage {
                if ( $result->isLocal() ) {
                        $userId = $result->getUser( 'id' );
                        $user = Linker::userLink( $userId, $userText );
-                       $user .= $this->getContext()->msg( 'word-separator' )->escaped();
                        $user .= '<span style="white-space: nowrap;">';
                        $user .= Linker::userToolLinks( $userId, $userText );
                        $user .= '</span>';
diff --git a/maintenance/dumpSisterSites.php b/maintenance/dumpSisterSites.php
deleted file mode 100644 (file)
index 784dc7a..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * Quickie page name dump script for SisterSites usage.
- * http://www.eekim.com/cgi-bin/wiki.pl?SisterSites
- *
- * Copyright Â© 2006 Brion Vibber <brion@pobox.com>
- * https://www.mediawiki.org/
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Maintenance
- */
-
-require_once __DIR__ . '/Maintenance.php';
-
-/**
- * Maintenance script that generates a page name dump for SisterSites usage.
- *
- * @ingroup Maintenance
- */
-class DumpSisterSites extends Maintenance {
-       public function __construct() {
-               parent::__construct();
-               $this->mDescription = "Quickie page name dump script for SisterSites usage";
-       }
-
-       public function execute() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $dbr->bufferResults( false );
-               $result = $dbr->select( 'page',
-                       array( 'page_namespace', 'page_title' ),
-                       array(
-                               'page_namespace' => NS_MAIN,
-                               'page_is_redirect' => 0,
-                       ),
-                       __METHOD__ );
-
-               foreach ( $result as $row ) {
-                       $title = Title::makeTitle( $row->page_namespace, $row->page_title );
-                       $url = $title->getFullURL();
-                       $text = $title->getPrefixedText();
-                       $this->output( "$url $text\n" );
-               }
-       }
-}
-
-$maintClass = "DumpSisterSites";
-require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/maintenance/exportSites.php b/maintenance/exportSites.php
new file mode 100644 (file)
index 0000000..1c71dc0
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+$basePath = getenv( 'MW_INSTALL_PATH' ) !== false ? getenv( 'MW_INSTALL_PATH' ) : __DIR__ . '/..';
+
+require_once $basePath . '/maintenance/Maintenance.php';
+
+/**
+ * Maintenance script for exporting site definitions from XML into the sites table.
+ *
+ * @since 1.25
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+class ExportSites extends Maintenance {
+
+       public function __construct() {
+               $this->mDescription = 'Exports site definitions the sites table to XML file';
+
+               $this->addArg( 'file', 'A file to write the XML to (see docs/sitelist.txt). Use "php://stdout" to write to stdout.', true );
+
+               parent::__construct();
+       }
+
+       /**
+        * Do the actual work. All child classes will need to implement this
+        */
+       public function execute() {
+               $file = $this->getArg( 0 );
+
+               if ( $file === 'php://output' || $file === 'php://stdout' ) {
+                       $this->mQuiet = true;
+               }
+
+               $handle = fopen( $file, 'w' );
+
+               if ( !$handle )  {
+                       $this->error( "Failed to open $file for writing.\n", 1 );
+               }
+
+               $exporter = new SiteExporter( $handle );
+
+               $sites = SiteSQLStore::newInstance()->getSites( 'recache' );
+               $exporter->exportSites( $sites );
+
+               fclose( $handle );
+
+               $this->output( "Exported sites to " . realpath( $file ) . ".\n" );
+       }
+
+}
+
+$maintClass = 'ExportSites';
+require_once( RUN_MAINTENANCE_IF_MAIN );
diff --git a/maintenance/importSites.php b/maintenance/importSites.php
new file mode 100644 (file)
index 0000000..7abb8d7
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+$basePath = getenv( 'MW_INSTALL_PATH' ) !== false ? getenv( 'MW_INSTALL_PATH' ) : __DIR__ . '/..';
+
+require_once $basePath . '/maintenance/Maintenance.php';
+
+/**
+ * Maintenance script for importing site definitions from XML into the sites table.
+ *
+ * @since 1.25
+ *
+ * @license GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+class ImportSites extends Maintenance {
+
+       public function __construct() {
+               $this->mDescription = 'Imports site definitions from XML into the sites table.';
+
+               $this->addArg( 'file', 'An XML file containing site definitions (see docs/sitelist.txt). Use "php://stdin" to read from stdin.', true );
+
+               parent::__construct();
+       }
+
+
+       /**
+        * Do the import.
+        */
+       public function execute() {
+               $file = $this->getArg( 0 );
+
+               $importer = new SiteImporter( SiteSQLStore::newInstance() );
+               $importer->setExceptionCallback( array( $this, 'reportException' ) );
+
+               $importer->importFromFile( $file );
+
+               $this->output( "Done.\n" );
+       }
+
+       /**
+        * Outputs a message via the output() method.
+        *
+        * @param Exception $ex
+        */
+       public function reportException( Exception $ex ) {
+               $msg = $ex->getMessage();
+               $this->output( "$msg\n" );
+       }
+}
+
+$maintClass = 'ImportSites';
+require_once( RUN_MAINTENANCE_IF_MAIN );
index bf0e07f..2de3ac0 100644 (file)
@@ -77,7 +77,7 @@ class SeeTag < CommonTag
       doc = $2 ? ': ' + $2 : ''
       return formatter.format("{@link #{name}} #{doc}")
     else
-      JsDuck::Logger.warn(nil, 'Unexpected @see argument: "'+tag+'"', position)
+      JsDuck::Logger.warn(nil, 'Unexpected @see argument: "' + tag + '"', position)
       return tag
     end
   end
@@ -109,7 +109,7 @@ class ContextTag < CommonTag
       name = $1
       return formatter.format("`context` : {@link #{name}}")
     else
-      JsDuck::Logger.warn(nil, 'Unexpected @context argument: "'+tag+'"', position)
+      JsDuck::Logger.warn(nil, 'Unexpected @context argument: "' + tag + '"', position)
       return tag
     end
   end
index 2168899..3750593 100644 (file)
@@ -1371,6 +1371,7 @@ return array(
                ),
                'dependencies' => array(
                        'mediawiki.language',
+                       'mediawiki.confirmCloseWindow',
                ),
        ),
        'mediawiki.special.recentchanges' => array(
index a3eedd6..4bd747b 100644 (file)
@@ -5,15 +5,18 @@ jQuery( function ( $ ) {
        var $preftoc, $preferences, $fieldsets, $legends,
                hash, labelFunc,
                $tzSelect, $tzTextbox, $localtimeHolder, servertime,
-               $checkBoxes, savedWindowOnBeforeUnload;
+               $checkBoxes, allowCloseWindowFn;
 
        labelFunc = function () {
                return this.id.replace( /^mw-prefsection/g, 'preftab' );
        };
 
        $( '#prefsubmit' ).attr( 'id', 'prefcontrol' );
-       $preftoc = $( '<ul id="preftoc"></ul>' )
-               .attr( 'role', 'tablist' );
+       $preftoc = $( '<ul>' )
+               .attr( {
+                       id: 'preftoc',
+                       role: 'tablist'
+               } );
        $preferences = $( '#preferences' )
                .addClass( 'jsprefs' )
                .before( $preftoc );
@@ -223,12 +226,8 @@ jQuery( function ( $ ) {
                localTime = servertime + minuteDiff;
 
                // Bring time within the [0,1440) range.
-               while ( localTime < 0 ) {
-                       localTime += 1440;
-               }
-               while ( localTime >= 1440 ) {
-                       localTime -= 1440;
-               }
+               localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
+
                $localtimeHolder.text( mediaWiki.language.convertNumber( minutesToHours( localTime ) ) );
        }
 
@@ -267,39 +266,14 @@ jQuery( function ( $ ) {
        // Set up a message to notify users if they try to leave the page without
        // saving.
        $( '#mw-prefs-form' ).data( 'origdata', $( '#mw-prefs-form' ).serialize() );
-       $( window )
-               .on( 'beforeunload.prefswarning', function () {
-                       var retval;
-
-                       // Check if anything changed
-                       if ( $( '#mw-prefs-form' ).serialize() !== $( '#mw-prefs-form' ).data( 'origdata' ) ) {
-                               // Return our message
-                               retval = mediaWiki.msg( 'prefswarning-warning', mediaWiki.msg( 'saveprefs' ) );
-                       }
+       allowCloseWindowFn = mediaWiki.confirmCloseWindow( {
+               test: function () {
+                       return $( '#mw-prefs-form' ).serialize() !== $( '#mw-prefs-form' ).data( 'origdata' );
+               },
 
-                       // Unset the onbeforeunload handler so we don't break page caching in Firefox
-                       savedWindowOnBeforeUnload = window.onbeforeunload;
-                       window.onbeforeunload = null;
-                       if ( retval !== undefined ) {
-                               // ...but if the user chooses not to leave the page, we need to rebind it
-                               setTimeout( function () {
-                                       window.onbeforeunload = savedWindowOnBeforeUnload;
-                               }, 1 );
-                               return retval;
-                       }
-               } )
-               .on( 'pageshow.prefswarning', function () {
-                       // Re-add onbeforeunload handler
-                       if ( !window.onbeforeunload ) {
-                               window.onbeforeunload = savedWindowOnBeforeUnload;
-                       }
-               } );
-       $( '#mw-prefs-form' ).submit( function () {
-               // Unbind our beforeunload handler
-               $( window ).off( '.prefswarning' );
-       } );
-       $( '#mw-prefs-restoreprefs' ).click( function () {
-               // Unbind our beforeunload handler
-               $( window ).off( '.prefswarning' );
+               message: mediaWiki.msg( 'prefswarning-warning', mediaWiki.msg( 'saveprefs' ) ),
+               namespace: 'prefswarning'
        } );
+       $( '#mw-prefs-form' ).submit( allowCloseWindowFn );
+       $( '#mw-prefs-restoreprefs' ).click( allowCloseWindowFn );
 } );
index 7fa2984..79507ca 100644 (file)
@@ -10,7 +10,7 @@
 # https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
 #
 Given(/^I go to Create account page at (.+)$/) do |path|
-  visit(CreateAccountPage, :using_params => {:page_title => path})
+  visit(CreateAccountPage, :using_params => { :page_title => path })
 end
 
 Then(/^form has Create account button$/) do
index ba41f7f..5271ea8 100644 (file)
@@ -6,23 +6,21 @@ end
 
 Given(/^I am on the (.+) page$/) do |article|
   article = article.gsub(/ /, '_')
-  visit(ZtargetPage, :using_params => {:article_name => article})
+  visit(ZtargetPage, :using_params => { :article_name => article })
 end
 
 Given(/^I create page "(.*?)" with content "(.*?)"$/) do |page_title, page_content|
   on(APIPage).create page_title, page_content
 end
 
-
 When(/^I click the Link Target link$/) do
   on(ZtargetPage).link_target_page_link
 end
 
 Then(/^I should be on the Link Target Test Page$/) do
-  @browser.url.should match /Link_Target_Test_Page/
+  @browser.url.should match(/Link_Target_Test_Page/)
 end
 
 Then(/^the page content should contain "(.*?)"$/) do |content|
   on(ZtargetPage).page_content.should match content
 end
-
index 5ab02be..713bb39 100644 (file)
@@ -21,4 +21,3 @@ end
 Then(/^the edited page content should contain "(.*?)"$/) do |content|
   on(MainPage).page_content.should match(content + @random_string)
 end
-
index a2ed1bf..6f017c1 100644 (file)
@@ -10,7 +10,7 @@
 # https://git.wikimedia.org/blob/qa%2Fbrowsertests/HEAD/CREDITS
 #
 Given(/^I am at file that does not exist$/) do
-  visit(FileDoesNotExistPage, using_params: {page_name: @random_string})
+  visit(FileDoesNotExistPage, using_params: { page_name: @random_string })
 end
 
 Then(/^page should show that no such file exists$/) do
index 1ecc008..6bb7e83 100644 (file)
@@ -5,4 +5,3 @@ end
 Then(/^I should see a link to a previous version of the page$/) do
   on(ViewHistoryPage).old_version_link_element.should be_visible
 end
-
index b619c34..237441e 100644 (file)
@@ -5,4 +5,4 @@ class EditPage
   button(:preview_button, id: "wpPreview")
   button(:show_changes_button, id: "wpDiff")
   button(:save_button, id: "wpSave")
-end
\ No newline at end of file
+end
index 4fc9ca7..fd000e6 100644 (file)
@@ -2,4 +2,4 @@ class LoginErrorPage
   include PageObject
 
   div(:error_box, class: "errorbox")
-end
\ No newline at end of file
+end
index 7d96c2b..8156800 100644 (file)
@@ -16,4 +16,4 @@ class MainPage
   li(:special_pages_link, id: "t-specialpages")
   a(:view_history_link, href: /action=history/)
   li(:what_links_here_link, id: "t-whatlinkshere")
-end
\ No newline at end of file
+end
index c24e386..ed4491e 100644 (file)
@@ -38,4 +38,3 @@ class PreferencesAppearancePage
   radio_button(:vector, id: "mw-input-wpskin-vector")
   radio_button(:year_mo_day_radio, id: "mw-input-wpdate-ymd")
 end
-
index 6689598..bb9c586 100644 (file)
@@ -3,5 +3,4 @@ class ViewHistoryPage
 
   a(:view_history_link, href: /action=history/)
   a(:old_version_link, href: /oldid=/)
-
-end
\ No newline at end of file
+end
index c1f46ec..7f168db 100644 (file)
@@ -4,4 +4,4 @@ class ZtargetPage < MainPage
   include PageObject
 
   a(:link_target_page_link, text: "link to the test target page")
-end
\ No newline at end of file
+end
diff --git a/tests/phpunit/includes/site/SiteExporterTest.php b/tests/phpunit/includes/site/SiteExporterTest.php
new file mode 100644 (file)
index 0000000..a3ef4be
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * Tests for the SiteExporter class.
+ *
+ * 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 Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteExporter
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+class SiteExporterTest extends PHPUnit_Framework_TestCase {
+
+       public function testConstructor_InvalidArgument() {
+               $this->setExpectedException( 'InvalidArgumentException' );
+
+               new SiteExporter( 'Foo' );
+       }
+
+       public function testExportSites() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( array( $foo, $acme ) );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16*1024 );
+
+               $this->assertContains( '<sites ', $xml );
+               $this->assertContains( '<site>', $xml );
+               $this->assertContains( '<globalid>Foo</globalid>', $xml );
+               $this->assertContains( '</site>', $xml );
+               $this->assertContains( '<globalid>acme.com</globalid>', $xml );
+               $this->assertContains( '<group>Test</group>', $xml );
+               $this->assertContains( '<localid type="interwiki">acme</localid>', $xml );
+               $this->assertContains( '<path type="link">http://acme.com/</path>', $xml );
+               $this->assertContains( '</sites>', $xml );
+
+               // NOTE: HHVM (at least on wmf Jenkins) doesn't like file URLs.
+               $xsdFile = __DIR__ . '/../../../../docs/sitelist-1.0.xsd';
+               $xsdData = file_get_contents( $xsdFile );
+
+               $document = new DOMDocument();
+               $document->loadXML( $xml, LIBXML_NONET );
+               $document->schemaValidateSource( $xsdData );
+       }
+
+       private function newSiteStore( SiteList $sites ) {
+               $store = $this->getMock( 'SiteStore' );
+
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $moreSites ) use ( $sites ) {
+                               foreach ( $moreSites as $site ) {
+                                       $sites->setSite( $site );
+                               }
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               return $store;
+       }
+
+       public function provideRoundTrip() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return array(
+                       'empty' => array(
+                               new SiteList()
+                       ),
+
+                       'some' => array(
+                               new SiteList( array( $foo, $acme, $dewiki ) ),
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideRoundTrip()
+        */
+       public function testRoundTrip( SiteList $sites ) {
+               $tmp = tmpfile();
+               $exporter = new SiteExporter( $tmp );
+
+               $exporter->exportSites( $sites );
+
+               fseek( $tmp, 0 );
+               $xml = fread( $tmp, 16*1024 );
+
+               $actualSites = new SiteList();
+               $store = $this->newSiteStore( $actualSites );
+
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( $xml );
+
+               $this->assertEquals( $sites, $actualSites );
+       }
+
+}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.php b/tests/phpunit/includes/site/SiteImporterTest.php
new file mode 100644 (file)
index 0000000..ceef1bf
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * Tests for the SiteImporter class.
+ *
+ * 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 Site
+ * @ingroup Test
+ *
+ * @group Site
+ *
+ * @covers SiteImporter
+ *
+ * @licence GNU GPL v2+
+ * @author Daniel Kinzler
+ */
+class SiteImporterTest extends PHPUnit_Framework_TestCase {
+
+       private function newSiteImporter( array $expectedSites, $errorCount ) {
+               $store = $this->getMock( 'SiteStore' );
+
+               $self = $this;
+               $store->expects( $this->once() )
+                       ->method( 'saveSites' )
+                       ->will( $this->returnCallback( function ( $sites ) use ( $expectedSites, $self ) {
+                               $self->assertSitesEqual( $expectedSites, $sites );
+                       } ) );
+
+               $store->expects( $this->any() )
+                       ->method( 'getSites' )
+                       ->will( $this->returnValue( new SiteList() ) );
+
+               $errorHandler = $this->getMock( 'Psr\Log\LoggerInterface' );
+               $errorHandler->expects( $this->exactly( $errorCount ) )
+                       ->method( 'error' );
+
+               $importer = new SiteImporter( $store );
+               $importer->setExceptionCallback( array( $errorHandler, 'error' ) );
+
+               return $importer;
+       }
+
+       public function assertSitesEqual( $expected, $actual, $message = '' ) {
+               $this->assertEquals(
+                       $this->getSerializedSiteList( $expected ),
+                       $this->getSerializedSiteList( $actual ),
+                       $message
+               );
+       }
+
+       public function provideImportFromXML() {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               return array(
+                       'empty' => array(
+                               '<sites></sites>',
+                               array(),
+                       ),
+                       'no sites' => array(
+                               '<sites><Foo><globalid>Foo</globalid></Foo><Bar><quux>Bla</quux></Bar></sites>',
+                               array(),
+                       ),
+                       'minimal' => array(
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                               '</sites>',
+                               array( $foo ),
+                       ),
+                       'full' => array(
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                                       '<site type="mediawiki">' .
+                                               '<source>meta.wikimedia.org</source>' .
+                                               '<globalid>dewiki</globalid>' .
+                                               '<localid type="interwiki">wikipedia</localid>' .
+                                               '<localid type="equivalent">de</localid>' .
+                                               '<group>wikipedia</group>' .
+                                               '<forward/>' .
+                                               '<path type="link">http://de.wikipedia.org/w/</path>' .
+                                               '<path type="page_path">http://de.wikipedia.org/wiki/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               array( $foo, $acme, $dewiki ),
+                       ),
+                       'skip' => array(
+                               '<sites>' .
+                                       '<site><globalid>Foo</globalid></site>' .
+                                       '<site><barf>Foo</barf></site>' .
+                                       '<site>' .
+                                               '<globalid>acme.com</globalid>' .
+                                               '<localid type="interwiki">acme</localid>' .
+                                               '<silly>boop!</silly>' .
+                                               '<group>Test</group>' .
+                                               '<path type="link">http://acme.com/</path>' .
+                                       '</site>' .
+                               '</sites>',
+                               array( $foo, $acme ),
+                               1
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideImportFromXML
+        */
+       public function testImportFromXML( $xml, array $expectedSites, $errorCount = 0 )  {
+               $importer = $this->newSiteImporter( $expectedSites, $errorCount );
+               $importer->importFromXML( $xml );
+       }
+
+       public function testImportFromXML_malformed()  {
+               $this->setExpectedException( 'Exception' );
+
+               $store = $this->getMock( 'SiteStore' );
+               $importer = new SiteImporter( $store );
+               $importer->importFromXML( 'THIS IS NOT XML' );
+       }
+
+       public function testImportFromFile()  {
+               $foo = Site::newForType( Site::TYPE_UNKNOWN );
+               $foo->setGlobalId( 'Foo' );
+
+               $acme = Site::newForType( Site::TYPE_UNKNOWN );
+               $acme->setGlobalId( 'acme.com' );
+               $acme->setGroup( 'Test' );
+               $acme->addLocalId( Site::ID_INTERWIKI, 'acme' );
+               $acme->setPath( Site::PATH_LINK, 'http://acme.com/' );
+
+               $dewiki = Site::newForType( Site::TYPE_MEDIAWIKI );
+               $dewiki->setGlobalId( 'dewiki' );
+               $dewiki->setGroup( 'wikipedia' );
+               $dewiki->setForward( true );
+               $dewiki->addLocalId( Site::ID_INTERWIKI, 'wikipedia' );
+               $dewiki->addLocalId( Site::ID_EQUIVALENT, 'de' );
+               $dewiki->setPath( Site::PATH_LINK, 'http://de.wikipedia.org/w/' );
+               $dewiki->setPath( MediaWikiSite::PATH_PAGE, 'http://de.wikipedia.org/wiki/' );
+               $dewiki->setSource( 'meta.wikimedia.org' );
+
+               $importer = $this->newSiteImporter( array( $foo, $acme, $dewiki ), 0 );
+
+               $file = __DIR__ . '/SiteImporterTest.xml';
+               $importer->importFromFile( $file );
+       }
+
+       /**
+        * @param Site[] $sites
+        *
+        * @return array[]
+        */
+       private function getSerializedSiteList( $sites ) {
+               $serialized = array();
+
+               foreach ( $sites as $site ) {
+                       $key = $site->getGlobalId();
+                       $data = unserialize( $site->serialize() );
+
+                       $serialized[$key] = $data;
+               }
+
+               return $serialized;
+       }
+}
diff --git a/tests/phpunit/includes/site/SiteImporterTest.xml b/tests/phpunit/includes/site/SiteImporterTest.xml
new file mode 100644 (file)
index 0000000..720b1fa
--- /dev/null
@@ -0,0 +1,19 @@
+<sites version="1.0" xmlns="http://www.mediawiki.org/xml/sitelist-1.0/">
+       <site><globalid>Foo</globalid></site>
+       <site>
+               <globalid>acme.com</globalid>
+               <localid type="interwiki">acme</localid>
+               <group>Test</group>
+               <path type="link">http://acme.com/</path>
+       </site>
+       <site type="mediawiki">
+               <source>meta.wikimedia.org</source>
+               <globalid>dewiki</globalid>
+               <localid type="interwiki">wikipedia</localid>
+               <localid type="equivalent">de</localid>
+               <group>wikipedia</group>
+               <forward/>
+               <path type="link">http://de.wikipedia.org/w/</path>
+               <path type="page_path">http://de.wikipedia.org/wiki/</path>
+       </site>
+</sites>