diff --git "a/include/class-cli\\Opts.php" "b/include/class-cli\\Opts.php" index 39f00ee..ee10f6c 100644 --- "a/include/class-cli\\Opts.php" +++ "b/include/class-cli\\Opts.php" @@ -1,190 +1,224 @@ . # command line interface namespace cli; /** * Register command line parameters and read their arguments */ class Opts { /** * All the registered parameters * * @var array */ private $params; /** * Internal flag cache * * @var bool */ private $read = false; /** * Constructor * * @param $params array Parameters */ public function __construct( $params = [] ) { $this->register( $params ); } /** * Static instance */ private static $_instance; /** * Get a singleton instance * * @param $params array * @return self */ public static function instance() { if( ! isset( self::$_instance ) ) { self::$_instance = new self(); } return self::$_instance; } /** * Register multiple parameters * * @param $params array * @return self */ public function register( $params ) { foreach( $params as $param ) { $this->add( $param ); } return $this; } /** * Register a new parameter * * @param $param object Parameter * @return self */ public function add( Param $param ) { $this->params[] = $param; $this->read = false; // invalidate the cache return $this; } /** * Populate all parameters with their arguments * * @see getopt() * @return self */ public function populate() { if( ! $this->read ) { // prepare getopt() syntax $shorts = ''; $longs = []; foreach( $this->params as $opt ) { $shorts .= $opt->getShortOptFormat(); if( $opt->hasLongName() ) { $longs[] = $opt->getLongOptFormat(); } } // get command line arguments $args = getopt( $shorts, $longs ); // fill all parameters with their arguments foreach( $this->params as $opt ) { // try with the short name if( $opt->hasShortName() ) { $name = $opt->getShortName(); if( isset( $args[ $name ] ) ) { $opt->setValue( $opt->isFlag() ? true : $args[ $name ] ); continue; } } // try with the long name if( $opt->hasLongName() ) { $name = $opt->getLongName(); if( isset( $args[ $name ] ) ) { $opt->setValue( $opt->isFlag() ? true : $args[ $name ] ); } } } $this->read = true; } return $this; } /** * Find a registered param from a whatever name, the long or short on * * @param $name string Parameter name (long or short) * @return Param */ protected function findParamFromName( $name ) { foreach( $this->params as $opt ) { if( $opt->isName( $name ) ) { return $opt; } } throw new \InvalidArgumentException( 'unregistered parameter ' . $name ); } /** * Get all the registered parameters * * @return array */ public function getParams() { return $this->params; } /** * Get a single argument * * @param $name string Param name (short or long) * @param $default string Default param value * @return string|null */ public function getArg( $name, $default = null ) { return $this->populate()->findParamFromName( $name )->getValue( $default ); } /** * Get all the unnamed arguments * * @return array */ public static function unnamedArguments() { $args = []; foreach( $GLOBALS[ 'argv' ] as $arg ) { if( '-' !== $arg[ 0 ] ) { $args[] = $arg; } } array_shift( $args ); return $args; } + + /** + * Print all the parameters + */ + public function printParams() { + + foreach( $this->getParams() as $param ) { + + $commands = []; + + if( $param->hasLongName() ) { + $commands[] = '--' . $param->getLongName(); + } + + if( $param->hasShortName() ) { + $commands[] = '-' . $param->getShortName(); + } + + $command = implode( '|', $commands ); + if( $command && ! $param->isFlag() ) { + $command .= $param->isValueOptional() + ? '=[VALUE]' + : '=VALUE'; + } + + printf( ' % -20s ', $command ); + + if( $param->hasDescription() ) { + echo ' ' . $param->getDescription(); + } + + echo "\n"; + } + } } diff --git a/tools/mega-export.php b/tools/mega-export.php index e410cb2..5e2436a 100755 --- a/tools/mega-export.php +++ b/tools/mega-export.php @@ -1,217 +1,201 @@ #!/usr/bin/php . // exit if not CLI $argv or exit( 1 ); // load boz-mw require __DIR__ . '/../autoload.php'; // the number '500' gives to much $DEFAULT_LIMIT = 100; // load configuration include 'config.php'; use \cli\Log; use \cli\Input; use \cli\Opts; use \cli\Param; use \cli\ParamFlag; use \cli\ParamFlagLong; use \cli\ParamValued; use \cli\ParamValuedLong; use \web\MediaWikis; use \mw\API\PageMatcher; // all the available wiki UIDs $mediawiki_uids = []; foreach( MediaWikis::all() as $site ) { $mediawiki_uids[] = $site::UID; } $mediawiki_uids = implode( ', ', $mediawiki_uids ); // register all CLI parameters $opts = new Opts( [ new ParamValuedLong( 'wiki', "Available wikis: $mediawiki_uids" ), new ParamValuedLong( 'limit', "Number of revisions for each request" ), new ParamValuedLong( 'file', "Output filename" ), new ParamFlag( 'help', 'h', "Show this help and quit" ), ] ); $messages = []; // choosen wiki $wiki_uid = $opts->getArg( 'wiki' ); if( !$wiki_uid ) { $messages[] = "Please specify --wiki=WIKI"; } // page titles $page_titles = Opts::unnamedArguments(); if( !$page_titles ) { $messages[] = "Please specify some page titles"; } // output filename $filename = $opts->getArg( 'file' ); if( !$filename ) { $messages[] = "Please specify a filename"; } $limit = (int) $opts->getArg( 'limit', $DEFAULT_LIMIT ); // show the help $show_help = $opts->getArg( 'help' ); if( $show_help ) { $messages = []; } else { $show_help = $messages; } if( $show_help ) { echo "Usage:\n {$argv[ 0 ]} --wiki=WIKI --file=out.xml [OPTIONS] Page_title\n"; echo "Allowed OPTIONS:\n"; - foreach( $opts->getParams() as $param ) { - $commands = []; - if( $param->hasLongName() ) { - $commands[] = '--' . $param->getLongName(); - } - if( $param->hasShortName() ) { - $commands[] = '-' . $param->getShortName(); - } - $command = implode( '|', $commands ); - if( $command && ! $param->isFlag() ) { - $command .= $param->isValueOptional() - ? '=[VALUE]' - : '=VALUE'; - } - printf( ' % -20s ', $command ); - if( $param->hasDescription() ) { - echo ' ' . $param->getDescription(); - } - echo "\n"; - } + + $opts->printParams(); + foreach( $messages as $msg ) { echo "\nError: $msg"; } echo "\n"; exit( $opts->getArg( 'help' ) ? 0 : 1 ); } // try to open the file $file = fopen( $filename, 'w' ); if( !$file ) { Log::error( "Can't open file '$filename'" ); exit( 1 ); } $wiki = MediaWikis::findFromUID( $wiki_uid ); $wiki->login(); $requests = $wiki->createQuery( [ 'action' => 'query', 'titles' => $page_titles, 'prop' => 'revisions', 'rvprop' => [ 'ids', 'flags', 'timestamp', 'user', 'userid', 'size', 'slotsize', 'sha1', 'comment', 'content', ], 'rvslots' => 'main', 'rvlimit' => $limit, ] ); // total number of revisions $total = 0; // do not print to the out $out = '' . "\n"; foreach( $requests as $request ) { foreach( $request->query->pages as $page ) { if( isset( $page->missing ) ) { Log::error( "Page '{$page->title}' is missing" ); exit( 1 ); } $alert_much_revisions = true; foreach( $page->revisions as $i => $revision ) { // avoid nonsense revisions if( empty( $revision->comment ) ) { if( $alert_much_revisions ) { $count = count( $page->revisions ); if( $count !== $limit ) { Log::warn( "response with $count revisions instead of $limit: consider to lower your limit" ); $alert_much_revisions = false; } } continue; } $total++; foreach( $revision->slots as $slot ) { // avoid nonsense slots if( empty( $slot->contentmodel ) ) { continue; } $safe_user = htmlentities( $revision->user ); $safe_userid = htmlentities( $revision->userid ); $safe_comment = htmlentities( $revision->comment ); $safe_model = htmlentities( $slot->contentmodel ); $safe_format = htmlentities( $slot->contentformat ); $safe_text = htmlentities( $slot->{'*'} ); $out .= "\n"; $out .= "\t{$revision->revid}\n"; $out .= "\t{$revision->parentid}\n"; $out .= "\t{$revision->timestamp}\n"; $out .= "\t\n"; $out .= "\t\t$safe_user\n"; $out .= "\t\t$safe_userid\n"; $out .= "\t\n"; $out .= "\t$safe_comment ?>"; $out .= "\t$safe_model\n"; $out .= "\t$safe_format\n"; $out .= "\tsize}\">$safe_text\n"; $out .= "\t{$revision->sha1}\n"; $out .= "\n"; } } } // write the file in chunks fwrite( $file, $out ); $out = ''; } Log::info( "you mega-exported $total revisions! nice shot!" ); fwrite( $file, "\n" ); fclose( $file ); diff --git a/tools/replace.php b/tools/replace.php index 3e1b04b..d6a7b93 100755 --- a/tools/replace.php +++ b/tools/replace.php @@ -1,282 +1,262 @@ #!/usr/bin/php . // exit if not CLI $argv or exit( 1 ); // default seconds to wait in always mode $DEFAULT_ALWAYS_WAIT = 3; // load boz-mw require __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'autoload.php'; // load configuration include 'config.php'; use \cli\Log; use \cli\Input; use \cli\Opts; use \cli\Param; use \cli\ParamFlag; use \cli\ParamFlagLong; use \cli\ParamValued; use \cli\ParamValuedLong; use \web\MediaWikis; // all the available wiki UIDs $mediawiki_uids = []; foreach( MediaWikis::all() as $site ) { $mediawiki_uids[] = $site::UID; } $mediawiki_uids = implode( ', ', $mediawiki_uids ); // these classes are useful only to distinguish this type of params :^) class APIParam extends ParamValuedLong {} class APIParamSub extends APIParam {} // register all CLI parameters $opts = new Opts( [ new ParamValuedLong( 'wiki', "Available wikis: $mediawiki_uids" ), new APIParam( 'generator', 'Choose between: linkshere, categorymembers, transcludedin, search' ), new APIParamSub( 'glhnamespace', 'Only in linkshere: Namespace number' ), new APIParamSub( 'gtinamespace', ' transcludedin: Namespace number' ), new APIParamSub( 'gcmtitle', ' Category name prefixed' ), new APIParamSub( 'gcmpageid', ' Category page ID' ), new APIParamSub( 'gcmnamespace', ' categorymembers: Namespace number' ), new APIParamSub( 'gsrsearch', ' search: Search term' ), new APIParamSub( 'gsrnamespace', ' Namespace number' ), new APIParam( 'titles', 'Title of pages to work on' ), new APIParam( 'pageids', 'Page IDs to work on' ), new ParamFlagLong( 'plain', 'Use plain text instead of regexes (default)' ), new ParamFlagLong( 'regex', 'Use regexes instead of plain text' ), new ParamValued( 'summary', 'm', 'Specify an edit summary' ), new ParamValuedLong( 'limit', 'Maximum number of replacements for each SEARCH' ), new ParamFlagLong( 'not-minor', 'Do not mark this change as minor' ), new ParamFlagLong( 'not-bot', 'Do not mark this change as edited by a bot' ), new ParamValuedLong( 'rvsection', 'Number of section to be edited' ), new ParamFlagLong( 'always', 'Always run without asking y/n' ), new ParamValuedLong( 'always-wait', "Seconds to wait during --always mode (default $DEFAULT_ALWAYS_WAIT)" ), new ParamFlagLong( 'simulate', 'Show changes without saving' ), new ParamFlagLong( 'show', 'Show the wikitext before saving' ), new ParamFlag( 'help', 'h', 'Show this help and quit' ), ] ); // unnamed arguments $main_args = Opts::unnamedArguments(); // wait seconds for --always $ALWAYS_WAIT = (int) $opts->getArg( 'always-wait', $DEFAULT_ALWAYS_WAIT ); // operate using regex or in plain text? $IS_PLAIN = $opts->getArg( 'plain', true ); $IS_REGEX = $opts->getArg( 'regex', false ); // maximum number of replacements for each LIMIT $LIMIT = (int) $opts->getArg( 'limit', -1 ); // show the help? $help = (bool) $opts->getArg( 'help' ); // the wiki is mandatory if( ! $help && ! $opts->getArg( 'wiki' ) ) { $help = "Please specify a --wiki=VALUE"; } // pick the wiki $wiki = MediaWikis::findFromUID( $opts->getArg( 'wiki' ) ); if( ! $help && ! $wiki ) { $help = "Please specify a valid wiki UID from: $mediawiki_uids"; } // the generator is mandatory if( ! $help && ! $opts->getArg( 'generator' ) ) { $help = "Please specify the --generator=VALUE"; } // the search and replace are mandatory if( ! $help && ! $main_args ) { $help = "Please specify a SEARCH and a REPLACE"; } // every search must have its replace if( ! $help && count( $main_args ) % 2 !== 0 ) { $last = $main_args[ count( $main_args ) - 1 ]; $help = "Please specify a REPLACE for your latest SEARCH ($last)"; } // plain vs regex if( ! $help && null === $IS_REGEX ) { $help = "Please specify if it's --plain or it's --regex"; } // plain with regex if( ! $help && $opts->getArg( 'regex' ) && $opts->getArg( 'plain' ) ) { $help = "Please do not specify both --regex or --plain"; } // show the help if( $help ) { echo "Usage:\n {$argv[ 0 ]} [OPTIONS] SEARCH... REPLACE...\n"; echo "Allowed OPTIONS:\n"; - foreach( $opts->getParams() as $param ) { - $commands = []; - if( $param->hasLongName() ) { - $commands[] = '--' . $param->getLongName(); - } - if( $param->hasShortName() ) { - $commands[] = '-' . $param->getShortName(); - } - $command = implode( '|', $commands ); - if( $command && ! $param->isFlag() ) { - $command .= $param->isValueOptional() - ? '=[VALUE]' - : '=VALUE'; - } - if( $param instanceof APIParamSub ) { - echo ' '; - } - printf( ' % -20s ', $command ); - if( $param->hasDescription() ) { - echo ' ' . $param->getDescription(); - } - echo "\n"; - } + + $opts->printParams(); + echo "Example:\n {$argv[ 0 ]} --wiki=itwiki --generator=allpages 'a' 'afa'\n"; if( is_string( $help ) ) { echo "\nError: $help\n"; } exit( $opts->getArg( 'help' ) ? 0 : 1 ); } // API arguments $args = [ 'action' => 'query', 'prop' => 'revisions', 'rvslots' => 'main', 'rvprop' => [ 'content', 'timestamp', ] ]; // apply API arguments from CLI API parameters foreach( $opts->getParams() as $param ) { if( $param instanceof APIParam ) { $value = $param->getValue(); if( $value ) { $args[ $param->getLongName() ] = $value; } } } $wiki->login(); // do the API query with continuation support $results = $wiki->createQuery( $args ); foreach( $results->getGenerator() as $response ) { if( ! isset( $response->query ) ) { Log::error( 'empty API output (have you specified nothing?) try --help' ); exit( 1 ); } foreach( $response->query->pages as $page ) { // show the page title Log::info( "Page [[{$page->title}]]" ); // populate the slot with backward compatibility to MediaWiki <= 1.32 $main_slot = $page->revisions[ 0 ]; if( isset( $main_slot->slots->main ) ) { $main_slot = $main_slot->slots->main; } // Wikitext object $wikitext = $wiki->createWikitext( $main_slot->{ '*' } ); // apply search and replacements $n = count( $main_args ); for( $i = 0; $i < $n; $i += 2 ) { $search = $main_args[ $i ]; $replace = $main_args[ $i + 1 ]; if( $IS_REGEX ) { $wikitext->pregReplace( $search, $replace, $LIMIT ); } else { $wikitext->strReplace( $search, $replace, $LIMIT ); } } // eventually show the wikitext if( $opts->getArg( 'show' ) ) { Log::info( "Wikitext:\n{$wikitext->getWikitext()}" ); } // are there changes? if( $wikitext->getSobstitutions() ) { // show changes Log::info( 'Changes:' ); foreach( $wikitext->getUniqueSobstitutions() as $change ) { list( $a, $b, $n ) = $change; Log::info( sprintf( '% 40s → % 40s (%d times)', $a, $b, $n ) ); } // edit summary $summary = $opts->getArg( 'summary', 'Bot: changed ' . implode( ', ', $wikitext->getHumanUniqueSobstitutions() ) ); // show the summary if it's auto-generated if( ! $opts->getArg( 'summary' ) ) { Log::info( "Summary: \t $summary" ); } if( ! $opts->getArg( 'simulate' ) ) { $proceed = true; if( $opts->getArg( 'always' ) && $ALWAYS_WAIT ) { Log::info( "Waiting $ALWAYS_WAIT seconds before saving", [ 'newline' => false ] ); for( $i = 0; $i < $ALWAYS_WAIT; $i++ ) { echo "."; sleep( 1 ); } echo "\n"; } else { $proceed = 'y' === Input::yesNoQuestion( "Save page [[{$page->title}]]?" ); } if( $proceed ) { // save $wiki->edit( [ 'pageid' => $page->pageid, 'basetimestamp' => $page->revisions[ 0 ]->timestamp, 'text' => $wikitext->getWikitext(), 'section' => $opts->getArg( 'rvsection' ), 'summary' => $summary, 'minor' => ! $opts->getArg( 'not-minor' ), 'bot' => ! $opts->getArg( 'not-bot' ), ] ); Log::info( 'saved' ); } else { Log::info( 'not saved' ); } } } else { Log::info( 'no changes' ); } } }