Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
45 / 45 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
SaveHooks | |
100.00% |
45 / 45 |
|
100.00% |
5 / 5 |
12 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
onRecentChange_save | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
onRevisionFromEditComplete | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
onManualLogEntryBeforePublish | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
onArticleRevisionVisibilitySet | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
8 |
1 | <?php |
2 | namespace MediaWiki\Extension\Hashtags; |
3 | |
4 | use IDBAccessObject; |
5 | use MediaWiki\ChangeTags\ChangeTagsStore; |
6 | use MediaWiki\CommentFormatter\CommentParserFactory; |
7 | use MediaWiki\Hook\ArticleRevisionVisibilitySetHook; |
8 | use MediaWiki\Hook\ManualLogEntryBeforePublishHook; |
9 | use MediaWiki\Hook\RecentChange_saveHook; |
10 | use MediaWiki\Page\Hook\RevisionFromEditCompleteHook; |
11 | use MediaWiki\Revision\RevisionLookup; |
12 | use MediaWiki\Revision\RevisionRecord; |
13 | use Wikimedia\Rdbms\IConnectionProvider; |
14 | |
15 | class SaveHooks implements |
16 | RevisionFromEditCompleteHook, |
17 | ManualLogEntryBeforePublishHook, |
18 | ArticleRevisionVisibilitySetHook, |
19 | RecentChange_saveHook |
20 | { |
21 | |
22 | private HashtagCommentParserFactory $cpFactory; |
23 | private ChangeTagsStore $changeTagsStore; |
24 | private IConnectionProvider $dbProvider; |
25 | private RevisionLookup $revisionLookup; |
26 | private TagCollector $tagCollector; |
27 | |
28 | public function __construct( |
29 | CommentParserFactory $commentParserFactory, |
30 | ChangeTagsStore $changeTagsStore, |
31 | IConnectionProvider $dbProvider, |
32 | RevisionLookup $revisionLookup, |
33 | TagCollector $tagCollector |
34 | ) { |
35 | $this->cpFactory = $commentParserFactory; |
36 | $this->changeTagsStore = $changeTagsStore; |
37 | $this->dbProvider = $dbProvider; |
38 | $this->revisionLookup = $revisionLookup; |
39 | $this->tagCollector = $tagCollector; |
40 | } |
41 | |
42 | // Previously we used onRecentChange_save, however onRevisionFromEditComplete |
43 | // combined with onManualLogEntryBeforePublish seems to cover more cases. |
44 | |
45 | /** |
46 | * @inheritDoc |
47 | */ |
48 | public function onRecentChange_save( $rc ) { |
49 | // This is unideal as it misses revisions with RC_SUPPRESS |
50 | // However is needed to work around T379152 |
51 | $comment = $rc->getAttribute( "rc_comment" ); |
52 | $newTags = $this->tagCollector->getTagsSeen( $this->cpFactory, $comment ); |
53 | $rc->addTags( $newTags ); |
54 | } |
55 | |
56 | /** |
57 | * @note In certain cases, this hook ignores the $tags parameter. Most of those |
58 | * cases are covered by onManualLogEntryBeforePublish. |
59 | * @inheritDoc |
60 | */ |
61 | public function onRevisionFromEditComplete( $wikiPage, $rev, $originalRevId, $user, &$tags ) { |
62 | // FIXME, this hasn't been tested with i18n-ized edit summaries, and it is a bit |
63 | // unclear how they work. |
64 | $comment = $rev->getComment()->text; |
65 | $newTags = $this->tagCollector->getTagsSeen( $this->cpFactory, $comment ); |
66 | $tags = array_merge( $tags, $newTags ); |
67 | } |
68 | |
69 | /** |
70 | * @inheritDoc |
71 | * @note This does not cover restricted logs or log entries |
72 | * not published to normal RC feed or irc RC feed |
73 | */ |
74 | public function onManualLogEntryBeforePublish( $logEntry ): void { |
75 | $comment = $logEntry->getComment(); |
76 | $newTags = $this->tagCollector->getTagsSeen( $this->cpFactory, $comment ); |
77 | // This will also add tags to the associated revision, |
78 | // including some cases that onRevisionFromEditComplete |
79 | // does not cover. |
80 | $logEntry->addTags( $newTags ); |
81 | } |
82 | |
83 | // FIXME: We need a solution for log entries being revdeleted/undeleted. |
84 | |
85 | /** |
86 | * @inheritDoc |
87 | */ |
88 | public function onArticleRevisionVisibilitySet( $title, $ids, $visibilityChangeMap ) { |
89 | foreach ( $visibilityChangeMap as $id => $change ) { |
90 | if ( |
91 | ( $change['oldBits'] & RevisionRecord::DELETED_COMMENT ) === 0 && |
92 | ( $change['newBits'] & RevisionRecord::DELETED_COMMENT ) !== 0 |
93 | ) { |
94 | // We are deleting this comment |
95 | $rcId = null; |
96 | $existingTags = $this->changeTagsStore->getTagsWithData( |
97 | $this->dbProvider->getPrimaryDatabase(), |
98 | $rcId, /* rc id */ |
99 | $id /* rev id */ |
100 | ); |
101 | $tagsToRemove = array_filter( |
102 | $existingTags, |
103 | static function ( $key ) { |
104 | return substr( $key, 0, |
105 | strlen( HashtagCommentParser::HASHTAG_PREFIX ) |
106 | ) === HashtagCommentParser::HASHTAG_PREFIX; |
107 | }, |
108 | ARRAY_FILTER_USE_KEY |
109 | ); |
110 | $this->changeTagsStore->updateTags( [], array_keys( $tagsToRemove ), $rcId, $id ); |
111 | } elseif ( |
112 | ( $change['oldBits'] & RevisionRecord::DELETED_COMMENT ) !== 0 && |
113 | ( $change['newBits'] & RevisionRecord::DELETED_COMMENT ) === 0 |
114 | ) { |
115 | // We are undeleting this comment. |
116 | $rev = $this->revisionLookup->getRevisionById( $id, IDBAccessObject::READ_LATEST ); |
117 | if ( $rev === null ) { |
118 | // Maybe we should just log this and silently ignore (?) |
119 | throw new \RuntimeException( "un-revdel on revision $id that does not exist" ); |
120 | } |
121 | $comment = $rev->getComment(); |
122 | if ( $comment === null ) { |
123 | // It returns null if access control fails. |
124 | // This generally should not happen. |
125 | wfWarn( "Recently unrevdeleted comment for $id cannot be accessed" ); |
126 | return; |
127 | } |
128 | $newTags = $this->tagCollector->getTagsSeen( $this->cpFactory, $comment->text ); |
129 | $rcId = null; |
130 | $this->changeTagsStore->updateTags( $newTags, [], $rcId, $id ); |
131 | } |
132 | } |
133 | } |
134 | } |