1<?php
2
3/**
4 * DokuWiki Plugin attribute (Helper Component)
5 *
6 * @author  Mike Wilmes <mwilmes@avc.edu>
7 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
8 */
9
10/**
11 * Class helper_plugin_attribute
12 */
13class helper_plugin_attribute extends DokuWiki_Plugin
14{
15    public $success = false;
16    protected $storepath = null;
17    protected $cache = null;
18    protected $secure = true;
19
20    /**
21     * Constructor
22     */
23    public function __construct()
24    {
25        $this->loadConfig();
26        // Create the path used for attribute data.
27        $path = substr($this->conf['store'], 0, 1) == '/' ? $this->conf['store'] : DOKU_INC . $this->conf['store'];
28        $this->storepath = ($this->conf['store'] === '' || !io_mkdir_p($path)) ? null : $path;
29        // A directory is needed.
30        if (is_null($this->storepath)) {
31            msg("Attribute: Configuration item 'store' is not set to a writeable directory.", -1);
32            return;
33        }
34        $this->success = true;
35        // Create a memory cache for this execution.
36        $this->cache = array();
37    }
38
39    /**
40     * Allows overriding the secure setting
41     *
42     * When set to false, no user validation checks are made.
43     *
44     * @param bool $secure
45     * @return void
46     */
47    public function setSecure($secure)
48    {
49        $this->secure = $secure;
50    }
51
52    /**
53     * Return info about supported methods in this Helper Plugin
54     *
55     * @return array Array of public methods
56     */
57    public function getMethods()
58    {
59        $result   = array();
60        $result[] = array(
61            'name'       => 'enumerateAttributes',
62            'desc'       => "Generates a list of known attributes in the specified namespace for a user. If user is present, must be an admin, otherwise defaults to currently logged in user.",
63            'parameters' => array(
64                'namespace' => 'string',
65                'user'      => 'string (optional)',
66            ),
67            'return'     => array('attributes' => 'array'), // returns false on error.
68        );
69        $result[] = array(
70            'name'       => 'enumerateUsers',
71            'desc'       => "Generates a list of users that have assigned attributes in the specified namespace.",
72            'parameters' => array(
73                'namespace' => 'string',
74            ),
75            'return'     => array('users' => 'array'), // returns false on error.
76        );
77        $result[] = array(
78            'name'       => 'set',
79            'desc'       => "Set the value of an attribute in a specified namespace. Returns boolean success (false if something went wrong). If user is present, must be an admin, otherwise defaults to currently logged in user.",
80            'parameters' => array(
81                'namespace' => 'string',
82                'attribute' => 'string',
83                'value'     => 'mixed (serializable)',
84                'user'      => 'string (optional)',
85            ),
86            'return'     => array('success' => 'boolean'),
87        );
88        $result[] = array(
89            'name'       => 'exists',
90            'desc'       => "Checks if an attribute exists for a user in a given namespace. If user is present, must be an admin, otherwise defaults to currently logged in user.",
91            'parameters' => array(
92                'namespace' => 'string',
93                'attribute' => 'string',
94                'user'      => 'string (optional)',
95            ),
96            'return'     => array('exists' => 'boolean'),
97        );
98        $result[] = array(
99            'name'       => 'del',
100            'desc'       => "Deletes attribute data in a specified namespace by its name. If user is present, must be an admin, otherwise defaults to currently logged in user.",
101            'parameters' => array(
102                'namespace' => 'string',
103                'attribute' => 'string',
104                'user'      => 'string (optional)',
105            ),
106            'return'     => array('success' => 'boolean'),
107        );
108        $result[] = array(
109            'name'       => 'get',
110            'desc'       => "Retrieves a value for an attribute in a specified namespace. Returns retrieved value or null. \$success out-parameter can be checked to check success (you may have false, null, 0, or '' as stored value). If user is present, must be an admin, otherwise defaults to currently logged in user.",
111            'parameters' => array(
112                'namespace' => 'string',
113                'attribute' => 'string',
114                'success'   => 'boolean (out)',
115                'user'      => 'string (optional)',
116            ),
117            'return'     => array('value' => 'mixed'), // returns false on error.
118        );
119        $result[] = array(
120            'name'       => 'purge',
121            'desc'       => "Deletes all attribute data for a specified namespace for a user. Only useable by an admin.",
122            'parameters' => array(
123                'namespace' => 'string',
124                'user'      => 'string',
125            ),
126            'return'     => array('success' => 'boolean'),
127        );
128        return $result;
129    }
130
131    /**
132     * Validate that the user may access another user's attribute. If the user
133     * is an admin and another user name is supplied, that value is returned.
134     * Otherwise the name of the logged in user is supplied. If no user is
135     * logged in, null is returned.
136     *
137     * This check can be disabled with the setSecure() method.
138     *
139     * @param string $user
140     *
141     * @return null|string
142     */
143    private function validateUser($user)
144    {
145        if(!$this->secure) return $user;
146
147        // We need a special circumstance.  If a user is not logged in, but we
148        // are performing a login, enable access to the attributes of the user
149        // being logged in IF DIRECTLY SPECIFIED.
150        global $INFO, $ACT, $USERINFO, $INPUT;
151        if ($ACT == 'login' && !$USERINFO && $user == $INPUT->str('u')) {
152            return $user;
153        }
154        // This does not meet the special circumstance listed above.
155        // Perform rights validation.
156        // If no one is logged in, then return null.
157        if ($_SERVER['REMOTE_USER'] == '') {
158            return null;
159        }
160        // If the user is not an admin, no user is specified, or the
161        // named user is not the logged in user, then return the currently
162        // logged in user.
163        if (!$user || ($user !== $_SERVER['REMOTE_USER'] && !$INFO['isadmin'])) {
164            return $_SERVER['REMOTE_USER'];
165        }
166        // The user is an admin and a name was specified.
167        return $user;
168    }
169
170    /**
171     * Load all attribute data for a user in the specified namespace.
172     * This loads all user attribute data from file.  A copy is stored in
173     * memory to alleviate repeated file accesses.
174     *
175     * @param $namespace
176     * @param $user
177     *
178     * @return array|mixed
179     */
180    private function loadAttributes($namespace, $user)
181    {
182        $key      = rawurlencode($namespace) . '.' . rawurlencode($user);
183        $filename = $this->storepath . "/" . $key;
184
185        // If the file does not exist, then return an empty attribute array.
186        if (!is_file($filename)) {
187            return array();
188        }
189
190        if (array_key_exists($filename, $this->cache)) {
191            return $this->cache[$filename];
192        }
193
194        $packet = io_readFile($filename, false);
195
196        // Unserialize returns false on bad data.
197        $preserial = @unserialize($packet);
198        if ($preserial !== false) {
199            list($compressed, $serial) = $preserial;
200            if ($compressed) {
201                $serial = gzuncompress($serial);
202            }
203            $unserial = @unserialize($serial);
204            if ($unserial !== false) {
205                list($filekey, $data) = $unserial;
206                if ($filekey != $key) {
207                    $data = array();
208                }
209            }
210        }
211
212        // Set a reasonable default if either unserialize failed.
213        if ($preserial == false || $unserial === false) {
214            $data = array();
215        }
216
217        $this->cache[$filename] = $data;
218
219        return $data;
220    }
221
222    /**
223     * Saves attributes in $data to a file.  The file is flagged with the
224     * namespace and use that the data was saved for. The data and key will
225     * normally be compressed, but this can be turned off for debugging.
226     * There is an uncompressed flag to denote whether the data was compressed
227     * or not, so both compressed and uncompressed data can be loaded
228     * regardless of the compression configuration.
229     *
230     * @param $namespace
231     * @param $user
232     * @param $data
233     *
234     * @return bool
235     */
236    private function saveAttributes($namespace, $user, $data)
237    {
238        $key = rawurlencode($namespace) . '.' . rawurlencode($user);
239        $filename = $this->storepath . "/" . $key;
240
241        $this->cache[$filename] = $data;
242
243        $serial = serialize(array($key, $data));
244        $compressed = $this->conf['no_compress'] === 0;
245        if ($compressed) {
246            $serial = gzcompress($serial);
247        }
248        $packet = serialize(array($compressed, $serial));
249
250        return io_saveFile($filename, $packet);
251    }
252
253    /**
254     * Generates a list of users that have assigned attributes in the
255     * specified namespace.
256     *
257     * @param string $namespace
258     *
259     * @return array|bool
260     */
261    public function enumerateUsers($namespace)
262    {
263        if (!$this->success) {
264            return false;
265        }
266
267        $listing = scandir($this->storepath, SCANDIR_SORT_DESCENDING);
268
269        // Restrict to namespace
270        $key = rawurlencode($namespace) . '.';
271        $files = array_filter(
272            $listing,
273            function ($x) use ($key) {
274                return substr($x, 0, strlen($key)) == $key;
275            }
276        );
277        // Get usernames from files
278        $users = array_map(
279            function ($x) use ($key) {
280                return rawurldecode(substr($x, strlen($key)));
281            },
282            $files
283        );
284
285        return $users;
286    }
287
288    /**
289     * set - Set the value of an attribute in a specified namespace. Returns
290     * boolean success (false if something went wrong). If user is present,
291     * must be an admin, otherwise defaults to currently logged in user.
292     *
293     * @param string $namespace
294     * @param string $attribute
295     * @param string $value
296     * @param null   $user
297     *
298     * @return bool
299     */
300    public function set($namespace, $attribute, $value, $user = null)
301    {
302        if (!$this->success) {
303            return false;
304        }
305
306        $user = $this->validateUser($user);
307        if ($user === null) {
308            return false;
309        }
310        $lock= $namespace . '.' . $user;
311        io_lock($lock);
312
313        $data = $this->loadAttributes($namespace, $user);
314
315        $result = false;
316        if ($data !== null) {
317            // Set the data in the array.
318            $data[$attribute] = $value;
319            // Store the changed data.
320            $result = $this->saveAttributes($namespace, $user, $data);
321        }
322
323        io_unlock($lock);
324
325        return $result;
326    }
327
328    /**
329     * Generates a list of users that have assigned attributes in the
330     * specified namespace.
331     *
332     * @param string      $namespace
333     * @param string|null $user
334     *
335     * @return array|bool
336     */
337    public function enumerateAttributes($namespace, $user = null)
338    {
339        if (!$this->success) {
340            return false;
341        }
342
343        $user = $this->validateUser($user);
344        if ($user === null) {
345            return false;
346        }
347
348        $lock = $namespace . '.' . $user;
349        io_lock($lock);
350
351        $data = $this->loadAttributes($namespace, $user);
352
353        io_unlock($lock);
354
355        if ($data === null) {
356            return false;
357        }
358
359        // Return just the keys. The values are cached.
360        return array_keys($data);
361    }
362
363    /**
364     * Checks if an attribute exists for a user in a given namespace. If user
365     * is present, must be an admin, otherwise defaults to currently logged in
366     * user.
367     *
368     * @param string      $namespace
369     * @param string      $attribute
370     * @param string|null $user
371     *
372     * @return bool
373     */
374    public function exists($namespace, $attribute, $user = null)
375    {
376        if (!$this->success) {
377            return false;
378        }
379
380        $user = $this->validateUser($user);
381        if ($user === null) {
382            return false;
383        }
384
385        $lock = $namespace . '.' . $user;
386        io_lock($lock);
387
388        $data = $this->loadAttributes($namespace, $user);
389
390        io_unlock($lock);
391
392        if (!is_array($data)) {
393            return false;
394        }
395
396        return array_key_exists($attribute, $data);
397    }
398
399    /**
400     * Deletes attribute data in a specified namespace by its name. If user is
401     * present, must be an admin, otherwise defaults to currently logged in
402     * user.
403     *
404     * @param string      $namespace
405     * @param string      $attribute
406     * @param string|null $user
407     *
408     * @return bool
409     */
410    public function del($namespace, $attribute, $user = null)
411    {
412        if (!$this->success) {
413            return false;
414        }
415
416        $user = $this->validateUser($user);
417        if ($user === null) {
418            return false;
419        }
420
421        $lock = $namespace . '.' . $user;
422        io_lock($lock);
423
424        $data = $this->loadAttributes($namespace, $user);
425        if ($data !== null) {
426            // Special case- if the attribute already does not exist, then
427            // return true. We are at the desired state.
428            if (array_key_exists($attribute, $data)) {
429                unset($data[$attribute]);
430                $result = $this->saveAttributes($namespace, $user, $data);
431            } else {
432                $result = true;
433            }
434        } else {
435            $result = false;
436        }
437
438        io_unlock($lock);
439
440        return $result;
441    }
442
443    /**
444     * Deletes all attribute data for a specified namespace for a user. Only
445     * usable the user themselves or an admin.
446     *
447     * @param string $namespace
448     * @param string $user
449     *
450     * @return bool
451     */
452    public function purge($namespace, $user)
453    {
454        if (!$this->success) {
455            return false;
456        }
457
458        if ($this->validateUser($user) === null) {
459            return false;
460        }
461
462        $lock = $namespace . '.' . $user;
463        io_lock($lock);
464
465        $key = rawurlencode($namespace) . '.' . rawurlencode($user);
466        $filename = $this->storepath . "/" . $key;
467
468        if (file_exists($filename)) {
469            $result = unlink($filename);
470        } else {
471            // If the file does not exist, the desired end state has been
472            // reached.
473            $result = true;
474        }
475
476        io_unlock($lock);
477
478        return $result;
479    }
480
481    /**
482     * Retrieves a value for an attribute in a specified namespace. Returns
483     * retrieved value or null. $success out-parameter can be checked to check
484     * success (you may have false, null, 0, or '' as stored value). If user
485     * is present, must be an admin, otherwise defaults to currently logged in
486     * user.
487     *
488     * @param string      $namespace
489     * @param string      $attribute
490     * @param bool        $success
491     * @param string|null $user
492     *
493     * @return bool
494     */
495    public function get($namespace, $attribute, &$success = false, $user = null)
496    {
497        // Prepare the supplied success flag as false.  It will be changed to
498        // true on success.
499        $success = false;
500
501        if (!$this->success) {
502            return false;
503        }
504
505        $user = $this->validateUser($user);
506        if ($user === null) {
507            return false;
508        }
509
510        $lock = $namespace . '.' . $user;
511        io_lock($lock);
512
513        $data = $this->loadAttributes($namespace, $user);
514
515        io_unlock($lock);
516
517        if ($data === null || !array_key_exists($attribute, $data)) {
518            return false;
519        }
520
521        $success = true;
522        return $data[$attribute];
523    }
524}
525
526// vim:ts=4:sw=4:et:
527