MediaWiki  REL1_19
LinkHolderArray.php
Go to the documentation of this file.
00001 <?php
00011 class LinkHolderArray {
00012         var $internals = array(), $interwikis = array();
00013         var $size = 0;
00014         var $parent;
00015         protected $tempIdOffset;
00016 
00017         function __construct( $parent ) {
00018                 $this->parent = $parent;
00019         }
00020 
00024         function __destruct() {
00025                 foreach ( $this as $name => $value ) {
00026                         unset( $this->$name );
00027                 }
00028         }
00029 
00037         function __sleep() {
00038                 foreach ( $this->internals as &$nsLinks ) {
00039                         foreach ( $nsLinks as &$entry ) {
00040                                 unset( $entry['title'] );
00041                         }
00042                 }
00043                 unset( $nsLinks );
00044                 unset( $entry );
00045 
00046                 foreach ( $this->interwikis as &$entry ) {
00047                         unset( $entry['title'] );
00048                 }
00049                 unset( $entry );
00050 
00051                 return array( 'internals', 'interwikis', 'size' );
00052         }
00053 
00057         function __wakeup() {
00058                 foreach ( $this->internals as &$nsLinks ) {
00059                         foreach ( $nsLinks as &$entry ) {
00060                                 $entry['title'] = Title::newFromText( $entry['pdbk'] );
00061                         }
00062                 }
00063                 unset( $nsLinks );
00064                 unset( $entry );
00065 
00066                 foreach ( $this->interwikis as &$entry ) {
00067                         $entry['title'] = Title::newFromText( $entry['pdbk'] );
00068                 }
00069                 unset( $entry );
00070         }
00071 
00076         function merge( $other ) {
00077                 foreach ( $other->internals as $ns => $entries ) {
00078                         $this->size += count( $entries );
00079                         if ( !isset( $this->internals[$ns] ) ) {
00080                                 $this->internals[$ns] = $entries;
00081                         } else {
00082                                 $this->internals[$ns] += $entries;
00083                         }
00084                 }
00085                 $this->interwikis += $other->interwikis;
00086         }
00087 
00100         function mergeForeign( $other, $texts ) {
00101                 $this->tempIdOffset = $idOffset = $this->parent->nextLinkID();
00102                 $maxId = 0;
00103 
00104                 # Renumber internal links
00105                 foreach ( $other->internals as $ns => $nsLinks ) {
00106                         foreach ( $nsLinks as $key => $entry ) {
00107                                 $newKey = $idOffset + $key;
00108                                 $this->internals[$ns][$newKey] = $entry;
00109                                 $maxId = $newKey > $maxId ? $newKey : $maxId;
00110                         }
00111                 }
00112                 $texts = preg_replace_callback( '/(<!--LINK \d+:)(\d+)(-->)/', 
00113                         array( $this, 'mergeForeignCallback' ), $texts );
00114 
00115                 # Renumber interwiki links
00116                 foreach ( $other->interwikis as $key => $entry ) {
00117                         $newKey = $idOffset + $key;
00118                         $this->interwikis[$newKey] = $entry;
00119                         $maxId = $newKey > $maxId ? $newKey : $maxId;
00120                 }
00121                 $texts = preg_replace_callback( '/(<!--IWLINK )(\d+)(-->)/', 
00122                         array( $this, 'mergeForeignCallback' ), $texts );
00123 
00124                 # Set the parent link ID to be beyond the highest used ID
00125                 $this->parent->setLinkID( $maxId + 1 );
00126                 $this->tempIdOffset = null;
00127                 return $texts;
00128         }
00129 
00130         protected function mergeForeignCallback( $m ) {
00131                 return $m[1] . ( $m[2] + $this->tempIdOffset ) . $m[3];
00132         }
00133 
00138         function getSubArray( $text ) {
00139                 $sub = new LinkHolderArray( $this->parent );
00140 
00141                 # Internal links
00142                 $pos = 0;
00143                 while ( $pos < strlen( $text ) ) {
00144                         if ( !preg_match( '/<!--LINK (\d+):(\d+)-->/', 
00145                                 $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) 
00146                         {
00147                                 break;
00148                         }
00149                         $ns = $m[1][0];
00150                         $key = $m[2][0];
00151                         $sub->internals[$ns][$key] = $this->internals[$ns][$key];
00152                         $pos = $m[0][1] + strlen( $m[0][0] );
00153                 }
00154 
00155                 # Interwiki links
00156                 $pos = 0;
00157                 while ( $pos < strlen( $text ) ) {
00158                         if ( !preg_match( '/<!--IWLINK (\d+)-->/', $text, $m, PREG_OFFSET_CAPTURE, $pos ) ) {
00159                                 break;
00160                         }
00161                         $key = $m[1][0];
00162                         $sub->interwikis[$key] = $this->interwikis[$key];
00163                         $pos = $m[0][1] + strlen( $m[0][0] );
00164                 }
00165                 return $sub;
00166         }
00167 
00171         function isBig() {
00172                 global $wgLinkHolderBatchSize;
00173                 return $this->size > $wgLinkHolderBatchSize;
00174         }
00175 
00180         function clear() {
00181                 $this->internals = array();
00182                 $this->interwikis = array();
00183                 $this->size = 0;
00184         }
00185 
00194         function makeHolder( $nt, $text = '', $query = array(), $trail = '', $prefix = ''  ) {
00195                 wfProfileIn( __METHOD__ );
00196                 if ( ! is_object($nt) ) {
00197                         # Fail gracefully
00198                         $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
00199                 } else {
00200                         # Separate the link trail from the rest of the link
00201                         list( $inside, $trail ) = Linker::splitTrail( $trail );
00202 
00203                         $entry = array(
00204                                 'title' => $nt,
00205                                 'text' => $prefix.$text.$inside,
00206                                 'pdbk' => $nt->getPrefixedDBkey(),
00207                         );
00208                         if ( $query !== array() ) {
00209                                 $entry['query'] = $query;
00210                         }
00211 
00212                         if ( $nt->isExternal() ) {
00213                                 // Use a globally unique ID to keep the objects mergable
00214                                 $key = $this->parent->nextLinkID();
00215                                 $this->interwikis[$key] = $entry;
00216                                 $retVal = "<!--IWLINK $key-->{$trail}";
00217                         } else {
00218                                 $key = $this->parent->nextLinkID();
00219                                 $ns = $nt->getNamespace();
00220                                 $this->internals[$ns][$key] = $entry;
00221                                 $retVal = "<!--LINK $ns:$key-->{$trail}";
00222                         }
00223                         $this->size++;
00224                 }
00225                 wfProfileOut( __METHOD__ );
00226                 return $retVal;
00227         }
00228 
00235         function replace( &$text ) {
00236                 wfProfileIn( __METHOD__ );
00237 
00238                 $colours = $this->replaceInternal( $text );
00239                 $this->replaceInterwiki( $text );
00240 
00241                 wfProfileOut( __METHOD__ );
00242                 return $colours;
00243         }
00244 
00248         protected function replaceInternal( &$text ) {
00249                 if ( !$this->internals ) {
00250                         return;
00251                 }
00252 
00253                 wfProfileIn( __METHOD__ );
00254                 global $wgContLang;
00255 
00256                 $colours = array();
00257                 $linkCache = LinkCache::singleton();
00258                 $output = $this->parent->getOutput();
00259 
00260                 wfProfileIn( __METHOD__.'-check' );
00261                 $dbr = wfGetDB( DB_SLAVE );
00262                 $threshold = $this->parent->getOptions()->getStubThreshold();
00263 
00264                 # Sort by namespace
00265                 ksort( $this->internals );
00266 
00267                 $linkcolour_ids = array();
00268 
00269                 # Generate query
00270                 $queries = array();
00271                 foreach ( $this->internals as $ns => $entries ) {
00272                         foreach ( $entries as $entry ) {
00273                                 $title = $entry['title'];
00274                                 $pdbk = $entry['pdbk'];
00275 
00276                                 # Skip invalid entries.
00277                                 # Result will be ugly, but prevents crash.
00278                                 if ( is_null( $title ) ) {
00279                                         continue;
00280                                 }
00281 
00282                                 # Check if it's a static known link, e.g. interwiki
00283                                 if ( $title->isAlwaysKnown() ) {
00284                                         $colours[$pdbk] = '';
00285                                 } elseif ( $ns == NS_SPECIAL ) {
00286                                         $colours[$pdbk] = 'new';
00287                                 } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
00288                                         $colours[$pdbk] = Linker::getLinkColour( $title, $threshold );
00289                                         $output->addLink( $title, $id );
00290                                         $linkcolour_ids[$id] = $pdbk;
00291                                 } elseif ( $linkCache->isBadLink( $pdbk ) ) {
00292                                         $colours[$pdbk] = 'new';
00293                                 } else {
00294                                         # Not in the link cache, add it to the query
00295                                         $queries[$ns][] = $title->getDBkey();
00296                                 }
00297                         }
00298                 }
00299                 if ( $queries ) {
00300                         $where = array();
00301                         foreach( $queries as $ns => $pages ){
00302                                 $where[] = $dbr->makeList(
00303                                         array(
00304                                                 'page_namespace' => $ns,
00305                                                 'page_title' => $pages,
00306                                         ),
00307                                         LIST_AND
00308                                 );
00309                         }
00310 
00311                         $res = $dbr->select(
00312                                 'page',
00313                                 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect', 'page_len', 'page_latest' ),
00314                                 $dbr->makeList( $where, LIST_OR ),
00315                                 __METHOD__
00316                         );
00317 
00318                         # Fetch data and form into an associative array
00319                         # non-existent = broken
00320                         foreach ( $res as $s ) {
00321                                 $title = Title::makeTitle( $s->page_namespace, $s->page_title );
00322                                 $pdbk = $title->getPrefixedDBkey();
00323                                 $linkCache->addGoodLinkObjFromRow( $title, $s );
00324                                 $output->addLink( $title, $s->page_id );
00325                                 # @todo FIXME: Convoluted data flow
00326                                 # The redirect status and length is passed to getLinkColour via the LinkCache
00327                                 # Use formal parameters instead
00328                                 $colours[$pdbk] = Linker::getLinkColour( $title, $threshold );
00329                                 //add id to the extension todolist
00330                                 $linkcolour_ids[$s->page_id] = $pdbk;
00331                         }
00332                         unset( $res );
00333                 }
00334                 if ( count($linkcolour_ids) ) {
00335                         //pass an array of page_ids to an extension
00336                         wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
00337                 }
00338                 wfProfileOut( __METHOD__.'-check' );
00339 
00340                 # Do a second query for different language variants of links and categories
00341                 if($wgContLang->hasVariants()) {
00342                         $this->doVariants( $colours );
00343                 }
00344 
00345                 # Construct search and replace arrays
00346                 wfProfileIn( __METHOD__.'-construct' );
00347                 $replacePairs = array();
00348                 foreach ( $this->internals as $ns => $entries ) {
00349                         foreach ( $entries as $index => $entry ) {
00350                                 $pdbk = $entry['pdbk'];
00351                                 $title = $entry['title'];
00352                                 $query = isset( $entry['query'] ) ? $entry['query'] : array();
00353                                 $key = "$ns:$index";
00354                                 $searchkey = "<!--LINK $key-->";
00355                                 $displayText = $entry['text'];
00356                                 if ( $displayText === '' ) {
00357                                         $displayText = null;
00358                                 }
00359                                 if ( !isset( $colours[$pdbk] ) ) {
00360                                         $colours[$pdbk] = 'new';
00361                                 }
00362                                 $attribs = array();
00363                                 if ( $colours[$pdbk] == 'new' ) {
00364                                         $linkCache->addBadLinkObj( $title );
00365                                         $output->addLink( $title, 0 );
00366                                         $type = array( 'broken' );
00367                                 } else {
00368                                         if ( $colours[$pdbk] != '' ) {
00369                                                 $attribs['class'] = $colours[$pdbk];
00370                                         }
00371                                         $type = array( 'known', 'noclasses' );
00372                                 }
00373                                 $replacePairs[$searchkey] = Linker::link( $title, $displayText,
00374                                                 $attribs, $query, $type );
00375                         }
00376                 }
00377                 $replacer = new HashtableReplacer( $replacePairs, 1 );
00378                 wfProfileOut( __METHOD__.'-construct' );
00379 
00380                 # Do the thing
00381                 wfProfileIn( __METHOD__.'-replace' );
00382                 $text = preg_replace_callback(
00383                         '/(<!--LINK .*?-->)/',
00384                         $replacer->cb(),
00385                         $text);
00386 
00387                 wfProfileOut( __METHOD__.'-replace' );
00388                 wfProfileOut( __METHOD__ );
00389         }
00390 
00394         protected function replaceInterwiki( &$text ) {
00395                 if ( empty( $this->interwikis ) ) {
00396                         return;
00397                 }
00398 
00399                 wfProfileIn( __METHOD__ );
00400                 # Make interwiki link HTML
00401                 $output = $this->parent->getOutput();
00402                 $replacePairs = array();
00403                 foreach( $this->interwikis as $key => $link ) {
00404                         $replacePairs[$key] = Linker::link( $link['title'], $link['text'] );
00405                         $output->addInterwikiLink( $link['title'] );
00406                 }
00407                 $replacer = new HashtableReplacer( $replacePairs, 1 );
00408 
00409                 $text = preg_replace_callback(
00410                         '/<!--IWLINK (.*?)-->/',
00411                         $replacer->cb(),
00412                         $text );
00413                 wfProfileOut( __METHOD__ );
00414         }
00415 
00419         protected function doVariants( &$colours ) {
00420                 global $wgContLang;
00421                 $linkBatch = new LinkBatch();
00422                 $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
00423                 $output = $this->parent->getOutput();
00424                 $linkCache = LinkCache::singleton();
00425                 $threshold = $this->parent->getOptions()->getStubThreshold();
00426                 $titlesToBeConverted = '';
00427                 $titlesAttrs = array();
00428 
00429                 // Concatenate titles to a single string, thus we only need auto convert the
00430                 // single string to all variants. This would improve parser's performance
00431                 // significantly.
00432                 foreach ( $this->internals as $ns => $entries ) {
00433                         foreach ( $entries as $index => $entry ) {
00434                                 $pdbk = $entry['pdbk'];
00435                                 // we only deal with new links (in its first query)
00436                                 if ( !isset( $colours[$pdbk] ) ) {
00437                                         $title = $entry['title'];
00438                                         $titleText = $title->getText();
00439                                         $titlesAttrs[] = array(
00440                                                 'ns' => $ns,
00441                                                 'key' => "$ns:$index",
00442                                                 'titleText' => $titleText,
00443                                         );
00444                                         // separate titles with \0 because it would never appears
00445                                         // in a valid title
00446                                         $titlesToBeConverted .= $titleText . "\0";
00447                                 }
00448                         }
00449                 }
00450 
00451                 // Now do the conversion and explode string to text of titles
00452                 $titlesAllVariants = $wgContLang->autoConvertToAllVariants( $titlesToBeConverted );
00453                 $allVariantsName = array_keys( $titlesAllVariants );
00454                 foreach ( $titlesAllVariants as &$titlesVariant ) {
00455                         $titlesVariant = explode( "\0", $titlesVariant );
00456                 }
00457                 $l = count( $titlesAttrs );
00458                 // Then add variants of links to link batch
00459                 for ( $i = 0; $i < $l; $i ++ ) {
00460                         foreach ( $allVariantsName as $variantName ) {
00461                                 $textVariant = $titlesAllVariants[$variantName][$i];
00462                                 if ( $textVariant != $titlesAttrs[$i]['titleText'] ) {
00463                                         $variantTitle = Title::makeTitle( $titlesAttrs[$i]['ns'], $textVariant );
00464                                         if( is_null( $variantTitle ) ) {
00465                                                 continue;
00466                                         }
00467                                         $linkBatch->addObj( $variantTitle );
00468                                         $variantMap[$variantTitle->getPrefixedDBkey()][] = $titlesAttrs[$i]['key'];
00469                                 }
00470                         }
00471                 }
00472 
00473                 // process categories, check if a category exists in some variant
00474                 $categoryMap = array(); // maps $category_variant => $category (dbkeys)
00475                 $varCategories = array(); // category replacements oldDBkey => newDBkey
00476                 foreach( $output->getCategoryLinks() as $category ){
00477                         $variants = $wgContLang->autoConvertToAllVariants( $category );
00478                         foreach($variants as $variant){
00479                                 if($variant != $category){
00480                                         $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
00481                                         if(is_null($variantTitle)) continue;
00482                                         $linkBatch->addObj( $variantTitle );
00483                                         $categoryMap[$variant] = $category;
00484                                 }
00485                         }
00486                 }
00487 
00488 
00489                 if(!$linkBatch->isEmpty()){
00490                         // construct query
00491                         $dbr = wfGetDB( DB_SLAVE );
00492                         $varRes = $dbr->select( 'page',
00493                                 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect', 'page_len', 'page_latest' ),
00494                                 $linkBatch->constructSet( 'page', $dbr ),
00495                                 __METHOD__
00496                         );
00497 
00498                         $linkcolour_ids = array();
00499 
00500                         // for each found variants, figure out link holders and replace
00501                         foreach ( $varRes as $s ) {
00502 
00503                                 $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
00504                                 $varPdbk = $variantTitle->getPrefixedDBkey();
00505                                 $vardbk = $variantTitle->getDBkey();
00506 
00507                                 $holderKeys = array();
00508                                 if( isset( $variantMap[$varPdbk] ) ) {
00509                                         $holderKeys = $variantMap[$varPdbk];
00510                                         $linkCache->addGoodLinkObjFromRow( $variantTitle, $s );
00511                                         $output->addLink( $variantTitle, $s->page_id );
00512                                 }
00513 
00514                                 // loop over link holders
00515                                 foreach( $holderKeys as $key ) {
00516                                         list( $ns, $index ) = explode( ':', $key, 2 );
00517                                         $entry =& $this->internals[$ns][$index];
00518                                         $pdbk = $entry['pdbk'];
00519 
00520                                         if(!isset($colours[$pdbk])){
00521                                                 // found link in some of the variants, replace the link holder data
00522                                                 $entry['title'] = $variantTitle;
00523                                                 $entry['pdbk'] = $varPdbk;
00524 
00525                                                 // set pdbk and colour
00526                                                 # @todo FIXME: Convoluted data flow
00527                                                 # The redirect status and length is passed to getLinkColour via the LinkCache
00528                                                 # Use formal parameters instead
00529                                                 $colours[$varPdbk] = Linker::getLinkColour( $variantTitle, $threshold );
00530                                                 $linkcolour_ids[$s->page_id] = $pdbk;
00531                                         }
00532                                 }
00533 
00534                                 // check if the object is a variant of a category
00535                                 if(isset($categoryMap[$vardbk])){
00536                                         $oldkey = $categoryMap[$vardbk];
00537                                         if($oldkey != $vardbk)
00538                                                 $varCategories[$oldkey]=$vardbk;
00539                                 }
00540                         }
00541                         wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
00542 
00543                         // rebuild the categories in original order (if there are replacements)
00544                         if(count($varCategories)>0){
00545                                 $newCats = array();
00546                                 $originalCats = $output->getCategories();
00547                                 foreach($originalCats as $cat => $sortkey){
00548                                         // make the replacement
00549                                         if( array_key_exists($cat,$varCategories) )
00550                                                 $newCats[$varCategories[$cat]] = $sortkey;
00551                                         else $newCats[$cat] = $sortkey;
00552                                 }
00553                                 $output->setCategoryLinks($newCats);
00554                         }
00555                 }
00556         }
00557 
00565         function replaceText( $text ) {
00566                 wfProfileIn( __METHOD__ );
00567 
00568                 $text = preg_replace_callback(
00569                         '/<!--(LINK|IWLINK) (.*?)-->/',
00570                         array( &$this, 'replaceTextCallback' ),
00571                         $text );
00572 
00573                 wfProfileOut( __METHOD__ );
00574                 return $text;
00575         }
00576 
00584         function replaceTextCallback( $matches ) {
00585                 $type = $matches[1];
00586                 $key  = $matches[2];
00587                 if( $type == 'LINK' ) {
00588                         list( $ns, $index ) = explode( ':', $key, 2 );
00589                         if( isset( $this->internals[$ns][$index]['text'] ) ) {
00590                                 return $this->internals[$ns][$index]['text'];
00591                         }
00592                 } elseif( $type == 'IWLINK' ) {
00593                         if( isset( $this->interwikis[$key]['text'] ) ) {
00594                                 return $this->interwikis[$key]['text'];
00595                         }
00596                 }
00597                 return $matches[0];
00598         }
00599 }