Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.00% covered (warning)
82.00%
41 / 50
75.00% covered (warning)
75.00%
9 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
CognatePageHookHandler
82.00% covered (warning)
82.00%
41 / 50
75.00% covered (warning)
75.00%
9 / 12
26.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
1.02
 overrideRevisionNewFromId
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 overridePreviousRevision
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 onPageContentSaveComplete
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 onWikiPageDeletionUpdates
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 newDeferrableDelete
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 onArticleUndelete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 newRevisionRecordFromId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onTitleMoveComplete
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isActionableTarget
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getRepo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onContentChange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Cognate\HookHandler;
4
5use BadMethodCallException;
6use Cognate\CognateRepo;
7use Cognate\CognateServices;
8use MediaWiki\Deferred\DeferrableUpdate;
9use MediaWiki\Deferred\MWCallableUpdate;
10use MediaWiki\Linker\LinkTarget;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Revision\RevisionRecord;
13use MediaWiki\Title\Title;
14
15/**
16 * @license GPL-2.0-or-later
17 * @author Addshore
18 */
19class CognatePageHookHandler {
20
21    /**
22     * @var string
23     */
24    private $dbName;
25
26    /**
27     * @var int[]
28     */
29    private $namespaces;
30
31    /**
32     * @var callable
33     */
34    private $newRevisionFromIdCallable;
35
36    /**
37     * @var callable
38     */
39    private $previousRevisionCallable;
40
41    /**
42     * @param int[] $namespaces array of namespace ids the hooks should operate on
43     * @param string $dbName The dbName of the current site
44     */
45    public function __construct( array $namespaces, $dbName ) {
46        $this->namespaces = $namespaces;
47        $this->dbName = $dbName;
48        $this->newRevisionFromIdCallable = static function ( $id ) {
49            return MediaWikiServices::getInstance()
50                ->getRevisionLookup()
51                ->getRevisionById( $id );
52        };
53        $this->previousRevisionCallable = static function ( $revRecord ) {
54            return MediaWikiServices::getInstance()
55                ->getRevisionLookup()
56                ->getPreviousRevision( $revRecord );
57        };
58    }
59
60    /**
61     * Overrides the use of RevisionLookup::getRevisionById in this class
62     * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
63     *
64     * @param callable $callback
65     */
66    public function overrideRevisionNewFromId( callable $callback ) {
67        if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
68            throw new BadMethodCallException(
69                'Cannot override RevisionLookup::getRevisionById callback in operation.'
70            );
71        }
72        $this->newRevisionFromIdCallable = $callback;
73    }
74
75    /**
76     * Overrides the use of RevisionLookup::getPreviousRevision in this class
77     * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
78     *
79     * @param callable $callback
80     */
81    public function overridePreviousRevision( callable $callback ) {
82        if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
83            throw new BadMethodCallException(
84                'Cannot override RevisionLookup::getPreviousRevision callback in operation.'
85            );
86        }
87        $this->previousRevisionCallable = $callback;
88    }
89
90    /**
91     * Occurs after the save page request has been processed.
92     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageContentSaveComplete
93     *
94     * @note for mediawiki 1.35+, this is run for the PageSaveComplete hook instead of the
95     * PageContentSaveComplete hook
96     *
97     * @param LinkTarget $title
98     * @param RevisionRecord|null $revisionRecord
99     */
100    public function onPageContentSaveComplete(
101        LinkTarget $title,
102        ?RevisionRecord $revisionRecord = null
103    ) {
104        // A null revision means a null edit / no-op edit was made, no need to process that.
105        if ( $revisionRecord === null ) {
106            return;
107        }
108
109        if ( !$this->isActionableTarget( $title ) ) {
110            return;
111        }
112
113        $this->onContentChange( $title );
114    }
115
116    /**
117     * Manipulate the list of DataUpdates to be applied when a page is deleted
118     * @see https://www.mediawiki.org/wiki/Manual:Hooks/WikiPageDeletionUpdates
119     *
120     * @param LinkTarget $title
121     * @param DeferrableUpdate[] &$updates
122     */
123    public function onWikiPageDeletionUpdates(
124        LinkTarget $title,
125        array &$updates
126    ) {
127        if ( $this->isActionableTarget( $title ) ) {
128            $updates[] = $this->newDeferrableDelete( $title, $this->dbName );
129        }
130    }
131
132    /**
133     * @param LinkTarget $linkTarget
134     * @param string $dbName
135     *
136     * @return MWCallableUpdate
137     */
138    private function newDeferrableDelete( LinkTarget $linkTarget, $dbName ) {
139        return new MWCallableUpdate(
140            function () use ( $dbName, $linkTarget ) {
141                $this->getRepo()->deletePage( $dbName, $linkTarget );
142            },
143            __METHOD__
144        );
145    }
146
147    /**
148     * When one or more revisions of an article are restored
149     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleUndelete
150     *
151     * @param Title $title
152     */
153    public function onArticleUndelete(
154        Title $title
155    ) {
156        $revision = $this->newRevisionRecordFromId( $title->getLatestRevID() );
157        if ( !$this->isActionableTarget( $title ) || $revision == null ) {
158            return;
159        }
160
161        $this->onContentChange( $title );
162    }
163
164    /**
165     * @param int $id
166     *
167     * @return null|RevisionRecord
168     */
169    private function newRevisionRecordFromId( $id ) {
170        return call_user_func( $this->newRevisionFromIdCallable, $id );
171    }
172
173    /**
174     * Occurs whenever a request to move an article is completed, after the database transaction
175     * commits.
176     *
177     * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleMoveComplete
178     *
179     * @note for mediawiki 1.35+, this is run for the PageMoveComplete hook instead of the
180     * TitleMoveComplete hook
181     *
182     * @param LinkTarget $title
183     * @param LinkTarget $newTitle
184     */
185    public function onTitleMoveComplete(
186        LinkTarget $title,
187        LinkTarget $newTitle
188    ) {
189        $repo = $this->getRepo();
190        if ( $this->isActionableTarget( $title ) ) {
191            $repo->deletePage( $this->dbName, $title );
192        }
193        if ( $this->isActionableTarget( $newTitle ) ) {
194            $repo->savePage( $this->dbName, $newTitle );
195        }
196    }
197
198    /**
199     * Actionable targets have a namespace id that is:
200     *  - One of the default MediaWiki (between NS_MAIN and NS_CATEGORY_TALK
201     *  - Defined as a namespace to record in the configuration
202     * @param LinkTarget $linkTarget
203     * @return bool
204     */
205    private function isActionableTarget( LinkTarget $linkTarget ) {
206        $namespace = $linkTarget->getNamespace();
207        return in_array( $namespace, $this->namespaces ) &&
208            $namespace >= NS_MAIN && $namespace <= NS_CATEGORY_TALK;
209    }
210
211    /**
212     * @return CognateRepo
213     */
214    private function getRepo() {
215        return CognateServices::getRepo();
216    }
217
218    /**
219     * @param LinkTarget $linkTarget
220     */
221    private function onContentChange( LinkTarget $linkTarget ) {
222        $this->getRepo()->savePage( $this->dbName, $linkTarget );
223    }
224
225}