Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.51% covered (warning)
60.51%
1048 / 1732
58.95% covered (warning)
58.95%
56 / 95
CRAP
0.00% covered (danger)
0.00%
0 / 1
SeedDMS_Core_DMS
60.21% covered (warning)
60.21%
1035 / 1719
58.95% covered (warning)
58.95%
56 / 95
40612.59
0.00% covered (danger)
0.00%
0 / 1
 checkIfEqual
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 inList
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 checkDate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 filterAccess
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 filterUsersByAccess
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 filterDocumentLinks
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
90
 filterDocumentFiles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
56
 __construct
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
4.00
 getClassname
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setClassname
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getDecorators
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addDecorator
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStorage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDBVersion
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 checkVersion
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 setMemcache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRootFolderID
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setMaxDirID
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRootFolder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setForceRename
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setForceLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUser
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getLoggedInUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocument
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDocumentsByUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocumentsLockedByUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocumentsExpired
64.71% covered (warning)
64.71%
44 / 68
0.00% covered (danger)
0.00%
0 / 1
59.05
 getDocumentByName
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 getDocumentByOriginalFilename
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
7
 getDocumentContent
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 countTasks
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
420
 getDocumentList
27.06% covered (danger)
27.06%
69 / 255
0.00% covered (danger)
0.00%
0 / 1
3597.40
 makeTimeStamp
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
23
 getSqlForAttribute
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
1482
 search
60.95% covered (warning)
60.95%
192 / 315
0.00% covered (danger)
0.00%
0 / 1
1624.52
 getFolder
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 getFolderByName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 checkFolders
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
13
 checkDocuments
97.06% covered (success)
97.06%
33 / 34
0.00% covered (danger)
0.00%
0 / 1
16
 getUser
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 getUserByLogin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUserByEmail
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAllUsers
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addUser
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
 getGroup
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 getGroupByName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getAllGroups
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addGroup
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 getKeywordCategory
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getKeywordCategoryByName
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 getAllKeywordCategories
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getAllUserKeywordCategories
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 addKeywordCategory
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 getDocumentCategory
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getDocumentCategories
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getDocumentCategoryByName
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 addDocumentCategory
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 getNotificationsByGroup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNotificationsByUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createPasswordRequest
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 checkPasswordRequest
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 deletePasswordRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getAttributeDefinition
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getAttributeDefinitionByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAllAttributeDefinitions
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
12.76
 addAttributeDefinition
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
8.01
 getAllWorkflows
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
7.01
 getWorkflow
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getWorkflowByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 addWorkflow
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getWorkflowState
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getWorkflowStateByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAllWorkflowStates
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 addWorkflowState
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getWorkflowAction
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getWorkflowActionByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAllWorkflowActions
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 addWorkflowAction
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getWorkflowTransition
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getUnlinkedDocumentContent
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
3.85
 getNoFileSizeDocumentContent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getNoChecksumDocumentContent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getDuplicateDocumentContent
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 getDuplicateSequenceNo
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getLinksToItself
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getProcessWithoutUserGroup
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 removeProcessWithoutUserGroup
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 getStatisticalData
63.74% covered (warning)
63.74%
58 / 91
0.00% covered (danger)
0.00%
0 / 1
141.57
 getTimeline
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
6.47
 getLatestChanges
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
380
 getMimeTypes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 setCallback
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 addCallback
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 hasCallback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare(strict_types=1);
3
4/**
5 * Implementation of the document management system
6 *
7 * @category   DMS
8 * @package    SeedDMS_Core
9 * @license    GPL 2
10 * @author     Uwe Steinmann <uwe@steinmann.cx>
11 * @copyright  Copyright (C) 2010-2024 Uwe Steinmann
12 */
13
14/**
15 * Include some files
16 */
17require_once("inc.AccessUtils.php");
18require_once("inc.FileUtils.php");
19require_once("inc.ClassAccess.php");
20require_once("inc.ClassObject.php");
21require_once("inc.ClassFolder.php");
22require_once("inc.ClassDocument.php");
23require_once("inc.ClassGroup.php");
24require_once("inc.ClassUser.php");
25require_once("inc.ClassKeywords.php");
26require_once("inc.ClassNotification.php");
27require_once("inc.ClassAttribute.php");
28require_once("inc.ClassStorage.php");
29require_once("inc.ClassStorageFile.php");
30
31/**
32 * Class to represent the complete document management system.
33 * This class is needed to do most of the dms operations. It needs
34 * an instance of {@see SeedDMS_Core_DatabaseAccess} to access the
35 * underlying database. Many methods are factory functions which create
36 * objects representing the entities in the dms, like folders, documents,
37 * users, or groups.
38 *
39 * Each dms has its own database for meta data and a data store for document
40 * content. Both must be specified when creating a new instance of this class.
41 * All folders and documents are organized in a hierachy like
42 * a regular file system starting with a {@see SeedDMS_Core_DMS::rootFolderID}
43 *
44 * This class does not enforce any access rights on documents and folders
45 * by design. It is up to the calling application to use the methods
46 * {@see SeedDMS_Core_Folder::getAccessMode()} and
47 * {@see SeedDMS_Core_Document::getAccessMode()} and interpret them as desired.
48 * Though, there are two convenient functions to filter a list of
49 * documents/folders for which users have access rights for. See
50 * {@see SeedDMS_Core_DMS::filterAccess()}
51 * and {@see SeedDMS_Core_DMS::filterUsersByAccess()}
52 *
53 * Though, this class has a method to set the currently logged in user
54 * ({@see SeedDMS_Core_DMS::setUser()}), it does not have to be called, because
55 * there is currently no class within the SeedDMS core which needs the logged
56 * in user. {@see SeedDMS_Core_DMS} itself does not do any user authentication.
57 * It is up to the application using this class.
58 *
59 * ```php
60 * <?php
61 * include("inc/inc.ClassDMS.php");
62 * $db = new SeedDMS_Core_DatabaseAccess($type, $hostname, $user, $passwd, $name);
63 * $db->connect() or die ("Could not connect to db-server");
64 * $dms = new SeedDMS_Core_DMS($db, $contentDir);
65 * $dms->setRootFolderID(1);
66 * ...
67 * ?>
68 * ```
69 *
70 * @category   DMS
71 * @package    SeedDMS_Core
72 * @author     Uwe Steinmann <uwe@steinmann.cx>
73 * @copyright  Copyright (C) 2010-2024 Uwe Steinmann
74 */
75class SeedDMS_Core_DMS {
76    /**
77     * @var SeedDMS_Core_DatabaseAccess $db reference to database object. This must be an instance
78     *      of {@see SeedDMS_Core_DatabaseAccess}.
79     * @access protected
80     */
81    protected $db;
82
83    /**
84     * @var SeedDMS_Core_Storage $storage reference to storage object.
85     * This must be an instance {@see SeedDMS_Core_Storage_File}.
86     * @access protected
87     */
88    protected $storage;
89
90    /**
91     * @var object $memcache reference to memcache.
92     * @access protected
93     */
94    public $memcache;
95
96    /**
97     * @var array $classnames list of classnames for objects being instanciate
98     *      by the dms
99     * @access protected
100     */
101    protected $classnames;
102
103    /**
104     * @var array $decorators list of decorators for objects being instanciate
105     *      by the dms
106     * @access protected
107     */
108    protected $decorators;
109
110    /**
111     * @var SeedDMS_Core_User $user reference to currently logged in user. This must be
112     *      an instance of {@see SeedDMS_Core_User}. This variable is currently not
113     *      used. It is set by {@see SeedDMS_Core_DMS::setUser()}.
114     * @access private
115     */
116    private $user;
117
118    /**
119     * @var string $contentDir location in the file system where all the
120     *      document data is located. This should be an absolute path.
121     * @access public
122     */
123    public $contentDir;
124
125    /**
126     * @var integer $rootFolderID ID of root folder
127     * @access public
128     */
129    public $rootFolderID;
130
131    /**
132     * @var integer $maxDirID maximum number of documents per folder on the
133     *      filesystem. If this variable is set to a value != 0, the content
134     *      directory will have a two level hierarchy for document storage.
135     * @access public
136     */
137    public $maxDirID;
138
139    /**
140     * @var boolean $forceRename use renameFile() instead of copyFile() when
141     *      copying the document content into the data store. The default is
142     *      to copy the file. This parameter only affects the methods
143     *      SeedDMS_Core_Document::addDocument() and
144     *      SeedDMS_Core_Document::addDocumentFile(). Setting this to true
145     *      may save resources especially for large files.
146     * @access public
147     */
148    public $forceRename;
149
150    /**
151     * @var boolean $forceLink use linkFile() instead of copyFile() when
152     *      copying the document content into the data store. The default is
153     *      to copy the file. This parameter only affects the method
154     *      SeedDMS_Core_Document::addDocument(). Use this with care,
155     *      because it will leave the original document at its place.
156     * @access public
157     */
158    public $forceLink;
159
160    /**
161     * @var array $noReadForStatus list of status without read right
162     *      online.
163     * @access public
164     */
165    public $noReadForStatus;
166
167    /**
168     * @var boolean $checkWithinRootDir check if folder/document being accessed
169     *      is within the rootdir
170     * @access public
171     */
172    public $checkWithinRootDir;
173
174    /**
175     * @var string $version version of pear package
176     * @access public
177     */
178    public $version;
179
180    /**
181     * @var boolean $usecache true if internal cache shall be used
182     * @access public
183     */
184    public $usecache;
185
186    /**
187     * @var array $cache cache for various objects
188     * @access public
189     */
190    protected $cache;
191
192    /**
193     * @var array $callbacks list of methods called when certain operations,
194     * like removing a document, are executed. Set a callback with
195     * {@see SeedDMS_Core_DMS::setCallback()}.
196     * The key of the array is the internal callback function name. Each
197     * array element is an array with two elements: the function name
198     * and the parameter passed to the function.
199     *
200     * Currently implemented callbacks are:
201     *
202     * onPreRemoveDocument($user_param, $document);
203     *   called before deleting a document. If this function returns false
204     *   the document will not be deleted.
205     *
206     * onPostRemoveDocument($user_param, $document_id);
207     *   called after the successful deletion of a document.
208     *
209     * @access public
210     */
211    public $callbacks;
212
213    /**
214     * @var string last error message. This can be set by hooks to pass an
215     * error message from the hook to the application which has called the
216     * method containing the hook. For example SeedDMS_Core_Document::remove()
217     * calls the hook 'onPreRemoveDocument'. The hook function can set $dms->lasterror
218     * which can than be read when SeedDMS_Core_Document::remove() fails.
219     * This variable could be set in any SeedDMS_Core class, but is currently
220     * only set by hooks.
221     * @access public
222     */
223    public $lasterror;
224
225    /**
226     * @var SeedDMS_Core_DMS
227     */
228//    public $_dms;
229
230
231    /**
232     * Checks if two objects are equal by comparing their IDs
233     *
234     * The regular php check done by '==' compares all attributes of
235     * two objects, which is often not required. This method will first check
236     * if the objects are instances of the same class and than if they
237     * have the same id.
238     *
239     * @param object $object1 first object to be compared
240     * @param object $object2 second object to be compared
241     * @return boolean true if objects are equal, otherwise false
242     */
243    public static function checkIfEqual($object1, $object2) { /* {{{ */
244        if (get_class($object1) != get_class($object2))
245            return false;
246        if ($object1->getID() != $object2->getID())
247            return false;
248        return true;
249    } /* }}} */
250
251    /**
252     * Checks if a list of objects contains a single object by comparing their IDs
253     *
254     * This method is only applicable on list containing objects which have
255     * a method getID() because it is used to check if two objects are equal.
256     * The regular php check on objects done by '==' compares all attributes of
257     * two objects, which often isn't required. The method will first check
258     * if the objects are instances of the same class.
259     *
260     * The result of the function can be 0 which happens if the first element
261     * of an indexed array matches.
262     *
263     * @param object $object object to look for (needle)
264     * @param array $list list of objects (haystack)
265     * @return boolean|integer index in array if object was found, otherwise false
266     */
267    public static function inList($object, $list) { /* {{{ */
268        foreach ($list as $i => $item) {
269            if (get_class($item) == get_class($object) && $item->getID() == $object->getID())
270                return $i;
271        }
272        return false;
273    } /* }}} */
274
275    /**
276     * Checks if date conforms to a given format
277     *
278     * @param string $date date to be checked
279     * @param string $format format of date. Will default to 'Y-m-d H:i:s' if
280     * format is not given.
281     * @return boolean true if date is in propper format, otherwise false
282     */
283    public static function checkDate($date, $format = 'Y-m-d H:i:s') { /* {{{ */
284        $d = DateTime::createFromFormat($format, $date);
285        return $d && $d->format($format) == $date;
286    } /* }}} */
287
288    /**
289     * Filter out objects which are not accessible in a given mode by a user.
290     *
291     * The list of objects to be checked can be of any class, but has to have
292     * a method getAccessMode($user) which checks if the given user has at
293     * least the access right on the object as passed in $minMode.
294     * Hence, passing a group instead of a user is possible.
295     *
296     * @param array $objArr list of objects (either documents or folders)
297     * @param object $user user for which access is checked
298     * @param integer $minMode minimum access mode required (M_ANY, M_NONE,
299     *        M_READ, M_READWRITE, M_ALL)
300     * @return array filtered list of objects
301     */
302    public static function filterAccess($objArr, $user, $minMode) { /* {{{ */
303        if (!is_array($objArr)) {
304            return array();
305        }
306        $newArr = array();
307        foreach ($objArr as $obj) {
308            if ($obj->getAccessMode($user) >= $minMode)
309                array_push($newArr, $obj);
310        }
311        return $newArr;
312    } /* }}} */
313
314    /**
315     * Filter out users which cannot access an object in a given mode.
316     *
317     * The list of users to be checked can be of any class, but has to have
318     * a method getAccessMode($user) which checks if a user has at least the
319     * access right as passed in $minMode. Hence, passing a list of groups
320     * instead of users is possible.
321     *
322     * @param object $obj object that shall be accessed
323     * @param array $users list of users/groups which are to check for sufficient
324     *        access rights
325     * @param integer $minMode minimum access right on the object for each user
326     *        (M_ANY, M_NONE, M_READ, M_READWRITE, M_ALL)
327     * @return array filtered list of users
328     */
329    public static function filterUsersByAccess($obj, $users, $minMode) { /* {{{ */
330        $newArr = array();
331        foreach ($users as $currUser) {
332            if ($obj->getAccessMode($currUser) >= $minMode)
333                array_push($newArr, $currUser);
334        }
335        return $newArr;
336    } /* }}} */
337
338    /**
339     * Filter out document links which can not be accessed by a given user
340     *
341     * Returns a filtered list of links which are accessible by the
342     * given user. A link is only accessible, if it is publically visible,
343     * owned by the user, or the accessing user is an administrator.
344     *
345     * @param SeedDMS_Core_DocumentLink[] $links list of objects of type SeedDMS_Core_DocumentLink
346     * @param object $user user for which access is being checked
347     * @param string $access set if source or target of link shall be checked
348     * for sufficient access rights. Set to 'source' if the source document
349     * of a link is to be checked, set to 'target' for the target document.
350     * If not set, then access rights will not be checked at all.
351     * @return array filtered list of links
352     */
353    public static function filterDocumentLinks($user, $links, $access = '') { /* {{{ */
354        $tmp = array();
355        foreach ($links as $link) {
356            if ($link->isPublic() || ($link->getUser()->getID() == $user->getID()) || $user->isAdmin()){
357                if ($access == 'source') {
358                    $obj = $link->getDocument();
359                    if ($obj->getAccessMode($user) >= M_READ)
360                        array_push($tmp, $link);
361                } elseif ($access == 'target') {
362                    $obj = $link->getTarget();
363                    if ($obj->getAccessMode($user) >= M_READ)
364                        array_push($tmp, $link);
365                } else {
366                    array_push($tmp, $link);
367                }
368            }
369        }
370        return $tmp;
371    } /* }}} */
372
373    /**
374     * Filter out document attachments which can not be accessed by a given user
375     *
376     * Returns a filtered list of files which are accessible by the
377     * given user. A file is only accessible, if it is publically visible,
378     * owned by the user, or the accessing user is an administrator.
379     *
380     * @param array $files list of objects of type SeedDMS_Core_DocumentFile
381     * @param object $user user for which access is being checked
382     * @return array filtered list of files
383     */
384    public static function filterDocumentFiles($user, $files) { /* {{{ */
385        $tmp = array();
386        if ($files) {
387            foreach ($files as $file)
388                if ($file->isPublic() || ($file->getUser()->getID() == $user->getID()) || $user->isAdmin() || ($file->getDocument()->getOwner()->getID() == $user->getID()))
389                    array_push($tmp, $file);
390        }
391        return $tmp;
392    } /* }}} */
393
394    /** @noinspection PhpUndefinedClassInspection */
395    /**
396     * Create a new instance of the dms
397     *
398     * @param SeedDMS_Core_DatabaseAccess $db object of class {@see SeedDMS_Core_DatabaseAccess}
399     *        to access the underlying database
400     * @param string $contentDir path in filesystem containing the data store
401     *        all document contents is stored
402     */
403    public function __construct($db, $contentDir) { /* {{{ */
404        $this->db = $db;
405        if (is_object($contentDir)) {
406            $this->storage = $contentDir;
407        } else {
408            $this->storage = null;
409            if (substr($contentDir, -1) == DIRECTORY_SEPARATOR)
410                $this->contentDir = $contentDir;
411            else
412                $this->contentDir = $contentDir.DIRECTORY_SEPARATOR;
413        }
414        $this->memcache = null;
415        $this->rootFolderID = 1;
416        $this->user = null;
417        $this->maxDirID = 0; //31998;
418        $this->forceRename = false;
419        $this->forceLink = false;
420        $this->checkWithinRootDir = false;
421        $this->noReadForStatus = array();
422        $this->user = null;
423        $this->classnames = array();
424        $this->classnames['folder'] = 'SeedDMS_Core_Folder';
425        $this->classnames['document'] = 'SeedDMS_Core_Document';
426        $this->classnames['documentcontent'] = 'SeedDMS_Core_DocumentContent';
427        $this->classnames['documentfile'] = 'SeedDMS_Core_DocumentFile';
428        $this->classnames['user'] = 'SeedDMS_Core_User';
429        $this->classnames['group'] = 'SeedDMS_Core_Group';
430        $this->usecache = false;
431        $this->cache['users'] = [];
432        $this->cache['groups'] = [];
433        $this->cache['folders'] = [];
434        $this->callbacks = array();
435        $this->lasterror = '';
436        $this->version = '@package_version@';
437        if ($this->version[0] == '@')
438            $this->version = '5.1.36';
439    } /* }}} */
440
441    /**
442     * Return class name of classes instanciated by SeedDMS_Core
443     *
444     * This method returns the class name of those objects being instantiated
445     * by the dms. Each class has an internal place holder, which must be
446     * passed to function.
447     *
448     * @param string $objectname placeholder (can be one of 'folder', 'document',
449     * 'documentcontent', 'user', 'group')
450     *
451     * @return string/boolean name of class or false if object name is invalid
452     */
453    public function getClassname($objectname) { /* {{{ */
454        if (isset($this->classnames[$objectname]))
455            return $this->classnames[$objectname];
456        else
457            return false;
458    } /* }}} */
459
460    /**
461     * Set class name of instantiated objects
462     *
463     * This method sets the class name of those objects being instatiated
464     * by the dms. It is mainly used to create a new class (possible
465     * inherited from one of the available classes) implementing new
466     * features. The method should be called in the postInitDMS hook.
467     *
468     * @param string $objectname placeholder (can be one of 'folder', 'document',
469     * 'documentcontent', 'user', 'group'
470     * @param string $classname name of class
471     *
472     * @return string/boolean name of old class or false if not set
473     */
474    public function setClassname($objectname, $classname) { /* {{{ */
475        if (isset($this->classnames[$objectname]))
476            $oldclass =  $this->classnames[$objectname];
477        else
478            $oldclass = false;
479        $this->classnames[$objectname] = $classname;
480        return $oldclass;
481    } /* }}} */
482
483    /**
484     * Return list of decorators
485     *
486     * This method returns the list of decorator class names of those objects
487     * being instantiated
488     * by the dms. Each class has an internal place holder, which must be
489     * passed to function.
490     *
491     * @param string $objectname placeholder (can be one of 'folder', 'document',
492     * 'documentcontent', 'user', 'group')
493     *
494     * @return array/boolean list of class names or false if object name is invalid
495     */
496    public function getDecorators($objectname) { /* {{{ */
497        if (isset($this->decorators[$objectname]))
498            return $this->decorators[$objectname];
499        else
500            return false;
501    } /* }}} */
502
503    /**
504     * Add a decorator
505     *
506     * This method adds a single decorator class name to the list of decorators
507     * of those objects being instantiated
508     * by the dms. Each class has an internal place holder, which must be
509     * passed to function.
510     *
511     * @param string $objectname placeholder (can be one of 'folder', 'document',
512     * 'documentcontent', 'user', 'group')
513     *
514     * @return boolean true if decorator could be added, otherwise false
515     */
516    public function addDecorator($objectname, $decorator) { /* {{{ */
517        $this->decorators[$objectname][] = $decorator;
518        return true;
519    } /* }}} */
520
521    /**
522     * Return database where meta data is stored
523     *
524     * This method returns the database object as it was set by the first
525     * parameter of the constructor.
526     *
527     * @return SeedDMS_Core_DatabaseAccess database
528     */
529    public function getDB() { /* {{{ */
530        return $this->db;
531    } /* }}} */
532
533    /**
534     * Return storage where files are stored
535     *
536     * This method returns the storage object as it was set by the second
537     * parameter of the constructor.
538     *
539     * @return SeedDMS_Core_Storage
540     */
541    public function getStorage() { /* {{{ */
542        return $this->storage;
543    } /* }}} */
544
545    /**
546     * Return the database version
547     *
548     * @return array|bool
549     */
550    public function getDBVersion() { /* {{{ */
551        $tbllist = $this->db->TableList();
552        $tbllist = explode(',', strtolower(join(',', $tbllist)));
553        if (!in_array('tblversion', $tbllist))
554            return false;
555        $queryStr = "SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1";
556        $resArr = $this->db->getResultArray($queryStr);
557        if (is_bool($resArr) && $resArr == false)
558            return false;
559        if (count($resArr) != 1)
560            return false;
561        $resArr = $resArr[0];
562        return $resArr;
563    } /* }}} */
564
565    /**
566     * Check if the version in the database is the same as of this package
567     * Only the major and minor version number will be checked.
568     *
569     * @return boolean returns false if versions do not match, but returns
570     *         true if version matches or table tblVersion does not exists.
571     */
572    public function checkVersion() { /* {{{ */
573        $tbllist = $this->db->TableList();
574        $tbllist = explode(',', strtolower(join(',', $tbllist)));
575        if (!in_array('tblversion', $tbllist))
576            return true;
577        $queryStr = "SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1";
578        $resArr = $this->db->getResultArray($queryStr);
579        if (is_bool($resArr) && $resArr == false)
580            return false;
581        if (count($resArr) != 1)
582            return false;
583        $resArr = $resArr[0];
584        $ver = explode('.', $this->version);
585        if (($resArr['major'] != $ver[0]) || ($resArr['minor'] != $ver[1]))
586            return false;
587        return true;
588    } /* }}} */
589
590    /**
591     * Set memcache server
592     *
593     * This method must be called right after creating an instance of
594     * {@see SeedDMS_Core_DMS}
595     *
596     * If the memcache server is set, SeedDMS_Core_DMS will make use of
597     * it if possible.
598     *
599     * @param object $memcache memcache object created with new Memcached()
600     * @return void
601     */
602    public function setMemcache($memcache) { /* {{{ */
603        $this->memcache = $memcache;
604    } /* }}} */
605
606    /**
607     * Set id of root folder
608     *
609     * This method must be called right after creating an instance of
610     * {@see SeedDMS_Core_DMS}
611     *
612     * The new root folder id will only be set if the folder actually
613     * exists. In that case the old root folder id will be returned.
614     * If it does not exists, the method will return false;
615     * @param integer $id id of root folder
616     * @return boolean/int old root folder id if new root folder exists, otherwise false
617     */
618    public function setRootFolderID($id) { /* {{{ */
619        if ($this->getFolder($id)) {
620            $oldid = $this->rootFolderID;
621            $this->rootFolderID = $id;
622            return $oldid;
623        }
624        return false;
625    } /* }}} */
626
627    /**
628     * Set maximum number of subdirectories per directory
629     *
630     * The value of maxDirID is quite crucial, because each document is
631     * stored within a directory in the filesystem. Consequently, there can be
632     * a maximum number of documents, because depending on the file system
633     * the maximum number of subdirectories is limited. Since version 3.3.0 of
634     * SeedDMS an additional directory level has been introduced, which
635     * will be created when maxDirID is not 0. All documents
636     * from 1 to maxDirID-1 will be saved in 1/<docid>, documents from maxDirID
637     * to 2*maxDirID-1 are stored in 2/<docid> and so on.
638     *
639     * Modern file systems like ext4 do not have any restrictions on the number
640     * of subdirectories anymore. Therefore it is best if this parameter is
641     * set to 0. Never change this parameter if documents has already been
642     * created.
643     *
644     * This method must be called right after creating an instance of
645     * {@see SeedDMS_Core_DMS}
646     *
647     * @param integer $id id of root folder
648     */
649    public function setMaxDirID($id) { /* {{{ */
650        $this->maxDirID = $id;
651    } /* }}} */
652
653    /**
654     * Get root folder
655     *
656     * @return SeedDMS_Core_Folder|boolean return the object of the root folder or false if
657     *        the root folder id was not set before with {@see SeedDMS_Core_DMS::setRootFolderID()}.
658     */
659    public function getRootFolder() { /* {{{ */
660        if (!$this->rootFolderID) return false;
661        return $this->getFolder($this->rootFolderID);
662    } /* }}} */
663
664    public function setForceRename($enable) { /* {{{ */
665        $this->forceRename = $enable;
666    } /* }}} */
667
668    public function setForceLink($enable) { /* {{{ */
669        $this->forceLink = $enable;
670    } /* }}} */
671
672    /**
673     * Set the logged in user
674     *
675     * This method tells SeeDMS_Core_DMS the currently logged in user. It must be
676     * called right after instanciating the class, because some methods in
677     * SeedDMS_Core_Document() require the currently logged in user.
678     *
679     * @param object $user this muss not be empty and an instance of SeedDMS_Core_User
680     * @return bool|object returns the old user object or null on success, otherwise false
681     *
682     */
683    public function setUser($user) { /* {{{ */
684        if (!$user) {
685            $olduser = $this->user;
686            $this->user = null;
687            return $olduser;
688        }
689        if (is_object($user) && (get_class($user) == $this->getClassname('user'))) {
690            $olduser = $this->user;
691            $this->user = $user;
692            return $olduser;
693        }
694        return false;
695    } /* }}} */
696
697    /**
698     * Get the logged in user
699     *
700     * Returns the currently logged in user, previously set by {@see SeedDMS_Core_DMS::setUser()}
701     *
702     * @return SeedDMS_Core_User $user
703     *
704     */
705    public function getLoggedInUser() { /* {{{ */
706        return $this->user;
707    } /* }}} */
708
709    /**
710     * Return a document by its id
711     *
712     * This method retrieves a document from the database by its id.
713     *
714     * @param integer $id internal id of document
715     * @return SeedDMS_Core_Document instance of {@see SeedDMS_Core_Document}, null or false
716     */
717    public function getDocument($id) { /* {{{ */
718        $classname = $this->classnames['document'];
719        return $classname::getInstance($id, $this);
720    } /* }}} */
721
722    /**
723     * Returns all documents of a given user
724     *
725     * @param object $user
726     * @return array list of documents
727     */
728    public function getDocumentsByUser($user) { /* {{{ */
729        return $user->getDocuments();
730    } /* }}} */
731
732    /**
733     * Returns all documents locked by a given user
734     *
735     * @param object $user
736     * @return array list of documents
737     */
738    public function getDocumentsLockedByUser($user) { /* {{{ */
739        return $user->getDocumentsLocked();
740    } /* }}} */
741
742    /**
743     * Returns all documents which already expired or will expire in the future
744     *
745     * The parameter $date will be relative to the start of the day. It can
746     * be either a number of days (if an integer is passed) or a date string
747     * in the format 'YYYY-MM-DD'.
748     * If the parameter $date is a negative number or a date in the past, then
749     * all documents from the start of that date till the end of the current
750     * day will be returned. If $date is a positive integer or $date is a
751     * date in the future, then all documents from the start of the current
752     * day till the end of the day of the given date will be returned.
753     * Passing 0 or the
754     * current date in $date, will return all documents expiring the current
755     * day.
756     * @param string $date date in format YYYY-MM-DD or an integer with the number
757     *   of days. A negative value will cover the days in the past.
758     * @param SeedDMS_Core_User $user limits the documents on those owned
759     *   by this user
760     * @param string $orderby n=name, e=expired
761     * @param string $orderdir d=desc or a=asc
762     * @param bool $update update status of document if set to true
763     * @return bool|SeedDMS_Core_Document[]
764     */
765    public function getDocumentsExpired($date, $user = null, $orderby = 'e', $orderdir = 'desc', $update = true) { /* {{{ */
766        $db = $this->getDB();
767
768        if (!$db->createTemporaryTable("ttstatid") || !$db->createTemporaryTable("ttcontentid")) {
769            return false;
770        }
771
772        $tsnow = mktime(0, 0, 0); /* Start of today */
773        if (is_int($date) || is_string($date)) {
774            if (is_int($date)) {
775                $ts = $tsnow + $date * 86400;
776            } else {
777                $tmp = explode('-', $date, 3);
778                if (count($tmp) != 3)
779                    return false;
780                if (!self::checkDate($date, 'Y-m-d'))
781                    return false;
782                $ts = mktime(0, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
783            }
784            if ($ts < $tsnow) { /* Check for docs expired in the past */
785                $startts = $ts;
786                $endts = $tsnow+86400; /* Use end of day */
787                $updatestatus = $update;
788            } else { /* Check for docs which will expire in the future */
789                $startts = $tsnow;
790                $endts = $ts+86400; /* Use end of day */
791                $updatestatus = false;
792            }
793        }    elseif (is_array($date)) { // start and end date
794            if (!empty($date['start'])) {
795                if (is_int($date['start']))
796                    $startts = $date['start'];
797                else {
798                    $tmp = explode('-', $date['start'], 3);
799                    if (count($tmp) != 3)
800                        return false;
801                    if (!self::checkDate($date, 'Y-m-d'))
802                        return false;
803                    $startts = mktime(0, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
804                }
805            } else {
806                $startts = time();
807            }
808            if (!empty($date['end'])) {
809                if (is_int($date['end']))
810                    $endts = $date['end'];
811                else {
812                    $tmp = explode('-', $date['end'], 3);
813                    if (count($tmp) != 3)
814                        return false;
815                    if (!self::checkDate($date, 'Y-m-d'))
816                        return false;
817                    $endts = mktime(24, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
818                }
819            } else {
820                $endts = time() + 365*86400;
821            }
822            if (($startts < $tsnow) && ($endts < $tsnow))
823                $updatestatus = $update;
824            else
825                $updatestatus = false;
826        } else
827            return false;
828
829        /* Get all documents which have an expiration date. It doesn't check for
830         * the latest status which should be S_EXPIRED, but doesn't have to, because
831         * status may have not been updated after the expiration date has been reached.
832         **/
833        $queryStr = "SELECT `tblDocuments`.`id`, `tblDocumentStatusLog`.`status`  FROM `tblDocuments` ".
834            "LEFT JOIN `ttcontentid` ON `ttcontentid`.`document` = `tblDocuments`.`id` ".
835            "LEFT JOIN `tblDocumentContent` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` AND `tblDocumentContent`.`version` = `ttcontentid`.`maxVersion` ".
836            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID` = `tblDocumentContent`.`document` AND `tblDocumentContent`.`version` = `tblDocumentStatus`.`version` ".
837            "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
838            "LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatusLog`.`statusLogID` = `ttstatid`.`maxLogID`";
839        $queryStr .=
840            " WHERE `tblDocuments`.`expires` >= ".$startts." AND `tblDocuments`.`expires` < ".$endts;
841        if ($user)
842            $queryStr .=
843                " AND `tblDocuments`.`owner` = '".$user->getID()."' ";
844        $queryStr .=
845            " ORDER BY ".($orderby == 'e' ? "`expires`" : "`name`")." ".($orderdir == 'd' ? "DESC" : "ASC");
846
847        $resArr = $db->getResultArray($queryStr);
848        if (is_bool($resArr) && !$resArr)
849            return false;
850
851        /** @var SeedDMS_Core_Document[] $documents */
852        $documents = array();
853        foreach ($resArr as $row) {
854            $document = $this->getDocument($row["id"]);
855            if ($updatestatus) {
856                $document->verifyLastestContentExpriry();
857            }
858            $documents[] = $document;
859        }
860        return $documents;
861    } /* }}} */
862
863    /**
864     * Returns a document by its name
865     *
866     * This method searches a document by its name and restricts the search
867     * to the given folder if passed as the second parameter.
868     * If there are more than one document with that name, then only the
869     * one with the highest id will be returned.
870     *
871     * @param string $name Name of the document
872     * @param object $folder parent folder of document
873     * @return SeedDMS_Core_Document|null|boolean found document or null if not document was found or false in case of an error
874     */
875    public function getDocumentByName($name, $folder = null) { /* {{{ */
876        $name = trim($name);
877        if (!$name) return false;
878
879        $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser` ".
880            "FROM `tblDocuments` ".
881            "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
882            "WHERE `tblDocuments`.`name` = " . $this->db->qstr($name);
883        if ($folder)
884            $queryStr .= " AND `tblDocuments`.`folder` = ". $folder->getID();
885        if ($this->checkWithinRootDir)
886            $queryStr .= " AND `tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%'";
887        $queryStr .= " ORDER BY `tblDocuments`.`id` DESC LIMIT 1";
888
889        $resArr = $this->db->getResultArray($queryStr);
890        if (is_bool($resArr) && !$resArr)
891            return false;
892
893        if (!$resArr)
894            return null;
895
896        $row = $resArr[0];
897        /** @var SeedDMS_Core_Document $document */
898        $document = new $this->classnames['document']($row["id"], $row["name"], $row["comment"], $row["date"], $row["expires"], $row["owner"], $row["folder"], $row["inheritAccess"], $row["defaultAccess"], $row["lockUser"], $row["keywords"], $row["sequence"]);
899        $document->setDMS($this);
900        return $document;
901    } /* }}} */
902
903    /**
904     * Returns a document by the original file name of the last version
905     *
906     * This method searches a document by the name of the last document
907     * version and restricts the search
908     * to given folder if passed as the second parameter.
909     * If there are more than one document with that name, then only the
910     * one with the highest id will be returned.
911     *
912     * @param string $name Name of the original file
913     * @param object $folder parent folder of document
914     * @return SeedDMS_Core_Document|null|boolean found document or null if not document was found or false in case of an error
915     */
916    public function getDocumentByOriginalFilename($name, $folder = null) { /* {{{ */
917        $name = trim($name);
918        if (!$name) return false;
919
920        if (!$this->db->createTemporaryTable("ttcontentid")) {
921            return false;
922        }
923        $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser` ".
924            "FROM `tblDocuments` ".
925            "LEFT JOIN `ttcontentid` ON `ttcontentid`.`document` = `tblDocuments`.`id` ".
926            "LEFT JOIN `tblDocumentContent` ON `tblDocumentContent`.`document` = `tblDocuments`.`id` AND `tblDocumentContent`.`version` = `ttcontentid`.`maxVersion` ".
927            "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
928            "WHERE `tblDocumentContent`.`orgFileName` = " . $this->db->qstr($name);
929        if ($folder)
930            $queryStr .= " AND `tblDocuments`.`folder` = ". $folder->getID();
931        $queryStr .= " ORDER BY `tblDocuments`.`id` DESC LIMIT 1";
932
933        $resArr = $this->db->getResultArray($queryStr);
934        if (is_bool($resArr) && !$resArr)
935            return false;
936
937        if (!$resArr)
938            return null;
939
940        $row = $resArr[0];
941        /** @var SeedDMS_Core_Document $document */
942        $document = new $this->classnames['document']($row["id"], $row["name"], $row["comment"], $row["date"], $row["expires"], $row["owner"], $row["folder"], $row["inheritAccess"], $row["defaultAccess"], $row["lockUser"], $row["keywords"], $row["sequence"]);
943        $document->setDMS($this);
944        return $document;
945    } /* }}} */
946
947    /**
948     * Return a document content by its id
949     *
950     * This method retrieves a document content from the database by its id.
951     *
952     * @param integer $id internal id of document content
953     * @return bool|null|SeedDMS_Core_DocumentContent found document content or null if not document content was found or false in case of an error
954
955     */
956    public function getDocumentContent($id) { /* {{{ */
957        if (!is_numeric($id)) return false;
958        if ($id < 1) return false;
959
960        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `id` = ".(int) $id;
961        $resArr = $this->db->getResultArray($queryStr);
962        if (is_bool($resArr) && $resArr == false)
963            return false;
964        if (count($resArr) != 1)
965            return null;
966        $row = $resArr[0];
967
968        $document = $this->getDocument($row['document']);
969        $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
970        return $version;
971    } /* }}} */
972
973    /**
974     * Returns number of documents with a given task
975     *
976     * @param string $listtype type of document list, can be 'AppRevByMe',
977     * 'AppRevOwner', 'WorkflowByMe'
978     * @param object $user user
979     * @return array number of tasks
980     */
981    public function countTasks($listtype, $user = null, $param5 = true) { /* {{{ */
982        if (!$this->db->createTemporaryTable("ttstatid") || !$this->db->createTemporaryTable("ttcontentid")) {
983            return false;
984        }
985        $groups = array();
986        if ($user) {
987            $tmp = $user->getGroups();
988            foreach ($tmp as $group)
989                $groups[] = $group->getID();
990        }
991        $selectStr = "count(distinct ttcontentid.document) c ";
992        $queryStr =
993            "FROM `ttcontentid` ".
994            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID`=`ttcontentid`.`document` AND `tblDocumentStatus`.`version`=`ttcontentid`.`maxVersion` ".
995            "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
996            "LEFT JOIN `tblDocumentStatusLog` ON `ttstatid`.`statusID` = `tblDocumentStatusLog`.`statusID` AND `ttstatid`.`maxLogID` = `tblDocumentStatusLog`.`statusLogID` ";
997        switch ($listtype) {
998        case 'ReviewByMe': // Documents I have to review {{{
999            if (!$this->db->createTemporaryTable("ttreviewid")) {
1000                return false;
1001            }
1002            $queryStr .=
1003                "LEFT JOIN `tblDocumentReviewers` on `ttcontentid`.`document`=`tblDocumentReviewers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentReviewers`.`version` ".
1004                "LEFT JOIN `ttreviewid` ON `ttreviewid`.`reviewID` = `tblDocumentReviewers`.`reviewID` ".
1005                "LEFT JOIN `tblDocumentReviewLog` ON `tblDocumentReviewLog`.`reviewLogID`=`ttreviewid`.`maxLogID` ";
1006
1007            $queryStr .= "WHERE (`tblDocumentReviewers`.`type` = 0 AND `tblDocumentReviewers`.`required` = ".$user->getID()." ";
1008            if ($groups)
1009                $queryStr .= "OR `tblDocumentReviewers`.`type` = 1 AND `tblDocumentReviewers`.`required` IN (".implode(',', $groups).") ";
1010            $queryStr .= ") ";
1011            $docstatarr = array(S_DRAFT_REV);
1012            if ($param5)
1013                $docstatarr[] = S_EXPIRED;
1014            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1015            $queryStr .= "AND `tblDocumentReviewLog`.`status` = 0 ";
1016            break; /* }}} */
1017        case 'ApproveByMe': // Documents I have to approve {{{
1018            if (!$this->db->createTemporaryTable("ttapproveid")) {
1019                return false;
1020            }
1021            $queryStr .=
1022                "LEFT JOIN `tblDocumentApprovers` on `ttcontentid`.`document`=`tblDocumentApprovers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentApprovers`.`version` ".
1023                "LEFT JOIN `ttapproveid` ON `ttapproveid`.`approveID` = `tblDocumentApprovers`.`approveID` ".
1024                "LEFT JOIN `tblDocumentApproveLog` ON `tblDocumentApproveLog`.`approveLogID`=`ttapproveid`.`maxLogID` ";
1025
1026            if ($user) {
1027                $queryStr .= "WHERE (`tblDocumentApprovers`.`type` = 0 AND `tblDocumentApprovers`.`required` = ".$user->getID()." ";
1028                if ($groups)
1029                    $queryStr .= "OR `tblDocumentApprovers`.`type` = 1 AND `tblDocumentApprovers`.`required` IN (".implode(',', $groups).") ";
1030                $queryStr .= ") ";
1031            }
1032            $docstatarr = array(S_DRAFT_APP);
1033            if ($param5)
1034                $docstatarr[] = S_EXPIRED;
1035            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1036            $queryStr .= "AND `tblDocumentApproveLog`.`status` = 0 ";
1037            break; /* }}} */
1038        case 'WorkflowByMe': // Documents which need my workflow action {{{
1039
1040            $queryStr .=
1041                "LEFT JOIN `tblWorkflowDocumentContent` on `ttcontentid`.`document`=`tblWorkflowDocumentContent`.`document` AND `ttcontentid`.`maxVersion`=`tblWorkflowDocumentContent`.`version` ".
1042                "LEFT JOIN `tblWorkflowTransitions` on `tblWorkflowDocumentContent`.`workflow`=`tblWorkflowTransitions`.`workflow` AND `tblWorkflowDocumentContent`.`state`=`tblWorkflowTransitions`.`state` ".
1043                "LEFT JOIN `tblWorkflowTransitionUsers` on `tblWorkflowTransitionUsers`.`transition` = `tblWorkflowTransitions`.`id` ".
1044                "LEFT JOIN `tblWorkflowTransitionGroups` on `tblWorkflowTransitionGroups`.`transition` = `tblWorkflowTransitions`.`id` ";
1045
1046            if ($user) {
1047                $queryStr .= "WHERE (`tblWorkflowTransitionUsers`.`userid` = ".$user->getID()." ";
1048                if ($groups)
1049                    $queryStr .= "OR `tblWorkflowTransitionGroups`.`groupid` IN (".implode(',', $groups).")";
1050                $queryStr .= ") ";
1051            }
1052            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_IN_WORKFLOW." ";
1053            break; // }}}
1054        }
1055        if ($queryStr) {
1056            $resArr = $this->db->getResultArray('SELECT '.$selectStr.$queryStr);
1057            if (is_bool($resArr) && !$resArr) {
1058                return false;
1059            }
1060        } else {
1061            return false;
1062        }
1063        return $resArr[0]['c'];
1064    } /* }}} */
1065
1066    /**
1067     * Returns all documents with a predefined search criteria
1068     *
1069     * The records return have the following elements
1070     *
1071     * From Table tblDocuments
1072     * [id] => id of document
1073     * [name] => name of document
1074     * [comment] => comment of document
1075     * [date] => timestamp of creation date of document
1076     * [expires] => timestamp of expiration date of document
1077     * [owner] => user id of owner
1078     * [folder] => id of parent folder
1079     * [folderList] => column separated list of folder ids, e.g. :1:41:
1080     * [inheritAccess] => 1 if access is inherited
1081     * [defaultAccess] => default access mode
1082     * [locked] => always -1 (TODO: is this field still used?)
1083     * [keywords] => keywords of document
1084     * [sequence] => sequence of document
1085     *
1086     * From Table tblDocumentLocks
1087     * [lockUser] => id of user locking the document
1088     *
1089     * From Table tblDocumentStatusLog
1090     * [version] => latest version of document
1091     * [statusID] => id of latest status log
1092     * [documentID] => id of document
1093     * [status] => current status of document
1094     * [statusComment] => comment of current status
1095     * [statusDate] => datetime when the status was entered, e.g. 2014-04-17 21:35:51
1096     * [userID] => id of user who has initiated the status change
1097     *
1098     * From Table tblUsers
1099     * [ownerName] => name of owner of document
1100     * [statusName] => name of user who has initiated the status change
1101     *
1102     * @param string $listtype type of document list, can be 'AppRevByMe',
1103     * 'AppRevOwner', 'ReceiptByMe', 'ReviseByMe', 'LockedByMe', 'MyDocs'
1104     * @param SeedDMS_Core_User $param1 user
1105     * @param bool|integer|string $param2 if set to true
1106     * 'ReviewByMe', 'ApproveByMe', 'AppRevByMe', 'ReviseByMe', 'ReceiptByMe'
1107     * will also return documents which the reviewer, approver, etc.
1108     * has already taken care of. If set to false only
1109     * untouched documents will be returned. In case of 'ExpiredOwner' this
1110     * parameter contains the number of days (a negative number is allowed)
1111     * relativ to the current date or a date in format 'yyyy-mm-dd'
1112     * (even in the past).
1113     * @param string $param3 sort list by this field
1114     * @param string $param4 order direction
1115     * @param bool $param5 set to false if expired documents shall not be considered
1116     * @return array|bool
1117     */
1118    public function getDocumentList($listtype, $param1 = null, $param2 = false, $param3 = '', $param4 = '', $param5 = true) { /* {{{ */
1119        /* The following query will get all documents and lots of additional
1120         * information. It requires the two temporary tables ttcontentid and
1121         * ttstatid.
1122         */
1123        if (!$this->db->createTemporaryTable("ttstatid") || !$this->db->createTemporaryTable("ttcontentid")) {
1124            return false;
1125        }
1126        /* The following statement retrieves the status of the last version of all
1127         * documents. It must be restricted by further where clauses.
1128         */
1129/*
1130        $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser`, ".
1131            "`tblDocumentContent`.`version`, `tblDocumentStatus`.*, `tblDocumentStatusLog`.`status`, ".
1132            "`tblDocumentStatusLog`.`comment` AS `statusComment`, `tblDocumentStatusLog`.`date` as `statusDate`, ".
1133            "`tblDocumentStatusLog`.`userID`, `oTbl`.`fullName` AS `ownerName`, `sTbl`.`fullName` AS `statusName` ".
1134            "FROM `tblDocumentContent` ".
1135            "LEFT JOIN `tblDocuments` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` ".
1136            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID` = `tblDocumentContent`.`document` ".
1137            "LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatusLog`.`statusID` = `tblDocumentStatus`.`statusID` ".
1138            "LEFT JOIN `ttstatid` ON `ttstatid`.`maxLogID` = `tblDocumentStatusLog`.`statusLogID` ".
1139            "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentStatus`.`version` AND `ttcontentid`.`document` = `tblDocumentStatus`.`documentID` ".
1140            "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
1141            "LEFT JOIN `tblUsers` AS `oTbl` on `oTbl`.`id` = `tblDocuments`.`owner` ".
1142            "LEFT JOIN `tblUsers` AS `sTbl` on `sTbl`.`id` = `tblDocumentStatusLog`.`userID` ".
1143            "WHERE `ttstatid`.`maxLogID`=`tblDocumentStatusLog`.`statusLogID` ".
1144            "AND `ttcontentid`.`maxVersion` = `tblDocumentContent`.`version` ";
1145 */
1146        /* New sql statement which retrieves all documents, its latest version and
1147         * status, the owner and user initiating the latest status.
1148         * It doesn't need the where clause anymore. Hence the statement could be
1149         * extended with further left joins.
1150         */
1151        $selectStr = "`tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser`, ".
1152            "`tblDocumentContent`.`version`, `tblDocumentStatus`.*, `tblDocumentStatusLog`.`status`, ".
1153            "`tblDocumentStatusLog`.`comment` AS `statusComment`, `tblDocumentStatusLog`.`date` as `statusDate`, ".
1154            "`tblDocumentStatusLog`.`userID`, `oTbl`.`fullName` AS `ownerName`, `sTbl`.`fullName` AS `statusName` ";
1155        $queryStr =
1156            "FROM `ttcontentid` ".
1157            "LEFT JOIN `tblDocuments` ON `tblDocuments`.`id` = `ttcontentid`.`document` ".
1158            "LEFT JOIN `tblDocumentContent` ON `tblDocumentContent`.`document` = `ttcontentid`.`document` AND `tblDocumentContent`.`version` = `ttcontentid`.`maxVersion` ".
1159            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID`=`ttcontentid`.`document` AND `tblDocumentStatus`.`version`=`ttcontentid`.`maxVersion` ".
1160            "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
1161            "LEFT JOIN `tblDocumentStatusLog` ON `ttstatid`.`statusID` = `tblDocumentStatusLog`.`statusID` AND `ttstatid`.`maxLogID` = `tblDocumentStatusLog`.`statusLogID` ".
1162            "LEFT JOIN `tblDocumentLocks` ON `ttcontentid`.`document`=`tblDocumentLocks`.`document` ".
1163            "LEFT JOIN `tblUsers` `oTbl` ON `oTbl`.`id` = `tblDocuments`.`owner` ".
1164            "LEFT JOIN `tblUsers` `sTbl` ON `sTbl`.`id` = `tblDocumentStatusLog`.`userID` ";
1165
1166//        echo $queryStr;
1167
1168        switch ($listtype) {
1169        case 'AppRevByMe': // Documents I have to review/approve {{{
1170            $queryStr .= "WHERE 1=1 ";
1171
1172            $user = $param1;
1173            // Get document list for the current user.
1174            $reviewStatus = $user->getReviewStatus();
1175            $approvalStatus = $user->getApprovalStatus();
1176
1177            // Create a comma separated list of all the documentIDs whose information is
1178            // required.
1179            // Take only those documents into account which hasn't be touched by the user
1180            $dList = array();
1181            foreach ($reviewStatus["indstatus"] as $st) {
1182                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1183                    $dList[] = $st["documentID"];
1184                }
1185            }
1186            foreach ($reviewStatus["grpstatus"] as $st) {
1187                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1188                    $dList[] = $st["documentID"];
1189                }
1190            }
1191            foreach ($approvalStatus["indstatus"] as $st) {
1192                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1193                    $dList[] = $st["documentID"];
1194                }
1195            }
1196            foreach ($approvalStatus["grpstatus"] as $st) {
1197                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1198                    $dList[] = $st["documentID"];
1199                }
1200            }
1201            $docCSV = "";
1202            foreach ($dList as $d) {
1203                $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'";
1204            }
1205
1206            if (strlen($docCSV)>0) {
1207                $docstatarr = array(S_DRAFT_REV, S_DRAFT_APP);
1208                if ($param5)
1209                    $docstatarr[] = S_EXPIRED;
1210                $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ".
1211                            "AND `tblDocuments`.`id` IN (" . $docCSV . ") ".
1212                            "ORDER BY `statusDate` DESC";
1213            } else {
1214                $queryStr = '';
1215            }
1216            break; // }}}
1217        case 'ReviewByMe': // Documents I have to review {{{
1218            if (!$this->db->createTemporaryTable("ttreviewid")) {
1219                return false;
1220            }
1221            $user = $param1;
1222            $orderby = $param3;
1223            if ($param4 == 'desc')
1224                $orderdir = 'DESC';
1225            else
1226                $orderdir = 'ASC';
1227
1228            $groups = array();
1229            if ($user) {
1230                $tmp = $user->getGroups();
1231                foreach ($tmp as $group)
1232                    $groups[] = $group->getID();
1233            }
1234
1235            $selectStr .= ", `tblDocumentReviewLog`.`date` as `duedate` ";
1236            $queryStr .=
1237                "LEFT JOIN `tblDocumentReviewers` ON `ttcontentid`.`document`=`tblDocumentReviewers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentReviewers`.`version` ".
1238                "LEFT JOIN `ttreviewid` ON `ttreviewid`.`reviewID` = `tblDocumentReviewers`.`reviewID` ".
1239                "LEFT JOIN `tblDocumentReviewLog` ON `tblDocumentReviewLog`.`reviewLogID`=`ttreviewid`.`maxLogID` ";
1240
1241            if ($user) {
1242                $queryStr .= "WHERE (`tblDocumentReviewers`.`type` = 0 AND `tblDocumentReviewers`.`required` = ".$user->getID()." ";
1243                if ($groups)
1244                    $queryStr .= "OR `tblDocumentReviewers`.`type` = 1 AND `tblDocumentReviewers`.`required` IN (".implode(',', $groups).") ";
1245                $queryStr .= ") ";
1246            }
1247            $docstatarr = array(S_DRAFT_REV);
1248            if ($param5)
1249                $docstatarr[] = S_EXPIRED;
1250            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1251            if (!$param2)
1252                $queryStr .= " AND `tblDocumentReviewLog`.`status` = 0 ";
1253            if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1254            elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1255            elseif ($orderby == 's') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`";
1256            else $queryStr .= "ORDER BY `name`";
1257            $queryStr .= " ".$orderdir;
1258            break; // }}}
1259        case 'ApproveByMe': // Documents I have to approve {{{
1260            if (!$this->db->createTemporaryTable("ttapproveid")) {
1261                return false;
1262            }
1263            $user = $param1;
1264            $orderby = $param3;
1265            if ($param4 == 'desc')
1266                $orderdir = 'DESC';
1267            else
1268                $orderdir = 'ASC';
1269
1270            $groups = array();
1271            if ($user) {
1272                $tmp = $user->getGroups();
1273                foreach ($tmp as $group)
1274                    $groups[] = $group->getID();
1275            }
1276
1277            $selectStr .= ", `tblDocumentApproveLog`.`date` as `duedate` ";
1278            $queryStr .=
1279                "LEFT JOIN `tblDocumentApprovers` ON `ttcontentid`.`document`=`tblDocumentApprovers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentApprovers`.`version` ".
1280                "LEFT JOIN `ttapproveid` ON `ttapproveid`.`approveID` = `tblDocumentApprovers`.`approveID` ".
1281                "LEFT JOIN `tblDocumentApproveLog` ON `tblDocumentApproveLog`.`approveLogID`=`ttapproveid`.`maxLogID` ";
1282
1283            if ($user) {
1284            $queryStr .= "WHERE (`tblDocumentApprovers`.`type` = 0 AND `tblDocumentApprovers`.`required` = ".$user->getID()." ";
1285            if ($groups)
1286                $queryStr .= "OR `tblDocumentApprovers`.`type` = 1 AND `tblDocumentApprovers`.`required` IN (".implode(',', $groups).")";
1287            $queryStr .= ") ";
1288            }
1289            $docstatarr = array(S_DRAFT_APP);
1290            if ($param5)
1291                $docstatarr[] = S_EXPIRED;
1292            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1293            if (!$param2)
1294                $queryStr .= " AND `tblDocumentApproveLog`.`status` = 0 ";
1295            if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1296            elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1297            elseif ($orderby == 's') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`";
1298            else $queryStr .= "ORDER BY `name`";
1299            $queryStr .= " ".$orderdir;
1300            break; // }}}
1301        case 'WorkflowByMe': // Documents I to trigger in Worklflow {{{
1302            $user = $param1;
1303            $orderby = $param3;
1304            if ($param4 == 'desc')
1305                $orderdir = 'DESC';
1306            else
1307                $orderdir = 'ASC';
1308
1309            $groups = array();
1310            if ($user) {
1311                $tmp = $user->getGroups();
1312                foreach ($tmp as $group)
1313                    $groups[] = $group->getID();
1314            }
1315            $selectStr = 'distinct '.$selectStr;
1316            $queryStr .=
1317                "LEFT JOIN `tblWorkflowDocumentContent` ON `ttcontentid`.`document`=`tblWorkflowDocumentContent`.`document` AND `ttcontentid`.`maxVersion`=`tblWorkflowDocumentContent`.`version` ".
1318                "LEFT JOIN `tblWorkflowTransitions` ON `tblWorkflowDocumentContent`.`workflow`=`tblWorkflowTransitions`.`workflow` AND `tblWorkflowDocumentContent`.`state`=`tblWorkflowTransitions`.`state` ".
1319                "LEFT JOIN `tblWorkflowTransitionUsers` ON `tblWorkflowTransitionUsers`.`transition` = `tblWorkflowTransitions`.`id` ".
1320                "LEFT JOIN `tblWorkflowTransitionGroups` ON `tblWorkflowTransitionGroups`.`transition` = `tblWorkflowTransitions`.`id` ";
1321
1322            if ($user) {
1323                $queryStr .= "WHERE (`tblWorkflowTransitionUsers`.`userid` = ".$user->getID()." ";
1324                if ($groups)
1325                    $queryStr .= "OR `tblWorkflowTransitionGroups`.`groupid` IN (".implode(',', $groups).")";
1326                $queryStr .= ") ";
1327            }
1328            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_IN_WORKFLOW." ";
1329//            echo 'SELECT '.$selectStr." ".$queryStr;
1330            if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1331            elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1332            else $queryStr .= "ORDER BY `name`";
1333            break; // }}}
1334        case 'AppRevOwner': // Documents waiting for review/approval/revision I'm owning {{{
1335            $queryStr .= "WHERE 1=1 ";
1336
1337            $user = $param1;
1338            $orderby = $param3;
1339            if ($param4 == 'desc')
1340                $orderdir = 'DESC';
1341            else
1342                $orderdir = 'ASC';
1343            /** @noinspection PhpUndefinedConstantInspection */
1344            $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1345                "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_DRAFT_APP.") ";
1346            if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1347            elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1348            elseif ($orderby == 's') $queryStr .= "ORDER BY `status`";
1349            else $queryStr .= "ORDER BY `name`";
1350            $queryStr .= " ".$orderdir;
1351//            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1352//                "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_DRAFT_APP.") ".
1353//                "ORDER BY `statusDate` DESC";
1354            break; // }}}
1355        case 'RejectOwner': // Documents that has been rejected and I'm owning {{{
1356            $queryStr .= "WHERE 1=1 ";
1357
1358            $user = $param1;
1359            $orderby = $param3;
1360            if ($param4 == 'desc')
1361                $orderdir = 'DESC';
1362            else
1363                $orderdir = 'ASC';
1364            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ";
1365            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_REJECTED.") ";
1366            //$queryStr .= "ORDER BY `statusDate` DESC";
1367            if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1368            elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1369            elseif ($orderby == 's') $queryStr .= "ORDER BY `status`";
1370            else $queryStr .= "ORDER BY `name`";
1371            $queryStr .= " ".$orderdir;
1372            break; // }}}
1373        case 'LockedByMe': // Documents locked by me {{{
1374            $queryStr .= "WHERE 1=1 ";
1375
1376            $user = $param1;
1377            $orderby = $param3;
1378            if ($param4 == 'desc')
1379                $orderdir = 'DESC';
1380            else
1381                $orderdir = 'ASC';
1382
1383            $qs = 'SELECT `document` FROM `tblDocumentLocks` WHERE `userID`='.$user->getID();
1384            $ra = $this->db->getResultArray($qs);
1385            if (is_bool($ra) && !$ra) {
1386                return false;
1387            }
1388            $docs = array();
1389            foreach ($ra as $d) {
1390                $docs[] = $d['document'];
1391            }
1392
1393            if ($docs) {
1394                $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") ";
1395                if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1396                elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1397                elseif ($orderby == 's') $queryStr .= "ORDER BY `status`";
1398                else $queryStr .= "ORDER BY `name`";
1399                $queryStr .= " ".$orderdir;
1400            } else {
1401                $queryStr = '';
1402            }
1403            break; // }}}
1404        case 'ExpiredOwner': // Documents expired and owned by me {{{
1405            if (is_int($param2)) {
1406                $ts = mktime(0, 0, 0) + $param2 * 86400;
1407            } elseif (is_string($param2)) {
1408                $tmp = explode('-', $param2, 3);
1409                if (count($tmp) != 3)
1410                    return false;
1411                if (!self::checkDate($param2, 'Y-m-d'))
1412                    return false;
1413                $ts = mktime(0, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
1414            } else
1415                $ts = mktime(0, 0, 0)-365*86400; /* Start of today - 1 year */
1416
1417            $tsnow = mktime(0, 0, 0); /* Start of today */
1418            if ($ts < $tsnow) { /* Check for docs expired in the past */
1419                $startts = $ts;
1420                $endts = $tsnow+86400; /* Use end of day */
1421            } else { /* Check for docs which will expire in the future */
1422                $startts = $tsnow;
1423                $endts = $ts+86400; /* Use end of day */
1424            }
1425
1426            $queryStr .=
1427                "WHERE `tblDocuments`.`expires` >= ".$startts." AND `tblDocuments`.`expires` <= ".$endts." ";
1428
1429            $user = $param1;
1430            $orderby = $param3;
1431            if ($param4 == 'desc')
1432                $orderdir = 'DESC';
1433            else
1434                $orderdir = 'ASC';
1435            $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."' ";
1436            if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1437            elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1438            elseif ($orderby == 's') $queryStr .= "ORDER BY `status`";
1439            else $queryStr .= "ORDER BY `name`";
1440            $queryStr .= " ".$orderdir;
1441            break; // }}}
1442        case 'WorkflowOwner': // Documents waiting for workflow trigger I'm owning {{{
1443            $queryStr .= "WHERE 1=1 ";
1444
1445            $user = $param1;
1446            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1447                "AND `tblDocumentStatusLog`.`status` IN (".S_IN_WORKFLOW.") ".
1448                "ORDER BY `statusDate` DESC";
1449            break; // }}}
1450        case 'MyDocs': // Documents owned by me {{{
1451            $queryStr .= "WHERE 1=1 ";
1452
1453            $user = $param1;
1454            $orderby = $param3;
1455            if ($param4 == 'desc')
1456                $orderdir = 'DESC';
1457            else
1458                $orderdir = 'ASC';
1459            $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."' ";
1460            if ($orderby == 'e') $queryStr .= "ORDER BY `expires`";
1461            elseif ($orderby == 'u') $queryStr .= "ORDER BY `statusDate`";
1462            elseif ($orderby == 's') $queryStr .= "ORDER BY `status`";
1463            else $queryStr .= "ORDER BY `name`";
1464            $queryStr .= " ".$orderdir;
1465            break; // }}}
1466        default: // {{{
1467            return false;
1468            break; // }}}
1469        }
1470
1471        if ($queryStr) {
1472            $resArr = $this->db->getResultArray('SELECT '.$selectStr.$queryStr);
1473            if (is_bool($resArr) && !$resArr) {
1474                return false;
1475            }
1476            /*
1477            $documents = array();
1478            foreach ($resArr as $row)
1479                $documents[] = $this->getDocument($row["id"]);
1480             */
1481        } else {
1482            return array();
1483        }
1484
1485        return $resArr;
1486    } /* }}} */
1487
1488    /**
1489     * Create a unix time stamp
1490     *
1491     * This method is much like `mktime()` but does some range checks
1492     * on the passed values.
1493     *
1494     * @param int $hour hour
1495     * @param int $min minute
1496     * @param int $sec second
1497     * @param int $year year
1498     * @param int $month month
1499     * @param int $day day
1500     * @return int|boolean unix time stamp or false if range check failed
1501     */
1502    public function makeTimeStamp($hour, $min, $sec, $year, $month, $day) { /* {{{ */
1503        $thirtyone = array (1, 3, 5, 7, 8, 10, 12);
1504        $thirty = array (4, 6, 9, 11);
1505
1506        // Very basic check that the terms are valid. Does not fail for illegal
1507        // dates such as 31 Feb.
1508        if (!is_numeric($hour) || !is_numeric($min) || !is_numeric($sec) || !is_numeric($year) || !is_numeric($month) || !is_numeric($day) || $month<1 || $month>12 || $day<1 || $day>31 || $hour<0 || $hour>23 || $min<0 || $min>59 || $sec<0 || $sec>59) {
1509            return false;
1510        }
1511        $year = (int) $year;
1512        $month = (int) $month;
1513        $day = (int) $day;
1514
1515        if (in_array($month, $thirtyone)) {
1516            $max = 31;
1517        } elseif (in_array($month, $thirty)) {
1518            $max = 30;
1519        } else {
1520            $max = (($year % 4 == 0) && ($year % 100 != 0 || $year % 400 == 0)) ? 29 : 28;
1521        }
1522
1523        // Check again if day of month is valid in the given month
1524        if ($day>$max) {
1525            return false;
1526        }
1527
1528        return mktime($hour, $min, $sec, $month, $day, $year);
1529    } /* }}} */
1530
1531    protected function getSqlForAttribute($attrdef, $attribute, $table, $field) { /* {{{ */
1532
1533        $attrdefid = $attrdef->getId();
1534        $sql = '';
1535        /* The only differenc between Document, Folder and DocumentContent is
1536         * the name of the tables. The tables for documents and folders have a
1537         * trailing 's' (tblDocuments, tblFolders), but the table for document
1538         * content doesn't have it (tblDocumentContent).
1539         * The sql statements are equal.
1540         */
1541        if($table == 'DocumentContent') {
1542            if ($valueset = $attrdef->getValueSet()) {
1543                if (is_string($attribute))
1544                    $attribute = array($attribute);
1545                foreach ($attribute as &$v)
1546                    $v = trim($this->db->qstr($v), "'");
1547                if ($attrdef->getMultipleValues()) {
1548                    $sql = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND (`tblDocumentContentAttributes`.`value` like '%".$valueset[0].implode("%' OR `tblDocumentContentAttributes`.`value` like '%".$valueset[0], $attribute)."%') AND `tblDocumentContentAttributes`.`content` = `tblDocumentContent`.`id`)";
1549                } else {
1550                    $sql = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND (`tblDocumentContentAttributes`.`value`='".(is_array($attribute) ? implode("' OR `tblDocumentContentAttributes`.`value` = '", $attribute) : $attribute)."') AND `tblDocumentContentAttributes`.content = `tblDocumentContent`.`id`)";
1551                }
1552            } else {
1553                if (in_array($attrdef->getType(), [SeedDMS_Core_AttributeDefinition::type_date, SeedDMS_Core_AttributeDefinition::type_int, SeedDMS_Core_AttributeDefinition::type_float]) && is_array($attribute)) {
1554                    $kkll = [];
1555                    if (!empty($attribute['from'])) {
1556                        if ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
1557                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS INTEGER)>=".(int) $attribute['from'];
1558                        elseif ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
1559                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS DECIMAL)>=".(float) $attribute['from'];
1560                        else
1561                            $kkll[] = "`tblDocumentContentAttributes`.`value`>=".$this->db->qstr($attribute['from']);
1562                    }
1563                    if (!empty($attribute['to'])) {
1564                        if ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
1565                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS INTEGER)<=".(int) $attribute['to'];
1566                        elseif ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
1567                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS DECIMAL)<=".(float) $attribute['to'];
1568                        else
1569                            $kkll[] = "`tblDocumentContentAttributes`.`value`<=".$this->db->qstr($attribute['to']);
1570                    }
1571                    if ($kkll)
1572                        $sql = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND ".implode(' AND ', $kkll)." AND `tblDocumentContentAttributes`.`content`=`tblDocumentContent`.`id`)";
1573                } elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_string) {
1574                    $sql = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND `tblDocumentContentAttributes`.`value` like ".$this->db->qstr("%".$attribute."%")." AND `tblDocumentContentAttributes`.`content` = `tblDocumentContent`.`id`)";
1575                } elseif (in_array($attrdef->getType(), [SeedDMS_Core_AttributeDefinition::type_user, SeedDMS_Core_AttributeDefinition::type_group, SeedDMS_Core_AttributeDefinition::type_document, SeedDMS_Core_AttributeDefinition::type_folder])) {
1576                    if ($attrdef->getMultipleValues()) {
1577                        $sql = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND ((`tblDocumentContentAttributes`.`value` like '%,".implode(",%' OR `tblDocumentContentAttributes`.`value` like '%,", $attribute).",%') OR (`tblDocumentContentAttributes`.`value` like '%,".implode("' OR `tblDocumentContentAttributes`.`value` like '%,", $attribute)."') ) AND `tblDocumentContentAttributes`.`content` = `tblDocumentContent`.`id`)";
1578                    } else {
1579            $sql = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND (`tblDocumentContentAttributes`.`value`='".(is_array($attribute) ? implode("' OR `tblDocumentContentAttributes`.`value`='", $attribute) : $attribute)."') AND `tblDocumentContentAttributes`.`content`=`tblDocumentContent`.`id`)";
1580                    }
1581                } else {
1582                    $sql = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND `tblDocumentContentAttributes`.`value`=".$this->db->qstr($attribute)." AND `tblDocumentContentAttributes`.`content` = `tblDocumentContent`.`id`)";
1583                }
1584            }
1585        } else {
1586            if ($valueset = $attrdef->getValueSet()) {
1587                if (is_string($attribute))
1588                    $attribute = array($attribute);
1589                foreach ($attribute as &$v)
1590                    $v = trim($this->db->qstr($v), "'");
1591                if ($attrdef->getMultipleValues()) {
1592                    $sql = "EXISTS (SELECT NULL FROM `tbl".$table."Attributes` WHERE `tbl".$table."Attributes`.`attrdef`=".$attrdefid." AND (`tbl".$table."Attributes`.`value` like '%".$valueset[0].implode("%' OR `tbl".$table."Attributes`.`value` like '%".$valueset[0], $attribute)."%') AND `tbl".$table."Attributes`.`".$field."`=`tbl".$table."s`.`id`)";
1593                } else {
1594                    $sql = "EXISTS (SELECT NULL FROM `tbl".$table."Attributes` WHERE `tbl".$table."Attributes`.`attrdef`=".$attrdefid." AND (`tbl".$table."Attributes`.`value`='".(is_array($attribute) ? implode("' OR `tbl".$table."Attributes`.`value` = '", $attribute) : $attribute)."') AND `tbl".$table."Attributes`.`".$field."`=`tbl".$table."s`.`id`)";
1595                }
1596            } else {
1597                if (in_array($attrdef->getType(), [SeedDMS_Core_AttributeDefinition::type_date, SeedDMS_Core_AttributeDefinition::type_int, SeedDMS_Core_AttributeDefinition::type_float]) && is_array($attribute)) {
1598                    $kkll = [];
1599                    if (!empty($attribute['from'])) {
1600                        if ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
1601                            $kkll[] = "CAST(`tbl".$table."Attributes`.`value` AS INTEGER)>=".(int) $attribute['from'];
1602                        elseif ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
1603                            $kkll[] = "CAST(`tbl".$table."Attributes`.`value` AS DECIMAL)>=".(float) $attribute['from'];
1604                        else
1605                            $kkll[] = "`tbl".$table."Attributes`.`value`>=".$this->db->qstr($attribute['from']);
1606                    }
1607                    if (!empty($attribute['to'])) {
1608                        if ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
1609                            $kkll[] = "CAST(`tbl".$table."Attributes`.`value` AS INTEGER)<=".(int) $attribute['to'];
1610                        elseif ($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
1611                            $kkll[] = "CAST(`tbl".$table."Attributes`.`value` AS DECIMAL)<=".(float) $attribute['to'];
1612                        else
1613                            $kkll[] = "`tbl".$table."Attributes`.`value`<=".$this->db->qstr($attribute['to']);
1614                    }
1615                    if ($kkll)
1616                        $sql = "EXISTS (SELECT NULL FROM `tbl".$table."Attributes` WHERE `tbl".$table."Attributes`.`attrdef`=".$attrdefid." AND ".implode(' AND ', $kkll)." AND `tbl".$table."Attributes`.`".$field."`=`tbl".$table."s`.`id`)";
1617                } elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_string) {
1618                    $sql = "EXISTS (SELECT NULL FROM `tbl".$table."Attributes` WHERE `tbl".$table."Attributes`.`attrdef`=".$attrdefid." AND `tbl".$table."Attributes`.`value` like ".$this->db->qstr("%".$attribute."%")." AND `tbl".$table."Attributes`.`".$field."`=`tbl".$table."s`.`id`)";
1619                } elseif (in_array($attrdef->getType(), [SeedDMS_Core_AttributeDefinition::type_user, SeedDMS_Core_AttributeDefinition::type_group, SeedDMS_Core_AttributeDefinition::type_document, SeedDMS_Core_AttributeDefinition::type_folder])) {
1620                    if ($attrdef->getMultipleValues()) {
1621                        $sql = "EXISTS (SELECT NULL FROM `tbl".$table."Attributes` WHERE `tbl".$table."Attributes`.`attrdef`=".$attrdefid." AND ((`tbl".$table."Attributes`.`value` like '%,".implode(",%' OR `tbl".$table."Attributes`.`value` like '%,", $attribute).",%') OR (`tbl".$table."Attributes`.`value` like '%,".implode("' OR `tbl".$table."Attributes`.`value` like '%,", $attribute)."') ) AND `tbl".$table."Attributes`.`".$field."` = `tbl".$table."s`.`id`)";
1622                    } else {
1623            $sql = "EXISTS (SELECT NULL FROM `tbl".$table."Attributes` WHERE `tbl".$table."Attributes`.`attrdef`=".$attrdefid." AND (`tbl".$table."Attributes`.`value`='".(is_array($attribute) ? implode("' OR `tbl".$table."Attributes`.`value`='", $attribute) : $attribute)."') AND `tbl".$table."Attributes`.`".$field."`=`tbl".$table."s`.`id`)";
1624                    }
1625                } else {
1626                    $sql = "EXISTS (SELECT NULL FROM `tbl".$table."Attributes` WHERE `tbl".$table."Attributes`.`attrdef`=".$attrdefid." AND `tbl".$table."Attributes`.`value`=".$this->db->qstr($attribute)." AND `tbl".$table."Attributes`.`".$field."`=`tbl".$table."s`.`id`)";
1627                }
1628            }
1629        }
1630        return $sql;
1631    } /* }}} /
1632
1633    /**
1634     * Search the database for documents
1635     *
1636     * Note: the creation date will be used to check againts the
1637     * date saved with the document
1638     * or folder. The modification date will only be used for documents. It
1639     * is checked against the creation date of the document content. This
1640     * meanÑ• that updateÑ• of a document will only result in a searchable
1641     * modification if a new version is uploaded.
1642     *
1643     * If the search is filtered by an expiration date, only documents with
1644     * an expiration date will be found. Even if just an end date is given.
1645     *
1646     * dates, integers and floats fields are treated as ranges (expecting a 'from'
1647     * and 'to' value) unless they have a value set.
1648     *
1649     * @param string $query seach query with space separated words
1650     * @param integer $limit number of items in result set
1651     * @param integer $offset index of first item in result set
1652     * @param string $logicalmode either AND or OR
1653     * @param array $searchin list of fields to search in
1654     *        1 = keywords, 2=name, 3=comment, 4=attributes, 5=id
1655     * @param SeedDMS_Core_Folder|null $startFolder search in the folder only (null for root folder)
1656     * @param SeedDMS_Core_User $owner search for documents owned by this user
1657     * @param array $status list of status
1658     * @param array $creationstartdate search for documents created after this date
1659     * @param array $creationenddate search for documents created before this date
1660     * @param array $modificationstartdate search for documents modified after this date
1661     * @param array $modificationenddate search for documents modified before this date
1662     * @param array $categories list of categories the documents must have assigned
1663     * @param array $attributes list of attributes. The key of this array is the
1664     * attribute definition id. The value of the array is the value of the
1665     * attribute. If the attribute may have multiple values it must be an array.
1666     * attributes with a range must have the elements 'from' and 'to'
1667     * @param integer $mode decide whether to search for documents/folders
1668     *        0x1 = documents only
1669     *        0x2 = folders only
1670     *        0x3 = both
1671     * @param array $expirationstartdate search for documents expiring after and on this date
1672     * @param array $expirationenddate search for documents expiring before and on this date
1673     * @return array|bool
1674     */
1675    public function search($query, $limit = 0, $offset = 0, $logicalmode = 'AND', $searchin = array(), $startFolder = null, $owner = null, $status = array(), $creationstartdate = array(), $creationenddate = array(), $modificationstartdate = array(), $modificationenddate = array(), $categories = array(), $attributes = array(), $mode = 0x3, $expirationstartdate = array(), $expirationenddate = array()) { /* {{{ */
1676        $orderby = '';
1677        $statusstartdate = array();
1678        $statusenddate = array();
1679        $mimetype = '';
1680        if (is_array($query)) {
1681            foreach (array('limit', 'offset', 'logicalmode', 'searchin', 'startFolder', 'owner', 'status', 'mimetype', 'creationstartdate', 'creationenddate', 'modificationstartdate', 'modificationenddate', 'categories', 'attributes', 'mode', 'expirationstartdate', 'expirationenddate') as $paramname)
1682                ${$paramname} = isset($query[$paramname]) ? $query[$paramname] : ${$paramname};
1683            foreach (array('orderby', 'statusstartdate', 'statusenddate') as $paramname)
1684                ${$paramname} = isset($query[$paramname]) ? $query[$paramname] : '';
1685            $query = isset($query['query']) ? $query['query'] : '';
1686        }
1687        /* Ensure $logicalmode has a valid value */
1688        if ($logicalmode != 'OR')
1689            $logicalmode = 'AND';
1690
1691        // Split the search string into constituent keywords.
1692        $tkeys = array();
1693        if (strlen($query)>0) {
1694            $tkeys = preg_split("/[\t\r\n ,]+/", $query);
1695        }
1696
1697        // if none is checkd search all
1698        if (count($searchin)==0)
1699            $searchin = array(1, 2, 3, 4, 5);
1700
1701        /*--------- Do it all over again for folders -------------*/
1702        $totalFolders = 0;
1703        if ($mode & 0x2) {
1704            $searchKey = "";
1705
1706            $classname = $this->classnames['folder'];
1707            $searchFields = $classname::getSearchFields($this, $searchin);
1708
1709            if (count($searchFields)>0) {
1710                foreach ($tkeys as $key) {
1711                    $key = trim($key);
1712                    if (strlen($key)>0) {
1713                        $searchKey = (strlen($searchKey)==0 ? "" : $searchKey." ".$logicalmode." ")."(".implode(" like ".$this->db->qstr("%".$key."%")." OR ", $searchFields)." like ".$this->db->qstr("%".$key."%").")";
1714                    }
1715                }
1716            }
1717
1718            // Check to see if the search has been restricted to a particular sub-tree in
1719            // the folder hierarchy.
1720            $searchFolder = "";
1721            if ($startFolder) {
1722                $searchFolder = "`tblFolders`.`folderList` LIKE '%:".$startFolder->getID().":%'";
1723                if ($this->checkWithinRootDir)
1724                    $searchFolder = '('.$searchFolder." AND `tblFolders`.`folderList` LIKE '%:".$this->rootFolderID.":%')";
1725            } elseif ($this->checkWithinRootDir) {
1726                $searchFolder = "`tblFolders`.`folderList` LIKE '%:".$this->rootFolderID.":%'";
1727            }
1728
1729            // Check to see if the search has been restricted to a particular
1730            // document owner.
1731            $searchOwner = "";
1732            if ($owner) {
1733                if (is_array($owner)) {
1734                    $ownerids = array();
1735                    foreach ($owner as $o)
1736                        $ownerids[] = $o->getID();
1737                    if ($ownerids)
1738                        $searchOwner = "`tblFolders`.`owner` IN (".implode(',', $ownerids).")";
1739                } else {
1740                    $searchOwner = "`tblFolders`.`owner` = '".$owner->getId()."'";
1741                }
1742            }
1743
1744            // Check to see if the search has been restricted to a particular
1745            // attribute.
1746            $searchAttributes = array();
1747            if ($attributes) {
1748                foreach ($attributes as $attrdefid => $attribute) {
1749                    if ($attribute) {
1750                        if($attrdef = $this->getAttributeDefinition($attrdefid)) {
1751                            if ($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_folder || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
1752                                if($sql = $this->getSqlForAttribute($attrdef, $attribute, 'Folder', 'folder'))
1753                                    $searchAttributes[] = $sql;
1754                            }
1755                        }
1756                    }
1757                }
1758            }
1759
1760            // Is the search restricted to documents created between two specific dates?
1761            $searchCreateDate = "";
1762            if ($creationstartdate) {
1763                if (is_numeric($creationstartdate))
1764                    $startdate = $creationstartdate;
1765                else
1766                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($creationstartdate['hour'], $creationstartdate['minute'], $creationstartdate['second'], $creationstartdate['year'], $creationstartdate["month"], $creationstartdate["day"]);
1767                if ($startdate) {
1768                    $searchCreateDate .= "`tblFolders`.`date` >= ".(int) $startdate;
1769                }
1770            }
1771            if ($creationenddate) {
1772                if (is_numeric($creationenddate))
1773                    $stopdate = $creationenddate;
1774                else
1775                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($creationenddate['hour'], $creationenddate['minute'], $creationenddate['second'], $creationenddate["year"], $creationenddate["month"], $creationenddate["day"]);
1776                if ($stopdate) {
1777                    /** @noinspection PhpUndefinedVariableInspection */
1778                    if ($startdate)
1779                        $searchCreateDate .= " AND ";
1780                    $searchCreateDate .= "`tblFolders`.`date` <= ".(int) $stopdate;
1781                }
1782            }
1783
1784            $searchQuery = "FROM ".$classname::getSearchTables()." WHERE 1=1";
1785
1786            if (strlen($searchKey)>0) {
1787                $searchQuery .= " AND (".$searchKey.")";
1788            }
1789            if (strlen($searchFolder)>0) {
1790                $searchQuery .= " AND ".$searchFolder;
1791            }
1792            if (strlen($searchOwner)>0) {
1793                $searchQuery .= " AND (".$searchOwner.")";
1794            }
1795            if (strlen($searchCreateDate)>0) {
1796                $searchQuery .= " AND (".$searchCreateDate.")";
1797            }
1798            if ($searchAttributes) {
1799                $searchQuery .= " AND (".implode(" AND ", $searchAttributes).")";
1800            }
1801
1802            /* Do not search for folders if not at least a search for a key,
1803             * an owner, or creation date is requested.
1804             */
1805            if ($searchKey || $searchOwner || $searchCreateDate || $searchAttributes) {
1806                // Count the number of rows that the search will produce.
1807                $resArr = $this->db->getResultArray("SELECT COUNT(*) AS num FROM (SELECT DISTINCT `tblFolders`.id ".$searchQuery.") a");
1808                if ($resArr && isset($resArr[0]) && is_numeric($resArr[0]["num"]) && $resArr[0]["num"]>0) {
1809                    $totalFolders = (integer)$resArr[0]["num"];
1810                }
1811
1812                // If there are no results from the count query, then there is no real need
1813                // to run the full query. TODO: re-structure code to by-pass additional
1814                // queries when no initial results are found.
1815
1816                // Only search if the offset is not beyond the number of folders
1817                if ($totalFolders > $offset) {
1818                    // Prepare the complete search query, including the LIMIT clause.
1819                    $searchQuery = "SELECT DISTINCT `tblFolders`.`id` ".$searchQuery." GROUP BY `tblFolders`.`id`";
1820
1821                    switch ($orderby) {
1822                    case 'dd':
1823                        $searchQuery .= " ORDER BY `tblFolders`.`date` DESC";
1824                        break;
1825                    case 'da':
1826                    case 'd':
1827                        $searchQuery .= " ORDER BY `tblFolders`.`date`";
1828                        break;
1829                    case 'nd':
1830                        $searchQuery .= " ORDER BY `tblFolders`.`name` DESC";
1831                        break;
1832                    case 'na':
1833                    case 'n':
1834                        $searchQuery .= " ORDER BY `tblFolders`.`name`";
1835                        break;
1836                    case 'id':
1837                        $searchQuery .= " ORDER BY `tblFolders`.`id` DESC";
1838                        break;
1839                    case 'ia':
1840                    case 'i':
1841                        $searchQuery .= " ORDER BY `tblFolders`.`id`";
1842                        break;
1843                    default:
1844                        break;
1845                    }
1846
1847                    if ($limit) {
1848                        $searchQuery .= " LIMIT ".$limit." OFFSET ".$offset;
1849                    }
1850
1851                    // Send the complete search query to the database.
1852                    $resArr = $this->db->getResultArray($searchQuery);
1853                } else {
1854                    $resArr = array();
1855                }
1856
1857                // ------------------- Ausgabe der Ergebnisse ----------------------------
1858                $numResults = count($resArr);
1859                if ($numResults == 0) {
1860                    $folderresult = array('totalFolders'=>$totalFolders, 'folders'=>array());
1861                } else {
1862                    foreach ($resArr as $folderArr) {
1863                        $folders[] = $this->getFolder($folderArr['id']);
1864                    }
1865                    /** @noinspection PhpUndefinedVariableInspection */
1866                    $folderresult = array('totalFolders'=>$totalFolders, 'folders'=>$folders);
1867                }
1868            } else {
1869                $folderresult = array('totalFolders'=>0, 'folders'=>array());
1870            }
1871        } else {
1872            $folderresult = array('totalFolders'=>0, 'folders'=>array());
1873        }
1874
1875        /*--------- Do it all over again for documents -------------*/
1876
1877        $totalDocs = 0;
1878        if ($mode & 0x1) {
1879            $searchKey = "";
1880
1881            $classname = $this->classnames['document'];
1882            $searchFields = $classname::getSearchFields($this, $searchin);
1883
1884            if (count($searchFields)>0) {
1885                foreach ($tkeys as $key) {
1886                    $key = trim($key);
1887                    if (strlen($key)>0) {
1888                        $searchKey = (strlen($searchKey)==0 ? "" : $searchKey." ".$logicalmode." ")."(".implode(" like ".$this->db->qstr("%".$key."%")." OR ", $searchFields)." like ".$this->db->qstr("%".$key."%").")";
1889                    }
1890                }
1891            }
1892
1893            // Check to see if the search has been restricted to a particular sub-tree in
1894            // the folder hierarchy.
1895            $searchFolder = "";
1896            if ($startFolder) {
1897                $searchFolder = "`tblDocuments`.`folderList` LIKE '%:".$startFolder->getID().":%'";
1898                if ($this->checkWithinRootDir)
1899                    $searchFolder = '('.$searchFolder." AND `tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%')";
1900            } elseif ($this->checkWithinRootDir) {
1901                $searchFolder = "`tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%'";
1902            }
1903
1904            // Check to see if the search has been restricted to a particular
1905            // document owner.
1906            $searchOwner = "";
1907            if ($owner) {
1908                if (is_array($owner)) {
1909                    $ownerids = array();
1910                    foreach ($owner as $o)
1911                        $ownerids[] = $o->getID();
1912                    if ($ownerids)
1913                        $searchOwner = "`tblDocuments`.`owner` IN (".implode(',', $ownerids).")";
1914                } else {
1915                    $searchOwner = "`tblDocuments`.`owner` = '".$owner->getId()."'";
1916                }
1917            }
1918
1919            // Check to see if the search has been restricted to a particular
1920            // document category.
1921            $searchCategories = "";
1922            if ($categories) {
1923                $catids = array();
1924                foreach ($categories as $category)
1925                    $catids[] = $category->getId();
1926                $searchCategories = "`tblDocumentCategory`.`categoryID` in (".implode(',', $catids).")";
1927            }
1928
1929            // Check to see if the search has been restricted to a particular
1930            // attribute.
1931            $searchAttributes = array();
1932            if ($attributes) {
1933                foreach ($attributes as $attrdefid => $attribute) {
1934                    if ($attribute) {
1935                        $lsearchAttributes = [];
1936                        if($attrdef = $this->getAttributeDefinition($attrdefid)) {
1937                            if ($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_document || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
1938                                if($sql = $this->getSqlForAttribute($attrdef, $attribute, 'Document', 'document'))
1939                                    $lsearchAttributes[] = $sql;
1940                            }
1941                            if ($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_documentcontent || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
1942                                if($sql = $this->getSqlForAttribute($attrdef, $attribute, 'DocumentContent', 'content'))
1943                                    $lsearchAttributes[] = $sql;
1944                            }
1945                        }
1946                        if ($lsearchAttributes)
1947                            $searchAttributes[] = "(".implode(" OR ", $lsearchAttributes).")";
1948                    }
1949                }
1950            }
1951
1952            // Is the search restricted to documents created between two specific dates?
1953            $searchCreateDate = "";
1954            if ($creationstartdate) {
1955                if (is_numeric($creationstartdate))
1956                    $startdate = $creationstartdate;
1957                else
1958                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($creationstartdate['hour'], $creationstartdate['minute'], $creationstartdate['second'], $creationstartdate['year'], $creationstartdate["month"], $creationstartdate["day"]);
1959                if ($startdate) {
1960                    $searchCreateDate .= "`tblDocuments`.`date` >= ".(int) $startdate;
1961                }
1962            }
1963            if ($creationenddate) {
1964                if (is_numeric($creationenddate))
1965                    $stopdate = $creationenddate;
1966                else
1967                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($creationenddate['hour'], $creationenddate['minute'], $creationenddate['second'], $creationenddate["year"], $creationenddate["month"], $creationenddate["day"]);
1968                if ($stopdate) {
1969                    if ($searchCreateDate)
1970                        $searchCreateDate .= " AND ";
1971                    $searchCreateDate .= "`tblDocuments`.`date` <= ".(int) $stopdate;
1972                }
1973            }
1974
1975            if ($modificationstartdate) {
1976                if (is_numeric($modificationstartdate))
1977                    $startdate = $modificationstartdate;
1978                else
1979                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($modificationstartdate['hour'], $modificationstartdate['minute'], $modificationstartdate['second'], $modificationstartdate['year'], $modificationstartdate["month"], $modificationstartdate["day"]);
1980                if ($startdate) {
1981                    if ($searchCreateDate)
1982                        $searchCreateDate .= " AND ";
1983                    $searchCreateDate .= "`tblDocumentContent`.`date` >= ".(int) $startdate;
1984                }
1985            }
1986            if ($modificationenddate) {
1987                if (is_numeric($modificationenddate))
1988                    $stopdate = $modificationenddate;
1989                else
1990                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($modificationenddate['hour'], $modificationenddate['minute'], $modificationenddate['second'], $modificationenddate["year"], $modificationenddate["month"], $modificationenddate["day"]);
1991                if ($stopdate) {
1992                    if ($searchCreateDate)
1993                        $searchCreateDate .= " AND ";
1994                    $searchCreateDate .= "`tblDocumentContent`.`date` <= ".(int) $stopdate;
1995                }
1996            }
1997            $searchExpirationDate = '';
1998            if ($expirationstartdate) {
1999                $startdate = SeedDMS_Core_DMS::makeTimeStamp($expirationstartdate['hour'], $expirationstartdate['minute'], $expirationstartdate['second'], $expirationstartdate['year'], $expirationstartdate["month"], $expirationstartdate["day"]);
2000                if ($startdate) {
2001                    $searchExpirationDate .= "`tblDocuments`.`expires` >= ".(int) $startdate;
2002                }
2003            }
2004            if ($expirationenddate) {
2005                $stopdate = SeedDMS_Core_DMS::makeTimeStamp($expirationenddate['hour'], $expirationenddate['minute'], $expirationenddate['second'], $expirationenddate["year"], $expirationenddate["month"], $expirationenddate["day"]);
2006                if ($stopdate) {
2007                    if ($searchExpirationDate)
2008                        $searchExpirationDate .= " AND ";
2009                    else // do not find documents without an expiration date
2010                        $searchExpirationDate .= "`tblDocuments`.`expires` != 0 AND ";
2011                    $searchExpirationDate .= "`tblDocuments`.`expires` <= ".(int) $stopdate;
2012                }
2013            }
2014            $searchStatusDate = '';
2015            if ($statusstartdate) {
2016                $startdate = $statusstartdate['year'].'-'.$statusstartdate["month"].'-'.$statusstartdate["day"].' '.$statusstartdate['hour'].':'.$statusstartdate['minute'].':'.$statusstartdate['second'];
2017                if ($startdate) {
2018                    if ($searchStatusDate)
2019                        $searchStatusDate .= " AND ";
2020                    $searchStatusDate .= "`tblDocumentStatusLog`.`date` >= ".$this->db->qstr($startdate);
2021                }
2022            }
2023            if ($statusenddate) {
2024                $stopdate = $statusenddate['year'].'-'.$statusenddate["month"].'-'.$statusenddate["day"].' '.$statusenddate['hour'].':'.$statusenddate['minute'].':'.$statusenddate['second'];
2025                if ($stopdate) {
2026                    if ($searchStatusDate)
2027                        $searchStatusDate .= " AND ";
2028                    $searchStatusDate .= "`tblDocumentStatusLog`.`date` <= ".$this->db->qstr($stopdate);
2029                }
2030            }
2031
2032            // ---------------------- Suche starten ----------------------------------
2033
2034            //
2035            // Construct the SQL query that will be used to search the database.
2036            //
2037
2038            if (!$this->db->createTemporaryTable("ttcontentid") || !$this->db->createTemporaryTable("ttstatid")) {
2039                return false;
2040            }
2041
2042            $searchQuery = "FROM `tblDocuments` ".
2043                "LEFT JOIN `tblDocumentContent` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` ".
2044                "LEFT JOIN `tblDocumentAttributes` ON `tblDocuments`.`id` = `tblDocumentAttributes`.`document` ".
2045                "LEFT JOIN `tblDocumentContentAttributes` ON `tblDocumentContent`.`id` = `tblDocumentContentAttributes`.`content` ".
2046                "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID` = `tblDocumentContent`.`document` ".
2047                "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
2048                "LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatusLog`.`statusLogID` = `ttstatid`.`maxLogID` ".
2049                "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentStatus`.`version` AND `ttcontentid`.`document` = `tblDocumentStatus`.`documentID` ".
2050                "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
2051                "LEFT JOIN `tblDocumentCategory` ON `tblDocuments`.`id`=`tblDocumentCategory`.`documentID` ".
2052                "WHERE ".
2053                // "`ttstatid`.`maxLogID`=`tblDocumentStatusLog`.`statusLogID` AND ".
2054                "`ttcontentid`.`maxVersion` = `tblDocumentContent`.`version`";
2055
2056            if (strlen($searchKey)>0) {
2057                $searchQuery .= " AND (".$searchKey.")";
2058            }
2059            if (strlen($searchFolder)>0) {
2060                $searchQuery .= " AND ".$searchFolder;
2061            }
2062            if (strlen($searchOwner)>0) {
2063                $searchQuery .= " AND (".$searchOwner.")";
2064            }
2065            if (strlen($searchCategories)>0) {
2066                $searchQuery .= " AND (".$searchCategories.")";
2067            }
2068            if (strlen($searchCreateDate)>0) {
2069                $searchQuery .= " AND (".$searchCreateDate.")";
2070            }
2071            if (strlen($searchExpirationDate)>0) {
2072                $searchQuery .= " AND (".$searchExpirationDate.")";
2073            }
2074            if (strlen($searchStatusDate)>0) {
2075                $searchQuery .= " AND (".$searchStatusDate.")";
2076            }
2077            if ($searchAttributes) {
2078                $searchQuery .= " AND (".implode(" AND ", $searchAttributes).")";
2079            }
2080
2081            // status
2082            if ($status) {
2083                $searchQuery .= " AND `tblDocumentStatusLog`.`status` IN (".implode(',', $status).")";
2084            }
2085
2086            // mime type
2087            if ($mimetype) {
2088                $searchQuery .= " AND `tblDocumentContent`.`mimeType` IN ('".implode("','", $mimetype)."')";
2089            }
2090            if ($searchKey || $searchOwner || $searchCategories || $searchCreateDate || $searchExpirationDate || $searchStatusDate || $searchAttributes || $status || $mimetype) {
2091                // Count the number of rows that the search will produce.
2092                $resArr = $this->db->getResultArray("SELECT COUNT(*) AS num FROM (SELECT DISTINCT `tblDocuments`.`id` ".$searchQuery.") a");
2093                $totalDocs = 0;
2094                if (is_numeric($resArr[0]["num"]) && $resArr[0]["num"]>0) {
2095                    $totalDocs = (integer)$resArr[0]["num"];
2096                }
2097
2098                // If there are no results from the count query, then there is no real need
2099                // to run the full query. TODO: re-structure code to by-pass additional
2100                // queries when no initial results are found.
2101
2102                // Prepare the complete search query, including the LIMIT clause.
2103                $searchQuery = "SELECT DISTINCT `tblDocuments`.*, ".
2104                    "`tblDocumentContent`.`version`, ".
2105                    "`tblDocumentStatusLog`.`status`, `tblDocumentLocks`.`userID` as `lockUser` ".$searchQuery;
2106
2107                switch ($orderby) {
2108                case 'dd':
2109                    $orderbyQuery = " ORDER BY `tblDocuments`.`date` DESC";
2110                    break;
2111                case 'da':
2112                case 'd':
2113                    $orderbyQuery = " ORDER BY `tblDocuments`.`date`";
2114                    break;
2115                case 'nd':
2116                    $orderbyQuery = " ORDER BY `tblDocuments`.`name` DESC";
2117                    break;
2118                case 'na':
2119                case 'n':
2120                    $orderbyQuery = " ORDER BY `tblDocuments`.`name`";
2121                    break;
2122                case 'id':
2123                    $orderbyQuery = " ORDER BY `tblDocuments`.`id` DESC";
2124                    break;
2125                case 'ia':
2126                case 'i':
2127                    $orderbyQuery = " ORDER BY `tblDocuments`.`id`";
2128                    break;
2129                default:
2130                    $orderbyQuery = "";
2131                    break;
2132                }
2133
2134                // calculate the remaining entrÑ—es of the current page
2135                // If page is not full yet, get remaining entries
2136                if ($limit) {
2137                    $remain = $limit - count($folderresult['folders']);
2138                    if ($remain) {
2139                        if ($remain == $limit)
2140                            $offset -= $totalFolders;
2141                        else
2142                            $offset = 0;
2143
2144                        $searchQuery .= $orderbyQuery;
2145
2146                        if ($limit)
2147                            $searchQuery .= " LIMIT ".$limit." OFFSET ".$offset;
2148
2149                        // Send the complete search query to the database.
2150                        $resArr = $this->db->getResultArray($searchQuery);
2151                        if ($resArr === false)
2152                            return false;
2153                    } else {
2154                        $resArr = array();
2155                    }
2156                } else {
2157                    $searchQuery .= $orderbyQuery;
2158
2159                    // Send the complete search query to the database.
2160                    $resArr = $this->db->getResultArray($searchQuery);
2161                    if ($resArr === false)
2162                        return false;
2163                }
2164
2165                // ------------------- Ausgabe der Ergebnisse ----------------------------
2166                $numResults = count($resArr);
2167                if ($numResults == 0) {
2168                    $docresult = array('totalDocs'=>$totalDocs, 'docs'=>array());
2169                } else {
2170                    foreach ($resArr as $docArr) {
2171                        $docs[] = $this->getDocument($docArr['id']);
2172                    }
2173                    /** @noinspection PhpUndefinedVariableInspection */
2174                    $docresult = array('totalDocs'=>$totalDocs, 'docs'=>$docs);
2175                }
2176            } else {
2177                $docresult = array('totalDocs'=>0, 'docs'=>array());
2178            }
2179        } else {
2180            $docresult = array('totalDocs'=>0, 'docs'=>array());
2181        }
2182
2183        if ($limit) {
2184            $totalPages = (integer)(($totalDocs+$totalFolders)/$limit);
2185            if ((($totalDocs+$totalFolders)%$limit) > 0) {
2186                $totalPages++;
2187            }
2188        } else {
2189            $totalPages = 1;
2190        }
2191
2192        return array_merge($docresult, $folderresult, array('totalPages'=>$totalPages));
2193    } /* }}} */
2194
2195    /**
2196     * Return a folder by its id
2197     *
2198     * This method retrieves a folder from the database by its id.
2199     *
2200     * @param integer $id internal id of folder
2201     * @return SeedDMS_Core_Folder instance of SeedDMS_Core_Folder or false
2202     */
2203    public function getFolder($id) { /* {{{ */
2204        if ($this->usecache && isset($this->cache['folders'][$id])) {
2205            return $this->cache['folders'][$id];
2206        }
2207        $classname = $this->classnames['folder'];
2208        $folder = $classname::getInstance($id, $this);
2209        if ($this->usecache)
2210            $this->cache['folders'][$id] = $folder;
2211        return $folder;
2212    } /* }}} */
2213
2214    /**
2215     * Return a folder by its name
2216     *
2217     * This method retrieves a folder from the database by its name. The
2218     * search covers the whole database. If
2219     * the parameter $folder is not null, it will search for the name
2220     * only within this parent folder. It will not be done recursively.
2221     *
2222     * @param string $name name of the folder
2223     * @param SeedDMS_Core_Folder $folder parent folder
2224     * @return SeedDMS_Core_Folder|boolean found folder or false
2225     */
2226    public function getFolderByName($name, $folder = null) { /* {{{ */
2227        $name = trim($name);
2228        $classname = $this->classnames['folder'];
2229        return $classname::getInstanceByName($name, $folder, $this);
2230    } /* }}} */
2231
2232    /**
2233     * Returns a list of folders and error message not linked in the tree
2234     *
2235     * This method checks all folders in the database.
2236     *
2237     * @return array|bool
2238     */
2239    public function checkFolders() { /* {{{ */
2240        $queryStr = "SELECT * FROM `tblFolders`";
2241        $resArr = $this->db->getResultArray($queryStr);
2242
2243        if (is_bool($resArr) && $resArr === false)
2244            return false;
2245
2246        $cache = array();
2247        foreach ($resArr as $rec) {
2248            $cache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['parent'], 'folderList'=>$rec['folderList']);
2249        }
2250        $errors = array();
2251        foreach ($cache as $id => $rec) {
2252            if (!array_key_exists($rec['parent'], $cache) && $rec['parent'] != 0) {
2253                $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Missing parent');
2254            }
2255            if (!isset($errors[$id]))    {
2256                /* Create the real folderList and compare it with the stored folderList */
2257                $parent = $rec['parent'];
2258                $fl = [];
2259                while($parent) {
2260                    array_unshift($fl, $parent);
2261                    $parent = $cache[$parent]['parent'];
2262                }
2263                if ($fl)
2264                    $flstr = ':'.implode(':', $fl).':';
2265                else
2266                    $flstr = '';
2267                if ($flstr != $rec['folderList'])
2268                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Wrong folder list '.$flstr.'!='.$rec['folderList']);
2269            }
2270            if (!isset($errors[$id]))    {
2271                /* This is the old insufficient test which will most likely not be called
2272                 * anymore, because the check for a wrong folder list will cache a folder
2273                 * list problem anyway.
2274                 */
2275                $tmparr = explode(':', $rec['folderList']);
2276                array_shift($tmparr);
2277                if (count($tmparr) != count(array_unique($tmparr))) {
2278                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Duplicate entry in folder list ('.$rec['folderList'].')');
2279                }
2280            }
2281        }
2282
2283        return $errors;
2284    } /* }}} */
2285
2286    /**
2287     * Returns a list of documents and error message not linked in the tree
2288     *
2289     * This method checks all documents in the database.
2290     *
2291     * @return array|bool
2292     */
2293    public function checkDocuments() { /* {{{ */
2294        $queryStr = "SELECT * FROM `tblFolders`";
2295        $resArr = $this->db->getResultArray($queryStr);
2296
2297        if (is_bool($resArr) && $resArr === false)
2298            return false;
2299
2300        $fcache = array();
2301        foreach ($resArr as $rec) {
2302            $fcache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['parent'], 'folderList'=>$rec['folderList']);
2303        }
2304
2305        $queryStr = "SELECT * FROM `tblDocuments`";
2306        $resArr = $this->db->getResultArray($queryStr);
2307
2308        if (is_bool($resArr) && $resArr === false)
2309            return false;
2310
2311        $dcache = array();
2312        foreach ($resArr as $rec) {
2313            $dcache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['folder'], 'folderList'=>$rec['folderList']);
2314        }
2315        $errors = array();
2316        foreach ($dcache as $id => $rec) {
2317            if (!array_key_exists($rec['parent'], $fcache) && $rec['parent'] != 0) {
2318                $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Missing parent');
2319            }
2320            if (!isset($errors[$id]))    {
2321                /* Create the real folderList and compare it with the stored folderList */
2322                $parent = $rec['parent'];
2323                $fl = [];
2324                while($parent) {
2325                    array_unshift($fl, $parent);
2326                    $parent = $fcache[$parent]['parent'];
2327                }
2328                if ($fl)
2329                    $flstr = ':'.implode(':', $fl).':';
2330                if ($flstr != $rec['folderList'])
2331                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Wrong folder list '.$flstr.'!='.$rec['folderList']);
2332            }
2333            if (!isset($errors[$id]))    {
2334                $tmparr = explode(':', $rec['folderList']);
2335                array_shift($tmparr);
2336                if (count($tmparr) != count(array_unique($tmparr))) {
2337                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Duplicate entry in folder list ('.$rec['folderList'].'');
2338                }
2339            }
2340        }
2341
2342        return $errors;
2343    } /* }}} */
2344
2345    /**
2346     * Return a user by its id
2347     *
2348     * This method retrieves a user from the database by its id.
2349     *
2350     * @param integer $id internal id of user
2351     * @return SeedDMS_Core_User|boolean instance of {@see SeedDMS_Core_User} or false
2352     */
2353    public function getUser($id) { /* {{{ */
2354        if ($this->usecache && isset($this->cache['users'][$id])) {
2355            return $this->cache['users'][$id];
2356        }
2357        $classname = $this->classnames['user'];
2358        $user = $classname::getInstance($id, $this);
2359        if ($this->usecache)
2360            $this->cache['users'][$id] = $user;
2361        return $user;
2362    } /* }}} */
2363
2364    /**
2365     * Return a user by its login
2366     *
2367     * This method retrieves a user from the database by its login.
2368     * If the second optional parameter $email is not empty, the user must
2369     * also have the given email.
2370     *
2371     * @param string $login internal login of user
2372     * @param string $email email of user
2373     * @return object instance of {@see SeedDMS_Core_User} or false
2374     */
2375    public function getUserByLogin($login, $email = '') { /* {{{ */
2376        $classname = $this->classnames['user'];
2377        return $classname::getInstance($login, $this, 'name', $email);
2378    } /* }}} */
2379
2380    /**
2381     * Return a user by its email
2382     *
2383     * This method retrieves a user from the database by its email.
2384     * It is needed when the user requests a new password.
2385     *
2386     * @param integer $email email address of user
2387     * @return object instance of {@see SeedDMS_Core_User} or false in case of an error
2388     */
2389    public function getUserByEmail($email) { /* {{{ */
2390        $classname = $this->classnames['user'];
2391        return $classname::getInstance($email, $this, 'email');
2392    } /* }}} */
2393
2394    /**
2395     * Return list of all users
2396     *
2397     * @param string $orderby
2398     * @return array list of instances of {@see SeedDMS_Core_User} or false in case of an error
2399     */
2400    public function getAllUsers($orderby = '') { /* {{{ */
2401        $classname = $this->classnames['user'];
2402        return $classname::getAllInstances($orderby, $this);
2403    } /* }}} */
2404
2405    /**
2406     * Add a new user
2407     *
2408     * This method calls the hook `onPostAddUser` after the user has been
2409     * added successfully.
2410     *
2411     * @param string $login login name
2412     * @param string $pwd hashed password of new user
2413     * @param string $fullName full name of user
2414     * @param string $email Email of new user
2415     * @param string $language language of new user
2416     * @param string $theme theme
2417     * @param string $comment comment of new user
2418     * @param int|string $role role of new user (can be 0=normal, 1=admin, 2=guest)
2419     * @param integer $isHidden hide user in all lists, if this is set login
2420     *        is still allowed
2421     * @param integer $isDisabled disable user and prevent login
2422     * @param string $pwdexpiration
2423     * @param int $quota
2424     * @param null $homefolder
2425     * @return bool|SeedDMS_Core_User or false if the user already exists or in case of an error
2426     */
2427    public function addUser($login, $pwd, $fullName, $email, $language, $theme, $comment, $role = '0', $isHidden = 0, $isDisabled = 0, $pwdexpiration = '', $quota = 0, $homefolder = null) { /* {{{ */
2428        $db = $this->db;
2429        if (is_object($this->getUserByLogin($login))) {
2430            return false;
2431        }
2432        if ($role == '')
2433            $role = '0';
2434        if (trim($pwdexpiration) == '' || trim($pwdexpiration) == 'never') {
2435            $pwdexpiration = 'NULL';
2436        } elseif (trim($pwdexpiration) == 'now') {
2437            $pwdexpiration = $db->qstr(date('Y-m-d H:i:s'));
2438        } else {
2439            $pwdexpiration = $db->qstr($pwdexpiration);
2440        }
2441        $queryStr = "INSERT INTO `tblUsers` (`login`, `pwd`, `fullName`, `email`, `language`, `theme`, `comment`, `role`, `hidden`, `disabled`, `pwdExpiration`, `quota`, `homefolder`) VALUES (".$db->qstr($login).", ".$db->qstr($pwd).", ".$db->qstr($fullName).", ".$db->qstr($email).", '".$language."', '".$theme."', ".$db->qstr($comment).", '".intval($role)."', '".intval($isHidden)."', '".intval($isDisabled)."', ".$pwdexpiration.", '".intval($quota)."', ".($homefolder ? intval($homefolder) : "NULL").")";
2442        $res = $this->db->getResult($queryStr);
2443        if (!$res)
2444            return false;
2445
2446        $user = $this->getUser($this->db->getInsertID('tblUsers'));
2447
2448        /* Check if 'onPostAddUser' callback is set */
2449        if (isset($this->callbacks['onPostAddUser'])) {
2450            foreach ($this->callbacks['onPostAddUser'] as $callback) {
2451                /** @noinspection PhpStatementHasEmptyBodyInspection */
2452                if (!call_user_func($callback[0], $callback[1], $user)) {
2453                }
2454            }
2455        }
2456
2457        return $user;
2458    } /* }}} */
2459
2460    /**
2461     * Get a group by its id
2462     *
2463     * @param integer $id id of group
2464     * @return SeedDMS_Core_Group|boolean group or false if no group was found
2465     */
2466    public function getGroup($id) { /* {{{ */
2467        if ($this->usecache && isset($this->cache['groups'][$id])) {
2468            return $this->cache['groups'][$id];
2469        }
2470        $classname = $this->classnames['group'];
2471        $group = $classname::getInstance($id, $this, '');
2472        if ($this->usecache)
2473            $this->cache['groups'][$id] = $group;
2474        return $group;
2475    } /* }}} */
2476
2477    /**
2478     * Get a group by its name
2479     *
2480     * @param string $name name of group
2481     * @return SeedDMS_Core_Group|boolean group or false if no group was found
2482     */
2483    public function getGroupByName($name) { /* {{{ */
2484        $name = trim($name);
2485        $classname = $this->classnames['group'];
2486        return $classname::getInstance($name, $this, 'name');
2487    } /* }}} */
2488
2489    /**
2490     * Get a list of all groups
2491     *
2492     * @return SeedDMS_Core_Group[] array of instances of {@see SeedDMS_Core_Group}
2493     */
2494    public function getAllGroups() { /* {{{ */
2495        $classname = $this->classnames['group'];
2496        return $classname::getAllInstances('name', $this);
2497    } /* }}} */
2498
2499    /**
2500     * Create a new user group
2501     *
2502     * @param string $name name of group
2503     * @param string $comment comment of group
2504     * @return SeedDMS_Core_Group|boolean instance of {@see SeedDMS_Core_Group} or false in
2505     *         case of an error.
2506     */
2507    public function addGroup($name, $comment) { /* {{{ */
2508        $name = trim($name);
2509        if (is_object($this->getGroupByName($name))) {
2510            return false;
2511        }
2512
2513        $queryStr = "INSERT INTO `tblGroups` (`name`, `comment`) VALUES (".$this->db->qstr($name).", ".$this->db->qstr($comment).")";
2514        if (!$this->db->getResult($queryStr))
2515            return false;
2516
2517        $group = $this->getGroup($this->db->getInsertID('tblGroups'));
2518
2519        /* Check if 'onPostAddGroup' callback is set */
2520        if (isset($this->callbacks['onPostAddGroup'])) {
2521            foreach ($this->callbacks['onPostAddGroup'] as $callback) {
2522                /** @noinspection PhpStatementHasEmptyBodyInspection */
2523                if (!call_user_func($callback[0], $callback[1], $group)) {
2524                }
2525            }
2526        }
2527
2528        return $group;
2529    } /* }}} */
2530
2531    public function getKeywordCategory($id) { /* {{{ */
2532        if (!is_numeric($id) || $id < 1)
2533            return false;
2534
2535        $queryStr = "SELECT * FROM `tblKeywordCategories` WHERE `id` = " . (int) $id;
2536        $resArr = $this->db->getResultArray($queryStr);
2537        if (is_bool($resArr) && !$resArr)
2538            return false;
2539        if (count($resArr) != 1)
2540            return null;
2541
2542        $resArr = $resArr[0];
2543        $cat = new SeedDMS_Core_KeywordCategory($resArr["id"], $resArr["owner"], $resArr["name"]);
2544        $cat->setDMS($this);
2545        return $cat;
2546    } /* }}} */
2547
2548    public function getKeywordCategoryByName($name, $userID) { /* {{{ */
2549        if (!is_numeric($userID) || $userID < 1)
2550            return false;
2551        $name = trim($name);
2552        $queryStr = "SELECT * FROM `tblKeywordCategories` WHERE `name` = " . $this->db->qstr($name) . " AND `owner` = " . (int) $userID;
2553        $resArr = $this->db->getResultArray($queryStr);
2554        if (is_bool($resArr) && !$resArr)
2555            return false;
2556        if (count($resArr) != 1)
2557            return null;
2558
2559        $resArr = $resArr[0];
2560        $cat = new SeedDMS_Core_KeywordCategory($resArr["id"], $resArr["owner"], $resArr["name"]);
2561        $cat->setDMS($this);
2562        return $cat;
2563    } /* }}} */
2564
2565    public function getAllKeywordCategories($userIDs = array()) { /* {{{ */
2566        $queryStr = "SELECT * FROM `tblKeywordCategories`";
2567        /* Ensure $userIDs() will only contain integers > 0 */
2568        $userIDs = array_filter(array_unique(array_map('intval', $userIDs)), function($a) {return $a > 0;});
2569        if ($userIDs) {
2570            $queryStr .= " WHERE `owner` IN (".implode(',', $userIDs).")";
2571        }
2572
2573        $resArr = $this->db->getResultArray($queryStr);
2574        if (is_bool($resArr) && !$resArr)
2575            return false;
2576
2577        $categories = array();
2578        foreach ($resArr as $row) {
2579            $cat = new SeedDMS_Core_KeywordCategory($row["id"], $row["owner"], $row["name"]);
2580            $cat->setDMS($this);
2581            array_push($categories, $cat);
2582        }
2583
2584        return $categories;
2585    } /* }}} */
2586
2587    /**
2588     * This method should be replaced by getAllKeywordCategories()
2589     *
2590     * @param $userID
2591     * @return SeedDMS_Core_KeywordCategory[]|bool
2592     */
2593    public function getAllUserKeywordCategories($userID) { /* {{{ */
2594        if (!is_numeric($userID) || $userID < 1)
2595            return false;
2596        return self::getAllKeywordCategories([$userID]);
2597    } /* }}} */
2598
2599    public function addKeywordCategory($userID, $name) { /* {{{ */
2600        if (!is_numeric($userID) || $userID < 1)
2601            return false;
2602        $name = trim($name);
2603        if (!$name)
2604            return false;
2605        if (is_object($this->getKeywordCategoryByName($name, $userID))) {
2606            return false;
2607        }
2608        $queryStr = "INSERT INTO `tblKeywordCategories` (`owner`, `name`) VALUES (".(int) $userID.", ".$this->db->qstr($name).")";
2609        if (!$this->db->getResult($queryStr))
2610            return false;
2611
2612        $category = $this->getKeywordCategory($this->db->getInsertID('tblKeywordCategories'));
2613
2614        /* Check if 'onPostAddKeywordCategory' callback is set */
2615        if (isset($this->callbacks['onPostAddKeywordCategory'])) {
2616            foreach ($this->callbacks['onPostAddKeywordCategory'] as $callback) {
2617                /** @noinspection PhpStatementHasEmptyBodyInspection */
2618                if (!call_user_func($callback[0], $callback[1], $category)) {
2619                }
2620            }
2621        }
2622
2623        return $category;
2624    } /* }}} */
2625
2626    public function getDocumentCategory($id) { /* {{{ */
2627        if (!is_numeric($id) || $id < 1)
2628            return false;
2629
2630        $queryStr = "SELECT * FROM `tblCategory` WHERE `id` = " . (int) $id;
2631        $resArr = $this->db->getResultArray($queryStr);
2632        if (is_bool($resArr) && !$resArr)
2633            return false;
2634        if (count($resArr) != 1)
2635            return null;
2636
2637        $resArr = $resArr[0];
2638        $cat = new SeedDMS_Core_DocumentCategory($resArr["id"], $resArr["name"]);
2639        $cat->setDMS($this);
2640        return $cat;
2641    } /* }}} */
2642
2643    public function getDocumentCategories() { /* {{{ */
2644        $queryStr = "SELECT * FROM `tblCategory` order by `name`";
2645
2646        $resArr = $this->db->getResultArray($queryStr);
2647        if (is_bool($resArr) && !$resArr)
2648            return false;
2649
2650        $categories = array();
2651        foreach ($resArr as $row) {
2652            $cat = new SeedDMS_Core_DocumentCategory($row["id"], $row["name"]);
2653            $cat->setDMS($this);
2654            array_push($categories, $cat);
2655        }
2656
2657        return $categories;
2658    } /* }}} */
2659
2660    /**
2661     * Get a category by its name
2662     *
2663     * The name of a category is by default unique.
2664     *
2665     * @param string $name human readable name of category
2666     * @return SeedDMS_Core_DocumentCategory|boolean instance of {@see SeedDMS_Core_DocumentCategory}
2667     */
2668    public function getDocumentCategoryByName($name) { /* {{{ */
2669        $name = trim($name);
2670        if (!$name) return false;
2671
2672        $queryStr = "SELECT * FROM `tblCategory` WHERE `name`=".$this->db->qstr($name);
2673        $resArr = $this->db->getResultArray($queryStr);
2674        if (!$resArr)
2675            return false;
2676
2677        $row = $resArr[0];
2678        $cat = new SeedDMS_Core_DocumentCategory($row["id"], $row["name"]);
2679        $cat->setDMS($this);
2680
2681        return $cat;
2682    } /* }}} */
2683
2684    /**
2685     * Add a new document category
2686     *
2687     * This method calls the hook `onPostAddDocumentCategory` if the new
2688     * category was added successfully.
2689     *
2690     * @param string $name name of category
2691     * @return SeedDMS_Core_DocumentCategory|boolean instance of {@see SeedDMS_Core_DocumentCategory} or false if the category already exists or in case of an error.
2692     */
2693    public function addDocumentCategory($name) { /* {{{ */
2694        $name = trim($name);
2695        if (!$name)
2696            return false;
2697        if (is_object($this->getDocumentCategoryByName($name))) {
2698            return false;
2699        }
2700        $queryStr = "INSERT INTO `tblCategory` (`name`) VALUES (".$this->db->qstr($name).")";
2701        if (!$this->db->getResult($queryStr))
2702            return false;
2703
2704        $category = $this->getDocumentCategory($this->db->getInsertID('tblCategory'));
2705
2706        /* Check if 'onPostAddDocumentCategory' callback is set */
2707        if (isset($this->callbacks['onPostAddDocumentCategory'])) {
2708            foreach ($this->callbacks['onPostAddDocumentCategory'] as $callback) {
2709                /** @noinspection PhpStatementHasEmptyBodyInspection */
2710                if (!call_user_func($callback[0], $callback[1], $category)) {
2711                }
2712            }
2713        }
2714
2715        return $category;
2716    } /* }}} */
2717
2718    /**
2719     * Get all notifications for a group
2720     *
2721     * deprecated: User {@see SeedDMS_Core_Group::getNotifications()}
2722     *
2723     * @param object $group group for which notifications are to be retrieved
2724     * @param integer $type type of item (T_DOCUMENT or T_FOLDER)
2725     * @return array array of notifications
2726     */
2727    public function getNotificationsByGroup($group, $type = 0) { /* {{{ */
2728        return $group->getNotifications($type);
2729    } /* }}} */
2730
2731    /**
2732     * Get all notifications for a user
2733     *
2734     * deprecated: User {@see SeedDMS_Core_User::getNotifications()}
2735     *
2736     * @param object $user user for which notifications are to be retrieved
2737     * @param integer $type type of item (T_DOCUMENT or T_FOLDER)
2738     * @return array array of notifications
2739     */
2740    public function getNotificationsByUser($user, $type = 0) { /* {{{ */
2741        return $user->getNotifications($type);
2742    } /* }}} */
2743
2744    /**
2745     * Create a token to request a new password.
2746     *
2747     * This method will not delete the password but just creates an entry
2748     * in `tblUserRequestPassword` indicating a password request.
2749     *
2750     * @param SeedDMS_Core_User $user
2751     * @return string|boolean hash value of false in case of an error
2752     */
2753    public function createPasswordRequest($user) { /* {{{ */
2754        $lenght = 32;
2755        if (function_exists("random_bytes")) {
2756            $bytes = random_bytes((int) ceil($lenght / 2));
2757        } elseif (function_exists("openssl_random_pseudo_bytes")) {
2758            $bytes = openssl_random_pseudo_bytes(ceil($lenght / 2));
2759        } else {
2760            return false;
2761        }
2762        $hash = bin2hex($bytes);
2763        $queryStr = "INSERT INTO `tblUserPasswordRequest` (`userID`, `hash`, `date`) VALUES (" . $user->getId() . ", " . $this->db->qstr($hash) .", ".$this->db->getCurrentDatetime().")";
2764        $resArr = $this->db->getResult($queryStr);
2765        if (is_bool($resArr) && !$resArr) return false;
2766        return $hash;
2767    } /* }}} */
2768
2769    /**
2770     * Check if hash for a password request is valid.
2771     *
2772     * This method searches a previously created password request and
2773     * returns the user.
2774     *
2775     * @param string $hash
2776     * @return bool|SeedDMS_Core_User
2777     */
2778    public function checkPasswordRequest($hash) { /* {{{ */
2779        /* Get the password request from the database */
2780        $queryStr = "SELECT * FROM `tblUserPasswordRequest` WHERE `hash`=".$this->db->qstr($hash);
2781        $resArr = $this->db->getResultArray($queryStr);
2782        if (is_bool($resArr) && !$resArr)
2783            return false;
2784
2785        if (count($resArr) != 1)
2786            return false;
2787        $resArr = $resArr[0];
2788
2789        return $this->getUser($resArr['userID']);
2790
2791    } /* }}} */
2792
2793    /**
2794     * Delete a password request
2795     *
2796     * @param string $hash
2797     * @return bool
2798     */
2799    public function deletePasswordRequest($hash) { /* {{{ */
2800        /* Delete the request, so nobody can use it a second time */
2801        $queryStr = "DELETE FROM `tblUserPasswordRequest` WHERE `hash`=".$this->db->qstr($hash);
2802        if (!$this->db->getResult($queryStr))
2803            return false;
2804        return true;
2805    } /* }}} */
2806
2807    /**
2808     * Return a attribute definition by its id
2809     *
2810     * This method retrieves a attribute definitionr from the database by
2811     * its id.
2812     *
2813     * @param integer $id internal id of attribute defintion
2814     * @return bool|SeedDMS_Core_AttributeDefinition or false
2815     */
2816    public function getAttributeDefinition($id) { /* {{{ */
2817        if (!is_numeric($id) || $id < 1)
2818            return false;
2819
2820        $queryStr = "SELECT * FROM `tblAttributeDefinitions` WHERE `id` = " . (int) $id;
2821        $resArr = $this->db->getResultArray($queryStr);
2822
2823        if (is_bool($resArr) && $resArr == false)
2824            return false;
2825        if (count($resArr) != 1)
2826            return null;
2827
2828        $resArr = $resArr[0];
2829
2830        $attrdef = new SeedDMS_Core_AttributeDefinition($resArr["id"], $resArr["name"], (int) $resArr["objtype"], (int) $resArr["type"], $resArr["multiple"], $resArr["minvalues"], $resArr["maxvalues"], $resArr["valueset"], $resArr["regex"]);
2831        $attrdef->setDMS($this);
2832        return $attrdef;
2833    } /* }}} */
2834
2835    /**
2836     * Return a attribute definition by its name
2837     *
2838     * This method retrieves an attribute def. from the database by its name.
2839     *
2840     * @param string $name internal name of attribute def.
2841     * @return SeedDMS_Core_AttributeDefinition|boolean instance of {@see SeedDMS_Core_AttributeDefinition} or false
2842     */
2843    public function getAttributeDefinitionByName($name) { /* {{{ */
2844        $name = trim($name);
2845        if (!$name) return false;
2846
2847        $queryStr = "SELECT * FROM `tblAttributeDefinitions` WHERE `name` = " . $this->db->qstr($name);
2848        $resArr = $this->db->getResultArray($queryStr);
2849
2850        if (is_bool($resArr) && $resArr == false)
2851            return false;
2852        if (count($resArr) != 1)
2853            return null;
2854
2855        $resArr = $resArr[0];
2856
2857        $attrdef = new SeedDMS_Core_AttributeDefinition($resArr["id"], $resArr["name"], (int) $resArr["objtype"], (int) $resArr["type"], $resArr["multiple"], $resArr["minvalues"], $resArr["maxvalues"], $resArr["valueset"], $resArr["regex"]);
2858        $attrdef->setDMS($this);
2859        return $attrdef;
2860    } /* }}} */
2861
2862    /**
2863     * Return list of all attribute definitions
2864     *
2865     * @param integer|array $objtype select those attribute definitions defined for an object type
2866     * @param integer|array $type select those attribute definitions defined for a type
2867     * @return bool|SeedDMS_Core_AttributeDefinition[] of instances of {@see SeedDMS_Core_AttributeDefinition} or false
2868     * or false
2869     */
2870    public function getAllAttributeDefinitions($objtype = 0, $type = 0) { /* {{{ */
2871        $queryStr = "SELECT * FROM `tblAttributeDefinitions`";
2872        if ($objtype || $type) {
2873            $queryStr .= ' WHERE ';
2874            if ($objtype) {
2875                if (is_array($objtype))
2876                    $queryStr .= '`objtype` in (\''.implode("','", $objtype).'\')';
2877                else
2878                    $queryStr .= '`objtype`='.intval($objtype);
2879            }
2880            if ($objtype && $type) {
2881                $queryStr .= ' AND ';
2882            }
2883            if ($type) {
2884                if (is_array($type))
2885                    $queryStr .= '`type` in (\''.implode("','", $type).'\')';
2886                else
2887                    $queryStr .= '`type`='.intval($type);
2888            }
2889        }
2890        $queryStr .= ' ORDER BY `name`';
2891        $resArr = $this->db->getResultArray($queryStr);
2892
2893        if (is_bool($resArr) && $resArr == false)
2894            return false;
2895
2896        /** @var SeedDMS_Core_AttributeDefinition[] $attrdefs */
2897        $attrdefs = array();
2898
2899        for ($i = 0; $i < count($resArr); $i++) {
2900            $attrdef = new SeedDMS_Core_AttributeDefinition($resArr[$i]["id"], $resArr[$i]["name"], (int) $resArr[$i]["objtype"], (int) $resArr[$i]["type"], $resArr[$i]["multiple"], $resArr[$i]["minvalues"], $resArr[$i]["maxvalues"], $resArr[$i]["valueset"], $resArr[$i]["regex"]);
2901            $attrdef->setDMS($this);
2902            $attrdefs[$i] = $attrdef;
2903        }
2904
2905        return $attrdefs;
2906    } /* }}} */
2907
2908    /**
2909     * Add a new attribute definition
2910     *
2911     * @param string $name name of attribute
2912     * @param $objtype
2913     * @param string $type type of attribute
2914     * @param bool|int $multiple set to 1 if attribute has multiple attributes
2915     * @param integer $minvalues minimum number of values
2916     * @param integer $maxvalues maximum number of values if multiple is set
2917     * @param string $valueset list of allowed values (csv format)
2918     * @param string $regex
2919     * @return bool|SeedDMS_Core_User
2920     */
2921    public function addAttributeDefinition($name, $objtype, $type, $multiple = 0, $minvalues = 0, $maxvalues = 1, $valueset = '', $regex = '') { /* {{{ */
2922        $name = trim($name);
2923        if (!$name)
2924            return false;
2925        if (is_object($this->getAttributeDefinitionByName($name))) {
2926            return false;
2927        }
2928        if ($objtype < SeedDMS_Core_AttributeDefinition::objtype_all || $objtype > SeedDMS_Core_AttributeDefinition::objtype_documentcontent)
2929            return false;
2930        if (!$type)
2931            return false;
2932        if (trim($valueset)) {
2933            $valuesetarr = array_map('trim', explode($valueset[0], substr($valueset, 1)));
2934            $valueset = $valueset[0].implode($valueset[0], $valuesetarr);
2935        } else {
2936            $valueset = '';
2937        }
2938        $queryStr = "INSERT INTO `tblAttributeDefinitions` (`name`, `objtype`, `type`, `multiple`, `minvalues`, `maxvalues`, `valueset`, `regex`) VALUES (".$this->db->qstr($name).", ".intval($objtype).", ".intval($type).", ".intval($multiple).", ".intval($minvalues).", ".intval($maxvalues).", ".$this->db->qstr($valueset).", ".$this->db->qstr($regex).")";
2939        $res = $this->db->getResult($queryStr);
2940        if (!$res)
2941            return false;
2942
2943        return $this->getAttributeDefinition($this->db->getInsertID('tblAttributeDefinitions'));
2944    } /* }}} */
2945
2946    /**
2947     * Return list of all workflows
2948     *
2949     * @return SeedDMS_Core_Workflow[]|bool of instances of {@see SeedDMS_Core_Workflow} or false
2950     */
2951    public function getAllWorkflows() { /* {{{ */
2952        $queryStr = "SELECT * FROM `tblWorkflows` ORDER BY `name`";
2953        $resArr = $this->db->getResultArray($queryStr);
2954
2955        if (is_bool($resArr) && $resArr == false)
2956            return false;
2957
2958        $queryStr = "SELECT * FROM `tblWorkflowStates` ORDER BY `name`";
2959        $ressArr = $this->db->getResultArray($queryStr);
2960
2961        if (is_bool($ressArr) && $ressArr == false)
2962            return false;
2963
2964        for ($i = 0; $i < count($ressArr); $i++) {
2965            $wkfstates[$ressArr[$i]["id"]] = new SeedDMS_Core_Workflow_State($ressArr[$i]["id"], $ressArr[$i]["name"], $ressArr[$i]["maxtime"], $ressArr[$i]["precondfunc"], $ressArr[$i]["documentstatus"]);
2966        }
2967
2968        /** @var SeedDMS_Core_Workflow[] $workflows */
2969        $workflows = array();
2970        for ($i = 0; $i < count($resArr); $i++) {
2971            /** @noinspection PhpUndefinedVariableInspection */
2972            $workflow = new SeedDMS_Core_Workflow($resArr[$i]["id"], $resArr[$i]["name"], $wkfstates[$resArr[$i]["initstate"]]);
2973            $workflow->setDMS($this);
2974            $workflows[$i] = $workflow;
2975        }
2976
2977        return $workflows;
2978    } /* }}} */
2979
2980    /**
2981     * Return workflow by its Id
2982     *
2983     * @param integer $id internal id of workflow
2984     * @return SeedDMS_Core_Workflow|bool of instances of {@see SeedDMS_Core_Workflow}, null if no workflow was found or false
2985     */
2986    public function getWorkflow($id) { /* {{{ */
2987        if (!is_numeric($id) || $id < 1)
2988            return false;
2989
2990        $queryStr = "SELECT * FROM `tblWorkflows` WHERE `id`=".intval($id);
2991        $resArr = $this->db->getResultArray($queryStr);
2992
2993        if (is_bool($resArr) && $resArr == false)
2994            return false;
2995
2996        if (!$resArr)
2997            return null;
2998
2999        $initstate = $this->getWorkflowState($resArr[0]['initstate']);
3000
3001        $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate);
3002        $workflow->setDMS($this);
3003
3004        return $workflow;
3005    } /* }}} */
3006
3007    /**
3008     * Return workflow by its name
3009     *
3010     * @param string $name name of workflow
3011     * @return SeedDMS_Core_Workflow|bool of instances of {@see SeedDMS_Core_Workflow} or null if no workflow was found or false
3012     */
3013    public function getWorkflowByName($name) { /* {{{ */
3014        $name = trim($name);
3015        if (!$name) return false;
3016
3017        $queryStr = "SELECT * FROM `tblWorkflows` WHERE `name`=".$this->db->qstr($name);
3018        $resArr = $this->db->getResultArray($queryStr);
3019
3020        if (is_bool($resArr) && $resArr == false)
3021            return false;
3022
3023        if (!$resArr)
3024            return null;
3025
3026        $initstate = $this->getWorkflowState($resArr[0]['initstate']);
3027
3028        $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate);
3029        $workflow->setDMS($this);
3030
3031        return $workflow;
3032    } /* }}} */
3033
3034    /**
3035     * Add a new workflow
3036     *
3037     * @param string $name name of workflow
3038     * @param SeedDMS_Core_Workflow_State $initstate initial state of workflow
3039     * @return bool|SeedDMS_Core_Workflow
3040     */
3041    public function addWorkflow($name, $initstate) { /* {{{ */
3042        $db = $this->db;
3043        $name = trim($name);
3044        if (!$name)
3045            return false;
3046        if (is_object($this->getWorkflowByName($name))) {
3047            return false;
3048        }
3049        $queryStr = "INSERT INTO `tblWorkflows` (`name`, `initstate`) VALUES (".$db->qstr($name).", ".$initstate->getID().")";
3050        $res = $db->getResult($queryStr);
3051        if (!$res)
3052            return false;
3053
3054        return $this->getWorkflow($db->getInsertID('tblWorkflows'));
3055    } /* }}} */
3056
3057    /**
3058     * Return a workflow state by its id
3059     *
3060     * This method retrieves a workflow state from the database by its id.
3061     *
3062     * @param integer $id internal id of workflow state
3063     * @return bool|SeedDMS_Core_Workflow_State or false
3064     */
3065    public function getWorkflowState($id) { /* {{{ */
3066        if (!is_numeric($id) || $id < 1)
3067            return false;
3068
3069        $queryStr = "SELECT * FROM `tblWorkflowStates` WHERE `id` = " . (int) $id;
3070        $resArr = $this->db->getResultArray($queryStr);
3071
3072        if (is_bool($resArr) && $resArr == false)
3073            return false;
3074
3075        if (count($resArr) != 1)
3076             return null;
3077
3078        $resArr = $resArr[0];
3079
3080        $state = new SeedDMS_Core_Workflow_State($resArr["id"], $resArr["name"], $resArr["maxtime"], $resArr["precondfunc"], $resArr["documentstatus"]);
3081        $state->setDMS($this);
3082        return $state;
3083    } /* }}} */
3084
3085    /**
3086     * Return workflow state by its name
3087     *
3088     * @param string $name name of workflow state
3089     * @return bool|SeedDMS_Core_Workflow_State or false
3090     */
3091    public function getWorkflowStateByName($name) { /* {{{ */
3092        $name = trim($name);
3093        if (!$name) return false;
3094
3095        $queryStr = "SELECT * FROM `tblWorkflowStates` WHERE `name`=".$this->db->qstr($name);
3096        $resArr = $this->db->getResultArray($queryStr);
3097
3098        if (is_bool($resArr) && $resArr == false)
3099            return false;
3100
3101        if (!$resArr)
3102            return null;
3103
3104        $resArr = $resArr[0];
3105
3106        $state = new SeedDMS_Core_Workflow_State($resArr["id"], $resArr["name"], $resArr["maxtime"], $resArr["precondfunc"], $resArr["documentstatus"]);
3107        $state->setDMS($this);
3108
3109        return $state;
3110    } /* }}} */
3111
3112    /**
3113     * Return list of all workflow states
3114     *
3115     * @return SeedDMS_Core_Workflow_State[]|bool of instances of {@see SeedDMS_Core_Workflow_State} or false
3116     */
3117    public function getAllWorkflowStates() { /* {{{ */
3118        $queryStr = "SELECT * FROM `tblWorkflowStates` ORDER BY `name`";
3119        $ressArr = $this->db->getResultArray($queryStr);
3120
3121        if (is_bool($ressArr) && $ressArr == false)
3122            return false;
3123
3124        $wkfstates = array();
3125        for ($i = 0; $i < count($ressArr); $i++) {
3126            $wkfstate = new SeedDMS_Core_Workflow_State($ressArr[$i]["id"], $ressArr[$i]["name"], $ressArr[$i]["maxtime"], $ressArr[$i]["precondfunc"], $ressArr[$i]["documentstatus"]);
3127            $wkfstate->setDMS($this);
3128            $wkfstates[$i] = $wkfstate;
3129        }
3130
3131        return $wkfstates;
3132    } /* }}} */
3133
3134    /**
3135     * Add new workflow state
3136     *
3137     * @param string $name name of workflow state
3138     * @param integer $docstatus document status when this state is reached
3139     * @return bool|SeedDMS_Core_Workflow_State
3140     */
3141    public function addWorkflowState($name, $docstatus) { /* {{{ */
3142        $db = $this->db;
3143        $name = trim($name);
3144        if (!$name)
3145            return false;
3146        if (is_object($this->getWorkflowStateByName($name))) {
3147            return false;
3148        }
3149        $queryStr = "INSERT INTO `tblWorkflowStates` (`name`, `documentstatus`) VALUES (".$db->qstr($name).", ".(int) $docstatus.")";
3150        $res = $db->getResult($queryStr);
3151        if (!$res)
3152            return false;
3153
3154        return $this->getWorkflowState($db->getInsertID('tblWorkflowStates'));
3155    } /* }}} */
3156
3157    /**
3158     * Return a workflow action by its id
3159     *
3160     * This method retrieves a workflow action from the database by its id.
3161     *
3162     * @param integer $id internal id of workflow action
3163     * @return SeedDMS_Core_Workflow_Action|bool instance of {@see SeedDMS_Core_Workflow_Action} or false
3164     */
3165    public function getWorkflowAction($id) { /* {{{ */
3166        if (!is_numeric($id) || $id < 1)
3167            return false;
3168
3169        $queryStr = "SELECT * FROM `tblWorkflowActions` WHERE `id` = " . (int) $id;
3170        $resArr = $this->db->getResultArray($queryStr);
3171
3172        if (is_bool($resArr) && $resArr == false)
3173            return false;
3174
3175        if (count($resArr) != 1)
3176             return null;
3177
3178        $resArr = $resArr[0];
3179
3180        $action = new SeedDMS_Core_Workflow_Action($resArr["id"], $resArr["name"]);
3181        $action->setDMS($this);
3182        return $action;
3183    } /* }}} */
3184
3185    /**
3186     * Return a workflow action by its name
3187     *
3188     * This method retrieves a workflow action from the database by its name.
3189     *
3190     * @param string $name name of workflow action
3191     * @return SeedDMS_Core_Workflow_Action|bool instance of {@see SeedDMS_Core_Workflow_Action} or false
3192     */
3193    public function getWorkflowActionByName($name) { /* {{{ */
3194        $name = trim($name);
3195        if (!$name) return false;
3196
3197        $queryStr = "SELECT * FROM `tblWorkflowActions` WHERE `name` = " . $this->db->qstr($name);
3198        $resArr = $this->db->getResultArray($queryStr);
3199
3200        if (is_bool($resArr) && $resArr == false)
3201            return false;
3202
3203        if (count($resArr) != 1)
3204             return null;
3205
3206        $resArr = $resArr[0];
3207
3208        $action = new SeedDMS_Core_Workflow_Action($resArr["id"], $resArr["name"]);
3209        $action->setDMS($this);
3210        return $action;
3211    } /* }}} */
3212
3213    /**
3214     * Return list of workflow action
3215     *
3216     * @return SeedDMS_Core_Workflow_Action[]|bool list of instances of {@see SeedDMS_Core_Workflow_Action} or false
3217     */
3218    public function getAllWorkflowActions() { /* {{{ */
3219        $queryStr = "SELECT * FROM `tblWorkflowActions`";
3220        $resArr = $this->db->getResultArray($queryStr);
3221
3222        if (is_bool($resArr) && $resArr == false)
3223            return false;
3224
3225        /** @var SeedDMS_Core_Workflow_Action[] $wkfactions */
3226        $wkfactions = array();
3227        for ($i = 0; $i < count($resArr); $i++) {
3228            $action = new SeedDMS_Core_Workflow_Action($resArr[$i]["id"], $resArr[$i]["name"]);
3229            $action->setDMS($this);
3230            $wkfactions[$i] = $action;
3231        }
3232
3233        return $wkfactions;
3234    } /* }}} */
3235
3236    /**
3237     * Add new workflow action
3238     *
3239     * @param string $name name of workflow action
3240     * @return SeedDMS_Core_Workflow_Action|bool
3241     */
3242    public function addWorkflowAction($name) { /* {{{ */
3243        $db = $this->db;
3244        $name = trim($name);
3245        if (!$name)
3246            return false;
3247        if (is_object($this->getWorkflowActionByName($name))) {
3248            return false;
3249        }
3250        $queryStr = "INSERT INTO `tblWorkflowActions` (`name`) VALUES (".$db->qstr($name).")";
3251        $res = $db->getResult($queryStr);
3252        if (!$res)
3253            return false;
3254
3255        return $this->getWorkflowAction($db->getInsertID('tblWorkflowActions'));
3256    } /* }}} */
3257
3258    /**
3259     * Return a workflow transition by its id
3260     *
3261     * This method retrieves a workflow transition from the database by its id.
3262     *
3263     * @param integer $id internal id of workflow transition
3264     * @return SeedDMS_Core_Workflow_Transition|bool instance of {@see SeedDMS_Core_Workflow_Transition} or false
3265     */
3266    public function getWorkflowTransition($id) { /* {{{ */
3267        if (!is_numeric($id))
3268            return false;
3269
3270        $queryStr = "SELECT * FROM `tblWorkflowTransitions` WHERE `id` = " . (int) $id;
3271        $resArr = $this->db->getResultArray($queryStr);
3272
3273        if (is_bool($resArr) && $resArr == false) return false;
3274        if (count($resArr) != 1) return false;
3275
3276        $resArr = $resArr[0];
3277
3278        $transition = new SeedDMS_Core_Workflow_Transition($resArr["id"], $this->getWorkflow($resArr["workflow"]), $this->getWorkflowState($resArr["state"]), $this->getWorkflowAction($resArr["action"]), $this->getWorkflowState($resArr["nextstate"]), $resArr["maxtime"]);
3279        $transition->setDMS($this);
3280        return $transition;
3281    } /* }}} */
3282
3283    /**
3284     * Returns document content which is not linked to a document
3285     *
3286     * This method is for finding straying document content without
3287     * a parent document. In normal operation this should not happen
3288     * but little checks for database consistency and possible errors
3289     * in the application may have left over document content though
3290     * the document is gone already.
3291     *
3292     * @return array|bool
3293     */
3294    public function getUnlinkedDocumentContent() { /* {{{ */
3295        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` NOT IN (SELECT id FROM `tblDocuments`)";
3296        $resArr = $this->db->getResultArray($queryStr);
3297        if ($resArr === false)
3298            return false;
3299
3300        $versions = array();
3301        foreach ($resArr as $row) {
3302            /** @var SeedDMS_Core_Document $document */
3303            $document = new $this->classnames['document']($row['document'], '', '', '', '', '', '', '', '', '', '', '');
3304            $document->setDMS($this);
3305            $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
3306            $versions[] = $version;
3307        }
3308        return $versions;
3309
3310    } /* }}} */
3311
3312    /**
3313     * Returns document content which has no file size set
3314     *
3315     * This method is for finding document content without a file size
3316     * set in the database. The file size of a document content was introduced
3317     * in version 4.0.0 of SeedDMS for implementation of user quotas.
3318     *
3319     * @return SeedDMS_Core_Document[]|bool
3320     */
3321    public function getNoFileSizeDocumentContent() { /* {{{ */
3322        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `fileSize` = 0 OR `fileSize` is null";
3323        $resArr = $this->db->getResultArray($queryStr);
3324        if ($resArr === false)
3325            return false;
3326
3327        /** @var SeedDMS_Core_Document[] $versions */
3328        $versions = array();
3329        foreach ($resArr as $row) {
3330            $document = $this->getDocument($row['document']);
3331            /* getting the document can fail if it is outside the root folder
3332             * and checkWithinRootDir is enabled.
3333             */
3334            if ($document) {
3335                $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum'], $row['fileSize'], $row['checksum']);
3336                $versions[] = $version;
3337            }
3338        }
3339        return $versions;
3340
3341    } /* }}} */
3342
3343    /**
3344     * Returns document content which has no checksum set
3345     *
3346     * This method is for finding document content without a checksum
3347     * set in the database. The checksum of a document content was introduced
3348     * in version 4.0.0 of SeedDMS for finding duplicates.
3349     * @return bool|SeedDMS_Core_Document[]
3350     */
3351    public function getNoChecksumDocumentContent() { /* {{{ */
3352        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `checksum` = '' OR `checksum` is null";
3353        $resArr = $this->db->getResultArray($queryStr);
3354        if ($resArr === false)
3355            return false;
3356
3357        /** @var SeedDMS_Core_Document[] $versions */
3358        $versions = array();
3359        foreach ($resArr as $row) {
3360            $document = $this->getDocument($row['document']);
3361            /* getting the document can fail if it is outside the root folder
3362             * and checkWithinRootDir is enabled.
3363             */
3364            if ($document) {
3365                $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
3366                $versions[] = $version;
3367            }
3368        }
3369        return $versions;
3370
3371    } /* }}} */
3372
3373    /**
3374     * Returns document content which is duplicated
3375     *
3376     * This method is for finding document content which is available twice
3377     * in the database. The checksum of a document content was introduced
3378     * in version 4.0.0 of SeedDMS for finding duplicates.
3379     * @return array|bool
3380     */
3381    public function getDuplicateDocumentContent() { /* {{{ */
3382        $queryStr = "SELECT a.*, b.`id` as dupid FROM `tblDocumentContent` a LEFT JOIN `tblDocumentContent` b ON a.`checksum`=b.`checksum` WHERE a.`id`!=b.`id` ORDER BY a.`id` LIMIT 1000";
3383        $resArr = $this->db->getResultArray($queryStr);
3384        if ($resArr === false)
3385            return false;
3386
3387        /** @var SeedDMS_Core_Document[] $versions */
3388        $versions = array();
3389        foreach ($resArr as $row) {
3390            $document = $this->getDocument($row['document']);
3391            /* getting the document can fail if it is outside the root folder
3392             * and checkWithinRootDir is enabled.
3393             */
3394            if ($document) {
3395                $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
3396                if (!isset($versions[$row['dupid']])) {
3397                    $versions[$row['id']]['content'] = $version;
3398                    $versions[$row['id']]['duplicates'] = array();
3399                } else
3400                    $versions[$row['dupid']]['duplicates'][] = $version;
3401            }
3402        }
3403        return $versions;
3404
3405    } /* }}} */
3406
3407    /**
3408     * Returns folders which contain documents with none unique sequence number
3409     *
3410     * This method is for finding folders with documents not having a
3411     * unique sequence number. Those documents cannot propperly be sorted
3412     * by sequence and changing their position is impossible if more than
3413     * two documents with the same sequence number exists, e.g.
3414     * doc 1: 3
3415     * doc 2: 5
3416     * doc 3: 5
3417     * doc 4: 5
3418     * doc 5: 7
3419     * If document 4 was to be moved between doc 1 and 2 it get sequence
3420     * number 4 ((5+3)/2).
3421     * But if document 4 was to be moved between doc 2 and 3 it will again
3422     * have sequence number 5.
3423     *
3424     * @return array|bool
3425     */
3426    public function getDuplicateSequenceNo() { /* {{{ */
3427        $queryStr = "SELECT DISTINCT `folder` FROM (SELECT `folder`, `sequence` FROM `tblDocuments` GROUP BY `folder`, `sequence` HAVING count(*) > 1) a";
3428        $resArr = $this->db->getResultArray($queryStr);
3429        if ($resArr === false)
3430            return false;
3431
3432        $folders = array();
3433        foreach ($resArr as $row) {
3434            $folder = $this->getFolder($row['folder']);
3435            if ($folder)
3436                $folders[] = $folder;
3437        }
3438        return $folders;
3439
3440    } /* }}} */
3441
3442    /**
3443     * Returns documents which have link to themselves
3444     *
3445     * @return array|bool
3446     */
3447    public function getLinksToItself() { /* {{{ */
3448        $queryStr = "SELECT * FROM `tblDocumentLinks` WHERE `document`=`target`";
3449        $resArr = $this->db->getResultArray($queryStr);
3450        if ($resArr === false)
3451            return false;
3452
3453        $documents = array();
3454        foreach ($resArr as $row) {
3455            $document = $this->getDocument($row['document']);
3456            if ($document)
3457                $documents[] = $document;
3458        }
3459        return $documents;
3460
3461    } /* }}} */
3462
3463    /**
3464     * Returns a list of reviews, approvals, receipts, revisions which are not
3465     * linked to a user, group anymore
3466     *
3467     * This method is for finding reviews or approvals whose user
3468     * or group  was deleted and not just removed from the process.
3469     *
3470     * @param string $process
3471     * @param string $usergroup
3472     * @return array
3473     */
3474    public function getProcessWithoutUserGroup($process, $usergroup) { /* {{{ */
3475        switch ($process) {
3476        case 'review':
3477            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentReviewers`";
3478            break;
3479        case 'approval':
3480            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentApprovers`";
3481            break;
3482        }
3483        /** @noinspection PhpUndefinedVariableInspection */
3484        $queryStr .= " a LEFT JOIN `tblDocuments` b ON a.`documentID`=b.`id` WHERE";
3485        switch ($usergroup) {
3486        case 'user':
3487            $queryStr .= " a.`type`=0 and a.`required` not in (SELECT `id` FROM `tblUsers`) ORDER BY b.`id`";
3488            break;
3489        case 'group':
3490            $queryStr .= " a.`type`=1 and a.`required` not in (SELECT `id` FROM `tblGroups`) ORDER BY b.`id`";
3491            break;
3492        }
3493        return $this->db->getResultArray($queryStr);
3494    } /* }}} */
3495
3496    /**
3497     * Removes all reviews, approvals which are not linked
3498     * to a user, group anymore
3499     *
3500     * This method is for removing all reviews or approvals whose user
3501     * or group  was deleted and not just removed from the process.
3502     * If the optional parameter $id is set, only this user/group id is removed.
3503     * @param string $process
3504     * @param string $usergroup
3505     * @param int $id
3506     * @return array
3507     */
3508    public function removeProcessWithoutUserGroup($process, $usergroup, $id = 0) { /* {{{ */
3509        /* Entries of tblDocumentReviewLog or tblDocumentApproveLog are deleted
3510         * because of CASCADE ON
3511         */
3512        switch ($process) {
3513        case 'review':
3514            $queryStr = "DELETE FROM tblDocumentReviewers";
3515            break;
3516        case 'approval':
3517            $queryStr = "DELETE FROM tblDocumentApprovers";
3518            break;
3519        }
3520        /** @noinspection PhpUndefinedVariableInspection */
3521        $queryStr .= " WHERE";
3522        switch ($usergroup) {
3523        case 'user':
3524            $queryStr .= " type=0 AND";
3525            if ($id)
3526                $queryStr .= " required=".((int) $id)." AND";
3527            $queryStr .= " required NOT IN (SELECT id FROM tblUsers)";
3528            break;
3529        case 'group':
3530            $queryStr .= " type=1 AND";
3531            if ($id)
3532                $queryStr .= " required=".((int) $id)." AND";
3533            $queryStr .= " required NOT IN (SELECT id FROM tblGroups)";
3534            break;
3535        }
3536        return $this->db->getResultArray($queryStr);
3537    } /* }}} */
3538
3539    /**
3540     * Returns statitical information
3541     *
3542     * This method returns all kind of statistical information like
3543     * documents or used space per user, recent activity, etc.
3544     *
3545     * @param string $type type of statistic
3546     * @return array|bool returns false if the sql statement fails, returns an empty
3547     * array if no documents or folder where found, otherwise returns a non empty
3548     * array with statistical data
3549     */
3550    public function getStatisticalData($type = '') { /* {{{ */
3551        switch ($type) {
3552            case 'docsperuser':
3553                $queryStr = "SELECT ".$this->db->concat(array('b.`fullName`', "' ('", 'b.`login`', "')'"))." AS `key`, count(`owner`) AS total, `b`.`id` AS res FROM `tblDocuments` a LEFT JOIN `tblUsers` b ON a.`owner`=b.`id` GROUP BY `owner`, `key`";
3554                $resArr = $this->db->getResultArray($queryStr);
3555                if (is_bool($resArr) && $resArr == false)
3556                    return false;
3557
3558                return $resArr;
3559            case 'foldersperuser':
3560                $queryStr = "SELECT ".$this->db->concat(array('b.`fullName`', "' ('", 'b.`login`', "')'"))." AS `key`, count(`owner`) AS total, `b`.`id` AS res FROM `tblFolders` a LEFT JOIN `tblUsers` b ON a.`owner`=b.`id` GROUP BY `owner`, `key`";
3561                $resArr = $this->db->getResultArray($queryStr);
3562                if (is_bool($resArr) && $resArr == false)
3563                    return false;
3564
3565                return $resArr;
3566            case 'docspermimetype':
3567                $queryStr = "SELECT b.`mimeType` AS `key`, count(`mimeType`) AS total FROM `tblDocuments` a LEFT JOIN `tblDocumentContent` b ON a.`id`=b.`document` GROUP BY b.`mimeType`";
3568                $resArr = $this->db->getResultArray($queryStr);
3569                if (is_bool($resArr) && $resArr == false)
3570                    return false;
3571
3572                return $resArr;
3573            case 'docspercategory':
3574                $queryStr = "SELECT b.`name` AS `key`, count(a.`categoryID`) AS total, `b`.`id` AS res FROM `tblDocumentCategory` a LEFT JOIN `tblCategory` b ON a.`categoryID`=b.id GROUP BY a.`categoryID`, b.`name`";
3575                $resArr = $this->db->getResultArray($queryStr);
3576                if (is_bool($resArr) && $resArr == false)
3577                    return false;
3578
3579                return $resArr;
3580            case 'docsperstatus':
3581                /** @noinspection PhpUnusedLocalVariableInspection */
3582                $queryStr = "SELECT b.`status` AS `key`, count(b.`status`) AS total FROM (SELECT a.id, max(b.version), max(c.`statusLogID`) AS maxlog FROM `tblDocuments` a LEFT JOIN `tblDocumentStatus` b ON a.id=b.`documentID` LEFT JOIN `tblDocumentStatusLog` c ON b.`statusID`=c.`statusID` GROUP BY a.`id`, b.`version` ORDER BY a.`id`, b.`statusID`) a LEFT JOIN `tblDocumentStatusLog` b ON a.`maxlog`=b.`statusLogID` GROUP BY b.`status`";
3583                $queryStr = "SELECT b.`status` AS `key`, count(b.`status`) AS total, b.`status` AS `res` FROM (SELECT a.`id`, max(c.`statusLogID`) AS maxlog FROM `tblDocuments` a LEFT JOIN `tblDocumentStatus` b ON a.id=b.`documentID` LEFT JOIN `tblDocumentStatusLog` c ON b.`statusID`=c.`statusID` GROUP BY a.`id` ORDER BY a.id) a LEFT JOIN `tblDocumentStatusLog` b ON a.maxlog=b.`statusLogID` GROUP BY b.`status`";
3584                $resArr = $this->db->getResultArray($queryStr);
3585                if (is_bool($resArr) && $resArr == false)
3586                    return false;
3587
3588                return $resArr;
3589            case 'docspermonth':
3590                $queryStr = "SELECT *, count(`key`) AS total FROM (SELECT ".$this->db->getDateExtract("date", '%Y-%m')." AS `key` FROM `tblDocuments`) a GROUP BY `key` ORDER BY `key`";
3591                $resArr = $this->db->getResultArray($queryStr);
3592                if (is_bool($resArr) && $resArr == false)
3593                    return false;
3594
3595                return $resArr;
3596            case 'docsaccumulated':
3597                $queryStr = "SELECT *, count(`key`) AS total FROM (SELECT ".$this->db->getDateExtract("date")." AS `key` FROM `tblDocuments`) a GROUP BY `key` ORDER BY `key`";
3598                $resArr = $this->db->getResultArray($queryStr);
3599                if (is_bool($resArr) && $resArr == false)
3600                    return false;
3601
3602                $sum = 0;
3603                foreach ($resArr as &$res) {
3604                    $sum += $res['total'];
3605                    /* auxially variable $key is need because sqlite returns
3606                     * a key '`key`'
3607                     */
3608                    $res['key'] = mktime(12, 0, 0, (int) substr($res['key'], 5, 2), (int) substr($res['key'], 8, 2), (int) substr($res['key'], 0, 4)) * 1000;
3609                    $res['total'] = $sum;
3610                }
3611                return $resArr;
3612            case 'docstotal':
3613                $queryStr = "SELECT count(*) AS total FROM `tblDocuments`";
3614                $resArr = $this->db->getResultArray($queryStr);
3615                if (is_bool($resArr) && $resArr == false)
3616                    return false;
3617                return (int) $resArr[0]['total'];
3618            case 'folderstotal':
3619                $queryStr = "SELECT count(*) AS total FROM `tblFolders`";
3620                $resArr = $this->db->getResultArray($queryStr);
3621                if (is_bool($resArr) && $resArr == false)
3622                    return false;
3623                return (int) $resArr[0]['total'];
3624            case 'userstotal':
3625                $queryStr = "SELECT count(*) AS total FROM `tblUsers`";
3626                $resArr = $this->db->getResultArray($queryStr);
3627                if (is_bool($resArr) && $resArr == false)
3628                    return false;
3629                return (int) $resArr[0]['total'];
3630            case 'groupstotal':
3631                $queryStr = "SELECT count(*) AS total FROM `tblGroups`";
3632                $resArr = $this->db->getResultArray($queryStr);
3633                if (is_bool($resArr) && $resArr == false)
3634                    return false;
3635                return (int) $resArr[0]['total'];
3636            case 'categoriestotal':
3637                $queryStr = "SELECT count(*) AS total FROM `tblCategory`";
3638                $resArr = $this->db->getResultArray($queryStr);
3639                if (is_bool($resArr) && $resArr == false)
3640                    return false;
3641                return (int) $resArr[0]['total'];
3642            case 'sizeperuser':
3643                $queryStr = "SELECT ".$this->db->concat(array('c.`fullName`', "' ('", 'c.`login`', "')'"))." AS `key`, sum(`fileSize`) AS total, `c`.`id` AS res FROM `tblDocuments` a LEFT JOIN `tblDocumentContent` b ON a.id=b.`document` LEFT JOIN `tblUsers` c ON a.`owner`=c.`id` GROUP BY a.`owner`, `key`";
3644                $resArr = $this->db->getResultArray($queryStr);
3645                if (is_bool($resArr) && $resArr == false)
3646                    return false;
3647
3648                return $resArr;
3649            case 'sizepermonth':
3650                $queryStr = "SELECT *, sum(`fileSize`) AS total FROM (SELECT ".$this->db->getDateExtract("date", '%Y-%m')." AS `key`, `fileSize` FROM `tblDocumentContent`) a GROUP BY `key` ORDER BY `key`";
3651                $resArr = $this->db->getResultArray($queryStr);
3652                if (is_bool($resArr) && $resArr == false)
3653                    return false;
3654
3655                return $resArr;
3656            default:
3657                return array();
3658        }
3659    } /* }}} */
3660
3661    /**
3662     * Returns changes with a period of time
3663     *
3664     * This method returns a list of all changes happened in the database
3665     * within a given period of time. It currently just checks for
3666     * entries in the database tables tblDocumentContent, tblDocumentFiles,
3667     * and tblDocumentStatusLog
3668     *
3669     * @param string $startts
3670     * @param string $endts
3671     * @return array|bool
3672     * @internal param string $start start date, defaults to start of current day
3673     * @internal param string $end end date, defaults to end of start day
3674     */
3675    public function getTimeline($startts = '', $endts = '') { /* {{{ */
3676        if (!$startts)
3677            $startts = mktime(0, 0, 0);
3678        if (!$endts)
3679            $endts = $startts+86400;
3680
3681        /** @var SeedDMS_Core_Document[] $timeline */
3682        $timeline = array();
3683
3684        if (0) {
3685        $queryStr = "SELECT DISTINCT `document` FROM `tblDocumentContent` WHERE `date` > ".$startts." AND `date` < ".$endts." UNION SELECT DISTINCT `document` FROM `tblDocumentFiles` WHERE `date` > ".$startts." AND `date` < ".$endts;
3686        } else {
3687        $startdate = date('Y-m-d H:i:s', $startts);
3688        $enddate = date('Y-m-d H:i:s', $endts);
3689        $queryStr = "SELECT DISTINCT `documentID` AS `document` FROM `tblDocumentStatus` LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatus`.`statusID`=`tblDocumentStatusLog`.`statusID` WHERE `date` > ".$this->db->qstr($startdate)." AND `date` < ".$this->db->qstr($enddate)." UNION SELECT DISTINCT document FROM `tblDocumentFiles` WHERE `date` > ".$this->db->qstr($startdate)." AND `date` < ".$this->db->qstr($enddate)." UNION SELECT DISTINCT `document` FROM `tblDocumentFiles` WHERE `date` > ".$startts." AND `date` < ".$endts;
3690        }
3691        $resArr = $this->db->getResultArray($queryStr);
3692        if ($resArr === false)
3693            return false;
3694        foreach ($resArr as $rec) {
3695            $document = $this->getDocument($rec['document']);
3696            $timeline = array_merge($timeline, $document->getTimeline());
3697        }
3698        return $timeline;
3699
3700    } /* }}} */
3701
3702    /**
3703     * Returns changes with a period of time
3704     *
3705     * This method is similar to getTimeline() but returns more dedicated lists
3706     * of documents or folders which has change in various ways.
3707     *
3708     * @param string $mode
3709     * @param string $startts
3710     * @param string $endts
3711     * @return array|bool
3712     * @internal param string $start start date, defaults to start of current day
3713     * @internal param string $end end date, defaults to end of start day
3714     */
3715    public function getLatestChanges($mode, $startts = '', $endts = '') { /* {{{ */
3716        if (!$startts)
3717            $startts = mktime(0, 0, 0);
3718        if (!$endts)
3719            $endts = $startts+86400;
3720
3721        $startdate = date('Y-m-d H:i:s', $startts);
3722        $enddate = date('Y-m-d H:i:s', $endts);
3723
3724        $objects = [];
3725        switch ($mode) {
3726        case 'statuschange':
3727            /* Count entries in tblDocumentStatusLog for each tblDocumentStatus and
3728             * take only those into account with at least 2 log entries. For the
3729             * document id do a left join with tblDocumentStatus
3730             * This is similar to ttstatid + the count + the join
3731             * c > 1 is required to find only those documents with a changed status
3732             * This sql statement appears to be much to complicated.
3733             */
3734            //$queryStr = "SELECT `a`.*, `tblDocumentStatus`.`documentID` as `document` FROM (SELECT `tblDocumentStatusLog`.`statusID` AS `statusID`, MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID`, COUNT(`tblDocumentStatusLog`.`statusLogID`) AS `c`, `tblDocumentStatusLog`.`date` FROM `tblDocumentStatusLog` GROUP BY `tblDocumentStatusLog`.`statusID` HAVING `c` > 1 ORDER BY `tblDocumentStatusLog`.`date` DESC) `a` LEFT JOIN `tblDocumentStatus` ON `a`.`statusID`=`tblDocumentStatus`.`statusID` WHERE `a`.`date` > ".$this->db->qstr($startdate)." AND `a`.`date` < ".$this->db->qstr($enddate)." ";
3735            $queryStr = "SELECT DISTINCT `tblDocumentStatus`.`documentID` as    `document` FROM `tblDocumentStatusLog` LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatusLog`.`statusID` = `tblDocumentStatus`.`statusID` WHERE `tblDocumentStatusLog`.`date` > ".$this->db->qstr($startdate)." AND `tblDocumentStatusLog`.`date` < ".$this->db->qstr($enddate)." ORDER BY `tblDocumentStatusLog`.`date` DESC";
3736            $resArr = $this->db->getResultArray($queryStr);
3737            if ($resArr === false)
3738                return false;
3739            foreach ($resArr as $rec) {
3740                if ($object = $this->getDocument($rec['document']))
3741                    $objects[] = $object;
3742            }
3743            break;
3744        case 'newdocuments':
3745            $queryStr = "SELECT `id` AS `document` FROM `tblDocuments` WHERE `date` > ".$startts." AND `date` < ".$endts." ORDER BY `date` DESC";
3746            $resArr = $this->db->getResultArray($queryStr);
3747            if ($resArr === false)
3748                return false;
3749            foreach ($resArr as $rec) {
3750                if ($object = $this->getDocument($rec['document']))
3751                    $objects[] = $object;
3752            }
3753            break;
3754        case 'updateddocuments':
3755            /* DISTINCT is need if there is more than 1 update of the document in the
3756             * given period of time. Without it, the query will return the document
3757             * more than once.
3758             */
3759            $queryStr = "SELECT DISTINCT `document` AS `document` FROM `tblDocumentContent` LEFT JOIN `tblDocuments` ON `tblDocumentContent`.`document`=`tblDocuments`.`id` WHERE `tblDocumentContent`.`date` > ".$startts." AND `tblDocumentContent`.`date` < ".$endts." AND `tblDocumentContent`.`date` > `tblDocuments`.`date` ORDER BY `tblDocumentContent`.`date` DESC";
3760            $resArr = $this->db->getResultArray($queryStr);
3761            if ($resArr === false)
3762                return false;
3763            foreach ($resArr as $rec) {
3764                if ($object = $this->getDocument($rec['document']))
3765                    $objects[] = $object;
3766            }
3767            break;
3768        case 'newfolders':
3769            $queryStr = "SELECT `id` AS `folder` FROM `tblFolders` WHERE `date` > ".$startts." AND `date` < ".$endts." ORDER BY `date` DESC";
3770            $resArr = $this->db->getResultArray($queryStr);
3771            if ($resArr === false)
3772                return false;
3773            foreach ($resArr as $rec) {
3774                if ($object = $this->getFolder($rec['folder']))
3775                    $objects[] = $object;
3776            }
3777            break;
3778        }
3779        return $objects;
3780    } /* }}} */
3781
3782    public function getMimeTypes() { /* {{{ */
3783        $queryStr = "SELECT `mimeType`, COUNT(`mimeType`) AS `c` FROM `tblDocumentContent` GROUP BY `mimeType` ORDER BY `mimeType`";
3784
3785        $resArr = $this->db->getResultArray($queryStr);
3786        if (is_bool($resArr) && !$resArr)
3787            return false;
3788
3789        return $resArr;
3790    } /* }}} */
3791
3792    /**
3793     * Set a callback function
3794     *
3795     * The function passed in $func must be a callable and $name must not be empty.
3796     *
3797     * Setting a callback with this method will remove all previously
3798     * set callbacks. Use {@see SeedDMS_Core_DMS::addCallback()} to register
3799     * additional callbacks.
3800     * This method does not check if there is a callback with the given name.
3801     *
3802     * @param string $name internal name of callback
3803     * @param mixed $func function name as expected by {call_user_method}
3804     * @param mixed $params parameter passed as the first argument to the
3805     *        callback
3806     * @return bool true if adding the callback succeeds otherwise false
3807     */
3808    public function setCallback($name, $func, $params = null) { /* {{{ */
3809        if ($name && $func && is_callable($func)) {
3810            $this->callbacks[$name] = array(array($func, $params));
3811            return true;
3812        } else {
3813            return false;
3814        }
3815    } /* }}} */
3816
3817    /**
3818     * Add a callback function
3819     *
3820     * The function passed in $func must be a callable and $name must not be empty.
3821     * This method does not check if there is a callback with the given name.
3822     *
3823     * @param string $name internal name of callback
3824     * @param mixed $func function name as expected by {call_user_method}
3825     * @param mixed $params parameter passed as the first argument to the
3826     *        callback
3827     * @return bool true if adding the callback succeeds otherwise false
3828     */
3829    public function addCallback($name, $func, $params = null) { /* {{{ */
3830        if ($name && $func && is_callable($func)) {
3831            $this->callbacks[$name][] = array($func, $params);
3832            return true;
3833        } else {
3834            return false;
3835        }
3836    } /* }}} */
3837
3838    /**
3839     * Check if a callback with the given name has been set
3840     *
3841     * @param string $name internal name of callback
3842     * @return bool true if callback exists otherwise false
3843     */
3844    public function hasCallback($name) { /* {{{ */
3845        if ($name && !empty($this->callbacks[$name]))
3846            return true;
3847        return false;
3848    } /* }}} */
3849
3850}