API: (bug 16629) "edit=:move=" in page.page_restrictions was interpreted incorrectly...
[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 = $fld_url = $fld_readable = 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 $fld_url = isset($prop['url']);
183 $fld_readable = isset($prop['readable']);
184 }
185
186 $pageSet = $this->getPageSet();
187 $titles = $pageSet->getGoodTitles();
188 $missing = $pageSet->getMissingTitles();
189 $result = $this->getResult();
190
191 $pageRestrictions = $pageSet->getCustomField('page_restrictions');
192 $pageIsRedir = $pageSet->getCustomField('page_is_redirect');
193 $pageIsNew = $pageSet->getCustomField('page_is_new');
194 $pageCounter = $pageSet->getCustomField('page_counter');
195 $pageTouched = $pageSet->getCustomField('page_touched');
196 $pageLatest = $pageSet->getCustomField('page_latest');
197 $pageLength = $pageSet->getCustomField('page_len');
198
199 $db = $this->getDB();
200 if ($fld_protection && count($titles)) {
201 $this->addTables('page_restrictions');
202 $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade'));
203 $this->addWhereFld('pr_page', array_keys($titles));
204
205 $res = $this->select(__METHOD__);
206 while($row = $db->fetchObject($res)) {
207 $a = array(
208 'type' => $row->pr_type,
209 'level' => $row->pr_level,
210 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 )
211 );
212 if($row->pr_cascade)
213 $a['cascade'] = '';
214 $protections[$row->pr_page][] = $a;
215
216 # Also check old restrictions
217 if($pageRestrictions[$row->pr_page]) {
218 foreach(explode(':', trim($pageRestrictions[$pageid])) as $restrict) {
219 $temp = explode('=', trim($restrict));
220 if(count($temp) == 1) {
221 // old old format should be treated as edit/move restriction
222 $restriction = trim( $temp[0] );
223 if($restriction == '')
224 continue;
225 $protections[$row->pr_page][] = array(
226 'type' => 'edit',
227 'level' => $restriction,
228 'expiry' => 'infinity',
229 );
230 $protections[$row->pr_page][] = array(
231 'type' => 'move',
232 'level' => $restriction,
233 'expiry' => 'infinity',
234 );
235 } else {
236 $restriction = trim( $temp[1] );
237 if($restriction == '')
238 continue;
239 $protections[$row->pr_page][] = array(
240 'type' => $temp[0],
241 'level' => $restriction,
242 'expiry' => 'infinity',
243 );
244 }
245 }
246 }
247 }
248 $db->freeResult($res);
249
250 $imageIds = array();
251 foreach ($titles as $id => $title)
252 if ($title->getNamespace() == NS_FILE)
253 $imageIds[] = $id;
254 // To avoid code duplication
255 $cascadeTypes = array(
256 array(
257 'prefix' => 'tl',
258 'table' => 'templatelinks',
259 'ns' => 'tl_namespace',
260 'title' => 'tl_title',
261 'ids' => array_diff(array_keys($titles), $imageIds)
262 ),
263 array(
264 'prefix' => 'il',
265 'table' => 'imagelinks',
266 'ns' => NS_FILE,
267 'title' => 'il_to',
268 'ids' => $imageIds
269 )
270 );
271
272 foreach ($cascadeTypes as $type)
273 {
274 if (count($type['ids']) != 0) {
275 $this->resetQueryParams();
276 $this->addTables(array('page_restrictions', $type['table']));
277 $this->addTables('page', 'page_source');
278 $this->addTables('page', 'page_target');
279 $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
280 'page_target.page_id AS page_target_id',
281 'page_source.page_namespace AS page_source_namespace',
282 'page_source.page_title AS page_source_title'));
283 $this->addWhere(array("{$type['prefix']}_from = pr_page",
284 'page_target.page_namespace = '.$type['ns'],
285 'page_target.page_title = '.$type['title'],
286 'page_source.page_id = pr_page'
287 ));
288 $this->addWhereFld('pr_cascade', 1);
289 $this->addWhereFld('page_target.page_id', $type['ids']);
290
291 $res = $this->select(__METHOD__);
292 while($row = $db->fetchObject($res)) {
293 $source = Title::makeTitle($row->page_source_namespace, $row->page_source_title);
294 $a = array(
295 'type' => $row->pr_type,
296 'level' => $row->pr_level,
297 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
298 'source' => $source->getPrefixedText()
299 );
300 $protections[$row->page_target_id][] = $a;
301 }
302 $db->freeResult($res);
303 }
304 }
305 }
306
307 // We don't need to check for pt stuff if there are no nonexistent titles
308 if($fld_protection && count($missing))
309 {
310 $this->resetQueryParams();
311 // Construct a custom WHERE clause that matches all titles in $missing
312 $lb = new LinkBatch($missing);
313 $this->addTables('protected_titles');
314 $this->addFields(array('pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry'));
315 $this->addWhere($lb->constructSet('pt', $db));
316 $res = $this->select(__METHOD__);
317 $prottitles = array();
318 while($row = $db->fetchObject($res)) {
319 $prottitles[$row->pt_namespace][$row->pt_title][] = array(
320 'type' => 'create',
321 'level' => $row->pt_create_perm,
322 'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601)
323 );
324 }
325 $db->freeResult($res);
326
327 $images = array();
328 $others = array();
329 foreach ($missing as $title)
330 if ($title->getNamespace() == NS_FILE)
331 $images[] = $title->getDBKey();
332 else
333 $others[] = $title;
334
335 if (count($others) != 0) {
336 $lb = new LinkBatch($others);
337 $this->resetQueryParams();
338 $this->addTables(array('page_restrictions', 'page', 'templatelinks'));
339 $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
340 'page_title', 'page_namespace',
341 'tl_title', 'tl_namespace'));
342 $this->addWhere($lb->constructSet('tl', $db));
343 $this->addWhere('pr_page = page_id');
344 $this->addWhere('pr_page = tl_from');
345 $this->addWhereFld('pr_cascade', 1);
346
347 $res = $this->select(__METHOD__);
348 while($row = $db->fetchObject($res)) {
349 $source = Title::makeTitle($row->page_namespace, $row->page_title);
350 $a = array(
351 'type' => $row->pr_type,
352 'level' => $row->pr_level,
353 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
354 'source' => $source->getPrefixedText()
355 );
356 $prottitles[$row->tl_namespace][$row->tl_title][] = $a;
357 }
358 $db->freeResult($res);
359 }
360
361 if (count($images) != 0) {
362 $this->resetQueryParams();
363 $this->addTables(array('page_restrictions', 'page', 'imagelinks'));
364 $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
365 'page_title', 'page_namespace', 'il_to'));
366 $this->addWhere('pr_page = page_id');
367 $this->addWhere('pr_page = il_from');
368 $this->addWhereFld('pr_cascade', 1);
369 $this->addWhereFld('il_to', $images);
370
371 $res = $this->select(__METHOD__);
372 while($row = $db->fetchObject($res)) {
373 $source = Title::makeTitle($row->page_namespace, $row->page_title);
374 $a = array(
375 'type' => $row->pr_type,
376 'level' => $row->pr_level,
377 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
378 'source' => $source->getPrefixedText()
379 );
380 $prottitles[NS_FILE][$row->il_to][] = $a;
381 }
382 $db->freeResult($res);
383 }
384 }
385
386 // Run the talkid/subjectid query
387 if($fld_talkid || $fld_subjectid)
388 {
389 $talktitles = $subjecttitles =
390 $talkids = $subjectids = array();
391 $everything = array_merge($titles, $missing);
392 foreach($everything as $t)
393 {
394 if(MWNamespace::isTalk($t->getNamespace()))
395 {
396 if($fld_subjectid)
397 $subjecttitles[] = $t->getSubjectPage();
398 }
399 else if($fld_talkid)
400 $talktitles[] = $t->getTalkPage();
401 }
402 if(count($talktitles) || count($subjecttitles))
403 {
404 // Construct a custom WHERE clause that matches
405 // all titles in $talktitles and $subjecttitles
406 $lb = new LinkBatch(array_merge($talktitles, $subjecttitles));
407 $this->resetQueryParams();
408 $this->addTables('page');
409 $this->addFields(array('page_title', 'page_namespace', 'page_id'));
410 $this->addWhere($lb->constructSet('page', $db));
411 $res = $this->select(__METHOD__);
412 while($row = $db->fetchObject($res))
413 {
414 if(MWNamespace::isTalk($row->page_namespace))
415 $talkids[MWNamespace::getSubject($row->page_namespace)][$row->page_title] = $row->page_id;
416 else
417 $subjectids[MWNamespace::getTalk($row->page_namespace)][$row->page_title] = $row->page_id;
418 }
419 }
420 }
421
422 foreach ( $titles as $pageid => $title ) {
423 $pageInfo = array (
424 'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]),
425 'lastrevid' => intval($pageLatest[$pageid]),
426 'counter' => intval($pageCounter[$pageid]),
427 'length' => intval($pageLength[$pageid]),
428 );
429
430 if ($pageIsRedir[$pageid])
431 $pageInfo['redirect'] = '';
432
433 if ($pageIsNew[$pageid])
434 $pageInfo['new'] = '';
435
436 if (!is_null($params['token'])) {
437 $tokenFunctions = $this->getTokenFunctions();
438 $pageInfo['starttimestamp'] = wfTimestamp(TS_ISO_8601, time());
439 foreach($params['token'] as $t)
440 {
441 $val = call_user_func($tokenFunctions[$t], $pageid, $title);
442 if($val === false)
443 $this->setWarning("Action '$t' is not allowed for the current user");
444 else
445 $pageInfo[$t . 'token'] = $val;
446 }
447 }
448
449 if($fld_protection) {
450 $pageInfo['protection'] = array();
451 if (isset($protections[$pageid])) {
452 $pageInfo['protection'] = $protections[$pageid];
453 $result->setIndexedTagName($pageInfo['protection'], 'pr');
454 }
455 }
456 if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()]))
457 $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()];
458 if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()]))
459 $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()];
460 if($fld_url) {
461 $pageInfo['fullurl'] = $title->getFullURL();
462 $pageInfo['editurl'] = $title->getFullURL('action=edit');
463 }
464 if($fld_readable)
465 if($title->userCanRead())
466 $pageInfo['readable'] = '';
467
468 $result->addValue(array (
469 'query',
470 'pages'
471 ), $pageid, $pageInfo);
472 }
473
474 // Get properties for missing titles if requested
475 if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid ||
476 $fld_url || $fld_readable)
477 {
478 $res = &$result->getData();
479 foreach($missing as $pageid => $title) {
480 if(!is_null($params['token']))
481 {
482 $tokenFunctions = $this->getTokenFunctions();
483 $res['query']['pages'][$pageid]['starttimestamp'] = wfTimestamp(TS_ISO_8601, time());
484 foreach($params['token'] as $t)
485 {
486 $val = call_user_func($tokenFunctions[$t], $pageid, $title);
487 if($val === false)
488 $this->setWarning("Action '$t' is not allowed for the current user");
489 else
490 $res['query']['pages'][$pageid][$t . 'token'] = $val;
491 }
492 }
493 if($fld_protection)
494 {
495 // Apparently the XML formatting code doesn't like array(null)
496 // This is painful to fix, so we'll just work around it
497 if(isset($prottitles[$title->getNamespace()][$title->getDBkey()]))
498 $res['query']['pages'][$pageid]['protection'] = $prottitles[$title->getNamespace()][$title->getDBkey()];
499 else
500 $res['query']['pages'][$pageid]['protection'] = array();
501 $result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr');
502 }
503 if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()]))
504 $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()];
505 if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()]))
506 $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()];
507 if($fld_url) {
508 $res['query']['pages'][$pageid]['fullurl'] = $title->getFullURL();
509 $res['query']['pages'][$pageid]['editurl'] = $title->getFullURL('action=edit');
510 }
511 if($fld_readable)
512 if($title->userCanRead())
513 $res['query']['pages'][$pageid]['readable'] = '';
514 }
515 }
516 }
517
518 public function getAllowedParams() {
519 return array (
520 'prop' => array (
521 ApiBase :: PARAM_DFLT => NULL,
522 ApiBase :: PARAM_ISMULTI => true,
523 ApiBase :: PARAM_TYPE => array (
524 'protection',
525 'talkid',
526 'subjectid',
527 'url',
528 'readable',
529 )),
530 'token' => array (
531 ApiBase :: PARAM_DFLT => NULL,
532 ApiBase :: PARAM_ISMULTI => true,
533 ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions())
534 )
535 );
536 }
537
538 public function getParamDescription() {
539 return array (
540 'prop' => array (
541 'Which additional properties to get:',
542 ' "protection" - List the protection level of each page',
543 ' "talkid" - The page ID of the talk page for each non-talk page',
544 ' "subjectid" - The page ID of the parent page for each talk page'
545 ),
546 'token' => 'Request a token to perform a data-modifying action on a page',
547 );
548 }
549
550
551 public function getDescription() {
552 return 'Get basic page information such as namespace, title, last touched date, ...';
553 }
554
555 protected function getExamples() {
556 return array (
557 'api.php?action=query&prop=info&titles=Main%20Page',
558 'api.php?action=query&prop=info&inprop=protection&titles=Main%20Page'
559 );
560 }
561
562 public function getVersion() {
563 return __CLASS__ . ': $Id$';
564 }
565 }