]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/parser/LinkHolderArray.php
MediaWiki 1.17.0
[autoinstalls/mediawiki.git] / includes / parser / LinkHolderArray.php
1 <?php
2 /**
3  * Holder of replacement pairs for wiki links
4  *
5  * @file
6  */
7
8 /**
9  * @ingroup Parser
10  */
11 class LinkHolderArray {
12         var $internals = array(), $interwikis = array();
13         var $size = 0;
14         var $parent;
15
16         function __construct( $parent ) {
17                 $this->parent = $parent;
18         }
19
20         /**
21          * Reduce memory usage to reduce the impact of circular references
22          */
23         function __destruct() {
24                 foreach ( $this as $name => $value ) {
25                         unset( $this->$name );
26                 }
27         }
28
29         /**
30          * Merge another LinkHolderArray into this one
31          */
32         function merge( $other ) {
33                 foreach ( $other->internals as $ns => $entries ) {
34                         $this->size += count( $entries );
35                         if ( !isset( $this->internals[$ns] ) ) {
36                                 $this->internals[$ns] = $entries;
37                         } else {
38                                 $this->internals[$ns] += $entries;
39                         }
40                 }
41                 $this->interwikis += $other->interwikis;
42         }
43
44         /**
45          * Returns true if the memory requirements of this object are getting large
46          */
47         function isBig() {
48                 global $wgLinkHolderBatchSize;
49                 return $this->size > $wgLinkHolderBatchSize;
50         }
51
52         /**
53          * Clear all stored link holders.
54          * Make sure you don't have any text left using these link holders, before you call this
55          */
56         function clear() {
57                 $this->internals = array();
58                 $this->interwikis = array();
59                 $this->size = 0;
60         }
61
62         /**
63          * Make a link placeholder. The text returned can be later resolved to a real link with
64          * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
65          * parsing of interwiki links, and secondly to allow all existence checks and
66          * article length checks (for stub links) to be bundled into a single query.
67          *
68          */
69         function makeHolder( $nt, $text = '', $query = '', $trail = '', $prefix = ''  ) {
70                 wfProfileIn( __METHOD__ );
71                 if ( ! is_object($nt) ) {
72                         # Fail gracefully
73                         $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
74                 } else {
75                         # Separate the link trail from the rest of the link
76                         list( $inside, $trail ) = Linker::splitTrail( $trail );
77
78                         $entry = array(
79                                 'title' => $nt,
80                                 'text' => $prefix.$text.$inside,
81                                 'pdbk' => $nt->getPrefixedDBkey(),
82                         );
83                         if ( $query !== '' ) {
84                                 $entry['query'] = $query;
85                         }
86
87                         if ( $nt->isExternal() ) {
88                                 // Use a globally unique ID to keep the objects mergable
89                                 $key = $this->parent->nextLinkID();
90                                 $this->interwikis[$key] = $entry;
91                                 $retVal = "<!--IWLINK $key-->{$trail}";
92                         } else {
93                                 $key = $this->parent->nextLinkID();
94                                 $ns = $nt->getNamespace();
95                                 $this->internals[$ns][$key] = $entry;
96                                 $retVal = "<!--LINK $ns:$key-->{$trail}";
97                         }
98                         $this->size++;
99                 }
100                 wfProfileOut( __METHOD__ );
101                 return $retVal;
102         }
103
104         /**
105          * Get the stub threshold
106          */
107         function getStubThreshold() {
108                 global $wgUser;
109                 if ( !isset( $this->stubThreshold ) ) {
110                         $this->stubThreshold = $wgUser->getStubThreshold();
111                 }
112                 return $this->stubThreshold;
113         }
114
115         /**
116          * FIXME: update documentation. makeLinkObj() is deprecated.
117          * Replace <!--LINK--> link placeholders with actual links, in the buffer
118          * Placeholders created in Skin::makeLinkObj()
119          * Returns an array of link CSS classes, indexed by PDBK.
120          */
121         function replace( &$text ) {
122                 wfProfileIn( __METHOD__ );
123
124                 $colours = $this->replaceInternal( $text );
125                 $this->replaceInterwiki( $text );
126
127                 wfProfileOut( __METHOD__ );
128                 return $colours;
129         }
130
131         /**
132          * Replace internal links
133          */
134         protected function replaceInternal( &$text ) {
135                 if ( !$this->internals ) {
136                         return;
137                 }
138
139                 wfProfileIn( __METHOD__ );
140                 global $wgContLang;
141
142                 $colours = array();
143                 $sk = $this->parent->getOptions()->getSkin( $this->parent->mTitle );
144                 $linkCache = LinkCache::singleton();
145                 $output = $this->parent->getOutput();
146
147                 wfProfileIn( __METHOD__.'-check' );
148                 $dbr = wfGetDB( DB_SLAVE );
149                 $page = $dbr->tableName( 'page' );
150                 $threshold = $this->getStubThreshold();
151
152                 # Sort by namespace
153                 ksort( $this->internals );
154
155                 $linkcolour_ids = array();
156
157                 # Generate query
158                 $query = false;
159                 $current = null;
160                 foreach ( $this->internals as $ns => $entries ) {
161                         foreach ( $entries as $entry ) {
162                                 $title = $entry['title'];
163                                 $pdbk = $entry['pdbk'];
164
165                                 # Skip invalid entries.
166                                 # Result will be ugly, but prevents crash.
167                                 if ( is_null( $title ) ) {
168                                         continue;
169                                 }
170
171                                 # Check if it's a static known link, e.g. interwiki
172                                 if ( $title->isAlwaysKnown() ) {
173                                         $colours[$pdbk] = '';
174                                 } elseif ( $ns == NS_SPECIAL ) {
175                                         $colours[$pdbk] = 'new';
176                                 } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
177                                         $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
178                                         $output->addLink( $title, $id );
179                                         $linkcolour_ids[$id] = $pdbk;
180                                 } elseif ( $linkCache->isBadLink( $pdbk ) ) {
181                                         $colours[$pdbk] = 'new';
182                                 } else {
183                                         # Not in the link cache, add it to the query
184                                         if ( !isset( $current ) ) {
185                                                 $current = $ns;
186                                                 $query =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len, page_latest";
187                                                 $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
188                                         } elseif ( $current != $ns ) {
189                                                 $current = $ns;
190                                                 $query .= ")) OR (page_namespace=$ns AND page_title IN(";
191                                         } else {
192                                                 $query .= ', ';
193                                         }
194
195                                         $query .= $dbr->addQuotes( $title->getDBkey() );
196                                 }
197                         }
198                 }
199                 if ( $query ) {
200                         $query .= '))';
201
202                         $res = $dbr->query( $query, __METHOD__ );
203
204                         # Fetch data and form into an associative array
205                         # non-existent = broken
206                         foreach ( $res as $s ) {
207                                 $title = Title::makeTitle( $s->page_namespace, $s->page_title );
208                                 $pdbk = $title->getPrefixedDBkey();
209                                 $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect, $s->page_latest );
210                                 $output->addLink( $title, $s->page_id );
211                                 # FIXME: convoluted data flow
212                                 # The redirect status and length is passed to getLinkColour via the LinkCache
213                                 # Use formal parameters instead
214                                 $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
215                                 //add id to the extension todolist
216                                 $linkcolour_ids[$s->page_id] = $pdbk;
217                         }
218                         unset( $res );
219                 }
220                 if ( count($linkcolour_ids) ) {
221                         //pass an array of page_ids to an extension
222                         wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
223                 }
224                 wfProfileOut( __METHOD__.'-check' );
225
226                 # Do a second query for different language variants of links and categories
227                 if($wgContLang->hasVariants()) {
228                         $this->doVariants( $colours );
229                 }
230
231                 # Construct search and replace arrays
232                 wfProfileIn( __METHOD__.'-construct' );
233                 $replacePairs = array();
234                 foreach ( $this->internals as $ns => $entries ) {
235                         foreach ( $entries as $index => $entry ) {
236                                 $pdbk = $entry['pdbk'];
237                                 $title = $entry['title'];
238                                 $query = isset( $entry['query'] ) ? $entry['query'] : '';
239                                 $key = "$ns:$index";
240                                 $searchkey = "<!--LINK $key-->";
241                                 if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
242                                         $linkCache->addBadLinkObj( $title );
243                                         $colours[$pdbk] = 'new';
244                                         $output->addLink( $title, 0 );
245                                         // FIXME: replace deprecated makeBrokenLinkObj() by link()
246                                         $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
247                                                                         $entry['text'],
248                                                                         $query );
249                                 } else {
250                                         // FIXME: replace deprecated makeColouredLinkObj() by link()
251                                         $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
252                                                                         $entry['text'],
253                                                                         $query );
254                                 }
255                         }
256                 }
257                 $replacer = new HashtableReplacer( $replacePairs, 1 );
258                 wfProfileOut( __METHOD__.'-construct' );
259
260                 # Do the thing
261                 wfProfileIn( __METHOD__.'-replace' );
262                 $text = preg_replace_callback(
263                         '/(<!--LINK .*?-->)/',
264                         $replacer->cb(),
265                         $text);
266
267                 wfProfileOut( __METHOD__.'-replace' );
268                 wfProfileOut( __METHOD__ );
269         }
270
271         /**
272          * Replace interwiki links
273          */
274         protected function replaceInterwiki( &$text ) {
275                 if ( empty( $this->interwikis ) ) {
276                         return;
277                 }
278
279                 wfProfileIn( __METHOD__ );
280                 # Make interwiki link HTML
281                 $sk = $this->parent->getOptions()->getSkin( $this->parent->mTitle );
282                 $output = $this->parent->getOutput();
283                 $replacePairs = array();
284                 foreach( $this->interwikis as $key => $link ) {
285                         $replacePairs[$key] = $sk->link( $link['title'], $link['text'] );
286                         $output->addInterwikiLink( $link['title'] );
287                 }
288                 $replacer = new HashtableReplacer( $replacePairs, 1 );
289
290                 $text = preg_replace_callback(
291                         '/<!--IWLINK (.*?)-->/',
292                         $replacer->cb(),
293                         $text );
294                 wfProfileOut( __METHOD__ );
295         }
296
297         /**
298          * Modify $this->internals and $colours according to language variant linking rules
299          */
300         protected function doVariants( &$colours ) {
301                 global $wgContLang;
302                 $linkBatch = new LinkBatch();
303                 $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
304                 $output = $this->parent->getOutput();
305                 $linkCache = LinkCache::singleton();
306                 $sk = $this->parent->getOptions()->getSkin( $this->parent->mTitle );
307                 $threshold = $this->getStubThreshold();
308                 $titlesToBeConverted = '';
309                 $titlesAttrs = array();
310                 
311                 // Concatenate titles to a single string, thus we only need auto convert the
312                 // single string to all variants. This would improve parser's performance
313                 // significantly.
314                 foreach ( $this->internals as $ns => $entries ) {
315                         foreach ( $entries as $index => $entry ) {
316                                 $pdbk = $entry['pdbk'];
317                                 // we only deal with new links (in its first query)
318                                 if ( !isset( $colours[$pdbk] ) ) {
319                                         $title = $entry['title'];
320                                         $titleText = $title->getText();
321                                         $titlesAttrs[] = array(
322                                                 'ns' => $ns,
323                                                 'key' => "$ns:$index",
324                                                 'titleText' => $titleText,
325                                         );                                      
326                                         // separate titles with \0 because it would never appears
327                                         // in a valid title
328                                         $titlesToBeConverted .= $titleText . "\0";
329                                 }
330                         }
331                 }
332                 
333                 // Now do the conversion and explode string to text of titles
334                 $titlesAllVariants = $wgContLang->autoConvertToAllVariants( $titlesToBeConverted );
335                 $allVariantsName = array_keys( $titlesAllVariants );
336                 foreach ( $titlesAllVariants as &$titlesVariant ) {
337                         $titlesVariant = explode( "\0", $titlesVariant );
338                 }
339                 $l = count( $titlesAttrs );
340                 // Then add variants of links to link batch
341                 for ( $i = 0; $i < $l; $i ++ ) {
342                         foreach ( $allVariantsName as $variantName ) {
343                                 $textVariant = $titlesAllVariants[$variantName][$i];
344                                 extract( $titlesAttrs[$i] );
345                                 if($textVariant != $titleText){
346                                         $variantTitle = Title::makeTitle( $ns, $textVariant );
347                                         if( is_null( $variantTitle ) ) {
348                                                 continue;
349                                         }
350                                         $linkBatch->addObj( $variantTitle );
351                                         $variantMap[$variantTitle->getPrefixedDBkey()][] = $titlesAttrs[$i]['key'];
352                                 }
353                         }
354                 }
355
356                 // process categories, check if a category exists in some variant
357                 $categoryMap = array(); // maps $category_variant => $category (dbkeys)
358                 $varCategories = array(); // category replacements oldDBkey => newDBkey
359                 foreach( $output->getCategoryLinks() as $category ){
360                         $variants = $wgContLang->autoConvertToAllVariants( $category );
361                         foreach($variants as $variant){
362                                 if($variant != $category){
363                                         $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
364                                         if(is_null($variantTitle)) continue;
365                                         $linkBatch->addObj( $variantTitle );
366                                         $categoryMap[$variant] = $category;
367                                 }
368                         }
369                 }
370
371
372                 if(!$linkBatch->isEmpty()){
373                         // construct query
374                         $dbr = wfGetDB( DB_SLAVE );
375                         $page = $dbr->tableName( 'page' );
376                         $titleClause = $linkBatch->constructSet('page', $dbr);
377                         $variantQuery =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
378                         $variantQuery .= " FROM $page WHERE $titleClause";
379                         $varRes = $dbr->query( $variantQuery, __METHOD__ );
380                         $linkcolour_ids = array();
381
382                         // for each found variants, figure out link holders and replace
383                         foreach ( $varRes as $s ) {
384
385                                 $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
386                                 $varPdbk = $variantTitle->getPrefixedDBkey();
387                                 $vardbk = $variantTitle->getDBkey();
388
389                                 $holderKeys = array();
390                                 if(isset($variantMap[$varPdbk])){
391                                         $holderKeys = $variantMap[$varPdbk];
392                                         $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
393                                         $output->addLink( $variantTitle, $s->page_id );
394                                 }
395
396                                 // loop over link holders
397                                 foreach($holderKeys as $key){
398                                         list( $ns, $index ) = explode( ':', $key, 2 );
399                                         $entry =& $this->internals[$ns][$index];
400                                         $pdbk = $entry['pdbk'];
401
402                                         if(!isset($colours[$pdbk])){
403                                                 // found link in some of the variants, replace the link holder data
404                                                 $entry['title'] = $variantTitle;
405                                                 $entry['pdbk'] = $varPdbk;
406
407                                                 // set pdbk and colour
408                                                 # FIXME: convoluted data flow
409                                                 # The redirect status and length is passed to getLinkColour via the LinkCache
410                                                 # Use formal parameters instead
411                                                 $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
412                                                 $linkcolour_ids[$s->page_id] = $pdbk;
413                                         }
414                                 }
415
416                                 // check if the object is a variant of a category
417                                 if(isset($categoryMap[$vardbk])){
418                                         $oldkey = $categoryMap[$vardbk];
419                                         if($oldkey != $vardbk)
420                                                 $varCategories[$oldkey]=$vardbk;
421                                 }
422                         }
423                         wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
424
425                         // rebuild the categories in original order (if there are replacements)
426                         if(count($varCategories)>0){
427                                 $newCats = array();
428                                 $originalCats = $output->getCategories();
429                                 foreach($originalCats as $cat => $sortkey){
430                                         // make the replacement
431                                         if( array_key_exists($cat,$varCategories) )
432                                                 $newCats[$varCategories[$cat]] = $sortkey;
433                                         else $newCats[$cat] = $sortkey;
434                                 }
435                                 $output->setCategoryLinks($newCats);
436                         }
437                 }
438         }
439
440         /**
441          * Replace <!--LINK--> link placeholders with plain text of links
442          * (not HTML-formatted).
443          *
444          * @param $text String
445          * @return String
446          */
447         function replaceText( $text ) {
448                 wfProfileIn( __METHOD__ );
449
450                 $text = preg_replace_callback(
451                         '/<!--(LINK|IWLINK) (.*?)-->/',
452                         array( &$this, 'replaceTextCallback' ),
453                         $text );
454
455                 wfProfileOut( __METHOD__ );
456                 return $text;
457         }
458
459         /**
460          * Callback for replaceText()
461          *
462          * @param $matches Array
463          * @return string
464          * @private
465          */
466         function replaceTextCallback( $matches ) {
467                 $type = $matches[1];
468                 $key  = $matches[2];
469                 if( $type == 'LINK' ) {
470                         list( $ns, $index ) = explode( ':', $key, 2 );
471                         if( isset( $this->internals[$ns][$index]['text'] ) ) {
472                                 return $this->internals[$ns][$index]['text'];
473                         }
474                 } elseif( $type == 'IWLINK' ) {
475                         if( isset( $this->interwikis[$key]['text'] ) ) {
476                                 return $this->interwikis[$key]['text'];
477                         }
478                 }
479                 return $matches[0];
480         }
481 }