SHARE
TWEET

removeOldManualUserPages.php

a guest Jan 28th, 2018 5 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  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.  *
  17.  * @file
  18.  * @author Szymon Świerkosz
  19.  * @author Kunal Mehta
  20.  */
  21.  
  22. namespace MediaWiki\GlobalCssJs;
  23.  
  24. use CssContent;
  25. use JavaScriptContent;
  26. use LinkBatch;
  27. use Maintenance;
  28. use ResourceLoader;
  29. use Revision;
  30. use Skin;
  31. use Title;
  32. use User;
  33. use WikiPage;
  34.  
  35. $IP = getenv( 'MW_INSTALL_PATH' );
  36. if ( $IP === false ) {
  37.     $IP = __DIR__ . '/../../..';
  38. }
  39. require_once "$IP/maintenance/Maintenance.php";
  40.  
  41. /**
  42.  * Script to remove manually created user .js and .css pages
  43.  * by users. You should run this script on every wiki where the user
  44.  * has an account.
  45.  */
  46. class RemoveOldManualUserPages extends Maintenance {
  47.  
  48.     /**
  49.      * @var bool
  50.      */
  51.     private $ignoreRevisionLimit;
  52.  
  53.     public function __construct() {
  54.         parent::__construct();
  55.         $this->mDescription = 'Remove reundant user script pages that ' .
  56.             'import global.js and/or global.css';
  57.         $this->addOption( 'user', 'User name', true, true );
  58.         $this->addOption( 'ignorerevisionlimit',
  59.             'Whether to ignore the 1 revision limit', false, false );
  60.         $this->requireExtension( 'GlobalCssJs' );
  61.     }
  62.  
  63.     public function execute() {
  64.         $this->ignoreRevisionLimit = $this->hasOption( 'ignorerevisionlimit' );
  65.         $userName = $this->getOption( 'user' );
  66.         $user = User::newFromName( $userName );
  67.  
  68.         if ( !$user->getId() || !Hooks::loadForUser( $user ) ) {
  69.             $this->output( "$userName does not load global modules on this wiki.\n" );
  70.             return;
  71.         }
  72.  
  73.         $skins = array_keys( Skin::getAllowedSkins() );
  74.         $skins[] = 'common';
  75.  
  76.         // Batch look up the existence of pages
  77.         $lb = new LinkBatch();
  78.         foreach ( $skins as $name ) {
  79.             $lb->addObj( $user->getUserPage()->getSubpage( "$name.js" ) );
  80.             $lb->addObj( $user->getUserPage()->getSubpage( "$name.css" ) );
  81.         }
  82.         $lb->execute();
  83.  
  84.         foreach ( $skins as $name ) {
  85.             $this->removeJS( $user, $name );
  86.             $this->removeCSS( $user, $name );
  87.         }
  88.     }
  89.  
  90.     /**
  91.      * Generic checks to see if we should work on a title.
  92.      *
  93.      * @param Title $title
  94.      * @return Revision|bool
  95.      */
  96.     private function checkTitle( Title $title ) {
  97.         if ( !$title->exists() ) {
  98.             $this->output( "{$title->getPrefixedText()} does not exist on this wiki.\n" );
  99.             return false;
  100.         }
  101.  
  102.         $rev = Revision::newFromTitle( $title );
  103.         if ( !$this->ignoreRevisionLimit &&
  104.             $title->getPreviousRevisionID( $rev->getId() ) !== false
  105.         ) {
  106.             $this->output( "{$title->getPrefixedText()} has more than one revision, skipping.\n" );
  107.             return false;
  108.         }
  109.  
  110.         return $rev;
  111.     }
  112.  
  113.     /**
  114.      * Returns the domain name of the central wiki
  115.      * escaped to use in a regex.
  116.      *
  117.      * @return string
  118.      */
  119.     private function getCentralWikiDomain() {
  120.         global $wgGlobalCssJsConfig;
  121.         $rl = new ResourceLoader;
  122.         $sources = $rl->getSources();
  123.         // Use api.php instead of load.php because it's more likely to be on the same domain
  124.         $api = $sources[$wgGlobalCssJsConfig['source']]['apiScript'];
  125.         $parsed = wfParseUrl( $api );
  126.         return preg_quote( $parsed['host'] );
  127.     }
  128.  
  129.     private function deletePage( Title $title, $reason, $userName ) {
  130.         global $wgUser;
  131.         $page = WikiPage::factory( $title );
  132.         $user = User::newFromName( 'Maintenance script' );
  133.         $user->addGroup( 'bot' );
  134.  
  135.         // For hooks not using RequestContext (e.g. AbuseFilter)
  136.         $wgUser = $user;
  137.         $errors = [];
  138.         $status = $page->doDeleteArticleReal(
  139.             wfMessage( $reason, $userName )->inContentLanguage()->text(),
  140.             false, 0, true, $errors, $user
  141.         );
  142.         if ( $status->isGood() ) {
  143.             $this->output( "{$title->getPrefixedText()} was deleted.\n" );
  144.         } else {
  145.             $this->output( "{$title->getPrefixedText()} could not be deleted:\n" .
  146.                 $status->getWikiText() . "\n" );
  147.         }
  148.     }
  149.  
  150.     /**
  151.      * Given a username, normalize and escape it to be
  152.      * safely used in regex
  153.      *
  154.      * @param string $userName
  155.      * @return string
  156.      */
  157.     public function normalizeUserName( $userName ) {
  158.         $userName = preg_quote( $userName );
  159.         // Spaces can be represented as space, underscore, plus, or %20.
  160.         $userName = str_replace( ' ', '( |_|\+|%20)', $userName );
  161.         return $userName;
  162.     }
  163.  
  164.     public function checkCss( $text, $domain, $userName ) {
  165.         $userName = $this->normalizeUserName( $userName );
  166.         preg_match( "/@import url\('(https?:)?\/\/$domain\/w\/index\.php\?title=User:$userName" .
  167.             "\/global\.css&action=raw&ctype=text\/css'\);/", $text, $matches );
  168.         return isset( $matches[0] ) ? $matches[0] === $text : false;
  169.     }
  170.  
  171.     private function removeCSS( User $user, $skin ) {
  172.         $userName = $user->getName();
  173.         $title = $user->getUserPage()->getSubpage( $skin . '.css' );
  174.         $rev = $this->checkTitle( $title );
  175.         if ( !$rev ) {
  176.             return;
  177.         }
  178.  
  179.         /** @var CssContent $content */
  180.         $content = $rev->getContent();
  181.         $text = trim( $content->getNativeData() );
  182.         $domain = $this->getCentralWikiDomain();
  183.         if ( !$this->checkCss( $text, $domain, $userName ) ) {
  184.             $this->output( "{$title->getPrefixedText()} did not match the specified regular " .
  185.                 "expression. Skipping.\n" );
  186.             return;
  187.         }
  188.  
  189.         // Delete!
  190.         $this->deletePage( $title, 'globalcssjs-delete-css', $userName );
  191.     }
  192.  
  193.     /**
  194.      * Remove lines that are entirely comments, by checking if they start with //
  195.      * Also get rid of empty lines while we're at it.
  196.      *
  197.      * @param string $js
  198.      * @return string
  199.      */
  200.     private function stripComments( $js ) {
  201.         $exploded = explode( "\n", $js );
  202.         $new = [];
  203.         foreach ( $exploded as $line ) {
  204.             $trimmed = trim( $line );
  205.             if ( $trimmed !== '' && substr( $trimmed, 0, 2 ) !== '//' ) {
  206.                 $new[] = $line;
  207.             }
  208.         }
  209.  
  210.         return implode( '\n', $new );
  211.     }
  212.  
  213.     public function checkJs( $text, $domain, $userName ) {
  214.         $text = $this->stripComments( $text );
  215.         $userName = $this->normalizeUserName( $userName );
  216.         preg_match( "/(mw\.loader\.load|importScriptURI)\s*\(\s*('|\")(https?:)?\/\/$domain" .
  217.             "\/w\/index\.php\?title=User:$userName\/global\.js&action=raw&ctype=text\/javascript" .
  218.             "(&smaxage=\d*?)?(&maxage=\d*?)?('|\")\s*\)\s*;?/", $text, $matches );
  219.         return isset( $matches[0] ) ? $matches[0] === $text : false;
  220.     }
  221.  
  222.     private function removeJS( User $user, $skin ) {
  223.         $userName = $user->getName();
  224.         $title = $user->getUserPage()->getSubpage( $skin . '.js' );
  225.         $rev = $this->checkTitle( $title );
  226.         if ( !$rev ) {
  227.             return;
  228.         }
  229.  
  230.         /** @var JavaScriptContent $content */
  231.         $content = $rev->getContent();
  232.         $text = trim( $content->getNativeData() );
  233.         $domain = $this->getCentralWikiDomain();
  234.         if ( !$this->checkJs( $text, $domain, $userName ) ) {
  235.             $this->output( "{$title->getPrefixedText()} did not match the specified regular " .
  236.                 "expression. Skipping.\n" );
  237.             return;
  238.         }
  239.  
  240.         // Delete!
  241.         $this->deletePage( $title, 'globalcssjs-delete-js', $userName );
  242.     }
  243.  
  244. }
  245.  
  246. $maintClass = RemoveOldManualUserPages::class;
  247. require_once RUN_MAINTENANCE_IF_MAIN;
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top