These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Utility class for creating and accessing recent change entries. |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | */ |
||
22 | |||
23 | /** |
||
24 | * Utility class for creating new RC entries |
||
25 | * |
||
26 | * mAttribs: |
||
27 | * rc_id id of the row in the recentchanges table |
||
28 | * rc_timestamp time the entry was made |
||
29 | * rc_namespace namespace # |
||
30 | * rc_title non-prefixed db key |
||
31 | * rc_type is new entry, used to determine whether updating is necessary |
||
32 | * rc_source string representation of change source |
||
33 | * rc_minor is minor |
||
34 | * rc_cur_id page_id of associated page entry |
||
35 | * rc_user user id who made the entry |
||
36 | * rc_user_text user name who made the entry |
||
37 | * rc_comment edit summary |
||
38 | * rc_this_oldid rev_id associated with this entry (or zero) |
||
39 | * rc_last_oldid rev_id associated with the entry before this one (or zero) |
||
40 | * rc_bot is bot, hidden |
||
41 | * rc_ip IP address of the user in dotted quad notation |
||
42 | * rc_new obsolete, use rc_type==RC_NEW |
||
43 | * rc_patrolled boolean whether or not someone has marked this edit as patrolled |
||
44 | * rc_old_len integer byte length of the text before the edit |
||
45 | * rc_new_len the same after the edit |
||
46 | * rc_deleted partial deletion |
||
47 | * rc_logid the log_id value for this log entry (or zero) |
||
48 | * rc_log_type the log type (or null) |
||
49 | * rc_log_action the log action (or null) |
||
50 | * rc_params log params |
||
51 | * |
||
52 | * mExtra: |
||
53 | * prefixedDBkey prefixed db key, used by external app via msg queue |
||
54 | * lastTimestamp timestamp of previous entry, used in WHERE clause during update |
||
55 | * oldSize text size before the change |
||
56 | * newSize text size after the change |
||
57 | * pageStatus status of the page: created, deleted, moved, restored, changed |
||
58 | * |
||
59 | * temporary: not stored in the database |
||
60 | * notificationtimestamp |
||
61 | * numberofWatchingusers |
||
62 | */ |
||
63 | class RecentChange { |
||
64 | // Constants for the rc_source field. Extensions may also have |
||
65 | // their own source constants. |
||
66 | const SRC_EDIT = 'mw.edit'; |
||
67 | const SRC_NEW = 'mw.new'; |
||
68 | const SRC_LOG = 'mw.log'; |
||
69 | const SRC_EXTERNAL = 'mw.external'; // obsolete |
||
70 | const SRC_CATEGORIZE = 'mw.categorize'; |
||
71 | |||
72 | public $mAttribs = []; |
||
73 | public $mExtra = []; |
||
74 | |||
75 | /** |
||
76 | * @var Title |
||
77 | */ |
||
78 | public $mTitle = false; |
||
79 | |||
80 | /** |
||
81 | * @var User |
||
82 | */ |
||
83 | private $mPerformer = false; |
||
84 | |||
85 | public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked |
||
86 | public $notificationtimestamp; |
||
87 | |||
88 | /** |
||
89 | * @var int Line number of recent change. Default -1. |
||
90 | */ |
||
91 | public $counter = -1; |
||
92 | |||
93 | /** |
||
94 | * @var array List of tags to apply |
||
95 | */ |
||
96 | private $tags = []; |
||
97 | |||
98 | /** |
||
99 | * @var array Array of change types |
||
100 | */ |
||
101 | private static $changeTypes = [ |
||
102 | 'edit' => RC_EDIT, |
||
103 | 'new' => RC_NEW, |
||
104 | 'log' => RC_LOG, |
||
105 | 'external' => RC_EXTERNAL, |
||
106 | 'categorize' => RC_CATEGORIZE, |
||
107 | ]; |
||
108 | |||
109 | # Factory methods |
||
110 | |||
111 | /** |
||
112 | * @param mixed $row |
||
113 | * @return RecentChange |
||
114 | */ |
||
115 | public static function newFromRow( $row ) { |
||
116 | $rc = new RecentChange; |
||
117 | $rc->loadFromRow( $row ); |
||
118 | |||
119 | return $rc; |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * Parsing text to RC_* constants |
||
124 | * @since 1.24 |
||
125 | * @param string|array $type |
||
126 | * @throws MWException |
||
127 | * @return int|array RC_TYPE |
||
128 | */ |
||
129 | public static function parseToRCType( $type ) { |
||
130 | if ( is_array( $type ) ) { |
||
131 | $retval = []; |
||
132 | foreach ( $type as $t ) { |
||
133 | $retval[] = RecentChange::parseToRCType( $t ); |
||
134 | } |
||
135 | |||
136 | return $retval; |
||
137 | } |
||
138 | |||
139 | if ( !array_key_exists( $type, self::$changeTypes ) ) { |
||
140 | throw new MWException( "Unknown type '$type'" ); |
||
141 | } |
||
142 | return self::$changeTypes[$type]; |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * Parsing RC_* constants to human-readable test |
||
147 | * @since 1.24 |
||
148 | * @param int $rcType |
||
149 | * @return string $type |
||
150 | */ |
||
151 | public static function parseFromRCType( $rcType ) { |
||
152 | return array_search( $rcType, self::$changeTypes, true ) ?: "$rcType"; |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * Get an array of all change types |
||
157 | * |
||
158 | * @since 1.26 |
||
159 | * |
||
160 | * @return array |
||
161 | */ |
||
162 | public static function getChangeTypes() { |
||
163 | return array_keys( self::$changeTypes ); |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * Obtain the recent change with a given rc_id value |
||
168 | * |
||
169 | * @param int $rcid The rc_id value to retrieve |
||
170 | * @return RecentChange|null |
||
171 | */ |
||
172 | public static function newFromId( $rcid ) { |
||
173 | return self::newFromConds( [ 'rc_id' => $rcid ], __METHOD__ ); |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Find the first recent change matching some specific conditions |
||
178 | * |
||
179 | * @param array $conds Array of conditions |
||
180 | * @param mixed $fname Override the method name in profiling/logs |
||
181 | * @param int $dbType DB_* constant |
||
182 | * |
||
183 | * @return RecentChange|null |
||
184 | */ |
||
185 | public static function newFromConds( |
||
186 | $conds, |
||
187 | $fname = __METHOD__, |
||
188 | $dbType = DB_REPLICA |
||
189 | ) { |
||
190 | $db = wfGetDB( $dbType ); |
||
191 | $row = $db->selectRow( 'recentchanges', self::selectFields(), $conds, $fname ); |
||
192 | if ( $row !== false ) { |
||
193 | return self::newFromRow( $row ); |
||
194 | } else { |
||
195 | return null; |
||
196 | } |
||
197 | } |
||
198 | |||
199 | /** |
||
200 | * Return the list of recentchanges fields that should be selected to create |
||
201 | * a new recentchanges object. |
||
202 | * @return array |
||
203 | */ |
||
204 | public static function selectFields() { |
||
205 | return [ |
||
206 | 'rc_id', |
||
207 | 'rc_timestamp', |
||
208 | 'rc_user', |
||
209 | 'rc_user_text', |
||
210 | 'rc_namespace', |
||
211 | 'rc_title', |
||
212 | 'rc_comment', |
||
213 | 'rc_minor', |
||
214 | 'rc_bot', |
||
215 | 'rc_new', |
||
216 | 'rc_cur_id', |
||
217 | 'rc_this_oldid', |
||
218 | 'rc_last_oldid', |
||
219 | 'rc_type', |
||
220 | 'rc_source', |
||
221 | 'rc_patrolled', |
||
222 | 'rc_ip', |
||
223 | 'rc_old_len', |
||
224 | 'rc_new_len', |
||
225 | 'rc_deleted', |
||
226 | 'rc_logid', |
||
227 | 'rc_log_type', |
||
228 | 'rc_log_action', |
||
229 | 'rc_params', |
||
230 | ]; |
||
231 | } |
||
232 | |||
233 | # Accessors |
||
234 | |||
235 | /** |
||
236 | * @param array $attribs |
||
237 | */ |
||
238 | public function setAttribs( $attribs ) { |
||
239 | $this->mAttribs = $attribs; |
||
240 | } |
||
241 | |||
242 | /** |
||
243 | * @param array $extra |
||
244 | */ |
||
245 | public function setExtra( $extra ) { |
||
246 | $this->mExtra = $extra; |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * @return Title |
||
251 | */ |
||
252 | public function &getTitle() { |
||
253 | if ( $this->mTitle === false ) { |
||
254 | $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); |
||
255 | } |
||
256 | |||
257 | return $this->mTitle; |
||
258 | } |
||
259 | |||
260 | /** |
||
261 | * Get the User object of the person who performed this change. |
||
262 | * |
||
263 | * @return User |
||
264 | */ |
||
265 | public function getPerformer() { |
||
266 | if ( $this->mPerformer === false ) { |
||
267 | if ( $this->mAttribs['rc_user'] ) { |
||
268 | $this->mPerformer = User::newFromId( $this->mAttribs['rc_user'] ); |
||
269 | } else { |
||
270 | $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false ); |
||
271 | } |
||
272 | } |
||
273 | |||
274 | return $this->mPerformer; |
||
275 | } |
||
276 | |||
277 | /** |
||
278 | * Writes the data in this object to the database |
||
279 | * @param bool $noudp |
||
280 | */ |
||
281 | public function save( $noudp = false ) { |
||
282 | global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang; |
||
283 | |||
284 | $dbw = wfGetDB( DB_MASTER ); |
||
285 | if ( !is_array( $this->mExtra ) ) { |
||
286 | $this->mExtra = []; |
||
287 | } |
||
288 | |||
289 | if ( !$wgPutIPinRC ) { |
||
290 | $this->mAttribs['rc_ip'] = ''; |
||
291 | } |
||
292 | |||
293 | # Strict mode fixups (not-NULL fields) |
||
294 | foreach ( [ 'minor', 'bot', 'new', 'patrolled', 'deleted' ] as $field ) { |
||
295 | $this->mAttribs["rc_$field"] = (int)$this->mAttribs["rc_$field"]; |
||
296 | } |
||
297 | # ...more fixups (NULL fields) |
||
298 | foreach ( [ 'old_len', 'new_len' ] as $field ) { |
||
299 | $this->mAttribs["rc_$field"] = isset( $this->mAttribs["rc_$field"] ) |
||
300 | ? (int)$this->mAttribs["rc_$field"] |
||
301 | : null; |
||
302 | } |
||
303 | |||
304 | # If our database is strict about IP addresses, use NULL instead of an empty string |
||
305 | $strictIPs = in_array( $dbw->getType(), [ 'oracle', 'postgres' ] ); // legacy |
||
306 | if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) { |
||
307 | unset( $this->mAttribs['rc_ip'] ); |
||
308 | } |
||
309 | |||
310 | # Trim spaces on user supplied text |
||
311 | $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] ); |
||
312 | |||
313 | # Make sure summary is truncated (whole multibyte characters) |
||
314 | $this->mAttribs['rc_comment'] = $wgContLang->truncate( $this->mAttribs['rc_comment'], 255 ); |
||
315 | |||
316 | # Fixup database timestamps |
||
317 | $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] ); |
||
318 | $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' ); |
||
319 | |||
320 | # # If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL |
||
321 | if ( $this->mAttribs['rc_cur_id'] == 0 ) { |
||
322 | unset( $this->mAttribs['rc_cur_id'] ); |
||
323 | } |
||
324 | |||
325 | # Insert new row |
||
326 | $dbw->insert( 'recentchanges', $this->mAttribs, __METHOD__ ); |
||
327 | |||
328 | # Set the ID |
||
329 | $this->mAttribs['rc_id'] = $dbw->insertId(); |
||
330 | |||
331 | # Notify extensions |
||
332 | Hooks::run( 'RecentChange_save', [ &$this ] ); |
||
333 | |||
334 | if ( count( $this->tags ) ) { |
||
335 | ChangeTags::addTags( $this->tags, $this->mAttribs['rc_id'], |
||
336 | $this->mAttribs['rc_this_oldid'], $this->mAttribs['rc_logid'], null, $this ); |
||
337 | } |
||
338 | |||
339 | # Notify external application via UDP |
||
340 | if ( !$noudp ) { |
||
341 | $this->notifyRCFeeds(); |
||
342 | } |
||
343 | |||
344 | # E-mail notifications |
||
345 | if ( $wgUseEnotif || $wgShowUpdatedMarker ) { |
||
346 | $editor = $this->getPerformer(); |
||
347 | $title = $this->getTitle(); |
||
348 | |||
349 | // Never send an RC notification email about categorization changes |
||
350 | if ( |
||
351 | $this->mAttribs['rc_type'] != RC_CATEGORIZE && |
||
352 | Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) |
||
353 | ) { |
||
354 | // @FIXME: This would be better as an extension hook |
||
355 | // Send emails or email jobs once this row is safely committed |
||
356 | $dbw->onTransactionIdle( |
||
357 | function () use ( $editor, $title ) { |
||
358 | $enotif = new EmailNotification(); |
||
359 | $enotif->notifyOnPageChange( |
||
360 | $editor, |
||
361 | $title, |
||
362 | $this->mAttribs['rc_timestamp'], |
||
363 | $this->mAttribs['rc_comment'], |
||
364 | $this->mAttribs['rc_minor'], |
||
365 | $this->mAttribs['rc_last_oldid'], |
||
366 | $this->mExtra['pageStatus'] |
||
367 | ); |
||
368 | }, |
||
369 | __METHOD__ |
||
370 | ); |
||
371 | } |
||
372 | } |
||
373 | |||
374 | // Update the cached list of active users |
||
375 | if ( $this->mAttribs['rc_user'] > 0 ) { |
||
376 | JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newCacheUpdateJob() ); |
||
377 | } |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * Notify all the feeds about the change. |
||
382 | * @param array $feeds Optional feeds to send to, defaults to $wgRCFeeds |
||
383 | */ |
||
384 | public function notifyRCFeeds( array $feeds = null ) { |
||
385 | global $wgRCFeeds; |
||
386 | if ( $feeds === null ) { |
||
387 | $feeds = $wgRCFeeds; |
||
388 | } |
||
389 | |||
390 | $performer = $this->getPerformer(); |
||
391 | |||
392 | foreach ( $feeds as $feed ) { |
||
393 | $feed += [ |
||
394 | 'omit_bots' => false, |
||
395 | 'omit_anon' => false, |
||
396 | 'omit_user' => false, |
||
397 | 'omit_minor' => false, |
||
398 | 'omit_patrolled' => false, |
||
399 | ]; |
||
400 | |||
401 | if ( |
||
402 | ( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) || |
||
403 | ( $feed['omit_anon'] && $performer->isAnon() ) || |
||
404 | ( $feed['omit_user'] && !$performer->isAnon() ) || |
||
405 | ( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) || |
||
406 | ( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) || |
||
407 | $this->mAttribs['rc_type'] == RC_EXTERNAL |
||
408 | ) { |
||
409 | continue; |
||
410 | } |
||
411 | |||
412 | $engine = self::getEngine( $feed['uri'] ); |
||
413 | |||
414 | if ( isset( $this->mExtra['actionCommentIRC'] ) ) { |
||
415 | $actionComment = $this->mExtra['actionCommentIRC']; |
||
416 | } else { |
||
417 | $actionComment = null; |
||
418 | } |
||
419 | |||
420 | /** @var $formatter RCFeedFormatter */ |
||
421 | $formatter = is_object( $feed['formatter'] ) ? $feed['formatter'] : new $feed['formatter'](); |
||
422 | $line = $formatter->getLine( $feed, $this, $actionComment ); |
||
423 | if ( !$line ) { |
||
0 ignored issues
–
show
|
|||
424 | // T109544 |
||
425 | // If a feed formatter returns null, this will otherwise cause an |
||
426 | // error in at least RedisPubSubFeedEngine. |
||
427 | // Not sure where/how this should best be handled. |
||
428 | continue; |
||
429 | } |
||
430 | |||
431 | $engine->send( $feed, $line ); |
||
432 | } |
||
433 | } |
||
434 | |||
435 | /** |
||
436 | * Gets the stream engine object for a given URI from $wgRCEngines |
||
437 | * |
||
438 | * @param string $uri URI to get the engine object for |
||
439 | * @throws MWException |
||
440 | * @return RCFeedEngine The engine object |
||
441 | */ |
||
442 | public static function getEngine( $uri ) { |
||
443 | global $wgRCEngines; |
||
444 | |||
445 | $scheme = parse_url( $uri, PHP_URL_SCHEME ); |
||
446 | if ( !$scheme ) { |
||
447 | throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" ); |
||
448 | } |
||
449 | |||
450 | if ( !isset( $wgRCEngines[$scheme] ) ) { |
||
451 | throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" ); |
||
452 | } |
||
453 | |||
454 | return new $wgRCEngines[$scheme]; |
||
455 | } |
||
456 | |||
457 | /** |
||
458 | * Mark a given change as patrolled |
||
459 | * |
||
460 | * @param RecentChange|int $change RecentChange or corresponding rc_id |
||
461 | * @param bool $auto For automatic patrol |
||
462 | * @param string|string[] $tags Change tags to add to the patrol log entry |
||
463 | * ($user should be able to add the specified tags before this is called) |
||
464 | * @return array See doMarkPatrolled(), or null if $change is not an existing rc_id |
||
465 | */ |
||
466 | public static function markPatrolled( $change, $auto = false, $tags = null ) { |
||
467 | global $wgUser; |
||
468 | |||
469 | $change = $change instanceof RecentChange |
||
470 | ? $change |
||
471 | : RecentChange::newFromId( $change ); |
||
472 | |||
473 | if ( !$change instanceof RecentChange ) { |
||
474 | return null; |
||
475 | } |
||
476 | |||
477 | return $change->doMarkPatrolled( $wgUser, $auto, $tags ); |
||
478 | } |
||
479 | |||
480 | /** |
||
481 | * Mark this RecentChange as patrolled |
||
482 | * |
||
483 | * NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and |
||
484 | * 'markedaspatrollederror-noautopatrol' as errors |
||
485 | * @param User $user User object doing the action |
||
486 | * @param bool $auto For automatic patrol |
||
487 | * @param string|string[] $tags Change tags to add to the patrol log entry |
||
488 | * ($user should be able to add the specified tags before this is called) |
||
489 | * @return array Array of permissions errors, see Title::getUserPermissionsErrors() |
||
490 | */ |
||
491 | public function doMarkPatrolled( User $user, $auto = false, $tags = null ) { |
||
492 | global $wgUseRCPatrol, $wgUseNPPatrol, $wgUseFilePatrol; |
||
493 | |||
494 | $errors = []; |
||
495 | // If recentchanges patrol is disabled, only new pages or new file versions |
||
496 | // can be patrolled, provided the appropriate config variable is set |
||
497 | if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) && |
||
498 | ( !$wgUseFilePatrol || !( $this->getAttribute( 'rc_type' ) == RC_LOG && |
||
499 | $this->getAttribute( 'rc_log_type' ) == 'upload' ) ) ) { |
||
500 | $errors[] = [ 'rcpatroldisabled' ]; |
||
501 | } |
||
502 | // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol" |
||
503 | $right = $auto ? 'autopatrol' : 'patrol'; |
||
504 | $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) ); |
||
505 | if ( !Hooks::run( 'MarkPatrolled', |
||
506 | [ $this->getAttribute( 'rc_id' ), &$user, false, $auto ] ) |
||
507 | ) { |
||
508 | $errors[] = [ 'hookaborted' ]; |
||
509 | } |
||
510 | // Users without the 'autopatrol' right can't patrol their |
||
511 | // own revisions |
||
512 | if ( $user->getName() === $this->getAttribute( 'rc_user_text' ) |
||
513 | && !$user->isAllowed( 'autopatrol' ) |
||
514 | ) { |
||
515 | $errors[] = [ 'markedaspatrollederror-noautopatrol' ]; |
||
516 | } |
||
517 | if ( $errors ) { |
||
518 | return $errors; |
||
519 | } |
||
520 | // If the change was patrolled already, do nothing |
||
521 | if ( $this->getAttribute( 'rc_patrolled' ) ) { |
||
522 | return []; |
||
523 | } |
||
524 | // Actually set the 'patrolled' flag in RC |
||
525 | $this->reallyMarkPatrolled(); |
||
526 | // Log this patrol event |
||
527 | PatrolLog::record( $this, $auto, $user, $tags ); |
||
528 | |||
529 | Hooks::run( |
||
530 | 'MarkPatrolledComplete', |
||
531 | [ $this->getAttribute( 'rc_id' ), &$user, false, $auto ] |
||
532 | ); |
||
533 | |||
534 | return []; |
||
535 | } |
||
536 | |||
537 | /** |
||
538 | * Mark this RecentChange patrolled, without error checking |
||
539 | * @return int Number of affected rows |
||
540 | */ |
||
541 | public function reallyMarkPatrolled() { |
||
542 | $dbw = wfGetDB( DB_MASTER ); |
||
543 | $dbw->update( |
||
544 | 'recentchanges', |
||
545 | [ |
||
546 | 'rc_patrolled' => 1 |
||
547 | ], |
||
548 | [ |
||
549 | 'rc_id' => $this->getAttribute( 'rc_id' ) |
||
550 | ], |
||
551 | __METHOD__ |
||
552 | ); |
||
553 | // Invalidate the page cache after the page has been patrolled |
||
554 | // to make sure that the Patrol link isn't visible any longer! |
||
555 | $this->getTitle()->invalidateCache(); |
||
556 | |||
557 | return $dbw->affectedRows(); |
||
558 | } |
||
559 | |||
560 | /** |
||
561 | * Makes an entry in the database corresponding to an edit |
||
562 | * |
||
563 | * @param string $timestamp |
||
564 | * @param Title $title |
||
565 | * @param bool $minor |
||
566 | * @param User $user |
||
567 | * @param string $comment |
||
568 | * @param int $oldId |
||
569 | * @param string $lastTimestamp |
||
570 | * @param bool $bot |
||
571 | * @param string $ip |
||
572 | * @param int $oldSize |
||
573 | * @param int $newSize |
||
574 | * @param int $newId |
||
575 | * @param int $patrol |
||
576 | * @param array $tags |
||
577 | * @return RecentChange |
||
578 | */ |
||
579 | public static function notifyEdit( |
||
580 | $timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp, |
||
581 | $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0, |
||
582 | $tags = [] |
||
583 | ) { |
||
584 | $rc = new RecentChange; |
||
585 | $rc->mTitle = $title; |
||
586 | $rc->mPerformer = $user; |
||
587 | $rc->mAttribs = [ |
||
588 | 'rc_timestamp' => $timestamp, |
||
589 | 'rc_namespace' => $title->getNamespace(), |
||
590 | 'rc_title' => $title->getDBkey(), |
||
591 | 'rc_type' => RC_EDIT, |
||
592 | 'rc_source' => self::SRC_EDIT, |
||
593 | 'rc_minor' => $minor ? 1 : 0, |
||
594 | 'rc_cur_id' => $title->getArticleID(), |
||
595 | 'rc_user' => $user->getId(), |
||
596 | 'rc_user_text' => $user->getName(), |
||
597 | 'rc_comment' => $comment, |
||
598 | 'rc_this_oldid' => $newId, |
||
599 | 'rc_last_oldid' => $oldId, |
||
600 | 'rc_bot' => $bot ? 1 : 0, |
||
601 | 'rc_ip' => self::checkIPAddress( $ip ), |
||
602 | 'rc_patrolled' => intval( $patrol ), |
||
603 | 'rc_new' => 0, # obsolete |
||
604 | 'rc_old_len' => $oldSize, |
||
605 | 'rc_new_len' => $newSize, |
||
606 | 'rc_deleted' => 0, |
||
607 | 'rc_logid' => 0, |
||
608 | 'rc_log_type' => null, |
||
609 | 'rc_log_action' => '', |
||
610 | 'rc_params' => '' |
||
611 | ]; |
||
612 | |||
613 | $rc->mExtra = [ |
||
614 | 'prefixedDBkey' => $title->getPrefixedDBkey(), |
||
615 | 'lastTimestamp' => $lastTimestamp, |
||
616 | 'oldSize' => $oldSize, |
||
617 | 'newSize' => $newSize, |
||
618 | 'pageStatus' => 'changed' |
||
619 | ]; |
||
620 | |||
621 | DeferredUpdates::addCallableUpdate( |
||
622 | View Code Duplication | function () use ( $rc, $tags ) { |
|
623 | $rc->addTags( $tags ); |
||
624 | $rc->save(); |
||
625 | if ( $rc->mAttribs['rc_patrolled'] ) { |
||
626 | PatrolLog::record( $rc, true, $rc->getPerformer() ); |
||
627 | } |
||
628 | }, |
||
629 | DeferredUpdates::POSTSEND, |
||
630 | wfGetDB( DB_MASTER ) |
||
631 | ); |
||
632 | |||
633 | return $rc; |
||
634 | } |
||
635 | |||
636 | /** |
||
637 | * Makes an entry in the database corresponding to page creation |
||
638 | * Note: the title object must be loaded with the new id using resetArticleID() |
||
639 | * |
||
640 | * @param string $timestamp |
||
641 | * @param Title $title |
||
642 | * @param bool $minor |
||
643 | * @param User $user |
||
644 | * @param string $comment |
||
645 | * @param bool $bot |
||
646 | * @param string $ip |
||
647 | * @param int $size |
||
648 | * @param int $newId |
||
649 | * @param int $patrol |
||
650 | * @param array $tags |
||
651 | * @return RecentChange |
||
652 | */ |
||
653 | public static function notifyNew( |
||
654 | $timestamp, &$title, $minor, &$user, $comment, $bot, |
||
655 | $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = [] |
||
656 | ) { |
||
657 | $rc = new RecentChange; |
||
658 | $rc->mTitle = $title; |
||
659 | $rc->mPerformer = $user; |
||
660 | $rc->mAttribs = [ |
||
661 | 'rc_timestamp' => $timestamp, |
||
662 | 'rc_namespace' => $title->getNamespace(), |
||
663 | 'rc_title' => $title->getDBkey(), |
||
664 | 'rc_type' => RC_NEW, |
||
665 | 'rc_source' => self::SRC_NEW, |
||
666 | 'rc_minor' => $minor ? 1 : 0, |
||
667 | 'rc_cur_id' => $title->getArticleID(), |
||
668 | 'rc_user' => $user->getId(), |
||
669 | 'rc_user_text' => $user->getName(), |
||
670 | 'rc_comment' => $comment, |
||
671 | 'rc_this_oldid' => $newId, |
||
672 | 'rc_last_oldid' => 0, |
||
673 | 'rc_bot' => $bot ? 1 : 0, |
||
674 | 'rc_ip' => self::checkIPAddress( $ip ), |
||
675 | 'rc_patrolled' => intval( $patrol ), |
||
676 | 'rc_new' => 1, # obsolete |
||
677 | 'rc_old_len' => 0, |
||
678 | 'rc_new_len' => $size, |
||
679 | 'rc_deleted' => 0, |
||
680 | 'rc_logid' => 0, |
||
681 | 'rc_log_type' => null, |
||
682 | 'rc_log_action' => '', |
||
683 | 'rc_params' => '' |
||
684 | ]; |
||
685 | |||
686 | $rc->mExtra = [ |
||
687 | 'prefixedDBkey' => $title->getPrefixedDBkey(), |
||
688 | 'lastTimestamp' => 0, |
||
689 | 'oldSize' => 0, |
||
690 | 'newSize' => $size, |
||
691 | 'pageStatus' => 'created' |
||
692 | ]; |
||
693 | |||
694 | DeferredUpdates::addCallableUpdate( |
||
695 | View Code Duplication | function () use ( $rc, $tags ) { |
|
696 | $rc->addTags( $tags ); |
||
697 | $rc->save(); |
||
698 | if ( $rc->mAttribs['rc_patrolled'] ) { |
||
699 | PatrolLog::record( $rc, true, $rc->getPerformer() ); |
||
700 | } |
||
701 | }, |
||
702 | DeferredUpdates::POSTSEND, |
||
703 | wfGetDB( DB_MASTER ) |
||
704 | ); |
||
705 | |||
706 | return $rc; |
||
707 | } |
||
708 | |||
709 | /** |
||
710 | * @param string $timestamp |
||
711 | * @param Title $title |
||
712 | * @param User $user |
||
713 | * @param string $actionComment |
||
714 | * @param string $ip |
||
715 | * @param string $type |
||
716 | * @param string $action |
||
717 | * @param Title $target |
||
718 | * @param string $logComment |
||
719 | * @param string $params |
||
720 | * @param int $newId |
||
721 | * @param string $actionCommentIRC |
||
722 | * @return bool |
||
723 | */ |
||
724 | public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type, |
||
725 | $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '' |
||
726 | ) { |
||
727 | global $wgLogRestrictions; |
||
728 | |||
729 | # Don't add private logs to RC! |
||
730 | if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) { |
||
731 | return false; |
||
732 | } |
||
733 | $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action, |
||
734 | $target, $logComment, $params, $newId, $actionCommentIRC ); |
||
735 | $rc->save(); |
||
736 | |||
737 | return true; |
||
738 | } |
||
739 | |||
740 | /** |
||
741 | * @param string $timestamp |
||
742 | * @param Title $title |
||
743 | * @param User $user |
||
744 | * @param string $actionComment |
||
745 | * @param string $ip |
||
746 | * @param string $type |
||
747 | * @param string $action |
||
748 | * @param Title $target |
||
749 | * @param string $logComment |
||
750 | * @param string $params |
||
751 | * @param int $newId |
||
752 | * @param string $actionCommentIRC |
||
753 | * @param int $revId Id of associated revision, if any |
||
754 | * @param bool $isPatrollable Whether this log entry is patrollable |
||
755 | * @return RecentChange |
||
756 | */ |
||
757 | public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip, |
||
758 | $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '', |
||
759 | $revId = 0, $isPatrollable = false ) { |
||
760 | global $wgRequest; |
||
761 | |||
762 | # # Get pageStatus for email notification |
||
763 | switch ( $type . '-' . $action ) { |
||
764 | case 'delete-delete': |
||
765 | $pageStatus = 'deleted'; |
||
766 | break; |
||
767 | case 'move-move': |
||
768 | case 'move-move_redir': |
||
769 | $pageStatus = 'moved'; |
||
770 | break; |
||
771 | case 'delete-restore': |
||
772 | $pageStatus = 'restored'; |
||
773 | break; |
||
774 | case 'upload-upload': |
||
775 | $pageStatus = 'created'; |
||
776 | break; |
||
777 | case 'upload-overwrite': |
||
778 | default: |
||
779 | $pageStatus = 'changed'; |
||
780 | break; |
||
781 | } |
||
782 | |||
783 | // Allow unpatrolled status for patrollable log entries |
||
784 | $markPatrolled = $isPatrollable ? $user->isAllowed( 'autopatrol' ) : true; |
||
785 | |||
786 | $rc = new RecentChange; |
||
787 | $rc->mTitle = $target; |
||
788 | $rc->mPerformer = $user; |
||
789 | $rc->mAttribs = [ |
||
790 | 'rc_timestamp' => $timestamp, |
||
791 | 'rc_namespace' => $target->getNamespace(), |
||
792 | 'rc_title' => $target->getDBkey(), |
||
793 | 'rc_type' => RC_LOG, |
||
794 | 'rc_source' => self::SRC_LOG, |
||
795 | 'rc_minor' => 0, |
||
796 | 'rc_cur_id' => $target->getArticleID(), |
||
797 | 'rc_user' => $user->getId(), |
||
798 | 'rc_user_text' => $user->getName(), |
||
799 | 'rc_comment' => $logComment, |
||
800 | 'rc_this_oldid' => $revId, |
||
801 | 'rc_last_oldid' => 0, |
||
802 | 'rc_bot' => $user->isAllowed( 'bot' ) ? (int)$wgRequest->getBool( 'bot', true ) : 0, |
||
803 | 'rc_ip' => self::checkIPAddress( $ip ), |
||
804 | 'rc_patrolled' => $markPatrolled ? 1 : 0, |
||
805 | 'rc_new' => 0, # obsolete |
||
806 | 'rc_old_len' => null, |
||
807 | 'rc_new_len' => null, |
||
808 | 'rc_deleted' => 0, |
||
809 | 'rc_logid' => $newId, |
||
810 | 'rc_log_type' => $type, |
||
811 | 'rc_log_action' => $action, |
||
812 | 'rc_params' => $params |
||
813 | ]; |
||
814 | |||
815 | $rc->mExtra = [ |
||
816 | 'prefixedDBkey' => $title->getPrefixedDBkey(), |
||
817 | 'lastTimestamp' => 0, |
||
818 | 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage |
||
819 | 'pageStatus' => $pageStatus, |
||
820 | 'actionCommentIRC' => $actionCommentIRC |
||
821 | ]; |
||
822 | |||
823 | return $rc; |
||
824 | } |
||
825 | |||
826 | /** |
||
827 | * Constructs a RecentChange object for the given categorization |
||
828 | * This does not call save() on the object and thus does not write to the db |
||
829 | * |
||
830 | * @since 1.27 |
||
831 | * |
||
832 | * @param string $timestamp Timestamp of the recent change to occur |
||
833 | * @param Title $categoryTitle Title of the category a page is being added to or removed from |
||
834 | * @param User $user User object of the user that made the change |
||
835 | * @param string $comment Change summary |
||
836 | * @param Title $pageTitle Title of the page that is being added or removed |
||
837 | * @param int $oldRevId Parent revision ID of this change |
||
838 | * @param int $newRevId Revision ID of this change |
||
839 | * @param string $lastTimestamp Parent revision timestamp of this change |
||
840 | * @param bool $bot true, if the change was made by a bot |
||
841 | * @param string $ip IP address of the user, if the change was made anonymously |
||
842 | * @param int $deleted Indicates whether the change has been deleted |
||
843 | * |
||
844 | * @return RecentChange |
||
845 | */ |
||
846 | public static function newForCategorization( |
||
847 | $timestamp, |
||
848 | Title $categoryTitle, |
||
849 | User $user = null, |
||
850 | $comment, |
||
851 | Title $pageTitle, |
||
852 | $oldRevId, |
||
853 | $newRevId, |
||
854 | $lastTimestamp, |
||
855 | $bot, |
||
856 | $ip = '', |
||
857 | $deleted = 0 |
||
858 | ) { |
||
859 | $rc = new RecentChange; |
||
860 | $rc->mTitle = $categoryTitle; |
||
861 | $rc->mPerformer = $user; |
||
862 | $rc->mAttribs = [ |
||
863 | 'rc_timestamp' => $timestamp, |
||
864 | 'rc_namespace' => $categoryTitle->getNamespace(), |
||
865 | 'rc_title' => $categoryTitle->getDBkey(), |
||
866 | 'rc_type' => RC_CATEGORIZE, |
||
867 | 'rc_source' => self::SRC_CATEGORIZE, |
||
868 | 'rc_minor' => 0, |
||
869 | 'rc_cur_id' => $pageTitle->getArticleID(), |
||
870 | 'rc_user' => $user ? $user->getId() : 0, |
||
871 | 'rc_user_text' => $user ? $user->getName() : '', |
||
872 | 'rc_comment' => $comment, |
||
873 | 'rc_this_oldid' => $newRevId, |
||
874 | 'rc_last_oldid' => $oldRevId, |
||
875 | 'rc_bot' => $bot ? 1 : 0, |
||
876 | 'rc_ip' => self::checkIPAddress( $ip ), |
||
877 | 'rc_patrolled' => 1, // Always patrolled, just like log entries |
||
878 | 'rc_new' => 0, # obsolete |
||
879 | 'rc_old_len' => null, |
||
880 | 'rc_new_len' => null, |
||
881 | 'rc_deleted' => $deleted, |
||
882 | 'rc_logid' => 0, |
||
883 | 'rc_log_type' => null, |
||
884 | 'rc_log_action' => '', |
||
885 | 'rc_params' => serialize( [ |
||
886 | 'hidden-cat' => WikiCategoryPage::factory( $categoryTitle )->isHidden() |
||
887 | ] ) |
||
888 | ]; |
||
889 | |||
890 | $rc->mExtra = [ |
||
891 | 'prefixedDBkey' => $categoryTitle->getPrefixedDBkey(), |
||
892 | 'lastTimestamp' => $lastTimestamp, |
||
893 | 'oldSize' => 0, |
||
894 | 'newSize' => 0, |
||
895 | 'pageStatus' => 'changed' |
||
896 | ]; |
||
897 | |||
898 | return $rc; |
||
899 | } |
||
900 | |||
901 | /** |
||
902 | * Get a parameter value |
||
903 | * |
||
904 | * @since 1.27 |
||
905 | * |
||
906 | * @param string $name parameter name |
||
907 | * @return mixed |
||
908 | */ |
||
909 | public function getParam( $name ) { |
||
910 | $params = $this->parseParams(); |
||
911 | return isset( $params[$name] ) ? $params[$name] : null; |
||
912 | } |
||
913 | |||
914 | /** |
||
915 | * Initialises the members of this object from a mysql row object |
||
916 | * |
||
917 | * @param mixed $row |
||
918 | */ |
||
919 | public function loadFromRow( $row ) { |
||
920 | $this->mAttribs = get_object_vars( $row ); |
||
921 | $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] ); |
||
922 | $this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set |
||
923 | } |
||
924 | |||
925 | /** |
||
926 | * Get an attribute value |
||
927 | * |
||
928 | * @param string $name Attribute name |
||
929 | * @return mixed |
||
930 | */ |
||
931 | public function getAttribute( $name ) { |
||
932 | return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null; |
||
933 | } |
||
934 | |||
935 | /** |
||
936 | * @return array |
||
937 | */ |
||
938 | public function getAttributes() { |
||
939 | return $this->mAttribs; |
||
940 | } |
||
941 | |||
942 | /** |
||
943 | * Gets the end part of the diff URL associated with this object |
||
944 | * Blank if no diff link should be displayed |
||
945 | * @param bool $forceCur |
||
946 | * @return string |
||
947 | */ |
||
948 | public function diffLinkTrail( $forceCur ) { |
||
949 | if ( $this->mAttribs['rc_type'] == RC_EDIT ) { |
||
950 | $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) . |
||
951 | "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] ); |
||
952 | if ( $forceCur ) { |
||
953 | $trail .= '&diff=0'; |
||
954 | } else { |
||
955 | $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] ); |
||
956 | } |
||
957 | } else { |
||
958 | $trail = ''; |
||
959 | } |
||
960 | |||
961 | return $trail; |
||
962 | } |
||
963 | |||
964 | /** |
||
965 | * Returns the change size (HTML). |
||
966 | * The lengths can be given optionally. |
||
967 | * @param int $old |
||
968 | * @param int $new |
||
969 | * @return string |
||
970 | */ |
||
971 | public function getCharacterDifference( $old = 0, $new = 0 ) { |
||
972 | if ( $old === 0 ) { |
||
973 | $old = $this->mAttribs['rc_old_len']; |
||
974 | } |
||
975 | if ( $new === 0 ) { |
||
976 | $new = $this->mAttribs['rc_new_len']; |
||
977 | } |
||
978 | if ( $old === null || $new === null ) { |
||
979 | return ''; |
||
980 | } |
||
981 | |||
982 | return ChangesList::showCharacterDifference( $old, $new ); |
||
983 | } |
||
984 | |||
985 | private static function checkIPAddress( $ip ) { |
||
986 | global $wgRequest; |
||
987 | if ( $ip ) { |
||
988 | if ( !IP::isIPAddress( $ip ) ) { |
||
989 | throw new MWException( "Attempt to write \"" . $ip . |
||
990 | "\" as an IP address into recent changes" ); |
||
991 | } |
||
992 | } else { |
||
993 | $ip = $wgRequest->getIP(); |
||
994 | if ( !$ip ) { |
||
995 | $ip = ''; |
||
996 | } |
||
997 | } |
||
998 | |||
999 | return $ip; |
||
1000 | } |
||
1001 | |||
1002 | /** |
||
1003 | * Check whether the given timestamp is new enough to have a RC row with a given tolerance |
||
1004 | * as the recentchanges table might not be cleared out regularly (so older entries might exist) |
||
1005 | * or rows which will be deleted soon shouldn't be included. |
||
1006 | * |
||
1007 | * @param mixed $timestamp MWTimestamp compatible timestamp |
||
1008 | * @param int $tolerance Tolerance in seconds |
||
1009 | * @return bool |
||
1010 | */ |
||
1011 | public static function isInRCLifespan( $timestamp, $tolerance = 0 ) { |
||
1012 | global $wgRCMaxAge; |
||
1013 | |||
1014 | return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge; |
||
1015 | } |
||
1016 | |||
1017 | /** |
||
1018 | * Parses and returns the rc_params attribute |
||
1019 | * |
||
1020 | * @since 1.26 |
||
1021 | * |
||
1022 | * @return mixed|bool false on failed unserialization |
||
1023 | */ |
||
1024 | public function parseParams() { |
||
1025 | $rcParams = $this->getAttribute( 'rc_params' ); |
||
1026 | |||
1027 | MediaWiki\suppressWarnings(); |
||
1028 | $unserializedParams = unserialize( $rcParams ); |
||
1029 | MediaWiki\restoreWarnings(); |
||
1030 | |||
1031 | return $unserializedParams; |
||
1032 | } |
||
1033 | |||
1034 | /** |
||
1035 | * Tags to append to the recent change, |
||
1036 | * and associated revision/log |
||
1037 | * |
||
1038 | * @since 1.28 |
||
1039 | * |
||
1040 | * @param string|array $tags |
||
1041 | */ |
||
1042 | public function addTags( $tags ) { |
||
1043 | if ( is_string( $tags ) ) { |
||
1044 | $this->tags[] = $tags; |
||
1045 | } else { |
||
1046 | $this->tags = array_merge( $tags, $this->tags ); |
||
1047 | } |
||
1048 | } |
||
1049 | } |
||
1050 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: