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