ApiQueryBacklinks.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. /*
  3. * Created on Oct 16, 2006
  4. *
  5. * API for MediaWiki 1.8+
  6. *
  7. * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
  8. *
  9. * This program is free software; you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation; either version 2 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License along
  20. * with this program; if not, write to the Free Software Foundation, Inc.,
  21. * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  22. * http://www.gnu.org/copyleft/gpl.html
  23. */
  24. if (!defined('MEDIAWIKI')) {
  25. // Eclipse helper - will be ignored in production
  26. require_once ("ApiQueryBase.php");
  27. }
  28. /**
  29. * This is a three-in-one module to query:
  30. * * backlinks - links pointing to the given page,
  31. * * embeddedin - what pages transclude the given page within themselves,
  32. * * imageusage - what pages use the given image
  33. *
  34. * @ingroup API
  35. */
  36. class ApiQueryBacklinks extends ApiQueryGeneratorBase {
  37. private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID, $redirID, $redirect;
  38. private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_sort, $bl_fields, $hasNS;
  39. private $pageMap, $resultArr;
  40. // output element name, database column field prefix, database table
  41. private $backlinksSettings = array (
  42. 'backlinks' => array (
  43. 'code' => 'bl',
  44. 'prefix' => 'pl',
  45. 'linktbl' => 'pagelinks'
  46. ),
  47. 'embeddedin' => array (
  48. 'code' => 'ei',
  49. 'prefix' => 'tl',
  50. 'linktbl' => 'templatelinks'
  51. ),
  52. 'imageusage' => array (
  53. 'code' => 'iu',
  54. 'prefix' => 'il',
  55. 'linktbl' => 'imagelinks'
  56. )
  57. );
  58. public function __construct($query, $moduleName) {
  59. extract($this->backlinksSettings[$moduleName]);
  60. $this->resultArr = array();
  61. parent :: __construct($query, $moduleName, $code);
  62. $this->bl_ns = $prefix . '_namespace';
  63. $this->bl_from = $prefix . '_from';
  64. $this->bl_table = $linktbl;
  65. $this->bl_code = $code;
  66. $this->hasNS = $moduleName !== 'imageusage';
  67. if ($this->hasNS) {
  68. $this->bl_title = $prefix . '_title';
  69. $this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}";
  70. $this->bl_fields = array (
  71. $this->bl_ns,
  72. $this->bl_title
  73. );
  74. } else {
  75. $this->bl_title = $prefix . '_to';
  76. $this->bl_sort = "{$this->bl_title}, {$this->bl_from}";
  77. $this->bl_fields = array (
  78. $this->bl_title
  79. );
  80. }
  81. }
  82. public function execute() {
  83. $this->run();
  84. }
  85. public function executeGenerator($resultPageSet) {
  86. $this->run($resultPageSet);
  87. }
  88. private function prepareFirstQuery($resultPageSet = null) {
  89. /* SELECT page_id, page_title, page_namespace, page_is_redirect
  90. * FROM pagelinks, page WHERE pl_from=page_id
  91. * AND pl_title='Foo' AND pl_namespace=0
  92. * LIMIT 11 ORDER BY pl_from
  93. */
  94. $db = $this->getDB();
  95. $this->addTables(array('page', $this->bl_table));
  96. $this->addWhere("{$this->bl_from}=page_id");
  97. if(is_null($resultPageSet))
  98. $this->addFields(array('page_id', 'page_title', 'page_namespace'));
  99. else
  100. $this->addFields($resultPageSet->getPageTableFields());
  101. $this->addFields('page_is_redirect');
  102. $this->addWhereFld($this->bl_title, $this->rootTitle->getDBKey());
  103. if($this->hasNS)
  104. $this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace());
  105. $this->addWhereFld('page_namespace', $this->params['namespace']);
  106. if(!is_null($this->contID))
  107. $this->addWhere("{$this->bl_from}>={$this->contID}");
  108. if($this->params['filterredir'] == 'redirects')
  109. $this->addWhereFld('page_is_redirect', 1);
  110. if($this->params['filterredir'] == 'nonredirects')
  111. $this->addWhereFld('page_is_redirect', 0);
  112. $this->addOption('LIMIT', $this->params['limit'] + 1);
  113. $this->addOption('ORDER BY', $this->bl_from);
  114. }
  115. private function prepareSecondQuery($resultPageSet = null) {
  116. /* SELECT page_id, page_title, page_namespace, page_is_redirect, pl_title, pl_namespace
  117. FROM pagelinks, page WHERE pl_from=page_id
  118. AND (pl_title='Foo' AND pl_namespace=0) OR (pl_title='Bar' AND pl_namespace=1)
  119. ORDER BY pl_namespace, pl_title, pl_from LIMIT 11
  120. */
  121. $db = $this->getDB();
  122. $this->addTables(array('page', $this->bl_table));
  123. $this->addWhere("{$this->bl_from}=page_id");
  124. if(is_null($resultPageSet))
  125. $this->addFields(array('page_id', 'page_title', 'page_namespace', 'page_is_redirect'));
  126. else
  127. $this->addFields($resultPageSet->getPageTableFields());
  128. $this->addFields($this->bl_title);
  129. if($this->hasNS)
  130. $this->addFields($this->bl_ns);
  131. // We can't use LinkBatch here because $this->hasNS may be false
  132. $titleWhere = array();
  133. foreach($this->redirTitles as $t)
  134. $titleWhere[] = "{$this->bl_title} = ".$db->addQuotes($t->getDBKey()).
  135. ($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "");
  136. $this->addWhere($db->makeList($titleWhere, LIST_OR));
  137. $this->addWhereFld('page_namespace', $this->params['namespace']);
  138. if(!is_null($this->redirID))
  139. {
  140. $first = $this->redirTitles[0];
  141. $title = $db->strencode($first->getDBKey());
  142. $ns = $first->getNamespace();
  143. $from = $this->redirID;
  144. if($this->hasNS)
  145. $this->addWhere("{$this->bl_ns} > $ns OR ".
  146. "({$this->bl_ns} = $ns AND ".
  147. "({$this->bl_title} > '$title' OR ".
  148. "({$this->bl_title} = '$title' AND ".
  149. "{$this->bl_from} >= $from)))");
  150. else
  151. $this->addWhere("{$this->bl_title} > '$title' OR ".
  152. "({$this->bl_title} = '$title' AND ".
  153. "{$this->bl_from} >= $from)");
  154. }
  155. if($this->params['filterredir'] == 'redirects')
  156. $this->addWhereFld('page_is_redirect', 1);
  157. if($this->params['filterredir'] == 'nonredirects')
  158. $this->addWhereFld('page_is_redirect', 0);
  159. $this->addOption('LIMIT', $this->params['limit'] + 1);
  160. $this->addOption('ORDER BY', $this->bl_sort);
  161. $this->addOption('USE INDEX', array('page' => 'PRIMARY'));
  162. }
  163. private function run($resultPageSet = null) {
  164. $this->params = $this->extractRequestParams(false);
  165. $this->redirect = isset($this->params['redirect']) && $this->params['redirect'];
  166. $userMax = ( $this->redirect ? ApiBase::LIMIT_BIG1/2 : ApiBase::LIMIT_BIG1 );
  167. $botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2/2 : ApiBase::LIMIT_BIG2 );
  168. if( $this->params['limit'] == 'max' ) {
  169. $this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
  170. $this->getResult()->addValue( 'limits', $this->getModuleName(), $this->params['limit'] );
  171. }
  172. $this->processContinue();
  173. $this->prepareFirstQuery($resultPageSet);
  174. $db = $this->getDB();
  175. $res = $this->select(__METHOD__.'::firstQuery');
  176. $count = 0;
  177. $this->pageMap = array(); // Maps ns and title to pageid
  178. $this->continueStr = null;
  179. $this->redirTitles = array();
  180. while ($row = $db->fetchObject($res)) {
  181. if (++ $count > $this->params['limit']) {
  182. // We've reached the one extra which shows that there are additional pages to be had. Stop here...
  183. // Continue string preserved in case the redirect query doesn't pass the limit
  184. $this->continueStr = $this->getContinueStr($row->page_id);
  185. break;
  186. }
  187. if (is_null($resultPageSet))
  188. $this->extractRowInfo($row);
  189. else
  190. {
  191. $this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
  192. if($row->page_is_redirect)
  193. $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title);
  194. $resultPageSet->processDbRow($row);
  195. }
  196. }
  197. $db->freeResult($res);
  198. if($this->redirect && count($this->redirTitles))
  199. {
  200. $this->resetQueryParams();
  201. $this->prepareSecondQuery($resultPageSet);
  202. $res = $this->select(__METHOD__.'::secondQuery');
  203. $count = 0;
  204. while($row = $db->fetchObject($res))
  205. {
  206. if(++$count > $this->params['limit'])
  207. {
  208. // We've reached the one extra which shows that there are additional pages to be had. Stop here...
  209. // We need to keep the parent page of this redir in
  210. if($this->hasNS)
  211. $parentID = $this->pageMap[$row->{$this->bl_ns}][$row->{$this->bl_title}];
  212. else
  213. $parentID = $this->pageMap[NS_IMAGE][$row->{$this->bl_title}];
  214. $this->continueStr = $this->getContinueRedirStr($parentID, $row->page_id);
  215. break;
  216. }
  217. if(is_null($resultPageSet))
  218. $this->extractRedirRowInfo($row);
  219. else
  220. $resultPageSet->processDbRow($row);
  221. }
  222. $db->freeResult($res);
  223. }
  224. if (is_null($resultPageSet)) {
  225. // Try to add the result data in one go and pray that it fits
  226. $fit = $this->getResult()->addValue('query', $this->getModuleName(), array_values($this->resultArr));
  227. if(!$fit)
  228. {
  229. // It didn't fit. Add elements one by one until the
  230. // result is full.
  231. foreach($this->resultArr as $pageID => $arr)
  232. {
  233. // Add the basic entry without redirlinks first
  234. $fit = $this->getResult()->addValue(
  235. array('query', $this->getModuleName()),
  236. null, array_diff_key($arr, array('redirlinks' => '')));
  237. if(!$fit)
  238. {
  239. $this->continueStr = $this->getContinueStr($pageID);
  240. break;
  241. }
  242. $hasRedirs = false;
  243. foreach((array)@$arr['redirlinks'] as $key => $redir)
  244. {
  245. $fit = $this->getResult()->addValue(
  246. array('query', $this->getModuleName(), $pageID, 'redirlinks'),
  247. $key, $redir);
  248. if(!$fit)
  249. {
  250. $this->continueStr = $this->getContinueRedirStr($pageID, $redir['pageid']);
  251. break;
  252. }
  253. $hasRedirs = true;
  254. }
  255. if($hasRedirs)
  256. $this->getResult()->setIndexedTagName_internal(
  257. array('query', $this->getModuleName(), $pageID, 'redirlinks'),
  258. $this->bl_code);
  259. if(!$fit)
  260. break;
  261. }
  262. }
  263. $this->getResult()->setIndexedTagName_internal(
  264. array('query', $this->getModuleName()),
  265. $this->bl_code);
  266. }
  267. if(!is_null($this->continueStr))
  268. $this->setContinueEnumParameter('continue', $this->continueStr);
  269. }
  270. private function extractRowInfo($row) {
  271. $this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
  272. $t = Title::makeTitle($row->page_namespace, $row->page_title);
  273. $a = array('pageid' => intval($row->page_id));
  274. ApiQueryBase::addTitleInfo($a, $t);
  275. if($row->page_is_redirect)
  276. {
  277. $a['redirect'] = '';
  278. $this->redirTitles[] = $t;
  279. }
  280. // Put all the results in an array first
  281. $this->resultArr[$a['pageid']] = $a;
  282. }
  283. private function extractRedirRowInfo($row)
  284. {
  285. $a['pageid'] = intval($row->page_id);
  286. ApiQueryBase::addTitleInfo($a, Title::makeTitle($row->page_namespace, $row->page_title));
  287. if($row->page_is_redirect)
  288. $a['redirect'] = '';
  289. $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE;
  290. $parentID = $this->pageMap[$ns][$row->{$this->bl_title}];
  291. // Put all the results in an array first
  292. $this->resultArr[$parentID]['redirlinks'][] = $a;
  293. $this->getResult()->setIndexedTagName($this->resultArr[$parentID]['redirlinks'], $this->bl_code);
  294. }
  295. protected function processContinue() {
  296. if (!is_null($this->params['continue']))
  297. $this->parseContinueParam();
  298. else {
  299. if ( $this->params['title'] !== "" ) {
  300. $title = Title::newFromText( $this->params['title'] );
  301. if ( !$title ) {
  302. $this->dieUsageMsg(array('invalidtitle', $this->params['title']));
  303. } else {
  304. $this->rootTitle = $title;
  305. }
  306. } else {
  307. $this->dieUsageMsg(array('missingparam', 'title'));
  308. }
  309. }
  310. // only image titles are allowed for the root in imageinfo mode
  311. if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE)
  312. $this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title');
  313. }
  314. protected function parseContinueParam() {
  315. $continueList = explode('|', $this->params['continue']);
  316. // expected format:
  317. // ns | key | id1 [| id2]
  318. // ns+key: root title
  319. // id1: first-level page ID to continue from
  320. // id2: second-level page ID to continue from
  321. // null stuff out now so we know what's set and what isn't
  322. $this->rootTitle = $this->contID = $this->redirID = null;
  323. $rootNs = intval($continueList[0]);
  324. if($rootNs === 0 && $continueList[0] !== '0')
  325. // Illegal continue parameter
  326. $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue");
  327. $this->rootTitle = Title::makeTitleSafe($rootNs, $continueList[1]);
  328. if(!$this->rootTitle)
  329. $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue");
  330. $contID = intval($continueList[2]);
  331. if($contID === 0 && $continueList[2] !== '0')
  332. $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue");
  333. $this->contID = $contID;
  334. $redirID = intval(@$continueList[3]);
  335. if($redirID === 0 && @$continueList[3] !== '0')
  336. // This one isn't required
  337. return;
  338. $this->redirID = $redirID;
  339. }
  340. protected function getContinueStr($lastPageID) {
  341. return $this->rootTitle->getNamespace() .
  342. '|' . $this->rootTitle->getDBkey() .
  343. '|' . $lastPageID;
  344. }
  345. protected function getContinueRedirStr($lastPageID, $lastRedirID) {
  346. return $this->getContinueStr($lastPageID) . '|' . $lastRedirID;
  347. }
  348. public function getAllowedParams() {
  349. $retval = array (
  350. 'title' => null,
  351. 'continue' => null,
  352. 'namespace' => array (
  353. ApiBase :: PARAM_ISMULTI => true,
  354. ApiBase :: PARAM_TYPE => 'namespace'
  355. ),
  356. 'filterredir' => array(
  357. ApiBase :: PARAM_DFLT => 'all',
  358. ApiBase :: PARAM_TYPE => array(
  359. 'all',
  360. 'redirects',
  361. 'nonredirects'
  362. )
  363. ),
  364. 'limit' => array (
  365. ApiBase :: PARAM_DFLT => 10,
  366. ApiBase :: PARAM_TYPE => 'limit',
  367. ApiBase :: PARAM_MIN => 1,
  368. ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
  369. ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
  370. )
  371. );
  372. if($this->getModuleName() == 'embeddedin')
  373. return $retval;
  374. $retval['redirect'] = false;
  375. return $retval;
  376. }
  377. public function getParamDescription() {
  378. $retval = array (
  379. 'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.',
  380. 'continue' => 'When more results are available, use this to continue.',
  381. 'namespace' => 'The namespace to enumerate.',
  382. 'filterredir' => 'How to filter for redirects'
  383. );
  384. if($this->getModuleName() != 'embeddedin')
  385. return array_merge($retval, array(
  386. 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect as well. Maximum limit is halved.',
  387. 'limit' => "How many total pages to return. If {$this->bl_code}redirect is enabled, limit applies to each level separately."
  388. ));
  389. return array_merge($retval, array(
  390. 'limit' => "How many total pages to return."
  391. ));
  392. }
  393. public function getDescription() {
  394. switch ($this->getModuleName()) {
  395. case 'backlinks' :
  396. return 'Find all pages that link to the given page';
  397. case 'embeddedin' :
  398. return 'Find all pages that embed (transclude) the given title';
  399. case 'imageusage' :
  400. return 'Find all pages that use the given image title.';
  401. default :
  402. ApiBase :: dieDebug(__METHOD__, 'Unknown module name');
  403. }
  404. }
  405. protected function getExamples() {
  406. static $examples = array (
  407. 'backlinks' => array (
  408. "api.php?action=query&list=backlinks&bltitle=Main%20Page",
  409. "api.php?action=query&generator=backlinks&gbltitle=Main%20Page&prop=info"
  410. ),
  411. 'embeddedin' => array (
  412. "api.php?action=query&list=embeddedin&eititle=Template:Stub",
  413. "api.php?action=query&generator=embeddedin&geititle=Template:Stub&prop=info"
  414. ),
  415. 'imageusage' => array (
  416. "api.php?action=query&list=imageusage&iutitle=File:Albert%20Einstein%20Head.jpg",
  417. "api.php?action=query&generator=imageusage&giutitle=File:Albert%20Einstein%20Head.jpg&prop=info"
  418. )
  419. );
  420. return $examples[$this->getModuleName()];
  421. }
  422. public function getVersion() {
  423. return __CLASS__ . ': $Id: ApiQueryBacklinks.php 50217 2009-05-05 13:12:16Z tstarling $';
  424. }
  425. }