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