Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
27.27% |
3 / 11 |
CRAP | |
12.04% |
13 / 108 |
RemoveOldManualUserPages | |
0.00% |
0 / 1 |
|
27.27% |
3 / 11 |
548.82 | |
12.75% |
13 / 102 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 8 |
|||
execute | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 18 |
|||
checkTitle | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 9 |
|||
getCentralWikiDomain | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
|||
deletePage | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 14 |
|||
normalizeUserName | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
checkCss | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
removeCSS | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 14 |
|||
stripComments | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 7 |
|||
checkJs | |
100.00% |
1 / 1 |
2 | |
100.00% |
6 / 6 |
|||
removeJS | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 14 |
<?php | |
/** | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation; either version 2 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License along | |
* with this program; if not, write to the Free Software Foundation, Inc., | |
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
* | |
* @file | |
* @author Szymon Ćwierkosz | |
* @author Kunal Mehta | |
*/ | |
namespace MediaWiki\GlobalCssJs; | |
use CssContent; | |
use JavaScriptContent; | |
use LinkBatch; | |
use Maintenance; | |
use MediaWiki\MediaWikiServices; | |
use MediaWiki\Revision\RevisionRecord; | |
use MediaWiki\Revision\SlotRecord; | |
use Title; | |
use User; | |
$IP = getenv( 'MW_INSTALL_PATH' ); | |
if ( $IP === false ) { | |
$IP = __DIR__ . '/../../..'; | |
} | |
require_once "$IP/maintenance/Maintenance.php"; | |
/** | |
* Script to remove manually created user .js and .css pages | |
* by users. You should run this script on every wiki where the user | |
* has an account. | |
*/ | |
class RemoveOldManualUserPages extends Maintenance { | |
/** | |
* @var bool | |
*/ | |
private $ignoreRevisionLimit; | |
public function __construct() { | |
parent::__construct(); | |
$this->addDescription( 'Remove redundant user script pages that ' . | |
'import global.js and/or global.css' ); | |
$this->addOption( 'user', 'User name', true, true ); | |
$this->addOption( 'ignorerevisionlimit', | |
'Whether to ignore the 1 revision limit', false, false ); | |
$this->requireExtension( 'GlobalCssJs' ); | |
} | |
public function execute() { | |
$this->ignoreRevisionLimit = $this->hasOption( 'ignorerevisionlimit' ); | |
$userName = $this->getOption( 'user' ); | |
$user = User::newFromName( $userName ); | |
if ( !$user->getId() || !Hooks::loadForUser( $user ) ) { | |
$this->output( "$userName does not load global modules on this wiki.\n" ); | |
return; | |
} | |
$skinFactory = MediaWikiServices::getInstance()->getSkinFactory(); | |
$skins = array_keys( $skinFactory->getAllowedSkins() ); | |
$skins[] = 'common'; | |
// Batch look up the existence of pages | |
$lb = new LinkBatch(); | |
foreach ( $skins as $name ) { | |
$lb->addObj( $user->getUserPage()->getSubpage( "$name.js" ) ); | |
$lb->addObj( $user->getUserPage()->getSubpage( "$name.css" ) ); | |
} | |
$lb->execute(); | |
foreach ( $skins as $name ) { | |
$this->removeJS( $user, $name ); | |
$this->removeCSS( $user, $name ); | |
} | |
} | |
/** | |
* Generic checks to see if we should work on a title. | |
* | |
* @param Title $title | |
* @return RevisionRecord|bool | |
*/ | |
private function checkTitle( Title $title ) { | |
if ( !$title->exists() ) { | |
$this->output( "{$title->getPrefixedText()} does not exist on this wiki.\n" ); | |
return false; | |
} | |
$revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup(); | |
$rev = $revisionLookup->getRevisionByTitle( $title ); | |
if ( !$this->ignoreRevisionLimit && $revisionLookup->getPreviousRevision( $rev ) ) { | |
$this->output( "{$title->getPrefixedText()} has more than one revision, skipping.\n" ); | |
return false; | |
} | |
return $rev; | |
} | |
/** | |
* Returns the domain name of the central wiki escaped to use in a regex. | |
* | |
* @return string | |
*/ | |
private function getCentralWikiDomain() { | |
global $wgGlobalCssJsConfig; | |
$rl = MediaWikiServices::getInstance()->getResourceLoader(); | |
$sources = $rl->getSources(); | |
// Use api.php instead of load.php because it's more likely to be on the same domain | |
$api = $sources[$wgGlobalCssJsConfig['source']]['apiScript']; | |
$parsed = wfParseUrl( $api ); | |
return preg_quote( $parsed['host'] ); | |
} | |
/** | |
* @param Title $title | |
* @param string $reason | |
* @param string $userName | |
*/ | |
private function deletePage( Title $title, $reason, $userName ) { | |
// phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser | |
global $wgUser; | |
$services = MediaWikiServices::getInstance(); | |
$page = $services->getWikiPageFactory()->newFromTitle( $title ); | |
$user = $services->getUserFactory()->newFromName( 'Maintenance script' ); | |
'@phan-var \MediaWiki\User\UserIdentity $user'; | |
$services->getUserGroupManager()->addUserToGroup( $user, 'bot' ); | |
// For hooks not using RequestContext (e.g. AbuseFilter) | |
$wgUser = $user; | |
$errors = []; | |
'@phan-var \MediaWiki\User\UserIdentity $user'; | |
$status = $page->doDeleteArticleReal( | |
wfMessage( $reason, $userName )->inContentLanguage()->text(), | |
$user, false, true, $errors, null, [], 'delete', true | |
); | |
if ( $status->isGood() ) { | |
$this->output( "{$title->getPrefixedText()} was deleted.\n" ); | |
} else { | |
$this->output( "{$title->getPrefixedText()} could not be deleted:\n" . | |
$status->getWikiText() . "\n" ); | |
} | |
} | |
/** | |
* Given a username, normalize and escape it to be | |
* safely used in regex | |
* | |
* @param string $userName | |
* @return string | |
*/ | |
public function normalizeUserName( $userName ) { | |
$userName = preg_quote( $userName ); | |
// Spaces can be represented as space, underscore, plus, or %20. | |
$userName = str_replace( ' ', '( |_|\+|%20)', $userName ); | |
return $userName; | |
} | |
/** | |
* @param string $text | |
* @param string $domain | |
* @param string $userName | |
* @return bool | |
*/ | |
public function checkCss( $text, $domain, $userName ) { | |
$userName = $this->normalizeUserName( $userName ); | |
preg_match( "/@import url\('(https?:)?\/\/$domain\/w\/index\.php\?title=User:$userName" . | |
"\/global\.css&action=raw&ctype=text\/css'\);/", $text, $matches ); | |
return isset( $matches[0] ) ? $matches[0] === $text : false; | |
} | |
/** | |
* @param User $user | |
* @param string $skin | |
*/ | |
private function removeCSS( User $user, $skin ) { | |
$userName = $user->getName(); | |
$title = $user->getUserPage()->getSubpage( $skin . '.css' ); | |
$rev = $this->checkTitle( $title ); | |
if ( !$rev ) { | |
return; | |
} | |
/** @var CssContent $content */ | |
$content = $rev->getContent( SlotRecord::MAIN ); | |
$text = trim( $content->getNativeData() ); | |
$domain = $this->getCentralWikiDomain(); | |
if ( !$this->checkCss( $text, $domain, $userName ) ) { | |
$this->output( "{$title->getPrefixedText()} did not match the specified regular " . | |
"expression. Skipping.\n" ); | |
return; | |
} | |
// Delete! | |
$this->deletePage( $title, 'globalcssjs-delete-css', $userName ); | |
} | |
/** | |
* Remove lines that are entirely comments, by checking if they start with // | |
* Also get rid of empty lines while we're at it. | |
* | |
* @param string $js | |
* @return string | |
*/ | |
private function stripComments( $js ) { | |
$exploded = explode( "\n", $js ); | |
$new = []; | |
foreach ( $exploded as $line ) { | |
$trimmed = trim( $line ); | |
if ( $trimmed !== '' && substr( $trimmed, 0, 2 ) !== '//' ) { | |
$new[] = $line; | |
} | |
} | |
return implode( '\n', $new ); | |
} | |
/** | |
* @param string $text | |
* @param string $domain | |
* @param string $userName | |
* @return bool | |
*/ | |
public function checkJs( $text, $domain, $userName ) { | |
$text = $this->stripComments( $text ); | |
$userName = $this->normalizeUserName( $userName ); | |
preg_match( "/(mw\.loader\.load|importScriptURI)\s*\(\s*('|\")(https?:)?\/\/$domain" . | |
"\/w\/index\.php\?title=User:$userName\/global\.js&action=raw&ctype=text\/javascript" . | |
"(&smaxage=\d*?)?(&maxage=\d*?)?('|\")\s*\)\s*;?/", $text, $matches ); | |
return isset( $matches[0] ) ? $matches[0] === $text : false; | |
} | |
/** | |
* @param User $user | |
* @param string $skin | |
*/ | |
private function removeJS( User $user, $skin ) { | |
$userName = $user->getName(); | |
$title = $user->getUserPage()->getSubpage( $skin . '.js' ); | |
$rev = $this->checkTitle( $title ); | |
if ( !$rev ) { | |
return; | |
} | |
/** @var JavaScriptContent $content */ | |
$content = $rev->getContent( SlotRecord::MAIN ); | |
$text = trim( $content->getNativeData() ); | |
$domain = $this->getCentralWikiDomain(); | |
if ( !$this->checkJs( $text, $domain, $userName ) ) { | |
$this->output( "{$title->getPrefixedText()} did not match the specified regular " . | |
"expression. Skipping.\n" ); | |
return; | |
} | |
// Delete! | |
$this->deletePage( $title, 'globalcssjs-delete-js', $userName ); | |
} | |
} | |
$maintClass = RemoveOldManualUserPages::class; | |
require_once RUN_MAINTENANCE_IF_MAIN; |