2012-01-17 06:13:46 +00:00
|
|
|
<?php
|
|
|
|
|
2022-04-08 13:05:02 +00:00
|
|
|
namespace MediaWiki\Extension\SpamBlacklist;
|
|
|
|
|
|
|
|
use ApiMessage;
|
|
|
|
use Content;
|
|
|
|
use LogicException;
|
2022-08-27 21:04:44 +00:00
|
|
|
use MediaWiki\Content\IContentHandlerFactory;
|
|
|
|
use MediaWiki\Content\Renderer\ContentRenderer;
|
2024-06-10 18:42:06 +00:00
|
|
|
use MediaWiki\Context\IContextSource;
|
2023-05-06 21:20:28 +00:00
|
|
|
use MediaWiki\EditPage\EditPage;
|
2023-05-26 14:00:18 +00:00
|
|
|
use MediaWiki\ExternalLinks\LinkFilter;
|
2022-04-08 13:05:02 +00:00
|
|
|
use MediaWiki\Hook\EditFilterHook;
|
|
|
|
use MediaWiki\Hook\EditFilterMergedContentHook;
|
|
|
|
use MediaWiki\Hook\UploadVerifyUploadHook;
|
2024-01-05 18:28:12 +00:00
|
|
|
use MediaWiki\Html\Html;
|
2024-06-10 18:42:06 +00:00
|
|
|
use MediaWiki\Message\Message;
|
2024-01-05 18:28:12 +00:00
|
|
|
use MediaWiki\Parser\ParserOutput;
|
2022-08-27 21:04:44 +00:00
|
|
|
use MediaWiki\Permissions\PermissionManager;
|
2020-06-16 04:27:37 +00:00
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
2024-01-05 18:28:12 +00:00
|
|
|
use MediaWiki\Status\Status;
|
2020-06-16 04:27:37 +00:00
|
|
|
use MediaWiki\Storage\EditResult;
|
2022-04-08 13:05:02 +00:00
|
|
|
use MediaWiki\Storage\Hook\PageSaveCompleteHook;
|
|
|
|
use MediaWiki\Storage\Hook\ParserOutputStashForEditHook;
|
2022-08-27 21:04:44 +00:00
|
|
|
use MediaWiki\Storage\PageEditStash;
|
2022-04-08 13:05:02 +00:00
|
|
|
use MediaWiki\User\Hook\UserCanSendEmailHook;
|
2024-01-05 18:28:12 +00:00
|
|
|
use MediaWiki\User\User;
|
2020-06-16 04:27:37 +00:00
|
|
|
use MediaWiki\User\UserIdentity;
|
2022-04-08 13:05:02 +00:00
|
|
|
use MessageSpecifier;
|
|
|
|
use ParserOptions;
|
|
|
|
use UploadBase;
|
2021-12-24 01:13:01 +00:00
|
|
|
use Wikimedia\Assert\PreconditionException;
|
2022-04-08 13:05:02 +00:00
|
|
|
use WikiPage;
|
2020-06-16 04:27:37 +00:00
|
|
|
|
2012-01-17 06:13:46 +00:00
|
|
|
/**
|
|
|
|
* Hooks for the spam blacklist extension
|
|
|
|
*/
|
2022-04-08 13:05:02 +00:00
|
|
|
class Hooks implements
|
|
|
|
EditFilterHook,
|
|
|
|
EditFilterMergedContentHook,
|
|
|
|
UploadVerifyUploadHook,
|
|
|
|
PageSaveCompleteHook,
|
|
|
|
ParserOutputStashForEditHook,
|
|
|
|
UserCanSendEmailHook
|
2021-03-07 11:11:25 +00:00
|
|
|
{
|
|
|
|
|
2022-08-27 21:04:44 +00:00
|
|
|
/** @var PermissionManager */
|
|
|
|
private $permissionManager;
|
|
|
|
|
|
|
|
/** @var PageEditStash */
|
|
|
|
private $pageEditStash;
|
|
|
|
|
|
|
|
/** @var ContentRenderer */
|
|
|
|
private $contentRenderer;
|
|
|
|
|
|
|
|
/** @var IContentHandlerFactory */
|
|
|
|
private $contentHandlerFactory;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PermissionManager $permissionManager
|
|
|
|
* @param PageEditStash $pageEditStash
|
|
|
|
* @param ContentRenderer $contentRenderer
|
|
|
|
* @param IContentHandlerFactory $contentHandlerFactory
|
|
|
|
*/
|
|
|
|
public function __construct(
|
|
|
|
PermissionManager $permissionManager,
|
|
|
|
PageEditStash $pageEditStash,
|
|
|
|
ContentRenderer $contentRenderer,
|
|
|
|
IContentHandlerFactory $contentHandlerFactory
|
|
|
|
) {
|
|
|
|
$this->permissionManager = $permissionManager;
|
|
|
|
$this->pageEditStash = $pageEditStash;
|
|
|
|
$this->contentRenderer = $contentRenderer;
|
|
|
|
$this->contentHandlerFactory = $contentHandlerFactory;
|
|
|
|
}
|
|
|
|
|
2012-01-17 06:13:46 +00:00
|
|
|
/**
|
2013-07-25 14:05:13 +00:00
|
|
|
* Hook function for EditFilterMergedContent
|
|
|
|
*
|
|
|
|
* @param IContextSource $context
|
2017-09-01 04:57:27 +00:00
|
|
|
* @param Content $content
|
|
|
|
* @param Status $status
|
|
|
|
* @param string $summary
|
|
|
|
* @param User $user
|
|
|
|
* @param bool $minoredit
|
2012-01-17 06:13:46 +00:00
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
2021-03-07 11:11:25 +00:00
|
|
|
public function onEditFilterMergedContent(
|
2017-06-02 16:02:09 +00:00
|
|
|
IContextSource $context,
|
|
|
|
Content $content,
|
|
|
|
Status $status,
|
|
|
|
$summary,
|
|
|
|
User $user,
|
|
|
|
$minoredit
|
|
|
|
) {
|
2022-08-27 21:04:44 +00:00
|
|
|
if ( $this->permissionManager->userHasRight( $user, 'sboverride' ) ) {
|
2019-11-23 02:45:58 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2013-07-25 14:05:13 +00:00
|
|
|
$title = $context->getTitle();
|
2022-01-10 06:33:05 +00:00
|
|
|
try {
|
|
|
|
// Try getting the update directly
|
|
|
|
$updater = $context->getWikiPage()->getCurrentUpdate();
|
|
|
|
$pout = $updater->getParserOutputForMetaData();
|
2022-01-13 07:08:01 +00:00
|
|
|
} catch ( PreconditionException | LogicException $exception ) {
|
2022-08-27 21:04:44 +00:00
|
|
|
$stashedEdit = $this->pageEditStash->checkCache(
|
2022-01-10 06:33:05 +00:00
|
|
|
$title,
|
|
|
|
$content,
|
|
|
|
$user
|
|
|
|
);
|
|
|
|
if ( $stashedEdit ) {
|
|
|
|
// Try getting the value from edit stash
|
|
|
|
/** @var ParserOutput $output */
|
|
|
|
$pout = $stashedEdit->output;
|
|
|
|
} else {
|
2021-12-24 01:13:01 +00:00
|
|
|
// Last resort, parse the page.
|
2022-08-27 21:04:44 +00:00
|
|
|
$pout = $this->contentRenderer->getParserOutput(
|
2021-12-24 01:13:01 +00:00
|
|
|
$content,
|
|
|
|
$title,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
}
|
2021-08-16 13:26:45 +00:00
|
|
|
}
|
2023-05-26 14:00:18 +00:00
|
|
|
$links = LinkFilter::getIndexedUrlsNonReversed( array_keys( $pout->getExternalLinks() ) );
|
2015-11-28 04:02:57 +00:00
|
|
|
// HACK: treat the edit summary as a link if it contains anything
|
|
|
|
// that looks like it could be a URL or e-mail address.
|
|
|
|
if ( preg_match( '/\S(\.[^\s\d]{2,}|[\/@]\S)/', $summary ) ) {
|
2013-07-25 14:05:13 +00:00
|
|
|
$links[] = $summary;
|
|
|
|
}
|
|
|
|
|
2018-02-25 01:25:04 +00:00
|
|
|
$spamObj = BaseBlacklist::getSpamBlacklist();
|
2020-01-04 11:16:36 +00:00
|
|
|
$matches = $spamObj->filter( $links, $title, $user );
|
2013-07-25 14:05:13 +00:00
|
|
|
|
|
|
|
if ( $matches !== false ) {
|
Actually return errors for action=edit API
Setting 'apiHookResult' results in a "successful" response; if we want
to report an error, we need to use ApiMessage. We already were doing
this for action=upload. Now our action=edit API responses will be
consistent with MediaWiki and other extensions, and will be able to
take advantage of errorformat=html.
Additionally, remove incorrect 'message' value from action=upload
output. It was anyway redundant to the normal error information.
To avoid user interface regressions in VisualEditor, the changes
I3b9c4fef (in VE) and I106dbd3c (in MediaWiki) should be merged first.
Before:
{
"edit": {
"spamblacklist": "example.com/test|example.net/test",
"result": "Failure"
}
}
After:
{
"errors": [
{
"code": "spamblacklist",
"data": {
"spamblacklist": {
"matches": [
"example.com/test",
"example.net/test"
]
}
},
"module": "edit",
"*": "The text you wanted to save was blocked ..."
}
],
"*": "See http://localhost:3080/w/api.php for API usage. ..."
}
For comparison, a 'readonly' error:
{
"errors": [
{
"code": "readonly",
"data": {
"readonlyreason": "foo bar"
},
"module": "main",
"*": "The wiki is currently in read-only mode."
}
],
"*": "See http://localhost:3080/w/api.php for API usage. ..."
}
Bug: T229539
Depends-On: I106dbd3cbdbf7082b1d1f1c1106ece6b19c22a86
Depends-On: I3b9c4fefc0869ef7999c21cef754434febd852ec
Change-Id: Id36aa6bdb8f873fe7deb8123a7fc774103721c01
2019-08-20 18:47:21 +00:00
|
|
|
$error = new ApiMessage(
|
|
|
|
wfMessage( 'spam-blacklisted-link', Message::listParam( $matches ) ),
|
|
|
|
'spamblacklist',
|
|
|
|
[
|
|
|
|
'spamblacklist' => [ 'matches' => $matches ],
|
|
|
|
]
|
|
|
|
);
|
|
|
|
$status->fatal( $error );
|
2021-04-17 10:00:39 +00:00
|
|
|
return false;
|
2012-01-17 06:13:46 +00:00
|
|
|
}
|
2013-07-25 14:05:13 +00:00
|
|
|
|
|
|
|
return true;
|
2012-01-17 06:13:46 +00:00
|
|
|
}
|
|
|
|
|
2021-03-07 11:11:25 +00:00
|
|
|
/**
|
|
|
|
* @param WikiPage $page
|
|
|
|
* @param Content $content
|
|
|
|
* @param ParserOutput $output
|
|
|
|
* @param string $summary
|
|
|
|
* @param User $user
|
|
|
|
*/
|
|
|
|
public function onParserOutputStashForEdit(
|
|
|
|
$page,
|
|
|
|
$content,
|
|
|
|
$output,
|
2020-01-04 11:16:36 +00:00
|
|
|
$summary,
|
2021-03-07 11:11:25 +00:00
|
|
|
$user
|
2016-06-30 21:26:36 +00:00
|
|
|
) {
|
2023-05-26 14:00:18 +00:00
|
|
|
$links = LinkFilter::getIndexedUrlsNonReversed( array_keys( $output->getExternalLinks() ) );
|
2018-02-25 01:25:04 +00:00
|
|
|
$spamObj = BaseBlacklist::getSpamBlacklist();
|
2020-01-04 11:16:36 +00:00
|
|
|
$spamObj->warmCachesForFilter( $page->getTitle(), $links, $user );
|
2016-02-16 21:39:06 +00:00
|
|
|
}
|
|
|
|
|
2012-01-18 23:29:37 +00:00
|
|
|
/**
|
|
|
|
* Verify that the user can send emails
|
|
|
|
*
|
2021-03-07 11:11:25 +00:00
|
|
|
* @param User $user
|
2017-10-07 09:15:06 +00:00
|
|
|
* @param array &$hookErr
|
2012-01-18 23:29:37 +00:00
|
|
|
* @return bool
|
|
|
|
*/
|
2021-03-07 11:11:25 +00:00
|
|
|
public function onUserCanSendEmail( $user, &$hookErr ) {
|
2023-05-30 07:09:00 +00:00
|
|
|
if ( $this->permissionManager->userHasRight( $user, 'sboverride' ) ) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-02-25 01:25:04 +00:00
|
|
|
$blacklist = BaseBlacklist::getEmailBlacklist();
|
2012-01-18 23:29:37 +00:00
|
|
|
if ( $blacklist->checkUser( $user ) ) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-06-06 16:29:27 +00:00
|
|
|
$hookErr = [ 'spam-blacklisted-email', 'spam-blacklisted-email-text', null ];
|
2012-01-18 23:29:37 +00:00
|
|
|
|
2021-03-07 11:11:25 +00:00
|
|
|
// No other hook handler should run
|
2012-01-18 23:29:37 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-01-17 06:13:46 +00:00
|
|
|
/**
|
|
|
|
* Hook function for EditFilter
|
|
|
|
* Confirm that a local blacklist page being saved is valid,
|
|
|
|
* and toss back a warning to the user if it isn't.
|
|
|
|
*
|
2017-09-01 20:24:40 +00:00
|
|
|
* @param EditPage $editPage
|
|
|
|
* @param string $text
|
|
|
|
* @param string $section
|
2017-10-07 09:15:06 +00:00
|
|
|
* @param string &$hookError
|
2021-03-07 11:11:25 +00:00
|
|
|
* @param string $summary
|
2012-01-17 06:13:46 +00:00
|
|
|
*/
|
2021-03-07 11:11:25 +00:00
|
|
|
public function onEditFilter( $editPage, $text, $section, &$hookError, $summary ) {
|
2017-09-01 20:24:40 +00:00
|
|
|
$title = $editPage->getTitle();
|
|
|
|
$thisPageName = $title->getPrefixedDBkey();
|
2012-01-17 06:13:46 +00:00
|
|
|
|
2017-09-01 20:24:40 +00:00
|
|
|
if ( !BaseBlacklist::isLocalSource( $title ) ) {
|
2017-06-02 16:02:09 +00:00
|
|
|
wfDebugLog( 'SpamBlacklist',
|
|
|
|
"Spam blacklist validator: [[$thisPageName]] not a local blacklist\n"
|
|
|
|
);
|
2021-03-07 11:11:25 +00:00
|
|
|
return;
|
2012-01-17 06:13:46 +00:00
|
|
|
}
|
|
|
|
|
2017-09-01 20:24:40 +00:00
|
|
|
$type = BaseBlacklist::getTypeFromTitle( $title );
|
2012-01-18 23:29:37 +00:00
|
|
|
if ( $type === false ) {
|
2021-03-07 11:11:25 +00:00
|
|
|
return;
|
2012-01-18 23:29:37 +00:00
|
|
|
}
|
|
|
|
|
2012-01-17 06:13:46 +00:00
|
|
|
$lines = explode( "\n", $text );
|
|
|
|
|
2012-01-18 23:29:37 +00:00
|
|
|
$badLines = SpamRegexBatch::getBadLines( $lines, BaseBlacklist::getInstance( $type ) );
|
2017-06-06 16:29:27 +00:00
|
|
|
if ( $badLines ) {
|
2017-06-02 16:02:09 +00:00
|
|
|
wfDebugLog( 'SpamBlacklist',
|
|
|
|
"Spam blacklist validator: [[$thisPageName]] given invalid input lines: " .
|
|
|
|
implode( ', ', $badLines ) . "\n"
|
|
|
|
);
|
2012-01-17 06:13:46 +00:00
|
|
|
|
2012-09-02 15:41:39 +00:00
|
|
|
$badList = "*<code>" .
|
|
|
|
implode( "</code>\n*<code>",
|
2012-01-17 06:13:46 +00:00
|
|
|
array_map( 'wfEscapeWikiText', $badLines ) ) .
|
2012-09-02 15:41:39 +00:00
|
|
|
"</code>\n";
|
2012-01-17 06:13:46 +00:00
|
|
|
$hookError =
|
2022-03-24 21:30:14 +00:00
|
|
|
Html::errorBox(
|
2012-09-02 15:41:39 +00:00
|
|
|
wfMessage( 'spam-invalid-lines' )->numParams( $badLines )->text() . "<br />" .
|
2022-03-24 21:30:14 +00:00
|
|
|
$badList
|
|
|
|
) .
|
|
|
|
"\n<br clear='all' />\n";
|
2012-01-17 06:13:46 +00:00
|
|
|
} else {
|
2017-06-02 16:02:09 +00:00
|
|
|
wfDebugLog( 'SpamBlacklist',
|
|
|
|
"Spam blacklist validator: [[$thisPageName]] ok or empty blacklist\n"
|
|
|
|
);
|
2012-01-17 06:13:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-16 04:27:37 +00:00
|
|
|
* Hook function for PageSaveComplete
|
2012-01-17 06:13:46 +00:00
|
|
|
* Clear local spam blacklist caches on page save.
|
|
|
|
*
|
2017-12-08 14:15:03 +00:00
|
|
|
* @param WikiPage $wikiPage
|
2020-06-16 04:27:37 +00:00
|
|
|
* @param UserIdentity $userIdentity
|
2017-09-01 04:57:27 +00:00
|
|
|
* @param string $summary
|
|
|
|
* @param int $flags
|
2020-06-16 04:27:37 +00:00
|
|
|
* @param RevisionRecord $revisionRecord
|
|
|
|
* @param EditResult $editResult
|
2012-01-17 06:13:46 +00:00
|
|
|
*/
|
2021-03-07 11:11:25 +00:00
|
|
|
public function onPageSaveComplete(
|
|
|
|
$wikiPage,
|
|
|
|
$userIdentity,
|
|
|
|
$summary,
|
|
|
|
$flags,
|
|
|
|
$revisionRecord,
|
|
|
|
$editResult
|
2013-07-25 14:05:13 +00:00
|
|
|
) {
|
2015-07-31 21:02:14 +00:00
|
|
|
if ( !BaseBlacklist::isLocalSource( $wikiPage->getTitle() ) ) {
|
2021-03-07 11:11:25 +00:00
|
|
|
return;
|
2012-01-17 06:13:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// This sucks because every Blacklist needs to be cleared
|
|
|
|
foreach ( BaseBlacklist::getBlacklistTypes() as $type => $class ) {
|
2015-07-31 21:02:14 +00:00
|
|
|
$blacklist = BaseBlacklist::getInstance( $type );
|
|
|
|
$blacklist->clearCache();
|
2012-01-17 06:13:46 +00:00
|
|
|
}
|
2016-07-22 14:02:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param UploadBase $upload
|
|
|
|
* @param User $user
|
2019-05-15 18:36:14 +00:00
|
|
|
* @param array|null $props
|
2016-07-22 14:02:31 +00:00
|
|
|
* @param string $comment
|
|
|
|
* @param string $pageText
|
2021-03-07 11:11:25 +00:00
|
|
|
* @param array|MessageSpecifier &$error
|
2016-07-22 14:02:31 +00:00
|
|
|
*/
|
2021-03-07 11:11:25 +00:00
|
|
|
public function onUploadVerifyUpload(
|
2017-06-02 16:02:09 +00:00
|
|
|
UploadBase $upload,
|
|
|
|
User $user,
|
2021-03-07 11:11:25 +00:00
|
|
|
?array $props,
|
2017-06-02 16:02:09 +00:00
|
|
|
$comment,
|
|
|
|
$pageText,
|
|
|
|
&$error
|
|
|
|
) {
|
2022-08-27 21:04:44 +00:00
|
|
|
if ( $this->permissionManager->userHasRight( $user, 'sboverride' ) ) {
|
2019-11-23 02:45:58 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-07-22 14:02:31 +00:00
|
|
|
$title = $upload->getTitle();
|
|
|
|
|
|
|
|
// get the link from the not-yet-saved page content.
|
2022-08-27 21:04:44 +00:00
|
|
|
$content = $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
|
|
|
|
->unserializeContent( $pageText );
|
2021-12-18 19:07:40 +00:00
|
|
|
$parserOptions = ParserOptions::newFromAnon();
|
2022-08-27 21:04:44 +00:00
|
|
|
$output = $this->contentRenderer->getParserOutput( $content, $title, null, $parserOptions );
|
2023-05-26 14:00:18 +00:00
|
|
|
$links = LinkFilter::getIndexedUrlsNonReversed( array_keys( $output->getExternalLinks() ) );
|
2016-07-22 14:02:31 +00:00
|
|
|
|
|
|
|
// HACK: treat comment as a link if it contains anything
|
|
|
|
// that looks like it could be a URL or e-mail address.
|
|
|
|
if ( preg_match( '/\S(\.[^\s\d]{2,}|[\/@]\S)/', $comment ) ) {
|
|
|
|
$links[] = $comment;
|
|
|
|
}
|
|
|
|
if ( !$links ) {
|
2021-03-07 11:11:25 +00:00
|
|
|
return;
|
2016-07-22 14:02:31 +00:00
|
|
|
}
|
|
|
|
|
2018-02-25 01:25:04 +00:00
|
|
|
$spamObj = BaseBlacklist::getSpamBlacklist();
|
2020-01-04 11:16:36 +00:00
|
|
|
$matches = $spamObj->filter( $links, $title, $user );
|
2016-07-22 14:02:31 +00:00
|
|
|
|
|
|
|
if ( $matches !== false ) {
|
|
|
|
$error = new ApiMessage(
|
2019-08-01 00:27:06 +00:00
|
|
|
wfMessage( 'spam-blacklisted-link', Message::listParam( $matches ) ),
|
2016-07-22 14:02:31 +00:00
|
|
|
'spamblacklist',
|
2017-06-06 16:29:27 +00:00
|
|
|
[
|
|
|
|
'spamblacklist' => [ 'matches' => $matches ],
|
|
|
|
]
|
2016-07-22 14:02:31 +00:00
|
|
|
);
|
|
|
|
}
|
2012-01-17 06:13:46 +00:00
|
|
|
}
|
|
|
|
}
|