Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.92% covered (warning)
76.92%
630 / 819
52.54% covered (warning)
52.54%
31 / 59
CRAP
0.00% covered (danger)
0.00%
0 / 1
SeedDMS_Core_Folder
76.92% covered (warning)
76.92%
630 / 819
52.54% covered (warning)
52.54%
31 / 59
2333.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 clearCache
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 isType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSearchFields
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getSearchTables
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getInstanceByData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getInstance
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 getInstanceByName
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
8.02
 applyDecorators
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setName
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
8.10
 getComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setComment
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
8.10
 getDate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getParent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 isSubFolder
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setParent
85.00% covered (warning)
85.00%
34 / 40
0.00% covered (danger)
0.00%
0 / 1
15.76
 getOwner
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setOwner
55.56% covered (warning)
55.56%
10 / 18
0.00% covered (danger)
0.00%
0 / 1
13.62
 getDefaultAccess
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setDefaultAccess
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 inheritsAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInheritAccess
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getSequence
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSequence
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 hasSubFolders
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 hasSubFolderByName
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getSubFolders
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
16
 addSubFolder
65.52% covered (warning)
65.52%
19 / 29
0.00% covered (danger)
0.00%
0 / 1
17.90
 getPath
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 getFolderPathPlain
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 isDescendant
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 hasDocuments
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 hasDocumentByName
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getDocuments
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
17
 countChildren
93.18% covered (success)
93.18%
41 / 44
0.00% covered (danger)
0.00%
0 / 1
15.07
 addDocument
56.10% covered (warning)
56.10%
23 / 41
0.00% covered (danger)
0.00%
0 / 1
45.42
 removeFromDatabase
50.00% covered (danger)
50.00%
16 / 32
0.00% covered (danger)
0.00%
0 / 1
30.00
 remove
74.07% covered (warning)
74.07%
20 / 27
0.00% covered (danger)
0.00%
0 / 1
26.97
 emptyFolder
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
14.50
 getAccessList
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
12.08
 clearAccessList
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 addAccess
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
8.01
 changeAccess
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
5.05
 removeAccess
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
6.07
 getAccessMode
80.00% covered (warning)
80.00%
24 / 30
0.00% covered (danger)
0.00%
0 / 1
24.53
 getGroupAccessMode
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
7.01
 getNotifyList
50.00% covered (danger)
50.00%
8 / 16
0.00% covered (danger)
0.00%
0 / 1
22.50
 cleanNotifyList
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
8.30
 addNotify
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
306
 removeNotify
55.00% covered (warning)
55.00%
11 / 20
0.00% covered (danger)
0.00%
0 / 1
13.83
 getApproversList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReadAccessList
100.00% covered (success)
100.00%
57 / 57
100.00% covered (success)
100.00%
1 / 1
26
 getFolderList
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 repair
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 getDocumentsMinMax
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getFoldersMinMax
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 reorderDocuments
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2declare(strict_types=1);
3
4/**
5 * Implementation of a folder in the document management system
6 *
7 * @category   DMS
8 * @package    SeedDMS_Core
9 * @license    GPL2
10 * @author     Markus Westphal, Malcolm Cowe, Matteo Lucarelli,
11 *             Uwe Steinmann <uwe@steinmann.cx>
12 * @copyright  Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe,
13 *             2010 Matteo Lucarelli, 2010-2024 Uwe Steinmann
14 * @version    Release: @package_version@
15 */
16
17/**
18 * Class to represent a folder in the document management system
19 *
20 * A folder in SeedDMS is equivalent to a directory in a regular file
21 * system. It can contain further subfolders and documents. Each folder
22 * has a single parent except for the root folder which has no parent.
23 *
24 * @category   DMS
25 * @package    SeedDMS_Core
26 * @version    @version@
27 * @author     Uwe Steinmann <uwe@steinmann.cx>
28 * @copyright  Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe,
29 *             2010 Matteo Lucarelli, 2010-2024 Uwe Steinmann
30 * @version    Release: @package_version@
31 */
32class SeedDMS_Core_Folder extends SeedDMS_Core_Object {
33    /**
34     * @var string name of folder
35     */
36    protected $_name;
37
38    /**
39     * @var integer id of parent folder
40     */
41    protected $_parentID;
42
43    /**
44     * @var string comment of document
45     */
46    protected $_comment;
47
48    /**
49     * @var integer id of user who is the owner
50     */
51    protected $_ownerID;
52
53    /**
54     * @var boolean true if access is inherited, otherwise false
55     */
56    protected $_inheritAccess;
57
58    /**
59     * @var integer default access if access rights are not inherited
60     */
61    protected $_defaultAccess;
62
63    /**
64     * @var array list of notifications for users and groups
65     */
66    protected $_readAccessList;
67
68    /**
69     * @var array list of notifications for users and groups
70     */
71    public $_notifyList;
72
73    /**
74     * @var integer position of folder within the parent folder
75     */
76    protected $_sequence;
77
78    /**
79     * @var
80     */
81    protected $_date;
82
83    /**
84     * @var SeedDMS_Core_Folder cached parent folder
85     */
86    protected $_parent;
87
88    /**
89     * @var SeedDMS_Core_User cached owner of folder
90     */
91    protected $_owner;
92
93    /**
94     * @var SeedDMS_Core_Folder[] cached array of sub folders
95     */
96    protected $_subFolders;
97
98    /**
99     * @var SeedDMS_Core_Document[] cache array of child documents
100     */
101    protected $_documents;
102
103    /**
104     * @var SeedDMS_Core_UserAccess[]|SeedDMS_Core_GroupAccess[]
105     */
106    protected $_accessList;
107
108    /**
109     * SeedDMS_Core_Folder constructor.
110     * @param $id
111     * @param $name
112     * @param $parentID
113     * @param $comment
114     * @param $date
115     * @param $ownerID
116     * @param $inheritAccess
117     * @param $defaultAccess
118     * @param $sequence
119     */
120    function __construct($id, $name, $parentID, $comment, $date, $ownerID, $inheritAccess, $defaultAccess, $sequence) { /* {{{ */
121        parent::__construct($id);
122        $this->_id = $id;
123        $this->_name = $name;
124        $this->_parentID = $parentID;
125        $this->_comment = $comment;
126        $this->_date = $date;
127        $this->_ownerID = $ownerID;
128        $this->_inheritAccess = $inheritAccess;
129        $this->_defaultAccess = $defaultAccess;
130        $this->_sequence = $sequence;
131        /* Cache */
132        $this->clearCache();
133    } /* }}} */
134
135    /**
136     * Clear cache of this instance.
137     *
138     * The result of some expensive database actions (e.g. get all subfolders
139     * or documents) will be saved in a class variable to speed up consecutive
140     * calls of the same method. If a second call of the same method shall not
141     * use the cache, then it must be cleared.
142     *
143     */
144    public function clearCache() { /* {{{ */
145        $this->_parent = null;
146        $this->_owner = null;
147        $this->_subFolders = null;
148        $this->_documents = null;
149        $this->_accessList = null;
150        $this->_notifyList = array();
151        $this->_readAccessList = array();
152    } /* }}} */
153
154    /**
155     * Check if this object is of type 'folder'.
156     *
157     * @param string $type type of object
158     */
159    public function isType($type) { /* {{{ */
160        return $type == 'folder';
161    } /* }}} */
162
163    /**
164     * Return an array of database fields which used for searching
165     * a term entered in the database search form
166     *
167     * @param SeedDMS_Core_DMS $dms
168     * @param array $searchin integer list of search scopes (2=name, 3=comment,
169     * 4=attributes)
170     * @return array list of database fields
171     */
172    public static function getSearchFields($dms, $searchin) { /* {{{ */
173        $db = $dms->getDB();
174
175        $searchFields = array();
176        if (in_array(2, $searchin)) {
177            $searchFields[] = "`tblFolders`.`name`";
178        }
179        if (in_array(3, $searchin)) {
180            $searchFields[] = "`tblFolders`.`comment`";
181        }
182        if (in_array(4, $searchin)) {
183            $searchFields[] = "`tblFolderAttributes`.`value`";
184        }
185        if (in_array(5, $searchin)) {
186            $searchFields[] = $db->castToText("`tblFolders`.`id`");
187        }
188        return $searchFields;
189    } /* }}} */
190
191    /**
192     * Return a sql statement with all tables used for searching.
193     * This must be a syntactically correct left join of all tables.
194     *
195     * @return string sql expression for left joining tables
196     */
197    public static function getSearchTables() { /* {{{ */
198        $sql = "`tblFolders` LEFT JOIN `tblFolderAttributes` on `tblFolders`.`id`=`tblFolderAttributes`.`folder`";
199        return $sql;
200    } /* }}} */
201
202    /**
203     * Return a folder by its database record
204     *
205     * @param array $resArr array of folder data as returned by database
206     * @param SeedDMS_Core_DMS $dms
207     * @return SeedDMS_Core_Folder|bool instance of SeedDMS_Core_Folder if document exists
208     */
209    public static function getInstanceByData($resArr, $dms) { /* {{{ */
210        $classname = $dms->getClassname('folder');
211        /** @var SeedDMS_Core_Folder $folder */
212        $folder = new $classname($resArr["id"], $resArr["name"], $resArr["parent"], $resArr["comment"], $resArr["date"], $resArr["owner"], $resArr["inheritAccess"], $resArr["defaultAccess"], $resArr["sequence"]);
213        $folder->setDMS($dms);
214        $folder = $folder->applyDecorators();
215        return $folder;
216    } /* }}} */
217
218    /**
219     * Return a folder by its id
220     *
221     * @param integer $id id of folder
222     * @param SeedDMS_Core_DMS $dms
223     * @return SeedDMS_Core_Folder|bool instance of SeedDMS_Core_Folder if document exists, null
224     * if document does not exist, false in case of error
225     */
226    public static function getInstance($id, $dms) { /* {{{ */
227        $db = $dms->getDB();
228
229        $queryStr = "SELECT * FROM `tblFolders` WHERE `id` = " . (int) $id;
230        if($dms->checkWithinRootDir && ($id != $dms->rootFolderID))
231            $queryStr .= " AND `folderList` LIKE '%:".$dms->rootFolderID.":%'";
232        $resArr = $db->getResultArray($queryStr);
233        if (is_bool($resArr) && $resArr == false)
234            return false;
235        elseif (count($resArr) != 1)
236            return null;
237
238        return self::getInstanceByData($resArr[0], $dms);
239    } /* }}} */
240
241    /**
242     * Return a folder by its name
243     *
244     * This method retrieves a folder from the database by its name. The
245     * search covers the whole database. If
246     * the parameter $folder is not null, it will search for the name
247     * only within this parent folder. It will not be done recursively.
248     *
249     * @param string $name name of the folder
250     * @param SeedDMS_Core_Folder $folder parent folder
251     * @return SeedDMS_Core_Folder|boolean found folder or false
252     */
253    public static function getInstanceByName($name, $folder, $dms) { /* {{{ */
254        if (!$name) return false;
255
256        $db = $dms->getDB();
257        $queryStr = "SELECT * FROM `tblFolders` WHERE `name` = " . $db->qstr($name);
258        if($folder)
259            $queryStr .= " AND `parent` = ". $folder->getID();
260        if($dms->checkWithinRootDir && ($id != $dms->rootFolderID))
261            $queryStr .= " AND `folderList` LIKE '%:".$dms->rootFolderID.":%'";
262        $queryStr .= " LIMIT 1";
263        $resArr = $db->getResultArray($queryStr);
264
265        if (is_bool($resArr) && $resArr == false)
266            return false;
267
268        if(!$resArr)
269            return null;
270
271        return self::getInstanceByData($resArr[0], $dms);
272    } /* }}} */
273
274    /**
275     * Apply decorators
276     *
277     * @return object final object after all decorators has been applied
278     */
279    function applyDecorators() { /* {{{ */
280        if($decorators = $this->_dms->getDecorators('folder')) {
281            $s = $this;
282            foreach($decorators as $decorator) {
283                $s = new $decorator($s);
284            }
285            return $s;
286        } else {
287            return $this;
288        }
289    } /* }}} */
290
291    /**
292     * Get the name of the folder.
293     *
294     * @return string name of folder
295     */
296    public function getName() { return $this->_name; }
297
298    /**
299     * Set the name of the folder.
300     *
301     * @param string $newName set a new name of the folder
302     * @return bool
303     */
304    public function setName($newName) { /* {{{ */
305        $db = $this->_dms->getDB();
306
307        /* Check if 'onPreSetName' callback is set */
308        if(isset($this->_dms->callbacks['onPreSetName'])) {
309            foreach($this->_dms->callbacks['onPreSetName'] as $callback) {
310                $ret = call_user_func($callback[0], $callback[1], $this, $newName);
311                if(is_bool($ret))
312                    return $ret;
313            }
314        }
315
316        $queryStr = "UPDATE `tblFolders` SET `name` = " . $db->qstr($newName) . " WHERE `id` = ". $this->_id;
317        if (!$db->getResult($queryStr))
318            return false;
319
320        $oldName = $this->_name;
321        $this->_name = $newName;
322
323        /* Check if 'onPostSetName' callback is set */
324        if(isset($this->_dms->callbacks['onPostSetName'])) {
325            foreach($this->_dms->callbacks['onPostSetName'] as $callback) {
326                $ret = call_user_func($callback[0], $callback[1], $this, $oldName);
327                if(is_bool($ret))
328                    return $ret;
329            }
330        }
331
332        return true;
333    } /* }}} */
334
335    /**
336     * Returns comment of folder
337     *
338     * @return string comment
339     */
340    public function getComment() { return $this->_comment; }
341
342    /**
343     * Set comment of folder
344     *
345     * This method calls the hooks `onPreSetComment` and `onPostSetComment`.
346     *
347     * @param $newComment new comment
348     * @return bool true if comment could be set, otherwise false
349     */
350    public function setComment($newComment) { /* {{{ */
351        $db = $this->_dms->getDB();
352
353        /* Check if 'onPreSetComment' callback is set */
354        if(isset($this->_dms->callbacks['onPreSetComment'])) {
355            foreach($this->_dms->callbacks['onPreSetComment'] as $callback) {
356                $ret = call_user_func($callback[0], $callback[1], $this, $newComment);
357                if(is_bool($ret))
358                    return $ret;
359            }
360        }
361
362        $queryStr = "UPDATE `tblFolders` SET `comment` = " . $db->qstr($newComment) . " WHERE `id` = ". $this->_id;
363        if (!$db->getResult($queryStr))
364            return false;
365
366        $oldComment = $this->_comment;
367        $this->_comment = $newComment;
368
369        /* Check if 'onPostSetComment' callback is set */
370        if(isset($this->_dms->callbacks['onPostSetComment'])) {
371            foreach($this->_dms->callbacks['onPostSetComment'] as $callback) {
372                $ret = call_user_func($callback[0], $callback[1], $this, $oldComment);
373                if(is_bool($ret))
374                    return $ret;
375            }
376        }
377
378        return true;
379    } /* }}} */
380
381    /**
382     * Return creation date of folder
383     *
384     * @return integer unix timestamp of creation date
385     */
386    public function getDate() { /* {{{ */
387        return $this->_date;
388    } /* }}} */
389
390    /**
391     * Set creation date of the folder
392     *
393     * @param integer $date timestamp of creation date. If false then set it
394     * to the current timestamp
395     * @return boolean true on success
396     */
397    function setDate($date) { /* {{{ */
398        $db = $this->_dms->getDB();
399
400        if($date === false)
401            $date = time();
402        else {
403            if(!is_numeric($date))
404                return false;
405        }
406
407        $queryStr = "UPDATE `tblFolders` SET `date` = " . (int) $date . " WHERE `id` = ". $this->_id;
408        if (!$db->getResult($queryStr))
409            return false;
410        $this->_date = $date;
411        return true;
412    } /* }}} */
413
414    /**
415     * Returns the parent
416     *
417     * @return null|bool|SeedDMS_Core_Folder returns null, if there is no parent folder
418     * and false in case of an error
419     */
420    public function getParent() { /* {{{ */
421        if ($this->_id == $this->_dms->rootFolderID || empty($this->_parentID)) {
422            return null;
423        }
424
425        if (!isset($this->_parent)) {
426            $this->_parent = $this->_dms->getFolder($this->_parentID);
427        }
428        return $this->_parent;
429    } /* }}} */
430
431    /**
432     * Check if the folder is subfolder
433     *
434     * This method checks if the current folder is in the path of the 
435     * passed subfolder. In that case the current folder is a parent,
436     * grant parent, grant grant parent, etc. of the subfolder or
437     * to say it differently the passed folder is somewhere below the
438     * current folder.
439     *
440     * This is basically the opposite of {@see SeedDMS_Core_Folder::isDescendant()}
441     *
442     * @param SeedDMS_Core_Folder $subfolder folder to be checked if it is
443     * a subfolder on any level of the current folder
444     * @return bool true if passed folder is a subfolder, otherwise false
445     */
446    function isSubFolder($subfolder) { /* {{{ */
447        $target_path = $subfolder->getPath();
448        foreach($target_path as $next_folder) {
449            // the target folder contains this instance in the parent path
450            if($this->getID() == $next_folder->getID()) return true;
451        }
452        return false;
453    } /* }}} */
454
455    /**
456     * Set a new folder
457     *
458     * This method moves a folder from one parent folder into another parent
459     * folder. It will fail if the root folder is moved or the folder is
460     * moved into one of its own subfolders.
461     *
462     * @param SeedDMS_Core_Folder $newParent new parent folder
463     * @return boolean true if operation was successful otherwise false
464     */
465    public function setParent($newParent) { /* {{{ */
466        $db = $this->_dms->getDB();
467
468        if ($this->_id == $this->_dms->rootFolderID || empty($this->_parentID)) {
469            return false;
470        }
471
472        /* Check if the new parent is the folder to be moved or even
473         * a subfolder of that folder
474         */
475        if($this->isSubFolder($newParent)) {
476            return false;
477        }
478
479        // Update the folderList of the folder
480        $pathPrefix="";
481        $path = $newParent->getPath();
482        foreach ($path as $f) {
483            $pathPrefix .= ":".$f->getID();
484        }
485        if (strlen($pathPrefix)>1) {
486            $pathPrefix .= ":";
487        }
488        $queryStr = "UPDATE `tblFolders` SET `parent` = ".$newParent->getID().", `folderList`='".$pathPrefix."' WHERE `id` = ". $this->_id;
489        $res = $db->getResult($queryStr);
490        if (!$res)
491            return false;
492
493        $this->_parentID = $newParent->getID();
494        $this->_parent = $newParent;
495
496        // Must also ensure that any documents in this folder tree have their
497        // folderLists updated.
498        $pathPrefix="";
499        $path = $this->getPath();
500        foreach ($path as $f) {
501            $pathPrefix .= ":".$f->getID();
502        }
503        if (strlen($pathPrefix)>1) {
504            $pathPrefix .= ":";
505        }
506
507        /* Update path in folderList for all documents */
508        $queryStr = "SELECT `tblDocuments`.`id`, `tblDocuments`.`folderList` FROM `tblDocuments` WHERE `folderList` LIKE '%:".$this->_id.":%'";
509        $resArr = $db->getResultArray($queryStr);
510        if (is_bool($resArr) && $resArr == false)
511            return false;
512
513        foreach ($resArr as $row) {
514            $newPath = preg_replace("/^.*:".$this->_id.":(.*$)/", $pathPrefix."\\1", $row["folderList"]);
515            $queryStr="UPDATE `tblDocuments` SET `folderList` = '".$newPath."' WHERE `tblDocuments`.`id` = '".$row["id"]."'";
516            /** @noinspection PhpUnusedLocalVariableInspection */
517            $res = $db->getResult($queryStr);
518        }
519
520        /* Update path in folderList for all folders */
521        $queryStr = "SELECT `tblFolders`.`id`, `tblFolders`.`folderList` FROM `tblFolders` WHERE `folderList` LIKE '%:".$this->_id.":%'";
522        $resArr = $db->getResultArray($queryStr);
523        if (is_bool($resArr) && $resArr == false)
524            return false;
525
526        foreach ($resArr as $row) {
527            $newPath = preg_replace("/^.*:".$this->_id.":(.*$)/", $pathPrefix."\\1", $row["folderList"]);
528            $queryStr="UPDATE `tblFolders` SET `folderList` = '".$newPath."' WHERE `tblFolders`.`id` = '".$row["id"]."'";
529            /** @noinspection PhpUnusedLocalVariableInspection */
530            $res = $db->getResult($queryStr);
531        }
532
533        return true;
534    } /* }}} */
535
536    /**
537     * Returns the owner
538     *
539     * @return object owner of the folder
540     */
541    public function getOwner() { /* {{{ */
542        if (!isset($this->_owner))
543            $this->_owner = $this->_dms->getUser($this->_ownerID);
544        return $this->_owner;
545    } /* }}} */
546
547    /**
548     * Set the owner
549     *
550     * @param SeedDMS_Core_User $newOwner of the folder
551     * @return boolean true if successful otherwise false
552     */
553    function setOwner($newOwner) { /* {{{ */
554        $db = $this->_dms->getDB();
555
556        /* Check if 'onPreSetOwner' callback is set */
557        if(isset($this->_dms->callbacks['onPreSetOwner'])) {
558            foreach($this->_dms->callbacks['onPreSetOwner'] as $callback) {
559                $ret = call_user_func($callback[0], $callback[1], $this, $newOwner);
560                if(is_bool($ret))
561                    return $ret;
562            }
563        }
564
565        $queryStr = "UPDATE `tblFolders` set `owner` = " . $newOwner->getID() . " WHERE `id` = " . $this->_id;
566        if (!$db->getResult($queryStr))
567            return false;
568
569        $this->_ownerID = $newOwner->getID();
570        $this->_owner = $newOwner;
571
572        $this->_readAccessList = array();
573
574        /* Check if 'onPostSetOwner' callback is set */
575        if(isset($this->_dms->callbacks['onPostSetOwner'])) {
576            foreach($this->_dms->callbacks['onPostSetOwner'] as $callback) {
577                $ret = call_user_func($callback[0], $callback[1], $this, $oldOwner);
578                if(is_bool($ret))
579                    return $ret;
580            }
581        }
582
583        return true;
584    } /* }}} */
585
586    /**
587     * Returns default access
588     *
589     * If access rights are inherited, the method will return the
590     * default access of the parent folder.
591     *
592     * @return boolean|int access right or fals in case of an error
593     */
594    function getDefaultAccess() { /* {{{ */
595        if ($this->inheritsAccess()) {
596            /* Access is supposed to be inherited but it could be that there
597             * is no parent because the configured root folder id is somewhere
598             * below the actual root folder.
599             */
600            $res = $this->getParent();
601            if ($res)
602                return $this->_parent->getDefaultAccess();
603        }
604
605        return $this->_defaultAccess;
606    } /* }}} */
607
608    /**
609     * Set default access mode
610     *
611     * This method sets the default access mode and also removes all notifiers which
612     * will not have read access anymore.
613     *
614     * @param integer $mode access mode
615     * @param boolean $noclean set to true if notifier list shall not be clean up
616     * @return bool
617     */
618    function setDefaultAccess($mode, $noclean=false) { /* {{{ */
619        $db = $this->_dms->getDB();
620
621        $queryStr = "UPDATE `tblFolders` set `defaultAccess` = " . (int) $mode . " WHERE `id` = " . $this->_id;
622        if (!$db->getResult($queryStr))
623            return false;
624
625        $this->_defaultAccess = $mode;
626        $this->_readAccessList = array();
627
628        if(!$noclean)
629            $this->cleanNotifyList();
630
631        return true;
632    } /* }}} */
633
634    /**
635     * Check, if folder inherits access rights
636     *
637     * @return boolean true, if access rights are inherited, otherwise false
638     */
639    function inheritsAccess() { return $this->_inheritAccess; }
640
641    /**
642     * Set inherited access mode
643     *
644     * Setting inherited access mode will set or unset the internal flag which
645     * controls if the access mode is inherited from the parent folder or not.
646     * It will not modify the
647     * access control list for the current object. It will remove all
648     * notifications of users which do not even have read access anymore
649     * after setting or unsetting inherited access.
650     *
651     * @param boolean $inheritAccess set to true for setting and false for
652     *        unsetting inherited access mode
653     * @param boolean $noclean set to true if notifier list shall not be clean up
654     * @return boolean true if operation was successful otherwise false
655     */
656    function setInheritAccess($inheritAccess, $noclean=false) { /* {{{ */
657        $db = $this->_dms->getDB();
658
659        $inheritAccess = ($inheritAccess) ? "1" : "0";
660
661        $queryStr = "UPDATE `tblFolders` SET `inheritAccess` = " . (int) $inheritAccess . " WHERE `id` = " . $this->_id;
662        if (!$db->getResult($queryStr))
663            return false;
664
665        $this->_inheritAccess = $inheritAccess;
666        $this->_readAccessList = array();
667
668        if(!$noclean)
669            $this->cleanNotifyList();
670
671        return true;
672    } /* }}} */
673
674    function getSequence() { return $this->_sequence; }
675
676    function setSequence($seq) { /* {{{ */
677        $db = $this->_dms->getDB();
678
679        $queryStr = "UPDATE `tblFolders` SET `sequence` = " . $seq . " WHERE `id` = " . $this->_id;
680        if (!$db->getResult($queryStr))
681            return false;
682
683        $this->_sequence = $seq;
684        return true;
685    } /* }}} */
686
687    /**
688     * Check, if folder has subfolders
689     *
690     * This method just checks if a folder has subfolders disregarding
691     * any access rights.
692     *
693     * @return int number of subfolders or false in case of an error
694     */
695    function hasSubFolders() { /* {{{ */
696        $db = $this->_dms->getDB();
697        if (isset($this->_subFolders)) {
698            /** @noinspection PhpUndefinedFieldInspection */
699            return count($this->_subFolders);
700        }
701        $queryStr = "SELECT count(*) as c FROM `tblFolders` WHERE `parent` = " . $this->_id;
702        $resArr = $db->getResultArray($queryStr);
703        if (is_bool($resArr) && !$resArr)
704            return false;
705
706        return (int) $resArr[0]['c'];
707    } /* }}} */
708
709    /**
710     * Check, if folder has as subfolder with the given name
711     *
712     * @param string $name
713     * @return bool true if subfolder exists, false if not or in case
714     * of an error
715     */
716    function hasSubFolderByName($name) { /* {{{ */
717        $db = $this->_dms->getDB();
718        /* Always check the database instead of iterating over $this->_documents, because
719         * it is probably not slower
720         */
721        $queryStr = "SELECT count(*) as c FROM `tblFolders` WHERE `parent` = " . $this->_id . " AND `name` = ".$db->qstr($name);
722        $resArr = $db->getResultArray($queryStr);
723        if (is_bool($resArr) && !$resArr)
724            return false;
725
726        return ($resArr[0]['c'] > 0);
727    } /* }}} */
728
729    /**
730     * Returns a list of subfolders
731     *
732     * This method does not check for access rights. Use
733     * {@link SeedDMS_Core_DMS::filterAccess} for checking each folder against
734     * the currently logged in user and the access rights.
735     *
736     * @param string $orderby if set to 'n' the list is ordered by name, otherwise
737     *        it will be ordered by sequence
738     * @param string $dir direction of sorting (asc or desc)
739     * @param integer $limit limit number of subfolders
740     * @param integer $offset offset in retrieved list of subfolders
741     * @return SeedDMS_Core_Folder[]|bool list of folder objects or false in case of an error
742     */
743    function getSubFolders($orderby="", $dir="asc", $limit=0, $offset=0) { /* {{{ */
744        $db = $this->_dms->getDB();
745
746        if (!isset($this->_subFolders)) {
747            $queryStr = "SELECT * FROM `tblFolders` WHERE `parent` = " . $this->_id;
748
749            if ($orderby && $orderby[0]=="n") $queryStr .= " ORDER BY `name`";
750            elseif ($orderby && $orderby[0]=="s") $queryStr .= " ORDER BY `sequence`";
751            elseif ($orderby && $orderby[0]=="d") $queryStr .= " ORDER BY `date`";
752            if($dir == 'desc')
753                $queryStr .= " DESC";
754            if(is_int($limit) && $limit > 0) {
755                $queryStr .= " LIMIT ".$limit;
756                if(is_int($offset) && $offset > 0)
757                    $queryStr .= " OFFSET ".$offset;
758            }
759
760            $resArr = $db->getResultArray($queryStr);
761            if (is_bool($resArr) && $resArr == false)
762                return false;
763
764            $classname = $this->_dms->getClassname('folder');
765            $this->_subFolders = array();
766            for ($i = 0; $i < count($resArr); $i++)
767//                $this->_subFolders[$i] = $this->_dms->getFolder($resArr[$i]["id"]);
768                $this->_subFolders[$i] = $classname::getInstanceByData($resArr[$i], $this->_dms);
769        }
770
771        return $this->_subFolders;
772    } /* }}} */
773
774    /**
775     * Add a new subfolder
776     *
777     * @param string $name name of folder
778     * @param string $comment comment of folder
779     * @param object $owner owner of folder
780     * @param integer $sequence position of folder in list of sub folders.
781     * @param array $attributes list of document attributes. The element key
782     *        must be the id of the attribute definition.
783     * @return bool|SeedDMS_Core_Folder
784     *         an error.
785     */
786    function addSubFolder($name, $comment, $owner, $sequence, $attributes=array()) { /* {{{ */
787        $db = $this->_dms->getDB();
788
789        // Set the folderList of the folder
790        $pathPrefix="";
791        $path = $this->getPath();
792        foreach ($path as $f) {
793            $pathPrefix .= ":".$f->getID();
794        }
795        if (strlen($pathPrefix)>1) {
796            $pathPrefix .= ":";
797        }
798
799        $db->startTransaction();
800
801        //inheritAccess = true, defaultAccess = M_READ
802        $queryStr = "INSERT INTO `tblFolders` (`name`, `parent`, `folderList`, `comment`, `date`, `owner`, `inheritAccess`, `defaultAccess`, `sequence`) ".
803                    "VALUES (".$db->qstr($name).", ".$this->_id.", ".$db->qstr($pathPrefix).", ".$db->qstr($comment).", ".$db->getCurrentTimestamp().", ".$owner->getID().", 1, ".M_READ.", ". $sequence.")";
804        if (!$db->getResult($queryStr)) {
805            $db->rollbackTransaction();
806            return false;
807        }
808        $newFolder = $this->_dms->getFolder($db->getInsertID('tblFolders'));
809        unset($this->_subFolders);
810
811        if($attributes) {
812            foreach($attributes as $attrdefid=>$attribute) {
813                if($attribute)
814                    if($attrdef = $this->_dms->getAttributeDefinition($attrdefid)) {
815                        if(!$newFolder->setAttributeValue($attrdef, $attribute)) {
816                            $db->rollbackTransaction();
817                            return false;
818                        }
819                    } else {
820                        $db->rollbackTransaction();
821                        return false;
822                    }
823            }
824        }
825
826        $db->commitTransaction();
827
828        /* Check if 'onPostAddSubFolder' callback is set */
829        if(isset($this->_dms->callbacks['onPostAddSubFolder'])) {
830            foreach($this->_dms->callbacks['onPostAddSubFolder'] as $callback) {
831                    /** @noinspection PhpStatementHasEmptyBodyInspection */
832                    if(!call_user_func($callback[0], $callback[1], $newFolder)) {
833                }
834            }
835        }
836
837        return $newFolder;
838    } /* }}} */
839
840    /**
841     * Returns an array of all parents, grand parent, etc. up to the root folder.
842     *
843     * The folder itself is the last element of the array.
844     *
845     * @return array|bool
846     */
847    function getPath() { /* {{{ */
848        if (!isset($this->_parentID) || ($this->_parentID == "") || ($this->_parentID == 0) || ($this->_id == $this->_dms->rootFolderID)) {
849            return array($this);
850        }
851        else {
852            $res = $this->getParent();
853            if (!$res) return false;
854
855            $path = $this->_parent->getPath();
856            if (!$path) return false;
857
858            array_push($path, $this);
859            return $path;
860        }
861    } /* }}} */
862
863    /**
864     * Returns a path like used for a file system
865     *
866     * This path contains by default spaces around the slashes for better readability.
867     * Run str_replace(' / ', '/', $path) on it or pass '/' as $sep to get a valid unix
868     * file system path.
869     *
870     * The returned path is not a real path in a file system. It just uses
871     * the common syntax on unix systems to format a path.
872     *
873     * @param bool $skiproot skip the name of the root folder and start with $sep
874     * @param string $sep separator between path elements
875     * @return string path separated with ' / '
876     */
877    function getFolderPathPlain($skiproot = false, $sep = ' / ') { /* {{{ */
878        $path="".$sep;
879        $folderPath = $this->getPath();
880        for ($i = 0; $i < count($folderPath); $i++) {
881            if($i > 0 || !$skiproot) {
882                $path .= $folderPath[$i]->getName();
883                if ($i+1 < count($folderPath))
884                    $path .= $sep;
885            }
886        }
887        return trim($path);
888    } /* }}} */
889
890    /**
891     * Check, if this folder is a subfolder of a given folder
892     *
893     * This is basically the opposite of {@see SeedDMS_Core_Folder::isSubFolder()}
894     *
895     * @param object $folder parent folder
896     * @return boolean true if folder is a subfolder
897     */
898    function isDescendant($folder) { /* {{{ */
899        /* If the current folder has no parent it cannot be a descendant */
900        if(!$this->getParent())
901            return false;
902        /* Check if the passed folder is the parent of the current folder.
903         * In that case the current folder is a subfolder of the passed folder.
904         */
905        if($this->getParent()->getID() == $folder->getID())
906            return true;
907        /* Recursively go up to the root folder */
908        return $this->getParent()->isDescendant($folder);
909    } /* }}} */
910
911    /**
912     * Check, if folder has documents
913     *
914     * This method just checks if a folder has documents diregarding
915     * any access rights.
916     *
917     * @return int number of documents or false in case of an error
918     */
919    function hasDocuments() { /* {{{ */
920        $db = $this->_dms->getDB();
921        /* Do not use the cache because it may not contain all documents if
922         * the former call getDocuments() limited the number of documents
923        if (isset($this->_documents)) {
924            return count($this->_documents);
925        }
926         */
927        $queryStr = "SELECT count(*) as c FROM `tblDocuments` WHERE `folder` = " . $this->_id;
928        $resArr = $db->getResultArray($queryStr);
929        if (is_bool($resArr) && !$resArr)
930            return false;
931
932        return (int) $resArr[0]['c'];
933    } /* }}} */
934
935    /**
936     * Check if folder has document with given name
937     *
938     * @param string $name
939     * @return bool true if document exists, false if not or in case
940     * of an error
941     */
942    function hasDocumentByName($name) { /* {{{ */
943        $db = $this->_dms->getDB();
944        /* Always check the database instead of iterating over $this->_documents, because
945         * it is probably not slower
946         */
947        $queryStr = "SELECT count(*) as c FROM `tblDocuments` WHERE `folder` = " . $this->_id . " AND `name` = ".$db->qstr($name);
948        $resArr = $db->getResultArray($queryStr);
949        if (is_bool($resArr) && !$resArr)
950            return false;
951
952        return ($resArr[0]['c'] > 0);
953    } /* }}} */
954
955    /**
956     * Get all documents of the folder
957     *
958     * This method does not check for access rights. Use
959     * {@link SeedDMS_Core_DMS::filterAccess} for checking each document against
960     * the currently logged in user and the access rights.
961     *
962     * @param string $orderby if set to 'n' the list is ordered by name, otherwise
963     *        it will be ordered by sequence
964     * @param string $dir direction of sorting (asc or desc)
965     * @param integer $limit limit number of documents
966     * @param integer $offset offset in retrieved list of documents
967     * @return SeedDMS_Core_Document[]|bool list of documents or false in case of an error
968     */
969    function getDocuments($orderby="", $dir="asc", $limit=0, $offset=0) { /* {{{ */
970        $db = $this->_dms->getDB();
971
972        if (!isset($this->_documents)) {
973            $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lock` FROM `tblDocuments` LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id` = `tblDocumentLocks`.`document` WHERE `folder` = " . $this->_id;
974            if ($orderby && $orderby[0]=="n") $queryStr .= " ORDER BY `name`";
975            elseif($orderby && $orderby[0]=="s") $queryStr .= " ORDER BY `sequence`";
976            elseif($orderby && $orderby[0]=="d") $queryStr .= " ORDER BY `date`";
977            if($dir == 'desc')
978                $queryStr .= " DESC";
979            if(is_int($limit) && $limit > 0) {
980                $queryStr .= " LIMIT ".$limit;
981                if(is_int($offset) && $offset > 0)
982                    $queryStr .= " OFFSET ".$offset;
983            }
984
985            $resArr = $db->getResultArray($queryStr);
986            if (is_bool($resArr) && !$resArr)
987                return false;
988
989            $this->_documents = array();
990            $classname = $this->_dms->getClassname('document');
991            foreach ($resArr as $row) {
992                    $row['lock'] = !$row['lock'] ? -1 : $row['lock'];
993//                array_push($this->_documents, $this->_dms->getDocument($row["id"]));
994                array_push($this->_documents, $classname::getInstanceByData($row, $this->_dms));
995            }
996        }
997        return $this->_documents;
998    } /* }}} */
999
1000    /**
1001     * Count all documents and subfolders of the folder
1002     *
1003     * This method also counts documents and folders of subfolders, so
1004     * basically it works like recursively counting children.
1005     *
1006     * This method checks for access rights up the given limit. If more
1007     * documents or folders are found, the returned value will be the number
1008     * of objects available and the precise flag in the return array will be
1009     * set to false. This number should not be revelead to the
1010     * user, because it allows to gain information about the existens of
1011     * objects without access right.
1012     * Setting the parameter $limit to 0 will turn off access right checking
1013     * which is reasonable if the $user is an administrator.
1014     *
1015     * @param SeedDMS_Core_User $user
1016     * @param integer $limit maximum number of folders and documents that will
1017     *        be precisly counted by taken the access rights into account
1018     * @return array|bool with four elements 'document_count', 'folder_count'
1019     *        'document_precise', 'folder_precise' holding
1020     * the counted number and a flag if the number is precise.
1021     * @internal param string $orderby if set to 'n' the list is ordered by name, otherwise
1022     *        it will be ordered by sequence
1023     */
1024    function countChildren($user, $limit=10000) { /* {{{ */
1025        $db = $this->_dms->getDB();
1026
1027        $pathPrefix="";
1028        $path = $this->getPath();
1029        foreach ($path as $f) {
1030            $pathPrefix .= ":".$f->getID();
1031        }
1032        if (strlen($pathPrefix)>1) {
1033            $pathPrefix .= ":";
1034        }
1035
1036        $queryStr = "SELECT id FROM `tblFolders` WHERE `folderList` like '".$pathPrefix. "%'";
1037        $resArr = $db->getResultArray($queryStr);
1038        if (is_bool($resArr) && !$resArr)
1039            return false;
1040
1041        $result = array();
1042
1043        $folders = array();
1044        $folderids = array($this->_id);
1045        $cfolders = count($resArr);
1046        if($cfolders < $limit) {
1047            foreach ($resArr as $row) {
1048                $folder = $this->_dms->getFolder($row["id"]);
1049                if ($folder->getAccessMode($user) >= M_READ) {
1050                    array_push($folders, $folder);
1051                    array_push($folderids, $row['id']);
1052                }
1053            }
1054            $result['folder_count'] = count($folders);
1055            $result['folder_precise'] = true;
1056        } else {
1057            foreach ($resArr as $row) {
1058                array_push($folderids, $row['id']);
1059            }
1060            $result['folder_count'] = $cfolders;
1061            $result['folder_precise'] = false;
1062        }
1063
1064        $documents = array();
1065        if($folderids) {
1066            $queryStr = "SELECT id FROM `tblDocuments` WHERE `folder` in (".implode(',', $folderids). ")";
1067            $resArr = $db->getResultArray($queryStr);
1068            if (is_bool($resArr) && !$resArr)
1069                return false;
1070
1071            $cdocs = count($resArr);
1072            if($cdocs < $limit) {
1073                foreach ($resArr as $row) {
1074                    $document = $this->_dms->getDocument($row["id"]);
1075                    if ($document->getAccessMode($user) >= M_READ)
1076                        array_push($documents, $document);
1077                }
1078                $result['document_count'] = count($documents);
1079                $result['document_precise'] = true;
1080            } else {
1081                $result['document_count'] = $cdocs;
1082                $result['document_precise'] = false;
1083            }
1084        }
1085
1086        return $result;
1087    } /* }}} */
1088
1089    /**
1090     * Add a new document to the folder
1091     * This method will add a new document and its content from a given file.
1092     * It does not check for access rights on the folder. The new documents
1093     * default access right is read only and the access right is inherited.
1094     *
1095     * @param string $name name of new document
1096     * @param string $comment comment of new document
1097     * @param integer $expires expiration date as a unix timestamp or 0 for no
1098     *        expiration date
1099     * @param object $owner owner of the new document
1100     * @param SeedDMS_Core_User $keywords keywords of new document
1101     * @param SeedDMS_Core_DocumentCategory[] $categories list of category objects
1102     * @param string $tmpFile the path of the file containing the content
1103     * @param string $orgFileName the original file name
1104     * @param string $fileType usually the extension of the filename
1105     * @param string $mimeType mime type of the content
1106     * @param float $sequence position of new document within the folder
1107     * @param array $reviewers list of users who must review this document
1108     * @param array $approvers list of users who must approve this document
1109     * @param int|string $reqversion version number of the content
1110     * @param string $version_comment comment of the content. If left empty
1111     *        the $comment will be used.
1112     * @param array $attributes list of document attributes. The element key
1113     *        must be the id of the attribute definition.
1114     * @param array $version_attributes list of document version attributes.
1115     *        The element key must be the id of the attribute definition.
1116     * @param SeedDMS_Core_Workflow $workflow
1117     * @param integer $initstate initial document state (only S_RELEASED and
1118     *        S_DRAFT are allowed)
1119     * @return array|bool false in case of error, otherwise an array
1120     *        containing two elements. The first one is the new document, the
1121     * second one is the result set returned when inserting the content.
1122     */
1123    function addDocument($name, $comment, $expires, $owner, $keywords, $categories, $tmpFile, $orgFileName, $fileType, $mimeType, $sequence, $reviewers=array(), $approvers=array(),$reqversion=0,$version_comment="", $attributes=array(), $version_attributes=array(), $workflow=null, $initstate=S_RELEASED) { /* {{{ */
1124        $db = $this->_dms->getDB();
1125
1126        $expires = (!$expires) ? 0 : $expires;
1127
1128        // Must also ensure that the document has a valid folderList.
1129        $pathPrefix="";
1130        $path = $this->getPath();
1131        foreach ($path as $f) {
1132            $pathPrefix .= ":".$f->getID();
1133        }
1134        if (strlen($pathPrefix)>1) {
1135            $pathPrefix .= ":";
1136        }
1137
1138        $db->startTransaction();
1139
1140        $queryStr = "INSERT INTO `tblDocuments` (`name`, `comment`, `date`, `expires`, `owner`, `folder`, `folderList`, `inheritAccess`, `defaultAccess`, `locked`, `keywords`, `sequence`) VALUES ".
1141                    "(".$db->qstr($name).", ".$db->qstr($comment).", ".$db->getCurrentTimestamp().", ".(int) $expires.", ".$owner->getID().", ".$this->_id.",".$db->qstr($pathPrefix).", 1, ".M_READ.", -1, ".$db->qstr($keywords).", " . $sequence . ")";
1142        if (!$db->getResult($queryStr)) {
1143            $db->rollbackTransaction();
1144            return false;
1145        }
1146
1147        $document = $this->_dms->getDocument($db->getInsertID('tblDocuments'));
1148
1149        $curuser = $this->_dms->getLoggedInUser();
1150        $res = $document->addContent($version_comment, $curuser ? $curuser : $owner, $tmpFile, $orgFileName, $fileType, $mimeType, $reviewers, $approvers, $reqversion, $version_attributes, $workflow, $initstate);
1151
1152        if (is_bool($res) && !$res) {
1153            $db->rollbackTransaction();
1154            return false;
1155        }
1156
1157        if($categories) {
1158            if(!$document->setCategories($categories)) {
1159                $document->remove();
1160                $db->rollbackTransaction();
1161                return false;
1162            }
1163        }
1164
1165        if($attributes) {
1166            foreach($attributes as $attrdefid=>$attribute) {
1167                /* $attribute can be a string or an array */
1168                if($attribute) {
1169                    if($attrdef = $this->_dms->getAttributeDefinition($attrdefid)) {
1170                        if(!$document->setAttributeValue($attrdef, $attribute)) {
1171                            $document->remove();
1172                            $db->rollbackTransaction();
1173                            return false;
1174                        }
1175                    } else {
1176                        $document->remove();
1177                        $db->rollbackTransaction();
1178                        return false;
1179                    }
1180                }
1181            }
1182        }
1183
1184        $db->commitTransaction();
1185
1186        /* Check if 'onPostAddDocument' callback is set */
1187        if(isset($this->_dms->callbacks['onPostAddDocument'])) {
1188            foreach($this->_dms->callbacks['onPostAddDocument'] as $callback) {
1189                    /** @noinspection PhpStatementHasEmptyBodyInspection */
1190                    if(!call_user_func($callback[0], $callback[1], $document)) {
1191                }
1192            }
1193        }
1194
1195        return array($document, $res);
1196    } /* }}} */
1197
1198    /**
1199     * Remove a single folder
1200     *
1201     * Removes just a single folder, but not its subfolders or documents
1202     * This method will fail if the folder has subfolders or documents
1203     * because of referencial integrity errors.
1204     *
1205     * @return boolean true on success, false in case of an error
1206     */
1207    protected function removeFromDatabase() { /* {{{ */
1208        $db = $this->_dms->getDB();
1209
1210        /* Check if 'onPreRemoveFolder' callback is set */
1211        if(isset($this->_dms->callbacks['onPreRemoveFromDatabaseFolder'])) {
1212            foreach($this->_dms->callbacks['onPreRemoveFromDatabaseFolder'] as $callback) {
1213                $ret = call_user_func($callback[0], $callback[1], $this);
1214                if(is_bool($ret))
1215                    return $ret;
1216            }
1217        }
1218
1219        $db->startTransaction();
1220        // unset homefolder as it will no longer exist
1221        $queryStr = "UPDATE `tblUsers` SET `homefolder`=NULL WHERE `homefolder` =  " . $this->_id;
1222        if (!$db->getResult($queryStr)) {
1223            $db->rollbackTransaction();
1224            return false;
1225        }
1226
1227        // Remove database entries
1228        $queryStr = "DELETE FROM `tblFolders` WHERE `id` =  " . $this->_id;
1229        if (!$db->getResult($queryStr)) {
1230            $db->rollbackTransaction();
1231            return false;
1232        }
1233        $queryStr = "DELETE FROM `tblFolderAttributes` WHERE `folder` =  " . $this->_id;
1234        if (!$db->getResult($queryStr)) {
1235            $db->rollbackTransaction();
1236            return false;
1237        }
1238        $queryStr = "DELETE FROM `tblACLs` WHERE `target` = ". $this->_id. " AND `targetType` = " . T_FOLDER;
1239        if (!$db->getResult($queryStr)) {
1240            $db->rollbackTransaction();
1241            return false;
1242        }
1243
1244        $queryStr = "DELETE FROM `tblNotify` WHERE `target` = ". $this->_id. " AND `targetType` = " . T_FOLDER;
1245        if (!$db->getResult($queryStr)) {
1246            $db->rollbackTransaction();
1247            return false;
1248        }
1249        $db->commitTransaction();
1250
1251        /* Check if 'onPostRemoveFolder' callback is set */
1252        if(isset($this->_dms->callbacks['onPostRemoveFromDatabaseFolder'])) {
1253            foreach($this->_dms->callbacks['onPostRemoveFromDatabaseFolder'] as $callback) {
1254                /** @noinspection PhpStatementHasEmptyBodyInspection */
1255                if(!call_user_func($callback[0], $callback[1], $this->_id)) {
1256                }
1257            }
1258        }
1259
1260        return true;
1261    } /* }}} */
1262
1263    /**
1264     * Remove recursively a folder
1265     *
1266     * Removes a folder, all its subfolders and documents
1267     * This method triggers the callbacks onPreRemoveFolder and onPostRemoveFolder.
1268     * If onPreRemoveFolder returns a boolean then this method will return
1269     * imediately with the value returned by the callback. Otherwise the
1270     * regular removal is executed, which in turn
1271     * triggers further onPreRemoveFolder and onPostRemoveFolder callbacks
1272     * and its counterparts for documents (onPreRemoveDocument, onPostRemoveDocument).
1273     *
1274     * @return boolean true on success, false in case of an error
1275     */
1276    function remove() { /* {{{ */
1277        /** @noinspection PhpUnusedLocalVariableInspection */
1278        $db = $this->_dms->getDB();
1279
1280        // Do not delete the root folder.
1281        if ($this->_id == $this->_dms->rootFolderID || !isset($this->_parentID) || ($this->_parentID == null) || ($this->_parentID == "") || ($this->_parentID == 0)) {
1282            return false;
1283        }
1284
1285        /* Check if 'onPreRemoveFolder' callback is set */
1286        if(isset($this->_dms->callbacks['onPreRemoveFolder'])) {
1287            foreach($this->_dms->callbacks['onPreRemoveFolder'] as $callback) {
1288                 $ret = call_user_func($callback[0], $callback[1], $this);
1289                if(is_bool($ret))
1290                    return $ret;
1291            }
1292        }
1293
1294        //Entfernen der Unterordner und Dateien
1295        $res = $this->getSubFolders();
1296        if (is_bool($res) && !$res) return false;
1297        $res = $this->getDocuments();
1298        if (is_bool($res) && !$res) return false;
1299
1300        foreach ($this->_subFolders as $subFolder) {
1301            $res = $subFolder->remove();
1302            if (!$res) {
1303                return false;
1304            }
1305        }
1306
1307        foreach ($this->_documents as $document) {
1308            $res = $document->remove();
1309            if (!$res) {
1310                return false;
1311            }
1312        }
1313
1314        $ret = $this->removeFromDatabase();
1315        if(!$ret)
1316            return $ret;
1317
1318        /* Check if 'onPostRemoveFolder' callback is set */
1319        if(isset($this->_dms->callbacks['onPostRemoveFolder'])) {
1320            foreach($this->_dms->callbacks['onPostRemoveFolder'] as $callback) {
1321                call_user_func($callback[0], $callback[1], $this);
1322            }
1323        }
1324
1325        return $ret;
1326    } /* }}} */
1327
1328    /**
1329     * Empty recursively a folder
1330     *
1331     * Removes all subfolders and documents of a folder but not the folder itself
1332     * This method will call remove() on all its children.
1333     * This method triggers the callbacks onPreEmptyFolder and onPostEmptyFolder.
1334     * If onPreEmptyFolder returns a boolean then this method will return
1335     * imediately.
1336     * Be aware that the recursive calls of remove() will trigger the callbacks
1337     * onPreRemoveFolder, onPostRemoveFolder, onPreRemoveDocument and onPostRemoveDocument.
1338     *
1339     * @return boolean true on success, false in case of an error
1340     */
1341    function emptyFolder() { /* {{{ */
1342        /** @noinspection PhpUnusedLocalVariableInspection */
1343        $db = $this->_dms->getDB();
1344
1345        /* Check if 'onPreEmptyFolder' callback is set */
1346        if(isset($this->_dms->callbacks['onPreEmptyFolder'])) {
1347            foreach($this->_dms->callbacks['onPreEmptyFolder'] as $callback) {
1348                $ret = call_user_func($callback[0], $callback[1], $this);
1349                if(is_bool($ret))
1350                    return $ret;
1351            }
1352        }
1353
1354        //Entfernen der Unterordner und Dateien
1355        $res = $this->getSubFolders();
1356        if (is_bool($res) && !$res) return false;
1357        $res = $this->getDocuments();
1358        if (is_bool($res) && !$res) return false;
1359
1360        foreach ($this->_subFolders as $subFolder) {
1361            $res = $subFolder->remove();
1362            if (!$res) {
1363                return false;
1364            }
1365        }
1366
1367        foreach ($this->_documents as $document) {
1368            $res = $document->remove();
1369            if (!$res) {
1370                return false;
1371            }
1372        }
1373
1374        /* Check if 'onPostEmptyFolder' callback is set */
1375        if(isset($this->_dms->callbacks['onPostEmptyFolder'])) {
1376            foreach($this->_dms->callbacks['onPostEmptyFolder'] as $callback) {
1377                call_user_func($callback[0], $callback[1], $this);
1378            }
1379        }
1380
1381        return true;
1382    } /* }}} */
1383
1384    /**
1385     * Returns a list of access rights
1386     *
1387     * If the folder inherits the access rights from the parent folder
1388     * those will be returned.
1389     * $mode and $op can be set to restrict the list of returned access
1390     * rights. If $mode is set to M_ANY no restriction will apply
1391     * regardless of the value of $op. The returned array contains a list
1392     * of {@link SeedDMS_Core_UserAccess} and
1393     * {@link SeedDMS_Core_GroupAccess} objects. Even if the document
1394     * has no access list the returned array contains the two elements
1395     * 'users' and 'groups' which are than empty. The methode returns false
1396     * if the function fails.
1397     * 
1398     * @param integer $mode access mode (defaults to M_ANY)
1399     * @param integer $op operation (defaults to O_EQ)
1400     * @return bool|SeedDMS_Core_GroupAccess|SeedDMS_Core_UserAccess
1401     */
1402    function getAccessList($mode = M_ANY, $op = O_EQ) { /* {{{ */
1403        $db = $this->_dms->getDB();
1404
1405        if ($this->inheritsAccess()) {
1406            /* Access is supposed to be inherited but it could be that there
1407             * is no parent because the configured root folder id is somewhere
1408             * below the actual root folder.
1409             */
1410            $res = $this->getParent();
1411            if ($res) {
1412                $pacl = $res->getAccessList($mode, $op);
1413                return $pacl;
1414            }
1415        } else {
1416            $pacl = array("groups" => array(), "users" => array());
1417        }
1418
1419        if (!isset($this->_accessList[$mode])) {
1420            if ($op!=O_GTEQ && $op!=O_LTEQ && $op!=O_EQ) {
1421                return false;
1422            }
1423            $modeStr = "";
1424            if ($mode!=M_ANY) {
1425                $modeStr = " AND `mode`".$op.(int)$mode;
1426            }
1427            $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1428                " AND `target` = " . $this->_id .    $modeStr . " ORDER BY `targetType`";
1429            $resArr = $db->getResultArray($queryStr);
1430            if (is_bool($resArr) && !$resArr)
1431                return false;
1432
1433            $this->_accessList[$mode] = array("groups" => array(), "users" => array());
1434            foreach ($resArr as $row) {
1435                if ($row["userID"] != -1)
1436                    array_push($this->_accessList[$mode]["users"], new SeedDMS_Core_UserAccess($this->_dms->getUser($row["userID"]), (int) $row["mode"]));
1437                else //if ($row["groupID"] != -1)
1438                    array_push($this->_accessList[$mode]["groups"], new SeedDMS_Core_GroupAccess($this->_dms->getGroup($row["groupID"]), (int) $row["mode"]));
1439            }
1440        }
1441
1442        return $this->_accessList[$mode];
1443        return SeedDMS_Core_DMS::mergeAccessLists($pacl, $this->_accessList[$mode]);
1444    } /* }}} */
1445
1446    /**
1447     * Delete all entries for this folder from the access control list
1448     *
1449     * @param boolean $noclean set to true if notifier list shall not be clean up
1450     * @return boolean true if operation was successful otherwise false
1451     */
1452    function clearAccessList($noclean=false) { /* {{{ */
1453        $db = $this->_dms->getDB();
1454
1455        $queryStr = "DELETE FROM `tblACLs` WHERE `targetType` = " . T_FOLDER . " AND `target` = " . $this->_id;
1456        if (!$db->getResult($queryStr))
1457            return false;
1458
1459        unset($this->_accessList);
1460        $this->_readAccessList = array();
1461
1462        if(!$noclean)
1463            $this->cleanNotifyList();
1464
1465        return true;
1466    } /* }}} */
1467
1468    /**
1469     * Add access right to folder
1470     *
1471     * This method may change in the future. Instead of passing the a flag
1472     * and a user/group id a user or group object will be expected.
1473     *
1474     * @param integer $mode access mode
1475     * @param integer $userOrGroupID id of user or group
1476     * @param integer $isUser set to 1 if $userOrGroupID is the id of a
1477     *        user
1478     * @return bool
1479     */
1480    function addAccess($mode, $userOrGroupID, $isUser) { /* {{{ */
1481        $db = $this->_dms->getDB();
1482
1483        if($mode < M_NONE || $mode > M_ALL)
1484            return false;
1485
1486        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1487
1488        /* Adding a second access right will return false */
1489        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1490                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ". (int) $userOrGroupID;
1491        $resArr = $db->getResultArray($queryStr);
1492        if (is_bool($resArr) || $resArr)
1493            return false;
1494
1495        $queryStr = "INSERT INTO `tblACLs` (`target`, `targetType`, ".$userOrGroup.", `mode`) VALUES 
1496                    (".$this->_id.", ".T_FOLDER.", " . (int) $userOrGroupID . ", " .(int) $mode. ")";
1497        if (!$db->getResult($queryStr))
1498            return false;
1499
1500        unset($this->_accessList);
1501        $this->_readAccessList = array();
1502
1503        // Update the notify list, if necessary.
1504        if ($mode == M_NONE) {
1505            $this->removeNotify($userOrGroupID, $isUser);
1506        }
1507
1508        return true;
1509    } /* }}} */
1510
1511    /**
1512     * Change access right of folder
1513     *
1514     * This method may change in the future. Instead of passing the a flag
1515     * and a user/group id a user or group object will be expected.
1516     *
1517     * @param integer $newMode access mode
1518     * @param integer $userOrGroupID id of user or group
1519     * @param integer $isUser set to 1 if $userOrGroupID is the id of a
1520     *        user
1521     * @return bool
1522     */
1523    function changeAccess($newMode, $userOrGroupID, $isUser) { /* {{{ */
1524        $db = $this->_dms->getDB();
1525
1526        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1527
1528        /* Get the old access right */
1529        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1530                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ". (int) $userOrGroupID;
1531        $resArr = $db->getResultArray($queryStr);
1532        if (!$resArr)
1533            return false;
1534
1535        $oldmode = $resArr[0]['mode'];
1536
1537        $queryStr = "UPDATE `tblACLs` SET `mode` = " . (int) $newMode . " WHERE `targetType` = ".T_FOLDER." AND `target` = " . $this->_id . " AND " . $userOrGroup . " = " . (int) $userOrGroupID;
1538        if (!$db->getResult($queryStr))
1539            return false;
1540
1541        unset($this->_accessList);
1542        $this->_readAccessList = array();
1543
1544        // Update the notify list, if necessary.
1545        if ($newMode == M_NONE) {
1546            $this->removeNotify($userOrGroupID, $isUser);
1547        }
1548
1549        return $oldmode;
1550    } /* }}} */
1551
1552    /**
1553     * Remove all access rights of folder
1554     *
1555     * This method removes all access rights of a given user or group.
1556     * 
1557     * @param $userOrGroupID
1558     * @param $isUser
1559     * @return bool
1560     */
1561    function removeAccess($userOrGroupID, $isUser) { /* {{{ */
1562        $db = $this->_dms->getDB();
1563
1564        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1565
1566        /* Get the old access right */
1567        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1568                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ". (int) $userOrGroupID;
1569        $resArr = $db->getResultArray($queryStr);
1570        if (!$resArr)
1571            return false;
1572
1573        $queryStr = "DELETE FROM `tblACLs` WHERE `targetType` = ".T_FOLDER." AND `target` = ".$this->_id." AND ".$userOrGroup." = " . (int) $userOrGroupID;
1574        if (!$db->getResult($queryStr))
1575            return false;
1576
1577        unset($this->_accessList);
1578        $this->_readAccessList = array();
1579
1580        // Update the notify list, if necessary.
1581        $mode = ($isUser ? $this->getAccessMode($this->_dms->getUser($userOrGroupID)) : $this->getGroupAccessMode($this->_dms->getGroup($userOrGroupID)));
1582        if ($mode == M_NONE) {
1583            $this->removeNotify($userOrGroupID, $isUser);
1584        }
1585
1586        return true;
1587    } /* }}} */
1588
1589    /**
1590     * Get the access mode of a user on the folder
1591     *
1592     * The access mode is either M_READ, M_READWRITE, M_ALL, or M_NONE.
1593     * It is determined
1594     * - by the user (admins and owners have always access mode M_ALL)
1595     * - by the access list for the user (possibly inherited)
1596     * - by the default access mode
1597     *
1598     * This method returns the access mode for a given user. An administrator
1599     * and the owner of the folder has unrestricted access. A guest user has
1600     * read only access or no access if access rights are further limited
1601     * by access control lists all the default access.
1602     * All other users have access rights according
1603     * to the access control lists or the default access. This method will
1604     * recursively check for access rights of parent folders if access rights
1605     * are inherited.
1606     *
1607     * Before checking the access itself a callback 'onCheckAccessFolder'
1608     * is called. If it returns a value > 0, then this will be returned by this
1609     * method without any further checks. The optional paramater $context
1610     * will be passed as a third parameter to the callback. It contains
1611     * the operation for which the access mode is retrieved. It is for example
1612     * set to 'removeDocument' if the access mode is used to check for sufficient
1613     * permission on deleting a document. This callback could be used to
1614     * override any existing access mode in a certain context.
1615     *
1616     * @param SeedDMS_Core_User $user user for which access shall be checked
1617     * @param string $context context in which the access mode is requested
1618     * @return integer access mode
1619     */
1620    function getAccessMode($user, $context='') { /* {{{ */
1621        if(!$user)
1622            return M_NONE;
1623
1624        /* Check if 'onCheckAccessFolder' callback is set */
1625        if(isset($this->_dms->callbacks['onCheckAccessFolder'])) {
1626            foreach($this->_dms->callbacks['onCheckAccessFolder'] as $callback) {
1627                if(($ret = call_user_func($callback[0], $callback[1], $this, $user, $context)) > 0) {
1628                    return $ret;
1629                }
1630            }
1631        }
1632
1633        /* Administrators have unrestricted access */
1634        if ($user->isAdmin()) return M_ALL;
1635
1636        /* The owner of the folder has unrestricted access */
1637        if ($user->getID() == $this->_ownerID) return M_ALL;
1638
1639        /* Check ACLs */
1640        $accessList = $this->getAccessList();
1641        if (!$accessList) return false;
1642
1643        /** @var SeedDMS_Core_UserAccess $userAccess */
1644        foreach ($accessList["users"] as $userAccess) {
1645            if ($userAccess->getUserID() == $user->getID()) {
1646                $mode = $userAccess->getMode();
1647                if ($user->isGuest()) {
1648                    if ($mode >= M_READ) $mode = M_READ;
1649                }
1650                return $mode;
1651            }
1652        }
1653
1654        /* Get the highest right defined by a group */
1655        if($accessList['groups']) {
1656            $mode = 0;
1657            /** @var SeedDMS_Core_GroupAccess $groupAccess */
1658            foreach ($accessList["groups"] as $groupAccess) {
1659                if ($user->isMemberOfGroup($groupAccess->getGroup())) {
1660                    if ($groupAccess->getMode() > $mode)
1661                        $mode = $groupAccess->getMode();
1662                }
1663            }
1664            if($mode) {
1665                if ($user->isGuest()) {
1666                    if ($mode >= M_READ) $mode = M_READ;
1667                }
1668                return $mode;
1669            }
1670        }
1671
1672        $mode = $this->getDefaultAccess();
1673        if ($user->isGuest()) {
1674            if ($mode >= M_READ) $mode = M_READ;
1675        }
1676        return $mode;
1677    } /* }}} */
1678
1679    /**
1680     * Get the access mode for a group on the folder
1681     *
1682     * This method returns the access mode for a given group. The algorithmn
1683     * applied to get the access mode is the same as describe at
1684     * {@link getAccessMode}
1685     *
1686     * @param SeedDMS_Core_Group $group group for which access shall be checked
1687     * @return integer access mode
1688     */
1689    function getGroupAccessMode($group) { /* {{{ */
1690        $highestPrivileged = M_NONE;
1691        $foundInACL = false;
1692        $accessList = $this->getAccessList();
1693        if (!$accessList)
1694            return false;
1695
1696        /** @var SeedDMS_Core_GroupAccess $groupAccess */
1697        foreach ($accessList["groups"] as $groupAccess) {
1698            if ($groupAccess->getGroupID() == $group->getID()) {
1699                $foundInACL = true;
1700                if ($groupAccess->getMode() > $highestPrivileged)
1701                    $highestPrivileged = $groupAccess->getMode();
1702                if ($highestPrivileged == M_ALL) /* no need to check further */
1703                    return $highestPrivileged;
1704            }
1705        }
1706        if ($foundInACL)
1707            return $highestPrivileged;
1708
1709        /* Take default access */
1710        return $this->getDefaultAccess();
1711    } /* }}} */
1712
1713    /**
1714     * Get a list of all notification
1715     *
1716     * This method returns all users and groups that have registerd a
1717     * notification for the folder
1718     *
1719     * @param integer $type type of notification (not yet used)
1720     * @param bool $incdisabled set to true if disabled user shall be included
1721     * @return SeedDMS_Core_User[]|SeedDMS_Core_Group[]|bool array with a the elements 'users' and 'groups' which
1722     *        contain a list of users and groups.
1723     */
1724    function getNotifyList($type=0, $incdisabled=false) { /* {{{ */
1725        if (empty($this->_notifyList)) {
1726            $db = $this->_dms->getDB();
1727
1728            $queryStr ="SELECT * FROM `tblNotify` WHERE `targetType` = " . T_FOLDER . " AND `target` = " . $this->_id;
1729            $resArr = $db->getResultArray($queryStr);
1730            if (is_bool($resArr) && $resArr == false)
1731                return false;
1732
1733            $this->_notifyList = array("groups" => array(), "users" => array());
1734            foreach ($resArr as $row)
1735            {
1736                if ($row["userID"] != -1) {
1737                    $u = $this->_dms->getUser($row["userID"]);
1738                    if($u && (!$u->isDisabled() || $incdisabled))
1739                        array_push($this->_notifyList["users"], $u);
1740                } else {//if ($row["groupID"] != -1)
1741                    $g = $this->_dms->getGroup($row["groupID"]);
1742                    if($g)
1743                        array_push($this->_notifyList["groups"], $g);
1744                }
1745            }
1746        }
1747        return $this->_notifyList;
1748    } /* }}} */
1749
1750    /**
1751     * Make sure only users/groups with read access are in the notify list
1752     *
1753     */
1754    function cleanNotifyList() { /* {{{ */
1755        // If any of the notification subscribers no longer have read access,
1756        // remove their subscription.
1757        if (empty($this->_notifyList))
1758            $this->getNotifyList();
1759
1760        /* Make a copy of both notifier lists because removeNotify will empty
1761         * $this->_notifyList and the second foreach will not work anymore.
1762         */
1763        /** @var SeedDMS_Core_User[] $nusers */
1764        $nusers = $this->_notifyList["users"];
1765        $ngroups = $this->_notifyList["groups"];
1766        foreach ($nusers as $u) {
1767            if ($this->getAccessMode($u) < M_READ) {
1768                $this->removeNotify($u->getID(), true);
1769            }
1770        }
1771
1772        /** @var SeedDMS_Core_Group[] $ngroups */
1773        foreach ($ngroups as $g) {
1774            if ($this->getGroupAccessMode($g) < M_READ) {
1775                $this->removeNotify($g->getID(), false);
1776            }
1777        }
1778    } /* }}} */
1779
1780    /**
1781     * Add a user/group to the notification list
1782     *
1783     * This method does not check if the currently logged in user
1784     * is allowed to add a notification. This must be checked by the calling
1785     * application.
1786     *
1787     * @param integer $userOrGroupID
1788     * @param boolean $isUser true if $userOrGroupID is a user id otherwise false
1789     * @return integer error code
1790     *    -1: Invalid User/Group ID.
1791     *    -2: Target User / Group does not have read access.
1792     *    -3: User is already subscribed.
1793     *    -4: Database / internal error.
1794     *     0: Update successful.
1795     */
1796    function addNotify($userOrGroupID, $isUser) { /* {{{ */
1797        $db = $this->_dms->getDB();
1798
1799        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1800
1801        /* Verify that user / group exists */
1802        /** @var SeedDMS_Core_User|SeedDMS_Core_Group $obj */
1803        $obj = ($isUser ? $this->_dms->getUser($userOrGroupID) : $this->_dms->getGroup($userOrGroupID));
1804        if (!is_object($obj)) {
1805            return -1;
1806        }
1807
1808        /* Verify that the requesting user has permission to add the target to
1809         * the notification system.
1810         */
1811        /*
1812         * The calling application should enforce the policy on who is allowed
1813         * to add someone to the notification system. If is shall remain here
1814         * the currently logged in user should be passed to this function
1815         *
1816        GLOBAL $user;
1817        if ($user->isGuest()) {
1818            return -2;
1819        }
1820        if (!$user->isAdmin()) {
1821            if ($isUser) {
1822                if ($user->getID() != $obj->getID()) {
1823                    return -2;
1824                }
1825            }
1826            else {
1827                if (!$obj->isMember($user)) {
1828                    return -2;
1829                }
1830            }
1831        }
1832        */
1833
1834        //
1835        // Verify that user / group has read access to the document.
1836        //
1837        if ($isUser) {
1838            // Users are straightforward to check.
1839            if ($this->getAccessMode($obj) < M_READ) {
1840                return -2;
1841            }
1842        }
1843        else {
1844            // FIXME: Why not check the access list first and if this returns
1845            // not result, then use the default access?
1846            // Groups are a little more complex.
1847            if ($this->getDefaultAccess() >= M_READ) {
1848                // If the default access is at least READ-ONLY, then just make sure
1849                // that the current group has not been explicitly excluded.
1850                $acl = $this->getAccessList(M_NONE, O_EQ);
1851                $found = false;
1852                /** @var SeedDMS_Core_GroupAccess $group */
1853                foreach ($acl["groups"] as $group) {
1854                    if ($group->getGroupID() == $userOrGroupID) {
1855                        $found = true;
1856                        break;
1857                    }
1858                }
1859                if ($found) {
1860                    return -2;
1861                }
1862            }
1863            else {
1864                // The default access is restricted. Make sure that the group has
1865                // been explicitly allocated access to the document.
1866                $acl = $this->getAccessList(M_READ, O_GTEQ);
1867                if (is_bool($acl)) {
1868                    return -4;
1869                }
1870                $found = false;
1871                /** @var SeedDMS_Core_GroupAccess $group */
1872                foreach ($acl["groups"] as $group) {
1873                    if ($group->getGroupID() == $userOrGroupID) {
1874                        $found = true;
1875                        break;
1876                    }
1877                }
1878                if (!$found) {
1879                    return -2;
1880                }
1881            }
1882        }
1883        //
1884        // Check to see if user/group is already on the list.
1885        //
1886        $queryStr = "SELECT * FROM `tblNotify` WHERE `tblNotify`.`target` = '".$this->_id."' ".
1887            "AND `tblNotify`.`targetType` = '".T_FOLDER."' ".
1888            "AND `tblNotify`.".$userOrGroup." = '". (int) $userOrGroupID."'";
1889        $resArr = $db->getResultArray($queryStr);
1890        if (is_bool($resArr)) {
1891            return -4;
1892        }
1893        if (count($resArr)>0) {
1894            return -3;
1895        }
1896
1897        $queryStr = "INSERT INTO `tblNotify` (`target`, `targetType`, " . $userOrGroup . ") VALUES (" . $this->_id . ", " . T_FOLDER . ", " .  (int) $userOrGroupID . ")";
1898        if (!$db->getResult($queryStr))
1899            return -4;
1900
1901        unset($this->_notifyList);
1902        return 0;
1903    } /* }}} */
1904
1905    /**
1906     * Removes notify for a user or group to folder
1907     *
1908     * This method does not check if the currently logged in user
1909     * is allowed to remove a notification. This must be checked by the calling
1910     * application.
1911     *
1912     * @param integer $userOrGroupID
1913     * @param boolean $isUser true if $userOrGroupID is a user id otherwise false
1914     * @param int $type type of notification (0 will delete all) Not used yet!
1915     * @return int error code
1916     *    -1: Invalid User/Group ID.
1917     * -3: User is not subscribed.
1918     * -4: Database / internal error.
1919     * 0: Update successful.
1920     */
1921    function removeNotify($userOrGroupID, $isUser, $type=0) { /* {{{ */
1922        $db = $this->_dms->getDB();
1923
1924        /* Verify that user / group exists. */
1925        $obj = ($isUser ? $this->_dms->getUser($userOrGroupID) : $this->_dms->getGroup($userOrGroupID));
1926        if (!is_object($obj)) {
1927            return -1;
1928        }
1929
1930        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1931
1932        /* Verify that the requesting user has permission to add the target to
1933         * the notification system.
1934         */
1935        /*
1936         * The calling application should enforce the policy on who is allowed
1937         * to add someone to the notification system. If is shall remain here
1938         * the currently logged in user should be passed to this function
1939         *
1940        GLOBAL  $user;
1941        if ($user->isGuest()) {
1942            return -2;
1943        }
1944        if (!$user->isAdmin()) {
1945            if ($isUser) {
1946                if ($user->getID() != $obj->getID()) {
1947                    return -2;
1948                }
1949            }
1950            else {
1951                if (!$obj->isMember($user)) {
1952                    return -2;
1953                }
1954            }
1955        }
1956        */
1957
1958        //
1959        // Check to see if the target is in the database.
1960        //
1961        $queryStr = "SELECT * FROM `tblNotify` WHERE `tblNotify`.`target` = '".$this->_id."' ".
1962            "AND `tblNotify`.`targetType` = '".T_FOLDER."' ".
1963            "AND `tblNotify`.".$userOrGroup." = '". (int) $userOrGroupID."'";
1964        $resArr = $db->getResultArray($queryStr);
1965        if (is_bool($resArr)) {
1966            return -4;
1967        }
1968        if (count($resArr)==0) {
1969            return -3;
1970        }
1971
1972        $queryStr = "DELETE FROM `tblNotify` WHERE `target` = " . $this->_id . " AND `targetType` = " . T_FOLDER . " AND " . $userOrGroup . " = " .  (int) $userOrGroupID;
1973        /* If type is given then delete only those notifications */
1974        if($type)
1975            $queryStr .= " AND `type` = ".(int) $type;
1976        if (!$db->getResult($queryStr))
1977            return -4;
1978
1979        unset($this->_notifyList);
1980        return 0;
1981    } /* }}} */
1982
1983    /**
1984     * Get List of users and groups which have read access on the folder.
1985     * The list will not include any guest users,
1986     * administrators and the owner of the folder.
1987     *
1988     * This method is deprecated. Use
1989     * {@see SeedDMS_Core_Folder::getReadAccessList()} instead.
1990     */
1991    function getApproversList() { /* {{{ */
1992        return $this->getReadAccessList(0, 0);
1993    } /* }}} */
1994
1995    /**
1996     * Returns a list of groups and users with read access on the folder
1997     *
1998     * The list will not include any guest users,
1999     * administrators and the owner of the folder unless $listadmin resp.
2000     * $listowner is set to true.
2001     *
2002     * @param boolean $listadmin if set to true any admin will be listed too
2003     * @param boolean $listowner if set to true the owner will be listed too
2004     * @param boolean $listguest if set to true any guest will be listed too
2005     * @return array list of users and groups
2006     */
2007    public function getReadAccessList($listadmin=0, $listowner=0, $listguest=0) { /* {{{ */
2008        $db = $this->_dms->getDB();
2009
2010        $cachehash = substr(md5($listadmin.$listowner.$listguest), 0, 3);
2011        if (!isset($this->_readAccessList[$cachehash])) {
2012            $this->_readAccessList[$cachehash] = array("groups" => array(), "users" => array());
2013            $userIDs = "";
2014            $groupIDs = "";
2015            $defAccess  = $this->getDefaultAccess();
2016
2017            /* Check if the default access is < read access or >= read access.
2018             * If default access is less than read access, then create a list
2019             * of users and groups with read access.
2020             * If default access is equal or greater then read access, then
2021             * create a list of users and groups without read access.
2022             */
2023            if ($defAccess<M_READ) {
2024                // Get the list of all users and groups that are listed in the ACL as
2025                // having read access to the folder.
2026                $tmpList = $this->getAccessList(M_READ, O_GTEQ);
2027            }
2028            else {
2029                // Get the list of all users and groups that DO NOT have read access
2030                // to the folder.
2031                $tmpList = $this->getAccessList(M_NONE, O_LTEQ);
2032            }
2033            /** @var SeedDMS_Core_GroupAccess $groupAccess */
2034            foreach ($tmpList["groups"] as $groupAccess) {
2035                $groupIDs .= (strlen($groupIDs)==0 ? "" : ", ") . $groupAccess->getGroupID();
2036            }
2037
2038            /** @var SeedDMS_Core_UserAccess $userAccess */
2039            foreach ($tmpList["users"] as $userAccess) {
2040                $user = $userAccess->getUser();
2041//                if (!$listadmin && $user->isAdmin()) continue;
2042//                if (!$listowner && $user->getID() == $this->_ownerID) continue;
2043//                if (!$listguest && $user->isGuest()) continue;
2044                $userIDs .= (strlen($userIDs)==0 ? "" : ", ") . $user->getID();
2045            }
2046
2047            // Construct a query against the users table to identify those users
2048            // that have read access on this folder, either directly through an
2049            // ACL entry, by virtue of ownership or by having administrative rights
2050            // on the database.
2051            $queryStr="";
2052            /* If default access is less then read, $userIDs and $groupIDs contains
2053             * a list of user with read access
2054             */
2055            if ($defAccess < M_READ) {
2056                $queryStr = "SELECT `tblUsers`.* FROM `tblUsers` ".
2057                    "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`userID`=`tblUsers`.`id` ".
2058                    "LEFT JOIN `tblRoles` ON `tblRoles`.`id`=`tblUsers`.`role` ".
2059                    "WHERE 1=0".
2060                    ((strlen($groupIDs) > 0) ? " OR (`tblGroupMembers`.`groupID` IN (". $groupIDs ."))" : "").
2061                    ((strlen($userIDs) > 0) ?  " OR (`tblUsers`.`id` IN (". $userIDs ."))" : "").
2062                    " OR (`tblRoles`.`role` = ".SeedDMS_Core_Role::role_admin.")".
2063                    " OR (`tblUsers`.`id` = ". $this->_ownerID . ")".
2064                    " ORDER BY `login`";
2065            }
2066            /* If default access is equal or greater than M_READ, $userIDs and
2067             * $groupIDs contains a list of user without read access
2068             * The sql statement will exclude those users and groups but include
2069             * admins and the owner
2070             */
2071            else {
2072                $queryStr = "SELECT `tblUsers`.* FROM `tblUsers` ".
2073                    "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`userID`=`tblUsers`.`id` ".
2074                    "LEFT JOIN `tblRoles` ON `tblRoles`.`id`=`tblUsers`.`role` ".
2075                    "WHERE 1=1".
2076                    (strlen($groupIDs) == 0 ? "" : " AND (`tblGroupMembers`.`groupID` NOT IN (". $groupIDs .") OR `tblGroupMembers`.`groupID` IS NULL)").
2077                    (strlen($userIDs) == 0 ? "" : " AND (`tblUsers`.`id` NOT IN (". $userIDs ."))").
2078                    " OR `tblUsers`.`id` = ". $this->_ownerID . " OR `tblRoles`.`role` = ".SeedDMS_Core_Role::role_admin." ORDER BY `login` ";
2079            }
2080            $resArr = $db->getResultArray($queryStr);
2081            if (!is_bool($resArr)) {
2082                foreach ($resArr as $row) {
2083                    $user = $this->_dms->getUser($row['id']);
2084                    if (!$listadmin && $user->isAdmin()) continue;
2085                    if (!$listowner && $user->getID() == $this->_ownerID) continue;
2086                    if (!$listguest && $user->isGuest()) continue;
2087                    $this->_readAccessList[$cachehash]["users"][] = $user;
2088                }
2089            }
2090
2091            // Assemble the list of groups that have read access to the folder.
2092            $queryStr="";
2093            if ($defAccess < M_READ) {
2094                if (strlen($groupIDs)>0) {
2095                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ".
2096                        "WHERE `tblGroups`.`id` IN (". $groupIDs .") ORDER BY `name`";
2097                }
2098            }
2099            else {
2100                if (strlen($groupIDs)>0) {
2101                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ".
2102                        "WHERE `tblGroups`.`id` NOT IN (". $groupIDs .") ORDER BY `name`";
2103                }
2104                else {
2105                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ORDER BY `name`";
2106                }
2107            }
2108            if (strlen($queryStr)>0) {
2109                $resArr = $db->getResultArray($queryStr);
2110                if (!is_bool($resArr)) {
2111                    foreach ($resArr as $row) {
2112                        $group = $this->_dms->getGroup($row["id"]);
2113                        $this->_readAccessList[$cachehash]["groups"][] = $group;
2114                    }
2115                }
2116            }
2117        }
2118        return $this->_readAccessList[$cachehash];
2119    } /* }}} */
2120
2121    /**
2122     * Get the internally used folderList which stores the ids of folders from
2123     * the root folder to the parent folder.
2124     *
2125     * @return string column separated list of folder ids
2126     */
2127    function getFolderList() { /* {{{ */
2128        $db = $this->_dms->getDB();
2129
2130        $queryStr = "SELECT `folderList` FROM `tblFolders` where `id` = ".$this->_id;
2131        $resArr = $db->getResultArray($queryStr);
2132        if (is_bool($resArr) && !$resArr)
2133            return false;
2134        return $resArr[0]['folderList'];
2135    } /* }}} */
2136
2137    /**
2138     * Checks the internal data of the folder and repairs it.
2139     * Currently, this function only repairs an incorrect folderList
2140     *
2141     * @return boolean true on success, otherwise false
2142     */
2143    function repair() { /* {{{ */
2144        $db = $this->_dms->getDB();
2145
2146        $curfolderlist = $this->getFolderList();
2147
2148        // calculate the folderList of the folder
2149        $parent = $this->getParent();
2150        $pathPrefix="";
2151        $path = $parent->getPath();
2152        foreach ($path as $f) {
2153            $pathPrefix .= ":".$f->getID();
2154        }
2155        if (strlen($pathPrefix)>1) {
2156            $pathPrefix .= ":";
2157        }
2158        if($curfolderlist != $pathPrefix) {
2159            $queryStr = "UPDATE `tblFolders` SET `folderList`='".$pathPrefix."' WHERE `id` = ". $this->_id;
2160            $res = $db->getResult($queryStr);
2161            if (!$res)
2162                return false;
2163        }
2164        return true;
2165    } /* }}} */
2166
2167    /**
2168     * Get the min and max sequence value for documents
2169     *
2170     * @return bool|array array with keys 'min' and 'max', false in case of an error
2171     */
2172    function getDocumentsMinMax() { /* {{{ */
2173        $db = $this->_dms->getDB();
2174
2175        $queryStr = "SELECT min(`sequence`) AS `min`, max(`sequence`) AS `max` FROM `tblDocuments` WHERE `folder` = " . (int) $this->_id;
2176        $resArr = $db->getResultArray($queryStr);
2177        if (is_bool($resArr) && $resArr == false)
2178            return false;
2179
2180        return $resArr[0];
2181    } /* }}} */
2182
2183    /**
2184     * Get the min and max sequence value for folders
2185     *
2186     * @return bool|array array with keys 'min' and 'max', false in case of an error
2187     */
2188    function getFoldersMinMax() { /* {{{ */
2189        $db = $this->_dms->getDB();
2190
2191        $queryStr = "SELECT min(`sequence`) AS `min`, max(`sequence`) AS `max` FROM `tblFolders` WHERE `parent` = " . (int) $this->_id;
2192        $resArr = $db->getResultArray($queryStr);
2193        if (is_bool($resArr) && $resArr == false)
2194            return false;
2195
2196        return $resArr[0];
2197    } /* }}} */
2198
2199    /**
2200     * Reorder documents of folder
2201     *
2202     * Fix the sequence numbers of all documents in the folder, by assigning new
2203     * numbers starting from 1 incrementing by 1. This can be necessary if sequence
2204     * numbers are not unique which makes manual reordering for documents with
2205     * identical sequence numbers impossible.
2206     *
2207     * @return bool false in case of an error, otherwise true
2208     */
2209    function reorderDocuments() { /* {{{ */
2210        $db = $this->_dms->getDB();
2211
2212        $queryStr = "SELECT `id` FROM `tblDocuments` WHERE `folder` = " . (int) $this->_id . " ORDER BY `sequence`";
2213        $resArr = $db->getResultArray($queryStr);
2214        if (is_bool($resArr) && $resArr == false)
2215            return false;
2216
2217        $db->startTransaction();
2218        $no = 1.0;
2219        foreach($resArr as $doc) {
2220            $queryStr = "UPDATE `tblDocuments` SET `sequence` = " . $no . " WHERE `id` = ". $doc['id'];
2221            if (!$db->getResult($queryStr)) {
2222                $db->rollbackTransaction();
2223                return false;
2224            }
2225            $no += 1.0;
2226        }
2227        $db->commitTransaction();
2228
2229        return true;
2230    } /* }}} */
2231
2232
2233}
2234
2235?>