Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 242 |
|
0.00% |
0 / 45 |
CRAP | |
0.00% |
0 / 1 |
Event | |
0.00% |
0 / 241 |
|
0.00% |
0 / 45 |
12432 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__sleep | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
__wakeup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__toString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
create | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
210 | |||
toDbArray | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
isEnabledEvent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
insert | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
resolveTargetPages | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
loadFromRow | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
182 | |||
loadFromID | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
newFromRow | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
newFromID | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
serializeExtra | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
userCan | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
getId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getVariant | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtra | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtraParam | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAgent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canNotifyAgent | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTitle | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 | |||
getRevision | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getCategory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUseJobQueue | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setVariant | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setAgent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTitle | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setExtra | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLinkMessage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getLinkDestination | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getBundleHash | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setBundleHash | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isDeleted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setBundledEvents | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getBundledEvents | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canBeBundled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getBundlingKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setBundledElements | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSortingKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
selectFields | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications\Model; |
4 | |
5 | use Exception; |
6 | use IDBAccessObject; |
7 | use InvalidArgumentException; |
8 | use MediaWiki\Extension\Notifications\Bundleable; |
9 | use MediaWiki\Extension\Notifications\Controller\NotificationController; |
10 | use MediaWiki\Extension\Notifications\DbFactory; |
11 | use MediaWiki\Extension\Notifications\Hooks\HookRunner; |
12 | use MediaWiki\Extension\Notifications\Mapper\EventMapper; |
13 | use MediaWiki\Extension\Notifications\Mapper\TargetPageMapper; |
14 | use MediaWiki\Extension\Notifications\Services; |
15 | use MediaWiki\Logger\LoggerFactory; |
16 | use MediaWiki\MediaWikiServices; |
17 | use MediaWiki\Revision\RevisionRecord; |
18 | use MediaWiki\Title\Title; |
19 | use MediaWiki\User\User; |
20 | use MediaWiki\User\UserIdentity; |
21 | use RuntimeException; |
22 | use stdClass; |
23 | |
24 | /** |
25 | * Immutable class to represent an event. |
26 | * In Echo nomenclature, an event is a single occurrence. |
27 | */ |
28 | class Event extends AbstractEntity implements Bundleable { |
29 | |
30 | /** @var string|null */ |
31 | protected $type = null; |
32 | /** @var int|null|false */ |
33 | protected $id = null; |
34 | /** @var string|null */ |
35 | protected $variant = null; |
36 | /** |
37 | * @var User|null |
38 | */ |
39 | protected $agent = null; |
40 | |
41 | /** |
42 | * Loaded dynamically on request |
43 | * |
44 | * @var Title|null |
45 | */ |
46 | protected $title = null; |
47 | /** @var int|null */ |
48 | protected $pageId = null; |
49 | |
50 | /** |
51 | * Loaded dynamically on request |
52 | * |
53 | * @var RevisionRecord|null |
54 | */ |
55 | protected $revision = null; |
56 | |
57 | /** @var array */ |
58 | protected $extra = []; |
59 | |
60 | /** |
61 | * Notification timestamp |
62 | * @var string|null |
63 | */ |
64 | protected $timestamp = null; |
65 | |
66 | /** |
67 | * A hash used to bundle a set of events, events that can be |
68 | * grouped for a user has the same bundle hash |
69 | * @var string|null |
70 | */ |
71 | protected $bundleHash; |
72 | |
73 | /** |
74 | * Other events bundled with this one |
75 | * |
76 | * @var Event[] |
77 | */ |
78 | protected $bundledEvents; |
79 | |
80 | /** |
81 | * Deletion flag |
82 | * |
83 | * @var int |
84 | */ |
85 | protected $deleted = 0; |
86 | |
87 | /** |
88 | * You should not call the constructor. |
89 | * Instead, use one of the factory functions: |
90 | * Event::create To create a new event |
91 | * Event::newFromRow To create an event object from a row object |
92 | * Event::newFromID To create an event object from the database given its ID |
93 | */ |
94 | protected function __construct() { |
95 | } |
96 | |
97 | ## Save the id and timestamp |
98 | public function __sleep() { |
99 | if ( !$this->id ) { |
100 | throw new RuntimeException( "Unable to serialize an uninitialized Event" ); |
101 | } |
102 | |
103 | return [ 'id', 'timestamp' ]; |
104 | } |
105 | |
106 | public function __wakeup() { |
107 | $this->loadFromID( $this->id ); |
108 | } |
109 | |
110 | public function __toString() { |
111 | return "Event(id={$this->id}; type={$this->type})"; |
112 | } |
113 | |
114 | /** |
115 | * Creates an Event object |
116 | * @param array $info Named arguments: |
117 | * type (required): The event type; |
118 | * variant: A variant of the type; |
119 | * agent: The user who caused the event; |
120 | * title: The page on which the event was triggered; |
121 | * extra: Event-specific extra information (e.g. post content, delay time, root job params). |
122 | * |
123 | * Delayed jobs extra params: |
124 | * delay: Amount of time in seconds for the notification to be delayed |
125 | * |
126 | * Job deduplication extra params: |
127 | * rootJobSignature: The sha1 signature of the job |
128 | * rootJobTimestamp: The timestamp when the job gets submitted |
129 | * |
130 | * For example to enqueue a new `example` root job or make a parent job |
131 | * no-op when submitting a new notification you need to pass this extra params: |
132 | * |
133 | * [ 'extra' => Job::newRootJobParams('example') ] |
134 | * |
135 | * @return Event|false False if aborted via hook or Echo DB is read-only |
136 | */ |
137 | public static function create( $info = [] ) { |
138 | global $wgEchoNotifications; |
139 | |
140 | $services = MediaWikiServices::getInstance(); |
141 | // Do not create event and notifications if write access is locked |
142 | if ( $services->getReadOnlyMode()->isReadOnly() |
143 | || DbFactory::newFromDefault()->getEchoDb( DB_PRIMARY )->isReadOnly() |
144 | ) { |
145 | return false; |
146 | } |
147 | |
148 | $obj = new Event; |
149 | static $validFields = [ 'type', 'variant', 'agent', 'title', 'extra' ]; |
150 | |
151 | if ( empty( $info['type'] ) ) { |
152 | throw new InvalidArgumentException( "'type' parameter is mandatory" ); |
153 | } |
154 | |
155 | if ( !isset( $wgEchoNotifications[$info['type']] ) ) { |
156 | return false; |
157 | } |
158 | |
159 | $obj->id = false; |
160 | $obj->timestamp = $info['timestamp'] ?? wfTimestampNow(); |
161 | foreach ( $validFields as $field ) { |
162 | if ( isset( $info[$field] ) ) { |
163 | $obj->$field = $info[$field]; |
164 | } |
165 | } |
166 | |
167 | // If the extra size is more than 50000 bytes, that means there is |
168 | // probably a problem with the design of this notification type. |
169 | // There might be data loss if the size exceeds the DB column size of |
170 | // event_extra. |
171 | if ( strlen( $obj->serializeExtra() ) > 50000 ) { |
172 | wfDebugLog( __CLASS__, __FUNCTION__ . ': event extra data is too huge for ' . $info['type'] ); |
173 | |
174 | return false; |
175 | } |
176 | |
177 | if ( $obj->title ) { |
178 | if ( !$obj->title instanceof Title ) { |
179 | throw new InvalidArgumentException( 'Invalid title parameter' ); |
180 | } |
181 | $obj->setTitle( $obj->title ); |
182 | } |
183 | |
184 | if ( $obj->agent ) { |
185 | if ( !$obj->agent instanceof UserIdentity ) { |
186 | throw new InvalidArgumentException( "Invalid user parameter" ); |
187 | } |
188 | |
189 | // RevisionStore returns UserIdentityValue now, convert to User for passing to hooks. |
190 | if ( !$obj->agent instanceof User ) { |
191 | $obj->agent = $services->getUserFactory()->newFromUserIdentity( $obj->agent ); |
192 | } |
193 | } |
194 | |
195 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
196 | if ( !$hookRunner->onBeforeEchoEventInsert( $obj ) ) { |
197 | return false; |
198 | } |
199 | |
200 | // @Todo - Database insert logic should not be inside the model |
201 | $obj->insert(); |
202 | |
203 | $hookRunner->onEventInsertComplete( $obj ); |
204 | |
205 | global $wgEchoUseJobQueue; |
206 | |
207 | NotificationController::notify( $obj, $wgEchoUseJobQueue ); |
208 | |
209 | $stats = $services->getStatsdDataFactory(); |
210 | $type = $info['type']; |
211 | $stats->increment( 'echo.event.all' ); |
212 | $stats->increment( "echo.event.$type" ); |
213 | |
214 | return $obj; |
215 | } |
216 | |
217 | /** |
218 | * Convert the object's database property to array |
219 | * @return array |
220 | */ |
221 | public function toDbArray() { |
222 | $data = [ |
223 | 'event_type' => $this->type, |
224 | 'event_variant' => $this->variant, |
225 | 'event_deleted' => $this->deleted, |
226 | 'event_extra' => $this->serializeExtra() |
227 | ]; |
228 | if ( $this->id ) { |
229 | $data['event_id'] = $this->id; |
230 | } |
231 | if ( $this->agent ) { |
232 | if ( !$this->agent->isRegistered() ) { |
233 | $data['event_agent_ip'] = $this->agent->getName(); |
234 | } else { |
235 | $data['event_agent_id'] = $this->agent->getId(); |
236 | } |
237 | } |
238 | |
239 | if ( $this->pageId ) { |
240 | $data['event_page_id'] = $this->pageId; |
241 | } elseif ( $this->title ) { |
242 | $pageId = $this->title->getArticleID(); |
243 | // Don't need any special handling for title with no id |
244 | // as they are already stored in extra data array |
245 | if ( $pageId ) { |
246 | $data['event_page_id'] = $pageId; |
247 | } |
248 | } |
249 | |
250 | return $data; |
251 | } |
252 | |
253 | /** |
254 | * Check whether the echo event is an enabled event |
255 | * @return bool |
256 | */ |
257 | public function isEnabledEvent(): bool { |
258 | global $wgEchoNotifications; |
259 | return isset( $wgEchoNotifications[$this->getType()] ); |
260 | } |
261 | |
262 | /** |
263 | * Inserts the object into the database. |
264 | */ |
265 | protected function insert() { |
266 | $eventMapper = new EventMapper(); |
267 | $this->id = $eventMapper->insert( $this ); |
268 | |
269 | $targetPages = self::resolveTargetPages( $this->getExtraParam( 'target-page' ) ); |
270 | if ( $targetPages ) { |
271 | $targetMapper = new TargetPageMapper(); |
272 | foreach ( $targetPages as $title ) { |
273 | $targetPage = TargetPage::create( $title, $this ); |
274 | if ( $targetPage ) { |
275 | $targetMapper->insert( $targetPage ); |
276 | } |
277 | } |
278 | } |
279 | } |
280 | |
281 | /** |
282 | * @param int[]|int|false $targetPageIds |
283 | * @return Title[] |
284 | */ |
285 | protected static function resolveTargetPages( $targetPageIds ) { |
286 | if ( !$targetPageIds ) { |
287 | return []; |
288 | } |
289 | if ( !is_array( $targetPageIds ) ) { |
290 | $targetPageIds = [ $targetPageIds ]; |
291 | } |
292 | $result = []; |
293 | foreach ( $targetPageIds as $targetPageId ) { |
294 | // Make sure the target-page id is a valid id |
295 | $title = Title::newFromID( $targetPageId ); |
296 | // Try primary database if there is no match |
297 | if ( !$title ) { |
298 | $title = Title::newFromID( $targetPageId, IDBAccessObject::READ_LATEST ); |
299 | } |
300 | if ( $title ) { |
301 | $result[] = $title; |
302 | } |
303 | } |
304 | |
305 | return $result; |
306 | } |
307 | |
308 | /** |
309 | * Loads data from the provided $row into this object. |
310 | * |
311 | * @param stdClass $row row object from echo_event |
312 | * @return bool Whether loading was successful |
313 | */ |
314 | public function loadFromRow( $row ) { |
315 | $this->id = (int)$row->event_id; |
316 | $this->type = $row->event_type; |
317 | |
318 | // If the object is loaded from __sleep(), timestamp should be already set |
319 | if ( !$this->timestamp ) { |
320 | if ( isset( $row->notification_timestamp ) ) { |
321 | $this->timestamp = wfTimestamp( TS_MW, $row->notification_timestamp ); |
322 | } else { |
323 | $this->timestamp = wfTimestampNow(); |
324 | } |
325 | } |
326 | |
327 | $this->variant = $row->event_variant; |
328 | try { |
329 | $this->extra = $row->event_extra ? unserialize( $row->event_extra ) : []; |
330 | } catch ( Exception $e ) { |
331 | // T73489: unserializing can fail for old notifications |
332 | LoggerFactory::getInstance( 'Echo' )->warning( |
333 | 'Failed to unserialize event {id}', |
334 | [ |
335 | 'id' => $row->event_id |
336 | ] |
337 | ); |
338 | return false; |
339 | } |
340 | $this->pageId = $row->event_page_id; |
341 | $this->deleted = $row->event_deleted; |
342 | |
343 | if ( $row->event_agent_id ) { |
344 | $this->agent = User::newFromId( (int)$row->event_agent_id ); |
345 | } elseif ( $row->event_agent_ip ) { |
346 | $this->agent = User::newFromName( (string)$row->event_agent_ip, false ); |
347 | } |
348 | |
349 | // Lazy load the title from getTitle() so that we can do a batch-load |
350 | if ( |
351 | isset( $this->extra['page_title'] ) && isset( $this->extra['page_namespace'] ) |
352 | && !$row->event_page_id |
353 | ) { |
354 | $this->title = Title::makeTitleSafe( |
355 | $this->extra['page_namespace'], |
356 | $this->extra['page_title'] |
357 | ); |
358 | } |
359 | if ( $row->event_page_id ) { |
360 | $titleCache = Services::getInstance()->getTitleLocalCache(); |
361 | $titleCache->add( (int)$row->event_page_id ); |
362 | } |
363 | if ( isset( $this->extra['revid'] ) && $this->extra['revid'] ) { |
364 | $revisionCache = Services::getInstance()->getRevisionLocalCache(); |
365 | $revisionCache->add( $this->extra['revid'] ); |
366 | } |
367 | |
368 | return true; |
369 | } |
370 | |
371 | /** |
372 | * Loads data from the database into this object, given the event ID. |
373 | * @param int $id Event ID |
374 | * @param bool $fromPrimary |
375 | * @return bool Whether it loaded successfully |
376 | */ |
377 | public function loadFromID( $id, $fromPrimary = false ) { |
378 | $eventMapper = new EventMapper(); |
379 | $event = $eventMapper->fetchById( $id, $fromPrimary ); |
380 | if ( !$event ) { |
381 | return false; |
382 | } |
383 | |
384 | // Copy over the attribute |
385 | $this->id = $event->id; |
386 | $this->type = $event->type; |
387 | $this->variant = $event->variant; |
388 | $this->extra = $event->extra; |
389 | $this->pageId = $event->pageId; |
390 | $this->agent = $event->agent; |
391 | $this->title = $event->title; |
392 | $this->deleted = $event->deleted; |
393 | // Don't overwrite timestamp if it exists already |
394 | if ( !$this->timestamp ) { |
395 | $this->timestamp = $event->timestamp; |
396 | } |
397 | |
398 | return true; |
399 | } |
400 | |
401 | /** |
402 | * Creates an Event from a row object |
403 | * |
404 | * @param stdClass $row row object from echo_event |
405 | * @return Event|false |
406 | */ |
407 | public static function newFromRow( $row ) { |
408 | $obj = new Event(); |
409 | return $obj->loadFromRow( $row ) |
410 | ? $obj |
411 | : false; |
412 | } |
413 | |
414 | /** |
415 | * Creates an Event from the database by ID |
416 | * |
417 | * @param int $id Event ID |
418 | * @return Event|false |
419 | */ |
420 | public static function newFromID( $id ) { |
421 | $obj = new Event(); |
422 | return $obj->loadFromID( $id ) |
423 | ? $obj |
424 | : false; |
425 | } |
426 | |
427 | /** |
428 | * Serialize the extra data for event |
429 | * @return string|null |
430 | */ |
431 | public function serializeExtra() { |
432 | if ( is_array( $this->extra ) || is_object( $this->extra ) ) { |
433 | $extra = serialize( $this->extra ); |
434 | } elseif ( $this->extra === null ) { |
435 | $extra = null; |
436 | } else { |
437 | $extra = serialize( [ $this->extra ] ); |
438 | } |
439 | |
440 | return $extra; |
441 | } |
442 | |
443 | /** |
444 | * Determine if the current user is allowed to view a particular |
445 | * field of this revision, if it's marked as deleted. When no |
446 | * revision is attached always returns true. |
447 | * |
448 | * @param int $field One of RevisionRecord::DELETED_TEXT, |
449 | * RevisionRecord::DELETED_COMMENT, |
450 | * RevisionRecord::DELETED_USER |
451 | * @param User $user User object to check |
452 | * @return bool |
453 | */ |
454 | public function userCan( $field, User $user ) { |
455 | $revision = $this->getRevision(); |
456 | // User is handled specially |
457 | if ( $field === RevisionRecord::DELETED_USER ) { |
458 | $agent = $this->getAgent(); |
459 | if ( !$agent ) { |
460 | // No user associated, so they can see it. |
461 | return true; |
462 | } |
463 | |
464 | if ( |
465 | $revision |
466 | && $agent->getName() === $revision->getUser( RevisionRecord::RAW )->getName() |
467 | ) { |
468 | // If the agent and the revision user are the same, use rev_deleted |
469 | return $revision->audienceCan( $field, RevisionRecord::FOR_THIS_USER, $user ); |
470 | } else { |
471 | // Use User::isHidden() |
472 | $permManager = MediaWikiServices::getInstance()->getPermissionManager(); |
473 | return $permManager->userHasAnyRight( $user, 'viewsuppressed', 'hideuser' ) |
474 | || !$agent->isHidden(); |
475 | } |
476 | } elseif ( $revision ) { |
477 | // A revision is set, use rev_deleted |
478 | return $revision->audienceCan( $field, RevisionRecord::FOR_THIS_USER, $user ); |
479 | } else { |
480 | // Not a user, and there is no associated revision, so the user can see it |
481 | return true; |
482 | } |
483 | } |
484 | |
485 | ## Accessors |
486 | |
487 | /** |
488 | * @return int |
489 | */ |
490 | public function getId() { |
491 | return $this->id; |
492 | } |
493 | |
494 | /** |
495 | * @return string |
496 | */ |
497 | public function getTimestamp() { |
498 | return $this->timestamp; |
499 | } |
500 | |
501 | /** |
502 | * @return string |
503 | */ |
504 | public function getType() { |
505 | return $this->type; |
506 | } |
507 | |
508 | /** |
509 | * @return string|null |
510 | */ |
511 | public function getVariant() { |
512 | return $this->variant; |
513 | } |
514 | |
515 | /** |
516 | * @return array |
517 | */ |
518 | public function getExtra() { |
519 | return $this->extra; |
520 | } |
521 | |
522 | /** |
523 | * @param string $key |
524 | * @param mixed|null $default |
525 | * @return mixed|null |
526 | */ |
527 | public function getExtraParam( $key, $default = null ) { |
528 | return $this->extra[$key] ?? $default; |
529 | } |
530 | |
531 | /** |
532 | * @return User|null |
533 | */ |
534 | public function getAgent() { |
535 | return $this->agent; |
536 | } |
537 | |
538 | /** |
539 | * Check whether this event allows its agent to be notified. |
540 | * |
541 | * Notifying the agent is only allowed if the event's type allows it, or if the event extra |
542 | * explicitly specifies 'notifyAgent' => true. |
543 | * |
544 | * @return bool |
545 | */ |
546 | public function canNotifyAgent() { |
547 | global $wgEchoNotifications; |
548 | $allowedInConfig = $wgEchoNotifications[$this->getType()]['canNotifyAgent'] ?? false; |
549 | $allowedInExtra = $this->getExtraParam( 'notifyAgent', false ); |
550 | return $allowedInConfig || $allowedInExtra; |
551 | } |
552 | |
553 | /** |
554 | * @param bool $fromPrimary |
555 | * @return null|Title |
556 | */ |
557 | public function getTitle( $fromPrimary = false ) { |
558 | if ( $this->title ) { |
559 | return $this->title; |
560 | } |
561 | if ( $this->pageId ) { |
562 | $titleCache = Services::getInstance()->getTitleLocalCache(); |
563 | $title = $titleCache->get( $this->pageId ); |
564 | if ( $title ) { |
565 | $this->title = $title; |
566 | return $this->title; |
567 | } |
568 | $this->title = Title::newFromID( $this->pageId, $fromPrimary ? IDBAccessObject::READ_LATEST : 0 ); |
569 | if ( $this->title ) { |
570 | return $this->title; |
571 | } |
572 | } |
573 | if ( isset( $this->extra['page_title'] ) && isset( $this->extra['page_namespace'] ) ) { |
574 | $this->title = Title::makeTitleSafe( |
575 | $this->extra['page_namespace'], |
576 | $this->extra['page_title'] |
577 | ); |
578 | return $this->title; |
579 | } |
580 | return null; |
581 | } |
582 | |
583 | /** |
584 | * @return RevisionRecord|null |
585 | */ |
586 | public function getRevision() { |
587 | if ( $this->revision ) { |
588 | return $this->revision; |
589 | } |
590 | |
591 | if ( isset( $this->extra['revid'] ) ) { |
592 | $revisionCache = Services::getInstance()->getRevisionLocalCache(); |
593 | $revision = $revisionCache->get( $this->extra['revid'] ); |
594 | if ( $revision ) { |
595 | $this->revision = $revision; |
596 | return $this->revision; |
597 | } |
598 | |
599 | $store = MediaWikiServices::getInstance()->getRevisionStore(); |
600 | $this->revision = $store->getRevisionById( $this->extra['revid'] ); |
601 | return $this->revision; |
602 | } |
603 | |
604 | return null; |
605 | } |
606 | |
607 | /** |
608 | * Get the category of the event type |
609 | * @return string |
610 | */ |
611 | public function getCategory() { |
612 | return Services::getInstance()->getAttributeManager()->getNotificationCategory( $this->type ); |
613 | } |
614 | |
615 | /** |
616 | * Get the section of the event type |
617 | * @return string |
618 | */ |
619 | public function getSection() { |
620 | return Services::getInstance()->getAttributeManager()->getNotificationSection( $this->type ); |
621 | } |
622 | |
623 | /** |
624 | * Determine whether an event can use the job queue, or should be immediate |
625 | * @return bool |
626 | */ |
627 | public function getUseJobQueue() { |
628 | global $wgEchoNotifications; |
629 | if ( isset( $wgEchoNotifications[$this->type]['immediate'] ) ) { |
630 | return !(bool)$wgEchoNotifications[$this->type]['immediate']; |
631 | } |
632 | |
633 | return true; |
634 | } |
635 | |
636 | public function setType( $type ) { |
637 | $this->type = $type; |
638 | } |
639 | |
640 | public function setVariant( $variant ) { |
641 | $this->variant = $variant; |
642 | } |
643 | |
644 | public function setAgent( User $agent ) { |
645 | $this->agent = $agent; |
646 | } |
647 | |
648 | public function setTitle( Title $title ) { |
649 | $this->title = $title; |
650 | $pageId = $title->getArticleID(); |
651 | if ( $pageId ) { |
652 | $this->pageId = $pageId; |
653 | } else { |
654 | $this->extra['page_title'] = $title->getDBkey(); |
655 | $this->extra['page_namespace'] = $title->getNamespace(); |
656 | } |
657 | } |
658 | |
659 | public function setExtra( $name, $value ) { |
660 | $this->extra[$name] = $value; |
661 | } |
662 | |
663 | /** |
664 | * Get the message key of the primary or secondary link for a notification type. |
665 | * |
666 | * @param string $rank 'primary' or 'secondary' |
667 | * @return string i18n message key |
668 | */ |
669 | public function getLinkMessage( $rank ) { |
670 | global $wgEchoNotifications; |
671 | $type = $this->getType(); |
672 | return $wgEchoNotifications[$type][$rank . '-link']['message'] ?? ''; |
673 | } |
674 | |
675 | /** |
676 | * Get the link destination of the primary or secondary link for a notification type. |
677 | * |
678 | * @param string $rank 'primary' or 'secondary' |
679 | * @return string The link destination, e.g. 'agent' |
680 | */ |
681 | public function getLinkDestination( $rank ) { |
682 | global $wgEchoNotifications; |
683 | $type = $this->getType(); |
684 | return $wgEchoNotifications[$type][$rank . '-link']['destination'] ?? ''; |
685 | } |
686 | |
687 | /** |
688 | * @return string|null |
689 | */ |
690 | public function getBundleHash() { |
691 | return $this->bundleHash; |
692 | } |
693 | |
694 | /** |
695 | * @param string|null $hash |
696 | */ |
697 | public function setBundleHash( $hash ) { |
698 | $this->bundleHash = $hash; |
699 | } |
700 | |
701 | /** |
702 | * @return bool |
703 | */ |
704 | public function isDeleted() { |
705 | return $this->deleted === 1; |
706 | } |
707 | |
708 | public function setBundledEvents( array $events ) { |
709 | $this->bundledEvents = $events; |
710 | } |
711 | |
712 | public function getBundledEvents() { |
713 | return $this->bundledEvents; |
714 | } |
715 | |
716 | /** |
717 | * @inheritDoc |
718 | */ |
719 | public function canBeBundled() { |
720 | return true; |
721 | } |
722 | |
723 | /** |
724 | * @inheritDoc |
725 | */ |
726 | public function getBundlingKey() { |
727 | return $this->getBundleHash(); |
728 | } |
729 | |
730 | /** |
731 | * @inheritDoc |
732 | */ |
733 | public function setBundledElements( array $bundleables ) { |
734 | $this->setBundledEvents( $bundleables ); |
735 | } |
736 | |
737 | /** |
738 | * @inheritDoc |
739 | */ |
740 | public function getSortingKey() { |
741 | return $this->getTimestamp(); |
742 | } |
743 | |
744 | /** |
745 | * Return the list of fields that should be selected to create |
746 | * a new event with Event::newFromRow |
747 | * @return string[] |
748 | */ |
749 | public static function selectFields() { |
750 | return [ |
751 | 'event_id', |
752 | 'event_type', |
753 | 'event_variant', |
754 | 'event_agent_id', |
755 | 'event_agent_ip', |
756 | 'event_extra', |
757 | 'event_page_id', |
758 | 'event_deleted', |
759 | ]; |
760 | } |
761 | |
762 | } |
763 | |
764 | class_alias( Event::class, 'EchoEvent' ); |