(bug 15535) prop=info&inprop=protection doesn't list pre-1.10 protections if the...
[lhc/web/wiklou.git] / includes / api / ApiQueryInfo.php
1 <?php
2
3 /*
4 * Created on Sep 25, 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 * A query module to show basic page information.
33 *
34 * @ingroup API
35 */
36 class ApiQueryInfo extends ApiQueryBase {
37
38 public function __construct($query, $moduleName) {
39 parent :: __construct($query, $moduleName, 'in');
40 }
41
42 public function requestExtraData($pageSet) {
43 $pageSet->requestField('page_restrictions');
44 $pageSet->requestField('page_is_redirect');
45 $pageSet->requestField('page_is_new');
46 $pageSet->requestField('page_counter');
47 $pageSet->requestField('page_touched');
48 $pageSet->requestField('page_latest');
49 $pageSet->requestField('page_len');
50 }
51
52 protected function getTokenFunctions() {
53 // tokenname => function
54 // function prototype is func($pageid, $title)
55 // should return token or false
56
57 // Don't call the hooks twice
58 if(isset($this->tokenFunctions))
59 return $this->tokenFunctions;
60
61 // If we're in JSON callback mode, no tokens can be obtained
62 if(!is_null($this->getMain()->getRequest()->getVal('callback')))
63 return array();
64
65 $this->tokenFunctions = array(
66 'edit' => array( 'ApiQueryInfo', 'getEditToken' ),
67 'delete' => array( 'ApiQueryInfo', 'getDeleteToken' ),
68 'protect' => array( 'ApiQueryInfo', 'getProtectToken' ),
69 'move' => array( 'ApiQueryInfo', 'getMoveToken' ),
70 'block' => array( 'ApiQueryInfo', 'getBlockToken' ),
71 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ),
72 'email' => array( 'ApiQueryInfo', 'getEmailToken' ),
73 );
74 wfRunHooks('APIQueryInfoTokens', array(&$this->tokenFunctions));
75 return $this->tokenFunctions;
76 }
77
78 public static function getEditToken($pageid, $title)
79 {
80 // We could check for $title->userCan('edit') here,
81 // but that's too expensive for this purpose
82 global $wgUser;
83 if(!$wgUser->isAllowed('edit'))
84 return false;
85
86 // The edit token is always the same, let's exploit that
87 static $cachedEditToken = null;
88 if(!is_null($cachedEditToken))
89 return $cachedEditToken;
90
91 $cachedEditToken = $wgUser->editToken();
92 return $cachedEditToken;
93 }
94
95 public static function getDeleteToken($pageid, $title)
96 {
97 global $wgUser;
98 if(!$wgUser->isAllowed('delete'))
99 return false;
100
101 static $cachedDeleteToken = null;
102 if(!is_null($cachedDeleteToken))
103 return $cachedDeleteToken;
104
105 $cachedDeleteToken = $wgUser->editToken();
106 return $cachedDeleteToken;
107 }
108
109 public static function getProtectToken($pageid, $title)
110 {
111 global $wgUser;
112 if(!$wgUser->isAllowed('protect'))
113 return false;
114
115 static $cachedProtectToken = null;
116 if(!is_null($cachedProtectToken))
117 return $cachedProtectToken;
118
119 $cachedProtectToken = $wgUser->editToken();
120 return $cachedProtectToken;
121 }
122
123 public static function getMoveToken($pageid, $title)
124 {
125 global $wgUser;
126 if(!$wgUser->isAllowed('move'))
127 return false;
128
129 static $cachedMoveToken = null;
130 if(!is_null($cachedMoveToken))
131 return $cachedMoveToken;
132
133 $cachedMoveToken = $wgUser->editToken();
134 return $cachedMoveToken;
135 }
136
137 public static function getBlockToken($pageid, $title)
138 {
139 global $wgUser;
140 if(!$wgUser->isAllowed('block'))
141 return false;
142
143 static $cachedBlockToken = null;
144 if(!is_null($cachedBlockToken))
145 return $cachedBlockToken;
146
147 $cachedBlockToken = $wgUser->editToken();
148 return $cachedBlockToken;
149 }
150
151 public static function getUnblockToken($pageid, $title)
152 {
153 // Currently, this is exactly the same as the block token
154 return self::getBlockToken($pageid, $title);
155 }
156
157 public static function getEmailToken($pageid, $title)
158 {
159 global $wgUser;
160 if(!$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailUser())
161 return false;
162
163 static $cachedEmailToken = null;
164 if(!is_null($cachedEmailToken))
165 return $cachedEmailToken;
166
167 $cachedEmailToken = $wgUser->editToken();
168 return $cachedEmailToken;
169 }
170
171 public function execute() {
172
173 global $wgUser;
174
175 $params = $this->extractRequestParams();
176 $fld_protection = $fld_talkid = $fld_subjectid = false;
177 if(!is_null($params['prop'])) {
178 $prop = array_flip($params['prop']);
179 $fld_protection = isset($prop['protection']);
180 $fld_talkid = isset($prop['talkid']);
181 $fld_subjectid = isset($prop['subjectid']);
182 }
183
184 $pageSet = $this->getPageSet();
185 $titles = $pageSet->getGoodTitles();
186 $missing = $pageSet->getMissingTitles();
187 $result = $this->getResult();
188
189 $pageRestrictions = $pageSet->getCustomField('page_restrictions');
190 $pageIsRedir = $pageSet->getCustomField('page_is_redirect');
191 $pageIsNew = $pageSet->getCustomField('page_is_new');
192 $pageCounter = $pageSet->getCustomField('page_counter');
193 $pageTouched = $pageSet->getCustomField('page_touched');
194 $pageLatest = $pageSet->getCustomField('page_latest');
195 $pageLength = $pageSet->getCustomField('page_len');
196
197 $db = $this->getDB();
198 if ($fld_protection && !empty($titles)) {
199 $this->addTables('page_restrictions');
200 $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade'));
201 $this->addWhereFld('pr_page', array_keys($titles));
202
203 $res = $this->select(__METHOD__);
204 while($row = $db->fetchObject($res)) {
205 $a = array(
206 'type' => $row->pr_type,
207 'level' => $row->pr_level,
208 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 )
209 );
210 if($row->pr_cascade)
211 $a['cascade'] = '';
212 $protections[$row->pr_page][] = $a;
213 }
214 $db->freeResult($res);
215
216 $imageIds = array();
217 foreach ($titles as $id => $title)
218 if ($title->getNamespace() == NS_IMAGE)
219 $imageIds[] = $id;
220 // To avoid code duplication
221 $cascadeTypes = array(
222 array(
223 'prefix' => 'tl',
224 'table' => 'templatelinks',
225 'ns' => 'tl_namespace',
226 'title' => 'tl_title',
227 'ids' => array_diff(array_keys($titles), $imageIds)
228 ),
229 array(
230 'prefix' => 'il',
231 'table' => 'imagelinks',
232 'ns' => NS_IMAGE,
233 'title' => 'il_to',
234 'ids' => $imageIds
235 )
236 );
237
238 foreach ($cascadeTypes as $type)
239 {
240 if (count($type['ids']) != 0) {
241 $this->resetQueryParams();
242 $this->addTables(array('page_restrictions', $type['table']));
243 $this->addTables('page', 'page_source');
244 $this->addTables('page', 'page_target');
245 $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
246 'page_target.page_id AS page_target_id',
247 'page_source.page_namespace AS page_source_namespace',
248 'page_source.page_title AS page_source_title'));
249 $this->addWhere(array("{$type['prefix']}_from = pr_page",
250 'page_target.page_namespace = '.$type['ns'],
251 'page_target.page_title = '.$type['title'],
252 'page_source.page_id = pr_page'
253 ));
254 $this->addWhereFld('pr_cascade', 1);
255 $this->addWhereFld('page_target.page_id', $type['ids']);
256
257 $res = $this->select(__METHOD__);
258 while($row = $db->fetchObject($res)) {
259 $source = Title::makeTitle($row->page_source_namespace, $row->page_source_title);
260 $a = array(
261 'type' => $row->pr_type,
262 'level' => $row->pr_level,
263 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
264 'source' => $source->getPrefixedText()
265 );
266 $protections[$row->page_target_id][] = $a;
267 }
268 $db->freeResult($res);
269 }
270 }
271 }
272
273 // We don't need to check for pt stuff if there are no nonexistent titles
274 if($fld_protection && !empty($missing))
275 {
276 $this->resetQueryParams();
277 // Construct a custom WHERE clause that matches all titles in $missing
278 $lb = new LinkBatch($missing);
279 $this->addTables('protected_titles');
280 $this->addFields(array('pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry'));
281 $this->addWhere($lb->constructSet('pt', $db));
282 $res = $this->select(__METHOD__);
283 $prottitles = array();
284 while($row = $db->fetchObject($res)) {
285 $prottitles[$row->pt_namespace][$row->pt_title][] = array(
286 'type' => 'create',
287 'level' => $row->pt_create_perm,
288 'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601)
289 );
290 }
291 $db->freeResult($res);
292
293 $images = array();
294 $others = array();
295 foreach ($missing as $title)
296 if ($title->getNamespace() == NS_IMAGE)
297 $images[] = $title->getDbKey();
298 else
299 $others[] = $title;
300
301 if (count($others) != 0) {
302 $lb = new LinkBatch($others);
303 $this->resetQueryParams();
304 $this->addTables(array('page_restrictions', 'page', 'templatelinks'));
305 $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
306 'page_title', 'page_namespace',
307 'tl_title', 'tl_namespace'));
308 $this->addWhere($lb->constructSet('tl', $db));
309 $this->addWhere('pr_page = page_id');
310 $this->addWhere('pr_page = tl_from');
311 $this->addWhereFld('pr_cascade', 1);
312
313 $res = $this->select(__METHOD__);
314 while($row = $db->fetchObject($res)) {
315 $source = Title::makeTitle($row->page_namespace, $row->page_title);
316 $a = array(
317 'type' => $row->pr_type,
318 'level' => $row->pr_level,
319 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
320 'source' => $source->getPrefixedText()
321 );
322 $prottitles[$row->tl_namespace][$row->tl_title][] = $a;
323 }
324 $db->freeResult($res);
325 }
326
327 if (count($images) != 0) {
328 $this->resetQueryParams();
329 $this->addTables(array('page_restrictions', 'page', 'imagelinks'));
330 $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
331 'page_title', 'page_namespace', 'il_to'));
332 $this->addWhere('pr_page = page_id');
333 $this->addWhere('pr_page = il_from');
334 $this->addWhereFld('pr_cascade', 1);
335 $this->addWhereFld('il_to', $images);
336
337 $res = $this->select(__METHOD__);
338 while($row = $db->fetchObject($res)) {
339 $source = Title::makeTitle($row->page_namespace, $row->page_title);
340 $a = array(
341 'type' => $row->pr_type,
342 'level' => $row->pr_level,
343 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
344 'source' => $source->getPrefixedText()
345 );
346 $prottitles[NS_IMAGE][$row->il_to][] = $a;
347 }
348 $db->freeResult($res);
349 }
350 }
351
352 // Run the talkid/subjectid query
353 if($fld_talkid || $fld_subjectid)
354 {
355 $talktitles = $subjecttitles =
356 $talkids = $subjectids = array();
357 $everything = array_merge($titles, $missing);
358 foreach($everything as $t)
359 {
360 if(MWNamespace::isTalk($t->getNamespace()))
361 {
362 if($fld_subjectid)
363 $subjecttitles[] = $t->getSubjectPage();
364 }
365 else if($fld_talkid)
366 $talktitles[] = $t->getTalkPage();
367 }
368 if(!empty($talktitles) || !empty($subjecttitles))
369 {
370 // Construct a custom WHERE clause that matches
371 // all titles in $talktitles and $subjecttitles
372 $lb = new LinkBatch(array_merge($talktitles, $subjecttitles));
373 $this->resetQueryParams();
374 $this->addTables('page');
375 $this->addFields(array('page_title', 'page_namespace', 'page_id'));
376 $this->addWhere($lb->constructSet('page', $db));
377 $res = $this->select(__METHOD__);
378 while($row = $db->fetchObject($res))
379 {
380 if(MWNamespace::isTalk($row->page_namespace))
381 $talkids[MWNamespace::getSubject($row->page_namespace)][$row->page_title] = $row->page_id;
382 else
383 $subjectids[MWNamespace::getTalk($row->page_namespace)][$row->page_title] = $row->page_id;
384 }
385 }
386 }
387
388 foreach ( $titles as $pageid => $title ) {
389 $pageInfo = array (
390 'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]),
391 'lastrevid' => intval($pageLatest[$pageid]),
392 'counter' => intval($pageCounter[$pageid]),
393 'length' => intval($pageLength[$pageid]),
394 );
395
396 if ($pageIsRedir[$pageid])
397 $pageInfo['redirect'] = '';
398
399 if ($pageIsNew[$pageid])
400 $pageInfo['new'] = '';
401
402 if (!is_null($params['token'])) {
403 $tokenFunctions = $this->getTokenFunctions();
404 foreach($params['token'] as $t)
405 {
406 $val = call_user_func($tokenFunctions[$t], $pageid, $title);
407 if($val === false)
408 $this->setWarning("Action '$t' is not allowed for the current user");
409 else
410 $pageInfo[$t . 'token'] = $val;
411 }
412 }
413
414 if($fld_protection) {
415 $pageInfo['protection'] = array();
416 if (isset($protections[$pageid])) {
417 $pageInfo['protection'] = $protections[$pageid];
418 $result->setIndexedTagName($pageInfo['protection'], 'pr');
419 }
420 # Also check old restrictions
421 if( $pageRestrictions[$pageid] ) {
422 foreach( explode( ':', trim( $pageRestrictions[$pageid] ) ) as $restrict ) {
423 $temp = explode( '=', trim( $restrict ) );
424 if(count($temp) == 1) {
425 // old old format should be treated as edit/move restriction
426 $restriction = trim( $temp[0] );
427 $pageInfo['protection'][] = array(
428 'type' => 'edit',
429 'level' => $restriction,
430 'expiry' => 'infinity',
431 );
432 $pageInfo['protection'][] = array(
433 'type' => 'move',
434 'level' => $restriction,
435 'expiry' => 'infinity',
436 );
437 } else {
438 $restriction = trim( $temp[1] );
439 $pageInfo['protection'][] = array(
440 'type' => $temp[0],
441 'level' => $restriction,
442 'expiry' => 'infinity',
443 );
444 }
445 }
446 $result->setIndexedTagName($pageInfo['protection'], 'pr');
447 }
448 }
449 if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()]))
450 $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()];
451 if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()]))
452 $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()];
453
454 $result->addValue(array (
455 'query',
456 'pages'
457 ), $pageid, $pageInfo);
458 }
459
460 // Get edit/protect tokens and protection data for missing titles if requested
461 // Delete and move tokens are N/A for missing titles anyway
462 if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid)
463 {
464 $res = &$result->getData();
465 foreach($missing as $pageid => $title) {
466 if(!is_null($params['token']))
467 {
468 $tokenFunctions = $this->getTokenFunctions();
469 foreach($params['token'] as $t)
470 {
471 $val = call_user_func($tokenFunctions[$t], $pageid, $title);
472 if($val !== false)
473 $res['query']['pages'][$pageid][$t . 'token'] = $val;
474 }
475 }
476 if($fld_protection)
477 {
478 // Apparently the XML formatting code doesn't like array(null)
479 // This is painful to fix, so we'll just work around it
480 if(isset($prottitles[$title->getNamespace()][$title->getDBkey()]))
481 $res['query']['pages'][$pageid]['protection'] = $prottitles[$title->getNamespace()][$title->getDBkey()];
482 else
483 $res['query']['pages'][$pageid]['protection'] = array();
484 $result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr');
485 }
486 if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()]))
487 $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()];
488 if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()]))
489 $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()];
490 }
491 }
492 }
493
494 public function getAllowedParams() {
495 return array (
496 'prop' => array (
497 ApiBase :: PARAM_DFLT => NULL,
498 ApiBase :: PARAM_ISMULTI => true,
499 ApiBase :: PARAM_TYPE => array (
500 'protection',
501 'talkid',
502 'subjectid'
503 )),
504 'token' => array (
505 ApiBase :: PARAM_DFLT => NULL,
506 ApiBase :: PARAM_ISMULTI => true,
507 ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions())
508 )
509 );
510 }
511
512 public function getParamDescription() {
513 return array (
514 'prop' => array (
515 'Which additional properties to get:',
516 ' "protection" - List the protection level of each page',
517 ' "talkid" - The page ID of the talk page for each non-talk page',
518 ' "subjectid" - The page ID of the parent page for each talk page'
519 ),
520 'token' => 'Request a token to perform a data-modifying action on a page',
521 );
522 }
523
524
525 public function getDescription() {
526 return 'Get basic page information such as namespace, title, last touched date, ...';
527 }
528
529 protected function getExamples() {
530 return array (
531 'api.php?action=query&prop=info&titles=Main%20Page',
532 'api.php?action=query&prop=info&inprop=protection&titles=Main%20Page'
533 );
534 }
535
536 public function getVersion() {
537 return __CLASS__ . ': $Id$';
538 }
539 }