Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 107 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 107 |
|
0.00% |
0 / 7 |
552 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onEditFilterMergedContent | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
42 | |||
onParserOutputStashForEdit | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
onUserCanSendEmail | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
onEditFilter | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
20 | |||
onPageSaveComplete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
onUploadVerifyUpload | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SpamBlacklist; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Api\ApiMessage; |
7 | use MediaWiki\Content\Content; |
8 | use MediaWiki\Content\IContentHandlerFactory; |
9 | use MediaWiki\Content\Renderer\ContentRenderer; |
10 | use MediaWiki\Context\IContextSource; |
11 | use MediaWiki\EditPage\EditPage; |
12 | use MediaWiki\ExternalLinks\LinkFilter; |
13 | use MediaWiki\Hook\EditFilterHook; |
14 | use MediaWiki\Hook\EditFilterMergedContentHook; |
15 | use MediaWiki\Hook\UploadVerifyUploadHook; |
16 | use MediaWiki\Html\Html; |
17 | use MediaWiki\Message\Message; |
18 | use MediaWiki\Parser\ParserOptions; |
19 | use MediaWiki\Parser\ParserOutput; |
20 | use MediaWiki\Permissions\PermissionManager; |
21 | use MediaWiki\Revision\RevisionRecord; |
22 | use MediaWiki\Status\Status; |
23 | use MediaWiki\Storage\EditResult; |
24 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
25 | use MediaWiki\Storage\Hook\ParserOutputStashForEditHook; |
26 | use MediaWiki\Storage\PageEditStash; |
27 | use MediaWiki\User\Hook\UserCanSendEmailHook; |
28 | use MediaWiki\User\User; |
29 | use MediaWiki\User\UserIdentity; |
30 | use UploadBase; |
31 | use Wikimedia\Assert\PreconditionException; |
32 | use Wikimedia\Message\MessageSpecifier; |
33 | use WikiPage; |
34 | |
35 | /** |
36 | * Hooks for the spam blacklist extension |
37 | */ |
38 | class Hooks implements |
39 | EditFilterHook, |
40 | EditFilterMergedContentHook, |
41 | UploadVerifyUploadHook, |
42 | PageSaveCompleteHook, |
43 | ParserOutputStashForEditHook, |
44 | UserCanSendEmailHook |
45 | { |
46 | |
47 | /** @var PermissionManager */ |
48 | private $permissionManager; |
49 | |
50 | /** @var PageEditStash */ |
51 | private $pageEditStash; |
52 | |
53 | /** @var ContentRenderer */ |
54 | private $contentRenderer; |
55 | |
56 | /** @var IContentHandlerFactory */ |
57 | private $contentHandlerFactory; |
58 | |
59 | /** |
60 | * @param PermissionManager $permissionManager |
61 | * @param PageEditStash $pageEditStash |
62 | * @param ContentRenderer $contentRenderer |
63 | * @param IContentHandlerFactory $contentHandlerFactory |
64 | */ |
65 | public function __construct( |
66 | PermissionManager $permissionManager, |
67 | PageEditStash $pageEditStash, |
68 | ContentRenderer $contentRenderer, |
69 | IContentHandlerFactory $contentHandlerFactory |
70 | ) { |
71 | $this->permissionManager = $permissionManager; |
72 | $this->pageEditStash = $pageEditStash; |
73 | $this->contentRenderer = $contentRenderer; |
74 | $this->contentHandlerFactory = $contentHandlerFactory; |
75 | } |
76 | |
77 | /** |
78 | * Hook function for EditFilterMergedContent |
79 | * |
80 | * @param IContextSource $context |
81 | * @param Content $content |
82 | * @param Status $status |
83 | * @param string $summary |
84 | * @param User $user |
85 | * @param bool $minoredit |
86 | * |
87 | * @return bool |
88 | */ |
89 | public function onEditFilterMergedContent( |
90 | IContextSource $context, |
91 | Content $content, |
92 | Status $status, |
93 | $summary, |
94 | User $user, |
95 | $minoredit |
96 | ) { |
97 | if ( $this->permissionManager->userHasRight( $user, 'sboverride' ) ) { |
98 | return true; |
99 | } |
100 | |
101 | $title = $context->getTitle(); |
102 | try { |
103 | // Try getting the update directly |
104 | $updater = $context->getWikiPage()->getCurrentUpdate(); |
105 | $pout = $updater->getParserOutputForMetaData(); |
106 | } catch ( PreconditionException | LogicException $exception ) { |
107 | $stashedEdit = $this->pageEditStash->checkCache( |
108 | $title, |
109 | $content, |
110 | $user |
111 | ); |
112 | if ( $stashedEdit ) { |
113 | // Try getting the value from edit stash |
114 | /** @var ParserOutput $output */ |
115 | $pout = $stashedEdit->output; |
116 | } else { |
117 | // Last resort, parse the page. |
118 | $pout = $this->contentRenderer->getParserOutput( |
119 | $content, |
120 | $title, |
121 | null, |
122 | null, |
123 | false |
124 | ); |
125 | } |
126 | } |
127 | $links = LinkFilter::getIndexedUrlsNonReversed( array_keys( $pout->getExternalLinks() ) ); |
128 | // HACK: treat the edit summary as a link if it contains anything |
129 | // that looks like it could be a URL or e-mail address. |
130 | if ( preg_match( '/\S(\.[^\s\d]{2,}|[\/@]\S)/', $summary ) ) { |
131 | $links[] = $summary; |
132 | } |
133 | |
134 | $spamObj = BaseBlacklist::getSpamBlacklist(); |
135 | $matches = $spamObj->filter( $links, $title, $user ); |
136 | |
137 | if ( $matches !== false ) { |
138 | $error = new ApiMessage( |
139 | wfMessage( 'spam-blacklisted-link', Message::listParam( $matches ) ), |
140 | 'spamblacklist', |
141 | [ |
142 | 'spamblacklist' => [ 'matches' => $matches ], |
143 | ] |
144 | ); |
145 | $status->fatal( $error ); |
146 | return false; |
147 | } |
148 | |
149 | return true; |
150 | } |
151 | |
152 | /** |
153 | * @param WikiPage $page |
154 | * @param Content $content |
155 | * @param ParserOutput $output |
156 | * @param string $summary |
157 | * @param User $user |
158 | */ |
159 | public function onParserOutputStashForEdit( |
160 | $page, |
161 | $content, |
162 | $output, |
163 | $summary, |
164 | $user |
165 | ) { |
166 | $links = LinkFilter::getIndexedUrlsNonReversed( array_keys( $output->getExternalLinks() ) ); |
167 | $spamObj = BaseBlacklist::getSpamBlacklist(); |
168 | $spamObj->warmCachesForFilter( $page->getTitle(), $links, $user ); |
169 | } |
170 | |
171 | /** |
172 | * Verify that the user can send emails |
173 | * |
174 | * @param User $user |
175 | * @param array &$hookErr |
176 | * @return bool |
177 | */ |
178 | public function onUserCanSendEmail( $user, &$hookErr ) { |
179 | if ( $this->permissionManager->userHasRight( $user, 'sboverride' ) ) { |
180 | return true; |
181 | } |
182 | $blacklist = BaseBlacklist::getEmailBlacklist(); |
183 | if ( $blacklist->checkUser( $user ) ) { |
184 | return true; |
185 | } |
186 | |
187 | $hookErr = [ 'spam-blacklisted-email', 'spam-blacklisted-email-text', null ]; |
188 | |
189 | // No other hook handler should run |
190 | return false; |
191 | } |
192 | |
193 | /** |
194 | * Hook function for EditFilter |
195 | * Confirm that a local blacklist page being saved is valid, |
196 | * and toss back a warning to the user if it isn't. |
197 | * |
198 | * @param EditPage $editPage |
199 | * @param string $text |
200 | * @param string $section |
201 | * @param string &$hookError |
202 | * @param string $summary |
203 | */ |
204 | public function onEditFilter( $editPage, $text, $section, &$hookError, $summary ) { |
205 | $title = $editPage->getTitle(); |
206 | $thisPageName = $title->getPrefixedDBkey(); |
207 | |
208 | if ( !BaseBlacklist::isLocalSource( $title ) ) { |
209 | wfDebugLog( 'SpamBlacklist', |
210 | "Spam blacklist validator: [[$thisPageName]] not a local blacklist\n" |
211 | ); |
212 | return; |
213 | } |
214 | |
215 | $type = BaseBlacklist::getTypeFromTitle( $title ); |
216 | if ( $type === false ) { |
217 | return; |
218 | } |
219 | |
220 | $lines = explode( "\n", $text ); |
221 | |
222 | $badLines = SpamRegexBatch::getBadLines( $lines, BaseBlacklist::getInstance( $type ) ); |
223 | if ( $badLines ) { |
224 | wfDebugLog( 'SpamBlacklist', |
225 | "Spam blacklist validator: [[$thisPageName]] given invalid input lines: " . |
226 | implode( ', ', $badLines ) . "\n" |
227 | ); |
228 | |
229 | $badList = "*<code>" . |
230 | implode( "</code>\n*<code>", |
231 | array_map( 'wfEscapeWikiText', $badLines ) ) . |
232 | "</code>\n"; |
233 | $hookError = |
234 | Html::errorBox( |
235 | wfMessage( 'spam-invalid-lines' )->numParams( $badLines )->parse() . "<br />" . |
236 | $badList |
237 | ) . |
238 | "\n<br clear='all' />\n"; |
239 | } else { |
240 | wfDebugLog( 'SpamBlacklist', |
241 | "Spam blacklist validator: [[$thisPageName]] ok or empty blacklist\n" |
242 | ); |
243 | } |
244 | } |
245 | |
246 | /** |
247 | * Hook function for PageSaveComplete |
248 | * Clear local spam blacklist caches on page save. |
249 | * |
250 | * @param WikiPage $wikiPage |
251 | * @param UserIdentity $userIdentity |
252 | * @param string $summary |
253 | * @param int $flags |
254 | * @param RevisionRecord $revisionRecord |
255 | * @param EditResult $editResult |
256 | */ |
257 | public function onPageSaveComplete( |
258 | $wikiPage, |
259 | $userIdentity, |
260 | $summary, |
261 | $flags, |
262 | $revisionRecord, |
263 | $editResult |
264 | ) { |
265 | if ( !BaseBlacklist::isLocalSource( $wikiPage->getTitle() ) ) { |
266 | return; |
267 | } |
268 | |
269 | // This sucks because every Blacklist needs to be cleared |
270 | foreach ( BaseBlacklist::getBlacklistTypes() as $type => $class ) { |
271 | $blacklist = BaseBlacklist::getInstance( $type ); |
272 | $blacklist->clearCache(); |
273 | } |
274 | } |
275 | |
276 | /** |
277 | * @param UploadBase $upload |
278 | * @param User $user |
279 | * @param array|null $props |
280 | * @param string $comment |
281 | * @param string $pageText |
282 | * @param array|MessageSpecifier &$error |
283 | */ |
284 | public function onUploadVerifyUpload( |
285 | UploadBase $upload, |
286 | User $user, |
287 | ?array $props, |
288 | $comment, |
289 | $pageText, |
290 | &$error |
291 | ) { |
292 | if ( $this->permissionManager->userHasRight( $user, 'sboverride' ) ) { |
293 | return; |
294 | } |
295 | |
296 | $title = $upload->getTitle(); |
297 | |
298 | // get the link from the not-yet-saved page content. |
299 | $content = $this->contentHandlerFactory->getContentHandler( $title->getContentModel() ) |
300 | ->unserializeContent( $pageText ); |
301 | $parserOptions = ParserOptions::newFromAnon(); |
302 | $output = $this->contentRenderer->getParserOutput( $content, $title, null, $parserOptions ); |
303 | $links = LinkFilter::getIndexedUrlsNonReversed( array_keys( $output->getExternalLinks() ) ); |
304 | |
305 | // HACK: treat comment as a link if it contains anything |
306 | // that looks like it could be a URL or e-mail address. |
307 | if ( preg_match( '/\S(\.[^\s\d]{2,}|[\/@]\S)/', $comment ) ) { |
308 | $links[] = $comment; |
309 | } |
310 | if ( !$links ) { |
311 | return; |
312 | } |
313 | |
314 | $spamObj = BaseBlacklist::getSpamBlacklist(); |
315 | $matches = $spamObj->filter( $links, $title, $user ); |
316 | |
317 | if ( $matches !== false ) { |
318 | $error = new ApiMessage( |
319 | wfMessage( 'spam-blacklisted-link', Message::listParam( $matches ) ), |
320 | 'spamblacklist', |
321 | [ |
322 | 'spamblacklist' => [ 'matches' => $matches ], |
323 | ] |
324 | ); |
325 | } |
326 | } |
327 | } |