Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
48.07% |
112 / 233 |
|
18.18% |
2 / 11 |
CRAP | |
0.00% |
0 / 1 |
EventDispatcher | |
48.07% |
112 / 233 |
|
18.18% |
2 / 11 |
777.00 | |
0.00% |
0 / 1 |
getParsedRevision | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
generateEventsForRevision | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
groupCommentsByThreadAndName | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
9 | |||
groupSubscribableHeadings | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
findAddedItems | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
8.23 | |||
generateEventsFromItemSets | |
84.54% |
82 / 97 |
|
0.00% |
0 / 1 |
21.48 | |||
addCommentChangeTag | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
addAutoSubscription | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
locateSubscribedUsers | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
logAddedComments | |
7.41% |
4 / 54 |
|
0.00% |
0 / 1 |
246.42 | |||
inEventSample | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * DiscussionTools event dispatcher |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @license MIT |
8 | */ |
9 | |
10 | namespace MediaWiki\Extension\DiscussionTools\Notifications; |
11 | |
12 | use DateInterval; |
13 | use DateTimeImmutable; |
14 | use ExtensionRegistry; |
15 | use IDBAccessObject; |
16 | use Iterator; |
17 | use MediaWiki\Deferred\DeferredUpdates; |
18 | use MediaWiki\Extension\DiscussionTools\CommentUtils; |
19 | use MediaWiki\Extension\DiscussionTools\ContentThreadItemSet; |
20 | use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils; |
21 | use MediaWiki\Extension\DiscussionTools\SubscriptionItem; |
22 | use MediaWiki\Extension\DiscussionTools\SubscriptionStore; |
23 | use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem; |
24 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem; |
25 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem; |
26 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem; |
27 | use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem; |
28 | use MediaWiki\Extension\EventLogging\EventLogging; |
29 | use MediaWiki\Extension\Notifications\Model\Event; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Page\PageIdentity; |
32 | use MediaWiki\Revision\RevisionRecord; |
33 | use MediaWiki\Title\Title; |
34 | use MediaWiki\User\UserIdentity; |
35 | use RequestContext; |
36 | use Wikimedia\Assert\Assert; |
37 | use Wikimedia\Parsoid\Core\ResourceLimitExceededException; |
38 | use Wikimedia\Parsoid\Utils\DOMCompat; |
39 | use Wikimedia\Parsoid\Utils\DOMUtils; |
40 | |
41 | class EventDispatcher { |
42 | /** |
43 | * @throws ResourceLimitExceededException |
44 | */ |
45 | private static function getParsedRevision( RevisionRecord $revRecord ): ContentThreadItemSet { |
46 | return HookUtils::parseRevisionParsoidHtml( $revRecord, __METHOD__ ); |
47 | } |
48 | |
49 | /** |
50 | * @throws ResourceLimitExceededException |
51 | */ |
52 | public static function generateEventsForRevision( array &$events, RevisionRecord $newRevRecord ): void { |
53 | $services = MediaWikiServices::getInstance(); |
54 | |
55 | $revisionStore = $services->getRevisionStore(); |
56 | $oldRevRecord = $revisionStore->getPreviousRevision( $newRevRecord, IDBAccessObject::READ_LATEST ); |
57 | |
58 | $title = Title::newFromLinkTarget( |
59 | $newRevRecord->getPageAsLinkTarget() |
60 | ); |
61 | if ( !HookUtils::isAvailableForTitle( $title ) ) { |
62 | // Not a talk page |
63 | return; |
64 | } |
65 | |
66 | $user = $newRevRecord->getUser(); |
67 | if ( !$user ) { |
68 | // User can be null if the user is deleted, but this is unlikely |
69 | // to be the case if the user just made an edit |
70 | return; |
71 | } |
72 | |
73 | if ( $oldRevRecord !== null ) { |
74 | $oldItemSet = static::getParsedRevision( $oldRevRecord ); |
75 | } else { |
76 | // Page creation |
77 | $doc = DOMUtils::parseHTML( '' ); |
78 | $container = DOMCompat::getBody( $doc ); |
79 | $oldItemSet = $services->getService( 'DiscussionTools.CommentParser' ) |
80 | ->parse( $container, $title->getTitleValue() ); |
81 | } |
82 | $newItemSet = static::getParsedRevision( $newRevRecord ); |
83 | |
84 | static::generateEventsFromItemSets( $events, $oldItemSet, $newItemSet, $newRevRecord, $title, $user ); |
85 | } |
86 | |
87 | /** |
88 | * For each level 2 heading, get a list of comments in the thread grouped by names, then IDs. |
89 | * (Compare by name first, as ID could be changed by a parent comment being moved/deleted.) |
90 | * Comments in level 3+ sub-threads are grouped together with the parent thread. |
91 | * |
92 | * For any other headings (including level 3+ before the first level 2 heading, level 1, and |
93 | * section zero placeholder headings), ignore comments in those threads. |
94 | * |
95 | * @param ContentThreadItem[] $items |
96 | * @return ContentCommentItem[][][] |
97 | */ |
98 | private static function groupCommentsByThreadAndName( array $items ): array { |
99 | $comments = []; |
100 | $threadName = null; |
101 | foreach ( $items as $item ) { |
102 | if ( $item instanceof HeadingItem && ( $item->getHeadingLevel() < 2 || $item->isPlaceholderHeading() ) ) { |
103 | $threadName = null; |
104 | } elseif ( $item instanceof HeadingItem && $item->getHeadingLevel() === 2 ) { |
105 | $threadName = $item->getName(); |
106 | } elseif ( $item instanceof CommentItem && $threadName !== null ) { |
107 | $comments[ $threadName ][ $item->getName() ][ $item->getId() ] = $item; |
108 | } |
109 | } |
110 | return $comments; |
111 | } |
112 | |
113 | /** |
114 | * Get a list of all subscribable headings, grouped by name in case there are duplicates. |
115 | * |
116 | * @param ContentHeadingItem[] $items |
117 | * @return ContentHeadingItem[][] |
118 | */ |
119 | private static function groupSubscribableHeadings( array $items ): array { |
120 | $headings = []; |
121 | foreach ( $items as $item ) { |
122 | if ( $item->isSubscribable() ) { |
123 | $headings[ $item->getName() ][ $item->getId() ] = $item; |
124 | } |
125 | } |
126 | return $headings; |
127 | } |
128 | |
129 | /** |
130 | * Compare two lists of thread items, return those in $new but not $old. |
131 | * |
132 | * @param ContentThreadItem[][] $old |
133 | * @param ContentThreadItem[][] $new |
134 | * @return iterable<ContentThreadItem> |
135 | */ |
136 | private static function findAddedItems( array $old, array $new ) { |
137 | foreach ( $new as $itemName => $nameNewItems ) { |
138 | // Usually, there will be 0 or 1 $nameNewItems, and 0 $nameOldItems, |
139 | // and $addedCount will be 0 or 1. |
140 | // |
141 | // But when multiple replies are added in one edit, or in multiple edits within the same |
142 | // minute, there may be more, and the complex logic below tries to make the best guess |
143 | // as to which items are actually new. See the 'multiple' and 'sametime' test cases. |
144 | // |
145 | $nameOldItems = $old[ $itemName ] ?? []; |
146 | $addedCount = count( $nameNewItems ) - count( $nameOldItems ); |
147 | |
148 | if ( $addedCount > 0 ) { |
149 | // For any name that occurs more times in new than old, report that many new items, |
150 | // preferring IDs that did not occur in old, then preferring items lower on the page. |
151 | foreach ( array_reverse( $nameNewItems ) as $itemId => $newItem ) { |
152 | if ( $addedCount > 0 && !isset( $nameOldItems[ $itemId ] ) ) { |
153 | yield $newItem; |
154 | $addedCount--; |
155 | } |
156 | } |
157 | foreach ( array_reverse( $nameNewItems ) as $itemId => $newItem ) { |
158 | if ( $addedCount > 0 ) { |
159 | yield $newItem; |
160 | $addedCount--; |
161 | } |
162 | } |
163 | Assert::postcondition( $addedCount === 0, 'Reported expected number of items' ); |
164 | } |
165 | } |
166 | } |
167 | |
168 | /** |
169 | * Helper for generateEventsForRevision(), separated out for easier testing. |
170 | */ |
171 | protected static function generateEventsFromItemSets( |
172 | array &$events, |
173 | ContentThreadItemSet $oldItemSet, |
174 | ContentThreadItemSet $newItemSet, |
175 | RevisionRecord $newRevRecord, |
176 | PageIdentity $title, |
177 | UserIdentity $user |
178 | ): void { |
179 | $newComments = static::groupCommentsByThreadAndName( $newItemSet->getThreadItems() ); |
180 | $oldComments = static::groupCommentsByThreadAndName( $oldItemSet->getThreadItems() ); |
181 | $addedComments = []; |
182 | foreach ( $newComments as $threadName => $threadNewComments ) { |
183 | $threadOldComments = $oldComments[ $threadName ] ?? []; |
184 | foreach ( static::findAddedItems( $threadOldComments, $threadNewComments ) as $newComment ) { |
185 | Assert::precondition( $newComment instanceof ContentCommentItem, 'Must be ContentCommentItem' ); |
186 | $addedComments[] = $newComment; |
187 | } |
188 | } |
189 | |
190 | $newHeadings = static::groupSubscribableHeadings( $newItemSet->getThreads() ); |
191 | $oldHeadings = static::groupSubscribableHeadings( $oldItemSet->getThreads() ); |
192 | |
193 | $addedHeadings = []; |
194 | foreach ( static::findAddedItems( $oldHeadings, $newHeadings ) as $newHeading ) { |
195 | Assert::precondition( $newHeading instanceof ContentHeadingItem, 'Must be ContentHeadingItem' ); |
196 | $addedHeadings[] = $newHeading; |
197 | } |
198 | |
199 | $removedHeadings = []; |
200 | // Pass swapped parameters to findAddedItems() to find *removed* items |
201 | foreach ( static::findAddedItems( $newHeadings, $oldHeadings ) as $oldHeading ) { |
202 | Assert::precondition( $oldHeading instanceof ContentHeadingItem, 'Must be ContentHeadingItem' ); |
203 | $removedHeadings[] = $oldHeading; |
204 | } |
205 | |
206 | $mentionedUsers = []; |
207 | foreach ( $events as &$event ) { |
208 | if ( $event['type'] === 'mention' || $event['type'] === 'mention-summary' ) { |
209 | // Save mentioned users in our events, so that we can exclude them from our notification, |
210 | // to avoid duplicate notifications for a single comment. |
211 | // Array is keyed by user id so we can do a simple array merge. |
212 | $mentionedUsers += $event['extra']['mentioned-users']; |
213 | } |
214 | |
215 | if ( count( $addedComments ) === 1 ) { |
216 | // If this edit was a new user talk message according to Echo, |
217 | // and we also found exactly one new comment, |
218 | // add some extra information to the edit-user-talk event. |
219 | if ( $event['type'] === 'edit-user-talk' ) { |
220 | $event['extra'] += [ |
221 | 'comment-id' => $addedComments[0]->getId(), |
222 | 'comment-name' => $addedComments[0]->getName(), |
223 | 'content' => $addedComments[0]->getBodyText( true ), |
224 | ]; |
225 | } |
226 | |
227 | // Similarly for mentions. |
228 | // We don't handle 'content' in this case, as Echo makes its own snippets. |
229 | if ( $event['type'] === 'mention' ) { |
230 | $event['extra'] += [ |
231 | 'comment-id' => $addedComments[0]->getId(), |
232 | 'comment-name' => $addedComments[0]->getName(), |
233 | ]; |
234 | } |
235 | } |
236 | } |
237 | |
238 | if ( $addedComments ) { |
239 | // It's a bit weird to do this here, in the middle of the hook handler for Echo. However: |
240 | // * Echo calls this from a PageSaveComplete hook handler as a DeferredUpdate, |
241 | // which is exactly how we would do this otherwise |
242 | // * It allows us to reuse the generated comment trees without any annoying caching |
243 | static::addCommentChangeTag( $newRevRecord ); |
244 | // For very similar reasons, we do logging here |
245 | static::logAddedComments( $addedComments, $newRevRecord, $title, $user ); |
246 | } |
247 | |
248 | foreach ( $addedComments as $newComment ) { |
249 | // Ignore comments by other users, e.g. in case of reverts or a discussion being moved. |
250 | // TODO: But what about someone signing another's comment? |
251 | if ( $newComment->getAuthor() !== $user->getName() ) { |
252 | continue; |
253 | } |
254 | // Ignore comments which are more than 10 minutes old, as this may be a user archiving |
255 | // their own comment. (T290803) |
256 | $revTimestamp = new DateTimeImmutable( $newRevRecord->getTimestamp() ); |
257 | $threshold = $revTimestamp->sub( new DateInterval( 'PT10M' ) ); |
258 | if ( $newComment->getTimestamp() <= $threshold ) { |
259 | continue; |
260 | } |
261 | $heading = $newComment->getSubscribableHeading(); |
262 | if ( !$heading ) { |
263 | continue; |
264 | } |
265 | $events[] = [ |
266 | // This probably should've been called "dt-new-comment": this code is |
267 | // unaware if there are any subscriptions to the containing topic and |
268 | // an event is generated for every comment posted. |
269 | // However, changing this would require a complex migration. |
270 | 'type' => 'dt-subscribed-new-comment', |
271 | 'title' => $title, |
272 | 'extra' => [ |
273 | 'subscribed-comment-name' => $heading->getName(), |
274 | 'comment-id' => $newComment->getId(), |
275 | 'comment-name' => $newComment->getName(), |
276 | 'content' => $newComment->getBodyText( true ), |
277 | 'section-title' => $heading->getLinkableTitle(), |
278 | 'revid' => $newRevRecord->getId(), |
279 | 'mentioned-users' => $mentionedUsers, |
280 | ], |
281 | 'agent' => $user, |
282 | ]; |
283 | |
284 | $titleForSubscriptions = Title::castFromPageIdentity( $title )->createFragmentTarget( $heading->getText() ); |
285 | static::addAutoSubscription( $user, $titleForSubscriptions, $heading->getName() ); |
286 | } |
287 | |
288 | foreach ( $removedHeadings as $oldHeading ) { |
289 | $events[] = [ |
290 | 'type' => 'dt-removed-topic', |
291 | 'title' => $title, |
292 | 'extra' => [ |
293 | 'subscribed-comment-name' => $oldHeading->getName(), |
294 | 'heading-id' => $oldHeading->getId(), |
295 | 'heading-name' => $oldHeading->getName(), |
296 | 'section-title' => $oldHeading->getLinkableTitle(), |
297 | 'revid' => $newRevRecord->getId(), |
298 | ], |
299 | 'agent' => $user, |
300 | ]; |
301 | } |
302 | |
303 | $titleObj = Title::castFromPageIdentity( $title ); |
304 | if ( $titleObj ) { |
305 | foreach ( $addedHeadings as $newHeading ) { |
306 | // Don't use $event here as that already exists as a reference from above |
307 | $addTopicEvent = [ |
308 | 'type' => 'dt-added-topic', |
309 | 'title' => $title, |
310 | 'extra' => [ |
311 | // As no one can be subscribed to a topic before it has been created, |
312 | // we will notify users who have subscribed to the whole page. |
313 | 'subscribed-comment-name' => CommentUtils::getNewTopicsSubscriptionId( $titleObj ), |
314 | 'heading-id' => $newHeading->getId(), |
315 | 'heading-name' => $newHeading->getName(), |
316 | 'section-title' => $newHeading->getLinkableTitle(), |
317 | 'revid' => $newRevRecord->getId(), |
318 | ], |
319 | 'agent' => $user, |
320 | ]; |
321 | // Add metadata about the accompanying comment |
322 | $firstComment = $newHeading->getOldestReply(); |
323 | if ( $firstComment ) { |
324 | $addTopicEvent['extra']['comment-id'] = $firstComment->getId(); |
325 | $addTopicEvent['extra']['comment-name'] = $firstComment->getName(); |
326 | $addTopicEvent['extra']['content'] = $firstComment->getBodyText( true ); |
327 | } |
328 | $events[] = $addTopicEvent; |
329 | } |
330 | } |
331 | } |
332 | |
333 | /** |
334 | * Add our change tag for a revision that adds new comments. |
335 | */ |
336 | protected static function addCommentChangeTag( RevisionRecord $newRevRecord ): void { |
337 | // Unclear if DeferredUpdates::addCallableUpdate() is needed, |
338 | // but every extension does it that way. |
339 | DeferredUpdates::addCallableUpdate( static function () use ( $newRevRecord ) { |
340 | MediaWikiServices::getInstance()->getChangeTagsStore() |
341 | ->addTags( [ 'discussiontools-added-comment' ], null, $newRevRecord->getId() ); |
342 | } ); |
343 | } |
344 | |
345 | /** |
346 | * Add an automatic subscription to the given item, assuming the user has automatic subscriptions |
347 | * enabled. |
348 | */ |
349 | protected static function addAutoSubscription( UserIdentity $user, Title $title, string $itemName ): void { |
350 | $dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' ); |
351 | |
352 | if ( |
353 | $dtConfig->get( 'DiscussionToolsAutoTopicSubEditor' ) === 'any' && |
354 | HookUtils::shouldAddAutoSubscription( $user, $title ) |
355 | ) { |
356 | /** @var SubscriptionStore $parser */ |
357 | $subscriptionStore = MediaWikiServices::getInstance()->getService( 'DiscussionTools.SubscriptionStore' ); |
358 | $subscriptionStore->addAutoSubscriptionForUser( $user, $title, $itemName ); |
359 | } |
360 | } |
361 | |
362 | /** |
363 | * Return all users subscribed to a comment |
364 | * |
365 | * @param Event $event |
366 | * @param int $batchSize |
367 | * @return UserIdentity[]|Iterator<UserIdentity> |
368 | */ |
369 | public static function locateSubscribedUsers( Event $event, $batchSize = 500 ) { |
370 | $commentName = $event->getExtraParam( 'subscribed-comment-name' ); |
371 | |
372 | /** @var SubscriptionStore $subscriptionStore */ |
373 | $subscriptionStore = MediaWikiServices::getInstance()->getService( 'DiscussionTools.SubscriptionStore' ); |
374 | $subscriptionItems = $subscriptionStore->getSubscriptionItemsForTopic( |
375 | $commentName, |
376 | [ SubscriptionStore::STATE_SUBSCRIBED, SubscriptionStore::STATE_AUTOSUBSCRIBED ] |
377 | ); |
378 | |
379 | // Update notified timestamps |
380 | $subscriptionStore->updateSubscriptionNotifiedTimestamp( |
381 | null, |
382 | $commentName |
383 | ); |
384 | |
385 | // TODD: Have this return an Iterator instead? |
386 | $users = array_map( static function ( SubscriptionItem $item ) { |
387 | return $item->getUserIdentity(); |
388 | }, $subscriptionItems ); |
389 | |
390 | return $users; |
391 | } |
392 | |
393 | /** |
394 | * Log stuff to EventLogging's Schema:TalkPageEvent |
395 | * If you don't have EventLogging installed, does nothing. |
396 | * |
397 | * @param array $addedComments |
398 | * @param RevisionRecord $newRevRecord |
399 | * @param PageIdentity $title |
400 | * @param UserIdentity $identity |
401 | * @return bool Whether events were logged |
402 | */ |
403 | protected static function logAddedComments( |
404 | array $addedComments, |
405 | RevisionRecord $newRevRecord, |
406 | PageIdentity $title, |
407 | UserIdentity $identity |
408 | ): bool { |
409 | global $wgDTSchemaEditAttemptStepOversample, $wgDBname; |
410 | $context = RequestContext::getMain(); |
411 | $request = $context->getRequest(); |
412 | // We've reached here through Echo's post-save deferredupdate, which |
413 | // might either be after an API request from DiscussionTools or a |
414 | // regular POST from WikiEditor. Both should have this value snuck |
415 | // into their request if their session is being logged. |
416 | if ( !$request->getCheck( 'editingStatsId' ) ) { |
417 | return false; |
418 | } |
419 | $editingStatsId = $request->getVal( 'editingStatsId' ); |
420 | $isDiscussionTools = $request->getCheck( 'dttags' ); |
421 | |
422 | $extensionRegistry = ExtensionRegistry::getInstance(); |
423 | if ( !$extensionRegistry->isLoaded( 'EventLogging' ) ) { |
424 | return false; |
425 | } |
426 | if ( !$extensionRegistry->isLoaded( 'WikimediaEvents' ) ) { |
427 | return false; |
428 | } |
429 | $inSample = static::inEventSample( $editingStatsId ); |
430 | $shouldOversample = ( $isDiscussionTools && $wgDTSchemaEditAttemptStepOversample ) || ( |
431 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
432 | \WikimediaEvents\WikimediaEventsHooks::shouldSchemaEditAttemptStepOversample( $context ) |
433 | ); |
434 | if ( !$inSample && !$shouldOversample ) { |
435 | return false; |
436 | } |
437 | |
438 | $services = MediaWikiServices::getInstance(); |
439 | $editTracker = $services->getUserEditTracker(); |
440 | $userIdentityUtils = $services->getUserIdentityUtils(); |
441 | |
442 | $commonData = [ |
443 | '$schema' => '/analytics/mediawiki/talk_page_edit/1.2.0', |
444 | 'action' => 'publish', |
445 | 'session_id' => $editingStatsId, |
446 | 'page_id' => $newRevRecord->getPageId(), |
447 | 'page_namespace' => $title->getNamespace(), |
448 | 'revision_id' => $newRevRecord->getId() ?: 0, |
449 | 'performer' => [ |
450 | // Note: we're logging the user who made the edit, not the user who's signed on the comment |
451 | 'user_id' => $identity->getId(), |
452 | 'user_edit_count' => $editTracker->getUserEditCount( $identity ) ?: 0, |
453 | // Retention-safe values: |
454 | 'user_is_anonymous' => !$identity->isRegistered(), |
455 | 'user_is_temp' => $userIdentityUtils->isTemp( $identity ), |
456 | 'user_edit_count_bucket' => \UserBucketProvider::getUserEditCountBucket( $identity ) ?: 'N/A', |
457 | ], |
458 | 'database' => $wgDBname, |
459 | // This is unreliable, but sufficient for our purposes; we |
460 | // mostly just want to see the difference between DT and |
461 | // everything-else: |
462 | 'integration' => $isDiscussionTools ? 'discussiontools' : 'page', |
463 | ]; |
464 | |
465 | foreach ( $addedComments as $comment ) { |
466 | $heading = $comment->getSubscribableHeading(); |
467 | $parent = $comment->getParent(); |
468 | if ( !$heading || !$parent ) { |
469 | continue; |
470 | } |
471 | if ( $parent->getType() === 'heading' ) { |
472 | if ( count( $heading->getReplies() ) === 1 ) { |
473 | // A new heading was added when this comment was created |
474 | $component_type = 'topic'; |
475 | } else { |
476 | $component_type = 'comment'; |
477 | } |
478 | } else { |
479 | $component_type = 'response'; |
480 | } |
481 | EventLogging::submit( 'mediawiki.talk_page_edit', array_merge( $commonData, [ |
482 | 'component_type' => $component_type, |
483 | 'topic_id' => $heading->getId(), |
484 | 'comment_id' => $comment->getId(), |
485 | 'comment_parent_id' => $parent->getId(), |
486 | ] ) ); |
487 | } |
488 | |
489 | return true; |
490 | } |
491 | |
492 | /** |
493 | * Should the current session be sampled for EventLogging? |
494 | * |
495 | * @param string $sessionId |
496 | * @return bool Whether to sample the session |
497 | */ |
498 | protected static function inEventSample( string $sessionId ): bool { |
499 | global $wgDTSchemaEditAttemptStepSamplingRate, $wgWMESchemaEditAttemptStepSamplingRate; |
500 | // Sample 6.25% |
501 | $samplingRate = 0.0625; |
502 | if ( isset( $wgDTSchemaEditAttemptStepSamplingRate ) ) { |
503 | $samplingRate = $wgDTSchemaEditAttemptStepSamplingRate; |
504 | } |
505 | if ( isset( $wgWMESchemaEditAttemptStepSamplingRate ) ) { |
506 | $samplingRate = $wgWMESchemaEditAttemptStepSamplingRate; |
507 | } |
508 | if ( $samplingRate === 0 ) { |
509 | return false; |
510 | } |
511 | $inSample = EventLogging::sessionInSample( |
512 | (int)( 1 / $samplingRate ), $sessionId |
513 | ); |
514 | return $inSample; |
515 | } |
516 | |
517 | } |