Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
EditRevUpdater
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
6 / 6
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLastEditPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearLastEditPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLogIdsForTarget
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 updateRev
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
9
 getCacheKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter;
4
5use InvalidArgumentException;
6use MediaWiki\Linker\LinkTarget;
7use MediaWiki\Page\WikiPage;
8use MediaWiki\Revision\RevisionLookup;
9use MediaWiki\Revision\RevisionRecord;
10use Wikimedia\Rdbms\LBFactory;
11
12/**
13 * This service allows "linking" the edit filter hook and the page save hook
14 */
15class EditRevUpdater {
16    public const SERVICE_NAME = 'AbuseFilterEditRevUpdater';
17
18    /** @var WikiPage|null */
19    private $wikiPage;
20    /**
21     * @var array<string,array{local:int[],global:int[]}> IDs of logged filters
22     * like [ page title => [ 'local' => [ids], 'global' => [ids] ] ].
23     */
24    private $logIds = [];
25
26    public function __construct(
27        private readonly CentralDBManager $centralDBManager,
28        private readonly RevisionLookup $revisionLookup,
29        private readonly LBFactory $lbFactory,
30        private readonly string $wikiID
31    ) {
32    }
33
34    /**
35     * Set the WikiPage object used for the ongoing edit
36     */
37    public function setLastEditPage( WikiPage $page ): void {
38        $this->wikiPage = $page;
39    }
40
41    /**
42     * Clear the WikiPage object used for the ongoing edit
43     */
44    public function clearLastEditPage(): void {
45        $this->wikiPage = null;
46    }
47
48    /**
49     * @param LinkTarget $target
50     * @param array{local:int[],global:int[]} $logIds
51     */
52    public function setLogIdsForTarget( LinkTarget $target, array $logIds ): void {
53        if ( count( $logIds ) !== 2 || array_diff( array_keys( $logIds ), [ 'local', 'global' ] ) ) {
54            throw new InvalidArgumentException( 'Wrong keys; got: ' . implode( ', ', array_keys( $logIds ) ) );
55        }
56        $key = $this->getCacheKey( $target );
57        $this->logIds[$key] = $logIds;
58    }
59
60    /**
61     * @param WikiPage $wikiPage
62     * @param RevisionRecord $revisionRecord
63     * @return bool Whether the DB was updated
64     */
65    public function updateRev( WikiPage $wikiPage, RevisionRecord $revisionRecord ): bool {
66        $key = $this->getCacheKey( $wikiPage->getTitle() );
67        if (
68            !isset( $this->logIds[ $key ] ) ||
69            $this->wikiPage === null ||
70            !$this->wikiPage->isSamePageAs( $wikiPage )
71        ) {
72            // This isn't the edit $this->logIds was set for
73            $this->logIds = [];
74            return false;
75        }
76
77        // Ignore null edit.
78        $parentRevId = $revisionRecord->getParentId();
79        if ( $parentRevId !== null ) {
80            $parentRev = $this->revisionLookup->getRevisionById( $parentRevId );
81            if ( $parentRev && $revisionRecord->hasSameContent( $parentRev ) ) {
82                $this->logIds = [];
83                return false;
84            }
85        }
86
87        $this->clearLastEditPage();
88
89        $ret = false;
90        $logs = $this->logIds[ $key ];
91        if ( $logs[ 'local' ] ) {
92            $dbw = $this->lbFactory->getPrimaryDatabase();
93            $dbw->newUpdateQueryBuilder()
94                ->update( 'abuse_filter_log' )
95                ->set( [ 'afl_rev_id' => $revisionRecord->getId() ] )
96                ->where( [ 'afl_id' => $logs['local'] ] )
97                ->caller( __METHOD__ )
98                ->execute();
99            $ret = true;
100        }
101
102        if ( $logs[ 'global' ] ) {
103            $fdb = $this->centralDBManager->getConnection( DB_PRIMARY );
104            $fdb->newUpdateQueryBuilder()
105                ->update( 'abuse_filter_log' )
106                ->set( [ 'afl_rev_id' => $revisionRecord->getId() ] )
107                ->where( [ 'afl_id' => $logs['global'], 'afl_wiki' => $this->wikiID ] )
108                ->caller( __METHOD__ )
109                ->execute();
110            $ret = true;
111        }
112        return $ret;
113    }
114
115    private function getCacheKey( LinkTarget $target ): string {
116        return $target->getNamespace() . '|' . $target->getText();
117    }
118}