Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
DedupeHelper
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 2
7.29
0.00% covered (danger)
0.00%
0 / 1
 getDedupeHash
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 hasRecentlyDeliveredDuplicate
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
1<?php
2
3namespace MediaWiki\MassMessage;
4
5use MediaWiki\Json\FormatJson;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Title\Title;
8use Wikimedia\Rdbms\SelectQueryBuilder;
9
10/**
11 * Compute a dedupe hash based on the message subject and contents for
12 * each message delivery job, and store it in ct_params alongside the
13 * massmessage-delivery change tag. When delivering a message, check
14 * whether there is another MassMessage delivery within the past 5 page
15 * revisions with an identical dedupe hash, and skip the delivery if
16 * there is.
17 *
18 * Notes:
19 * - This relies on direct database queries since ct_params is not exposed
20 *      through the API.
21 * - This only works for wikitext talk pages since we don't attach the
22 *   change tag for either Flow or LQT.
23 */
24class DedupeHelper {
25
26    private const RECENT_REVISIONS_LIMIT = 5;
27
28    /**
29     * Get the dedupe hash corresponding to a MassMessageJob
30     *
31     * @param string $subject
32     * @param string $message
33     * @param ?LanguageAwareText $pageSubject
34     * @param ?LanguageAwareText $pageMessage
35     * @return string
36     */
37    public static function getDedupeHash(
38        string $subject,
39        string $message,
40        ?LanguageAwareText $pageSubject,
41        ?LanguageAwareText $pageMessage
42    ): string {
43        $pageSubjectText = $pageSubject !== null ? $pageSubject->getWikitext() : '';
44        $pageMessageText = $pageMessage !== null ? $pageMessage->getWikitext() : '';
45        return md5( $subject . $message . $pageSubjectText . $pageMessageText );
46    }
47
48    /**
49     * For the given title, check if any of the most recent RECENT_REVISIONS_LIMIT revisions is a
50     * MassMessage delivery for the same message.
51     *
52     * @param Title $title
53     * @param string $dedupeHash
54     * @return bool
55     */
56    public static function hasRecentlyDeliveredDuplicate( Title $title, string $dedupeHash ): bool {
57        $services = MediaWikiServices::getInstance();
58
59        $changeTagId = $services->getChangeTagDefStore()->acquireId( 'massmessage-delivery' );
60
61        // Connect to the primary to avoid issues with replication lag.
62        $dbw = $services->getDBLoadBalancerFactory()->getPrimaryDatabase();
63        $res = $dbw->newSelectQueryBuilder()
64            ->select( 'ct_params' )
65            ->from( 'revision' )
66            ->leftJoin( 'change_tag', null, [ 'ct_rev_id = rev_id', 'ct_tag_id' => $changeTagId ] )
67            ->where( [ 'rev_page' => $title->getArticleID() ] )
68            ->orderBy( 'rev_timestamp', SelectQueryBuilder::SORT_DESC )
69            ->limit( self::RECENT_REVISIONS_LIMIT )
70            ->caller( __METHOD__ )
71            ->fetchResultSet();
72
73        foreach ( $res as $row ) {
74            if ( $row->ct_params === null ) {
75                continue;
76            }
77            $params = FormatJson::decode( $row->ct_params, true );
78            if ( $dedupeHash === ( $params['dedupe_hash'] ?? null ) ) {
79                return true;
80            }
81        }
82        return false;
83    }
84}