source: trunk/lib/ACL.inc.php @ 534

Last change on this file since 534 was 534, checked in by anonymous, 9 years ago

Improved module maker validation output. Allow disabling cache at run time for ACL. Added ACL getList() method. Improved ACL CLI listing. Fixed app boomerang array initialization. Now retaining identical boomerang URLs if the key is different. Added a maximum boomerang time. Added a way to disable cache per request through a query string. Added validator isDecimal() method. Added disableSelectOptions() HTML method. Added getGravatarURL() method. Change how navigation page array is managed. Updated navigation currentPage() method to test an array of URLs.

File size: 32.4 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/*
24* ACL.inc.php
25*
26* Uses the ARO/ACO/AXO model of Access Control Lists.
27* Uses Modified Preorder Tree Traversal to maintain a tree-structure.
28* See: http://www.sitepoint.com/print/hierarchical-data-database
29* Includes a command-line tool for managing rights (codebase/bin/acl.cli.php).
30*
31*
32* @author   Quinn Comendant <quinn@strangecode.com>
33* @version  1.0
34* @since    14 Jun 2006 22:35:11
35*/
36
37require_once dirname(__FILE__) . '/Cache.inc.php';
38
39class ACL
40{
41
42    // A place to keep an object instance for the singleton pattern.
43    protected static $instance = null;
44
45    // Configuration parameters for this object.
46    protected $_params = array(
47        // If false nothing will be cached or retrieved. Useful for testing realtime data requests.
48        'enable_cache' => true,
49
50        // Automatically create table and verify columns. Better set to false after site launch.
51        'create_table' => false,
52
53        // Maximum allowed length of names.
54        // This value can be increased only if {aro,aco,axo}_tbl.name VARCHAR length is increased.
55        'name_max_length' => 32.
56    );
57
58    /**
59     * Constructor.
60     */
61    public function __construct()
62    {
63        $app =& App::getInstance();
64
65        // Configure the cache object.
66        $this->cache = new Cache('acl');
67
68        // Get create tables config from global context.
69        if (!is_null($app->getParam('db_create_tables'))) {
70            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
71        }
72    }
73
74    /**
75     * This method enforces the singleton pattern for this class.
76     *
77     * @return  object  Reference to the global ACL object.
78     * @access  public
79     * @static
80     */
81    public static function &getInstance()
82    {
83        if (self::$instance === null) {
84            self::$instance = new self();
85        }
86
87        return self::$instance;
88    }
89
90    /**
91     * Set (or overwrite existing) parameters by passing an array of new parameters.
92     *
93     * @access public
94     *
95     * @param  array    $params     Array of parameters (key => val pairs).
96     */
97    public function setParam($params)
98    {
99        $app =& App::getInstance();
100
101        if (isset($params) && is_array($params)) {
102            // Some params require special processing. Catch those in a loop and process individually.
103            foreach ($params as $key => $val) {
104                switch ($key) {
105                case 'enable_cache':
106                    $this->cache->setParam(array('enabled' => $val));
107                    break;
108                }
109
110            }
111            unset($key, $value);
112
113            // Merge new parameters with old overriding only those passed.
114            $this->_params = array_merge($this->_params, $params);
115        } else {
116            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
117        }
118    }
119
120    /**
121     * Return the value of a parameter, if it exists.
122     *
123     * @access public
124     * @param string $param        Which parameter to return.
125     * @return mixed               Configured parameter value.
126     */
127    public function getParam($param)
128    {
129        $app =& App::getInstance();
130
131        if (array_key_exists($param, $this->_params)) {
132            return $this->_params[$param];
133        } else {
134            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_NOTICE, __FILE__, __LINE__);
135            return null;
136        }
137    }
138
139    /**
140     * Setup the database tables for this class.
141     *
142     * @access  public
143     * @author  Quinn Comendant <quinn@strangecode.com>
144     * @since   04 Jun 2006 16:41:42
145     */
146    public function initDB($recreate_db=false)
147    {
148        $app =& App::getInstance();
149        $db =& DB::getInstance();
150
151        static $_db_tested = false;
152
153        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
154
155            if ($recreate_db) {
156                $db->query("DROP TABLE IF EXISTS acl_tbl");
157                $db->query("DROP TABLE IF EXISTS aro_tbl");
158                $db->query("DROP TABLE IF EXISTS aco_tbl");
159                $db->query("DROP TABLE IF EXISTS axo_tbl");
160                $app->logMsg(sprintf('Dropping and recreating tables acl_tbl, aro_tbl, aco_tbl, axo_tbl.', null), LOG_INFO, __FILE__, __LINE__);
161            }
162
163            // acl_tbl
164            $db->query("
165                CREATE TABLE IF NOT EXISTS acl_tbl (
166                    aro_id SMALLINT UNSIGNED NOT NULL DEFAULT '0',
167                    aco_id SMALLINT UNSIGNED NOT NULL DEFAULT '0',
168                    axo_id SMALLINT UNSIGNED NOT NULL DEFAULT '0',
169                    access ENUM('allow', 'deny') DEFAULT NULL,
170                    added_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
171                    UNIQUE KEY (aro_id, aco_id, axo_id),
172                    KEY (access)
173                ) ENGINE=MyISAM
174            ");
175            if (!$db->columnExists('acl_tbl', array(
176                'aro_id',
177                'aco_id',
178                'axo_id',
179                'access',
180                'added_datetime',
181            ), false, false)) {
182                $app->logMsg(sprintf('Database table acl_tbl has invalid columns. Please update this table manually.', null), LOG_ALERT, __FILE__, __LINE__);
183                trigger_error(sprintf('Database table acl_tbl has invalid columns. Please update this table manually.', null), E_USER_ERROR);
184            } else {
185                // Insert root node data if nonexistant, dely all by default.
186                $qid = $db->query("SELECT 1 FROM acl_tbl");
187                if (mysql_num_rows($qid) == 0) {
188                    $qid = $db->query("REPLACE INTO acl_tbl VALUES ('1', '1', '1', 'deny', NOW())");
189                }
190            }
191
192            // aro_tbl, aco_tbl, axo_tbl
193            foreach (array('aro', 'aco', 'axo') as $a_o) {
194                $db->query("
195                    CREATE TABLE IF NOT EXISTS {$a_o}_tbl (
196                        {$a_o}_id SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
197                        name VARCHAR(" . $this->getParam('name_max_length') . ") NOT NULL DEFAULT '',
198                        lft MEDIUMINT UNSIGNED NOT NULL DEFAULT '0',
199                        rgt MEDIUMINT UNSIGNED NOT NULL DEFAULT '0',
200                        added_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
201                        UNIQUE KEY name (name(15)),
202                        KEY transversal (lft, rgt)
203                    ) ENGINE=MyISAM;
204                ");
205
206                if (!$db->columnExists("{$a_o}_tbl", array(
207                    "{$a_o}_id",
208                    'name',
209                    'lft',
210                    'rgt',
211                    'added_datetime',
212                ), false, false)) {
213                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', "{$a_o}_tbl"), LOG_ALERT, __FILE__, __LINE__);
214                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', "{$a_o}_tbl"), E_USER_ERROR);
215                } else {
216                    // Insert root node data if nonexistent.
217                    $qid = $db->query("SELECT 1 FROM {$a_o}_tbl WHERE name = 'root'");
218                    if (mysql_num_rows($qid) == 0) {
219                        $qid = $db->query("REPLACE INTO {$a_o}_tbl (name, lft, rgt, added_datetime) VALUES ('root', 1, 2, NOW())");
220                    }
221                }
222
223            }
224        }
225        $_db_tested = true;
226        return true;
227    }
228
229    /*
230    * Add a node to one of the aro/aco/axo tables.
231    *
232    * @access   public
233    * @param    string $name A unique identifier for the new node.
234    * @param    string $parent The name of the parent under-which to attach the new node.
235    * @param    string $type The tree to add to, one of: aro, aco, axo.
236    * @return   bool | int False on error, or the last_insert_id primary key of the new node.
237    * @author   Quinn Comendant <quinn@strangecode.com>
238    * @version  1.0
239    * @since    14 Jun 2006 22:39:29
240    */
241    public function add($name, $parent=null, $type)
242    {
243        $app =& App::getInstance();
244        $db =& DB::getInstance();
245
246        $this->initDB();
247
248        switch ($type) {
249        case 'aro' :
250            $tbl = 'aro_tbl';
251            break;
252        case 'aco' :
253            $tbl = 'aco_tbl';
254            break;
255        case 'axo' :
256            $tbl = 'axo_tbl';
257            break;
258        default :
259            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
260            return false;
261            break;
262        }
263
264        // If $parent is null, use root object.
265        if (is_null($parent)) {
266            $parent = 'root';
267        }
268
269        // Ensure node and parent name aren't empty.
270        if ('' == trim($name) || '' == trim($parent)) {
271            $app->logMsg(sprintf('Cannot add node, parent (%s) or name (%s) missing.', $name, $parent), LOG_WARNING, __FILE__, __LINE__);
272            return false;
273        }
274
275        // Ensure node node name fits in the column size.
276        // This value can be increased if {aro,aco.axo}_tbl.name VARCHAR length is increased.
277        if (strlen(trim($name)) > $this->getParam('name_max_length')) {
278            $app->logMsg(sprintf('Cannot add node, %s character limit exceeded for name "%s"', $this->getParam('name_max_length'), $name, $parent), LOG_WARNING, __FILE__, __LINE__);
279            return false;
280        }
281
282        // Ensure node is unique.
283        $qid = $db->query("SELECT 1 FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
284        if (mysql_num_rows($qid) > 0) {
285            $app->logMsg(sprintf('Cannot add %s node, already exists: %s', $type, $name), LOG_INFO, __FILE__, __LINE__);
286            return false;
287        }
288
289        // Select the rgt of $parent.
290        $qid = $db->query("SELECT rgt FROM $tbl WHERE name = '" . $db->escapeString($parent) . "'");
291        if (!list($parent_rgt) = mysql_fetch_row($qid)) {
292            $app->logMsg(sprintf('Cannot add %s node to nonexistent parent: %s', $type, $parent), LOG_WARNING, __FILE__, __LINE__);
293            return false;
294        }
295
296        // Update transversal numbers for all nodes to the rgt of $parent.
297        $db->query("UPDATE $tbl SET lft = lft + 2 WHERE lft >= $parent_rgt");
298        $db->query("UPDATE $tbl SET rgt = rgt + 2 WHERE rgt >= $parent_rgt");
299
300        // Insert new node just below parent. Lft is parent's old rgt.
301        $db->query("
302            INSERT INTO $tbl (name, lft, rgt, added_datetime)
303            VALUES ('" . $db->escapeString($name) . "', $parent_rgt, $parent_rgt + 1, NOW())
304        ");
305
306        $app->logMsg(sprintf('Added %s node %s to parent %s', $type, $name, $parent), LOG_INFO, __FILE__, __LINE__);
307        return mysql_insert_id($db->getDBH());
308    }
309
310    // Alias functions for the different object types.
311    public function addRequestObject($name, $parent=null)
312    {
313        return $this->add($name, $parent, 'aro');
314    }
315    public function addControlObject($name, $parent=null)
316    {
317        return $this->add($name, $parent, 'aco');
318    }
319    public function addXtraObject($name, $parent=null)
320    {
321        return $this->add($name, $parent, 'axo');
322    }
323
324    /*
325    * Remove a node from one of the aro/aco/axo tables.
326    *
327    * @access   public
328    * @param    string $name The identifier for the node to remove.
329    * @param    string $type The tree to modify, one of: aro, aco, axo.
330    * @return   bool | int False on error, or true on success.
331    * @author   Quinn Comendant <quinn@strangecode.com>
332    * @version  1.0
333    * @since    14 Jun 2006 22:39:29
334    */
335    public function remove($name, $type)
336    {
337        $app =& App::getInstance();
338        $db =& DB::getInstance();
339
340        $this->initDB();
341
342        switch ($type) {
343        case 'aro' :
344            $tbl = 'aro_tbl';
345            $primary_key = 'aro_id';
346            break;
347        case 'aco' :
348            $tbl = 'aco_tbl';
349            $primary_key = 'aco_id';
350            break;
351        case 'axo' :
352            $tbl = 'axo_tbl';
353            $primary_key = 'axo_id';
354            break;
355        default :
356            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
357            return false;
358            break;
359        }
360
361        // Ensure node name isn't empty.
362        if ('' == trim($name)) {
363            $app->logMsg(sprintf('Cannot add node, name missing.', null), LOG_WARNING, __FILE__, __LINE__);
364            return false;
365        }
366
367        // Select the lft and rgt of $name to use for selecting children and reordering transversals.
368        $qid = $db->query("SELECT lft, rgt FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
369        if (!list($lft, $rgt) = mysql_fetch_row($qid)) {
370            $app->logMsg(sprintf('Cannot delete nonexistent %s name: %s', $type, $name), LOG_WARNING, __FILE__, __LINE__);
371            return false;
372        }
373
374        // Remove node and all children of node, as well as acl_tbl links.
375        $db->query("
376            DELETE $tbl, acl_tbl
377            FROM $tbl
378            LEFT JOIN acl_tbl ON ($tbl.$primary_key = acl_tbl.$primary_key)
379            WHERE $tbl.lft BETWEEN $lft AND $rgt
380        ");
381        $num_deleted_nodes = mysql_affected_rows($db->getDBH());
382
383        // Update transversal numbers for all nodes to the rgt of $parent, taking in to account the absence of it's children.
384        $db->query("UPDATE $tbl SET lft = lft - ($rgt - $lft + 1) WHERE lft > $lft");
385        $db->query("UPDATE $tbl SET rgt = rgt - ($rgt - $lft + 1) WHERE rgt > $rgt");
386
387        $app->logMsg(sprintf('Removed %s node %s along with %s children.', $type, $name, $num_deleted_nodes - 1), LOG_INFO, __FILE__, __LINE__);
388        return true;
389    }
390
391    // Alias functions for the different object types.
392    public function removeRequestObject($name)
393    {
394        return $this->remove($name, 'aro');
395    }
396    public function removeControlObject($name)
397    {
398        return $this->remove($name, 'aco');
399    }
400    public function removeXtraObject($name)
401    {
402        return $this->remove($name, 'axo');
403    }
404
405    /*
406    * Move a node to a new parent in one of the aro/aco/axo tables.
407    *
408    * @access   public
409    * @param    string $name The identifier for the node to remove.
410    * @param    string $new_parent The name of the parent under-which to attach the new node.
411    * @param    string $type The tree to modify, one of: aro, aco, axo.
412    * @return   bool | int False on error, or the last_insert_id primary key of the new node.
413    * @author   Quinn Comendant <quinn@strangecode.com>
414    * @version  1.0
415    * @since    14 Jun 2006 22:39:29
416    */
417    public function move($name, $new_parent, $type)
418    {
419        $app =& App::getInstance();
420        $db =& DB::getInstance();
421
422        $this->initDB();
423
424        switch ($type) {
425        case 'aro' :
426            $tbl = 'aro_tbl';
427            $primary_key = 'aro_id';
428            break;
429        case 'aco' :
430            $tbl = 'aco_tbl';
431            $primary_key = 'aco_id';
432            break;
433        case 'axo' :
434            $tbl = 'axo_tbl';
435            $primary_key = 'axo_id';
436            break;
437        default :
438            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
439            return false;
440            break;
441        }
442
443        // If $new_parent is null, use root object.
444        if (is_null($new_parent)) {
445            $new_parent = 'root';
446        }
447
448        // Ensure node and parent name aren't empty.
449        if ('' == trim($name) || '' == trim($new_parent)) {
450            $app->logMsg(sprintf('Cannot add node, parent (%s) or name (%s) missing.', $name, $new_parent), LOG_WARNING, __FILE__, __LINE__);
451            return false;
452        }
453
454        // Select the lft and rgt of $name to use for selecting children and reordering transversals.
455        $qid = $db->query("SELECT lft, rgt FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
456        if (!list($lft, $rgt) = mysql_fetch_row($qid)) {
457            $app->logMsg(sprintf('Cannot move nonexistent %s name: %s', $type, $name), LOG_WARNING, __FILE__, __LINE__);
458            return false;
459        }
460
461        // Total number of transversal values (that is, the count of self plus all children times two).
462        $total_transversal_value = ($rgt - $lft + 1);
463
464        // Select the rgt of the new parent.
465        $qid = $db->query("SELECT rgt FROM $tbl WHERE name = '" . $db->escapeString($new_parent) . "'");
466        if (!list($new_parent_rgt) = mysql_fetch_row($qid)) {
467            $app->logMsg(sprintf('Cannot move %s node to nonexistent parent: %s', $type, $new_parent), LOG_WARNING, __FILE__, __LINE__);
468            return false;
469        }
470
471        // Ensure the new parent is not a child of the node being moved.
472        if ($new_parent_rgt <= $rgt && $new_parent_rgt >= $lft) {
473            $app->logMsg(sprintf('Cannot move %s node %s to parent %s because it is a child of itself.', $type, $name, $new_parent), LOG_WARNING, __FILE__, __LINE__);
474            return false;
475        }
476
477        // Collect unique ids of all nodes being moved. The transversal numbers will become duplicated so these will be needed to identify these.
478        $qid = $db->query("
479            SELECT $primary_key
480            FROM $tbl
481            WHERE lft BETWEEN $lft AND $rgt
482            AND rgt BETWEEN $lft AND $rgt
483        ");
484        $ids = array();
485        while (list($id) = mysql_fetch_row($qid)) {
486            $ids[] = $id;
487        }
488
489        // Update transversal numbers for all nodes to the rgt of the node being moved, taking in to account the absence of it's children.
490        // This will temporarily "remove" the node from the tree, and its transversal values will be duplicated.
491        $db->query("UPDATE $tbl SET lft = lft - $total_transversal_value WHERE lft > $rgt");
492        $db->query("UPDATE $tbl SET rgt = rgt - $total_transversal_value WHERE rgt > $rgt");
493
494        // Apply transformation to new parent rgt also.
495        $new_parent_rgt = $new_parent_rgt > $rgt ? $new_parent_rgt - $total_transversal_value : $new_parent_rgt;
496
497        // Update transversal values of moved node and children.
498        $db->query("
499            UPDATE $tbl SET
500                lft = lft - ($lft - $new_parent_rgt),
501                rgt = rgt - ($lft - $new_parent_rgt)
502            WHERE $primary_key IN ('" . join("','", $ids) . "')
503        ");
504
505        // Update transversal values of all nodes to the rgt of moved node.
506        $db->query("UPDATE $tbl SET lft = lft + $total_transversal_value WHERE lft >= $new_parent_rgt AND $primary_key NOT IN ('" . join("','", $ids) . "')");
507        $db->query("UPDATE $tbl SET rgt = rgt + $total_transversal_value WHERE rgt >= $new_parent_rgt AND $primary_key NOT IN ('" . join("','", $ids) . "')");
508
509        $app->logMsg(sprintf('Moved %s node %s to new parent %s', $type, $name, $new_parent), LOG_INFO, __FILE__, __LINE__);
510        return true;
511    }
512
513    // Alias functions for the different object types.
514    public function moveRequestObject($name, $new_parent=null)
515    {
516        return $this->move($name, $new_parent, 'aro');
517    }
518    public function moveControlObject($name, $new_parent=null)
519    {
520        return $this->move($name, $new_parent, 'aco');
521    }
522    public function moveXtraObject($name, $new_parent=null)
523    {
524        return $this->move($name, $new_parent, 'axo');
525    }
526
527    /*
528    * Add an entry to the acl_tbl to allow (or deny) a truple with the specified
529    * ARO -> ACO -> AXO entry.
530    *
531    * @access   public
532    * @param    string|null $aro Identifier of an existing ARO object (or null to use root).
533    * @param    string|null $aco Identifier of an existing ACO object (or null to use root).
534    * @param    string|null $axo Identifier of an existing AXO object (or null to use root).
535    * @return   bool False on error, true on success.
536    * @author   Quinn Comendant <quinn@strangecode.com>
537    * @version  1.0
538    * @since    15 Jun 2006 01:58:48
539    */
540    public function grant($aro=null, $aco=null, $axo=null, $access='allow')
541    {
542        $app =& App::getInstance();
543        $db =& DB::getInstance();
544
545        $this->initDB();
546
547        // If any access objects are null, assume using root values.
548        // However if they're empty we don't want to escalate the grant command to root!
549        $aro = is_null($aro) ? 'root' : $aro;
550        $aco = is_null($aco) ? 'root' : $aco;
551        $axo = is_null($axo) ? 'root' : $axo;
552
553        // Flush old cached values.
554        $cache_hash = $aro . '|' . $aco . '|' . $axo;
555        $this->cache->delete($cache_hash);
556
557        // Ensure values exist.
558        $qid = $db->query("SELECT aro_tbl.aro_id FROM aro_tbl WHERE aro_tbl.name = '" . $db->escapeString($aro) . "'");
559        if (!list($aro_id) = mysql_fetch_row($qid)) {
560            $app->logMsg(sprintf('Grant failed, aro_tbl.name = "%s" does not exist.', $aro), LOG_WARNING, __FILE__, __LINE__);
561            return false;
562        }
563        $qid = $db->query("SELECT aco_tbl.aco_id FROM aco_tbl WHERE aco_tbl.name = '" . $db->escapeString($aco) . "'");
564        if (!list($aco_id) = mysql_fetch_row($qid)) {
565            $app->logMsg(sprintf('Grant failed, aco_tbl.name = "%s" does not exist.', $aco), LOG_WARNING, __FILE__, __LINE__);
566            return false;
567        }
568        $qid = $db->query("SELECT axo_tbl.axo_id FROM axo_tbl WHERE axo_tbl.name = '" . $db->escapeString($axo) . "'");
569        if (!list($axo_id) = mysql_fetch_row($qid)) {
570            $app->logMsg(sprintf('Grant failed, axo_tbl.name = "%s" does not exist.', $axo), LOG_WARNING, __FILE__, __LINE__);
571            return false;
572        }
573
574        // Access must be 'allow' or 'deny'.
575        $allow = 'allow' == $access ? 'allow' : 'deny';
576
577        $db->query("REPLACE INTO acl_tbl VALUES ('$aro_id', '$aco_id', '$axo_id', '$allow', NOW())");
578        $app->logMsg(sprintf('Set %s: %s -> %s -> %s', $allow, $aro, $aco, $axo), LOG_INFO, __FILE__, __LINE__);
579
580        return true;
581    }
582
583    /*
584    * Add an entry to the acl_tbl to deny a truple with the specified
585    * ARO -> ACO -> AXO entry. This calls the ACL::grant function to create the entry
586    * but uses 'deny' as the fourth argument.
587    *
588    * @access   public
589    * @param    string|null $aro Identifier of an existing ARO object (or null to use root).
590    * @param    string|null $aco Identifier of an existing ACO object (or null to use root).
591    * @param    string|null $axo Identifier of an existing AXO object (or null to use root).
592    * @return   bool False on error, true on success.
593    * @author   Quinn Comendant <quinn@strangecode.com>
594    * @version  1.0
595    * @since    15 Jun 2006 04:35:54
596    */
597    public function revoke($aro=null, $aco=null, $axo=null)
598    {
599        return $this->grant($aro, $aco, $axo, 'deny');
600    }
601
602    /*
603    * Delete an entry from the acl_tbl completely to allow other permissions to cascade down.
604    * Null values act as a "wildcard" and will cause ALL matches in that column to be deleted.
605    *
606    * @access   public
607    * @param    string|null $aro Identifier of an existing ARO object (or null for *).
608    * @param    string|null $aco Identifier of an existing ACO object (or null for *).
609    * @param    string|null $axo Identifier of an existing AXO object (or null for *).
610    * @return   bool False on error, true on success.
611    * @author   Quinn Comendant <quinn@strangecode.com>
612    * @version  1.0
613    * @since    20 Jun 2006 20:16:12
614    */
615    public function delete($aro=null, $aco=null, $axo=null)
616    {
617        $app =& App::getInstance();
618        $db =& DB::getInstance();
619
620        $this->initDB();
621
622        // If any access objects are null, assume using root values.
623        // However if they're empty we don't want to escalate the grant command to root!
624        $where = array();
625        $where[] = is_null($aro) ? "aro_tbl.name IS NOT NULL" : "aro_tbl.name = '" . $db->escapeString($aro) . "' ";
626        $where[] = is_null($aco) ? "aco_tbl.name IS NOT NULL" : "aco_tbl.name = '" . $db->escapeString($aco) . "' ";
627        $where[] = is_null($axo) ? "axo_tbl.name IS NOT NULL" : "axo_tbl.name = '" . $db->escapeString($axo) . "' ";
628
629        // If any access objects are null, assume using root values.
630        // However if they're empty we don't want to escalate the grant command to root!
631        $aro = is_null($aro) ? 'root' : $aro;
632        $aco = is_null($aco) ? 'root' : $aco;
633        $axo = is_null($axo) ? 'root' : $axo;
634
635        // Flush old cached values.
636        $cache_hash = $aro . '|' . $aco . '|' . $axo;
637        $this->cache->delete($cache_hash);
638
639        $final_where = join(' AND ', $where);
640        if (mb_substr_count($final_where, 'IS NOT NULL') == 3) {
641            // Null on all three tables will delete ALL entries including the root -> root -> root = deny.
642            $app->logMsg('Cannot allow deletion of ALL acl entries.', LOG_WARNING, __FILE__, __LINE__);
643            return false;
644        }
645
646        $qid = $db->query("
647            DELETE acl_tbl
648            FROM acl_tbl
649            LEFT JOIN aro_tbl ON (acl_tbl.aro_id = aro_tbl.aro_id)
650            LEFT JOIN aco_tbl ON (acl_tbl.aco_id = aco_tbl.aco_id)
651            LEFT JOIN axo_tbl ON (acl_tbl.axo_id = axo_tbl.axo_id)
652            WHERE $final_where
653        ");
654
655        $app->logMsg(sprintf('Deleted %s acl_tbl links: %s -> %s -> %s', mysql_affected_rows($db->getDBH()), $aro, $aco, $axo), LOG_INFO, __FILE__, __LINE__);
656
657        return true;
658    }
659
660    /*
661    * Calculates the most specific cascading privilege found for a requested
662    * ARO -> ACO -> AXO entry. Returns FALSE if the entry is denied. By default,
663    * all entries are denied, unless some point in the hierarchy is set to "allow."
664    *
665    * @access   public
666    * @param    string $aro Identifier of an existing ARO object.
667    * @param    string $aco Identifier of an existing ACO object (or null to use root).
668    * @param    string $axo Identifier of an existing AXO object (or null to use root).
669    * @return   bool False if denied, true on allowed.
670    * @author   Quinn Comendant <quinn@strangecode.com>
671    * @version  1.0
672    * @since    15 Jun 2006 03:58:23
673    */
674    public function check($aro, $aco=null, $axo=null)
675    {
676        $app =& App::getInstance();
677        $db =& DB::getInstance();
678
679        $this->initDB();
680
681        // If any access objects are null or empty, assume using root values.
682        $aro = is_null($aro) || '' == trim($aro) ? 'root' : $aro;
683        $aco = is_null($aco) || '' == trim($aco) ? 'root' : $aco;
684        $axo = is_null($axo) || '' == trim($axo) ? 'root' : $axo;
685
686        $cache_hash = $aro . '|' . $aco . '|' . $axo;
687        if (true === $this->getParam('enable_cache') && $this->cache->exists($cache_hash)) {
688            // Access value is cached.
689            $access = $this->cache->get($cache_hash);
690        } else {
691            // Retrieve access value from db.
692            $qid = $db->query("
693                SELECT acl_tbl.access
694                FROM acl_tbl
695                LEFT JOIN aro_tbl ON (acl_tbl.aro_id = aro_tbl.aro_id)
696                LEFT JOIN aco_tbl ON (acl_tbl.aco_id = aco_tbl.aco_id)
697                LEFT JOIN axo_tbl ON (acl_tbl.axo_id = axo_tbl.axo_id)
698                WHERE (aro_tbl.lft <= (SELECT lft FROM aro_tbl WHERE name = '" . $db->escapeString($aro) . "') AND aro_tbl.rgt >= (SELECT rgt FROM aro_tbl WHERE name = '" . $db->escapeString($aro) . "'))
699                AND (aco_tbl.lft <= (SELECT lft FROM aco_tbl WHERE name = '" . $db->escapeString($aco) . "') AND aco_tbl.rgt >= (SELECT rgt FROM aco_tbl WHERE name = '" . $db->escapeString($aco) . "'))
700                AND (axo_tbl.lft <= (SELECT lft FROM axo_tbl WHERE name = '" . $db->escapeString($axo) . "') AND axo_tbl.rgt >= (SELECT rgt FROM axo_tbl WHERE name = '" . $db->escapeString($axo) . "'))
701                ORDER BY aro_tbl.lft DESC, aco_tbl.lft DESC, axo_tbl.lft DESC
702                LIMIT 1
703            ");
704            if (!list($access) = mysql_fetch_row($qid)) {
705                $this->cache->set($cache_hash, 'deny');
706                $app->logMsg(sprintf('Access denied: %s -> %s -> %s (no records found).', $aro, $aco, $axo), LOG_WARNING, __FILE__, __LINE__);
707                return false;
708            }
709            $this->cache->set($cache_hash, $access);
710        }
711
712        if ('allow' == $access) {
713            $app->logMsg(sprintf('Access granted: %s -> %s -> %s', $aro, $aco, $axo), LOG_DEBUG, __FILE__, __LINE__);
714            return true;
715        } else {
716            $app->logMsg(sprintf('Access denied: %s -> %s -> %s', $aro, $aco, $axo), LOG_NOTICE, __FILE__, __LINE__);
717            return false;
718        }
719    }
720
721    /*
722    * Bounce user if they are denied access. Because this function calls dieURL() it must be called before any other HTTP header output.
723    *
724    * @access   public
725    * @param    string $aro Identifier of an existing ARO object.
726    * @param    string $aco Identifier of an existing ACO object (or null to use root).
727    * @param    string $axo Identifier of an existing AXO object (or null to use root).
728    * @param    string $message The text description of a message to raise.
729    * @param    int    $type    The type of message: MSG_NOTICE,
730    *                           MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
731    * @param    string $file    __FILE__.
732    * @param    string $line    __LINE__.
733    * @author   Quinn Comendant <quinn@strangecode.com>
734    * @version  1.0
735    * @since    20 Jan 2014 12:09:03
736    */
737    public function requireAllow($aro, $aco=null, $axo=null, $message='', $type=MSG_NOTICE, $file=null, $line=null)
738    {
739        $app =& App::getInstance();
740
741        if (!$this->check($aro, $aco, $axo)) {
742            $message = '' == trim($message) ? sprintf(_("Sorry, you have insufficient privileges for <em>%s %s</em>."), $aco, $axo) : $message;
743            $app->raiseMsg($message, $type, $file, $line);
744            $app->dieBoomerangURL();
745        }
746    }
747
748    /*
749    * Returns an array of the specified object type starting specified root.
750    *
751    * @access   public
752    * @param    string $type Table to list, one of: aro, aco, or axo.
753    * @param    string $root Root node from which to begin from.
754    * @return   mixed        Returns a multidimensional array of objects, or false on error.
755    * @author   Quinn Comendant <quinn@strangecode.com>
756    * @version  1.0
757    * @since    17 Jun 2006 23:41:22
758    */
759    function getList($type, $root=null)
760    {
761        $app =& App::getInstance();
762        $db =& DB::getInstance();
763
764        switch ($type) {
765        case 'aro' :
766            $tbl = 'aro_tbl';
767            break;
768        case 'aco' :
769            $tbl = 'aco_tbl';
770            break;
771        case 'axo' :
772            $tbl = 'axo_tbl';
773            break;
774        default :
775            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
776            return false;
777        }
778
779        // By default start with the 'root' node.
780        $root = !isset($root) ? 'root' : $root;
781
782        // Retrieve the left and right value of the $root node.
783        $qid = $db->query("SELECT lft, rgt FROM $tbl WHERE name = '" . $db->escapeString($root) . "'");
784        list($lft, $rgt) = mysql_fetch_row($qid);
785
786        $results = array();
787        $depth = array();
788
789        // Retrieve all descendants of the root node
790        $qid = $db->query("SELECT name, lft, rgt, added_datetime FROM $tbl WHERE lft BETWEEN $lft AND $rgt ORDER BY lft ASC");
791        while (list($name, $lft, $rgt, $added_datetime) = mysql_fetch_row($qid)) {
792            // If the last element of $depth is less than the current rgt it means we finished with a set of children nodes.
793            while (sizeof($depth) > 0 && end($depth) < $rgt) {
794                array_pop($depth);
795            }
796
797            $results[] = array(
798                'name' => $name,
799                'added_datetime' => $added_datetime,
800                'depth' => sizeof($depth),
801            );
802
803            // Add this node to the stack.
804            $depth[] = $rgt;
805        }
806
807        return $results;
808    }
809
810} // End class.
Note: See TracBrowser for help on using the repository browser.