(bug 19004) Added support for tags to the API. Patch by Matthew Britton.
[lhc/web/wiklou.git] / includes / api / ApiQueryUserContributions.php
1 <?php
2
3 /*
4 * Created on Oct 16, 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 query action adds a list of a specified user's contributions to the output.
33 *
34 * @ingroup API
35 */
36 class ApiQueryContributions extends ApiQueryBase {
37
38 public function __construct($query, $moduleName) {
39 parent :: __construct($query, $moduleName, 'uc');
40 }
41
42 private $params, $username;
43 private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
44 $fld_comment = false, $fld_flags = false,
45 $fld_patrolled = false, $fld_tags = false;
46
47 public function execute() {
48
49 // Parse some parameters
50 $this->params = $this->extractRequestParams();
51
52 $prop = array_flip($this->params['prop']);
53 $this->fld_ids = isset($prop['ids']);
54 $this->fld_title = isset($prop['title']);
55 $this->fld_comment = isset($prop['comment']);
56 $this->fld_size = isset($prop['size']);
57 $this->fld_flags = isset($prop['flags']);
58 $this->fld_timestamp = isset($prop['timestamp']);
59 $this->fld_patrolled = isset($prop['patrolled']);
60 $this->fld_tags = isset($prop['tags']);
61
62 // TODO: if the query is going only against the revision table, should this be done?
63 $this->selectNamedDB('contributions', DB_SLAVE, 'contributions');
64 $db = $this->getDB();
65
66 if(isset($this->params['userprefix']))
67 {
68 $this->prefixMode = true;
69 $this->multiUserMode = true;
70 $this->userprefix = $this->params['userprefix'];
71 }
72 else
73 {
74 $this->usernames = array();
75 if(!is_array($this->params['user']))
76 $this->params['user'] = array($this->params['user']);
77 foreach($this->params['user'] as $u)
78 $this->prepareUsername($u);
79 $this->prefixMode = false;
80 $this->multiUserMode = (count($this->params['user']) > 1);
81 }
82 $this->prepareQuery();
83
84 //Do the actual query.
85 $res = $this->select( __METHOD__ );
86
87 //Initialise some variables
88 $count = 0;
89 $limit = $this->params['limit'];
90
91 //Fetch each row
92 while ( $row = $db->fetchObject( $res ) ) {
93 if (++ $count > $limit) {
94 // We've reached the one extra which shows that there are additional pages to be had. Stop here...
95 if($this->multiUserMode)
96 $this->setContinueEnumParameter('continue', $this->continueStr($row));
97 else
98 $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp));
99 break;
100 }
101
102 $vals = $this->extractRowInfo($row);
103 $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals);
104 if(!$fit)
105 {
106 if($this->multiUserMode)
107 $this->setContinueEnumParameter('continue', $this->continueStr($row));
108 else
109 $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp));
110 break;
111 }
112 }
113
114 //Free the database record so the connection can get on with other stuff
115 $db->freeResult($res);
116
117 $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'item');
118 }
119
120 /**
121 * Validate the 'user' parameter and set the value to compare
122 * against `revision`.`rev_user_text`
123 */
124 private function prepareUsername($user) {
125 if( $user ) {
126 $name = User::isIP( $user )
127 ? $user
128 : User::getCanonicalName( $user, 'valid' );
129 if( $name === false ) {
130 $this->dieUsage( "User name {$user} is not valid", 'param_user' );
131 } else {
132 $this->usernames[] = $name;
133 }
134 } else {
135 $this->dieUsage( 'User parameter may not be empty', 'param_user' );
136 }
137 }
138
139 /**
140 * Prepares the query and returns the limit of rows requested
141 */
142 private function prepareQuery() {
143 // We're after the revision table, and the corresponding page
144 // row for anything we retrieve. We may also need the
145 // recentchanges row and/or tag summary row.
146 global $wgUser;
147 $tables = array('page', 'revision'); // Order may change
148 $this->addWhere('page_id=rev_page');
149
150 // Handle continue parameter
151 if($this->multiUserMode && !is_null($this->params['continue']))
152 {
153 $continue = explode('|', $this->params['continue']);
154 if(count($continue) != 2)
155 $this->dieUsage("Invalid continue param. You should pass the original " .
156 "value returned by the previous query", "_badcontinue");
157 $encUser = $this->getDB()->strencode($continue[0]);
158 $encTS = wfTimestamp(TS_MW, $continue[1]);
159 $op = ($this->params['dir'] == 'older' ? '<' : '>');
160 $this->addWhere("rev_user_text $op '$encUser' OR " .
161 "(rev_user_text = '$encUser' AND " .
162 "rev_timestamp $op= '$encTS')");
163 }
164
165 if(!$wgUser->isAllowed('hideuser'))
166 $this->addWhere($this->getDB()->bitAnd('rev_deleted',Revision::DELETED_USER) . ' = 0');
167 // We only want pages by the specified users.
168 if($this->prefixMode)
169 $this->addWhere("rev_user_text LIKE '" . $this->getDB()->escapeLike($this->userprefix) . "%'");
170 else
171 $this->addWhereFld('rev_user_text', $this->usernames);
172 // ... and in the specified timeframe.
173 // Ensure the same sort order for rev_user_text and rev_timestamp
174 // so our query is indexed
175 if($this->multiUserMode)
176 $this->addWhereRange('rev_user_text', $this->params['dir'], null, null);
177 $this->addWhereRange('rev_timestamp',
178 $this->params['dir'], $this->params['start'], $this->params['end'] );
179 $this->addWhereFld('page_namespace', $this->params['namespace']);
180
181 $show = $this->params['show'];
182 if (!is_null($show)) {
183 $show = array_flip($show);
184 if ((isset($show['minor']) && isset($show['!minor']))
185 || (isset($show['patrolled']) && isset($show['!patrolled'])))
186 $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
187
188 $this->addWhereIf('rev_minor_edit = 0', isset($show['!minor']));
189 $this->addWhereIf('rev_minor_edit != 0', isset($show['minor']));
190 $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled']));
191 $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled']));
192 }
193 $this->addOption('LIMIT', $this->params['limit'] + 1);
194 $index['revision'] = 'usertext_timestamp';
195
196 // Mandatory fields: timestamp allows request continuation
197 // ns+title checks if the user has access rights for this page
198 // user_text is necessary if multiple users were specified
199 $this->addFields(array(
200 'rev_timestamp',
201 'page_namespace',
202 'page_title',
203 'rev_user_text',
204 'rev_deleted'
205 ));
206
207 if(isset($show['patrolled']) || isset($show['!patrolled']) ||
208 $this->fld_patrolled)
209 {
210 global $wgUser;
211 if(!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
212 $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
213 // Use a redundant join condition on both
214 // timestamp and ID so we can use the timestamp
215 // index
216 $index['recentchanges'] = 'rc_user_text';
217 if(isset($show['patrolled']) || isset($show['!patrolled']))
218 {
219 // Put the tables in the right order for
220 // STRAIGHT_JOIN
221 $tables = array('revision', 'recentchanges', 'page');
222 $this->addOption('STRAIGHT_JOIN');
223 $this->addWhere('rc_user_text=rev_user_text');
224 $this->addWhere('rc_timestamp=rev_timestamp');
225 $this->addWhere('rc_this_oldid=rev_id');
226 }
227 else
228 {
229 $tables[] = 'recentchanges';
230 $this->addJoinConds(array('recentchanges' => array(
231 'LEFT JOIN', array(
232 'rc_user_text=rev_user_text',
233 'rc_timestamp=rev_timestamp',
234 'rc_this_oldid=rev_id'))));
235 }
236 }
237
238 $this->addTables($tables);
239 $this->addOption('USE INDEX', $index);
240 $this->addFieldsIf('rev_page', $this->fld_ids);
241 $this->addFieldsIf('rev_id', $this->fld_ids || $this->fld_flags);
242 $this->addFieldsIf('page_latest', $this->fld_flags);
243 // $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed?
244 $this->addFieldsIf('rev_comment', $this->fld_comment);
245 $this->addFieldsIf('rev_len', $this->fld_size);
246 $this->addFieldsIf('rev_minor_edit', $this->fld_flags);
247 $this->addFieldsIf('rev_parent_id', $this->fld_flags);
248 $this->addFieldsIf('rc_patrolled', $this->fld_patrolled);
249
250 if($this->fld_tags || !is_null($this->params['tag'])) {
251 $this->addTables('tag_summary');
252 $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('rev_id=ts_rev_id'))));
253 $this->addFields('ts_tags');
254 }
255
256 if( !is_null($this->params['tag']) ) {
257 $this->addWhereFld('ts_tags', $this->params['tag']);
258 }
259 }
260
261 /**
262 * Extract fields from the database row and append them to a result array
263 */
264 private function extractRowInfo($row) {
265
266 $vals = array();
267
268 $vals['user'] = $row->rev_user_text;
269 if ($row->rev_deleted & Revision::DELETED_USER)
270 $vals['userhidden'] = '';
271 if ($this->fld_ids) {
272 $vals['pageid'] = intval($row->rev_page);
273 $vals['revid'] = intval($row->rev_id);
274 // $vals['textid'] = intval($row->rev_text_id); // todo: Should this field be exposed?
275 }
276
277 if ($this->fld_title)
278 ApiQueryBase :: addTitleInfo($vals,
279 Title :: makeTitle($row->page_namespace, $row->page_title));
280
281 if ($this->fld_timestamp)
282 $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
283
284 if ($this->fld_flags) {
285 if ($row->rev_parent_id == 0 && !is_null($row->rev_parent_id))
286 $vals['new'] = '';
287 if ($row->rev_minor_edit)
288 $vals['minor'] = '';
289 if ($row->page_latest == $row->rev_id)
290 $vals['top'] = '';
291 }
292
293 if ($this->fld_comment && isset($row->rev_comment)) {
294 if ($row->rev_deleted & Revision::DELETED_COMMENT)
295 $vals['commenthidden'] = '';
296 else
297 $vals['comment'] = $row->rev_comment;
298 }
299
300 if ($this->fld_patrolled && $row->rc_patrolled)
301 $vals['patrolled'] = '';
302
303 if ($this->fld_size && !is_null($row->rev_len))
304 $vals['size'] = intval($row->rev_len);
305
306 if ($this->fld_tags && $row->ts_tags)
307 $vals['tags'] = $row->ts_tags;
308
309 return $vals;
310 }
311
312 private function continueStr($row)
313 {
314 return $row->rev_user_text . '|' .
315 wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
316 }
317
318 public function getAllowedParams() {
319 return array (
320 'limit' => array (
321 ApiBase :: PARAM_DFLT => 10,
322 ApiBase :: PARAM_TYPE => 'limit',
323 ApiBase :: PARAM_MIN => 1,
324 ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
325 ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
326 ),
327 'start' => array (
328 ApiBase :: PARAM_TYPE => 'timestamp'
329 ),
330 'end' => array (
331 ApiBase :: PARAM_TYPE => 'timestamp'
332 ),
333 'continue' => null,
334 'user' => array (
335 ApiBase :: PARAM_ISMULTI => true
336 ),
337 'userprefix' => null,
338 'dir' => array (
339 ApiBase :: PARAM_DFLT => 'older',
340 ApiBase :: PARAM_TYPE => array (
341 'newer',
342 'older'
343 )
344 ),
345 'namespace' => array (
346 ApiBase :: PARAM_ISMULTI => true,
347 ApiBase :: PARAM_TYPE => 'namespace'
348 ),
349 'prop' => array (
350 ApiBase :: PARAM_ISMULTI => true,
351 ApiBase :: PARAM_DFLT => 'ids|title|timestamp|comment|size|flags',
352 ApiBase :: PARAM_TYPE => array (
353 'ids',
354 'title',
355 'timestamp',
356 'comment',
357 'size',
358 'flags',
359 'patrolled',
360 'tags',
361 )
362 ),
363 'show' => array (
364 ApiBase :: PARAM_ISMULTI => true,
365 ApiBase :: PARAM_TYPE => array (
366 'minor',
367 '!minor',
368 'patrolled',
369 '!patrolled',
370 )
371 ),
372 'tag' => null,
373 );
374 }
375
376 public function getParamDescription() {
377 return array (
378 'limit' => 'The maximum number of contributions to return.',
379 'start' => 'The start timestamp to return from.',
380 'end' => 'The end timestamp to return to.',
381 'continue' => 'When more results are available, use this to continue.',
382 'user' => 'The user to retrieve contributions for.',
383 'userprefix' => 'Retrieve contibutions for all users whose names begin with this value. Overrides ucuser.',
384 'dir' => 'The direction to search (older or newer).',
385 'namespace' => 'Only list contributions in these namespaces',
386 'prop' => 'Include additional pieces of information',
387 'show' => array('Show only items that meet this criteria, e.g. non minor edits only: show=!minor',
388 'NOTE: if show=patrolled or show=!patrolled is set, revisions older than $wgRCMaxAge won\'t be shown',),
389 'tag' => 'Only list contributions with this tag',
390 );
391 }
392
393 public function getDescription() {
394 return 'Get all edits by a user';
395 }
396
397 protected function getExamples() {
398 return array (
399 'api.php?action=query&list=usercontribs&ucuser=YurikBot',
400 'api.php?action=query&list=usercontribs&ucuserprefix=217.121.114.',
401 );
402 }
403
404 public function getVersion() {
405 return __CLASS__ . ': $Id$';
406 }
407 }