- 'skins/**/*'
- 'tests/frontend/node_modules/**/*'
- 'vendor/**/*'
+
+Metrics/LineLength:
+ Max: 100
# 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:
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
-#ruby=ruby-2.1.2
-#ruby-gemset=core
+# ruby=ruby-2.1.2
+# ruby-gemset=core
source "https://rubygems.org"
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
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 ==
'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',
'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',
'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
--- /dev/null
+<?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>
--- /dev/null
+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
// 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
$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 ) );
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 ) );
)
);
+ Hooks::run( 'CategoryViewer::doCategoryQuery', array( $type, $res ) );
+
$count = 0;
foreach ( $res as $row ) {
$title = Title::newFromRow( $row );
'PublishStashedFile' => 'PublishStashedFileJob',
'ThumbnailRender' => 'ThumbnailRenderJob',
'recentChangesUpdate' => 'RecentChangesUpdateJob',
- 'refreshLinksPrioritized' => 'RefreshLinksJob', // for cascading protection
'null' => 'NullJob'
);
$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();
*/
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() ) {
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__
- ) );
- }
}
}
public function doUpdate() {
+
$job = new HTMLCacheUpdateJob(
$this->mTitle,
array(
$job->run(); // just do the purge query now
} );
}
+
}
}
* 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 );
}
/**
"refreshlinks:{$table}:{$title->getPrefixedText()}"
)
);
-
JobQueueGroup::singleton()->push( $job );
JobQueueGroup::singleton()->deduplicateRootJob( $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)
return true;
}
- /**
- * @param Title $title
- * @return bool
- */
protected function runForTitle( Title $title = null ) {
$linkCache = LinkCache::singleton();
$linkCache->clear();
}
# 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;
} 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>';
}
/**
- * 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();
+ }
}
/**
$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
*
}
if ( $isCurrent ) {
- $this->page->triggerOpportunisticLinksUpdate( $this->parserOutput );
+ $this->page->doCascadeProtectionUpdates( $this->parserOutput );
}
return true;
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 );
--- /dev/null
+<?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" );
+ }
+
+}
--- /dev/null
+<?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() );
+ }
+ }
+
+}
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>';
+++ /dev/null
-<?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;
--- /dev/null
+<?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 );
--- /dev/null
+<?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 );
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
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
),
'dependencies' => array(
'mediawiki.language',
+ 'mediawiki.confirmCloseWindow',
),
),
'mediawiki.special.recentchanges' => array(
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 );
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 ) ) );
}
// 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 );
} );
# 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
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
-
Then(/^the edited page content should contain "(.*?)"$/) do |content|
on(MainPage).page_content.should match(content + @random_string)
end
-
# 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
page.auto_number_check_element.should exist
end
end
-
-
Then(/^I can see my signature$/) do
on(PreferencesUserProfilePage).signature_table_element.should exist
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
-
button(:preview_button, id: "wpPreview")
button(:show_changes_button, id: "wpDiff")
button(:save_button, id: "wpSave")
-end
\ No newline at end of file
+end
include PageObject
div(:error_box, class: "errorbox")
-end
\ No newline at end of file
+end
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
radio_button(:vector, id: "mw-input-wpskin-vector")
radio_button(:year_mo_day_radio, id: "mw-input-wpdate-ymd")
end
-
a(:view_history_link, href: /action=history/)
a(:old_version_link, href: /oldid=/)
-
-end
\ No newline at end of file
+end
include PageObject
a(:link_target_page_link, text: "link to the test target page")
-end
\ No newline at end of file
+end
--- /dev/null
+<?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 );
+ }
+
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<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>