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