Merge the iwtransclusion branch back into trunk
authorSam Reed <reedy@users.mediawiki.org>
Wed, 24 Aug 2011 13:03:03 +0000 (13:03 +0000)
committerSam Reed <reedy@users.mediawiki.org>
Wed, 24 Aug 2011 13:03:03 +0000 (13:03 +0000)
Hexmode fixed broken unit tests in revisions after last time

30 files changed:
includes/AutoLoader.php
includes/BacklinkCache.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalUsageQuery.php [new file with mode: 0644]
includes/Linker.php
includes/LinksUpdate.php
includes/OutputPage.php
includes/Revision.php
includes/SpecialPageFactory.php
includes/Title.php
includes/WikiPage.php
includes/cache/HTMLCacheUpdate.php
includes/db/Database.php
includes/installer/MysqlUpdater.php
includes/installer/SqliteUpdater.php
includes/interwiki/Interwiki.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
includes/parser/Preprocessor_DOM.php
includes/parser/Preprocessor_Hash.php
includes/specials/SpecialGlobalFileUsage.php [new file with mode: 0644]
includes/specials/SpecialGlobalTemplateUsage.php [new file with mode: 0644]
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/archives/patch-globalinterwiki.sql [new file with mode: 0644]
maintenance/archives/patch-globalnamespaces.sql [new file with mode: 0644]
maintenance/archives/patch-globaltemplatelinks.sql [new file with mode: 0644]
maintenance/language/messages.inc
maintenance/tables.sql

index bd3d9d0..62da731 100644 (file)
@@ -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',
index 2263051..978abec 100644 (file)
@@ -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] ) ) {
index 83078bc..d9e0de7 100644 (file)
@@ -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',
index 28032da..20a1f48 100644 (file)
@@ -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
 <div class='templatesUsed'>
 {$formattedtemplates}
 </div>
+HTML
+);
+
+               if ( $wgEnableInterwikiTranscluding && $wgEnableInterwikiTemplatesTracking ) {
+                                       $wgOut->addHTML( <<<HTML
+{$this->editFormTextAfterTools}
+<div class='distantTemplatesUsed'>
+{$formattedDistantTemplates}
+</div>
+HTML
+);
+               }
+
+               $wgOut->addHTML( <<<HTML
+{$this->editFormTextAfterTools}
 <div class='hiddencats'>
 {$formattedhiddencats}
 </div>
@@ -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 (file)
index 0000000..8e4ad35
--- /dev/null
@@ -0,0 +1,368 @@
+<?php
+/**
+ * A helper class to query the globalimagelinks table
+ * 
+ */
+class GlobalUsageQuery {
+       private $limit = 50;
+       private $offset;
+       private $hasMore = false;
+       private $filterLocal = false;
+       private $result;
+       private $continue;
+       private $reversed = false;
+       private $target = null;
+       
+       /**
+        * @param $target mixed Title or db key, or array of db keys of target(s)
+        */
+       public function __construct( $target ) {
+               global $wgGlobalDatabase;
+               $this->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 );
+       }
+}
index f1fb577..a1cf6a6 100644 (file)
@@ -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 = '<div class="mw-templatesUsedExplanation">';
+                       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 .= "</div><ul>\n";
+
+                       usort( $templates, array( 'Title', 'compare' ) );
+                       foreach ( $templates as $titleObj ) {
+                               $outText .= '<li>' . self::link( $titleObj ) . '</li>';
+                       }
+                       $outText .= '</ul>';
+               }
+               wfProfileOut( __METHOD__  );
+               return $outText;
+       }
+
        /**
         * Returns HTML for the "hidden categories on this page" list.
         *
index 3252fb6..da77601 100644 (file)
@@ -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
index a80e315..4d06feb 100644 (file)
@@ -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
 </div>
 " );
+                       if ( $wgEnableInterwikiTranscluding && $wgEnableInterwikiTemplatesTracking ) {
+                               $this->addHTML( "<div class='distantTemplatesUsed'>
+{$skin->formatDistantTemplates( $article->getUsedDistantTemplates( ) )}
+</div>
+" );
+               }
                }
 
                # If the title doesn't exist, it's fairly pointless to print a return
index f931080..243c9c0 100644 (file)
@@ -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 );
index 2b8e173..40a981d 100644 (file)
@@ -126,6 +126,8 @@ class SpecialPageFactory {
                // Page tools
                'ComparePages'              => 'SpecialComparePages',
                'Export'                    => 'SpecialExport',
+               'GlobalFileUsage'           => 'SpecialGlobalFileUsage',
+               'GlobalTemplateUsage'       => 'SpecialGlobalTemplateUsage',
                'Import'                    => 'SpecialImport',
                'Undelete'                  => 'SpecialUndelete',
                'Whatlinkshere'             => 'SpecialWhatlinkshere',
index 4f61897..f209995 100644 (file)
@@ -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
index dbff4d8..047dd20 100644 (file)
@@ -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.
index d542800..433e91c 100644 (file)
@@ -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();
+               }
+       }
 }
 
 /**
index be06053..279948f 100644 (file)
@@ -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 => <ignored>, ...), ...), ...)
+        * @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
         */
index e2a6926..29d2673 100644 (file)
@@ -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' ),
                );
        }
 
index 2e81f6c..0527385 100644 (file)
@@ -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' ),
                );
        }
 
index d1592c0..ac9bee7 100644 (file)
@@ -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 );  
+                               }
+                       }
+               }
+       }
 }
index 3233fbf..7774972 100644 (file)
@@ -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
index c643d75..045a852 100644 (file)
@@ -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
index 5b79876..dcda8a2 100644 (file)
@@ -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'];
index c2d7d3d..50af6b6 100644 (file)
@@ -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 (file)
index 0000000..e376ea6
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+/**
+ * Special page to show global file usage. Also contains hook functions for
+ * showing usage on an image page.
+ */
+
+class SpecialGlobalFileUsage extends SpecialPage {
+       public function __construct() {
+               parent::__construct( 'GlobalFileUsage' );
+       }
+
+       /**
+        * Entry point
+        */
+       public function execute( $par ) {
+               global $wgOut, $wgRequest;
+
+               $target = $par ? $par : $wgRequest->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<p>" . Xml::checkLabel( wfMsg( 'globalfileusage-filterlocal' ),
+                                       'filterlocal', 'mw-filterlocal', $this->filterLocal ) . '</p>';
+               
+               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</form>";
+
+               $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( '<div id="mw-globalfileusage-result">' );
+               foreach ( $query->getSingleResult() as $wiki => $result ) {
+                       $wgOut->addHtml(
+                                       '<h2>' . wfMsgExt(
+                                               'globalfileusage-on-wiki', 'parseinline',
+                                               $targetName, WikiMap::getWikiName( $wiki ) )
+                                       . "</h2><ul>\n" );
+                       foreach ( $result as $item ) {
+                               $wgOut->addHtml( "\t<li>" . self::formatItem( $item ) . "</li>\n" );
+                       }
+                       $wgOut->addHtml( "</ul>\n" );
+               }
+               $wgOut->addHtml( '</div>' );
+
+               // 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 (file)
index 0000000..5f7e8ae
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+/**
+ * This file has been copied from Extension:GlobalUsage and adapted
+ * to show the usage of a template instead of a file.
+ * Special page to show global template usage. Also contains hook functions for
+ * showing usage on an template page.
+ * 
+ * @author Bryan Tong Minh <bryan.tongminh@gmail.com>
+ * @author Peter Potrowl <peter017@gmail.com>
+ */
+
+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</form>";
+
+               $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( '<div id="mw-globaltemplateusage-result">' );
+               foreach ( $query->getSingleResult() as $wiki => $result ) {
+                       $wgOut->addHtml(
+                                       '<h2>' . wfMsgExt(
+                                               'globaltemplateusage-on-wiki', 'parseinline',
+                                               $targetName, WikiMap::getWikiName( $wiki ) )
+                                       . "</h2><ul>\n" );
+                       foreach ( $result as $item ) {
+                               $wgOut->addHtml( "\t<li>" . self::formatItem( $item ) . "</li>\n" );
+                       }
+                       $wgOut->addHtml( "</ul>\n" );
+               }
+               $wgOut->addHtml( '</div>' );
+
+               // 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 );
+       }
+}
+
+
index 22a153c..83bc114 100644 (file)
@@ -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!
index f021f8e..a1d5fde 100644 (file)
@@ -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 (file)
index 0000000..6571a5a
--- /dev/null
@@ -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 (file)
index 0000000..91b3578
--- /dev/null
@@ -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 (file)
index 0000000..d2da21b
--- /dev/null
@@ -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);
index 2d89268..ed0ad65 100644 (file)
@@ -640,6 +640,9 @@ $wgMessageStructure = array(
                'templatesused',
                'templatesusedpreview',
                'templatesusedsection',
+               'distanttemplatesused',
+               'distanttemplatesusedpreview',
+               'distanttemplatesusedsection',
                'template-protected',
                'template-semiprotected',
                'hiddencategories',
index 2ab431f..556fd54 100644 (file)
@@ -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