Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.36% covered (warning)
76.36%
630 / 825
52.54% covered (warning)
52.54%
31 / 59
CRAP
0.00% covered (danger)
0.00%
0 / 1
SeedDMS_Core_Folder
76.36% covered (warning)
76.36%
630 / 825
52.54% covered (warning)
52.54%
31 / 59
2524.39
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
57.89% covered (warning)
57.89%
11 / 19
0.00% covered (danger)
0.00%
0 / 1
12.78
 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
55.00% covered (warning)
55.00%
22 / 40
0.00% covered (danger)
0.00%
0 / 1
43.34
 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
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
12.11
 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
70.00% covered (warning)
70.00%
28 / 40
0.00% covered (danger)
0.00%
0 / 1
44.25
 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%
55 / 55
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    public 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 && ($folder->getID() != $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    public 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    public 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    public 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    public 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        $oldOwner = $this->_owner;
570        $this->_ownerID = $newOwner->getID();
571        $this->_owner = $newOwner;
572
573        $this->_readAccessList = array();
574
575        /* Check if 'onPostSetOwner' callback is set */
576        if (isset($this->_dms->callbacks['onPostSetOwner'])) {
577            foreach ($this->_dms->callbacks['onPostSetOwner'] as $callback) {
578                $ret = call_user_func($callback[0], $callback[1], $this, $oldOwner);
579                if (is_bool($ret))
580                    return $ret;
581            }
582        }
583
584        return true;
585    } /* }}} */
586
587    /**
588     * Returns default access
589     *
590     * If access rights are inherited, the method will return the
591     * default access of the parent folder.
592     *
593     * @return boolean|int access right or fals in case of an error
594     */
595    public function getDefaultAccess() { /* {{{ */
596        if ($this->inheritsAccess()) {
597            /* Access is supposed to be inherited but it could be that there
598             * is no parent because the configured root folder id is somewhere
599             * below the actual root folder.
600             */
601            $res = $this->getParent();
602            if ($res)
603                return $this->_parent->getDefaultAccess();
604        }
605
606        return $this->_defaultAccess;
607    } /* }}} */
608
609    /**
610     * Set default access mode
611     *
612     * This method sets the default access mode and also removes all notifiers which
613     * will not have read access anymore.
614     *
615     * @param integer $mode access mode
616     * @param boolean $noclean set to true if notifier list shall not be clean up
617     * @return bool
618     */
619    public function setDefaultAccess($mode, $noclean = false) { /* {{{ */
620        $db = $this->_dms->getDB();
621
622        $queryStr = "UPDATE `tblFolders` set `defaultAccess` = " . (int) $mode . " WHERE `id` = " . $this->_id;
623        if (!$db->getResult($queryStr))
624            return false;
625
626        $this->_defaultAccess = $mode;
627        $this->_readAccessList = array();
628
629        if (!$noclean)
630            $this->cleanNotifyList();
631
632        return true;
633    } /* }}} */
634
635    /**
636     * Check, if folder inherits access rights
637     *
638     * @return boolean true, if access rights are inherited, otherwise false
639     */
640    public function inheritsAccess() { return $this->_inheritAccess; }
641
642    /**
643     * Set inherited access mode
644     *
645     * Setting inherited access mode will set or unset the internal flag which
646     * controls if the access mode is inherited from the parent folder or not.
647     * It will not modify the
648     * access control list for the current object. It will remove all
649     * notifications of users which do not even have read access anymore
650     * after setting or unsetting inherited access.
651     *
652     * @param boolean $inheritAccess set to true for setting and false for
653     *        unsetting inherited access mode
654     * @param boolean $noclean set to true if notifier list shall not be clean up
655     * @return boolean true if operation was successful otherwise false
656     */
657    public function setInheritAccess($inheritAccess, $noclean = false) { /* {{{ */
658        $db = $this->_dms->getDB();
659
660        $inheritAccess = ($inheritAccess) ? "1" : "0";
661
662        $queryStr = "UPDATE `tblFolders` SET `inheritAccess` = " . (int) $inheritAccess . " WHERE `id` = " . $this->_id;
663        if (!$db->getResult($queryStr))
664            return false;
665
666        $this->_inheritAccess = $inheritAccess;
667        $this->_readAccessList = array();
668
669        if (!$noclean)
670            $this->cleanNotifyList();
671
672        return true;
673    } /* }}} */
674
675    public function getSequence() { return $this->_sequence; }
676
677    public function setSequence($seq) { /* {{{ */
678        $db = $this->_dms->getDB();
679
680        $queryStr = "UPDATE `tblFolders` SET `sequence` = " . $seq . " WHERE `id` = " . $this->_id;
681        if (!$db->getResult($queryStr))
682            return false;
683
684        $this->_sequence = $seq;
685        return true;
686    } /* }}} */
687
688    /**
689     * Check, if folder has subfolders
690     *
691     * This method just checks if a folder has subfolders disregarding
692     * any access rights.
693     *
694     * @return int number of subfolders or false in case of an error
695     */
696    public function hasSubFolders() { /* {{{ */
697        $db = $this->_dms->getDB();
698        if (isset($this->_subFolders)) {
699            /** @noinspection PhpUndefinedFieldInspection */
700            return count($this->_subFolders);
701        }
702        $queryStr = "SELECT count(*) as c FROM `tblFolders` WHERE `parent` = " . $this->_id;
703        $resArr = $db->getResultArray($queryStr);
704        if (is_bool($resArr) && !$resArr)
705            return false;
706
707        return (int) $resArr[0]['c'];
708    } /* }}} */
709
710    /**
711     * Check, if folder has as subfolder with the given name
712     *
713     * @param string $name
714     * @return bool true if subfolder exists, false if not or in case
715     * of an error
716     */
717    public function hasSubFolderByName($name) { /* {{{ */
718        $db = $this->_dms->getDB();
719        /* Always check the database instead of iterating over $this->_documents, because
720         * it is probably not slower
721         */
722        $queryStr = "SELECT count(*) as c FROM `tblFolders` WHERE `parent` = " . $this->_id . " AND `name` = ".$db->qstr($name);
723        $resArr = $db->getResultArray($queryStr);
724        if (is_bool($resArr) && !$resArr)
725            return false;
726
727        return ($resArr[0]['c'] > 0);
728    } /* }}} */
729
730    /**
731     * Returns a list of subfolders
732     *
733     * This method does not check for access rights. Use
734     * {@link SeedDMS_Core_DMS::filterAccess} for checking each folder against
735     * the currently logged in user and the access rights.
736     *
737     * @param string $orderby if set to 'n' the list is ordered by name, otherwise
738     *        it will be ordered by sequence
739     * @param string $dir direction of sorting (asc or desc)
740     * @param integer $limit limit number of subfolders
741     * @param integer $offset offset in retrieved list of subfolders
742     * @return SeedDMS_Core_Folder[]|bool list of folder objects or false in case of an error
743     */
744    public function getSubFolders($orderby = "", $dir = "asc", $limit = 0, $offset = 0) { /* {{{ */
745        $db = $this->_dms->getDB();
746
747        if (!isset($this->_subFolders)) {
748            $queryStr = "SELECT * FROM `tblFolders` WHERE `parent` = " . $this->_id;
749
750            if ($orderby && $orderby[0]=="n") $queryStr .= " ORDER BY `name`";
751            elseif ($orderby && $orderby[0]=="s") $queryStr .= " ORDER BY `sequence`";
752            elseif ($orderby && $orderby[0]=="d") $queryStr .= " ORDER BY `date`";
753            if ($dir == 'desc')
754                $queryStr .= " DESC";
755            if (is_int($limit) && $limit > 0) {
756                $queryStr .= " LIMIT ".$limit;
757                if (is_int($offset) && $offset > 0)
758                    $queryStr .= " OFFSET ".$offset;
759            }
760
761            $resArr = $db->getResultArray($queryStr);
762            if (is_bool($resArr) && $resArr == false)
763                return false;
764
765            $classname = $this->_dms->getClassname('folder');
766            $this->_subFolders = array();
767            for ($i = 0; $i < count($resArr); $i++)
768//                $this->_subFolders[$i] = $this->_dms->getFolder($resArr[$i]["id"]);
769                $this->_subFolders[$i] = $classname::getInstanceByData($resArr[$i], $this->_dms);
770        }
771
772        return $this->_subFolders;
773    } /* }}} */
774
775    /**
776     * Add a new subfolder
777     *
778     * @param string $name name of folder
779     * @param string $comment comment of folder
780     * @param object $owner owner of folder
781     * @param integer $sequence position of folder in list of sub folders.
782     * @param array $attributes list of document attributes. The element key
783     *        must be the id of the attribute definition.
784     * @return bool|SeedDMS_Core_Folder
785     *         an error.
786     */
787    public function addSubFolder($name, $comment, $owner, $sequence, $attributes = array()) { /* {{{ */
788        $db = $this->_dms->getDB();
789
790        // Set the folderList of the folder
791        $pathPrefix = "";
792        $path = $this->getPath();
793        foreach ($path as $f) {
794            $pathPrefix .= ":".$f->getID();
795        }
796        if (strlen($pathPrefix)>1) {
797            $pathPrefix .= ":";
798        }
799
800        $db->startTransaction();
801
802        //inheritAccess = true, defaultAccess = M_READ
803        $queryStr = "INSERT INTO `tblFolders` (`name`, `parent`, `folderList`, `comment`, `date`, `owner`, `inheritAccess`, `defaultAccess`, `sequence`) ".
804                    "VALUES (".$db->qstr($name).", ".$this->_id.", ".$db->qstr($pathPrefix).", ".$db->qstr($comment).", ".$db->getCurrentTimestamp().", ".$owner->getID().", 1, ".M_READ.", ". $sequence.")";
805        if (!$db->getResult($queryStr)) {
806            $db->rollbackTransaction();
807            return false;
808        }
809        $newFolder = $this->_dms->getFolder($db->getInsertID('tblFolders'));
810        unset($this->_subFolders);
811
812        if ($attributes) {
813            foreach ($attributes as $attrdefid => $attribute) {
814                if ($attribute) {
815                    if ($attrdef = $this->_dms->getAttributeDefinition($attrdefid)) {
816                        if (!$newFolder->setAttributeValue($attrdef, $attribute)) {
817                            $db->rollbackTransaction();
818                            return false;
819                        }
820                    } else {
821                        $db->rollbackTransaction();
822                        return false;
823                    }
824                }
825            }
826        }
827
828        $db->commitTransaction();
829
830        /* Check if 'onPostAddSubFolder' callback is set */
831        if (isset($this->_dms->callbacks['onPostAddSubFolder'])) {
832            foreach ($this->_dms->callbacks['onPostAddSubFolder'] as $callback) {
833                /** @noinspection PhpStatementHasEmptyBodyInspection */
834                if (!call_user_func($callback[0], $callback[1], $newFolder)) {
835                }
836            }
837        }
838
839        return $newFolder;
840    } /* }}} */
841
842    /**
843     * Returns an array of all parents, grand parent, etc. up to the root folder.
844     *
845     * The folder itself is the last element of the array.
846     *
847     * @return array|bool
848     */
849    public function getPath() { /* {{{ */
850        if (!isset($this->_parentID) || ($this->_parentID == "") || ($this->_parentID == 0) || ($this->_id == $this->_dms->rootFolderID)) {
851            return array($this);
852        } else {
853            $res = $this->getParent();
854            if (!$res) return false;
855
856            $path = $this->_parent->getPath();
857            if (!$path) return false;
858
859            array_push($path, $this);
860            return $path;
861        }
862    } /* }}} */
863
864    /**
865     * Returns a path like used for a file system
866     *
867     * This path contains by default spaces around the slashes for better readability.
868     * Run str_replace(' / ', '/', $path) on it or pass '/' as $sep to get a valid unix
869     * file system path.
870     *
871     * The returned path is not a real path in a file system. It just uses
872     * the common syntax on unix systems to format a path.
873     *
874     * @param bool $skiproot skip the name of the root folder and start with $sep
875     * @param string $sep separator between path elements
876     * @return string path separated with ' / '
877     */
878    public function getFolderPathPlain($skiproot = false, $sep = ' / ') { /* {{{ */
879        $path = "".$sep;
880        $folderPath = $this->getPath();
881        for ($i = 0; $i < count($folderPath); $i++) {
882            if ($i > 0 || !$skiproot) {
883                $path .= $folderPath[$i]->getName();
884                if ($i+1 < count($folderPath))
885                    $path .= $sep;
886            }
887        }
888        return trim($path);
889    } /* }}} */
890
891    /**
892     * Check, if this folder is a subfolder of a given folder
893     *
894     * This is basically the opposite of {@see SeedDMS_Core_Folder::isSubFolder()}
895     *
896     * @param object $folder parent folder
897     * @return boolean true if folder is a subfolder
898     */
899    public function isDescendant($folder) { /* {{{ */
900        /* If the current folder has no parent it cannot be a descendant */
901        if (!$this->getParent())
902            return false;
903        /* Check if the passed folder is the parent of the current folder.
904         * In that case the current folder is a subfolder of the passed folder.
905         */
906        if ($this->getParent()->getID() == $folder->getID())
907            return true;
908        /* Recursively go up to the root folder */
909        return $this->getParent()->isDescendant($folder);
910    } /* }}} */
911
912    /**
913     * Check, if folder has documents
914     *
915     * This method just checks if a folder has documents diregarding
916     * any access rights.
917     *
918     * @return int number of documents or false in case of an error
919     */
920    public function hasDocuments() { /* {{{ */
921        $db = $this->_dms->getDB();
922        /* Do not use the cache because it may not contain all documents if
923         * the former call getDocuments() limited the number of documents
924        if (isset($this->_documents)) {
925            return count($this->_documents);
926        }
927         */
928        $queryStr = "SELECT count(*) as c FROM `tblDocuments` WHERE `folder` = " . $this->_id;
929        $resArr = $db->getResultArray($queryStr);
930        if (is_bool($resArr) && !$resArr)
931            return false;
932
933        return (int) $resArr[0]['c'];
934    } /* }}} */
935
936    /**
937     * Check if folder has document with given name
938     *
939     * @param string $name
940     * @return bool true if document exists, false if not or in case
941     * of an error
942     */
943    public function hasDocumentByName($name) { /* {{{ */
944        $db = $this->_dms->getDB();
945        /* Always check the database instead of iterating over $this->_documents, because
946         * it is probably not slower
947         */
948        $queryStr = "SELECT count(*) as c FROM `tblDocuments` WHERE `folder` = " . $this->_id . " AND `name` = ".$db->qstr($name);
949        $resArr = $db->getResultArray($queryStr);
950        if (is_bool($resArr) && !$resArr)
951            return false;
952
953        return ($resArr[0]['c'] > 0);
954    } /* }}} */
955
956    /**
957     * Get all documents of the folder
958     *
959     * This method does not check for access rights. Use
960     * {@link SeedDMS_Core_DMS::filterAccess} for checking each document against
961     * the currently logged in user and the access rights.
962     *
963     * @param string $orderby if set to 'n' the list is ordered by name, otherwise
964     *        it will be ordered by sequence
965     * @param string $dir direction of sorting (asc or desc)
966     * @param integer $limit limit number of documents
967     * @param integer $offset offset in retrieved list of documents
968     * @return SeedDMS_Core_Document[]|bool list of documents or false in case of an error
969     */
970    public function getDocuments($orderby = "", $dir = "asc", $limit = 0, $offset = 0) { /* {{{ */
971        $db = $this->_dms->getDB();
972
973        if (!isset($this->_documents)) {
974            $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lock` FROM `tblDocuments` LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id` = `tblDocumentLocks`.`document` WHERE `folder` = " . $this->_id;
975            if ($orderby && $orderby[0]=="n") $queryStr .= " ORDER BY `name`";
976            elseif ($orderby && $orderby[0]=="s") $queryStr .= " ORDER BY `sequence`";
977            elseif ($orderby && $orderby[0]=="d") $queryStr .= " ORDER BY `date`";
978            if ($dir == 'desc')
979                $queryStr .= " DESC";
980            if (is_int($limit) && $limit > 0) {
981                $queryStr .= " LIMIT ".$limit;
982                if (is_int($offset) && $offset > 0)
983                    $queryStr .= " OFFSET ".$offset;
984            }
985
986            $resArr = $db->getResultArray($queryStr);
987            if (is_bool($resArr) && !$resArr)
988                return false;
989
990            $this->_documents = array();
991            $classname = $this->_dms->getClassname('document');
992            foreach ($resArr as $row) {
993                    $row['lock'] = !$row['lock'] ? -1 : $row['lock'];
994//                array_push($this->_documents, $this->_dms->getDocument($row["id"]));
995                array_push($this->_documents, $classname::getInstanceByData($row, $this->_dms));
996            }
997        }
998        return $this->_documents;
999    } /* }}} */
1000
1001    /**
1002     * Count all documents and subfolders of the folder
1003     *
1004     * This method also counts documents and folders of subfolders, so
1005     * basically it works like recursively counting children.
1006     *
1007     * This method checks for access rights up the given limit. If more
1008     * documents or folders are found, the returned value will be the number
1009     * of objects available and the precise flag in the return array will be
1010     * set to false. This number should not be revelead to the
1011     * user, because it allows to gain information about the existens of
1012     * objects without access right.
1013     * Setting the parameter $limit to 0 will turn off access right checking
1014     * which is reasonable if the $user is an administrator.
1015     *
1016     * @param SeedDMS_Core_User $user
1017     * @param integer $limit maximum number of folders and documents that will
1018     *        be precisly counted by taken the access rights into account
1019     * @return array|bool with four elements 'document_count', 'folder_count'
1020     *        'document_precise', 'folder_precise' holding
1021     * the counted number and a flag if the number is precise.
1022     * @internal param string $orderby if set to 'n' the list is ordered by name, otherwise
1023     *        it will be ordered by sequence
1024     */
1025    public function countChildren($user, $limit = 10000) { /* {{{ */
1026        $db = $this->_dms->getDB();
1027
1028        $pathPrefix = "";
1029        $path = $this->getPath();
1030        foreach ($path as $f) {
1031            $pathPrefix .= ":".$f->getID();
1032        }
1033        if (strlen($pathPrefix)>1) {
1034            $pathPrefix .= ":";
1035        }
1036
1037        $queryStr = "SELECT id FROM `tblFolders` WHERE `folderList` like '".$pathPrefix. "%'";
1038        $resArr = $db->getResultArray($queryStr);
1039        if (is_bool($resArr) && !$resArr)
1040            return false;
1041
1042        $result = array();
1043
1044        $folders = array();
1045        $folderids = array($this->_id);
1046        $cfolders = count($resArr);
1047        if ($cfolders < $limit) {
1048            foreach ($resArr as $row) {
1049                $folder = $this->_dms->getFolder($row["id"]);
1050                if ($folder->getAccessMode($user) >= M_READ) {
1051                    array_push($folders, $folder);
1052                    array_push($folderids, $row['id']);
1053                }
1054            }
1055            $result['folder_count'] = count($folders);
1056            $result['folder_precise'] = true;
1057        } else {
1058            foreach ($resArr as $row) {
1059                array_push($folderids, $row['id']);
1060            }
1061            $result['folder_count'] = $cfolders;
1062            $result['folder_precise'] = false;
1063        }
1064
1065        $documents = array();
1066        if ($folderids) {
1067            $queryStr = "SELECT id FROM `tblDocuments` WHERE `folder` in (".implode(',', $folderids). ")";
1068            $resArr = $db->getResultArray($queryStr);
1069            if (is_bool($resArr) && !$resArr)
1070                return false;
1071
1072            $cdocs = count($resArr);
1073            if ($cdocs < $limit) {
1074                foreach ($resArr as $row) {
1075                    $document = $this->_dms->getDocument($row["id"]);
1076                    if ($document->getAccessMode($user) >= M_READ)
1077                        array_push($documents, $document);
1078                }
1079                $result['document_count'] = count($documents);
1080                $result['document_precise'] = true;
1081            } else {
1082                $result['document_count'] = $cdocs;
1083                $result['document_precise'] = false;
1084            }
1085        }
1086
1087        return $result;
1088    } /* }}} */
1089
1090    /**
1091     * Add a new document to the folder
1092     * This method will add a new document and its content from a given file.
1093     * It does not check for access rights on the folder. The new documents
1094     * default access right is read only and the access right is inherited.
1095     *
1096     * @param string $name name of new document
1097     * @param string $comment comment of new document
1098     * @param integer $expires expiration date as a unix timestamp or 0 for no
1099     *        expiration date
1100     * @param object $owner owner of the new document
1101     * @param SeedDMS_Core_User $keywords keywords of new document
1102     * @param SeedDMS_Core_DocumentCategory[] $categories list of category objects
1103     * @param string $tmpFile the path of the file containing the content
1104     * @param string $orgFileName the original file name
1105     * @param string $fileType usually the extension of the filename
1106     * @param string $mimeType mime type of the content
1107     * @param float $sequence position of new document within the folder
1108     * @param array $reviewers list of users who must review this document
1109     * @param array $approvers list of users who must approve this document
1110     * @param int|string $reqversion version number of the content
1111     * @param string $version_comment comment of the content. If left empty
1112     *        the $comment will be used.
1113     * @param array $attributes list of document attributes. The element key
1114     *        must be the id of the attribute definition.
1115     * @param array $version_attributes list of document version attributes.
1116     *        The element key must be the id of the attribute definition.
1117     * @param SeedDMS_Core_Workflow $workflow
1118     * @return array|bool false in case of error, otherwise an array
1119     *        containing two elements. The first one is the new document, the
1120     * second one is the result set returned when inserting the content.
1121     */
1122    public 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) { /* {{{ */
1123        $db = $this->_dms->getDB();
1124
1125        $expires = (!$expires) ? 0 : $expires;
1126
1127        // Must also ensure that the document has a valid folderList.
1128        $pathPrefix = "";
1129        $path = $this->getPath();
1130        foreach ($path as $f) {
1131            $pathPrefix .= ":".$f->getID();
1132        }
1133        if (strlen($pathPrefix)>1) {
1134            $pathPrefix .= ":";
1135        }
1136
1137        $db->startTransaction();
1138
1139        $queryStr = "INSERT INTO `tblDocuments` (`name`, `comment`, `date`, `expires`, `owner`, `folder`, `folderList`, `inheritAccess`, `defaultAccess`, `locked`, `keywords`, `sequence`) VALUES ".
1140                    "(".$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 . ")";
1141        if (!$db->getResult($queryStr)) {
1142            $db->rollbackTransaction();
1143            return false;
1144        }
1145
1146        $document = $this->_dms->getDocument($db->getInsertID('tblDocuments'));
1147
1148        $res = $document->addContent($version_comment, $owner, $tmpFile, $orgFileName, $fileType, $mimeType, $reviewers, $approvers, $reqversion, $version_attributes, $workflow);
1149
1150        if (is_bool($res) && !$res) {
1151            $db->rollbackTransaction();
1152            return false;
1153        }
1154
1155        if ($categories) {
1156            if (!$document->setCategories($categories)) {
1157                $document->remove();
1158                $db->rollbackTransaction();
1159                return false;
1160            }
1161        }
1162
1163        if ($attributes) {
1164            foreach ($attributes as $attrdefid => $attribute) {
1165                /* $attribute can be a string or an array */
1166                if ($attribute) {
1167                    if ($attrdef = $this->_dms->getAttributeDefinition($attrdefid)) {
1168                        if (!$document->setAttributeValue($attrdef, $attribute)) {
1169                            $document->remove();
1170                            $db->rollbackTransaction();
1171                            return false;
1172                        }
1173                    } else {
1174                        $document->remove();
1175                        $db->rollbackTransaction();
1176                        return false;
1177                    }
1178                }
1179            }
1180        }
1181
1182        $db->commitTransaction();
1183
1184        /* Check if 'onPostAddDocument' callback is set */
1185        if (isset($this->_dms->callbacks['onPostAddDocument'])) {
1186            foreach ($this->_dms->callbacks['onPostAddDocument'] as $callback) {
1187                /** @noinspection PhpStatementHasEmptyBodyInspection */
1188                if (!call_user_func($callback[0], $callback[1], $document)) {
1189                }
1190            }
1191        }
1192
1193        return array($document, $res);
1194    } /* }}} */
1195
1196    /**
1197     * Remove a single folder
1198     *
1199     * Removes just a single folder, but not its subfolders or documents
1200     * This method will fail if the folder has subfolders or documents
1201     * because of referencial integrity errors.
1202     *
1203     * @return boolean true on success, false in case of an error
1204     */
1205    protected function removeFromDatabase() { /* {{{ */
1206        $db = $this->_dms->getDB();
1207
1208        /* Check if 'onPreRemoveFolder' callback is set */
1209        if (isset($this->_dms->callbacks['onPreRemoveFromDatabaseFolder'])) {
1210            foreach ($this->_dms->callbacks['onPreRemoveFromDatabaseFolder'] as $callback) {
1211                $ret = call_user_func($callback[0], $callback[1], $this);
1212                if (is_bool($ret))
1213                    return $ret;
1214            }
1215        }
1216
1217        $db->startTransaction();
1218        // unset homefolder as it will no longer exist
1219        $queryStr = "UPDATE `tblUsers` SET `homefolder`=NULL WHERE `homefolder` =  " . $this->_id;
1220        if (!$db->getResult($queryStr)) {
1221            $db->rollbackTransaction();
1222            return false;
1223        }
1224
1225        // Remove database entries
1226        $queryStr = "DELETE FROM `tblFolders` WHERE `id` =  " . $this->_id;
1227        if (!$db->getResult($queryStr)) {
1228            $db->rollbackTransaction();
1229            return false;
1230        }
1231        $queryStr = "DELETE FROM `tblFolderAttributes` WHERE `folder` =  " . $this->_id;
1232        if (!$db->getResult($queryStr)) {
1233            $db->rollbackTransaction();
1234            return false;
1235        }
1236        $queryStr = "DELETE FROM `tblACLs` WHERE `target` = ". $this->_id. " AND `targetType` = " . T_FOLDER;
1237        if (!$db->getResult($queryStr)) {
1238            $db->rollbackTransaction();
1239            return false;
1240        }
1241
1242        $queryStr = "DELETE FROM `tblNotify` WHERE `target` = ". $this->_id. " AND `targetType` = " . T_FOLDER;
1243        if (!$db->getResult($queryStr)) {
1244            $db->rollbackTransaction();
1245            return false;
1246        }
1247        $db->commitTransaction();
1248
1249        /* Check if 'onPostRemoveFolder' callback is set */
1250        if (isset($this->_dms->callbacks['onPostRemoveFromDatabaseFolder'])) {
1251            foreach ($this->_dms->callbacks['onPostRemoveFromDatabaseFolder'] as $callback) {
1252                /** @noinspection PhpStatementHasEmptyBodyInspection */
1253                if (!call_user_func($callback[0], $callback[1], $this->_id)) {
1254                }
1255            }
1256        }
1257
1258        return true;
1259    } /* }}} */
1260
1261    /**
1262     * Remove recursively a folder
1263     *
1264     * Removes a folder, all its subfolders and documents
1265     * This method triggers the callbacks onPreRemoveFolder and onPostRemoveFolder.
1266     * If onPreRemoveFolder returns a boolean then this method will return
1267     * imediately with the value returned by the callback. Otherwise the
1268     * regular removal is executed, which in turn
1269     * triggers further onPreRemoveFolder and onPostRemoveFolder callbacks
1270     * and its counterparts for documents (onPreRemoveDocument, onPostRemoveDocument).
1271     *
1272     * @return boolean true on success, false in case of an error
1273     */
1274    public function remove() { /* {{{ */
1275        /** @noinspection PhpUnusedLocalVariableInspection */
1276        $db = $this->_dms->getDB();
1277
1278        // Do not delete the root folder.
1279        if ($this->_id == $this->_dms->rootFolderID || !isset($this->_parentID) || ($this->_parentID == null) || ($this->_parentID == "") || ($this->_parentID == 0)) {
1280            return false;
1281        }
1282
1283        /* Check if 'onPreRemoveFolder' callback is set */
1284        if (isset($this->_dms->callbacks['onPreRemoveFolder'])) {
1285            foreach ($this->_dms->callbacks['onPreRemoveFolder'] as $callback) {
1286                $ret = call_user_func($callback[0], $callback[1], $this);
1287                if (is_bool($ret))
1288                    return $ret;
1289            }
1290        }
1291
1292        //Entfernen der Unterordner und Dateien
1293        $res = $this->getSubFolders();
1294        if (is_bool($res) && !$res) return false;
1295        $res = $this->getDocuments();
1296        if (is_bool($res) && !$res) return false;
1297
1298        foreach ($this->_subFolders as $subFolder) {
1299            $res = $subFolder->remove();
1300            if (!$res) {
1301                return false;
1302            }
1303        }
1304
1305        foreach ($this->_documents as $document) {
1306            $res = $document->remove();
1307            if (!$res) {
1308                return false;
1309            }
1310        }
1311
1312        $ret = $this->removeFromDatabase();
1313        if (!$ret)
1314            return $ret;
1315
1316        /* Check if 'onPostRemoveFolder' callback is set */
1317        if (isset($this->_dms->callbacks['onPostRemoveFolder'])) {
1318            foreach ($this->_dms->callbacks['onPostRemoveFolder'] as $callback) {
1319                call_user_func($callback[0], $callback[1], $this);
1320            }
1321        }
1322
1323        return $ret;
1324    } /* }}} */
1325
1326    /**
1327     * Empty recursively a folder
1328     *
1329     * Removes all subfolders and documents of a folder but not the folder itself
1330     * This method will call remove() on all its children.
1331     * This method triggers the callbacks onPreEmptyFolder and onPostEmptyFolder.
1332     * If onPreEmptyFolder returns a boolean then this method will return
1333     * imediately.
1334     * Be aware that the recursive calls of remove() will trigger the callbacks
1335     * onPreRemoveFolder, onPostRemoveFolder, onPreRemoveDocument and onPostRemoveDocument.
1336     *
1337     * @return boolean true on success, false in case of an error
1338     */
1339    public function emptyFolder() { /* {{{ */
1340        /** @noinspection PhpUnusedLocalVariableInspection */
1341        $db = $this->_dms->getDB();
1342
1343        /* Check if 'onPreEmptyFolder' callback is set */
1344        if (isset($this->_dms->callbacks['onPreEmptyFolder'])) {
1345            foreach ($this->_dms->callbacks['onPreEmptyFolder'] as $callback) {
1346                $ret = call_user_func($callback[0], $callback[1], $this);
1347                if (is_bool($ret))
1348                    return $ret;
1349            }
1350        }
1351
1352        //Entfernen der Unterordner und Dateien
1353        $res = $this->getSubFolders();
1354        if (is_bool($res) && !$res) return false;
1355        $res = $this->getDocuments();
1356        if (is_bool($res) && !$res) return false;
1357
1358        foreach ($this->_subFolders as $subFolder) {
1359            $res = $subFolder->remove();
1360            if (!$res) {
1361                return false;
1362            }
1363        }
1364
1365        foreach ($this->_documents as $document) {
1366            $res = $document->remove();
1367            if (!$res) {
1368                return false;
1369            }
1370        }
1371
1372        /* Check if 'onPostEmptyFolder' callback is set */
1373        if (isset($this->_dms->callbacks['onPostEmptyFolder'])) {
1374            foreach ($this->_dms->callbacks['onPostEmptyFolder'] as $callback) {
1375                call_user_func($callback[0], $callback[1], $this);
1376            }
1377        }
1378
1379        return true;
1380    } /* }}} */
1381
1382    /**
1383     * Returns a list of access rights
1384     *
1385     * If the folder inherits the access rights from the parent folder
1386     * those will be returned.
1387     * $mode and $op can be set to restrict the list of returned access
1388     * rights. If $mode is set to M_ANY no restriction will apply
1389     * regardless of the value of $op. The returned array contains a list
1390     * of {@link SeedDMS_Core_UserAccess} and
1391     * {@link SeedDMS_Core_GroupAccess} objects. Even if the document
1392     * has no access list the returned array contains the two elements
1393     * 'users' and 'groups' which are than empty. The methode returns false
1394     * if the function fails.
1395     *
1396     * @param integer $mode access mode (defaults to M_ANY)
1397     * @param integer $op operation (defaults to O_EQ)
1398     * @return bool|SeedDMS_Core_GroupAccess|SeedDMS_Core_UserAccess
1399     */
1400    public function getAccessList($mode = M_ANY, $op = O_EQ) { /* {{{ */
1401        $db = $this->_dms->getDB();
1402
1403        if ($this->inheritsAccess()) {
1404            /* Access is supposed to be inherited but it could be that there
1405             * is no parent because the configured root folder id is somewhere
1406             * below the actual root folder.
1407             */
1408            $res = $this->getParent();
1409            if ($res)
1410                return $this->_parent->getAccessList($mode, $op);
1411        }
1412
1413        if (!isset($this->_accessList[$mode])) {
1414            if ($op!=O_GTEQ && $op!=O_LTEQ && $op!=O_EQ) {
1415                return false;
1416            }
1417            $modeStr = "";
1418            if ($mode!=M_ANY) {
1419                $modeStr = " AND `mode`".$op.(int)$mode;
1420            }
1421            $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1422                " AND `target` = " . $this->_id .    $modeStr . " ORDER BY `targetType`";
1423            $resArr = $db->getResultArray($queryStr);
1424            if (is_bool($resArr) && !$resArr)
1425                return false;
1426
1427            $this->_accessList[$mode] = array("groups" => array(), "users" => array());
1428            foreach ($resArr as $row) {
1429                if ($row["userID"] != -1)
1430                    array_push($this->_accessList[$mode]["users"], new SeedDMS_Core_UserAccess($this->_dms->getUser($row["userID"]), (int) $row["mode"]));
1431                else //if ($row["groupID"] != -1)
1432                    array_push($this->_accessList[$mode]["groups"], new SeedDMS_Core_GroupAccess($this->_dms->getGroup($row["groupID"]), (int) $row["mode"]));
1433            }
1434        }
1435
1436        return $this->_accessList[$mode];
1437    } /* }}} */
1438
1439    /**
1440     * Delete all entries for this folder from the access control list
1441     *
1442     * @param boolean $noclean set to true if notifier list shall not be clean up
1443     * @return boolean true if operation was successful otherwise false
1444     */
1445    public function clearAccessList($noclean = false) { /* {{{ */
1446        $db = $this->_dms->getDB();
1447
1448        $queryStr = "DELETE FROM `tblACLs` WHERE `targetType` = " . T_FOLDER . " AND `target` = " . $this->_id;
1449        if (!$db->getResult($queryStr))
1450            return false;
1451
1452        unset($this->_accessList);
1453        $this->_readAccessList = array();
1454
1455        if (!$noclean)
1456            $this->cleanNotifyList();
1457
1458        return true;
1459    } /* }}} */
1460
1461    /**
1462     * Add access right to folder
1463     *
1464     * This method may change in the future. Instead of passing the a flag
1465     * and a user/group id a user or group object will be expected.
1466     *
1467     * @param integer $mode access mode
1468     * @param integer $userOrGroupID id of user or group
1469     * @param integer $isUser set to 1 if $userOrGroupID is the id of a
1470     *        user
1471     * @return bool
1472     */
1473    public function addAccess($mode, $userOrGroupID, $isUser) { /* {{{ */
1474        $db = $this->_dms->getDB();
1475
1476        if ($mode < M_NONE || $mode > M_ALL)
1477            return false;
1478
1479        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1480
1481        /* Adding a second access right will return false */
1482        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1483                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ". (int) $userOrGroupID;
1484        $resArr = $db->getResultArray($queryStr);
1485        if (is_bool($resArr) || $resArr)
1486            return false;
1487
1488        $queryStr = "INSERT INTO `tblACLs` (`target`, `targetType`, ".$userOrGroup.", `mode`) VALUES
1489                    (".$this->_id.", ".T_FOLDER.", " . (int) $userOrGroupID . ", " .(int) $mode. ")";
1490        if (!$db->getResult($queryStr))
1491            return false;
1492
1493        unset($this->_accessList);
1494        $this->_readAccessList = array();
1495
1496        // Update the notify list, if necessary.
1497        if ($mode == M_NONE) {
1498            $this->removeNotify($userOrGroupID, $isUser);
1499        }
1500
1501        return true;
1502    } /* }}} */
1503
1504    /**
1505     * Change access right of folder
1506     *
1507     * This method may change in the future. Instead of passing the a flag
1508     * and a user/group id a user or group object will be expected.
1509     *
1510     * @param integer $newMode access mode
1511     * @param integer $userOrGroupID id of user or group
1512     * @param integer $isUser set to 1 if $userOrGroupID is the id of a
1513     *        user
1514     * @return bool
1515     */
1516    public function changeAccess($newMode, $userOrGroupID, $isUser) { /* {{{ */
1517        $db = $this->_dms->getDB();
1518
1519        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1520
1521        /* Get the old access right */
1522        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1523                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ". (int) $userOrGroupID;
1524        $resArr = $db->getResultArray($queryStr);
1525        if (!$resArr)
1526            return false;
1527
1528        $oldmode = $resArr[0]['mode'];
1529
1530        $queryStr = "UPDATE `tblACLs` SET `mode` = " . (int) $newMode . " WHERE `targetType` = ".T_FOLDER." AND `target` = " . $this->_id . " AND " . $userOrGroup . " = " . (int) $userOrGroupID;
1531        if (!$db->getResult($queryStr))
1532            return false;
1533
1534        unset($this->_accessList);
1535        $this->_readAccessList = array();
1536
1537        // Update the notify list, if necessary.
1538        if ($newMode == M_NONE) {
1539            $this->removeNotify($userOrGroupID, $isUser);
1540        }
1541
1542        return $oldmode;
1543    } /* }}} */
1544
1545    /**
1546     * Remove all access rights of folder
1547     *
1548     * This method removes all access rights of a given user or group.
1549     *
1550     * @param $userOrGroupID
1551     * @param $isUser
1552     * @return bool
1553     */
1554    public function removeAccess($userOrGroupID, $isUser) { /* {{{ */
1555        $db = $this->_dms->getDB();
1556
1557        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1558
1559        /* Get the old access right */
1560        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_FOLDER.
1561                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ". (int) $userOrGroupID;
1562        $resArr = $db->getResultArray($queryStr);
1563        if (!$resArr)
1564            return false;
1565
1566        $queryStr = "DELETE FROM `tblACLs` WHERE `targetType` = ".T_FOLDER." AND `target` = ".$this->_id." AND ".$userOrGroup." = " . (int) $userOrGroupID;
1567        if (!$db->getResult($queryStr))
1568            return false;
1569
1570        unset($this->_accessList);
1571        $this->_readAccessList = array();
1572
1573        // Update the notify list, if necessary.
1574        $mode = ($isUser ? $this->getAccessMode($this->_dms->getUser($userOrGroupID)) : $this->getGroupAccessMode($this->_dms->getGroup($userOrGroupID)));
1575        if ($mode == M_NONE) {
1576            $this->removeNotify($userOrGroupID, $isUser);
1577        }
1578
1579        return true;
1580    } /* }}} */
1581
1582    /**
1583     * Get the access mode of a user on the folder
1584     *
1585     * The access mode is either M_READ, M_READWRITE, M_ALL, or M_NONE.
1586     * It is determined
1587     * - by the user (admins and owners have always access mode M_ALL)
1588     * - by the access list for the user (possibly inherited)
1589     * - by the default access mode
1590     *
1591     * This method returns the access mode for a given user. An administrator
1592     * and the owner of the folder has unrestricted access. A guest user has
1593     * read only access or no access if access rights are further limited
1594     * by access control lists all the default access.
1595     * All other users have access rights according
1596     * to the access control lists or the default access. This method will
1597     * recursively check for access rights of parent folders if access rights
1598     * are inherited.
1599     *
1600     * Before checking the access itself a callback 'onCheckAccessFolder'
1601     * is called. If it returns a value > 0, then this will be returned by this
1602     * method without any further checks. The optional paramater $context
1603     * will be passed as a third parameter to the callback. It contains
1604     * the operation for which the access mode is retrieved. It is for example
1605     * set to 'removeDocument' if the access mode is used to check for sufficient
1606     * permission on deleting a document. This callback could be used to
1607     * override any existing access mode in a certain context.
1608     *
1609     * @param SeedDMS_Core_User $user user for which access shall be checked
1610     * @param string $context context in which the access mode is requested
1611     * @return integer access mode
1612     */
1613    public function getAccessMode($user, $context = '') { /* {{{ */
1614        if (!$user)
1615            return M_NONE;
1616
1617        /* Check if 'onCheckAccessFolder' callback is set */
1618        if (isset($this->_dms->callbacks['onCheckAccessFolder'])) {
1619            foreach ($this->_dms->callbacks['onCheckAccessFolder'] as $callback) {
1620                if (($ret = call_user_func($callback[0], $callback[1], $this, $user, $context)) > 0) {
1621                    return $ret;
1622                }
1623            }
1624        }
1625
1626        /* Administrators have unrestricted access */
1627        if ($user->isAdmin()) return M_ALL;
1628
1629        /* The owner of the folder has unrestricted access */
1630        if ($user->getID() == $this->_ownerID) return M_ALL;
1631
1632        if ($this->_dms->memcache) {
1633            $ck = "am:f".$this->_id.":".$user->getId().":".$context;
1634            if ($cobj = $this->_dms->memcache->get($ck))
1635                return $cobj;
1636        }
1637
1638        /* Check ACLs */
1639        $accessList = $this->getAccessList();
1640        if (!$accessList) return false;
1641
1642        /** @var SeedDMS_Core_UserAccess $userAccess */
1643        foreach ($accessList["users"] as $userAccess) {
1644            if ($userAccess->getUserID() == $user->getID()) {
1645                $mode = $userAccess->getMode();
1646                if ($user->isGuest()) {
1647                    if ($mode >= M_READ) $mode = M_READ;
1648                }
1649                if ($this->_dms->memcache)
1650                    $this->_dms->memcache->set($ck, $mode, 600);
1651                return $mode;
1652            }
1653        }
1654
1655        /* Get the highest right defined by a group */
1656        if ($accessList['groups']) {
1657            $mode = 0;
1658            /** @var SeedDMS_Core_GroupAccess $groupAccess */
1659            foreach ($accessList["groups"] as $groupAccess) {
1660                if ($user->isMemberOfGroup($groupAccess->getGroup())) {
1661                    if ($groupAccess->getMode() > $mode)
1662                        $mode = $groupAccess->getMode();
1663                }
1664            }
1665            if ($mode) {
1666                if ($user->isGuest()) {
1667                    if ($mode >= M_READ) $mode = M_READ;
1668                }
1669                if ($this->_dms->memcache)
1670                    $this->_dms->memcache->set($ck, $mode, 600);
1671                return $mode;
1672            }
1673        }
1674
1675        $mode = $this->getDefaultAccess();
1676        if ($user->isGuest()) {
1677            if ($mode >= M_READ) $mode = M_READ;
1678        }
1679        if ($this->_dms->memcache)
1680            $this->_dms->memcache->set($ck, $mode, 600);
1681        return $mode;
1682    } /* }}} */
1683
1684    /**
1685     * Get the access mode for a group on the folder
1686     *
1687     * This method returns the access mode for a given group. The algorithmn
1688     * applied to get the access mode is the same as describe at
1689     * {@link getAccessMode}
1690     *
1691     * @param SeedDMS_Core_Group $group group for which access shall be checked
1692     * @return integer access mode
1693     */
1694    public function getGroupAccessMode($group) { /* {{{ */
1695        $highestPrivileged = M_NONE;
1696        $foundInACL = false;
1697        $accessList = $this->getAccessList();
1698        if (!$accessList)
1699            return false;
1700
1701        /** @var SeedDMS_Core_GroupAccess $groupAccess */
1702        foreach ($accessList["groups"] as $groupAccess) {
1703            if ($groupAccess->getGroupID() == $group->getID()) {
1704                $foundInACL = true;
1705                if ($groupAccess->getMode() > $highestPrivileged)
1706                    $highestPrivileged = $groupAccess->getMode();
1707                if ($highestPrivileged == M_ALL) /* no need to check further */
1708                    return $highestPrivileged;
1709            }
1710        }
1711        if ($foundInACL)
1712            return $highestPrivileged;
1713
1714        /* Take default access */
1715        return $this->getDefaultAccess();
1716    } /* }}} */
1717
1718    /**
1719     * Get a list of all notification
1720     *
1721     * This method returns all users and groups that have registerd a
1722     * notification for the folder
1723     *
1724     * @param integer $type type of notification (not yet used)
1725     * @param bool $incdisabled set to true if disabled user shall be included
1726     * @return SeedDMS_Core_User[]|SeedDMS_Core_Group[]|bool array with a the elements 'users' and 'groups' which
1727     *        contain a list of users and groups.
1728     */
1729    public function getNotifyList($type = 0, $incdisabled = false) { /* {{{ */
1730        if (empty($this->_notifyList)) {
1731            $db = $this->_dms->getDB();
1732
1733            $queryStr ="SELECT * FROM `tblNotify` WHERE `targetType` = " . T_FOLDER . " AND `target` = " . $this->_id;
1734            $resArr = $db->getResultArray($queryStr);
1735            if (is_bool($resArr) && $resArr == false)
1736                return false;
1737
1738            $this->_notifyList = array("groups" => array(), "users" => array());
1739            foreach ($resArr as $row) {
1740                if ($row["userID"] != -1) {
1741                    $u = $this->_dms->getUser($row["userID"]);
1742                    if ($u && (!$u->isDisabled() || $incdisabled))
1743                        array_push($this->_notifyList["users"], $u);
1744                } else {//if ($row["groupID"] != -1)
1745                    $g = $this->_dms->getGroup($row["groupID"]);
1746                    if ($g)
1747                        array_push($this->_notifyList["groups"], $g);
1748                }
1749            }
1750        }
1751        return $this->_notifyList;
1752    } /* }}} */
1753
1754    /**
1755     * Make sure only users/groups with read access are in the notify list
1756     *
1757     */
1758    public function cleanNotifyList() { /* {{{ */
1759        // If any of the notification subscribers no longer have read access,
1760        // remove their subscription.
1761        if (empty($this->_notifyList))
1762            $this->getNotifyList();
1763
1764        /* Make a copy of both notifier lists because removeNotify will empty
1765         * $this->_notifyList and the second foreach will not work anymore.
1766         */
1767        /** @var SeedDMS_Core_User[] $nusers */
1768        $nusers = $this->_notifyList["users"];
1769        $ngroups = $this->_notifyList["groups"];
1770        foreach ($nusers as $u) {
1771            if ($this->getAccessMode($u) < M_READ) {
1772                $this->removeNotify($u->getID(), true);
1773            }
1774        }
1775
1776        /** @var SeedDMS_Core_Group[] $ngroups */
1777        foreach ($ngroups as $g) {
1778            if ($this->getGroupAccessMode($g) < M_READ) {
1779                $this->removeNotify($g->getID(), false);
1780            }
1781        }
1782    } /* }}} */
1783
1784    /**
1785     * Add a user/group to the notification list
1786     *
1787     * This method does not check if the currently logged in user
1788     * is allowed to add a notification. This must be checked by the calling
1789     * application.
1790     *
1791     * @param integer $userOrGroupID
1792     * @param boolean $isUser true if $userOrGroupID is a user id otherwise false
1793     * @return integer error code
1794     *    -1: Invalid User/Group ID.
1795     *    -2: Target User / Group does not have read access.
1796     *    -3: User is already subscribed.
1797     *    -4: Database / internal error.
1798     *     0: Update successful.
1799     */
1800    public function addNotify($userOrGroupID, $isUser) { /* {{{ */
1801        $db = $this->_dms->getDB();
1802
1803        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1804
1805        /* Verify that user / group exists */
1806        /** @var SeedDMS_Core_User|SeedDMS_Core_Group $obj */
1807        $obj = ($isUser ? $this->_dms->getUser($userOrGroupID) : $this->_dms->getGroup($userOrGroupID));
1808        if (!is_object($obj)) {
1809            return -1;
1810        }
1811
1812        /* Verify that the requesting user has permission to add the target to
1813         * the notification system.
1814         */
1815        /*
1816         * The calling application should enforce the policy on who is allowed
1817         * to add someone to the notification system. If is shall remain here
1818         * the currently logged in user should be passed to this function
1819         *
1820        GLOBAL $user;
1821        if ($user->isGuest()) {
1822            return -2;
1823        }
1824        if (!$user->isAdmin()) {
1825            if ($isUser) {
1826                if ($user->getID() != $obj->getID()) {
1827                    return -2;
1828                }
1829            }
1830            else {
1831                if (!$obj->isMember($user)) {
1832                    return -2;
1833                }
1834            }
1835        }
1836        */
1837
1838        //
1839        // Verify that user / group has read access to the document.
1840        //
1841        if ($isUser) {
1842            // Users are straightforward to check.
1843            if ($this->getAccessMode($obj) < M_READ) {
1844                return -2;
1845            }
1846        } else {
1847            // FIXME: Why not check the access list first and if this returns
1848            // not result, then use the default access?
1849            // Groups are a little more complex.
1850            if ($this->getDefaultAccess() >= M_READ) {
1851                // If the default access is at least READ-ONLY, then just make sure
1852                // that the current group has not been explicitly excluded.
1853                $acl = $this->getAccessList(M_NONE, O_EQ);
1854                $found = false;
1855                /** @var SeedDMS_Core_GroupAccess $group */
1856                foreach ($acl["groups"] as $group) {
1857                    if ($group->getGroupID() == $userOrGroupID) {
1858                        $found = true;
1859                        break;
1860                    }
1861                }
1862                if ($found) {
1863                    return -2;
1864                }
1865            } else {
1866                // The default access is restricted. Make sure that the group has
1867                // been explicitly allocated access to the document.
1868                $acl = $this->getAccessList(M_READ, O_GTEQ);
1869                if (is_bool($acl)) {
1870                    return -4;
1871                }
1872                $found = false;
1873                /** @var SeedDMS_Core_GroupAccess $group */
1874                foreach ($acl["groups"] as $group) {
1875                    if ($group->getGroupID() == $userOrGroupID) {
1876                        $found = true;
1877                        break;
1878                    }
1879                }
1880                if (!$found) {
1881                    return -2;
1882                }
1883            }
1884        }
1885        //
1886        // Check to see if user/group is already on the list.
1887        //
1888        $queryStr = "SELECT * FROM `tblNotify` WHERE `tblNotify`.`target` = '".$this->_id."' ".
1889            "AND `tblNotify`.`targetType` = '".T_FOLDER."' ".
1890            "AND `tblNotify`.".$userOrGroup." = '". (int) $userOrGroupID."'";
1891        $resArr = $db->getResultArray($queryStr);
1892        if (is_bool($resArr)) {
1893            return -4;
1894        }
1895        if (count($resArr)>0) {
1896            return -3;
1897        }
1898
1899        $queryStr = "INSERT INTO `tblNotify` (`target`, `targetType`, " . $userOrGroup . ") VALUES (" . $this->_id . ", " . T_FOLDER . ", " .  (int) $userOrGroupID . ")";
1900        if (!$db->getResult($queryStr))
1901            return -4;
1902
1903        unset($this->_notifyList);
1904        return 0;
1905    } /* }}} */
1906
1907    /**
1908     * Removes notify for a user or group to folder
1909     *
1910     * This method does not check if the currently logged in user
1911     * is allowed to remove a notification. This must be checked by the calling
1912     * application.
1913     *
1914     * @param integer $userOrGroupID
1915     * @param boolean $isUser true if $userOrGroupID is a user id otherwise false
1916     * @param int $type type of notification (0 will delete all) Not used yet!
1917     * @return int error code
1918     *    -1: Invalid User/Group ID.
1919     * -3: User is not subscribed.
1920     * -4: Database / internal error.
1921     * 0: Update successful.
1922     */
1923    public function removeNotify($userOrGroupID, $isUser, $type = 0) { /* {{{ */
1924        $db = $this->_dms->getDB();
1925
1926        /* Verify that user / group exists. */
1927        $obj = ($isUser ? $this->_dms->getUser($userOrGroupID) : $this->_dms->getGroup($userOrGroupID));
1928        if (!is_object($obj)) {
1929            return -1;
1930        }
1931
1932        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1933
1934        /* Verify that the requesting user has permission to add the target to
1935         * the notification system.
1936         */
1937        /*
1938         * The calling application should enforce the policy on who is allowed
1939         * to add someone to the notification system. If is shall remain here
1940         * the currently logged in user should be passed to this function
1941         *
1942        GLOBAL  $user;
1943        if ($user->isGuest()) {
1944            return -2;
1945        }
1946        if (!$user->isAdmin()) {
1947            if ($isUser) {
1948                if ($user->getID() != $obj->getID()) {
1949                    return -2;
1950                }
1951            }
1952            else {
1953                if (!$obj->isMember($user)) {
1954                    return -2;
1955                }
1956            }
1957        }
1958        */
1959
1960        //
1961        // Check to see if the target is in the database.
1962        //
1963        $queryStr = "SELECT * FROM `tblNotify` WHERE `tblNotify`.`target` = '".$this->_id."' ".
1964            "AND `tblNotify`.`targetType` = '".T_FOLDER."' ".
1965            "AND `tblNotify`.".$userOrGroup." = '". (int) $userOrGroupID."'";
1966        $resArr = $db->getResultArray($queryStr);
1967        if (is_bool($resArr)) {
1968            return -4;
1969        }
1970        if (count($resArr)==0) {
1971            return -3;
1972        }
1973
1974        $queryStr = "DELETE FROM `tblNotify` WHERE `target` = " . $this->_id . " AND `targetType` = " . T_FOLDER . " AND " . $userOrGroup . " = " .  (int) $userOrGroupID;
1975        /* If type is given then delete only those notifications */
1976        if ($type)
1977            $queryStr .= " AND `type` = ".(int) $type;
1978        if (!$db->getResult($queryStr))
1979            return -4;
1980
1981        unset($this->_notifyList);
1982        return 0;
1983    } /* }}} */
1984
1985    /**
1986     * Get List of users and groups which have read access on the folder.
1987     * The list will not include any guest users,
1988     * administrators and the owner of the folder.
1989     *
1990     * This method is deprecated. Use
1991     * {@see SeedDMS_Core_Folder::getReadAccessList()} instead.
1992     */
1993    public function getApproversList() { /* {{{ */
1994        return $this->getReadAccessList(0, 0);
1995    } /* }}} */
1996
1997    /**
1998     * Returns a list of groups and users with read access on the folder
1999     *
2000     * The list will not include any guest users,
2001     * administrators and the owner of the folder unless $listadmin resp.
2002     * $listowner is set to true.
2003     *
2004     * @param boolean $listadmin if set to true any admin will be listed too
2005     * @param boolean $listowner if set to true the owner will be listed too
2006     * @param boolean $listguest if set to true any guest will be listed too
2007     * @return array list of users and groups
2008     */
2009    public function getReadAccessList($listadmin = 0, $listowner = 0, $listguest = 0) { /* {{{ */
2010        $db = $this->_dms->getDB();
2011
2012        $cachehash = substr(md5($listadmin.$listowner.$listguest), 0, 3);
2013        if (!isset($this->_readAccessList[$cachehash])) {
2014            $this->_readAccessList[$cachehash] = array("groups" => array(), "users" => array());
2015            $userIDs = "";
2016            $groupIDs = "";
2017            $defAccess  = $this->getDefaultAccess();
2018
2019            /* Check if the default access is < read access or >= read access.
2020             * If default access is less than read access, then create a list
2021             * of users and groups with read access.
2022             * If default access is equal or greater then read access, then
2023             * create a list of users and groups without read access.
2024             */
2025            if ($defAccess<M_READ) {
2026                // Get the list of all users and groups that are listed in the ACL as
2027                // having read access to the folder.
2028                $tmpList = $this->getAccessList(M_READ, O_GTEQ);
2029            } else {
2030                // Get the list of all users and groups that DO NOT have read access
2031                // to the folder.
2032                $tmpList = $this->getAccessList(M_NONE, O_LTEQ);
2033            }
2034            /** @var SeedDMS_Core_GroupAccess $groupAccess */
2035            foreach ($tmpList["groups"] as $groupAccess) {
2036                $groupIDs .= (strlen($groupIDs)==0 ? "" : ", ") . $groupAccess->getGroupID();
2037            }
2038
2039            /** @var SeedDMS_Core_UserAccess $userAccess */
2040            foreach ($tmpList["users"] as $userAccess) {
2041                $user = $userAccess->getUser();
2042//                if (!$listadmin && $user->isAdmin()) continue;
2043//                if (!$listowner && $user->getID() == $this->_ownerID) continue;
2044//                if (!$listguest && $user->isGuest()) continue;
2045                $userIDs .= (strlen($userIDs)==0 ? "" : ", ") . $user->getID();
2046            }
2047
2048            // Construct a query against the users table to identify those users
2049            // that have read access on this folder, either directly through an
2050            // ACL entry, by virtue of ownership or by having administrative rights
2051            // on the database.
2052            $queryStr = "";
2053            /* If default access is less then read, $userIDs and $groupIDs contains
2054             * a list of user with read access
2055             */
2056            if ($defAccess < M_READ) {
2057                $queryStr = "SELECT DISTINCT `tblUsers`.* FROM `tblUsers` ".
2058                    "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`userID`=`tblUsers`.`id` ".
2059                    "WHERE 1=0".
2060                    ((strlen($groupIDs) > 0) ? " OR (`tblGroupMembers`.`groupID` IN (". $groupIDs ."))" : "").
2061                    ((strlen($userIDs) > 0) ?  " OR (`tblUsers`.`id` IN (". $userIDs ."))" : "").
2062                    " OR (`tblUsers`.`role` = ".SeedDMS_Core_User::role_admin.")".
2063                    " OR (`tblUsers`.`id` = ". $this->_ownerID . ")".
2064                    " ORDER BY `login`";
2065            } else {
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                $queryStr = "SELECT DISTINCT `tblUsers`.* FROM `tblUsers` ".
2072                    "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`userID`=`tblUsers`.`id` ".
2073                    "WHERE 1=1".
2074                    (strlen($groupIDs) == 0 ? "" : " AND (`tblGroupMembers`.`groupID` NOT IN (". $groupIDs .") OR `tblGroupMembers`.`groupID` IS NULL)").
2075                    (strlen($userIDs) == 0 ? "" : " AND (`tblUsers`.`id` NOT IN (". $userIDs ."))").
2076                    " OR `tblUsers`.`id` = ". $this->_ownerID . " OR `tblUsers`.`role` = ".SeedDMS_Core_User::role_admin." ORDER BY `login` ";
2077            }
2078            $resArr = $db->getResultArray($queryStr);
2079            if (!is_bool($resArr)) {
2080                foreach ($resArr as $row) {
2081                    $user = $this->_dms->getUser($row['id']);
2082                    if (!$listadmin && $user->isAdmin()) continue;
2083                    if (!$listowner && $user->getID() == $this->_ownerID) continue;
2084                    if (!$listguest && $user->isGuest()) continue;
2085                    $this->_readAccessList[$cachehash]["users"][] = $user;
2086                }
2087            }
2088
2089            // Assemble the list of groups that have read access to the folder.
2090            $queryStr = "";
2091            if ($defAccess < M_READ) {
2092                if (strlen($groupIDs)>0) {
2093                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ".
2094                        "WHERE `tblGroups`.`id` IN (". $groupIDs .") ORDER BY `name`";
2095                }
2096            } else {
2097                if (strlen($groupIDs)>0) {
2098                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ".
2099                        "WHERE `tblGroups`.`id` NOT IN (". $groupIDs .") ORDER BY `name`";
2100                } else {
2101                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ORDER BY `name`";
2102                }
2103            }
2104            if (strlen($queryStr)>0) {
2105                $resArr = $db->getResultArray($queryStr);
2106                if (!is_bool($resArr)) {
2107                    foreach ($resArr as $row) {
2108                        $group = $this->_dms->getGroup($row["id"]);
2109                        $this->_readAccessList[$cachehash]["groups"][] = $group;
2110                    }
2111                }
2112            }
2113        }
2114        return $this->_readAccessList[$cachehash];
2115    } /* }}} */
2116
2117    /**
2118     * Get the internally used folderList which stores the ids of folders from
2119     * the root folder to the parent folder.
2120     *
2121     * @return string column separated list of folder ids
2122     */
2123    public function getFolderList() { /* {{{ */
2124        $db = $this->_dms->getDB();
2125
2126        $queryStr = "SELECT `folderList` FROM `tblFolders` where `id` = ".$this->_id;
2127        $resArr = $db->getResultArray($queryStr);
2128        if (is_bool($resArr) && !$resArr)
2129            return false;
2130        return $resArr[0]['folderList'];
2131    } /* }}} */
2132
2133    /**
2134     * Checks the internal data of the folder and repairs it.
2135     * Currently, this function only repairs an incorrect folderList
2136     *
2137     * @return boolean true on success, otherwise false
2138     */
2139    public function repair() { /* {{{ */
2140        $db = $this->_dms->getDB();
2141
2142        $curfolderlist = $this->getFolderList();
2143
2144        // calculate the folderList of the folder
2145        $parent = $this->getParent();
2146        $pathPrefix = "";
2147        $path = $parent->getPath();
2148        foreach ($path as $f) {
2149            $pathPrefix .= ":".$f->getID();
2150        }
2151        if (strlen($pathPrefix)>1) {
2152            $pathPrefix .= ":";
2153        }
2154        if ($curfolderlist != $pathPrefix) {
2155            $queryStr = "UPDATE `tblFolders` SET `folderList`='".$pathPrefix."' WHERE `id` = ". $this->_id;
2156            $res = $db->getResult($queryStr);
2157            if (!$res)
2158                return false;
2159        }
2160        return true;
2161    } /* }}} */
2162
2163    /**
2164     * Get the min and max sequence value for documents
2165     *
2166     * @return bool|array array with keys 'min' and 'max', false in case of an error
2167     */
2168    public function getDocumentsMinMax() { /* {{{ */
2169        $db = $this->_dms->getDB();
2170
2171        $queryStr = "SELECT min(`sequence`) AS `min`, max(`sequence`) AS `max` FROM `tblDocuments` WHERE `folder` = " . (int) $this->_id;
2172        $resArr = $db->getResultArray($queryStr);
2173        if (is_bool($resArr) && $resArr == false)
2174            return false;
2175
2176        return $resArr[0];
2177    } /* }}} */
2178
2179    /**
2180     * Get the min and max sequence value for folders
2181     *
2182     * @return bool|array array with keys 'min' and 'max', false in case of an error
2183     */
2184    public function getFoldersMinMax() { /* {{{ */
2185        $db = $this->_dms->getDB();
2186
2187        $queryStr = "SELECT min(`sequence`) AS `min`, max(`sequence`) AS `max` FROM `tblFolders` WHERE `parent` = " . (int) $this->_id;
2188        $resArr = $db->getResultArray($queryStr);
2189        if (is_bool($resArr) && $resArr == false)
2190            return false;
2191
2192        return $resArr[0];
2193    } /* }}} */
2194
2195    /**
2196     * Reorder documents of folder
2197     *
2198     * Fix the sequence numbers of all documents in the folder, by assigning new
2199     * numbers starting from 1 incrementing by 1. This can be necessary if sequence
2200     * numbers are not unique which makes manual reordering for documents with
2201     * identical sequence numbers impossible.
2202     *
2203     * @return bool false in case of an error, otherwise true
2204     */
2205    public function reorderDocuments() { /* {{{ */
2206        $db = $this->_dms->getDB();
2207
2208        $queryStr = "SELECT `id` FROM `tblDocuments` WHERE `folder` = " . (int) $this->_id . " ORDER BY `sequence`";
2209        $resArr = $db->getResultArray($queryStr);
2210        if (is_bool($resArr) && $resArr == false)
2211            return false;
2212
2213        $db->startTransaction();
2214        $no = 1.0;
2215        foreach ($resArr as $doc) {
2216            $queryStr = "UPDATE `tblDocuments` SET `sequence` = " . $no . " WHERE `id` = ". $doc['id'];
2217            if (!$db->getResult($queryStr)) {
2218                $db->rollbackTransaction();
2219                return false;
2220            }
2221            $no += 1.0;
2222        }
2223        $db->commitTransaction();
2224
2225        return true;
2226    } /* }}} */
2227
2228
2229}