1#!/usr/bin/env php
2<?php
3
4use splitbrain\phpcli\CLI;
5use splitbrain\phpcli\Options;
6use dokuwiki\Utf8\PhpString;
7
8if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/');
9define('NOSESSION', 1);
10require_once(DOKU_INC . 'inc/init.php');
11
12/**
13 * Checkout and commit pages from the command line while maintaining the history
14 */
15class PageCLI extends CLI
16{
17    protected $force = false;
18    protected $username = '';
19
20    /**
21     * Register options and arguments on the given $options object
22     *
23     * @param Options $options
24     * @return void
25     */
26    protected function setup(Options $options)
27    {
28        /* global */
29        $options->registerOption(
30            'force',
31            'force obtaining a lock for the page (generally bad idea)',
32            'f'
33        );
34        $options->registerOption(
35            'user',
36            'work as this user. defaults to current CLI user',
37            'u',
38            'username'
39        );
40        $options->setHelp(
41            'Utility to help command line Dokuwiki page editing, allow ' .
42            'pages to be checked out for editing then committed after changes'
43        );
44
45        /* checkout command */
46        $options->registerCommand(
47            'checkout',
48            'Checks out a file from the repository, using the wiki id and obtaining ' .
49            'a lock for the page. ' . "\n" .
50            'If a working_file is specified, this is where the page is copied to. ' .
51            'Otherwise defaults to the same as the wiki page in the current ' .
52            'working directory.'
53        );
54        $options->registerArgument(
55            'wikipage',
56            'The wiki page to checkout',
57            true,
58            'checkout'
59        );
60        $options->registerArgument(
61            'workingfile',
62            'How to name the local checkout',
63            false,
64            'checkout'
65        );
66
67        /* commit command */
68        $options->registerCommand(
69            'commit',
70            'Checks in the working_file into the repository using the specified ' .
71            'wiki id, archiving the previous version.'
72        );
73        $options->registerArgument(
74            'workingfile',
75            'The local file to commit',
76            true,
77            'commit'
78        );
79        $options->registerArgument(
80            'wikipage',
81            'The wiki page to create or update',
82            true,
83            'commit'
84        );
85        $options->registerOption(
86            'message',
87            'Summary describing the change (required)',
88            'm',
89            'summary',
90            'commit'
91        );
92        $options->registerOption(
93            'trivial',
94            'minor change',
95            't',
96            false,
97            'commit'
98        );
99
100        /* lock command */
101        $options->registerCommand(
102            'lock',
103            'Obtains or updates a lock for a wiki page'
104        );
105        $options->registerArgument(
106            'wikipage',
107            'The wiki page to lock',
108            true,
109            'lock'
110        );
111
112        /* unlock command */
113        $options->registerCommand(
114            'unlock',
115            'Removes a lock for a wiki page.'
116        );
117        $options->registerArgument(
118            'wikipage',
119            'The wiki page to unlock',
120            true,
121            'unlock'
122        );
123
124        /* gmeta command */
125        $options->registerCommand(
126            'getmeta',
127            'Prints metadata value for a page to stdout.'
128        );
129        $options->registerArgument(
130            'wikipage',
131            'The wiki page to get the metadata for',
132            true,
133            'getmeta'
134        );
135        $options->registerArgument(
136            'key',
137            'The name of the metadata item to be retrieved.' . "\n" .
138            'If empty, an array of all the metadata items is returned.' . "\n" .
139            'For retrieving items that are stored in sub-arrays, separate the ' .
140            'keys of the different levels by spaces, in quotes, eg "date modified".',
141            false,
142            'getmeta'
143        );
144    }
145
146    /**
147     * Your main program
148     *
149     * Arguments and options have been parsed when this is run
150     *
151     * @param Options $options
152     * @return void
153     */
154    protected function main(Options $options)
155    {
156        $this->force = $options->getOpt('force', false);
157        $this->username = $options->getOpt('user', $this->getUser());
158
159        $command = $options->getCmd();
160        $args = $options->getArgs();
161        switch ($command) {
162            case 'checkout':
163                $wiki_id = array_shift($args);
164                $localfile = array_shift($args);
165                $this->commandCheckout($wiki_id, $localfile);
166                break;
167            case 'commit':
168                $localfile = array_shift($args);
169                $wiki_id = array_shift($args);
170                $this->commandCommit(
171                    $localfile,
172                    $wiki_id,
173                    $options->getOpt('message', ''),
174                    $options->getOpt('trivial', false)
175                );
176                break;
177            case 'lock':
178                $wiki_id = array_shift($args);
179                $this->obtainLock($wiki_id);
180                $this->success("$wiki_id locked");
181                break;
182            case 'unlock':
183                $wiki_id = array_shift($args);
184                $this->clearLock($wiki_id);
185                $this->success("$wiki_id unlocked");
186                break;
187            case 'getmeta':
188                $wiki_id = array_shift($args);
189                $key = trim(array_shift($args));
190                $meta = p_get_metadata($wiki_id, $key, METADATA_RENDER_UNLIMITED);
191                echo trim(json_encode($meta, JSON_PRETTY_PRINT));
192                echo "\n";
193                break;
194            default:
195                echo $options->help();
196        }
197    }
198
199    /**
200     * Check out a file
201     *
202     * @param string $wiki_id
203     * @param string $localfile
204     */
205    protected function commandCheckout($wiki_id, $localfile)
206    {
207        global $conf;
208
209        $wiki_id = cleanID($wiki_id);
210        $wiki_fn = wikiFN($wiki_id);
211
212        if (!file_exists($wiki_fn)) {
213            $this->fatal("$wiki_id does not yet exist");
214        }
215
216        if (empty($localfile)) {
217            $localfile = getcwd() . '/' . PhpString::basename($wiki_fn);
218        }
219
220        if (is_dir($localfile)) {
221            $this->fatal("Working file $localfile cannot be a directory");
222        }
223
224        if (!file_exists(dirname($localfile))) {
225            $this->fatal("Directory " . dirname($localfile) . " does not exist");
226        }
227
228        if (stristr(realpath(dirname($localfile)), (string) realpath($conf['datadir'])) !== false) {
229            $this->fatal("Attempt to check out file into data directory - not allowed");
230        }
231
232        $this->obtainLock($wiki_id);
233
234        if (!copy($wiki_fn, $localfile)) {
235            $this->clearLock($wiki_id);
236            $this->fatal("Unable to copy $wiki_fn to $localfile");
237        }
238
239        $this->success("$wiki_id > $localfile");
240    }
241
242    /**
243     * Save a file as a new page revision
244     *
245     * @param string $localfile
246     * @param string $wiki_id
247     * @param string $message
248     * @param bool $minor
249     */
250    protected function commandCommit($localfile, $wiki_id, $message, $minor)
251    {
252        $wiki_id = cleanID($wiki_id);
253        $message = trim($message);
254
255        if (!file_exists($localfile)) {
256            $this->fatal("$localfile does not exist");
257        }
258
259        if (!is_readable($localfile)) {
260            $this->fatal("Cannot read from $localfile");
261        }
262
263        if (!$message) {
264            $this->fatal("Summary message required");
265        }
266
267        $this->obtainLock($wiki_id);
268
269        saveWikiText($wiki_id, file_get_contents($localfile), $message, $minor);
270
271        $this->clearLock($wiki_id);
272
273        $this->success("$localfile > $wiki_id");
274    }
275
276    /**
277     * Lock the given page or exit
278     *
279     * @param string $wiki_id
280     */
281    protected function obtainLock($wiki_id)
282    {
283        if ($this->force) $this->deleteLock($wiki_id);
284
285        $_SERVER['REMOTE_USER'] = $this->username;
286
287        if (checklock($wiki_id)) {
288            $this->error("Page $wiki_id is already locked by another user");
289            exit(1);
290        }
291
292        lock($wiki_id);
293
294        if (checklock($wiki_id)) {
295            $this->error("Unable to obtain lock for $wiki_id ");
296            var_dump(checklock($wiki_id));
297            exit(1);
298        }
299    }
300
301    /**
302     * Clear the lock on the given page
303     *
304     * @param string $wiki_id
305     */
306    protected function clearLock($wiki_id)
307    {
308        if ($this->force) $this->deleteLock($wiki_id);
309
310        $_SERVER['REMOTE_USER'] = $this->username;
311        if (checklock($wiki_id)) {
312            $this->error("Page $wiki_id is locked by another user");
313            exit(1);
314        }
315
316        unlock($wiki_id);
317
318        if (file_exists(wikiLockFN($wiki_id))) {
319            $this->error("Unable to clear lock for $wiki_id");
320            exit(1);
321        }
322    }
323
324    /**
325     * Forcefully remove a lock on the page given
326     *
327     * @param string $wiki_id
328     */
329    protected function deleteLock($wiki_id)
330    {
331        $wikiLockFN = wikiLockFN($wiki_id);
332
333        if (file_exists($wikiLockFN)) {
334            if (!unlink($wikiLockFN)) {
335                $this->error("Unable to delete $wikiLockFN");
336                exit(1);
337            }
338        }
339    }
340
341    /**
342     * Get the current user's username from the environment
343     *
344     * @return string
345     */
346    protected function getUser()
347    {
348        $user = getenv('USER');
349        if (empty($user)) {
350            $user = getenv('USERNAME');
351        } else {
352            return $user;
353        }
354        if (empty($user)) {
355            $user = 'admin';
356        }
357        return $user;
358    }
359}
360
361// Main
362$cli = new PageCLI();
363$cli->run();
364