1 #!/usr/bin/env php
2 <?php
3 
4 use splitbrain\phpcli\CLI;
5 use splitbrain\phpcli\Options;
6 use dokuwiki\Utf8\PhpString;
7 
8 if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/');
9 define('NOSESSION', 1);
10 require_once(DOKU_INC . 'inc/init.php');
11 
12 /**
13  * Checkout and commit pages from the command line while maintaining the history
14  */
15 class 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