]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiPageSet.php
MediaWiki 1.15.0
[autoinstalls/mediawiki.git] / includes / api / ApiPageSet.php
1 <?php
2
3 /*
4  * Created on Sep 24, 2006
5  *
6  * API for MediaWiki 1.8+
7  *
8  * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License along
21  * with this program; if not, write to the Free Software Foundation, Inc.,
22  * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23  * http://www.gnu.org/copyleft/gpl.html
24  */
25
26 if (!defined('MEDIAWIKI')) {
27         // Eclipse helper - will be ignored in production
28         require_once ('ApiQueryBase.php');
29 }
30
31 /**
32  * This class contains a list of pages that the client has requested.
33  * Initially, when the client passes in titles=, pageids=, or revisions=
34  * parameter, an instance of the ApiPageSet class will normalize titles,
35  * determine if the pages/revisions exist, and prefetch any additional page
36  * data requested.
37  *
38  * When a generator is used, the result of the generator will become the input
39  * for the second instance of this class, and all subsequent actions will use
40  * the second instance for all their work.
41  *
42  * @ingroup API
43  */
44 class ApiPageSet extends ApiQueryBase {
45
46         private $mAllPages; // [ns][dbkey] => page_id or negative when missing
47         private $mTitles, $mGoodTitles, $mMissingTitles, $mInvalidTitles;
48         private $mMissingPageIDs, $mRedirectTitles;
49         private $mNormalizedTitles, $mInterwikiTitles;
50         private $mResolveRedirects, $mPendingRedirectIDs;
51         private $mGoodRevIDs, $mMissingRevIDs;
52         private $mFakePageId;
53
54         private $mRequestedPageFields;
55
56         /**
57          * Constructor
58          * @param $query ApiQuery
59          * @param $resolveRedirects bool Whether redirects should be resolved
60          */
61         public function __construct($query, $resolveRedirects = false) {
62                 parent :: __construct($query, 'query');
63
64                 $this->mAllPages = array ();
65                 $this->mTitles = array();
66                 $this->mGoodTitles = array ();
67                 $this->mMissingTitles = array ();
68                 $this->mInvalidTitles = array ();
69                 $this->mMissingPageIDs = array ();
70                 $this->mRedirectTitles = array ();
71                 $this->mNormalizedTitles = array ();
72                 $this->mInterwikiTitles = array ();
73                 $this->mGoodRevIDs = array();
74                 $this->mMissingRevIDs = array();
75
76                 $this->mRequestedPageFields = array ();
77                 $this->mResolveRedirects = $resolveRedirects;
78                 if($resolveRedirects)
79                         $this->mPendingRedirectIDs = array();
80
81                 $this->mFakePageId = -1;
82         }
83
84         /**
85          * Check whether this PageSet is resolving redirects
86          * @return bool
87          */
88         public function isResolvingRedirects() {
89                 return $this->mResolveRedirects;
90         }
91
92         /**
93          * Request an additional field from the page table. Must be called
94          * before execute()
95          * @param $fieldName string Field name
96          */
97         public function requestField($fieldName) {
98                 $this->mRequestedPageFields[$fieldName] = null;
99         }
100
101         /**
102          * Get the value of a custom field previously requested through
103          * requestField()
104          * @param $fieldName string Field name
105          * @return mixed Field value
106          */
107         public function getCustomField($fieldName) {
108                 return $this->mRequestedPageFields[$fieldName];
109         }
110
111         /**
112          * Get the fields that have to be queried from the page table:
113          * the ones requested through requestField() and a few basic ones
114          * we always need
115          * @return array of field names
116          */
117         public function getPageTableFields() {
118                 // Ensure we get minimum required fields
119                 // DON'T change this order
120                 $pageFlds = array (
121                         'page_namespace' => null,
122                         'page_title' => null,
123                         'page_id' => null,
124                 );
125
126                 if ($this->mResolveRedirects)
127                         $pageFlds['page_is_redirect'] = null;
128
129                 // only store non-default fields
130                 $this->mRequestedPageFields = array_diff_key($this->mRequestedPageFields, $pageFlds);
131
132                 $pageFlds = array_merge($pageFlds, $this->mRequestedPageFields);
133                 return array_keys($pageFlds);
134         }
135
136         /**
137          * Returns an array [ns][dbkey] => page_id for all requested titles.
138          * page_id is a unique negative number in case title was not found.
139          * Invalid titles will also have negative page IDs and will be in namespace 0
140          * @return array
141          */
142         public function getAllTitlesByNamespace() {
143                 return $this->mAllPages;
144         }
145
146         /**
147          * All Title objects provided.
148          * @return array of Title objects
149          */
150         public function getTitles() {
151                 return $this->mTitles;
152         }
153
154         /**
155          * Returns the number of unique pages (not revisions) in the set.
156          * @return int
157          */
158         public function getTitleCount() {
159                 return count($this->mTitles);
160         }
161
162         /**
163          * Title objects that were found in the database.
164          * @return array page_id (int) => Title (obj)
165          */
166         public function getGoodTitles() {
167                 return $this->mGoodTitles;
168         }
169
170         /**
171          * Returns the number of found unique pages (not revisions) in the set.
172          * @return int
173          */
174         public function getGoodTitleCount() {
175                 return count($this->mGoodTitles);
176         }
177
178         /**
179          * Title objects that were NOT found in the database.
180          * The array's index will be negative for each item
181          * @return array of Title objects
182          */
183         public function getMissingTitles() {
184                 return $this->mMissingTitles;
185         }
186
187         /**
188          * Titles that were deemed invalid by Title::newFromText()
189          * The array's index will be unique and negative for each item
190          * @return array of strings (not Title objects)
191          */
192         public function getInvalidTitles() {
193                 return $this->mInvalidTitles;
194         }
195
196         /**
197          * Page IDs that were not found in the database
198          * @return array of page IDs
199          */
200         public function getMissingPageIDs() {
201                 return $this->mMissingPageIDs;
202         }
203
204         /**
205          * Get a list of redirect resolutions - maps a title to its redirect
206          * target.
207          * @return array prefixed_title (string) => prefixed_title (string)
208          */
209         public function getRedirectTitles() {
210                 return $this->mRedirectTitles;
211         }
212
213         /**
214          * Get a list of title normalizations - maps a title to its normalized
215          * version.
216          * @return array raw_prefixed_title (string) => prefixed_title (string)
217          */
218         public function getNormalizedTitles() {
219                 return $this->mNormalizedTitles;
220         }
221
222         /**
223          * Get a list of interwiki titles - maps a title to its interwiki
224          * prefix.
225          * @return array raw_prefixed_title (string) => interwiki_prefix (string)
226          */
227         public function getInterwikiTitles() {
228                 return $this->mInterwikiTitles;
229         }
230
231         /**
232          * Get the list of revision IDs (requested with the revids= parameter)
233          * @return array revID (int) => pageID (int)
234          */
235         public function getRevisionIDs() {
236                 return $this->mGoodRevIDs;
237         }
238
239         /**
240          * Revision IDs that were not found in the database
241          * @return array of revision IDs
242          */
243         public function getMissingRevisionIDs() {
244                 return $this->mMissingRevIDs;
245         }
246
247         /**
248          * Returns the number of revisions (requested with revids= parameter)\
249          * @return int
250          */
251         public function getRevisionCount() {
252                 return count($this->getRevisionIDs());
253         }
254
255         /**
256          * Populate the PageSet from the request parameters.
257          */
258         public function execute() {
259                 $this->profileIn();
260                 $params = $this->extractRequestParams();
261
262                 // Only one of the titles/pageids/revids is allowed at the same time
263                 $dataSource = null;
264                 if (isset ($params['titles']))
265                         $dataSource = 'titles';
266                 if (isset ($params['pageids'])) {
267                         if (isset ($dataSource))
268                                 $this->dieUsage("Cannot use 'pageids' at the same time as '$dataSource'", 'multisource');
269                         $dataSource = 'pageids';
270                 }
271                 if (isset ($params['revids'])) {
272                         if (isset ($dataSource))
273                                 $this->dieUsage("Cannot use 'revids' at the same time as '$dataSource'", 'multisource');
274                         $dataSource = 'revids';
275                 }
276
277                 switch ($dataSource) {
278                         case 'titles' :
279                                 $this->initFromTitles($params['titles']);
280                                 break;
281                         case 'pageids' :
282                                 $this->initFromPageIds($params['pageids']);
283                                 break;
284                         case 'revids' :
285                                 if($this->mResolveRedirects)
286                                         $this->setWarning('Redirect resolution cannot be used together with the revids= parameter. '.
287                                         'Any redirects the revids= point to have not been resolved.');
288                                 $this->mResolveRedirects = false;
289                                 $this->initFromRevIDs($params['revids']);
290                                 break;
291                         default :
292                                 // Do nothing - some queries do not need any of the data sources.
293                                 break;
294                 }
295                 $this->profileOut();
296         }
297
298         /**
299          * Populate this PageSet from a list of Titles
300          * @param $titles array of Title objects
301          */
302         public function populateFromTitles($titles) {
303                 $this->profileIn();
304                 $this->initFromTitles($titles);
305                 $this->profileOut();
306         }
307
308         /**
309          * Populate this PageSet from a list of page IDs
310          * @param $pageIDs array of page IDs
311          */
312         public function populateFromPageIDs($pageIDs) {
313                 $this->profileIn();
314                 $this->initFromPageIds($pageIDs);
315                 $this->profileOut();
316         }
317
318         /**
319          * Populate this PageSet from a rowset returned from the database
320          * @param $db Database object
321          * @param $queryResult Query result object
322          */
323         public function populateFromQueryResult($db, $queryResult) {
324                 $this->profileIn();
325                 $this->initFromQueryResult($db, $queryResult);
326                 $this->profileOut();
327         }
328
329         /**
330          * Populate this PageSet from a list of revision IDs
331          * @param $revIDs array of revision IDs
332          */
333         public function populateFromRevisionIDs($revIDs) {
334                 $this->profileIn();
335                 $this->initFromRevIDs($revIDs);
336                 $this->profileOut();
337         }
338
339         /**
340          * Extract all requested fields from the row received from the database
341          * @param $row Result row
342          */
343         public function processDbRow($row) {
344
345                 // Store Title object in various data structures
346                 $title = Title :: makeTitle($row->page_namespace, $row->page_title);
347
348                 $pageId = intval($row->page_id);
349                 $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
350                 $this->mTitles[] = $title;
351
352                 if ($this->mResolveRedirects && $row->page_is_redirect == '1') {
353                         $this->mPendingRedirectIDs[$pageId] = $title;
354                 } else {
355                         $this->mGoodTitles[$pageId] = $title;
356                 }
357
358                 foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues)
359                         $fieldValues[$pageId] = $row-> $fieldName;
360         }
361
362         /**
363          * Resolve redirects, if applicable
364          */
365         public function finishPageSetGeneration() {
366                 $this->profileIn();
367                 $this->resolvePendingRedirects();
368                 $this->profileOut();
369         }
370
371         /**
372          * This method populates internal variables with page information
373          * based on the given array of title strings.
374          *
375          * Steps:
376          * #1 For each title, get data from `page` table
377          * #2 If page was not found in the DB, store it as missing
378          *
379          * Additionally, when resolving redirects:
380          * #3 If no more redirects left, stop.
381          * #4 For each redirect, get its target from the `redirect` table.
382          * #5 Substitute the original LinkBatch object with the new list
383          * #6 Repeat from step #1
384          *
385          * @param $titles array of Title objects or strings
386          */
387         private function initFromTitles($titles) {
388
389                 // Get validated and normalized title objects
390                 $linkBatch = $this->processTitlesArray($titles);
391                 if($linkBatch->isEmpty())
392                         return;
393
394                 $db = $this->getDB();
395                 $set = $linkBatch->constructSet('page', $db);
396
397                 // Get pageIDs data from the `page` table
398                 $this->profileDBIn();
399                 $res = $db->select('page', $this->getPageTableFields(), $set,
400                                         __METHOD__);
401                 $this->profileDBOut();
402
403                 // Hack: get the ns:titles stored in array(ns => array(titles)) format
404                 $this->initFromQueryResult($db, $res, $linkBatch->data, true);  // process Titles
405
406                 // Resolve any found redirects
407                 $this->resolvePendingRedirects();
408         }
409
410         /**
411          * Does the same as initFromTitles(), but is based on page IDs instead
412          * @param $pageids array of page IDs
413          */
414         private function initFromPageIds($pageids) {
415                 if(!count($pageids))
416                         return;
417
418                 $pageids = array_map('intval', $pageids); // paranoia
419                 $set = array (
420                         'page_id' => $pageids
421                 );
422                 $db = $this->getDB();
423
424                 // Get pageIDs data from the `page` table
425                 $this->profileDBIn();
426                 $res = $db->select('page', $this->getPageTableFields(), $set,
427                                         __METHOD__);
428                 $this->profileDBOut();
429
430                 $remaining = array_flip($pageids);
431                 $this->initFromQueryResult($db, $res, $remaining, false);       // process PageIDs
432
433                 // Resolve any found redirects
434                 $this->resolvePendingRedirects();
435         }
436
437         /**
438          * Iterate through the result of the query on 'page' table,
439          * and for each row create and store title object and save any extra fields requested.
440          * @param $db Database
441          * @param $res DB Query result
442          * @param $remaining array of either pageID or ns/title elements (optional).
443          *        If given, any missing items will go to $mMissingPageIDs and $mMissingTitles
444          * @param $processTitles bool Must be provided together with $remaining.
445          *        If true, treat $remaining as an array of [ns][title]
446          *        If false, treat it as an array of [pageIDs]
447          */
448         private function initFromQueryResult($db, $res, &$remaining = null, $processTitles = null) {
449                 if (!is_null($remaining) && is_null($processTitles))
450                         ApiBase :: dieDebug(__METHOD__, 'Missing $processTitles parameter when $remaining is provided');
451
452                 while ($row = $db->fetchObject($res)) {
453
454                         $pageId = intval($row->page_id);
455
456                         // Remove found page from the list of remaining items
457                         if (isset($remaining)) {
458                                 if ($processTitles)
459                                         unset ($remaining[$row->page_namespace][$row->page_title]);
460                                 else
461                                         unset ($remaining[$pageId]);
462                         }
463
464                         // Store any extra fields requested by modules
465                         $this->processDbRow($row);
466                 }
467                 $db->freeResult($res);
468
469                 if(isset($remaining)) {
470                         // Any items left in the $remaining list are added as missing
471                         if($processTitles) {
472                                 // The remaining titles in $remaining are non-existent pages
473                                 foreach ($remaining as $ns => $dbkeys) {
474                                         foreach ( $dbkeys as $dbkey => $unused ) {
475                                                 $title = Title :: makeTitle($ns, $dbkey);
476                                                 $this->mAllPages[$ns][$dbkey] = $this->mFakePageId;
477                                                 $this->mMissingTitles[$this->mFakePageId] = $title;
478                                                 $this->mFakePageId--;
479                                                 $this->mTitles[] = $title;
480                                         }
481                                 }
482                         }
483                         else
484                         {
485                                 // The remaining pageids do not exist
486                                 if(!$this->mMissingPageIDs)
487                                         $this->mMissingPageIDs = array_keys($remaining);
488                                 else
489                                         $this->mMissingPageIDs = array_merge($this->mMissingPageIDs, array_keys($remaining));
490                         }
491                 }
492         }
493
494         /**
495          * Does the same as initFromTitles(), but is based on revision IDs
496          * instead
497          * @param $revids array of revision IDs
498          */
499         private function initFromRevIDs($revids) {
500
501                 if(!count($revids))
502                         return;
503
504                 $revids = array_map('intval', $revids); // paranoia
505                 $db = $this->getDB();
506                 $pageids = array();
507                 $remaining = array_flip($revids);
508
509                 $tables = array('revision', 'page');
510                 $fields = array('rev_id', 'rev_page');
511                 $where = array('rev_id' => $revids, 'rev_page = page_id');
512
513                 // Get pageIDs data from the `page` table
514                 $this->profileDBIn();
515                 $res = $db->select($tables, $fields, $where,  __METHOD__);
516                 while ($row = $db->fetchObject($res)) {
517                         $revid = intval($row->rev_id);
518                         $pageid = intval($row->rev_page);
519                         $this->mGoodRevIDs[$revid] = $pageid;
520                         $pageids[$pageid] = '';
521                         unset($remaining[$revid]);
522                 }
523                 $db->freeResult($res);
524                 $this->profileDBOut();
525
526                 $this->mMissingRevIDs = array_keys($remaining);
527
528                 // Populate all the page information
529                 $this->initFromPageIds(array_keys($pageids));
530         }
531
532         /**
533          * Resolve any redirects in the result if redirect resolution was
534          * requested. This function is called repeatedly until all redirects
535          * have been resolved.
536          */
537         private function resolvePendingRedirects() {
538
539                 if($this->mResolveRedirects) {
540                         $db = $this->getDB();
541                         $pageFlds = $this->getPageTableFields();
542
543                         // Repeat until all redirects have been resolved
544                         // The infinite loop is prevented by keeping all known pages in $this->mAllPages
545                         while ($this->mPendingRedirectIDs) {
546
547                                 // Resolve redirects by querying the pagelinks table, and repeat the process
548                                 // Create a new linkBatch object for the next pass
549                                 $linkBatch = $this->getRedirectTargets();
550
551                                 if ($linkBatch->isEmpty())
552                                         break;
553
554                                 $set = $linkBatch->constructSet('page', $db);
555                                 if($set === false)
556                                         break;
557
558                                 // Get pageIDs data from the `page` table
559                                 $this->profileDBIn();
560                                 $res = $db->select('page', $pageFlds, $set, __METHOD__);
561                                 $this->profileDBOut();
562
563                                 // Hack: get the ns:titles stored in array(ns => array(titles)) format
564                                 $this->initFromQueryResult($db, $res, $linkBatch->data, true);
565                         }
566                 }
567         }
568
569         /**
570          * Get the targets of the pending redirects from the database
571          *
572          * Also creates entries in the redirect table for redirects that don't
573          * have one.
574          * @return LinkBatch
575          */
576         private function getRedirectTargets() {
577                 $lb = new LinkBatch();
578                 $db = $this->getDB();
579
580                 $this->profileDBIn();
581                 $res = $db->select('redirect', array(
582                                 'rd_from',
583                                 'rd_namespace',
584                                 'rd_title'
585                         ), array('rd_from' => array_keys($this->mPendingRedirectIDs)),
586                         __METHOD__
587                 );
588                 $this->profileDBOut();
589
590                 while($row = $db->fetchObject($res))
591                 {
592                         $rdfrom = intval($row->rd_from);
593                         $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText();
594                         $to = Title::makeTitle($row->rd_namespace, $row->rd_title)->getPrefixedText();
595                         unset($this->mPendingRedirectIDs[$rdfrom]);
596                         if(!isset($this->mAllPages[$row->rd_namespace][$row->rd_title]))
597                                 $lb->add($row->rd_namespace, $row->rd_title);
598                         $this->mRedirectTitles[$from] = $to;
599                 }
600                 $db->freeResult($res);
601                 if($this->mPendingRedirectIDs)
602                 {
603                         # We found pages that aren't in the redirect table
604                         # Add them
605                         foreach($this->mPendingRedirectIDs as $id => $title)
606                         {
607                                 $article = new Article($title);
608                                 $rt = $article->insertRedirect();
609                                 if(!$rt)
610                                         # What the hell. Let's just ignore this
611                                         continue;
612                                 $lb->addObj($rt);
613                                 $this->mRedirectTitles[$title->getPrefixedText()] = $rt->getPrefixedText();
614                                 unset($this->mPendingRedirectIDs[$id]);
615                         }
616                 }
617                 return $lb;
618         }
619
620         /**
621          * Given an array of title strings, convert them into Title objects.
622          * Alternativelly, an array of Title objects may be given.
623          * This method validates access rights for the title,
624          * and appends normalization values to the output.
625          *
626          * @param $titles array of Title objects or strings
627          * @return LinkBatch
628          */
629         private function processTitlesArray($titles) {
630
631                 $linkBatch = new LinkBatch();
632
633                 foreach ($titles as $title) {
634
635                         $titleObj = is_string($title) ? Title :: newFromText($title) : $title;
636                         if (!$titleObj)
637                         {
638                                 # Handle invalid titles gracefully
639                                 $this->mAllpages[0][$title] = $this->mFakePageId;
640                                 $this->mInvalidTitles[$this->mFakePageId] = $title;
641                                 $this->mFakePageId--;
642                                 continue; // There's nothing else we can do
643                         }
644                         $iw = $titleObj->getInterwiki();
645                         if (strval($iw) !== '') {
646                                 // This title is an interwiki link.
647                                 $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw;
648                         } else {
649
650                                 // Validation
651                                 if ($titleObj->getNamespace() < 0)
652                                         $this->setWarning("No support for special pages has been implemented");
653                                 else
654                                         $linkBatch->addObj($titleObj);
655                         }
656
657                         // Make sure we remember the original title that was
658                         // given to us. This way the caller can correlate new
659                         // titles with the originally requested when e.g. the
660                         // namespace is localized or the capitalization is
661                         // different
662                         if (is_string($title) && $title !== $titleObj->getPrefixedText()) {
663                                 $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
664                         }
665                 }
666
667                 return $linkBatch;
668         }
669
670         protected function getAllowedParams() {
671                 return array (
672                         'titles' => array (
673                                 ApiBase :: PARAM_ISMULTI => true
674                         ),
675                         'pageids' => array (
676                                 ApiBase :: PARAM_TYPE => 'integer',
677                                 ApiBase :: PARAM_ISMULTI => true
678                         ),
679                         'revids' => array (
680                                 ApiBase :: PARAM_TYPE => 'integer',
681                                 ApiBase :: PARAM_ISMULTI => true
682                         )
683                 );
684         }
685
686         protected function getParamDescription() {
687                 return array (
688                         'titles' => 'A list of titles to work on',
689                         'pageids' => 'A list of page IDs to work on',
690                         'revids' => 'A list of revision IDs to work on'
691                 );
692         }
693
694         public function getVersion() {
695                 return __CLASS__ . ': $Id: ApiPageSet.php 47424 2009-02-18 05:29:11Z werdna $';
696         }
697 }