Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 101 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 101 |
|
0.00% |
0 / 7 |
812 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onBeforePageDisplay | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
onWikiPageDeletionUpdates | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
onRevisionFromEditComplete | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
onAPIQuerySiteInfoGeneralInfo | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onInfoAction | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
20 | |||
onParserLogLinterData | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
156 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Linter; |
22 | |
23 | use ApiQuerySiteinfo; |
24 | use Content; |
25 | use IContextSource; |
26 | use JobQueueGroup; |
27 | use MediaWiki\Api\Hook\APIQuerySiteInfoGeneralInfoHook; |
28 | use MediaWiki\Deferred\MWCallableUpdate; |
29 | use MediaWiki\Hook\BeforePageDisplayHook; |
30 | use MediaWiki\Hook\InfoActionHook; |
31 | use MediaWiki\Hook\ParserLogLinterDataHook; |
32 | use MediaWiki\Linker\LinkRenderer; |
33 | use MediaWiki\Logger\LoggerFactory; |
34 | use MediaWiki\Output\OutputPage; |
35 | use MediaWiki\Page\Hook\RevisionFromEditCompleteHook; |
36 | use MediaWiki\Page\Hook\WikiPageDeletionUpdatesHook; |
37 | use MediaWiki\Revision\RevisionRecord; |
38 | use MediaWiki\SpecialPage\SpecialPage; |
39 | use MediaWiki\Title\Title; |
40 | use MediaWiki\User\UserIdentity; |
41 | use Skin; |
42 | use WikiPage; |
43 | |
44 | class Hooks implements |
45 | APIQuerySiteInfoGeneralInfoHook, |
46 | BeforePageDisplayHook, |
47 | InfoActionHook, |
48 | ParserLogLinterDataHook, |
49 | RevisionFromEditCompleteHook, |
50 | WikiPageDeletionUpdatesHook |
51 | { |
52 | private LinkRenderer $linkRenderer; |
53 | private JobQueueGroup $jobQueueGroup; |
54 | private CategoryManager $categoryManager; |
55 | private TotalsLookup $totalsLookup; |
56 | private Database $database; |
57 | |
58 | /** |
59 | * @param LinkRenderer $linkRenderer |
60 | * @param JobQueueGroup $jobQueueGroup |
61 | * @param CategoryManager $categoryManager |
62 | * @param TotalsLookup $totalsLookup |
63 | * @param Database $database |
64 | */ |
65 | public function __construct( |
66 | LinkRenderer $linkRenderer, |
67 | JobQueueGroup $jobQueueGroup, |
68 | CategoryManager $categoryManager, |
69 | TotalsLookup $totalsLookup, |
70 | Database $database |
71 | ) { |
72 | $this->linkRenderer = $linkRenderer; |
73 | $this->jobQueueGroup = $jobQueueGroup; |
74 | $this->categoryManager = $categoryManager; |
75 | $this->totalsLookup = $totalsLookup; |
76 | $this->database = $database; |
77 | } |
78 | |
79 | /** |
80 | * Hook: BeforePageDisplay |
81 | * |
82 | * If there is a lintid parameter, look up that error in the database |
83 | * and setup and output our client-side helpers |
84 | * |
85 | * @param OutputPage $out |
86 | * @param Skin $skin |
87 | */ |
88 | public function onBeforePageDisplay( $out, $skin ): void { |
89 | $request = $out->getRequest(); |
90 | $lintId = $request->getInt( 'lintid' ); |
91 | if ( !$lintId ) { |
92 | return; |
93 | } |
94 | $title = $out->getTitle(); |
95 | if ( !$title ) { |
96 | return; |
97 | } |
98 | |
99 | $lintError = $this->database->getFromId( $lintId ); |
100 | if ( !$lintError ) { |
101 | // Already fixed or bogus URL parameter? |
102 | return; |
103 | } |
104 | |
105 | $out->addJsConfigVars( [ |
106 | 'wgLinterErrorCategory' => $lintError->category, |
107 | 'wgLinterErrorLocation' => $lintError->location, |
108 | ] ); |
109 | $out->addModules( 'ext.linter.edit' ); |
110 | } |
111 | |
112 | /** |
113 | * Hook: WikiPageDeletionUpdates |
114 | * |
115 | * Remove entries from the linter table upon page deletion |
116 | * |
117 | * @param WikiPage $wikiPage |
118 | * @param Content $content |
119 | * @param array &$updates |
120 | */ |
121 | public function onWikiPageDeletionUpdates( $wikiPage, $content, &$updates ) { |
122 | $updates[] = new MWCallableUpdate( function () use ( $wikiPage ) { |
123 | $this->totalsLookup->updateStats( |
124 | $this->database->setForPage( |
125 | $wikiPage->getId(), $wikiPage->getNamespace(), [] |
126 | ) |
127 | ); |
128 | }, __METHOD__ ); |
129 | } |
130 | |
131 | /** |
132 | * This should match Parsoid's PageConfig::hasLintableContentModel() |
133 | */ |
134 | public const LINTABLE_CONTENT_MODELS = [ CONTENT_MODEL_WIKITEXT, 'proofread-page' ]; |
135 | |
136 | /** |
137 | * Hook: RevisionFromEditComplete |
138 | * |
139 | * Remove entries from the linter table upon page content model change away from wikitext |
140 | * |
141 | * @param WikiPage $wikiPage |
142 | * @param RevisionRecord $newRevisionRecord |
143 | * @param bool|int $originalRevId |
144 | * @param UserIdentity $user |
145 | * @param string[] &$tags |
146 | */ |
147 | public function onRevisionFromEditComplete( |
148 | $wikiPage, $newRevisionRecord, $originalRevId, $user, &$tags |
149 | ) { |
150 | // This is just a stop-gap to deal with callers that aren't complying |
151 | // with the advertised hook signature. |
152 | if ( !is_array( $tags ) ) { |
153 | return; |
154 | } |
155 | |
156 | if ( |
157 | in_array( "mw-blank", $tags ) || |
158 | ( in_array( "mw-contentmodelchange", $tags ) && |
159 | !in_array( $wikiPage->getContentModel(), self::LINTABLE_CONTENT_MODELS ) ) |
160 | ) { |
161 | $this->totalsLookup->updateStats( |
162 | $this->database->setForPage( |
163 | $wikiPage->getId(), $wikiPage->getNamespace(), [] |
164 | ) |
165 | ); |
166 | } |
167 | } |
168 | |
169 | /** |
170 | * Hook: APIQuerySiteInfoGeneralInfo |
171 | * |
172 | * Expose categories via action=query&meta=siteinfo |
173 | * |
174 | * @param ApiQuerySiteInfo $api |
175 | * @param array &$data |
176 | */ |
177 | public function onAPIQuerySiteInfoGeneralInfo( $api, &$data ) { |
178 | $data['linter'] = [ |
179 | 'high' => $this->categoryManager->getHighPriority(), |
180 | 'medium' => $this->categoryManager->getMediumPriority(), |
181 | 'low' => $this->categoryManager->getLowPriority(), |
182 | ]; |
183 | } |
184 | |
185 | /** |
186 | * Hook: InfoAction |
187 | * |
188 | * Display quick summary of errors for this page on ?action=info |
189 | * |
190 | * @param IContextSource $context |
191 | * @param array &$pageInfo |
192 | */ |
193 | public function onInfoAction( $context, &$pageInfo ) { |
194 | $title = $context->getTitle(); |
195 | $pageId = $title->getArticleID(); |
196 | if ( !$pageId ) { |
197 | return; |
198 | } |
199 | $totals = array_filter( $this->database->getTotalsForPage( $pageId ) ); |
200 | if ( !$totals ) { |
201 | // No errors, yay! |
202 | return; |
203 | } |
204 | |
205 | foreach ( $totals as $name => $count ) { |
206 | $pageInfo['linter'][] = [ |
207 | $context->msg( "linter-category-$name" ), |
208 | htmlspecialchars( (string)$count ) |
209 | ]; |
210 | } |
211 | |
212 | $pageInfo['linter'][] = [ |
213 | 'below', |
214 | $this->linkRenderer->makeKnownLink( |
215 | SpecialPage::getTitleFor( 'LintErrors' ), |
216 | $context->msg( 'pageinfo-linter-moreinfo' )->text(), |
217 | [], |
218 | [ 'wpNamespaceRestrictions' => $title->getNamespace(), |
219 | 'titlesearch' => $title->getText(), 'exactmatch' => 1 ] |
220 | ), |
221 | ]; |
222 | } |
223 | |
224 | /** |
225 | * Hook: ParserLogLinterData |
226 | * |
227 | * To record a lint errors. |
228 | * |
229 | * @param string $page |
230 | * @param int $revision |
231 | * @param array[] $data |
232 | * @return bool |
233 | */ |
234 | public function onParserLogLinterData( |
235 | string $page, int $revision, array $data |
236 | ): bool { |
237 | $errors = []; |
238 | $title = Title::newFromText( $page ); |
239 | if ( |
240 | !$title || !$title->getArticleID() || |
241 | $title->getLatestRevID() != $revision |
242 | ) { |
243 | return false; |
244 | } |
245 | $catCounts = []; |
246 | foreach ( $data as $info ) { |
247 | if ( $this->categoryManager->isKnownCategory( $info['type'] ) ) { |
248 | $info[ 'dbid' ] = null; |
249 | } elseif ( !isset( $info[ 'dbid' ] ) ) { |
250 | continue; |
251 | } |
252 | $count = $catCounts[$info['type']] ?? 0; |
253 | if ( $count > Database::MAX_PER_CAT ) { |
254 | // Drop |
255 | continue; |
256 | } |
257 | $catCounts[$info['type']] = $count + 1; |
258 | if ( !isset( $info['dsr'] ) ) { |
259 | LoggerFactory::getInstance( 'Linter' )->warning( |
260 | 'dsr for {page} @ rev {revid}, for lint: {lint} is missing', |
261 | [ |
262 | 'page' => $page, |
263 | 'revid' => $revision, |
264 | 'lint' => $info['type'], |
265 | ] |
266 | ); |
267 | continue; |
268 | } |
269 | $info['location'] = array_slice( $info['dsr'], 0, 2 ); |
270 | if ( !isset( $info['params'] ) ) { |
271 | $info['params'] = []; |
272 | } |
273 | if ( isset( $info['templateInfo'] ) && $info['templateInfo'] ) { |
274 | $info['params']['templateInfo'] = $info['templateInfo']; |
275 | } |
276 | $errors[] = $info; |
277 | } |
278 | $job = new RecordLintJob( $title, [ |
279 | 'errors' => $errors, |
280 | 'revision' => $revision, |
281 | ], $this->totalsLookup, $this->database ); |
282 | $this->jobQueueGroup->push( $job ); |
283 | return true; |
284 | } |
285 | } |