From 08aa4418c30cfc18ccc69a0f0f9cb9e17be6c196 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Mon, 12 Aug 2013 09:28:15 +0200 Subject: Update to MediaWiki 1.21.1 --- includes/site/MediaWikiSite.php | 352 ++++++++++++++++++++ includes/site/Site.php | 702 ++++++++++++++++++++++++++++++++++++++++ includes/site/SiteList.php | 300 +++++++++++++++++ includes/site/SiteSQLStore.php | 491 ++++++++++++++++++++++++++++ includes/site/SiteStore.php | 85 +++++ 5 files changed, 1930 insertions(+) create mode 100644 includes/site/MediaWikiSite.php create mode 100644 includes/site/Site.php create mode 100644 includes/site/SiteList.php create mode 100644 includes/site/SiteSQLStore.php create mode 100644 includes/site/SiteStore.php (limited to 'includes/site') diff --git a/includes/site/MediaWikiSite.php b/includes/site/MediaWikiSite.php new file mode 100644 index 00000000..05092723 --- /dev/null +++ b/includes/site/MediaWikiSite.php @@ -0,0 +1,352 @@ + + * @author Daniel Kinzler + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +/** + * Class representing a MediaWiki site. + * + * @since 1.21 + * + * @ingroup Site + */ +class MediaWikiSite extends Site { + + const PATH_FILE = 'file_path'; + const PATH_PAGE = 'page_path'; + + /** + * @since 1.21 + * @deprecated Just use the constructor or the factory Site::newForType + * + * @param integer $globalId + * + * @return MediaWikiSite + */ + public static function newFromGlobalId( $globalId ) { + $site = new static(); + $site->setGlobalId( $globalId ); + return $site; + } + + /** + * Constructor. + * + * @since 1.21 + * + * @param string $type + */ + public function __construct( $type = self::TYPE_MEDIAWIKI ) { + parent::__construct( $type ); + } + + /** + * Returns the database form of the given title. + * + * @since 1.21 + * + * @param string $title the target page's title, in normalized form. + * + * @return String + */ + public function toDBKey( $title ) { + return str_replace( ' ', '_', $title ); + } + + /** + * Returns the normalized form of the given page title, using the normalization rules of the given site. + * If the given title is a redirect, the redirect weill be resolved and the redirect target is returned. + * + * @note : This actually makes an API request to the remote site, so beware that this function is slow and depends + * on an external service. + * + * @note : If MW_PHPUNIT_TEST is defined, the call to the external site is skipped, and the title + * is normalized using the local normalization rules as implemented by the Title class. + * + * @see Site::normalizePageName + * + * @since 1.21 + * + * @param string $pageName + * + * @return string + * @throws MWException + */ + public function normalizePageName( $pageName ) { + + // Check if we have strings as arguments. + if ( !is_string( $pageName ) ) { + throw new MWException( '$pageName must be a string' ); + } + + // Go on call the external site + if ( defined( 'MW_PHPUNIT_TEST' ) ) { + // If the code is under test, don't call out to other sites, just normalize locally. + // Note: this may cause results to be inconsistent with the actual normalization used by the respective remote site! + + $t = Title::newFromText( $pageName ); + return $t->getPrefixedText(); + } else { + + // Make sure the string is normalized into NFC (due to the bug 40017) + // but do nothing to the whitespaces, that should work appropriately. + // @see https://bugzilla.wikimedia.org/show_bug.cgi?id=40017 + $pageName = UtfNormal::cleanUp( $pageName ); + + // Build the args for the specific call + $args = array( + 'action' => 'query', + 'prop' => 'info', + 'redirects' => true, + 'converttitles' => true, + 'format' => 'json', + 'titles' => $pageName, + //@todo: options for maxlag and maxage + // Note that maxlag will lead to a long delay before a reply is made, + // but that maxage can avoid the extreme delay. On the other hand + // maxage could be nice to use anyhow as it stops unnecessary requests. + // Also consider smaxage if maxage is used. + ); + + $url = $this->getFileUrl( 'api.php' ) . '?' . wfArrayToCgi( $args ); + + // Go on call the external site + //@todo: we need a good way to specify a timeout here. + $ret = Http::get( $url ); + } + + if ( $ret === false ) { + wfDebugLog( "MediaWikiSite", "call to external site failed: $url" ); + return false; + } + + $data = FormatJson::decode( $ret, true ); + + if ( !is_array( $data ) ) { + wfDebugLog( "MediaWikiSite", "call to <$url> returned bad json: " . $ret ); + return false; + } + + $page = static::extractPageRecord( $data, $pageName ); + + if ( isset( $page['missing'] ) ) { + wfDebugLog( "MediaWikiSite", "call to <$url> returned a marker for a missing page title! " . $ret ); + return false; + } + + if ( isset( $page['invalid'] ) ) { + wfDebugLog( "MediaWikiSite", "call to <$url> returned a marker for an invalid page title! " . $ret ); + return false; + } + + if ( !isset( $page['title'] ) ) { + wfDebugLog( "MediaWikiSite", "call to <$url> did not return a page title! " . $ret ); + return false; + } + + return $page['title']; + } + + /** + * Get normalization record for a given page title from an API response. + * + * @since 1.21 + * + * @param array $externalData A reply from the API on a external server. + * @param string $pageTitle Identifies the page at the external site, needing normalization. + * + * @return array|boolean a 'page' structure representing the page identified by $pageTitle. + */ + private static function extractPageRecord( $externalData, $pageTitle ) { + // If there is a special case with only one returned page + // we can cheat, and only return + // the single page in the "pages" substructure. + if ( isset( $externalData['query']['pages'] ) ) { + $pages = array_values( $externalData['query']['pages'] ); + if ( count( $pages) === 1 ) { + return $pages[0]; + } + } + // This is only used during internal testing, as it is assumed + // a more optimal (and lossfree) storage. + // Make initial checks and return if prerequisites are not meet. + if ( !is_array( $externalData ) || !isset( $externalData['query'] ) ) { + return false; + } + // Loop over the tree different named structures, that otherwise are similar + $structs = array( + 'normalized' => 'from', + 'converted' => 'from', + 'redirects' => 'from', + 'pages' => 'title' + ); + foreach ( $structs as $listId => $fieldId ) { + // Check if the substructure exist at all. + if ( !isset( $externalData['query'][$listId] ) ) { + continue; + } + // Filter the substructure down to what we actually are using. + $collectedHits = array_filter( + array_values( $externalData['query'][$listId] ), + function( $a ) use ( $fieldId, $pageTitle ) { + return $a[$fieldId] === $pageTitle; + } + ); + // If still looping over normalization, conversion or redirects, + // then we need to keep the new page title for later rounds. + if ( $fieldId === 'from' && is_array( $collectedHits ) ) { + switch ( count( $collectedHits ) ) { + case 0: + break; + case 1: + $pageTitle = $collectedHits[0]['to']; + break; + default: + return false; + } + } + // If on the pages structure we should prepare for returning. + elseif ( $fieldId === 'title' && is_array( $collectedHits ) ) { + switch ( count( $collectedHits ) ) { + case 0: + return false; + case 1: + return array_shift( $collectedHits ); + default: + return false; + } + } + } + // should never be here + return false; + } + + /** + * @see Site::getLinkPathType + * Returns Site::PATH_PAGE + * + * @since 1.21 + * + * @return string + */ + public function getLinkPathType() { + return self::PATH_PAGE; + } + + /** + * Returns the relative page path. + * + * @since 1.21 + * + * @return string + */ + public function getRelativePagePath() { + return parse_url( $this->getPath( self::PATH_PAGE ), PHP_URL_PATH ); + } + + /** + * Returns the relative file path. + * + * @since 1.21 + * + * @return string + */ + public function getRelativeFilePath() { + return parse_url( $this->getPath( self::PATH_FILE ), PHP_URL_PATH ); + } + + /** + * Sets the relative page path. + * + * @since 1.21 + * + * @param string $path + */ + public function setPagePath( $path ) { + $this->setPath( self::PATH_PAGE, $path ); + } + + /** + * Sets the relative file path. + * + * @since 1.21 + * + * @param string $path + */ + public function setFilePath( $path ) { + $this->setPath( self::PATH_FILE, $path ); + } + + /** + * @see Site::getPageUrl + * + * This implementation returns a URL constructed using the path returned by getLinkPath(). + * In addition to the default behavior implemented by Site::getPageUrl(), this + * method converts the $pageName to DBKey-format by replacing spaces with underscores + * before using it in the URL. + * + * @since 1.21 + * + * @param string|boolean $pageName Page name or false (default: false) + * + * @return string + */ + public function getPageUrl( $pageName = false ) { + $url = $this->getLinkPath(); + + if ( $url === false ) { + return false; + } + + if ( $pageName !== false ) { + $pageName = $this->toDBKey( trim( $pageName ) ); + $url = str_replace( '$1', wfUrlencode( $pageName ), $url ); + } + + return $url; + } + + /** + * Returns the full file path (ie site url + relative file path). + * The path should go at the $1 marker. If the $path + * argument is provided, the marker will be replaced by it's value. + * + * @since 1.21 + * + * @param string|boolean $path + * + * @return string + */ + public function getFileUrl( $path = false ) { + $filePath = $this->getPath( self::PATH_FILE ); + + if ( $filePath !== false ) { + $filePath = str_replace( '$1', $path, $filePath ); + } + + return $filePath; + } + +} diff --git a/includes/site/Site.php b/includes/site/Site.php new file mode 100644 index 00000000..076dc88c --- /dev/null +++ b/includes/site/Site.php @@ -0,0 +1,702 @@ + + */ +class Site implements Serializable { + + const TYPE_UNKNOWN = 'unknown'; + const TYPE_MEDIAWIKI = 'mediawiki'; + + const GROUP_NONE = 'none'; + + const ID_INTERWIKI = 'interwiki'; + const ID_EQUIVALENT = 'equivalent'; + + const SOURCE_LOCAL = 'local'; + + const PATH_LINK = 'link'; + + /** + * A version ID that identifies the serialization structure used by getSerializationData() + * and unserialize(). This is useful for constructing cache keys in cases where the cache relies + * on serialization for storing the SiteList. + * + * @var string A string uniquely identifying the version of the serialization structure. + */ + const SERIAL_VERSION_ID = '2013-01-23'; + + /** + * @since 1.21 + * + * @var string|null + */ + protected $globalId = null; + + /** + * @since 1.21 + * + * @var string + */ + protected $type = self::TYPE_UNKNOWN; + + /** + * @since 1.21 + * + * @var string + */ + protected $group = self::GROUP_NONE; + + /** + * @since 1.21 + * + * @var string + */ + protected $source = self::SOURCE_LOCAL; + + /** + * @since 1.21 + * + * @var string|null + */ + protected $languageCode = null; + + /** + * Holds the local ids for this site. + * local id type => [ ids for this type (strings) ] + * + * @since 1.21 + * + * @var array[] + */ + protected $localIds = array(); + + /** + * @since 1.21 + * + * @var array + */ + protected $extraData = array(); + + /** + * @since 1.21 + * + * @var array + */ + protected $extraConfig = array(); + + /** + * @since 1.21 + * + * @var bool + */ + protected $forward = false; + + /** + * @since 1.21 + * + * @var int|null + */ + protected $internalId = null; + + /** + * Constructor. + * + * @since 1.21 + * + * @param string $type + */ + public function __construct( $type = self::TYPE_UNKNOWN ) { + $this->type = $type; + } + + /** + * Returns the global site identifier (ie enwiktionary). + * + * @since 1.21 + * + * @return string|null + */ + public function getGlobalId() { + return $this->globalId; + } + + /** + * Sets the global site identifier (ie enwiktionary). + * + * @since 1.21 + * + * @param string|null $globalId + * + * @throws MWException + */ + public function setGlobalId( $globalId ) { + if ( $globalId !== null && !is_string( $globalId ) ) { + throw new MWException( '$globalId needs to be string or null' ); + } + + $this->globalId = $globalId; + } + + /** + * Returns the type of the site (ie mediawiki). + * + * @since 1.21 + * + * @return string + */ + public function getType() { + return $this->type; + } + + /** + * Gets the type of the site (ie wikipedia). + * + * @since 1.21 + * + * @return string + */ + public function getGroup() { + return $this->group; + } + + /** + * Sets the type of the site (ie wikipedia). + * + * @since 1.21 + * + * @param string $group + * + * @throws MWException + */ + public function setGroup( $group ) { + if ( !is_string( $group ) ) { + throw new MWException( '$group needs to be a string' ); + } + + $this->group = $group; + } + + /** + * Returns the source of the site data (ie 'local', 'wikidata', 'my-magical-repo'). + * + * @since 1.21 + * + * @return string + */ + public function getSource() { + return $this->source; + } + + /** + * Sets the source of the site data (ie 'local', 'wikidata', 'my-magical-repo'). + * + * @since 1.21 + * + * @param string $source + * + * @throws MWException + */ + public function setSource( $source ) { + if ( !is_string( $source ) ) { + throw new MWException( '$source needs to be a string' ); + } + + $this->source = $source; + } + + /** + * Gets if site.tld/path/key:pageTitle should forward users to the page on + * the actual site, where "key" is the local identifier. + * + * @since 1.21 + * + * @return boolean + */ + public function shouldForward() { + return $this->forward; + } + + /** + * Sets if site.tld/path/key:pageTitle should forward users to the page on + * the actual site, where "key" is the local identifier. + * + * @since 1.21 + * + * @param boolean $shouldForward + * + * @throws MWException + */ + public function setForward( $shouldForward ) { + if ( !is_bool( $shouldForward ) ) { + throw new MWException( '$shouldForward needs to be a boolean' ); + } + + $this->forward = $shouldForward; + } + + /** + * Returns the domain of the site, ie en.wikipedia.org + * Or false if it's not known. + * + * @since 1.21 + * + * @return string|null + */ + public function getDomain() { + $path = $this->getLinkPath(); + + if ( $path === null ) { + return null; + } + + return parse_url( $path, PHP_URL_HOST ); + } + + /** + * Returns the protocol of the site. + * + * @since 1.21 + * + * @throws MWException + * @return string + */ + public function getProtocol() { + $path = $this->getLinkPath(); + + if ( $path === null ) { + return ''; + } + + $protocol = parse_url( $path, PHP_URL_SCHEME ); + + // Malformed URL + if ( $protocol === false ) { + throw new MWException( "failed to parse URL '$path'" ); + } + + // No schema + if ( $protocol === null ) { + // Used for protocol relative URLs + $protocol = ''; + } + + return $protocol; + } + + /** + * Sets the path used to construct links with. + * Shall be equivalent to setPath( getLinkPathType(), $fullUrl ). + * + * @param string $fullUrl + * + * @since 1.21 + * + * @throws MWException + */ + public function setLinkPath( $fullUrl ) { + $type = $this->getLinkPathType(); + + if ( $type === null ) { + throw new MWException( "This Site does not support link paths." ); + } + + $this->setPath( $type, $fullUrl ); + } + + /** + * Returns the path used to construct links with or false if there is no such path. + * + * Shall be equivalent to getPath( getLinkPathType() ). + * + * @return string|null + */ + public function getLinkPath() { + $type = $this->getLinkPathType(); + return $type === null ? null: $this->getPath( $type ); + } + + /** + * Returns the main path type, that is the type of the path that should generally be used to construct links + * to the target site. + * + * This default implementation returns Site::PATH_LINK as the default path type. Subclasses can override this + * to define a different default path type, or return false to disable site links. + * + * @since 1.21 + * + * @return string|null + */ + public function getLinkPathType() { + return self::PATH_LINK; + } + + /** + * Returns the full URL for the given page on the site. + * Or false if the needed information is not known. + * + * This generated URL is usually based upon the path returned by getLinkPath(), + * but this is not a requirement. + * + * This implementation returns a URL constructed using the path returned by getLinkPath(). + * + * @since 1.21 + * + * @param bool|String $pageName + * + * @return string|boolean false + */ + public function getPageUrl( $pageName = false ) { + $url = $this->getLinkPath(); + + if ( $url === false ) { + return false; + } + + if ( $pageName !== false ) { + $url = str_replace( '$1', rawurlencode( $pageName ), $url ); + } + + return $url; + } + + /** + * Returns $pageName without changes. + * Subclasses may override this to apply some kind of normalization. + * + * @see Site::normalizePageName + * + * @since 1.21 + * + * @param string $pageName + * + * @return string + */ + public function normalizePageName( $pageName ) { + return $pageName; + } + + /** + * Returns the type specific fields. + * + * @since 1.21 + * + * @return array + */ + public function getExtraData() { + return $this->extraData; + } + + /** + * Sets the type specific fields. + * + * @since 1.21 + * + * @param array $extraData + */ + public function setExtraData( array $extraData ) { + $this->extraData = $extraData; + } + + /** + * Returns the type specific config. + * + * @since 1.21 + * + * @return array + */ + public function getExtraConfig() { + return $this->extraConfig; + } + + /** + * Sets the type specific config. + * + * @since 1.21 + * + * @param array $extraConfig + */ + public function setExtraConfig( array $extraConfig ) { + $this->extraConfig = $extraConfig; + } + + /** + * Returns language code of the sites primary language. + * Or null if it's not known. + * + * @since 1.21 + * + * @return string|null + */ + public function getLanguageCode() { + return $this->languageCode; + } + + /** + * Sets language code of the sites primary language. + * + * @since 1.21 + * + * @param string $languageCode + */ + public function setLanguageCode( $languageCode ) { + $this->languageCode = $languageCode; + } + + /** + * Returns the set internal identifier for the site. + * + * @since 1.21 + * + * @return string|null + */ + public function getInternalId() { + return $this->internalId; + } + + /** + * Sets the internal identifier for the site. + * This typically is a primary key in a db table. + * + * @since 1.21 + * + * @param int|null $internalId + */ + public function setInternalId( $internalId = null ) { + $this->internalId = $internalId; + } + + /** + * Adds a local identifier. + * + * @since 1.21 + * + * @param string $type + * @param string $identifier + */ + public function addLocalId( $type, $identifier ) { + if ( $this->localIds === false ) { + $this->localIds = array(); + } + + if ( !array_key_exists( $type, $this->localIds ) ) { + $this->localIds[$type] = array(); + } + + if ( !in_array( $identifier, $this->localIds[$type] ) ) { + $this->localIds[$type][] = $identifier; + } + } + + /** + * Adds an interwiki id to the site. + * + * @since 1.21 + * + * @param string $identifier + */ + public function addInterwikiId( $identifier ) { + $this->addLocalId( self::ID_INTERWIKI, $identifier ); + } + + /** + * Adds a navigation id to the site. + * + * @since 1.21 + * + * @param string $identifier + */ + public function addNavigationId( $identifier ) { + $this->addLocalId( self::ID_EQUIVALENT, $identifier ); + } + + /** + * Returns the interwiki link identifiers that can be used for this site. + * + * @since 1.21 + * + * @return string[] + */ + public function getInterwikiIds() { + return array_key_exists( self::ID_INTERWIKI, $this->localIds ) ? $this->localIds[self::ID_INTERWIKI] : array(); + } + + /** + * Returns the equivalent link identifiers that can be used to make + * the site show up in interfaces such as the "language links" section. + * + * @since 1.21 + * + * @return string[] + */ + public function getNavigationIds() { + return array_key_exists( self::ID_EQUIVALENT, $this->localIds ) ? $this->localIds[self::ID_EQUIVALENT] : array(); + } + + /** + * Returns all local ids + * + * @since 1.21 + * + * @return array[] + */ + public function getLocalIds() { + return $this->localIds; + } + + /** + * Sets the path used to construct links with. + * Shall be equivalent to setPath( getLinkPathType(), $fullUrl ). + * + * @since 1.21 + * + * @param string $pathType + * @param string $fullUrl + * + * @throws MWException + */ + public function setPath( $pathType, $fullUrl ) { + if ( !is_string( $fullUrl ) ) { + throw new MWException( '$fullUrl needs to be a string' ); + } + + if ( !array_key_exists( 'paths', $this->extraData ) ) { + $this->extraData['paths'] = array(); + } + + $this->extraData['paths'][$pathType] = $fullUrl; + } + + /** + * Returns the path of the provided type or false if there is no such path. + * + * @since 1.21 + * + * @param string $pathType + * + * @return string|null + */ + public function getPath( $pathType ) { + $paths = $this->getAllPaths(); + return array_key_exists( $pathType, $paths ) ? $paths[$pathType] : null; + } + + /** + * Returns the paths as associative array. + * The keys are path types, the values are the path urls. + * + * @since 1.21 + * + * @return string[] + */ + public function getAllPaths() { + return array_key_exists( 'paths', $this->extraData ) ? $this->extraData['paths'] : array(); + } + + /** + * Removes the path of the provided type if it's set. + * + * @since 1.21 + * + * @param string $pathType + */ + public function removePath( $pathType ) { + if ( array_key_exists( 'paths', $this->extraData ) ) { + unset( $this->extraData['paths'][$pathType] ); + } + } + + /** + * @since 1.21 + * + * @param string $siteType + * + * @return Site + */ + public static function newForType( $siteType ) { + global $wgSiteTypes; + + if ( array_key_exists( $siteType, $wgSiteTypes ) ) { + return new $wgSiteTypes[$siteType](); + } + + return new Site(); + } + + /** + * @see Serializable::serialize + * + * @since 1.21 + * + * @return string + */ + public function serialize() { + $fields = array( + 'globalid' => $this->globalId, + 'type' => $this->type, + 'group' => $this->group, + 'source' => $this->source, + 'language' => $this->languageCode, + 'localids' => $this->localIds, + 'config' => $this->extraConfig, + 'data' => $this->extraData, + 'forward' => $this->forward, + 'internalid' => $this->internalId, + + ); + + return serialize( $fields ); + } + + /** + * @see Serializable::unserialize + * + * @since 1.21 + * + * @param string $serialized + */ + public function unserialize( $serialized ) { + $fields = unserialize( $serialized ); + + $this->__construct( $fields['type'] ); + + $this->setGlobalId( $fields['globalid'] ); + $this->setGroup( $fields['group'] ); + $this->setSource( $fields['source'] ); + $this->setLanguageCode( $fields['language'] ); + $this->localIds = $fields['localids']; + $this->setExtraConfig( $fields['config'] ); + $this->setExtraData( $fields['data'] ); + $this->setForward( $fields['forward'] ); + $this->setInternalId( $fields['internalid'] ); + } + +} + +/** + * @deprecated + */ +class SiteObject extends Site {} diff --git a/includes/site/SiteList.php b/includes/site/SiteList.php new file mode 100644 index 00000000..b0d1f95b --- /dev/null +++ b/includes/site/SiteList.php @@ -0,0 +1,300 @@ + + */ +class SiteList extends GenericArrayObject { + + /** + * Internal site identifiers pointing to their sites offset value. + * + * @since 1.21 + * + * @var array of integer + */ + protected $byInternalId = array(); + + /** + * Global site identifiers pointing to their sites offset value. + * + * @since 1.21 + * + * @var array of string + */ + protected $byGlobalId = array(); + + /** + * @see GenericArrayObject::getObjectType + * + * @since 1.21 + * + * @return string + */ + public function getObjectType() { + return 'Site'; + } + + /** + * @see GenericArrayObject::preSetElement + * + * @since 1.21 + * + * @param int|string $index + * @param Site $site + * + * @return boolean + */ + protected function preSetElement( $index, $site ) { + if ( $this->hasSite( $site->getGlobalId() ) ) { + $this->removeSite( $site->getGlobalId() ); + } + + $this->byGlobalId[$site->getGlobalId()] = $index; + $this->byInternalId[$site->getInternalId()] = $index; + + return true; + } + + /** + * @see ArrayObject::offsetUnset() + * + * @since 1.21 + * + * @param mixed $index + */ + public function offsetUnset( $index ) { + if ( $this->offsetExists( $index ) ) { + /** + * @var Site $site + */ + $site = $this->offsetGet( $index ); + + unset( $this->byGlobalId[$site->getGlobalId()] ); + unset( $this->byInternalId[$site->getInternalId()] ); + } + + parent::offsetUnset( $index ); + } + + /** + * Returns all the global site identifiers. + * Optionally only those belonging to the specified group. + * + * @since 1.21 + * + * @return array + */ + public function getGlobalIdentifiers() { + return array_keys( $this->byGlobalId ); + } + + /** + * Returns if the list contains the site with the provided global site identifier. + * + * @param string $globalSiteId + * + * @return boolean + */ + public function hasSite( $globalSiteId ) { + return array_key_exists( $globalSiteId, $this->byGlobalId ); + } + + /** + * Returns the Site with the provided global site identifier. + * The site needs to exist, so if not sure, call hasGlobalId first. + * + * @since 1.21 + * + * @param string $globalSiteId + * + * @return Site + */ + public function getSite( $globalSiteId ) { + return $this->offsetGet( $this->byGlobalId[$globalSiteId] ); + } + + /** + * Removes the site with the specified global site identifier. + * The site needs to exist, so if not sure, call hasGlobalId first. + * + * @since 1.21 + * + * @param string $globalSiteId + */ + public function removeSite( $globalSiteId ) { + $this->offsetUnset( $this->byGlobalId[$globalSiteId] ); + } + + /** + * Returns if the list contains no sites. + * + * @since 1.21 + * + * @return boolean + */ + public function isEmpty() { + return $this->byGlobalId === array(); + } + + /** + * Returns if the list contains the site with the provided site id. + * + * @param integer $id + * + * @return boolean + */ + public function hasInternalId( $id ) { + return array_key_exists( $id, $this->byInternalId ); + } + + /** + * Returns the Site with the provided site id. + * The site needs to exist, so if not sure, call has first. + * + * @since 1.21 + * + * @param integer $id + * + * @return Site + */ + public function getSiteByInternalId( $id ) { + return $this->offsetGet( $this->byInternalId[$id] ); + } + + /** + * Removes the site with the specified site id. + * The site needs to exist, so if not sure, call has first. + * + * @since 1.21 + * + * @param integer $id + */ + public function removeSiteByInternalId( $id ) { + $this->offsetUnset( $this->byInternalId[$id] ); + } + + /** + * Sets a site in the list. If the site was not there, + * it will be added. If it was, it will be updated. + * + * @since 1.21 + * + * @param Site $site + */ + public function setSite( Site $site ) { + $this[] = $site; + } + + /** + * Returns the sites that are in the provided group. + * + * @since 1.21 + * + * @param string $groupName + * + * @return SiteList + */ + public function getGroup( $groupName ) { + $group = new self(); + + /** + * @var \Site $site + */ + foreach ( $this as $site ) { + if ( $site->getGroup() === $groupName ) { + $group[] = $site; + } + } + + return $group; + } + + /** + * A version ID that identifies the serialization structure used by getSerializationData() + * and unserialize(). This is useful for constructing cache keys in cases where the cache relies + * on serialization for storing the SiteList. + * + * @var string A string uniquely identifying the version of the serialization structure, + * not including any sub-structures. + */ + const SERIAL_VERSION_ID = '2013-02-07'; + + /** + * Returns the version ID that identifies the serialization structure used by + * getSerializationData() and unserialize(), including the structure of any nested structures. + * This is useful for constructing cache keys in cases where the cache relies + * on serialization for storing the SiteList. + * + * @return string A string uniquely identifying the version of the serialization structure, + * including any sub-structures. + */ + public static function getSerialVersionId() { + return self::SERIAL_VERSION_ID . '+Site:' . Site::SERIAL_VERSION_ID; + } + + /** + * @see GenericArrayObject::getSerializationData + * + * @since 1.21 + * + * @return array + */ + protected function getSerializationData() { + //NOTE: When changing the structure, either implement unserialize() to handle the + // old structure too, or update SERIAL_VERSION_ID to kill any caches. + return array_merge( + parent::getSerializationData(), + array( + 'internalIds' => $this->byInternalId, + 'globalIds' => $this->byGlobalId, + ) + ); + } + + /** + * @see GenericArrayObject::unserialize + * + * @since 1.21 + * + * @param string $serialization + * + * @return array + */ + public function unserialize( $serialization ) { + $serializationData = parent::unserialize( $serialization ); + + $this->byInternalId = $serializationData['internalIds']; + $this->byGlobalId = $serializationData['globalIds']; + + return $serializationData; + } + +} + +/** + * @deprecated + */ +class SiteArray extends SiteList {} diff --git a/includes/site/SiteSQLStore.php b/includes/site/SiteSQLStore.php new file mode 100644 index 00000000..41238055 --- /dev/null +++ b/includes/site/SiteSQLStore.php @@ -0,0 +1,491 @@ + + */ +class SiteSQLStore implements SiteStore { + + /** + * @since 1.21 + * + * @var SiteList|null + */ + protected $sites = null; + + /** + * @var ORMTable + */ + protected $sitesTable; + + /** + * @var string|null + */ + private $cacheKey = null; + + /** + * @var int + */ + private $cacheTimeout = 3600; + + /** + * @since 1.21 + * + * @param ORMTable|null $sitesTable + * + * @return SiteStore + */ + public static function newInstance( ORMTable $sitesTable = null ) { + return new static( $sitesTable ); + } + + /** + * Constructor. + * + * @since 1.21 + * + * @param ORMTable|null $sitesTable + */ + protected function __construct( ORMTable $sitesTable = null ) { + if ( $sitesTable === null ) { + $sitesTable = $this->newSitesTable(); + } + + $this->sitesTable = $sitesTable; + } + + /** + * Constructs a cache key to use for caching the list of sites. + * + * This includes the concrete class name of the site list as well as a version identifier + * for the list's serialization, to avoid problems when unserializing site lists serialized + * by an older version, e.g. when reading from a cache. + * + * The cache key also includes information about where the sites were loaded from, e.g. + * the name of a database table. + * + * @see SiteList::getSerialVersionId + * + * @return String The cache key. + */ + protected function getCacheKey() { + wfProfileIn( __METHOD__ ); + + if ( $this->cacheKey === null ) { + $type = 'SiteList#' . SiteList::getSerialVersionId(); + $source = $this->sitesTable->getName(); + + if ( $this->sitesTable->getTargetWiki() !== false ) { + $source = $this->sitesTable->getTargetWiki() . '.' . $source; + } + + $this->cacheKey = wfMemcKey( "$source/$type" ); + } + + wfProfileOut( __METHOD__ ); + return $this->cacheKey; + } + + /** + * @see SiteStore::getSites + * + * @since 1.21 + * + * @param string $source either 'cache' or 'recache' + * + * @return SiteList + */ + public function getSites( $source = 'cache' ) { + wfProfileIn( __METHOD__ ); + + if ( $source === 'cache' ) { + if ( $this->sites === null ) { + $cache = wfGetMainCache(); + $sites = $cache->get( $this->getCacheKey() ); + + if ( is_object( $sites ) ) { + $this->sites = $sites; + } else { + $this->loadSites(); + } + } + } + else { + $this->loadSites(); + } + + wfProfileOut( __METHOD__ ); + return $this->sites; + } + + /** + * Returns a new Site object constructed from the provided ORMRow. + * + * @since 1.21 + * + * @param ORMRow $siteRow + * + * @return Site + */ + protected function siteFromRow( ORMRow $siteRow ) { + wfProfileIn( __METHOD__ ); + + $site = Site::newForType( $siteRow->getField( 'type', Site::TYPE_UNKNOWN ) ); + + $site->setGlobalId( $siteRow->getField( 'global_key' ) ); + + $site->setInternalId( $siteRow->getField( 'id' ) ); + + if ( $siteRow->hasField( 'forward' ) ) { + $site->setForward( $siteRow->getField( 'forward' ) ); + } + + if ( $siteRow->hasField( 'group' ) ) { + $site->setGroup( $siteRow->getField( 'group' ) ); + } + + if ( $siteRow->hasField( 'language' ) ) { + $site->setLanguageCode( $siteRow->getField( 'language' ) === '' ? null : $siteRow->getField( 'language' ) ); + } + + if ( $siteRow->hasField( 'source' ) ) { + $site->setSource( $siteRow->getField( 'source' ) ); + } + + if ( $siteRow->hasField( 'data' ) ) { + $site->setExtraData( $siteRow->getField( 'data' ) ); + } + + if ( $siteRow->hasField( 'config' ) ) { + $site->setExtraConfig( $siteRow->getField( 'config' ) ); + } + + wfProfileOut( __METHOD__ ); + return $site; + } + + /** + * Fetches the site from the database and loads them into the sites field. + * + * @since 1.21 + */ + protected function loadSites() { + wfProfileIn( __METHOD__ ); + + $this->sites = new SiteList(); + + foreach ( $this->sitesTable->select() as $siteRow ) { + $this->sites[] = $this->siteFromRow( $siteRow ); + } + + // Batch load the local site identifiers. + $ids = wfGetDB( $this->sitesTable->getReadDb() )->select( + 'site_identifiers', + array( + 'si_site', + 'si_type', + 'si_key', + ), + array(), + __METHOD__ + ); + + foreach ( $ids as $id ) { + if ( $this->sites->hasInternalId( $id->si_site ) ) { + $site = $this->sites->getSiteByInternalId( $id->si_site ); + $site->addLocalId( $id->si_type, $id->si_key ); + $this->sites->setSite( $site ); + } + } + + $cache = wfGetMainCache(); + $cache->set( $this->getCacheKey(), $this->sites, $this->cacheTimeout ); + + wfProfileOut( __METHOD__ ); + } + + /** + * @see SiteStore::getSite + * + * @since 1.21 + * + * @param string $globalId + * @param string $source + * + * @return Site|null + */ + public function getSite( $globalId, $source = 'cache' ) { + wfProfileIn( __METHOD__ ); + + $sites = $this->getSites( $source ); + + wfProfileOut( __METHOD__ ); + return $sites->hasSite( $globalId ) ? $sites->getSite( $globalId ) : null; + } + + /** + * @see SiteStore::saveSite + * + * @since 1.21 + * + * @param Site $site + * + * @return boolean Success indicator + */ + public function saveSite( Site $site ) { + return $this->saveSites( array( $site ) ); + } + + /** + * @see SiteStore::saveSites + * + * @since 1.21 + * + * @param Site[] $sites + * + * @return boolean Success indicator + */ + public function saveSites( array $sites ) { + wfProfileIn( __METHOD__ ); + + if ( empty( $sites ) ) { + wfProfileOut( __METHOD__ ); + return true; + } + + $dbw = $this->sitesTable->getWriteDbConnection(); + + $trx = $dbw->trxLevel(); + + if ( $trx == 0 ) { + $dbw->begin( __METHOD__ ); + } + + $success = true; + + $internalIds = array(); + $localIds = array(); + + foreach ( $sites as $site ) { + $fields = array( + // Site data + 'global_key' => $site->getGlobalId(), // TODO: check not null + 'type' => $site->getType(), + 'group' => $site->getGroup(), + 'source' => $site->getSource(), + 'language' => $site->getLanguageCode() === null ? '' : $site->getLanguageCode(), + 'protocol' => $site->getProtocol(), + 'domain' => strrev( $site->getDomain() ) . '.', + 'data' => $site->getExtraData(), + + // Site config + 'forward' => $site->shouldForward(), + 'config' => $site->getExtraConfig(), + ); + + if ( $site->getInternalId() !== null ) { + $fields['id'] = $site->getInternalId(); + $internalIds[] = $site->getInternalId(); + } + + $siteRow = new ORMRow( $this->sitesTable, $fields ); + $success = $siteRow->save( __METHOD__ ) && $success; + + foreach ( $site->getLocalIds() as $idType => $ids ) { + foreach ( $ids as $id ) { + $localIds[] = array( $siteRow->getId(), $idType, $id ); + } + } + } + + if ( $internalIds !== array() ) { + $dbw->delete( + 'site_identifiers', + array( 'si_site' => $internalIds ), + __METHOD__ + ); + } + + foreach ( $localIds as $localId ) { + $dbw->insert( + 'site_identifiers', + array( + 'si_site' => $localId[0], + 'si_type' => $localId[1], + 'si_key' => $localId[2], + ), + __METHOD__ + ); + } + + if ( $trx == 0 ) { + $dbw->commit( __METHOD__ ); + } + + // purge cache + $this->reset(); + + wfProfileOut( __METHOD__ ); + return $success; + } + + /** + * Purges the internal and external cache of the site list, forcing the list + * of sites to be re-read from the database. + * + * @since 1.21 + */ + public function reset() { + wfProfileIn( __METHOD__ ); + // purge cache + $cache = wfGetMainCache(); + $cache->delete( $this->getCacheKey() ); + $this->sites = null; + + wfProfileOut( __METHOD__ ); + } + + /** + * Clears the list of sites stored in the database. + * + * @see SiteStore::clear() + * + * @return bool success + */ + public function clear() { + wfProfileIn( __METHOD__ ); + $dbw = $this->sitesTable->getWriteDbConnection(); + + $trx = $dbw->trxLevel(); + + if ( $trx == 0 ) { + $dbw->begin( __METHOD__ ); + } + + $ok = $dbw->delete( 'sites', '*', __METHOD__ ); + $ok = $dbw->delete( 'site_identifiers', '*', __METHOD__ ) && $ok; + + if ( $trx == 0 ) { + $dbw->commit( __METHOD__ ); + } + + $this->reset(); + + wfProfileOut( __METHOD__ ); + return $ok; + } + + /** + * @since 1.21 + * + * @return ORMTable + */ + protected function newSitesTable() { + return new ORMTable( + 'sites', + array( + 'id' => 'id', + + // Site data + 'global_key' => 'str', + 'type' => 'str', + 'group' => 'str', + 'source' => 'str', + 'language' => 'str', + 'protocol' => 'str', + 'domain' => 'str', + 'data' => 'array', + + // Site config + 'forward' => 'bool', + 'config' => 'array', + ), + array( + 'type' => Site::TYPE_UNKNOWN, + 'group' => Site::GROUP_NONE, + 'source' => Site::SOURCE_LOCAL, + 'data' => array(), + + 'forward' => false, + 'config' => array(), + 'language' => '', + ), + 'ORMRow', + 'site_' + ); + } + +} + +/** + * @deprecated + */ +class Sites extends SiteSQLStore { + + /** + * Factory for creating new site objects. + * + * @since 1.21 + * @deprecated + * + * @param string|boolean false $globalId + * + * @return Site + */ + public static function newSite( $globalId = false ) { + $site = new Site(); + + if ( $globalId !== false ) { + $site->setGlobalId( $globalId ); + } + + return $site; + } + + /** + * @deprecated + * @return SiteStore + */ + public static function singleton() { + static $singleton; + + if ( $singleton === null ) { + $singleton = new static(); + } + + return $singleton; + } + + /** + * @deprecated + * @return SiteList + */ + public function getSiteGroup( $group ) { + return $this->getSites()->getGroup( $group ); + } + +} diff --git a/includes/site/SiteStore.php b/includes/site/SiteStore.php new file mode 100644 index 00000000..52ba8fbf --- /dev/null +++ b/includes/site/SiteStore.php @@ -0,0 +1,85 @@ + + */ +interface SiteStore { + + /** + * Saves the provided site. + * + * @since 1.21 + * + * @param Site $site + * + * @return boolean Success indicator + */ + public function saveSite( Site $site ); + + /** + * Saves the provided sites. + * + * @since 1.21 + * + * @param Site[] $sites + * + * @return boolean Success indicator + */ + public function saveSites( array $sites ); + + /** + * Returns the site with provided global id, or null if there is no such site. + * + * @since 1.21 + * + * @param string $globalId + * @param string $source either 'cache' or 'recache'. + * If 'cache', the values are allowed (but not obliged) to come from a cache. + * + * @return Site|null + */ + public function getSite( $globalId, $source = 'cache' ); + + /** + * Returns a list of all sites. By default this site is + * fetched from the cache, which can be changed to loading + * the list from the database using the $useCache parameter. + * + * @since 1.21 + * + * @param string $source either 'cache' or 'recache'. + * If 'cache', the values are allowed (but not obliged) to come from a cache. + * + * @return SiteList + */ + public function getSites( $source = 'cache' ); + + /** + * Deletes all sites from the database. After calling clear(), getSites() will return an empty + * list and getSite() will return null until saveSite() or saveSites() is called. + */ + public function clear(); +} -- cgit v1.2.2