Completed
Pull Request — master (#3386)
by
unknown
02:59
created

PageCLI::obtainLock()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 6
nop 1
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
1
#!/usr/bin/env php
2
<?php
3
4
use splitbrain\phpcli\CLI;
5
use splitbrain\phpcli\Options;
6
7
if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../') . '/');
8
define('NOSESSION', 1);
9
require_once(DOKU_INC . 'inc/init.php');
10
11
/**
12
 * Checkout and commit pages from the command line while maintaining the history
13
 */
14
class 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
            'gmeta',
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
            'gmeta'
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
            'gmeta'
141
        );
142
143
    }
144
145
    /**
146
     * Your main program
147
     *
148
     * Arguments and options have been parsed when this is run
149
     *
150
     * @param Options $options
151
     * @return void
152
     */
153
    protected function main(Options $options) {
154
        $this->force = $options->getOpt('force', false);
0 ignored issues
show
Documentation Bug introduced by
It seems like $options->getOpt('force', false) can also be of type string or array<integer,string>. However, the property $force is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
155
        $this->username = $options->getOpt('user', $this->getUser());
0 ignored issues
show
Documentation Bug introduced by
It seems like $options->getOpt('user', $this->getUser()) can also be of type boolean or array<integer,string>. However, the property $username is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
156
157
        $command = $options->getCmd();
158
        $args = $options->getArgs();
159
        switch($command) {
160
            case 'checkout':
161
                $wiki_id = array_shift($args);
162
                $localfile = array_shift($args);
163
                $this->commandCheckout($wiki_id, $localfile);
164
                break;
165
            case 'commit':
166
                $localfile = array_shift($args);
167
                $wiki_id = array_shift($args);
168
                $this->commandCommit(
169
                    $localfile,
170
                    $wiki_id,
171
                    $options->getOpt('message', ''),
172
                    $options->getOpt('trivial', false)
173
                );
174
                break;
175
            case 'lock':
176
                $wiki_id = array_shift($args);
177
                $this->obtainLock($wiki_id);
178
                $this->success("$wiki_id locked");
179
                break;
180
            case 'unlock':
181
                $wiki_id = array_shift($args);
182
                $this->clearLock($wiki_id);
183
                $this->success("$wiki_id unlocked");
184
                break;
185
            case 'gmeta':
186
                $wiki_id = array_shift($args);
187
                $key = trim(array_shift($args));
188
                $meta=print_r(p_get_metadata($wiki_id, $key), true);
189
                print($meta);
190
                if (strcmp(substr($meta, -1), "\n"))
191
                    print("\n");
192
                break;
193
            default:
194
                echo $options->help();
195
        }
196
    }
197
198
    /**
199
     * Check out a file
200
     *
201
     * @param string $wiki_id
202
     * @param string $localfile
203
     */
204
    protected function commandCheckout($wiki_id, $localfile) {
205
        global $conf;
206
207
        $wiki_id = cleanID($wiki_id);
208
        $wiki_fn = wikiFN($wiki_id);
209
210
        if(!file_exists($wiki_fn)) {
211
            $this->fatal("$wiki_id does not yet exist");
212
        }
213
214
        if(empty($localfile)) {
215
            $localfile = getcwd() . '/' . \dokuwiki\Utf8\PhpString::basename($wiki_fn);
216
        }
217
218
        if(!file_exists(dirname($localfile))) {
219
            $this->fatal("Directory " . dirname($localfile) . " does not exist");
220
        }
221
222
        if(stristr(realpath(dirname($localfile)), realpath($conf['datadir'])) !== false) {
223
            $this->fatal("Attempt to check out file into data directory - not allowed");
224
        }
225
226
        $this->obtainLock($wiki_id);
227
228
        if(!copy($wiki_fn, $localfile)) {
229
            $this->clearLock($wiki_id);
230
            $this->fatal("Unable to copy $wiki_fn to $localfile");
231
        }
232
233
        $this->success("$wiki_id > $localfile");
234
    }
235
236
    /**
237
     * Save a file as a new page revision
238
     *
239
     * @param string $localfile
240
     * @param string $wiki_id
241
     * @param string $message
242
     * @param bool $minor
243
     */
244
    protected function commandCommit($localfile, $wiki_id, $message, $minor) {
245
        $wiki_id = cleanID($wiki_id);
246
        $message = trim($message);
247
248
        if(!file_exists($localfile)) {
249
            $this->fatal("$localfile does not exist");
250
        }
251
252
        if(!is_readable($localfile)) {
253
            $this->fatal("Cannot read from $localfile");
254
        }
255
256
        if(!$message) {
257
            $this->fatal("Summary message required");
258
        }
259
260
        $this->obtainLock($wiki_id);
261
262
        saveWikiText($wiki_id, file_get_contents($localfile), $message, $minor);
263
264
        $this->clearLock($wiki_id);
265
266
        $this->success("$localfile > $wiki_id");
267
    }
268
269
    /**
270
     * Lock the given page or exit
271
     *
272
     * @param string $wiki_id
273
     */
274
    protected function obtainLock($wiki_id) {
275
        if($this->force) $this->deleteLock($wiki_id);
276
277
        $_SERVER['REMOTE_USER'] = $this->username;
278
279
        if(checklock($wiki_id)) {
280
            $this->error("Page $wiki_id is already locked by another user");
281
            exit(1);
282
        }
283
284
        lock($wiki_id);
285
286
        if(checklock($wiki_id)) {
287
            $this->error("Unable to obtain lock for $wiki_id ");
288
            var_dump(checklock($wiki_id));
0 ignored issues
show
Security Debugging Code introduced by
var_dump(checklock($wiki_id)); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
289
            exit(1);
290
        }
291
    }
292
293
    /**
294
     * Clear the lock on the given page
295
     *
296
     * @param string $wiki_id
297
     */
298
    protected function clearLock($wiki_id) {
299
        if($this->force) $this->deleteLock($wiki_id);
300
301
        $_SERVER['REMOTE_USER'] = $this->username;
302
        if(checklock($wiki_id)) {
303
            $this->error("Page $wiki_id is locked by another user");
304
            exit(1);
305
        }
306
307
        unlock($wiki_id);
308
309
        if(file_exists(wikiLockFN($wiki_id))) {
310
            $this->error("Unable to clear lock for $wiki_id");
311
            exit(1);
312
        }
313
    }
314
315
    /**
316
     * Forcefully remove a lock on the page given
317
     *
318
     * @param string $wiki_id
319
     */
320
    protected function deleteLock($wiki_id) {
321
        $wikiLockFN = wikiLockFN($wiki_id);
322
323
        if(file_exists($wikiLockFN)) {
324
            if(!unlink($wikiLockFN)) {
325
                $this->error("Unable to delete $wikiLockFN");
326
                exit(1);
327
            }
328
        }
329
    }
330
331
    /**
332
     * Get the current user's username from the environment
333
     *
334
     * @return string
335
     */
336
    protected function getUser() {
337
        $user = getenv('USER');
338
        if(empty ($user)) {
339
            $user = getenv('USERNAME');
340
        } else {
341
            return $user;
342
        }
343
        if(empty ($user)) {
344
            $user = 'admin';
345
        }
346
        return $user;
347
    }
348
}
349
350
// Main
351
$cli = new PageCLI();
352
$cli->run();
353