From 44020e254c9fa3b60b8f0874df84490ba99bb464 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Wed, 24 Aug 2011 13:03:03 +0000 Subject: [PATCH] Merge the iwtransclusion branch back into trunk Hexmode fixed broken unit tests in revisions after last time --- includes/AutoLoader.php | 3 + includes/BacklinkCache.php | 20 + includes/DefaultSettings.php | 30 +- includes/EditPage.php | 43 +- includes/GlobalUsageQuery.php | 368 ++++++++++++++++++ includes/Linker.php | 36 ++ includes/LinksUpdate.php | 145 +++++++ includes/OutputPage.php | 9 + includes/Revision.php | 43 +- includes/SpecialPageFactory.php | 2 + includes/Title.php | 49 ++- includes/WikiPage.php | 57 ++- includes/cache/HTMLCacheUpdate.php | 49 +++ includes/db/Database.php | 33 ++ includes/installer/MysqlUpdater.php | 3 + includes/installer/SqliteUpdater.php | 5 + includes/interwiki/Interwiki.php | 165 ++++++++ includes/parser/Parser.php | 100 ++--- includes/parser/ParserOutput.php | 29 ++ includes/parser/Preprocessor_DOM.php | 3 +- includes/parser/Preprocessor_Hash.php | 1 + includes/specials/SpecialGlobalFileUsage.php | 224 +++++++++++ .../specials/SpecialGlobalTemplateUsage.php | 207 ++++++++++ languages/messages/MessagesEn.php | 29 ++ languages/messages/MessagesQqq.php | 10 + .../archives/patch-globalinterwiki.sql | 10 + .../archives/patch-globalnamespaces.sql | 14 + .../archives/patch-globaltemplatelinks.sql | 36 ++ maintenance/language/messages.inc | 3 + maintenance/tables.sql | 64 +++ 30 files changed, 1713 insertions(+), 77 deletions(-) create mode 100644 includes/GlobalUsageQuery.php create mode 100644 includes/specials/SpecialGlobalFileUsage.php create mode 100644 includes/specials/SpecialGlobalTemplateUsage.php create mode 100644 maintenance/archives/patch-globalinterwiki.sql create mode 100644 maintenance/archives/patch-globalnamespaces.sql create mode 100644 maintenance/archives/patch-globaltemplatelinks.sql diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index bd3d9d0523..62da731aef 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -539,6 +539,7 @@ $wgAutoloadLocalClasses = array( 'BmpHandler' => 'includes/media/BMP.php', 'DjVuHandler' => 'includes/media/DjVu.php', 'Exif' => 'includes/media/Exif.php', + 'GlobalUsageQuery' => 'includes/GlobalUsageQuery.php', 'FormatExif' => 'includes/media/FormatMetadata.php', 'FormatMetadata' => 'includes/media/FormatMetadata.php', 'GIFHandler' => 'includes/media/GIF.php', @@ -752,6 +753,8 @@ $wgAutoloadLocalClasses = array( 'SpecialEmailUser' => 'includes/specials/SpecialEmailuser.php', 'SpecialExport' => 'includes/specials/SpecialExport.php', 'SpecialFilepath' => 'includes/specials/SpecialFilepath.php', + 'SpecialGlobalFileUsage' => 'includes/specials/SpecialGlobalFileUsage.php', + 'SpecialGlobalTemplateUsage' => 'includes/specials/SpecialGlobalTemplateUsage.php', 'SpecialImport' => 'includes/specials/SpecialImport.php', 'SpecialListFiles' => 'includes/specials/SpecialListfiles.php', 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php index 2263051456..978abecad3 100644 --- a/includes/BacklinkCache.php +++ b/includes/BacklinkCache.php @@ -174,6 +174,25 @@ class BacklinkCache { return $ta; } + /** + * Get the distant backtemplatelinks for the table globaltemplatelinks. Cached in process memory only. + * @return ResultWrapper list of distant pages that use the local title + */ + public function getDistantTemplateLinks( ) { + global $wgGlobalDatabase, $wgLocalInterwiki; + + $dbr = $dbr = wfGetDB( DB_SLAVE, array(), $wgGlobalDatabase ); + $res = $dbr->select( + array( 'globaltemplatelinks', 'globalinterwiki' ), + array( 'gtl_from_wiki', 'gtl_from_page', 'gtl_from_title', 'giw_prefix' ), + array( 'gtl_to_prefix' => $wgLocalInterwiki, 'gtl_to_title' => $this->title->getDBkey( ) ), + __METHOD__, + null, + array( 'gtl_from_wiki = giw_wikiid' ) + ); + return $res; + } + /** * Get the field name prefix for a given table * @param $table String @@ -185,6 +204,7 @@ class BacklinkCache { 'categorylinks' => 'cl', 'templatelinks' => 'tl', 'redirect' => 'rd', + 'globaltemplatelinks' => 'gtl', ); if ( isset( $prefixes[$table] ) ) { diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 83078bc1fb..d9e0de72df 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2983,12 +2983,34 @@ $wgExpensiveParserFunctionLimit = 100; $wgPreprocessorCacheThreshold = 1000; /** - * Enable interwiki transcluding. Only when iw_trans=1. + * Enable interwiki transcluding. Only when iw_trans=1 in the interwiki table. + * If the interwiki prefix is associated with a wiki ID in the interwiki table, + * then the distant templates will be retrieved in the distant DB. If there is + * no wiki ID but a API URL for that prefix, the distant templates will be + * retrieved using the API and cached in memcached. */ -$wgEnableScaryTranscluding = false; +$wgEnableInterwikiTranscluding = false; /** - * Expiry time for interwiki transclusion + * If $wgEnableInterwikiTranscluding is set to true and if an interwiki prefix + * is associated with a wiki ID, then, this option should be set to true to + * enable the cache invalidation of the distant pages when the local templates + * are edited and also to display the list of the distant templates used by + * the local pages. Enabling this requires to set up a global shared database + * (see next option $wgGlobalDatabase). + */ +$wgEnableInterwikiTemplatesTracking = false; + +/** + * If $wgEnableInterwikiTemplatesTracking is set to true, this option should + * contain the wiki ID of the database that hosts the globaltemplatelinks table. + */ +$wgGlobalDatabase = ''; + +/** + * If $wgEnableInterwikiTranscluding is set to true and if an interwiki + * prefix is associated with an API URL and no wiki ID, this will be + * the expiry time for the transcluded templates cached in memcached. */ $wgTranscludeCacheExpiry = 3600; @@ -5131,6 +5153,8 @@ $wgSpecialPageGroups = array( 'Export' => 'pagetools', 'Import' => 'pagetools', 'Whatlinkshere' => 'pagetools', + 'GlobalFileUsage' => 'pagetools', + 'GlobalTemplateUsage' => 'pagetools', 'Statistics' => 'wiki', 'Version' => 'wiki', diff --git a/includes/EditPage.php b/includes/EditPage.php index 28032da57e..20a1f4834a 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -1313,7 +1313,7 @@ class EditPage { * during form output near the top, for captchas and the like. */ function showEditForm( $formCallback = null ) { - global $wgOut, $wgUser; + global $wgOut, $wgUser, $wgEnableInterwikiTranscluding, $wgEnableInterwikiTemplatesTracking; wfProfileIn( __METHOD__ ); @@ -1347,7 +1347,6 @@ class EditPage { $toolbar = ''; } - $wgOut->addHTML( $this->editFormPageTop ); if ( $wgUser->getOption( 'previewontop' ) ) { @@ -1359,6 +1358,9 @@ class EditPage { $templates = $this->getTemplates(); $formattedtemplates = Linker::formatTemplates( $templates, $this->preview, $this->section != ''); + $distantTemplates = $this->getDistantTemplates(); + $formattedDistantTemplates = Linker::formatDistantTemplates( $distantTemplates, $this->preview, $this->section != '' ); + $hiddencats = $this->mArticle->getHiddenCategories(); $formattedhiddencats = Linker::formatHiddenCategories( $hiddencats ); @@ -1457,6 +1459,21 @@ HTML
{$formattedtemplates}
+HTML +); + + if ( $wgEnableInterwikiTranscluding && $wgEnableInterwikiTemplatesTracking ) { + $wgOut->addHTML( <<editFormTextAfterTools} +
+{$formattedDistantTemplates} +
+HTML +); + } + + $wgOut->addHTML( <<editFormTextAfterTools}
{$formattedhiddencats}
@@ -2118,6 +2135,28 @@ HTML } } + function getDistantTemplates() { + global $wgEnableInterwikiTemplatesTracking; + if ( !$wgEnableInterwikiTemplatesTracking ) { + return array( ); + } + if ( $this->preview || $this->section != '' ) { + $templates = array(); + if ( !isset( $this->mParserOutput ) ) return $templates; + $templatesList = $this->mParserOutput->getDistantTemplates(); + foreach( $templatesList as $prefix => $templatesbyns ) { + foreach( $templatesbyns as $ns => $template ) { + foreach( array_keys( $template ) as $dbk ) { + $templates[] = Title::makeTitle( $ns, $dbk, null, $prefix ); + } + } + } + return $templates; + } else { + return $this->mArticle->getUsedDistantTemplates(); + } + } + /** * Call the stock "user is blocked" page */ diff --git a/includes/GlobalUsageQuery.php b/includes/GlobalUsageQuery.php new file mode 100644 index 0000000000..8e4ad35126 --- /dev/null +++ b/includes/GlobalUsageQuery.php @@ -0,0 +1,368 @@ +db = wfGetDB( DB_SLAVE, array(), $wgGlobalDatabase ); + if ( $target instanceof Title && $target->getNamespace( ) == NS_FILE ) { + $this->target = $target->getDBKey(); + } else { + $this->target = $target; + } + $this->offset = array(); + + } + + /** + * Set the offset parameter + * + * @param $offset string offset + * @param $reversed bool True if this is the upper offset + */ + public function setOffset( $offset, $reversed = null ) { + if ( !is_null( $reversed ) ) { + $this->reversed = $reversed; + } + + if ( !is_array( $offset ) ) { + $offset = explode( '|', $offset ); + } + + if ( count( $offset ) == 3 ) { + $this->offset = $offset; + return true; + } else { + return false; + } + } + /** + * Return the offset set by the user + * + * @return array offset + */ + public function getOffsetString() { + return implode( '|', $this->offset ); + } + /** + * Is the result reversed + * + * @return bool + */ + public function isReversed() { + return $this->reversed; + } + + /** + * Returns the string used for continuation in a file search + * + * @return string + * + */ + public function getContinueFileString() { + if ( $this->hasMore() ) { + return "{$this->lastRow->gil_to}|{$this->lastRow->gil_wiki}|{$this->lastRow->gil_page}"; + } else { + return ''; + } + } + + /** + * Returns the string used for continuation in a template search + * + * @return string + * + */ + public function getContinueTemplateString() { + if ( $this->hasMore() ) { + return "{$this->lastRow->gtl_to_title}|{$this->lastRow->gtl_from_wiki}|{$this->lastRow->gtl_from_page}"; + } else { + return ''; + } + } + + /** + * Set the maximum amount of items to return. Capped at 500. + * + * @param $limit int The limit + */ + public function setLimit( $limit ) { + $this->limit = min( $limit, 500 ); + } + /** + * Returns the user set limit + */ + public function getLimit() { + return $this->limit; + } + + /** + * Set whether to filter out the local usage + */ + public function filterLocal( $value = true ) { + $this->filterLocal = $value; + } + + /** + * Executes the query for a file search + */ + public function searchTemplate() { + global $wgLocalInterwiki; + + /* Construct a where clause */ + // Add target template(s) + $where = array( 'gtl_to_prefix' => $wgLocalInterwiki, + 'gtl_to_namespace' => $this->target->getNamespace( ), + 'gtl_to_title' => $this->target->getDBkey( ) + ); + + // Set the continuation condition + $order = 'ASC'; + if ( $this->offset ) { + $qTo = $this->db->addQuotes( $this->offset[0] ); + $qWiki = $this->db->addQuotes( $this->offset[1] ); + $qPage = intval( $this->offset[2] ); + + // Check which limit we got in order to determine which way to traverse rows + if ( $this->reversed ) { + // Reversed traversal; do not include offset row + $op1 = '<'; + $op2 = '<'; + $order = 'DESC'; + } else { + // Normal traversal; include offset row + $op1 = '>'; + $op2 = '>='; + $order = 'ASC'; + } + + $where[] = "(gtl_to_title $op1 $qTo) OR " . + "(gtl_to_title = $qTo AND gtl_from_wiki $op1 $qWiki) OR " . + "(gtl_to_title = $qTo AND gtl_from_wiki = $qWiki AND gtl_from_page $op2 $qPage)"; + } + + /* Perform select (Duh.) */ + $res = $this->db->select( + array( + 'globaltemplatelinks', + 'globalnamespaces' + ), + array( + 'gtl_to_title', + 'gtl_from_wiki', + 'gtl_from_page', + 'gtl_from_namespace', + 'gtl_from_title' + ), + $where, + __METHOD__, + array( + 'ORDER BY' => "gtl_to_title $order, gtl_from_wiki $order, gtl_from_page $order", + // Select an extra row to check whether we have more rows available + 'LIMIT' => $this->limit + 1, + ), + array( + 'gtl_from_namespace = gn_namespace' + ) + ); + + /* Process result */ + // Always return the result in the same order; regardless whether reversed was specified + // reversed is really only used to determine from which direction the offset is + $rows = array(); + foreach ( $res as $row ) { + $rows[] = $row; + } + if ( $this->reversed ) { + $rows = array_reverse( $rows ); + } + + // Build the result array + $count = 0; + $this->hasMore = false; + $this->result = array(); + foreach ( $rows as $row ) { + $count++; + if ( $count > $this->limit ) { + // We've reached the extra row that indicates that there are more rows + $this->hasMore = true; + $this->lastRow = $row; + break; + } + + if ( !isset( $this->result[$row->gtl_to_title] ) ) { + $this->result[$row->gtl_to_title] = array(); + } + if ( !isset( $this->result[$row->gtl_to_title][$row->gtl_from_wiki] ) ) { + $this->result[$row->gtl_to_title][$row->gtl_from_wiki] = array(); + } + + $this->result[$row->gtl_to_title][$row->gtl_from_wiki][] = array( + 'template' => $row->gtl_to_title, + 'id' => $row->gtl_from_page, + 'namespace' => $row->gn_namespacetext, + 'title' => $row->gtl_from_title, + 'wiki' => $row->gtl_from_wiki, + ); + } + } + + /** + * Executes the query for a template search + */ + public function searchFile() { + /* Construct a where clause */ + // Add target image(s) + $where = array( 'gil_to' => $this->target ); + + if ( $this->filterLocal ) { + // Don't show local file usage + $where[] = 'gil_wiki != ' . $this->db->addQuotes( wfWikiId() ); + } + + // Set the continuation condition + $order = 'ASC'; + if ( $this->offset ) { + $qTo = $this->db->addQuotes( $this->offset[0] ); + $qWiki = $this->db->addQuotes( $this->offset[1] ); + $qPage = intval( $this->offset[2] ); + + // Check which limit we got in order to determine which way to traverse rows + if ( $this->reversed ) { + // Reversed traversal; do not include offset row + $op1 = '<'; + $op2 = '<'; + $order = 'DESC'; + } else { + // Normal traversal; include offset row + $op1 = '>'; + $op2 = '>='; + $order = 'ASC'; + } + + $where[] = "(gil_to $op1 $qTo) OR " . + "(gil_to = $qTo AND gil_wiki $op1 $qWiki) OR " . + "(gil_to = $qTo AND gil_wiki = $qWiki AND gil_page $op2 $qPage)"; + } + + /* Perform select (Duh.) */ + $res = $this->db->select( 'globalimagelinks', + array( + 'gil_to', + 'gil_wiki', + 'gil_page', + 'gil_page_namespace', + 'gil_page_title' + ), + $where, + __METHOD__, + array( + 'ORDER BY' => "gil_to $order, gil_wiki $order, gil_page $order", + // Select an extra row to check whether we have more rows available + 'LIMIT' => $this->limit + 1, + ) + ); + + /* Process result */ + // Always return the result in the same order; regardless whether reversed was specified + // reversed is really only used to determine from which direction the offset is + $rows = array(); + foreach ( $res as $row ) { + $rows[] = $row; + } + if ( $this->reversed ) { + $rows = array_reverse( $rows ); + } + + // Build the result array + $count = 0; + $this->hasMore = false; + $this->result = array(); + foreach ( $rows as $row ) { + $count++; + if ( $count > $this->limit ) { + // We've reached the extra row that indicates that there are more rows + $this->hasMore = true; + $this->lastRow = $row; + break; + } + + if ( !isset( $this->result[$row->gil_to] ) ) { + $this->result[$row->gil_to] = array(); + } + if ( !isset( $this->result[$row->gil_to][$row->gil_wiki] ) ) { + $this->result[$row->gil_to][$row->gil_wiki] = array(); + } + + $this->result[$row->gil_to][$row->gil_wiki][] = array( + 'image' => $row->gil_to, + 'id' => $row->gil_page, + 'namespace' => $row->gil_page_namespace, + 'title' => $row->gil_page_title, + 'wiki' => $row->gil_wiki, + ); + } + } + + /** + * Returns the result set. The result is a 4 dimensional array + * (file, wiki, page), whose items are arrays with keys: + * - image or template: File name or template name + * - id: Page id + * - namespace: Page namespace text + * - title: Unprefixed page title + * - wiki: Wiki id + * + * @return array Result set + */ + public function getResult() { + return $this->result; + } + /** + * Returns a 3 dimensional array with the result of the first file. Useful + * if only one resource was queried. + * + * For further information see documentation of getResult() + * + * @return array Result set + */ + public function getSingleResult() { + if ( $this->result ) { + return current( $this->result ); + } else { + return array(); + } + } + + /** + * Returns whether there are more results + * + * @return bool + */ + public function hasMore() { + return $this->hasMore; + } + + /** + * Returns the result length + * + * @return int + */ + public function count() { + return count( $this->result ); + } +} diff --git a/includes/Linker.php b/includes/Linker.php index f1fb577174..a1cf6a6329 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1645,6 +1645,42 @@ class Linker { return $outText; } + /** + * Returns HTML for the "templates used on this page" list. + * + * @param $templates Array of templates from Article::getUsedTemplate + * or similar + * @param $preview Boolean: whether this is for a preview + * @param $section Boolean: whether this is for a section edit + * @return String: HTML output + */ + public static function formatDistantTemplates( $templates, $preview = false, $section = false ) { + wfProfileIn( __METHOD__ ); + + $outText = ''; + if ( count( $templates ) > 0 ) { + + # Construct the HTML + $outText = '
'; + if ( $preview ) { + $outText .= wfMsgExt( 'distanttemplatesusedpreview', array( 'parse' ), count( $templates ) ); + } elseif ( $section ) { + $outText .= wfMsgExt( 'distanttemplatesusedsection', array( 'parse' ), count( $templates ) ); + } else { + $outText .= wfMsgExt( 'distanttemplatesused', array( 'parse' ), count( $templates ) ); + } + $outText .= "
'; + } + wfProfileOut( __METHOD__ ); + return $outText; + } + /** * Returns HTML for the "hidden categories on this page" list. * diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 3252fb655f..da77601319 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -29,6 +29,7 @@ class LinksUpdate { $mLinks, //!< Map of title strings to IDs for the links in the document $mImages, //!< DB keys of the images used, in the array key only $mTemplates, //!< Map of title strings to IDs for the template references, including broken ones + $mDistantTemplates,//!< Map of title strings to IDs for the distant template references, including broken ones $mExternals, //!< URLs of external links, array key only $mCategories, //!< Map of category names to sort keys $mInterlangs, //!< Map of language codes to titles @@ -66,6 +67,7 @@ class LinksUpdate { $this->mLinks = $parserOutput->getLinks(); $this->mImages = $parserOutput->getImages(); $this->mTemplates = $parserOutput->getTemplates(); + $this->mDistantTemplates = $parserOutput->getDistantTemplates(); $this->mExternals = $parserOutput->getExternalLinks(); $this->mCategories = $parserOutput->getCategories(); $this->mProperties = $parserOutput->getProperties(); @@ -152,6 +154,15 @@ class LinksUpdate { $this->incrTableUpdate( 'templatelinks', 'tl', $this->getTemplateDeletions( $existing ), $this->getTemplateInsertions( $existing ) ); + # Distant template links + global $wgGlobalDB; + if ( $wgGlobalDB ) { + $existing = $this->getDistantExistingTemplates(); + $this->incrSharedTableUpdate( 'globaltemplatelinks', 'gtl', + $this->getDistantTemplateDeletions( $existing ), + $this->getDistantTemplateInsertions( $existing ) ); + } + # Category links $existing = $this->getExistingCategories(); @@ -367,11 +378,55 @@ class LinksUpdate { if ( $where ) { $this->mDb->delete( $table, $where, __METHOD__ ); } + if ( isset( $insertions['globaltemplatelinks'] ) ) { + $this->mDb->insert( 'globaltemplatelinks', $insertions['globaltemplatelinks'], __METHOD__, 'IGNORE' ); + unset( $insertions['globaltemplatelinks'] ); + } + if ( isset( $insertions['globalnamespaces'] ) ) { + $this->mDb->insert( 'globalnamespaces', $insertions['globalnamespaces'], __METHOD__, 'IGNORE' ); + unset( $insertions['globalnamespaces'] ); + } + if ( isset( $insertions['globalinterwiki'] ) ) { + $this->mDb->insert( 'globalinterwiki', $insertions['globalinterwiki'], __METHOD__, 'IGNORE' ); + unset( $insertions['globalinterwiki'] ); + } if ( count( $insertions ) ) { $this->mDb->insert( $table, $insertions, __METHOD__, 'IGNORE' ); } } + /** + * Update a shared table by doing a delete query then an insert query + * @private + */ + function incrSharedTableUpdate( $table, $prefix, $deletions, $insertions ) { + + global $wgWikiID; + global $wgGlobalDB; + + if ( $wgGlobalDB ) { + $dbw = wfGetDB( DB_MASTER, array(), $wgGlobalDB ); + $where = array( "{$prefix}_from_wiki" => $wgWikiID, + "{$prefix}_from_page" => $this->mId + ); + $baseKey = "{$prefix}_to_wiki"; + $middleKey = "{$prefix}_to_namespace"; + + $clause = $dbw->makeWhereFrom3d( $deletions, $baseKey, $middleKey, "{$prefix}_to_title" ); + if ( $clause ) { + $where[] = $clause; + } else { + $where = false; + } + + if ( $where ) { + $dbw->delete( $table, $where, __METHOD__ ); + } + if ( count( $insertions ) ) { + $dbw->insert( $table, $insertions, __METHOD__, 'IGNORE' ); + } + } + } /** * Get an array of pagelinks insertions for passing to the DB @@ -414,6 +469,45 @@ class LinksUpdate { return $arr; } + /** + * Get an array of distant template insertions. Like getLinkInsertions() + * @private + */ + function getDistantTemplateInsertions( $existing = array() ) { + global $wgWikiID; + $arr = array(); + foreach( $this->mDistantTemplates as $wikiid => $templatesToNS ) { + foreach( $templatesToNS as $ns => $dbkeys ) { + $diffs = isset( $existing[$wikiid] ) && isset( $existing[$wikiid][$ns] ) + ? array_diff_key( $dbkeys, $existing[$wikiid][$ns] ) + : $dbkeys; + $interwiki = Interwiki::fetch( $wikiid ); + $wikiid = $interwiki->getWikiID(); + foreach ( $diffs as $dbk => $id ) { + $arr['globaltemplatelinks'][] = array( + 'gtl_from_wiki' => $wgWikiID, + 'gtl_from_page' => $this->mId, + 'gtl_from_namespace' => $this->mTitle->getNamespace(), + 'gtl_from_title' => $this->mTitle->getText(), + 'gtl_to_wiki' => $wikiid, + 'gtl_to_namespace' => $ns, + 'gtl_to_title' => $dbk + ); + $arr['globalinterwiki'][] = array( + 'giw_wikiid' => $wikiid, + 'giw_prefix' => $prefix // FIXME: $prefix ix undefined + ); + $arr['globalnamespaces'][] = array( + 'gn_wiki' => wfWikiID( ), + 'gn_namespace' => $this->mTitle->getNamespace(), + 'gn_namespacetext' => $this->mTitle->getNsText(), + ); + } + } + } + return $arr; + } + /** * Get an array of image insertions * Skips the names specified in $existing @@ -580,6 +674,30 @@ class LinksUpdate { return $del; } + /** + * Given an array of existing templates, returns those templates which are not in $this + * and thus should be deleted. + * @private + */ + function getDistantTemplateDeletions( $existing ) { + $del = array(); + foreach ( $existing as $wikiid => $templatesForNS ) { + if ( isset( $this->mDistantTemplates[$wikiid] ) ) { + $del[$wikiid] = array_diff_key( $existing[$wikiid], $this->mDistantTemplates[$wikiid] ); + } else { + $del[$wikiid] = $existing[$wikiid]; + } + foreach ( $templatesForNS as $ns => $dbkeys ) { + if ( isset( $this->mDistantTemplates[$wikiid][$ns] ) ) { + $del[$wikiid][$ns] = array_diff_key( $existing[$wikiid][$ns], $this->mDistantTemplates[$wikiid][$ns] ); + } else { + $del[$wikiid][$ns] = $existing[$wikiid][$ns]; + } + } + } + return $del; + } + /** * Given an array of existing images, returns those images which are not in $this * and thus should be deleted. @@ -675,6 +793,33 @@ class LinksUpdate { return $arr; } + /** + * Get an array of existing distant templates, as a 3-D array + * @private + */ + function getDistantExistingTemplates() { + global $wgWikiID; + global $wgGlobalDB; + + $arr = array(); + if ( $wgGlobalDB ) { + $dbr = wfGetDB( DB_SLAVE, array(), $wgGlobalDB ); + $res = $dbr->select( 'globaltemplatelinks', array( 'gtl_to_wiki', 'gtl_to_namespace', 'gtl_to_title' ), + array( 'gtl_from_wiki' => $wgWikiID, 'gtl_from_page' => $this->mId ), __METHOD__, $this->mOptions ); + while ( $row = $dbr->fetchObject( $res ) ) { + if ( !isset( $arr[$row->gtl_to_wiki] ) ) { + $arr[$row->gtl_to_wiki] = array(); + } + if ( !isset( $arr[$row->gtl_to_wiki][$row->gtl_to_namespace] ) ) { + $arr[$row->gtl_to_wiki][$row->gtl_to_namespace] = array(); + } + $arr[$row->gtl_to_wiki][$row->gtl_to_namespace][$row->gtl_to_title] = 1; + } + $dbr->freeResult( $res ); + } + return $arr; + } + /** * Get an array of existing images, image names in the keys * @private diff --git a/includes/OutputPage.php b/includes/OutputPage.php index a80e3156b1..4d06feb4d3 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -2048,6 +2048,9 @@ class OutputPage extends ContextSource { * @param $action String: action that was denied or null if unknown */ public function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) { + global $wgUser, $wgEnableInterwikiTranscluding, $wgEnableInterwikiTemplatesTracking; + $skin = $wgUser->getSkin(); + $this->setRobotPolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); @@ -2093,6 +2096,12 @@ class OutputPage extends ContextSource { $templates " ); + if ( $wgEnableInterwikiTranscluding && $wgEnableInterwikiTemplatesTracking ) { + $this->addHTML( "
+{$skin->formatDistantTemplates( $article->getUsedDistantTemplates( ) )} +
+" ); + } } # If the title doesn't exist, it's fairly pointless to print a return diff --git a/includes/Revision.php b/includes/Revision.php index f93108017b..243c9c06a0 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -165,6 +165,30 @@ class Revision { return Revision::loadFromConds( $db, $conds ); } + /** + * Stores the origin wiki of a revision in case it is a foreign wiki + */ + function setWikiID( $wikiID ) { + $this->mWikiID = $wikiID; + } + + /** + * Load the current revision of a given page of a foreign wiki. + * The WikiID is stored for further use, such as loadText() and getTimestampFromId() + */ + public static function loadFromTitleForeignWiki( $wikiID, $title ) { + $dbr = wfGetDB( DB_SLAVE, array(), $wikiID ); + + $revision = self::loadFromTitle( $dbr, $title ); + + if( $revision ) { + $revision->setWikiID( $wikiID ); + } + + return $revision; + + } + /** * Load either the current, or a specified, revision * that's attached to a given page. If not attached @@ -402,6 +426,7 @@ class Revision { throw new MWException( 'Revision constructor passed invalid row format.' ); } $this->mUnpatrolled = null; + $this->mWikiID = false; } /** @@ -449,7 +474,8 @@ class Revision { if( isset( $this->mTitle ) ) { return $this->mTitle; } - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE, array(), $this->mWikiID ); + $row = $dbr->selectRow( array( 'page', 'revision' ), array( 'page_namespace', 'page_title' ), @@ -588,7 +614,7 @@ class Revision { if( $this->mUnpatrolled !== null ) { return $this->mUnpatrolled; } - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE, array(), $this->mWikiID ); $this->mUnpatrolled = $dbr->selectField( 'recentchanges', 'rc_id', array( // Add redundant user,timestamp condition so we can use the existing index @@ -924,7 +950,11 @@ class Revision { // Caching may be beneficial for massive use of external storage global $wgRevisionCacheExpiry, $wgMemc; $textId = $this->getTextId(); + if( isset( $this->mWikiID ) && $this->mWikiID !== false ) { + $key = wfForeignMemcKey( $this->mWikiID, null, 'revisiontext', 'textid', $textId ); + } else { $key = wfMemcKey( 'revisiontext', 'textid', $textId ); + } if( $wgRevisionCacheExpiry ) { $text = $wgMemc->get( $key ); if( is_string( $text ) ) { @@ -944,7 +974,7 @@ class Revision { if( !$row ) { // Text data is immutable; check slaves first. - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE, array(), $this->mWikiID ); $row = $dbr->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $this->getTextId() ), @@ -953,7 +983,7 @@ class Revision { if( !$row && wfGetLB()->getServerCount() > 1 ) { // Possible slave lag! - $dbw = wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER, array(), $this->mWikiID ); $row = $dbw->selectRow( 'text', array( 'old_text', 'old_flags' ), array( 'old_id' => $this->getTextId() ), @@ -1064,7 +1094,8 @@ class Revision { * @return String */ static function getTimestampFromId( $title, $id ) { - $dbr = wfGetDB( DB_SLAVE ); + $wikiId = wfWikiID(); + $dbr = wfGetDB( DB_SLAVE, array(), $wikiId ); // Casting fix for DB2 if ( $id == '' ) { $id = 0; @@ -1074,7 +1105,7 @@ class Revision { $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); if ( $timestamp === false && wfGetLB()->getServerCount() > 1 ) { # Not in slave, try master - $dbw = wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER, array(), $wikiId ); $timestamp = $dbw->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); } return wfTimestamp( TS_MW, $timestamp ); diff --git a/includes/SpecialPageFactory.php b/includes/SpecialPageFactory.php index 2b8e173f07..40a981d14c 100644 --- a/includes/SpecialPageFactory.php +++ b/includes/SpecialPageFactory.php @@ -126,6 +126,8 @@ class SpecialPageFactory { // Page tools 'ComparePages' => 'SpecialComparePages', 'Export' => 'SpecialExport', + 'GlobalFileUsage' => 'SpecialGlobalFileUsage', + 'GlobalTemplateUsage' => 'SpecialGlobalTemplateUsage', 'Import' => 'SpecialImport', 'Undelete' => 'SpecialUndelete', 'Whatlinkshere' => 'SpecialWhatlinkshere', diff --git a/includes/Title.php b/includes/Title.php index 4f61897f0a..f2099950f2 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -757,6 +757,20 @@ class Title { return $this->mPrefixedText; } + /** + * Return the prefixed title with spaces _without_ the interwiki prefix + * + * @return \type{\string} the title, prefixed by the namespace but not by the interwiki prefix, with spaces + */ + public function getSemiPrefixedText() { + if ( !isset( $this->mSemiPrefixedText ) ){ + $s = ( $this->mNamespace === NS_MAIN ? '' : $this->getNsText() . ':' ) . $this->mTextform; + $s = str_replace( '_', ' ', $s ); + $this->mSemiPrefixedText = $s; + } + return $this->mSemiPrefixedText; + } + /** * Get the prefixed title with spaces, plus any fragment * (part beginning with '#') @@ -1009,8 +1023,12 @@ class Title { * @return String the URL */ public function getInternalURL( $query = '', $variant = false ) { - global $wgInternalServer, $wgServer; - $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; + if ( $this->isExternal( ) ) { + $server = ''; + } else { + global $wgInternalServer, $wgServer; + $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; + } $url = wfExpandUrl( $server . $this->getLocalURL( $query, $variant ), PROTO_HTTP ); wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); return $url; @@ -2836,6 +2854,10 @@ class Title { $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); } + public function setInterwiki( $interwiki ) { + $this->mInterwiki = $interwiki; + } + /** * Get a Title object associated with the talk page of this article * @@ -3140,6 +3162,8 @@ class Title { * @return Mixed true on success, getUserPermissionsErrors()-like array on failure */ public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { + global $wgContLang, $wgEnableInterwikiTemplatesTracking, $wgGlobalDatabase; + $err = $this->isValidMoveOperation( $nt, $auth, $reason ); if ( is_array( $err ) ) { return $err; @@ -3196,6 +3220,15 @@ class Title { ); } + if ( $wgEnableInterwikiTemplatesTracking && $wgGlobalDatabase ) { + $dbw2 = wfGetDB( DB_MASTER, array(), $wgGlobalDatabase ); + $dbw2->update( 'globaltemplatelinks', + array( 'gtl_from_namespace' => $nt->getNamespace(), + 'gtl_from_title' => $nt->getText() ), + array ( 'gtl_from_page' => $pageid ), + __METHOD__ ); + } + if ( $protected ) { # Protect the redirect title as the title used to be... $dbw->insertSelect( 'page_restrictions', 'page_restrictions', @@ -3289,8 +3322,8 @@ class Title { * @param $createRedirect Bool Whether to leave a redirect at the old title. Ignored * if the user doesn't have the suppressredirect right */ - private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) { - global $wgUser, $wgContLang; + private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) { + global $wgUseSquid, $wgUser, $wgContLang, $wgEnableInterwikiTemplatesTracking, $wgGlobalDatabase; $moveOverRedirect = $nt->exists(); @@ -3340,6 +3373,14 @@ class Title { array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ), __METHOD__ ); + + if ( $wgEnableInterwikiTemplatesTracking && $wgGlobalDatabase ) { + $dbw2 = wfGetDB( DB_MASTER, array(), $wgGlobalDatabase ); + $dbw2->delete( 'globaltemplatelinks', + array( 'gtl_from_wiki' => wfGetID(), + 'gtl_from_page' => $newid ), + __METHOD__ ); + } } # Save a null revision in the page's history notifying of the move diff --git a/includes/WikiPage.php b/includes/WikiPage.php index dbff4d8521..047dd20236 100644 --- a/includes/WikiPage.php +++ b/includes/WikiPage.php @@ -1604,7 +1604,7 @@ class WikiPage extends Page { public function doDeleteArticle( $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null ) { - global $wgDeferredUpdateList, $wgUseTrackbacks, $wgUser; + global $wgDeferredUpdateList, $wgUseTrackbacks, $wgEnableInterwikiTemplatesTracking, $wgGlobalDatabase, $wgUser; $user = is_null( $user ) ? $wgUser : $user; wfDebug( __METHOD__ . "\n" ); @@ -1709,6 +1709,14 @@ class WikiPage extends Page { $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ ); $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ ); $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ ); + + if ( $wgEnableInterwikiTemplatesTracking && $wgGlobalDatabase ) { + $dbw2 = wfGetDB( DB_MASTER, array(), $wgGlobalDatabase ); + $dbw2->delete( 'globaltemplatelinks', + array( 'gtl_from_wiki' => wfGetID(), + 'gtl_from_page' => $id ) + ); + } } # If using cleanup triggers, we can skip some manual deletes @@ -2213,6 +2221,8 @@ class WikiPage extends Page { * @param $title Title object */ public static function onArticleCreate( $title ) { + global $wgDeferredUpdateList; + # Update existence markers on article/talk tabs... if ( $title->isTalkPage() ) { $other = $title->getSubjectPage(); @@ -2226,6 +2236,9 @@ class WikiPage extends Page { $title->touchLinks(); $title->purgeSquid(); $title->deleteTitleProtection(); + + # Invalidate caches of distant articles which transclude this page + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'globaltemplatelinks' ); } /** @@ -2234,6 +2247,8 @@ class WikiPage extends Page { * @param $title Title */ public static function onArticleDelete( $title ) { + global $wgMessageCache, $wgDeferredUpdateList; + # Update existence markers on article/talk tabs... if ( $title->isTalkPage() ) { $other = $title->getSubjectPage(); @@ -2269,6 +2284,9 @@ class WikiPage extends Page { # Image redirects RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); + + # Invalidate caches of distant articles which transclude this page + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'globaltemplatelinks' ); } /** @@ -2283,6 +2301,9 @@ class WikiPage extends Page { // Invalidate caches of articles which include this page $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); + // Invalidate caches of distant articles which transclude this page + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'globaltemplatelinks' ); + // Invalidate the caches of all pages which redirect here $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' ); @@ -2324,6 +2345,40 @@ class WikiPage extends Page { return $result; } + /** + * Return a list of distant templates used by this article. + * Uses the globaltemplatelinks table + * + * @return Array of Title objects + */ + public function getUsedDistantTemplates() { + global $wgGlobalDatabase; + + $result = array(); + + if ( $wgGlobalDatabase ) { + $id = $this->mTitle->getArticleID(); + + if ( $id == 0 ) { + return array(); + } + + $dbr = wfGetDB( DB_SLAVE, array(), $wgGlobalDatabase ); + $res = $dbr->select( 'globaltemplatelinks', + array( 'gtl_to_prefix', 'gtl_to_namespace', 'gtl_to_title' ), + array( 'gtl_from_wiki' => wfWikiID( ), 'gtl_from_page' => $id ), + __METHOD__ ); + + if ( $res !== false ) { + foreach ( $res as $row ) { + $result[] = Title::makeTitle( $row->gtl_to_namespace, $row->gtl_to_title, null, $row->gtl_to_prefix ); + } + } + } + + return $result; + } + /** * Returns a list of hidden categories this page is a member of. * Uses the page_props and categorylinks tables. diff --git a/includes/cache/HTMLCacheUpdate.php b/includes/cache/HTMLCacheUpdate.php index d542800db2..433e91cb5a 100644 --- a/includes/cache/HTMLCacheUpdate.php +++ b/includes/cache/HTMLCacheUpdate.php @@ -51,6 +51,16 @@ class HTMLCacheUpdate return; } + if ( $this->mTable === 'globaltemplatelinks' ) { + global $wgEnableInterwikiTemplatesTracking; + + if ( $wgEnableInterwikiTemplatesTracking ) { + $distantPageArray = $this->mCache->getDistantTemplateLinks( 'globaltemplatelinks' ); + $this->invalidateDistantTitles( $distantPageArray ); + } + return; + } + # Get an estimate of the number of rows from the BacklinkCache $numRows = $this->mCache->getNumLinks( $this->mTable ); if ( $numRows > $this->mRowsPerJob * 2 ) { @@ -68,6 +78,7 @@ class HTMLCacheUpdate $this->invalidateTitles( $titleArray ); } } + wfRunHooks( 'HTMLCacheUpdate::doUpdate', array($this->mTitle) ); } /** @@ -198,6 +209,44 @@ class HTMLCacheUpdate } } + /** + * Invalidate an array of distant pages, given the wiki ID and page ID of those pages + */ + protected function invalidateDistantTitles( $distantPageArray ) { + global $wgUseFileCache, $wgUseSquid, $wgLocalInterwiki; + + $pagesByWiki = array(); + $titleArray = array(); + # Sort by WikiID in $pagesByWiki + # Create the distant titles for Squid in $titleArray + foreach ( $distantPageArray as $row ) { + $wikiid = $row->gtl_from_wiki; + if( !isset( $pagesByWiki[$wikiid] ) ) { + $pagesByWiki[$wikiid] = array(); +} + $pagesByWiki[$wikiid][] = $row->gtl_from_page; + $titleArray[] = Title::makeTitle( $row->gtl_from_namespace, $row->gtl_from_title, '', $row->gil_interwiki ); + } + + foreach ( $pagesByWiki as $wikiid => $pages ) { + $dbw = wfGetDB( DB_MASTER, array( ), $wikiid ); + $timestamp = $dbw->timestamp(); + $batches = array_chunk( $pages, $this->mRowsPerQuery ); + foreach ( $batches as $batch ) { + $dbw->update( 'page', + array( 'page_touched' => $timestamp ), + array( 'page_id IN (' . $dbw->makeList( $batch ) . ')' ), + __METHOD__ + ); + } + } + + # Update squid + if ( $wgUseSquid ) { + $u = SquidUpdate::newFromTitles( $titleArray ); + $u->doUpdate(); + } + } } /** diff --git a/includes/db/Database.php b/includes/db/Database.php index be06053e94..279948fb37 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -1819,6 +1819,39 @@ abstract class DatabaseBase implements DatabaseType { } } + /** + * Build a partial where clause from a 3-d array + * The keys on each level may be either integers or strings. + * + * @param $data Array: organized as 3-d array(baseKeyVal => array(middleKeyVal => array(subKeyVal => , ...), ...), ...) + * @param $baseKey String: field name to match the base-level keys to (eg 'gtl_to_prefix') + * @param $middleKey String: field name to match the middle-level keys to (eg 'gtl_to_namespace') + * @param $subKey String: field name to match the sub-level keys to (eg 'gtl_to_title') + * @return Mixed: string SQL fragment, or false if no items in array. + */ + function makeWhereFrom3d( $data, $baseKey, $middleKey, $subKey ) { + $conds = array(); + foreach ( $data as $base => $subdata ) { + foreach ( $subdata as $middle => $sub ) { + if ( count( $sub ) ) { + $conds[] = $this->makeList( + array( $baseKey => $base, + $middleKey => $middle, + $subKey => array_keys( $sub ) ), + LIST_AND + ); + } + } + } + + if ( $conds ) { + return $this->makeList( $conds, LIST_OR ); + } else { + // Nothing to search for... + return false; + } + } + /** * Bitwise operations */ diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php index e2a6926d9f..29d2673178 100644 --- a/includes/installer/MysqlUpdater.php +++ b/includes/installer/MysqlUpdater.php @@ -185,6 +185,9 @@ class MysqlUpdater extends DatabaseUpdater { // 1.19 array( 'addTable', 'config', 'patch-config.sql' ), array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql'), + array( 'addTable', 'globaltemplatelinks', 'patch-globaltemplatelinks.sql' ), + array( 'addTable', 'globalnamespaces', 'patch-globalnamespaces.sql' ), + array( 'addTable', 'globalinterwiki', 'patch-globalinterwiki.sql' ), ); } diff --git a/includes/installer/SqliteUpdater.php b/includes/installer/SqliteUpdater.php index 2e81f6cbe6..05273853b9 100644 --- a/includes/installer/SqliteUpdater.php +++ b/includes/installer/SqliteUpdater.php @@ -63,6 +63,11 @@ class SqliteUpdater extends DatabaseUpdater { // 1.19 array( 'addTable', 'config', 'patch-config.sql' ), array( 'addIndex', 'logging', 'type_action', 'patch-logging-type-action-index.sql') + + // 1.19 + array( 'addTable', 'globaltemplatelinks', 'patch-globaltemplatelinks.sql' ), + array( 'addTable', 'globalnamespaces', 'patch-globalnamespaces.sql' ), + array( 'addTable', 'globalinterwiki', 'patch-globalinterwiki.sql' ), ); } diff --git a/includes/interwiki/Interwiki.php b/includes/interwiki/Interwiki.php index d1592c0bf0..ac9bee7657 100644 --- a/includes/interwiki/Interwiki.php +++ b/includes/interwiki/Interwiki.php @@ -9,6 +9,7 @@ * All information is loaded on creation when called by Interwiki::fetch( $prefix ). * All work is done on slave, because this should *never* change (except during * schema updates etc, which aren't wiki-related) + * This class also contains the functions that allow interwiki templates transclusion. */ class Interwiki { @@ -168,6 +169,7 @@ class Interwiki { $mc = array( 'iw_url' => $iw->mURL, 'iw_api' => $iw->mAPI, + 'iw_wikiid' => $iw->mWikiID, 'iw_local' => $iw->mLocal, 'iw_trans' => $iw->mTrans ); @@ -190,6 +192,7 @@ class Interwiki { $iw->mURL = $mc['iw_url']; $iw->mLocal = isset( $mc['iw_local'] ) ? $mc['iw_local'] : 0; $iw->mTrans = isset( $mc['iw_trans'] ) ? $mc['iw_trans'] : 0; + $iw->mAPI = isset( $mc['iw_api'] ) ? $mc['iw_api'] : $iw->mAPI = isset( $mc['iw_api'] ) ? $mc['iw_api'] : ''; $iw->mWikiID = isset( $mc['iw_wikiid'] ) ? $mc['iw_wikiid'] : ''; @@ -384,4 +387,166 @@ class Interwiki { $msg = wfMessage( 'interwiki-desc-' . $this->mPrefix )->inContentLanguage(); return !$msg->exists() ? '' : $msg; } + + + /** + * Transclude an interwiki link. + */ + public static function interwikiTransclude( $title ) { + + // If we have a wikiID, we will use it to get an access to the remote database + // if not, we will use the API URL to retrieve the data through a HTTP Get + + $wikiID = $title->getTransWikiID( ); + $transAPI = $title->getTransAPI( ); + + if ( $wikiID !== '') { + + $finalText = self::fetchTemplateFromDB( $wikiID, $title ); + return $finalText; + + } else if( $transAPI !== '' ) { + + $interwiki = $title->getInterwiki( ); + $fullTitle = $title->getSemiPrefixedText( ); + + $finalText = self::fetchTemplateFromAPI( $interwiki, $transAPI, $fullTitle ); + + return $finalText; + +} + return false; + } + + /** + * Retrieve the wikitext of a distant page accessing the foreign DB + */ + public static function fetchTemplateFromDB ( $wikiID, $title ) { + + $revision = Revision::loadFromTitleForeignWiki( $wikiID, $title ); + + if ( $revision ) { + $text = $revision->getText(); + return $text; + } + + return false; + } + + /** + * Retrieve the wikitext of a distant page using the API of the foreign wiki + */ + public static function fetchTemplateFromAPI( $interwiki, $transAPI, $fullTitle ) { + global $wgMemc, $wgTranscludeCacheExpiry; + + $key = wfMemcKey( 'iwtransclustiontext', 'textid', $interwiki, $fullTitle ); + $text = $wgMemc->get( $key ); + if( is_array ( $text ) && + isset ( $text['missing'] ) && + $text['missing'] === true ) { + return false; + } else if ( $text ) { + return $text; + } + + $url = wfAppendQuery( + $transAPI, + array( 'action' => 'query', + 'titles' => $fullTitle, + 'prop' => 'revisions', + 'rvprop' => 'content', + 'format' => 'json' + ) + ); + + $get = Http::get( $url ); + $content = FormatJson::decode( $get, true ); + + if ( isset ( $content['query'] ) && + isset ( $content['query']['pages'] ) ) { + $page = array_pop( $content['query']['pages'] ); + if ( $page && isset( $page['revisions'][0]['*'] ) ) { + $text = $page['revisions'][0]['*']; + $wgMemc->set( $key, $text, $wgTranscludeCacheExpiry ); + + // When we cache a template, we also retrieve and cache its subtemplates + $subtemplates = self::getSubtemplatesListFromAPI( $interwiki, $transAPI, $fullTitle ); + self::cacheTemplatesFromAPI( $interwiki, $transAPI, $subtemplates ); + + return $text; + } else { + $wgMemc->set( $key, array ( 'missing' => true ), $wgTranscludeCacheExpiry ); + } + } + return false; + } + + public static function getSubtemplatesListFromAPI ( $interwiki, $transAPI, $title ) { + $url = wfAppendQuery( $transAPI, + array( 'action' => 'query', + 'titles' => $title, + 'prop' => 'templates', + 'format' => 'json' + ) + ); + + $get = Http::get( $url ); + $myArray = FormatJson::decode($get, true); + + $templates = array( ); + if ( ! empty( $myArray['query'] )) { + if ( ! empty( $myArray['query']['pages'] )) { + $templates = array_pop( $myArray['query']['pages'] ); + if ( ! empty( $templates['templates'] )) { + $templates = $templates['templates']; + } + } + return $templates; + } + } + + public static function cacheTemplatesFromAPI( $interwiki, $transAPI, $titles ){ + global $wgMemc, $wgTranscludeCacheExpiry; + + $outdatedTitles = array( ); + + foreach( $titles as $title ){ + if ( isset ( $title['title'] ) ) { + $key = wfMemcKey( 'iwtransclustiontext', 'textid', $interwiki, $title['title'] ); + $text = $wgMemc->get( $key ); + if( !$text ){ + $outdatedTitles[] = $title['title']; + } + } + } + + $batches = array_chunk( $outdatedTitles, 50 ); + + foreach( $batches as $batch ){ + $url = wfAppendQuery( + $transAPI, + array( 'action' => 'query', + 'titles' => implode( '|', $batch ), + 'prop' => 'revisions', + 'rvprop' => 'content', + 'format' => 'json' + ) + ); + $get = Http::get( $url ); + $content = FormatJson::decode( $get, true ); + + if ( isset ( $content['query'] ) && + isset ( $content['query']['pages'] ) ) { + foreach( $content['query']['pages'] as $page ) { + $key = wfMemcKey( 'iwtransclustiontext', 'textid', $interwiki, $page['title'] ); + if ( isset ( $page['revisions'][0]['*'] ) ) { + $text = $page['revisions'][0]['*']; + } else { + $text = array ( 'missing' => true ); + } + $wgMemc->set( $key, $text, $wgTranscludeCacheExpiry ); + } + } + } + } } diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index 3233fbf6b7..77749726ae 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -3140,7 +3140,7 @@ class Parser { * @private */ function braceSubstitution( $piece, $frame ) { - global $wgContLang, $wgNonincludableNamespaces; + global $wgContLang, $wgNonincludableNamespaces, $wgEnableInterwikiTranscluding, $wgEnableInterwikiTemplatesTracking; wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__.'-setup' ); @@ -3148,7 +3148,6 @@ class Parser { $found = false; # $text has been filled $nowiki = false; # wiki markup in $text should be escaped $isHTML = false; # $text is HTML, armour it against wikitext transformation - $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered $isChildObj = false; # $text is a DOM node needing expansion in a child frame $isLocalObj = false; # $text is a DOM node needing expansion in the current frame @@ -3313,6 +3312,9 @@ class Parser { } $title = Title::newFromText( $part1, $ns ); if ( $title ) { + if ( !$title->isExternal() && $piece['interwiki'] !== '' ) { + $title->setInterwiki( $piece['interwiki'] ); + } $titleText = $title->getPrefixedText(); # Check for language variants if the template is not found if ( $wgContLang->hasVariants() && $title->getArticleID() == 0 ) { @@ -3375,18 +3377,22 @@ class Parser { $text = "[[:$titleText]]"; $found = true; } - } elseif ( $title->isTrans() ) { - # Interwiki transclusion - if ( $this->ot['html'] && !$forceRawInterwiki ) { - $text = $this->interwikiTransclude( $title, 'render' ); - $isHTML = true; - } else { - $text = $this->interwikiTransclude( $title, 'raw' ); + } elseif ( $wgEnableInterwikiTranscluding && $title->isTrans() ) { + + $text = Interwiki::interwikiTransclude( $title ); + $this->registerDistantTemplate( $title ); + + if ( $wgEnableInterwikiTemplatesTracking ) { + $this->registerDistantTemplate( $title ); + } + + if ( $text !== false ) { # Preprocess it like a template $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); + $found = true; $isChildObj = true; } - $found = true; + } # Do infinite loop check @@ -3536,10 +3542,19 @@ class Parser { } /** - * Fetch the unparsed text of a template and register a reference to it. - * @param Title $title - * @return mixed string or false + * Register a distant template as used */ + function registerDistantTemplate( $title ) { + $stuff = Parser::distantTemplateCallback( $title, $this ); + $text = $stuff['text']; + $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title; + if ( isset( $stuff['deps'] ) ) { + foreach ( $stuff['deps'] as $dep ) { + $this->mOutput->addDistantTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); + } + } + } + function fetchTemplate( $title ) { $rv = $this->fetchTemplateAndTitle( $title ); return $rv[0]; @@ -3668,57 +3683,22 @@ class Parser { return array( $file, $title ); } - /** - * Transclude an interwiki link. - * - * @param $title Title - * @param $action - * - * @return string - */ - function interwikiTransclude( $title, $action ) { - global $wgEnableScaryTranscluding; - - if ( !$wgEnableScaryTranscluding ) { - return wfMsgForContent('scarytranscludedisabled'); - } - - $url = $title->getFullUrl( "action=$action" ); + static function distantTemplateCallback( $title, $parser=false ) { + $text = ''; + $rev_id = null; + $deps[] = array( + 'title' => $title, + 'page_id' => $title->getArticleID(), + 'rev_id' => $rev_id ); - if ( strlen( $url ) > 255 ) { - return wfMsgForContent( 'scarytranscludetoolong' ); - } - return $this->fetchScaryTemplateMaybeFromCache( $url ); - } - - /** - * @param $url string - * @return Mixed|String - */ - function fetchScaryTemplateMaybeFromCache( $url ) { - global $wgTranscludeCacheExpiry; - $dbr = wfGetDB( DB_SLAVE ); - $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry ); - $obj = $dbr->selectRow( 'transcache', array('tc_time', 'tc_contents' ), - array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) ); - if ( $obj ) { - return $obj->tc_contents; - } + $finalTitle = $title; - $text = Http::get( $url ); - if ( !$text ) { - return wfMsgForContent( 'scarytranscludefailed', $url ); + return array( + 'text' => $text, + 'finalTitle' => $finalTitle, + 'deps' => $deps ); } - $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'transcache', array('tc_url'), array( - 'tc_url' => $url, - 'tc_time' => $dbw->timestamp( time() ), - 'tc_contents' => $text) - ); - return $text; - } - /** * Triple brace replacement -- used for template arguments * @private diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php index c643d75617..045a85250c 100644 --- a/includes/parser/ParserOutput.php +++ b/includes/parser/ParserOutput.php @@ -121,6 +121,8 @@ class ParserOutput extends CacheTime { $mLinks = array(), # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken. $mTemplates = array(), # 2-D map of NS/DBK to ID for the template references. ID=zero for broken. $mTemplateIds = array(), # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken. + $mDistantTemplates = array(), # 3-D map of WIKIID/NS/DBK to ID for the template references. ID=zero for broken. + $mDistantTemplateIds = array(), # 3-D map of WIKIID/NS/DBK to rev ID for the template references. ID=zero for broken. $mImages = array(), # DB keys of the images used, in the array key only $mImageTimeKeys = array(), # DB keys of the images used mapped to sha1 and MW timestamp $mExternalLinks = array(), # External link URLs, in the key only @@ -191,6 +193,8 @@ class ParserOutput extends CacheTime { function getEditSectionTokens() { return $this->mEditSectionTokens; } function &getLinks() { return $this->mLinks; } function &getTemplates() { return $this->mTemplates; } + function &getDistantTemplates() { return $this->mDistantTemplates; } + function &getDistantTemplateIds() { return $this->mDistantTemplateIds; } function &getTemplateIds() { return $this->mTemplateIds; } function &getImages() { return $this->mImages; } function &getImageTimeKeys() { return $this->mImageTimeKeys; } @@ -312,6 +316,31 @@ class ParserOutput extends CacheTime { $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning } + function addDistantTemplate( $title, $page_id, $rev_id ) { + $prefix = $title->getInterwiki(); + if ( $prefix !=='' ) { + $ns = $title->getNamespace(); + $dbk = $title->getDBkey(); + + if ( !isset( $this->mDistantTemplates[$prefix] ) ) { + $this->mDistantTemplates[$prefix] = array(); + } + if ( !isset( $this->mDistantTemplates[$prefix][$ns] ) ) { + $this->mDistantTemplates[$prefix][$ns] = array(); + } + $this->mDistantTemplates[$prefix][$ns][$dbk] = $page_id; + + // For versioning + if ( !isset( $this->mDistantTemplateIds[$prefix] ) ) { + $this->mDistantTemplateIds[$prefix] = array(); + } + if ( !isset( $this->mDistantTemplateIds[$prefix][$ns] ) ) { + $this->mDistantTemplateIds[$prefix][$ns] = array(); + } + $this->mDistantTemplateIds[$prefix][$ns][$dbk] = $rev_id; + } + } + /** * @param $title Title object, must be an interwiki link * @throws MWException if given invalid input diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php index 5b79876bea..dcda8a211f 100644 --- a/includes/parser/Preprocessor_DOM.php +++ b/includes/parser/Preprocessor_DOM.php @@ -1038,7 +1038,8 @@ class PPFrame_DOM implements PPFrame { $params = array( 'title' => new PPNode_DOM( $title ), 'parts' => new PPNode_DOM( $parts ), - 'lineStart' => $lineStart ); + 'lineStart' => $lineStart, + 'interwiki' => $this->title->getInterwiki( ) ); $ret = $this->parser->braceSubstitution( $params, $this ); if ( isset( $ret['object'] ) ) { $newIterator = $ret['object']; diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php index c2d7d3d8de..50af6b6b63 100644 --- a/includes/parser/Preprocessor_Hash.php +++ b/includes/parser/Preprocessor_Hash.php @@ -976,6 +976,7 @@ class PPFrame_Hash implements PPFrame { if ( $flags & PPFrame::NO_TEMPLATES ) { $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] ); } else { + $bits['interwiki'] = $this->title->getInterwiki( ); $ret = $this->parser->braceSubstitution( $bits, $this ); if ( isset( $ret['object'] ) ) { $newIterator = $ret['object']; diff --git a/includes/specials/SpecialGlobalFileUsage.php b/includes/specials/SpecialGlobalFileUsage.php new file mode 100644 index 0000000000..e376ea6821 --- /dev/null +++ b/includes/specials/SpecialGlobalFileUsage.php @@ -0,0 +1,224 @@ +getVal( 'target' ); + $this->target = Title::makeTitleSafe( NS_FILE, $target ); + + $this->filterLocal = $wgRequest->getCheck( 'filterlocal' ); + + $this->setHeaders(); + + $this->showForm(); + + if ( is_null( $this->target ) ) { + $wgOut->setPageTitle( wfMsg( 'globalfileusage' ) ); + return; + } + + $wgOut->setPageTitle( wfMsg( 'globalfileusage-for', $this->target->getPrefixedText() ) ); + + $this->showResult(); + } + + /** + * Shows the search form + */ + private function showForm() { + global $wgScript, $wgOut, $wgRequest; + + /* Build form */ + $html = Xml::openElement( 'form', array( 'action' => $wgScript ) ) . "\n"; + // Name of SpecialPage + $html .= Xml::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; + // Limit + $html .= Xml::hidden( 'limit', $wgRequest->getInt( 'limit', 50 ) ); + // Input box with target prefilled if available + $formContent = "\t" . Xml::input( 'target', 40, is_null( $this->target ) ? '' + : $this->target->getText() ) + // Submit button + . "\n\t" . Xml::element( 'input', array( + 'type' => 'submit', + 'value' => wfMsg( 'globalfileusage-ok' ) + ) ) + // Filter local checkbox + . "\n\t

" . Xml::checkLabel( wfMsg( 'globalfileusage-filterlocal' ), + 'filterlocal', 'mw-filterlocal', $this->filterLocal ) . '

'; + + if ( !is_null( $this->target ) && wfFindFile( $this->target ) ) { + // Show the image if it exists + global $wgUser, $wgContLang; + $skin = $wgUser->getSkin(); + + $html .= $skin->makeImageLinkObj( $this->target, + $this->target->getPrefixedText(), + /* $alt */ '', /* $align */ $wgContLang->alignEnd(), + /* $handlerParams */ array(), /* $framed */ false, + /* $thumb */ true ); + } + + // Wrap the entire form in a nice fieldset + $html .= Xml::fieldSet( wfMsg( 'globalfileusage-text' ), $formContent ) . "\n"; + + $wgOut->addHtml( $html ); + } + + /** + * Creates as queryer and executes it based on $wgRequest + */ + private function showResult() { + global $wgRequest; + + $query = new GlobalUsageQuery( $this->target ); + + // Extract params from $wgRequest + if ( $wgRequest->getText( 'from' ) ) { + $query->setOffset( $wgRequest->getText( 'from' ) ); + } elseif ( $wgRequest->getText( 'to' ) ) { + $query->setOffset( $wgRequest->getText( 'to' ), true ); + } + $query->setLimit( $wgRequest->getInt( 'limit', 50 ) ); + $query->filterLocal( $this->filterLocal ); + + // Perform query + $query->searchFile(); + + // Show result + global $wgOut; + + // Don't show form element if there is no data + if ( $query->count() == 0 ) { + $wgOut->addWikiMsg( 'globalfileusage-no-results', $this->target->getPrefixedText() ); + return; + } + + $offset = $query->getOffsetString(); + $navbar = $this->getNavBar( $query ); + $targetName = $this->target->getText(); + + // Top navbar + $wgOut->addHtml( $navbar ); + + $wgOut->addHtml( '
' ); + foreach ( $query->getSingleResult() as $wiki => $result ) { + $wgOut->addHtml( + '

' . wfMsgExt( + 'globalfileusage-on-wiki', 'parseinline', + $targetName, WikiMap::getWikiName( $wiki ) ) + . "

    \n" ); + foreach ( $result as $item ) { + $wgOut->addHtml( "\t
  • " . self::formatItem( $item ) . "
  • \n" ); + } + $wgOut->addHtml( "
\n" ); + } + $wgOut->addHtml( '
' ); + + // Bottom navbar + $wgOut->addHtml( $navbar ); + } + /** + * Helper to format a specific item + */ + public static function formatItem( $item ) { + if ( !$item['namespace'] ) { + $page = $item['title']; + } else { + $page = "{$item['namespace']}:{$item['title']}"; + } + + $link = WikiMap::makeForeignLink( $item['wiki'], $page, + str_replace( '_', ' ', $page ) ); + // Return only the title if no link can be constructed + return $link === false ? $page : $link; + } + + /** + * Helper function to create the navbar, stolen from wfViewPrevNext + * + * @param $query GlobalUsageQuery An executed GlobalUsageQuery object + * @return string Navbar HTML + */ + protected function getNavBar( $query ) { + global $wgLang, $wgUser; + + $skin = $wgUser->getSkin(); + + $target = $this->target->getText(); + $limit = $query->getLimit(); + $fmtLimit = $wgLang->formatNum( $limit ); + + # Find out which strings are for the prev and which for the next links + $offset = $query->getOffsetString(); + $continue = $query->getContinueFileString(); + if ( $query->isReversed() ) { + $from = $offset; + $to = $continue; + } else { + $from = $continue; + $to = $offset; + } + + # Get prev/next link display text + $prev = wfMsgExt( 'prevn', array( 'parsemag', 'escape' ), $fmtLimit ); + $next = wfMsgExt( 'nextn', array( 'parsemag', 'escape' ), $fmtLimit ); + # Get prev/next link title text + $pTitle = wfMsgExt( 'prevn-title', array( 'parsemag', 'escape' ), $fmtLimit ); + $nTitle = wfMsgExt( 'nextn-title', array( 'parsemag', 'escape' ), $fmtLimit ); + + # Fetch the title object + $title = $this->getTitle(); + + # Make 'previous' link + if ( $to ) { + $attr = array( 'title' => $pTitle, 'class' => 'mw-prevlink' ); + $q = array( 'limit' => $limit, 'to' => $to, 'target' => $target ); + if ( $this->filterLocal ) + $q['filterlocal'] = '1'; + $plink = $skin->link( $title, $prev, $attr, $q ); + } else { + $plink = $prev; + } + + # Make 'next' link + if ( $from ) { + $attr = array( 'title' => $nTitle, 'class' => 'mw-nextlink' ); + $q = array( 'limit' => $limit, 'from' => $from, 'target' => $target ); + if ( $this->filterLocal ) + $q['filterlocal'] = '1'; + $nlink = $skin->link( $title, $next, $attr, $q ); + } else { + $nlink = $next; + } + + # Make links to set number of items per page + $numLinks = array(); + foreach ( array( 20, 50, 100, 250, 500 ) as $num ) { + $fmtLimit = $wgLang->formatNum( $num ); + + $q = array( 'offset' => $offset, 'limit' => $num, 'target' => $target ); + if ( $this->filterLocal ) + $q['filterlocal'] = '1'; + $lTitle = wfMsgExt( 'shown-title', array( 'parsemag', 'escape' ), $num ); + $attr = array( 'title' => $lTitle, 'class' => 'mw-numlink' ); + + $numLinks[] = $skin->link( $title, $fmtLimit, $attr, $q ); + } + $nums = $wgLang->pipeList( $numLinks ); + + return wfMsgHtml( 'viewprevnext', $plink, $nlink, $nums ); + } +} + diff --git a/includes/specials/SpecialGlobalTemplateUsage.php b/includes/specials/SpecialGlobalTemplateUsage.php new file mode 100644 index 0000000000..5f7e8aea1f --- /dev/null +++ b/includes/specials/SpecialGlobalTemplateUsage.php @@ -0,0 +1,207 @@ + + * @author Peter Potrowl + */ + +class SpecialGlobalTemplateUsage extends SpecialPage { + public function __construct() { + parent::__construct( 'GlobalTemplateUsage' ); + } + + /** + * Entry point + */ + public function execute( $par ) { + global $wgOut, $wgRequest; + + $target = $par ? $par : $wgRequest->getVal( 'target' ); + $this->target = Title::newFromText( $target ); + + $this->setHeaders(); + + $this->showForm(); + + if ( is_null( $this->target ) ) { + $wgOut->setPageTitle( wfMsg( 'globaltemplateusage' ) ); + return; + } + + $wgOut->setPageTitle( wfMsg( 'globaltemplateusage-for', $this->target->getPrefixedText() ) ); + + $this->showResult(); + } + + /** + * Shows the search form + */ + private function showForm() { + global $wgScript, $wgOut, $wgRequest; + + /* Build form */ + $html = Xml::openElement( 'form', array( 'action' => $wgScript ) ) . "\n"; + // Name of SpecialPage + $html .= Xml::hidden( 'title', $this->getTitle( )->getPrefixedText( ) ) . "\n"; + // Limit + $html .= Xml::hidden( 'limit', $wgRequest->getInt( 'limit', 50 ) ); + // Input box with target prefilled if available + $formContent = "\t" . Xml::input( 'target', 40, is_null( $this->target ) ? '' + : $this->target->getPrefixedText( ) ) + // Submit button + . "\n\t" . Xml::element( 'input', array( + 'type' => 'submit', + 'value' => wfMsg( 'globaltemplateusage-ok' ) + ) ); + + // Wrap the entire form in a nice fieldset + $html .= Xml::fieldSet( wfMsg( 'globaltemplateusage-text' ), $formContent ) . "\n"; + + $wgOut->addHtml( $html ); + } + + /** + * Creates as queryer and executes it based on $wgRequest + */ + private function showResult() { + global $wgRequest; + + $query = new GlobalUsageQuery( $this->target ); + + // Extract params from $wgRequest + if ( $wgRequest->getText( 'from' ) ) { + $query->setOffset( $wgRequest->getText( 'from' ) ); + } elseif ( $wgRequest->getText( 'to' ) ) { + $query->setOffset( $wgRequest->getText( 'to' ), true ); + } + $query->setLimit( $wgRequest->getInt( 'limit', 50 ) ); + + // Perform query + $query->searchTemplate(); + + // Show result + global $wgOut; + + // Don't show form element if there is no data + if ( $query->count() == 0 ) { + $wgOut->addWikiMsg( 'globaltemplateusage-no-results', $this->target->getPrefixedText( ) ); + return; + } + + $offset = $query->getOffsetString( ); + $navbar = $this->getNavBar( $query ); + $targetName = $this->target->getPrefixedText( ); + + // Top navbar + $wgOut->addHtml( $navbar ); + + $wgOut->addHtml( '
' ); + foreach ( $query->getSingleResult() as $wiki => $result ) { + $wgOut->addHtml( + '

' . wfMsgExt( + 'globaltemplateusage-on-wiki', 'parseinline', + $targetName, WikiMap::getWikiName( $wiki ) ) + . "

    \n" ); + foreach ( $result as $item ) { + $wgOut->addHtml( "\t
  • " . self::formatItem( $item ) . "
  • \n" ); + } + $wgOut->addHtml( "
\n" ); + } + $wgOut->addHtml( '
' ); + + // Bottom navbar + $wgOut->addHtml( $navbar ); + } + + /** + * Helper to format a specific item + */ + public static function formatItem( $item ) { + if ( !$item['namespace'] ) { + $page = $item['title']; + } else { + $page = "{$item['namespace']}:{$item['title']}"; + } + + $link = WikiMap::makeForeignLink( $item['wiki'], $page, + str_replace( '_', ' ', $page ) ); + // Return only the title if no link can be constructed + return $link === false ? $page : $link; + } + + /** + * Helper function to create the navbar, stolen from wfViewPrevNext + * + * @param $query GlobalTemplateUsageQuery An executed GlobalTemplateUsageQuery object + * @return string Navbar HTML + */ + protected function getNavBar( $query ) { + global $wgLang, $wgUser; + + $skin = $wgUser->getSkin(); + + $target = $this->target->getPrefixedText(); + $limit = $query->getLimit(); + $fmtLimit = $wgLang->formatNum( $limit ); + + # Find out which strings are for the prev and which for the next links + $offset = $query->getOffsetString(); + $continue = $query->getContinueTemplateString(); + if ( $query->isReversed() ) { + $from = $offset; + $to = $continue; + } else { + $from = $continue; + $to = $offset; + } + + # Get prev/next link display text + $prev = wfMsgExt( 'prevn', array( 'parsemag', 'escape' ), $fmtLimit ); + $next = wfMsgExt( 'nextn', array( 'parsemag', 'escape' ), $fmtLimit ); + # Get prev/next link title text + $pTitle = wfMsgExt( 'prevn-title', array( 'parsemag', 'escape' ), $fmtLimit ); + $nTitle = wfMsgExt( 'nextn-title', array( 'parsemag', 'escape' ), $fmtLimit ); + + # Fetch the title object + $title = $this->getTitle(); + + # Make 'previous' link + if ( $to ) { + $attr = array( 'title' => $pTitle, 'class' => 'mw-prevlink' ); + $q = array( 'limit' => $limit, 'to' => $to, 'target' => $target ); + $plink = $skin->link( $title, $prev, $attr, $q ); + } else { + $plink = $prev; + } + + # Make 'next' link + if ( $from ) { + $attr = array( 'title' => $nTitle, 'class' => 'mw-nextlink' ); + $q = array( 'limit' => $limit, 'from' => $from, 'target' => $target ); + $nlink = $skin->link( $title, $next, $attr, $q ); + } else { + $nlink = $next; + } + + # Make links to set number of items per page + $numLinks = array(); + foreach ( array( 20, 50, 100, 250, 500 ) as $num ) { + $fmtLimit = $wgLang->formatNum( $num ); + + $q = array( 'offset' => $offset, 'limit' => $num, 'target' => $target ); + $lTitle = wfMsgExt( 'shown-title', array( 'parsemag', 'escape' ), $num ); + $attr = array( 'title' => $lTitle, 'class' => 'mw-numlink' ); + + $numLinks[] = $skin->link( $title, $fmtLimit, $attr, $q ); + } + $nums = $wgLang->pipeList( $numLinks ); + + return wfMsgHtml( 'viewprevnext', $plink, $nlink, $nums ); + } +} + + diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 22a153ca99..83bc114163 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -458,6 +458,8 @@ $specialPageAliases = array( 'Watchlist' => array( 'Watchlist' ), 'Whatlinkshere' => array( 'WhatLinksHere' ), 'Withoutinterwiki' => array( 'WithoutInterwiki' ), + 'Globalfileusage' => array( 'GlobalFileUsage' ), + 'Globaltemplateusage' => array( 'GlobalTemplateUsage' ), ); /** @@ -1402,6 +1404,9 @@ The latest log entry is provided below for reference:", 'templatesused' => '{{PLURAL:$1|Template|Templates}} used on this page:', 'templatesusedpreview' => '{{PLURAL:$1|Template|Templates}} used in this preview:', 'templatesusedsection' => '{{PLURAL:$1|Template|Templates}} used in this section:', +'distanttemplatesused' => 'Distant {{PLURAL:$1|template|templates}} used on this page:', +'distanttemplatesusedpreview' => 'Distant {{PLURAL:$1|template|templates}} used in this preview:', +'distanttemplatesusedsection' => 'Distant {{PLURAL:$1|template|templates}} used in this section:', 'template-protected' => '(protected)', 'template-semiprotected' => '(semi-protected)', 'hiddencategories' => 'This page is a member of {{PLURAL:$1|1 hidden category|$1 hidden categories}}:', @@ -4582,6 +4587,30 @@ Enter the file name without the "{{ns:file}}:" prefix.', 'compare-title-not-exists' => 'The title you specified does not exist.', 'compare-revision-not-exists' => 'The revision you specified does not exist.', +# Special:GlobalFileUsage +'globalfileusage' => 'Global file usage', +'globalfileusage-for' => 'Global file usage for "$1"', +'globalfileusage-desc' => '[[Special:GlobalFileUsage|Special page]] to view global file usage', +'globalfileusage-ok' => 'Search', +'globalfileusage-text' => 'Search global file usage', +'globalfileusage-no-results' => '[[$1]] is not used on other wikis.', +'globalfileusage-on-wiki' => 'Usage on $2', +'globalfileusage-of-file' => 'The following other wikis use this file:', +'globalfileusage-more' => 'View [[{{#Special:GlobalUsage}}/$1|more global usage]] of this file.', +'globalfileusage-filterlocal' => 'Do not show local usage', + +# Special:GlobalTemplateUsage +'globaltemplateusage' => 'Global template usage', +'globaltemplateusage-for' => 'Global template usage for "$1"', +'globaltemplateusage-desc' => '[[Special:GlobalTemplateUsage|Special page]] to view global template usage', +'globaltemplateusage-ok' => 'Search', +'globaltemplateusage-text' => 'Search global template usage', +'globaltemplateusage-no-results' => '[[$1]] is not used on other wikis.', +'globaltemplateusage-on-wiki' => 'Usage on $2', +'globaltemplateusage-of-file' => 'The following other wikis use this template:', +'globaltemplateusage-more' => 'View [[{{#Special:GlobalUsage}}/$1|more global usage]] of this template.', +'globaltemplateusage-filterlocal' => 'Do not show local usage', + # Database error messages 'dberr-header' => 'This wiki has a problem', 'dberr-problems' => 'Sorry! diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index f021f8e09d..a1d5fdef0c 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -4259,6 +4259,16 @@ Used on [[Special:Tags]]. Verb. Used as display text on a link to create/edit a {{Identical|Other}}', +# Special:GlobalFileUsage +'globalfileusage-for' => '$1 is a file name', +'globalfileusage-no-results' => '$1 is a file name', +'globalfileusage-on-wiki' => '$2 is a wiki name', + +# Special:GlobalTemplateUsage +'globaltemplateusage-for' => '$1 is a template name', +'globaltemplateusage-no-results' => '$1 is a template name', +'globaltemplateusage-on-wiki' => '$2 is a wiki name', + # SQLite database support 'sqlite-has-fts' => 'Shown on Special:Version, $1 is version', 'sqlite-no-fts' => 'Shown on Special:Version, $1 is version', diff --git a/maintenance/archives/patch-globalinterwiki.sql b/maintenance/archives/patch-globalinterwiki.sql new file mode 100644 index 0000000000..6571a5aff2 --- /dev/null +++ b/maintenance/archives/patch-globalinterwiki.sql @@ -0,0 +1,10 @@ +-- Table associating distant wiki IDs with their interwiki prefixes. +CREATE TABLE /*_*/globalinterwiki ( + -- The wiki ID of the wiki + giw_wikiid varchar(64) NOT NULL, + + -- The interwiki prefix of that wiki + giw_prefix varchar(32) NOT NULL + +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/giw_index ON /*_*/globalinterwiki (giw_wikiid, giw_prefix); diff --git a/maintenance/archives/patch-globalnamespaces.sql b/maintenance/archives/patch-globalnamespaces.sql new file mode 100644 index 0000000000..91b3578f6a --- /dev/null +++ b/maintenance/archives/patch-globalnamespaces.sql @@ -0,0 +1,14 @@ +-- Table listing distant wiki namespace texts. +CREATE TABLE /*_*/globalnamespaces ( + -- The wiki ID of the remote wiki + gn_wiki varchar(64) NOT NULL, + + -- The namespace ID of the transcluded page on that wiki + gn_namespace int NOT NULL, + + -- The namespace text of transcluded page + -- Needed for display purposes, since the local namespace ID doesn't necessarily match a distant one + gn_namespacetext varchar(255) NOT NULL + +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/gn_index ON /*_*/globalnamespaces (gn_wiki, gn_namespace, gn_namespacetext); diff --git a/maintenance/archives/patch-globaltemplatelinks.sql b/maintenance/archives/patch-globaltemplatelinks.sql new file mode 100644 index 0000000000..d2da21bd82 --- /dev/null +++ b/maintenance/archives/patch-globaltemplatelinks.sql @@ -0,0 +1,36 @@ +-- Table tracking interwiki transclusions in the spirit of templatelinks. +-- This table tracks transclusions of this wiki's templates on another wiki +-- The gtl_from_* fields describe the (remote) page the template is transcluded from +-- The gtl_to_* fields describe the (local) template being transcluded +CREATE TABLE /*_*/globaltemplatelinks ( + -- The wiki ID of the remote wiki + gtl_from_wiki varchar(64) NOT NULL, + + -- The page ID of the calling page on the remote wiki + gtl_from_page int unsigned NOT NULL, + + -- The namespace of the calling page on the remote wiki + -- Needed for display purposes, since the foreign namespace ID doesn't necessarily match a local one + -- The link between the namespace and the namespace name is made by the globalnamespaces table + gtl_from_namespace int NOT NULL, + + -- The title of the calling page on the remote wiki + -- Needed for display purposes + gtl_from_title varchar(255) binary NOT NULL, + + -- The interwiki prefix of the wiki that hosts the transcluded page + gtl_to_prefix varchar(32) NOT NULL, + + -- The namespace of the transcluded page on that wiki + gtl_to_namespace int NOT NULL, + + -- The namespace name of transcluded page + -- Needed for display purposes, since the local namespace ID doesn't necessarily match a distant one + gtl_to_namespacetext varchar(255) NOT NULL, + + -- The title of the transcluded page on that wiki + gtl_to_title varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/gtl_to_from ON /*_*/globaltemplatelinks (gtl_to_prefix, gtl_to_namespace, gtl_to_title, gtl_from_wiki, gtl_from_page); +CREATE UNIQUE INDEX /*i*/gtl_from_to ON /*_*/globaltemplatelinks (gtl_from_wiki, gtl_from_page, gtl_to_prefix, gtl_to_namespace, gtl_to_title); diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index 2d89268798..ed0ad65062 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -640,6 +640,9 @@ $wgMessageStructure = array( 'templatesused', 'templatesusedpreview', 'templatesusedsection', + 'distanttemplatesused', + 'distanttemplatesusedpreview', + 'distanttemplatesusedsection', 'template-protected', 'template-semiprotected', 'hiddencategories', diff --git a/maintenance/tables.sql b/maintenance/tables.sql index 2ab431f49a..556fd54d8c 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -1480,4 +1480,68 @@ CREATE TABLE /*_*/config ( -- Should cover *most* configuration - strings, ints, bools, etc. CREATE INDEX /*i*/cf_name_value ON /*_*/config (cf_name,cf_value(255)); +-- Table tracking interwiki transclusions in the spirit of templatelinks. +-- This table tracks transclusions of this wiki's templates on another wiki +-- The gtl_from_* fields describe the (remote) page the template is transcluded from +-- The gtl_to_* fields describe the (local) template being transcluded +CREATE TABLE /*_*/globaltemplatelinks ( + -- The wiki ID of the remote wiki + gtl_from_wiki varchar(64) NOT NULL, + + -- The page ID of the calling page on the remote wiki + gtl_from_page int unsigned NOT NULL, + + -- The namespace of the calling page on the remote wiki + -- Needed for display purposes, since the foreign namespace ID doesn't necessarily match a local one + -- The link between the namespace and the namespace name is made by the globalnamespaces table + gtl_from_namespace int NOT NULL, + + -- The title of the calling page on the remote wiki + -- Needed for display purposes + gtl_from_title varchar(255) binary NOT NULL, + + -- The interwiki prefix of the wiki that hosts the transcluded page + gtl_to_prefix varchar(32) NOT NULL, + + -- The namespace of the transcluded page on that wiki + gtl_to_namespace int NOT NULL, + + -- The namespace name of transcluded page + -- Needed for display purposes, since the local namespace ID doesn't necessarily match a distant one + gtl_to_namespacetext varchar(255) NOT NULL, + + -- The title of the transcluded page on that wiki + gtl_to_title varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/gtl_to_from ON /*_*/globaltemplatelinks (gtl_to_prefix, gtl_to_namespace, gtl_to_title, gtl_from_wiki, gtl_from_page); +CREATE UNIQUE INDEX /*i*/gtl_from_to ON /*_*/globaltemplatelinks (gtl_from_wiki, gtl_from_page, gtl_to_prefix, gtl_to_namespace, gtl_to_title); + +-- Table listing distant wiki namespace texts. +CREATE TABLE /*_*/globalnamespaces ( + -- The wiki ID of the remote wiki + gn_wiki varchar(64) NOT NULL, + + -- The namespace ID of the transcluded page on that wiki + gn_namespace int NOT NULL, + + -- The namespace text of transcluded page + -- Needed for display purposes, since the local namespace ID doesn't necessarily match a distant one + gn_namespacetext varchar(255) NOT NULL + +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/gn_index ON /*_*/globalnamespaces (gn_wiki, gn_namespace, gn_namespacetext); + +-- Table associating distant wiki IDs with their interwiki prefixes. +CREATE TABLE /*_*/globalinterwiki ( + -- The wiki ID of the wiki + giw_wikiid varchar(64) NOT NULL, + + -- The interwiki prefix of that wiki + giw_prefix varchar(32) NOT NULL + +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/giw_index ON /*_*/globalinterwiki (giw_wikiid, giw_prefix); + + -- vim: sw=2 sts=2 et -- 2.20.1