about summary refs log tree commit diff stats
path: root/wiki/bin
diff options
context:
space:
mode:
authorahriman <ahriman@falte.red>2018-12-03 19:22:25 -0500
committerahriman <ahriman@falte.red>2018-12-03 19:22:25 -0500
commit0ae8cbf5c0b1a198b963490985b7738392ebcb97 (patch)
treeb2c77ae72c6b717e2b97492065196ac5ffb2d9e2 /wiki/bin
parentf57f6cc5a2d159f90168d292437dc4bd8cd7f934 (diff)
downloadsite-0ae8cbf5c0b1a198b963490985b7738392ebcb97.tar.gz
installed dokuwiki, added to navbar, updated news
Diffstat (limited to 'wiki/bin')
-rw-r--r--wiki/bin/.htaccess7
-rwxr-xr-xwiki/bin/dwpage.php322
-rwxr-xr-xwiki/bin/gittool.php340
-rwxr-xr-xwiki/bin/indexer.php107
-rwxr-xr-xwiki/bin/plugin.php103
-rwxr-xr-xwiki/bin/render.php64
-rwxr-xr-xwiki/bin/striplangs.php114
-rwxr-xr-xwiki/bin/wantedpages.php186
8 files changed, 1243 insertions, 0 deletions
diff --git a/wiki/bin/.htaccess b/wiki/bin/.htaccess
new file mode 100644
index 0000000..5f279f1
--- /dev/null
+++ b/wiki/bin/.htaccess
@@ -0,0 +1,7 @@
+<IfModule mod_authz_host>
+    Require all denied
+</IfModule>
+<IfModule !mod_authz_host>
+    Order allow,deny
+    Deny from all
+</IfModule>
diff --git a/wiki/bin/dwpage.php b/wiki/bin/dwpage.php
new file mode 100755
index 0000000..3124563
--- /dev/null
+++ b/wiki/bin/dwpage.php
@@ -0,0 +1,322 @@
+#!/usr/bin/php
+<?php
+
+use splitbrain\phpcli\CLI;
+use splitbrain\phpcli\Options;
+
+if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
+define('NOSESSION', 1);
+require_once(DOKU_INC . 'inc/init.php');
+
+/**
+ * Checkout and commit pages from the command line while maintaining the history
+ */
+class PageCLI extends CLI {
+
+    protected $force = false;
+    protected $username = '';
+
+    /**
+     * Register options and arguments on the given $options object
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function setup(Options $options) {
+        /* global */
+        $options->registerOption(
+            'force',
+            'force obtaining a lock for the page (generally bad idea)',
+            'f'
+        );
+        $options->registerOption(
+            'user',
+            'work as this user. defaults to current CLI user',
+            'u',
+            'username'
+        );
+        $options->setHelp(
+            'Utility to help command line Dokuwiki page editing, allow ' .
+            'pages to be checked out for editing then committed after changes'
+        );
+
+        /* checkout command */
+        $options->registerCommand(
+            'checkout',
+            'Checks out a file from the repository, using the wiki id and obtaining ' .
+            'a lock for the page. ' . "\n" .
+            'If a working_file is specified, this is where the page is copied to. ' .
+            'Otherwise defaults to the same as the wiki page in the current ' .
+            'working directory.'
+        );
+        $options->registerArgument(
+            'wikipage',
+            'The wiki page to checkout',
+            true,
+            'checkout'
+        );
+        $options->registerArgument(
+            'workingfile',
+            'How to name the local checkout',
+            false,
+            'checkout'
+        );
+
+        /* commit command */
+        $options->registerCommand(
+            'commit',
+            'Checks in the working_file into the repository using the specified ' .
+            'wiki id, archiving the previous version.'
+        );
+        $options->registerArgument(
+            'workingfile',
+            'The local file to commit',
+            true,
+            'commit'
+        );
+        $options->registerArgument(
+            'wikipage',
+            'The wiki page to create or update',
+            true,
+            'commit'
+        );
+        $options->registerOption(
+            'message',
+            'Summary describing the change (required)',
+            'm',
+            'summary',
+            'commit'
+        );
+        $options->registerOption(
+            'trivial',
+            'minor change',
+            't',
+            false,
+            'commit'
+        );
+
+        /* lock command */
+        $options->registerCommand(
+            'lock',
+            'Obtains or updates a lock for a wiki page'
+        );
+        $options->registerArgument(
+            'wikipage',
+            'The wiki page to lock',
+            true,
+            'lock'
+        );
+
+        /* unlock command */
+        $options->registerCommand(
+            'unlock',
+            'Removes a lock for a wiki page.'
+        );
+        $options->registerArgument(
+            'wikipage',
+            'The wiki page to unlock',
+            true,
+            'unlock'
+        );
+    }
+
+    /**
+     * Your main program
+     *
+     * Arguments and options have been parsed when this is run
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function main(Options $options) {
+        $this->force = $options->getOpt('force', false);
+        $this->username = $options->getOpt('user', $this->getUser());
+
+        $command = $options->getCmd();
+        $args = $options->getArgs();
+        switch($command) {
+            case 'checkout':
+                $wiki_id = array_shift($args);
+                $localfile = array_shift($args);
+                $this->commandCheckout($wiki_id, $localfile);
+                break;
+            case 'commit':
+                $localfile = array_shift($args);
+                $wiki_id = array_shift($args);
+                $this->commandCommit(
+                    $localfile,
+                    $wiki_id,
+                    $options->getOpt('message', ''),
+                    $options->getOpt('trivial', false)
+                );
+                break;
+            case 'lock':
+                $wiki_id = array_shift($args);
+                $this->obtainLock($wiki_id);
+                $this->success("$wiki_id locked");
+                break;
+            case 'unlock':
+                $wiki_id = array_shift($args);
+                $this->clearLock($wiki_id);
+                $this->success("$wiki_id unlocked");
+                break;
+            default:
+                echo $options->help();
+        }
+    }
+
+    /**
+     * Check out a file
+     *
+     * @param string $wiki_id
+     * @param string $localfile
+     */
+    protected function commandCheckout($wiki_id, $localfile) {
+        global $conf;
+
+        $wiki_id = cleanID($wiki_id);
+        $wiki_fn = wikiFN($wiki_id);
+
+        if(!file_exists($wiki_fn)) {
+            $this->fatal("$wiki_id does not yet exist");
+        }
+
+        if(empty($localfile)) {
+            $localfile = getcwd() . '/' . utf8_basename($wiki_fn);
+        }
+
+        if(!file_exists(dirname($localfile))) {
+            $this->fatal("Directory " . dirname($localfile) . " does not exist");
+        }
+
+        if(stristr(realpath(dirname($localfile)), realpath($conf['datadir'])) !== false) {
+            $this->fatal("Attempt to check out file into data directory - not allowed");
+        }
+
+        $this->obtainLock($wiki_id);
+
+        if(!copy($wiki_fn, $localfile)) {
+            $this->clearLock($wiki_id);
+            $this->fatal("Unable to copy $wiki_fn to $localfile");
+        }
+
+        $this->success("$wiki_id > $localfile");
+    }
+
+    /**
+     * Save a file as a new page revision
+     *
+     * @param string $localfile
+     * @param string $wiki_id
+     * @param string $message
+     * @param bool $minor
+     */
+    protected function commandCommit($localfile, $wiki_id, $message, $minor) {
+        $wiki_id = cleanID($wiki_id);
+        $message = trim($message);
+
+        if(!file_exists($localfile)) {
+            $this->fatal("$localfile does not exist");
+        }
+
+        if(!is_readable($localfile)) {
+            $this->fatal("Cannot read from $localfile");
+        }
+
+        if(!$message) {
+            $this->fatal("Summary message required");
+        }
+
+        $this->obtainLock($wiki_id);
+
+        saveWikiText($wiki_id, file_get_contents($localfile), $message, $minor);
+
+        $this->clearLock($wiki_id);
+
+        $this->success("$localfile > $wiki_id");
+    }
+
+    /**
+     * Lock the given page or exit
+     *
+     * @param string $wiki_id
+     */
+    protected function obtainLock($wiki_id) {
+        if($this->force) $this->deleteLock($wiki_id);
+
+        $_SERVER['REMOTE_USER'] = $this->username;
+
+        if(checklock($wiki_id)) {
+            $this->error("Page $wiki_id is already locked by another user");
+            exit(1);
+        }
+
+        lock($wiki_id);
+
+        if(checklock($wiki_id)) {
+            $this->error("Unable to obtain lock for $wiki_id ");
+            var_dump(checklock($wiki_id));
+            exit(1);
+        }
+    }
+
+    /**
+     * Clear the lock on the given page
+     *
+     * @param string $wiki_id
+     */
+    protected function clearLock($wiki_id) {
+        if($this->force) $this->deleteLock($wiki_id);
+
+        $_SERVER['REMOTE_USER'] = $this->username;
+        if(checklock($wiki_id)) {
+            $this->error("Page $wiki_id is locked by another user");
+            exit(1);
+        }
+
+        unlock($wiki_id);
+
+        if(file_exists(wikiLockFN($wiki_id))) {
+            $this->error("Unable to clear lock for $wiki_id");
+            exit(1);
+        }
+    }
+
+    /**
+     * Forcefully remove a lock on the page given
+     *
+     * @param string $wiki_id
+     */
+    protected function deleteLock($wiki_id) {
+        $wikiLockFN = wikiLockFN($wiki_id);
+
+        if(file_exists($wikiLockFN)) {
+            if(!unlink($wikiLockFN)) {
+                $this->error("Unable to delete $wikiLockFN");
+                exit(1);
+            }
+        }
+    }
+
+    /**
+     * Get the current user's username from the environment
+     *
+     * @return string
+     */
+    protected function getUser() {
+        $user = getenv('USER');
+        if(empty ($user)) {
+            $user = getenv('USERNAME');
+        } else {
+            return $user;
+        }
+        if(empty ($user)) {
+            $user = 'admin';
+        }
+        return $user;
+    }
+}
+
+// Main
+$cli = new PageCLI();
+$cli->run();
diff --git a/wiki/bin/gittool.php b/wiki/bin/gittool.php
new file mode 100755
index 0000000..63d5b44
--- /dev/null
+++ b/wiki/bin/gittool.php
@@ -0,0 +1,340 @@
+#!/usr/bin/php
+<?php
+
+use splitbrain\phpcli\CLI;
+use splitbrain\phpcli\Options;
+
+if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
+define('NOSESSION', 1);
+require_once(DOKU_INC . 'inc/init.php');
+
+/**
+ * Easily manage DokuWiki git repositories
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+class GitToolCLI extends CLI {
+
+    /**
+     * Register options and arguments on the given $options object
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function setup(Options $options) {
+        $options->setHelp(
+            "Manage git repositories for DokuWiki and its plugins and templates.\n\n" .
+            "$> ./bin/gittool.php clone gallery template:ach\n" .
+            "$> ./bin/gittool.php repos\n" .
+            "$> ./bin/gittool.php origin -v"
+        );
+
+        $options->registerArgument(
+            'command',
+            'Command to execute. See below',
+            true
+        );
+
+        $options->registerCommand(
+            'clone',
+            'Tries to install a known plugin or template (prefix with template:) via git. Uses the DokuWiki.org ' .
+            'plugin repository to find the proper git repository. Multiple extensions can be given as parameters'
+        );
+        $options->registerArgument(
+            'extension',
+            'name of the extension to install, prefix with \'template:\' for templates',
+            true,
+            'clone'
+        );
+
+        $options->registerCommand(
+            'install',
+            'The same as clone, but when no git source repository can be found, the extension is installed via ' .
+            'download'
+        );
+        $options->registerArgument(
+            'extension',
+            'name of the extension to install, prefix with \'template:\' for templates',
+            true,
+            'install'
+        );
+
+        $options->registerCommand(
+            'repos',
+            'Lists all git repositories found in this DokuWiki installation'
+        );
+
+        $options->registerCommand(
+            '*',
+            'Any unknown commands are assumed to be arguments to git and will be executed in all repositories ' .
+            'found within this DokuWiki installation'
+        );
+    }
+
+    /**
+     * Your main program
+     *
+     * Arguments and options have been parsed when this is run
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function main(Options $options) {
+        $command = $options->getCmd();
+        $args = $options->getArgs();
+        if(!$command) $command = array_shift($args);
+
+        switch($command) {
+            case '':
+                echo $options->help();
+                break;
+            case 'clone':
+                $this->cmd_clone($args);
+                break;
+            case 'install':
+                $this->cmd_install($args);
+                break;
+            case 'repo':
+            case 'repos':
+                $this->cmd_repos();
+                break;
+            default:
+                $this->cmd_git($command, $args);
+        }
+    }
+
+    /**
+     * Tries to install the given extensions using git clone
+     *
+     * @param array $extensions
+     */
+    public function cmd_clone($extensions) {
+        $errors = array();
+        $succeeded = array();
+
+        foreach($extensions as $ext) {
+            $repo = $this->getSourceRepo($ext);
+
+            if(!$repo) {
+                $this->error("could not find a repository for $ext");
+                $errors[] = $ext;
+            } else {
+                if($this->cloneExtension($ext, $repo)) {
+                    $succeeded[] = $ext;
+                } else {
+                    $errors[] = $ext;
+                }
+            }
+        }
+
+        echo "\n";
+        if($succeeded) $this->success('successfully cloned the following extensions: ' . join(', ', $succeeded));
+        if($errors) $this->error('failed to clone the following extensions: ' . join(', ', $errors));
+    }
+
+    /**
+     * Tries to install the given extensions using git clone with fallback to install
+     *
+     * @param array $extensions
+     */
+    public function cmd_install($extensions) {
+        $errors = array();
+        $succeeded = array();
+
+        foreach($extensions as $ext) {
+            $repo = $this->getSourceRepo($ext);
+
+            if(!$repo) {
+                $this->info("could not find a repository for $ext");
+                if($this->downloadExtension($ext)) {
+                    $succeeded[] = $ext;
+                } else {
+                    $errors[] = $ext;
+                }
+            } else {
+                if($this->cloneExtension($ext, $repo)) {
+                    $succeeded[] = $ext;
+                } else {
+                    $errors[] = $ext;
+                }
+            }
+        }
+
+        echo "\n";
+        if($succeeded) $this->success('successfully installed the following extensions: ' . join(', ', $succeeded));
+        if($errors) $this->error('failed to install the following extensions: ' . join(', ', $errors));
+    }
+
+    /**
+     * Executes the given git command in every repository
+     *
+     * @param $cmd
+     * @param $arg
+     */
+    public function cmd_git($cmd, $arg) {
+        $repos = $this->findRepos();
+
+        $shell = array_merge(array('git', $cmd), $arg);
+        $shell = array_map('escapeshellarg', $shell);
+        $shell = join(' ', $shell);
+
+        foreach($repos as $repo) {
+            if(!@chdir($repo)) {
+                $this->error("Could not change into $repo");
+                continue;
+            }
+
+            $this->info("executing $shell in $repo");
+            $ret = 0;
+            system($shell, $ret);
+
+            if($ret == 0) {
+                $this->success("git succeeded in $repo");
+            } else {
+                $this->error("git failed in $repo");
+            }
+        }
+    }
+
+    /**
+     * Simply lists the repositories
+     */
+    public function cmd_repos() {
+        $repos = $this->findRepos();
+        foreach($repos as $repo) {
+            echo "$repo\n";
+        }
+    }
+
+    /**
+     * Install extension from the given download URL
+     *
+     * @param string $ext
+     * @return bool|null
+     */
+    private function downloadExtension($ext) {
+        /** @var helper_plugin_extension_extension $plugin */
+        $plugin = plugin_load('helper', 'extension_extension');
+        if(!$ext) die("extension plugin not available, can't continue");
+
+        $plugin->setExtension($ext);
+
+        $url = $plugin->getDownloadURL();
+        if(!$url) {
+            $this->error("no download URL for $ext");
+            return false;
+        }
+
+        $ok = false;
+        try {
+            $this->info("installing $ext via download from $url");
+            $ok = $plugin->installFromURL($url);
+        } catch(Exception $e) {
+            $this->error($e->getMessage());
+        }
+
+        if($ok) {
+            $this->success("installed $ext via download");
+            return true;
+        } else {
+            $this->success("failed to install $ext via download");
+            return false;
+        }
+    }
+
+    /**
+     * Clones the extension from the given repository
+     *
+     * @param string $ext
+     * @param string $repo
+     * @return bool
+     */
+    private function cloneExtension($ext, $repo) {
+        if(substr($ext, 0, 9) == 'template:') {
+            $target = fullpath(tpl_incdir() . '../' . substr($ext, 9));
+        } else {
+            $target = DOKU_PLUGIN . $ext;
+        }
+
+        $this->info("cloning $ext from $repo to $target");
+        $ret = 0;
+        system("git clone $repo $target", $ret);
+        if($ret === 0) {
+            $this->success("cloning of $ext succeeded");
+            return true;
+        } else {
+            $this->error("cloning of $ext failed");
+            return false;
+        }
+    }
+
+    /**
+     * Returns all git repositories in this DokuWiki install
+     *
+     * Looks in root, template and plugin directories only.
+     *
+     * @return array
+     */
+    private function findRepos() {
+        $this->info('Looking for .git directories');
+        $data = array_merge(
+            glob(DOKU_INC . '.git', GLOB_ONLYDIR),
+            glob(DOKU_PLUGIN . '*/.git', GLOB_ONLYDIR),
+            glob(fullpath(tpl_incdir() . '../') . '/*/.git', GLOB_ONLYDIR)
+        );
+
+        if(!$data) {
+            $this->error('Found no .git directories');
+        } else {
+            $this->success('Found ' . count($data) . ' .git directories');
+        }
+        $data = array_map('fullpath', array_map('dirname', $data));
+        return $data;
+    }
+
+    /**
+     * Returns the repository for the given extension
+     *
+     * @param $extension
+     * @return false|string
+     */
+    private function getSourceRepo($extension) {
+        /** @var helper_plugin_extension_extension $ext */
+        $ext = plugin_load('helper', 'extension_extension');
+        if(!$ext) die("extension plugin not available, can't continue");
+
+        $ext->setExtension($extension);
+
+        $repourl = $ext->getSourcerepoURL();
+        if(!$repourl) return false;
+
+        // match github repos
+        if(preg_match('/github\.com\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) {
+            $user = $m[1];
+            $repo = $m[2];
+            return 'https://github.com/' . $user . '/' . $repo . '.git';
+        }
+
+        // match gitorious repos
+        if(preg_match('/gitorious.org\/([^\/]+)\/([^\/]+)?/i', $repourl, $m)) {
+            $user = $m[1];
+            $repo = $m[2];
+            if(!$repo) $repo = $user;
+
+            return 'https://git.gitorious.org/' . $user . '/' . $repo . '.git';
+        }
+
+        // match bitbucket repos - most people seem to use mercurial there though
+        if(preg_match('/bitbucket\.org\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) {
+            $user = $m[1];
+            $repo = $m[2];
+            return 'https://bitbucket.org/' . $user . '/' . $repo . '.git';
+        }
+
+        return false;
+    }
+}
+
+// Main
+$cli = new GitToolCLI();
+$cli->run();
diff --git a/wiki/bin/indexer.php b/wiki/bin/indexer.php
new file mode 100755
index 0000000..4d19a95
--- /dev/null
+++ b/wiki/bin/indexer.php
@@ -0,0 +1,107 @@
+#!/usr/bin/php
+<?php
+
+use splitbrain\phpcli\CLI;
+use splitbrain\phpcli\Options;
+
+if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
+define('NOSESSION', 1);
+require_once(DOKU_INC . 'inc/init.php');
+
+/**
+ * Update the Search Index from command line
+ */
+class IndexerCLI extends CLI {
+
+    private $quiet = false;
+    private $clear = false;
+
+    /**
+     * Register options and arguments on the given $options object
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function setup(Options $options) {
+        $options->setHelp(
+            'Updates the searchindex by indexing all new or changed pages. When the -c option is ' .
+            'given the index is cleared first.'
+        );
+
+        $options->registerOption(
+            'clear',
+            'clear the index before updating',
+            'c'
+        );
+        $options->registerOption(
+            'quiet',
+            'don\'t produce any output',
+            'q'
+        );
+    }
+
+    /**
+     * Your main program
+     *
+     * Arguments and options have been parsed when this is run
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function main(Options $options) {
+        $this->clear = $options->getOpt('clear');
+        $this->quiet = $options->getOpt('quiet');
+
+        if($this->clear) $this->clearindex();
+
+        $this->update();
+    }
+
+    /**
+     * Update the index
+     */
+    function update() {
+        global $conf;
+        $data = array();
+        $this->quietecho("Searching pages... ");
+        search($data, $conf['datadir'], 'search_allpages', array('skipacl' => true));
+        $this->quietecho(count($data) . " pages found.\n");
+
+        foreach($data as $val) {
+            $this->index($val['id']);
+        }
+    }
+
+    /**
+     * Index the given page
+     *
+     * @param string $id
+     */
+    function index($id) {
+        $this->quietecho("$id... ");
+        idx_addPage($id, !$this->quiet, $this->clear);
+        $this->quietecho("done.\n");
+    }
+
+    /**
+     * Clear all index files
+     */
+    function clearindex() {
+        $this->quietecho("Clearing index... ");
+        idx_get_indexer()->clear();
+        $this->quietecho("done.\n");
+    }
+
+    /**
+     * Print message if not supressed
+     *
+     * @param string $msg
+     */
+    function quietecho($msg) {
+        if(!$this->quiet) echo $msg;
+    }
+}
+
+// Main
+$cli = new IndexerCLI();
+$cli->run();
diff --git a/wiki/bin/plugin.php b/wiki/bin/plugin.php
new file mode 100755
index 0000000..cbe0b1f
--- /dev/null
+++ b/wiki/bin/plugin.php
@@ -0,0 +1,103 @@
+#!/usr/bin/php
+<?php
+
+use splitbrain\phpcli\CLI;
+use splitbrain\phpcli\Colors;
+use splitbrain\phpcli\Options;
+
+if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
+define('NOSESSION', 1);
+require_once(DOKU_INC . 'inc/init.php');
+
+class PluginCLI extends CLI {
+
+    /**
+     * Register options and arguments on the given $options object
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function setup(Options $options) {
+        $options->setHelp('Excecutes Plugin command line tools');
+        $options->registerArgument('plugin', 'The plugin CLI you want to run. Leave off to see list', false);
+    }
+
+    /**
+     * Your main program
+     *
+     * Arguments and options have been parsed when this is run
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function main(Options $options) {
+        global $argv;
+        $argv = $options->getArgs();
+
+        if($argv) {
+            $plugin = $this->loadPlugin($argv[0]);
+            if($plugin !== null) {
+                $plugin->run();
+            } else {
+                $this->fatal('Command {cmd} not found.', ['cmd' => $argv[0]]);
+            }
+        } else {
+            echo $options->help();
+            $this->listPlugins();
+        }
+    }
+
+    /**
+     * List available plugins
+     */
+    protected function listPlugins() {
+        /** @var Doku_Plugin_Controller $plugin_controller */
+        global $plugin_controller;
+
+        echo "\n";
+        echo "\n";
+        echo $this->colors->wrap('AVAILABLE PLUGINS:', Colors::C_BROWN);
+        echo "\n";
+
+        $list = $plugin_controller->getList('cli');
+        sort($list);
+        if(!count($list)) {
+            echo $this->colors->wrap("  No plugins providing CLI components available\n", Colors::C_RED);
+        } else {
+            $tf = new \splitbrain\phpcli\TableFormatter($this->colors);
+
+            foreach($list as $name) {
+                $plugin = $this->loadPlugin($name);
+                if($plugin === null) continue;
+                $info = $plugin->getInfo();
+
+                echo $tf->format(
+                    [2, '30%', '*'],
+                    ['', $name, $info['desc']],
+                    ['', Colors::C_CYAN, '']
+
+                );
+            }
+        }
+    }
+
+    /**
+     * Instantiate a CLI plugin
+     *
+     * @param string $name
+     * @return DokuWiki_CLI_Plugin|null
+     */
+    protected
+    function loadPlugin($name) {
+        // execute the plugin CLI
+        $class = "cli_plugin_$name";
+        if(class_exists($class)) {
+            return new $class();
+        }
+        return null;
+    }
+}
+
+// Main
+$cli = new PluginCLI();
+$cli->run();
diff --git a/wiki/bin/render.php b/wiki/bin/render.php
new file mode 100755
index 0000000..cc4a003
--- /dev/null
+++ b/wiki/bin/render.php
@@ -0,0 +1,64 @@
+#!/usr/bin/php
+<?php
+
+use splitbrain\phpcli\CLI;
+use splitbrain\phpcli\Options;
+
+if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
+define('NOSESSION', 1);
+require_once(DOKU_INC . 'inc/init.php');
+
+/**
+ * A simple commandline tool to render some DokuWiki syntax with a given
+ * renderer.
+ *
+ * This may not work for plugins that expect a certain environment to be
+ * set up before rendering, but should work for most or even all standard
+ * DokuWiki markup
+ *
+ * @license GPL2
+ * @author  Andreas Gohr <andi@splitbrain.org>
+ */
+class RenderCLI extends CLI {
+
+    /**
+     * Register options and arguments on the given $options object
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function setup(Options $options) {
+        $options->setHelp(
+            'A simple commandline tool to render some DokuWiki syntax with a given renderer.' .
+            "\n\n" .
+            'This may not work for plugins that expect a certain environment to be ' .
+            'set up before rendering, but should work for most or even all standard ' .
+            'DokuWiki markup'
+        );
+        $options->registerOption('renderer', 'The renderer mode to use. Defaults to xhtml', 'r', 'mode');
+    }
+
+    /**
+     * Your main program
+     *
+     * Arguments and options have been parsed when this is run
+     *
+     * @param Options $options
+     * @throws DokuCLI_Exception
+     * @return void
+     */
+    protected function main(Options $options) {
+        $renderer = $options->getOpt('renderer', 'xhtml');
+
+        // do the action
+        $source = stream_get_contents(STDIN);
+        $info = array();
+        $result = p_render($renderer, p_get_instructions($source), $info);
+        if(is_null($result)) throw new DokuCLI_Exception("No such renderer $renderer");
+        echo $result;
+    }
+}
+
+// Main
+$cli = new RenderCLI();
+$cli->run();
diff --git a/wiki/bin/striplangs.php b/wiki/bin/striplangs.php
new file mode 100755
index 0000000..1800971
--- /dev/null
+++ b/wiki/bin/striplangs.php
@@ -0,0 +1,114 @@
+#!/usr/bin/php
+<?php
+
+use splitbrain\phpcli\CLI;
+use splitbrain\phpcli\Options;
+
+if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
+define('NOSESSION', 1);
+require_once(DOKU_INC . 'inc/init.php');
+
+/**
+ * Remove unwanted languages from a DokuWiki install
+ */
+class StripLangsCLI extends CLI {
+
+    /**
+     * Register options and arguments on the given $options object
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function setup(Options $options) {
+
+        $options->setHelp(
+            'Remove all languages from the installation, besides the ones specified. English language ' .
+            'is never removed!'
+        );
+
+        $options->registerOption(
+            'keep',
+            'Comma separated list of languages to keep in addition to English.',
+            'k',
+            'langcodes'
+        );
+        $options->registerOption(
+            'english-only',
+            'Remove all languages except English',
+            'e'
+        );
+    }
+
+    /**
+     * Your main program
+     *
+     * Arguments and options have been parsed when this is run
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function main(Options $options) {
+        if($options->getOpt('keep')) {
+            $keep = explode(',', $options->getOpt('keep'));
+            if(!in_array('en', $keep)) $keep[] = 'en';
+        } elseif($options->getOpt('english-only')) {
+            $keep = array('en');
+        } else {
+            echo $options->help();
+            exit(0);
+        }
+
+        // Kill all language directories in /inc/lang and /lib/plugins besides those in $langs array
+        $this->stripDirLangs(realpath(dirname(__FILE__) . '/../inc/lang'), $keep);
+        $this->processExtensions(realpath(dirname(__FILE__) . '/../lib/plugins'), $keep);
+        $this->processExtensions(realpath(dirname(__FILE__) . '/../lib/tpl'), $keep);
+    }
+
+    /**
+     * Strip languages from extensions
+     *
+     * @param string $path path to plugin or template dir
+     * @param array $keep_langs languages to keep
+     */
+    protected function processExtensions($path, $keep_langs) {
+        if(is_dir($path)) {
+            $entries = scandir($path);
+
+            foreach($entries as $entry) {
+                if($entry != "." && $entry != "..") {
+                    if(is_dir($path . '/' . $entry)) {
+
+                        $plugin_langs = $path . '/' . $entry . '/lang';
+
+                        if(is_dir($plugin_langs)) {
+                            $this->stripDirLangs($plugin_langs, $keep_langs);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Strip languages from path
+     *
+     * @param string $path path to lang dir
+     * @param array $keep_langs languages to keep
+     */
+    protected function stripDirLangs($path, $keep_langs) {
+        $dir = dir($path);
+
+        while(($cur_dir = $dir->read()) !== false) {
+            if($cur_dir != '.' and $cur_dir != '..' and is_dir($path . '/' . $cur_dir)) {
+
+                if(!in_array($cur_dir, $keep_langs, true)) {
+                    io_rmdir($path . '/' . $cur_dir, true);
+                }
+            }
+        }
+        $dir->close();
+    }
+}
+
+$cli = new StripLangsCLI();
+$cli->run();
diff --git a/wiki/bin/wantedpages.php b/wiki/bin/wantedpages.php
new file mode 100755
index 0000000..0240eb9
--- /dev/null
+++ b/wiki/bin/wantedpages.php
@@ -0,0 +1,186 @@
+#!/usr/bin/php
+<?php
+
+use splitbrain\phpcli\CLI;
+use splitbrain\phpcli\Options;
+
+if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
+define('NOSESSION', 1);
+require_once(DOKU_INC . 'inc/init.php');
+
+/**
+ * Find wanted pages
+ */
+class WantedPagesCLI extends CLI {
+
+    const DIR_CONTINUE = 1;
+    const DIR_NS = 2;
+    const DIR_PAGE = 3;
+
+    private $skip = false;
+    private $sort = 'wanted';
+
+    private $result = array();
+
+    /**
+     * Register options and arguments on the given $options object
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function setup(Options $options) {
+        $options->setHelp(
+            'Outputs a list of wanted pages (pages that do not exist yet) and their origin pages ' .
+            ' (the pages that are linkin to these missing pages).'
+        );
+        $options->registerArgument(
+            'namespace',
+            'The namespace to lookup. Defaults to root namespace',
+            false
+        );
+
+        $options->registerOption(
+            'sort',
+            'Sort by wanted or origin page',
+            's',
+            '(wanted|origin)'
+        );
+
+        $options->registerOption(
+            'skip',
+            'Do not show the second dimension',
+            'k'
+        );
+    }
+
+    /**
+     * Your main program
+     *
+     * Arguments and options have been parsed when this is run
+     *
+     * @param Options $options
+     * @return void
+     */
+    protected function main(Options $options) {
+        $args = $options->getArgs();
+        if($args) {
+            $startdir = dirname(wikiFN($args[0] . ':xxx'));
+        } else {
+            $startdir = dirname(wikiFN('xxx'));
+        }
+
+        $this->skip = $options->getOpt('skip');
+        $this->sort = $options->getOpt('sort');
+
+        $this->info("searching $startdir");
+
+        foreach($this->get_pages($startdir) as $page) {
+            $this->internal_links($page);
+        }
+        ksort($this->result);
+        foreach($this->result as $main => $subs) {
+            if($this->skip) {
+                print "$main\n";
+            } else {
+                $subs = array_unique($subs);
+                sort($subs);
+                foreach($subs as $sub) {
+                    printf("%-40s %s\n", $main, $sub);
+                }
+            }
+        }
+    }
+
+    /**
+     * Determine directions of the search loop
+     *
+     * @param string $entry
+     * @param string $basepath
+     * @return int
+     */
+    protected function dir_filter($entry, $basepath) {
+        if($entry == '.' || $entry == '..') {
+            return WantedPagesCLI::DIR_CONTINUE;
+        }
+        if(is_dir($basepath . '/' . $entry)) {
+            if(strpos($entry, '_') === 0) {
+                return WantedPagesCLI::DIR_CONTINUE;
+            }
+            return WantedPagesCLI::DIR_NS;
+        }
+        if(preg_match('/\.txt$/', $entry)) {
+            return WantedPagesCLI::DIR_PAGE;
+        }
+        return WantedPagesCLI::DIR_CONTINUE;
+    }
+
+    /**
+     * Collects recursively the pages in a namespace
+     *
+     * @param string $dir
+     * @return array
+     * @throws DokuCLI_Exception
+     */
+    protected function get_pages($dir) {
+        static $trunclen = null;
+        if(!$trunclen) {
+            global $conf;
+            $trunclen = strlen($conf['datadir'] . ':');
+        }
+
+        if(!is_dir($dir)) {
+            throw new DokuCLI_Exception("Unable to read directory $dir");
+        }
+
+        $pages = array();
+        $dh = opendir($dir);
+        while(false !== ($entry = readdir($dh))) {
+            $status = $this->dir_filter($entry, $dir);
+            if($status == WantedPagesCLI::DIR_CONTINUE) {
+                continue;
+            } else if($status == WantedPagesCLI::DIR_NS) {
+                $pages = array_merge($pages, $this->get_pages($dir . '/' . $entry));
+            } else {
+                $page = array(
+                    'id' => pathID(substr($dir . '/' . $entry, $trunclen)),
+                    'file' => $dir . '/' . $entry,
+                );
+                $pages[] = $page;
+            }
+        }
+        closedir($dh);
+        return $pages;
+    }
+
+    /**
+     * Parse instructions and add the non-existing links to the result array
+     *
+     * @param array $page array with page id and file path
+     */
+    function internal_links($page) {
+        global $conf;
+        $instructions = p_get_instructions(file_get_contents($page['file']));
+        $cns = getNS($page['id']);
+        $exists = false;
+        $pid = $page['id'];
+        foreach($instructions as $ins) {
+            if($ins[0] == 'internallink' || ($conf['camelcase'] && $ins[0] == 'camelcaselink')) {
+                $mid = $ins[1][0];
+                resolve_pageid($cns, $mid, $exists);
+                if(!$exists) {
+                    list($mid) = explode('#', $mid); //record pages without hashes
+
+                    if($this->sort == 'origin') {
+                        $this->result[$pid][] = $mid;
+                    } else {
+                        $this->result[$mid][] = $pid;
+                    }
+                }
+            }
+        }
+    }
+}
+
+// Main
+$cli = new WantedPagesCLI();
+$cli->run();