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 (!file_exists(dirname($localfile))) {
221            $this->fatal("Directory " . dirname($localfile) . " does not exist");
222        }
223
224        if (stristr(realpath(dirname($localfile)), (string) realpath($conf['datadir'])) !== false) {
225            $this->fatal("Attempt to check out file into data directory - not allowed");
226        }
227
228        $this->obtainLock($wiki_id);
229
230        if (!copy($wiki_fn, $localfile)) {
231            $this->clearLock($wiki_id);
232            $this->fatal("Unable to copy $wiki_fn to $localfile");
233        }
234
235        $this->success("$wiki_id > $localfile");
236    }
237
238    /**
239     * Save a file as a new page revision
240     *
241     * @param string $localfile
242     * @param string $wiki_id
243     * @param string $message
244     * @param bool $minor
245     */
246    protected function commandCommit($localfile, $wiki_id, $message, $minor)
247    {
248        $wiki_id = cleanID($wiki_id);
249        $message = trim($message);
250
251        if (!file_exists($localfile)) {
252            $this->fatal("$localfile does not exist");
253        }
254
255        if (!is_readable($localfile)) {
256            $this->fatal("Cannot read from $localfile");
257        }
258
259        if (!$message) {
260            $this->fatal("Summary message required");
261        }
262
263        $this->obtainLock($wiki_id);
264
265        saveWikiText($wiki_id, file_get_contents($localfile), $message, $minor);
266
267        $this->clearLock($wiki_id);
268
269        $this->success("$localfile > $wiki_id");
270    }
271
272    /**
273     * Lock the given page or exit
274     *
275     * @param string $wiki_id
276     */
277    protected function obtainLock($wiki_id)
278    {
279        if ($this->force) $this->deleteLock($wiki_id);
280
281        $_SERVER['REMOTE_USER'] = $this->username;
282
283        if (checklock($wiki_id)) {
284            $this->error("Page $wiki_id is already locked by another user");
285            exit(1);
286        }
287
288        lock($wiki_id);
289
290        if (checklock($wiki_id)) {
291            $this->error("Unable to obtain lock for $wiki_id ");
292            var_dump(checklock($wiki_id));
293            exit(1);
294        }
295    }
296
297    /**
298     * Clear the lock on the given page
299     *
300     * @param string $wiki_id
301     */
302    protected function clearLock($wiki_id)
303    {
304        if ($this->force) $this->deleteLock($wiki_id);
305
306        $_SERVER['REMOTE_USER'] = $this->username;
307        if (checklock($wiki_id)) {
308            $this->error("Page $wiki_id is locked by another user");
309            exit(1);
310        }
311
312        unlock($wiki_id);
313
314        if (file_exists(wikiLockFN($wiki_id))) {
315            $this->error("Unable to clear lock for $wiki_id");
316            exit(1);
317        }
318    }
319
320    /**
321     * Forcefully remove a lock on the page given
322     *
323     * @param string $wiki_id
324     */
325    protected function deleteLock($wiki_id)
326    {
327        $wikiLockFN = wikiLockFN($wiki_id);
328
329        if (file_exists($wikiLockFN)) {
330            if (!unlink($wikiLockFN)) {
331                $this->error("Unable to delete $wikiLockFN");
332                exit(1);
333            }
334        }
335    }
336
337    /**
338     * Get the current user's username from the environment
339     *
340     * @return string
341     */
342    protected function getUser()
343    {
344        $user = getenv('USER');
345        if (empty($user)) {
346            $user = getenv('USERNAME');
347        } else {
348            return $user;
349        }
350        if (empty($user)) {
351            $user = 'admin';
352        }
353        return $user;
354    }
355}
356
357// Main
358$cli = new PageCLI();
359$cli->run();
360