Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.81% covered (warning)
60.81%
1046 / 1720
58.95% covered (warning)
58.95%
56 / 95
CRAP
0.00% covered (danger)
0.00%
0 / 1
SeedDMS_Core_DMS
60.52% covered (warning)
60.52%
1033 / 1707
58.95% covered (warning)
58.95%
56 / 95
38913.65
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
1585.37
 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
70.89% covered (warning)
70.89%
56 / 79
0.00% covered (danger)
0.00%
0 / 1
76.53
 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                        $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            // Is the search restricted to documents created between two specific dates?
1760            $searchCreateDate = "";
1761            if ($creationstartdate) {
1762                if (is_numeric($creationstartdate))
1763                    $startdate = $creationstartdate;
1764                else
1765                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($creationstartdate['hour'], $creationstartdate['minute'], $creationstartdate['second'], $creationstartdate['year'], $creationstartdate["month"], $creationstartdate["day"]);
1766                if ($startdate) {
1767                    $searchCreateDate .= "`tblFolders`.`date` >= ".(int) $startdate;
1768                }
1769            }
1770            if ($creationenddate) {
1771                if (is_numeric($creationenddate))
1772                    $stopdate = $creationenddate;
1773                else
1774                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($creationenddate['hour'], $creationenddate['minute'], $creationenddate['second'], $creationenddate["year"], $creationenddate["month"], $creationenddate["day"]);
1775                if ($stopdate) {
1776                    /** @noinspection PhpUndefinedVariableInspection */
1777                    if ($startdate)
1778                        $searchCreateDate .= " AND ";
1779                    $searchCreateDate .= "`tblFolders`.`date` <= ".(int) $stopdate;
1780                }
1781            }
1782
1783            $searchQuery = "FROM ".$classname::getSearchTables()." WHERE 1=1";
1784
1785            if (strlen($searchKey)>0) {
1786                $searchQuery .= " AND (".$searchKey.")";
1787            }
1788            if (strlen($searchFolder)>0) {
1789                $searchQuery .= " AND ".$searchFolder;
1790            }
1791            if (strlen($searchOwner)>0) {
1792                $searchQuery .= " AND (".$searchOwner.")";
1793            }
1794            if (strlen($searchCreateDate)>0) {
1795                $searchQuery .= " AND (".$searchCreateDate.")";
1796            }
1797            if ($searchAttributes) {
1798                $searchQuery .= " AND (".implode(" AND ", $searchAttributes).")";
1799            }
1800
1801            /* Do not search for folders if not at least a search for a key,
1802             * an owner, or creation date is requested.
1803             */
1804            if ($searchKey || $searchOwner || $searchCreateDate || $searchAttributes) {
1805                // Count the number of rows that the search will produce.
1806                $resArr = $this->db->getResultArray("SELECT COUNT(*) AS num FROM (SELECT DISTINCT `tblFolders`.id ".$searchQuery.") a");
1807                if ($resArr && isset($resArr[0]) && is_numeric($resArr[0]["num"]) && $resArr[0]["num"]>0) {
1808                    $totalFolders = (integer)$resArr[0]["num"];
1809                }
1810
1811                // If there are no results from the count query, then there is no real need
1812                // to run the full query. TODO: re-structure code to by-pass additional
1813                // queries when no initial results are found.
1814
1815                // Only search if the offset is not beyond the number of folders
1816                if ($totalFolders > $offset) {
1817                    // Prepare the complete search query, including the LIMIT clause.
1818                    $searchQuery = "SELECT DISTINCT `tblFolders`.`id` ".$searchQuery." GROUP BY `tblFolders`.`id`";
1819
1820                    switch ($orderby) {
1821                    case 'dd':
1822                        $searchQuery .= " ORDER BY `tblFolders`.`date` DESC";
1823                        break;
1824                    case 'da':
1825                    case 'd':
1826                        $searchQuery .= " ORDER BY `tblFolders`.`date`";
1827                        break;
1828                    case 'nd':
1829                        $searchQuery .= " ORDER BY `tblFolders`.`name` DESC";
1830                        break;
1831                    case 'na':
1832                    case 'n':
1833                        $searchQuery .= " ORDER BY `tblFolders`.`name`";
1834                        break;
1835                    case 'id':
1836                        $searchQuery .= " ORDER BY `tblFolders`.`id` DESC";
1837                        break;
1838                    case 'ia':
1839                    case 'i':
1840                        $searchQuery .= " ORDER BY `tblFolders`.`id`";
1841                        break;
1842                    default:
1843                        break;
1844                    }
1845
1846                    if ($limit) {
1847                        $searchQuery .= " LIMIT ".$limit." OFFSET ".$offset;
1848                    }
1849
1850                    // Send the complete search query to the database.
1851                    $resArr = $this->db->getResultArray($searchQuery);
1852                } else {
1853                    $resArr = array();
1854                }
1855
1856                // ------------------- Ausgabe der Ergebnisse ----------------------------
1857                $numResults = count($resArr);
1858                if ($numResults == 0) {
1859                    $folderresult = array('totalFolders'=>$totalFolders, 'folders'=>array());
1860                } else {
1861                    foreach ($resArr as $folderArr) {
1862                        $folders[] = $this->getFolder($folderArr['id']);
1863                    }
1864                    /** @noinspection PhpUndefinedVariableInspection */
1865                    $folderresult = array('totalFolders'=>$totalFolders, 'folders'=>$folders);
1866                }
1867            } else {
1868                $folderresult = array('totalFolders'=>0, 'folders'=>array());
1869            }
1870        } else {
1871            $folderresult = array('totalFolders'=>0, 'folders'=>array());
1872        }
1873
1874        /*--------- Do it all over again for documents -------------*/
1875
1876        $totalDocs = 0;
1877        if ($mode & 0x1) {
1878            $searchKey = "";
1879
1880            $classname = $this->classnames['document'];
1881            $searchFields = $classname::getSearchFields($this, $searchin);
1882
1883            if (count($searchFields)>0) {
1884                foreach ($tkeys as $key) {
1885                    $key = trim($key);
1886                    if (strlen($key)>0) {
1887                        $searchKey = (strlen($searchKey)==0 ? "" : $searchKey." ".$logicalmode." ")."(".implode(" like ".$this->db->qstr("%".$key."%")." OR ", $searchFields)." like ".$this->db->qstr("%".$key."%").")";
1888                    }
1889                }
1890            }
1891
1892            // Check to see if the search has been restricted to a particular sub-tree in
1893            // the folder hierarchy.
1894            $searchFolder = "";
1895            if ($startFolder) {
1896                $searchFolder = "`tblDocuments`.`folderList` LIKE '%:".$startFolder->getID().":%'";
1897                if ($this->checkWithinRootDir)
1898                    $searchFolder = '('.$searchFolder." AND `tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%')";
1899            } elseif ($this->checkWithinRootDir) {
1900                $searchFolder = "`tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%'";
1901            }
1902
1903            // Check to see if the search has been restricted to a particular
1904            // document owner.
1905            $searchOwner = "";
1906            if ($owner) {
1907                if (is_array($owner)) {
1908                    $ownerids = array();
1909                    foreach ($owner as $o)
1910                        $ownerids[] = $o->getID();
1911                    if ($ownerids)
1912                        $searchOwner = "`tblDocuments`.`owner` IN (".implode(',', $ownerids).")";
1913                } else {
1914                    $searchOwner = "`tblDocuments`.`owner` = '".$owner->getId()."'";
1915                }
1916            }
1917
1918            // Check to see if the search has been restricted to a particular
1919            // document category.
1920            $searchCategories = "";
1921            if ($categories) {
1922                $catids = array();
1923                foreach ($categories as $category)
1924                    $catids[] = $category->getId();
1925                $searchCategories = "`tblDocumentCategory`.`categoryID` in (".implode(',', $catids).")";
1926            }
1927
1928            // Check to see if the search has been restricted to a particular
1929            // attribute.
1930            $searchAttributes = array();
1931            if ($attributes) {
1932                foreach ($attributes as $attrdefid => $attribute) {
1933                    if ($attribute) {
1934                        $lsearchAttributes = [];
1935                        $attrdef = $this->getAttributeDefinition($attrdefid);
1936                        if ($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_document || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
1937                            if($sql = $this->getSqlForAttribute($attrdef, $attribute, 'Document', 'document'))
1938                                $lsearchAttributes[] = $sql;
1939                        }
1940                        if ($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_documentcontent || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
1941                            if($sql = $this->getSqlForAttribute($attrdef, $attribute, 'DocumentContent', 'content'))
1942                                $lsearchAttributes[] = $sql;
1943                        }
1944                        if ($lsearchAttributes)
1945                            $searchAttributes[] = "(".implode(" OR ", $lsearchAttributes).")";
1946                    }
1947                }
1948            }
1949
1950            // Is the search restricted to documents created between two specific dates?
1951            $searchCreateDate = "";
1952            if ($creationstartdate) {
1953                if (is_numeric($creationstartdate))
1954                    $startdate = $creationstartdate;
1955                else
1956                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($creationstartdate['hour'], $creationstartdate['minute'], $creationstartdate['second'], $creationstartdate['year'], $creationstartdate["month"], $creationstartdate["day"]);
1957                if ($startdate) {
1958                    $searchCreateDate .= "`tblDocuments`.`date` >= ".(int) $startdate;
1959                }
1960            }
1961            if ($creationenddate) {
1962                if (is_numeric($creationenddate))
1963                    $stopdate = $creationenddate;
1964                else
1965                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($creationenddate['hour'], $creationenddate['minute'], $creationenddate['second'], $creationenddate["year"], $creationenddate["month"], $creationenddate["day"]);
1966                if ($stopdate) {
1967                    if ($searchCreateDate)
1968                        $searchCreateDate .= " AND ";
1969                    $searchCreateDate .= "`tblDocuments`.`date` <= ".(int) $stopdate;
1970                }
1971            }
1972
1973            if ($modificationstartdate) {
1974                if (is_numeric($modificationstartdate))
1975                    $startdate = $modificationstartdate;
1976                else
1977                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($modificationstartdate['hour'], $modificationstartdate['minute'], $modificationstartdate['second'], $modificationstartdate['year'], $modificationstartdate["month"], $modificationstartdate["day"]);
1978                if ($startdate) {
1979                    if ($searchCreateDate)
1980                        $searchCreateDate .= " AND ";
1981                    $searchCreateDate .= "`tblDocumentContent`.`date` >= ".(int) $startdate;
1982                }
1983            }
1984            if ($modificationenddate) {
1985                if (is_numeric($modificationenddate))
1986                    $stopdate = $modificationenddate;
1987                else
1988                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($modificationenddate['hour'], $modificationenddate['minute'], $modificationenddate['second'], $modificationenddate["year"], $modificationenddate["month"], $modificationenddate["day"]);
1989                if ($stopdate) {
1990                    if ($searchCreateDate)
1991                        $searchCreateDate .= " AND ";
1992                    $searchCreateDate .= "`tblDocumentContent`.`date` <= ".(int) $stopdate;
1993                }
1994            }
1995            $searchExpirationDate = '';
1996            if ($expirationstartdate) {
1997                $startdate = SeedDMS_Core_DMS::makeTimeStamp($expirationstartdate['hour'], $expirationstartdate['minute'], $expirationstartdate['second'], $expirationstartdate['year'], $expirationstartdate["month"], $expirationstartdate["day"]);
1998                if ($startdate) {
1999                    $searchExpirationDate .= "`tblDocuments`.`expires` >= ".(int) $startdate;
2000                }
2001            }
2002            if ($expirationenddate) {
2003                $stopdate = SeedDMS_Core_DMS::makeTimeStamp($expirationenddate['hour'], $expirationenddate['minute'], $expirationenddate['second'], $expirationenddate["year"], $expirationenddate["month"], $expirationenddate["day"]);
2004                if ($stopdate) {
2005                    if ($searchExpirationDate)
2006                        $searchExpirationDate .= " AND ";
2007                    else // do not find documents without an expiration date
2008                        $searchExpirationDate .= "`tblDocuments`.`expires` != 0 AND ";
2009                    $searchExpirationDate .= "`tblDocuments`.`expires` <= ".(int) $stopdate;
2010                }
2011            }
2012            $searchStatusDate = '';
2013            if ($statusstartdate) {
2014                $startdate = $statusstartdate['year'].'-'.$statusstartdate["month"].'-'.$statusstartdate["day"].' '.$statusstartdate['hour'].':'.$statusstartdate['minute'].':'.$statusstartdate['second'];
2015                if ($startdate) {
2016                    if ($searchStatusDate)
2017                        $searchStatusDate .= " AND ";
2018                    $searchStatusDate .= "`tblDocumentStatusLog`.`date` >= ".$this->db->qstr($startdate);
2019                }
2020            }
2021            if ($statusenddate) {
2022                $stopdate = $statusenddate['year'].'-'.$statusenddate["month"].'-'.$statusenddate["day"].' '.$statusenddate['hour'].':'.$statusenddate['minute'].':'.$statusenddate['second'];
2023                if ($stopdate) {
2024                    if ($searchStatusDate)
2025                        $searchStatusDate .= " AND ";
2026                    $searchStatusDate .= "`tblDocumentStatusLog`.`date` <= ".$this->db->qstr($stopdate);
2027                }
2028            }
2029
2030            // ---------------------- Suche starten ----------------------------------
2031
2032            //
2033            // Construct the SQL query that will be used to search the database.
2034            //
2035
2036            if (!$this->db->createTemporaryTable("ttcontentid") || !$this->db->createTemporaryTable("ttstatid")) {
2037                return false;
2038            }
2039
2040            $searchQuery = "FROM `tblDocuments` ".
2041                "LEFT JOIN `tblDocumentContent` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` ".
2042                "LEFT JOIN `tblDocumentAttributes` ON `tblDocuments`.`id` = `tblDocumentAttributes`.`document` ".
2043                "LEFT JOIN `tblDocumentContentAttributes` ON `tblDocumentContent`.`id` = `tblDocumentContentAttributes`.`content` ".
2044                "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID` = `tblDocumentContent`.`document` ".
2045                "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
2046                "LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatusLog`.`statusLogID` = `ttstatid`.`maxLogID` ".
2047                "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentStatus`.`version` AND `ttcontentid`.`document` = `tblDocumentStatus`.`documentID` ".
2048                "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
2049                "LEFT JOIN `tblDocumentCategory` ON `tblDocuments`.`id`=`tblDocumentCategory`.`documentID` ".
2050                "WHERE ".
2051                // "`ttstatid`.`maxLogID`=`tblDocumentStatusLog`.`statusLogID` AND ".
2052                "`ttcontentid`.`maxVersion` = `tblDocumentContent`.`version`";
2053
2054            if (strlen($searchKey)>0) {
2055                $searchQuery .= " AND (".$searchKey.")";
2056            }
2057            if (strlen($searchFolder)>0) {
2058                $searchQuery .= " AND ".$searchFolder;
2059            }
2060            if (strlen($searchOwner)>0) {
2061                $searchQuery .= " AND (".$searchOwner.")";
2062            }
2063            if (strlen($searchCategories)>0) {
2064                $searchQuery .= " AND (".$searchCategories.")";
2065            }
2066            if (strlen($searchCreateDate)>0) {
2067                $searchQuery .= " AND (".$searchCreateDate.")";
2068            }
2069            if (strlen($searchExpirationDate)>0) {
2070                $searchQuery .= " AND (".$searchExpirationDate.")";
2071            }
2072            if (strlen($searchStatusDate)>0) {
2073                $searchQuery .= " AND (".$searchStatusDate.")";
2074            }
2075            if ($searchAttributes) {
2076                $searchQuery .= " AND (".implode(" AND ", $searchAttributes).")";
2077            }
2078
2079            // status
2080            if ($status) {
2081                $searchQuery .= " AND `tblDocumentStatusLog`.`status` IN (".implode(',', $status).")";
2082            }
2083
2084            // mime type
2085            if ($mimetype) {
2086                $searchQuery .= " AND `tblDocumentContent`.`mimeType` IN ('".implode("','", $mimetype)."')";
2087            }
2088            if ($searchKey || $searchOwner || $searchCategories || $searchCreateDate || $searchExpirationDate || $searchStatusDate || $searchAttributes || $status || $mimetype) {
2089                // Count the number of rows that the search will produce.
2090                $resArr = $this->db->getResultArray("SELECT COUNT(*) AS num FROM (SELECT DISTINCT `tblDocuments`.`id` ".$searchQuery.") a");
2091                $totalDocs = 0;
2092                if (is_numeric($resArr[0]["num"]) && $resArr[0]["num"]>0) {
2093                    $totalDocs = (integer)$resArr[0]["num"];
2094                }
2095
2096                // If there are no results from the count query, then there is no real need
2097                // to run the full query. TODO: re-structure code to by-pass additional
2098                // queries when no initial results are found.
2099
2100                // Prepare the complete search query, including the LIMIT clause.
2101                $searchQuery = "SELECT DISTINCT `tblDocuments`.*, ".
2102                    "`tblDocumentContent`.`version`, ".
2103                    "`tblDocumentStatusLog`.`status`, `tblDocumentLocks`.`userID` as `lockUser` ".$searchQuery;
2104
2105                switch ($orderby) {
2106                case 'dd':
2107                    $orderbyQuery = " ORDER BY `tblDocuments`.`date` DESC";
2108                    break;
2109                case 'da':
2110                case 'd':
2111                    $orderbyQuery = " ORDER BY `tblDocuments`.`date`";
2112                    break;
2113                case 'nd':
2114                    $orderbyQuery = " ORDER BY `tblDocuments`.`name` DESC";
2115                    break;
2116                case 'na':
2117                case 'n':
2118                    $orderbyQuery = " ORDER BY `tblDocuments`.`name`";
2119                    break;
2120                case 'id':
2121                    $orderbyQuery = " ORDER BY `tblDocuments`.`id` DESC";
2122                    break;
2123                case 'ia':
2124                case 'i':
2125                    $orderbyQuery = " ORDER BY `tblDocuments`.`id`";
2126                    break;
2127                default:
2128                    $orderbyQuery = "";
2129                    break;
2130                }
2131
2132                // calculate the remaining entrÑ—es of the current page
2133                // If page is not full yet, get remaining entries
2134                if ($limit) {
2135                    $remain = $limit - count($folderresult['folders']);
2136                    if ($remain) {
2137                        if ($remain == $limit)
2138                            $offset -= $totalFolders;
2139                        else
2140                            $offset = 0;
2141
2142                        $searchQuery .= $orderbyQuery;
2143
2144                        if ($limit)
2145                            $searchQuery .= " LIMIT ".$limit." OFFSET ".$offset;
2146
2147                        // Send the complete search query to the database.
2148                        $resArr = $this->db->getResultArray($searchQuery);
2149                        if ($resArr === false)
2150                            return false;
2151                    } else {
2152                        $resArr = array();
2153                    }
2154                } else {
2155                    $searchQuery .= $orderbyQuery;
2156
2157                    // Send the complete search query to the database.
2158                    $resArr = $this->db->getResultArray($searchQuery);
2159                    if ($resArr === false)
2160                        return false;
2161                }
2162
2163                // ------------------- Ausgabe der Ergebnisse ----------------------------
2164                $numResults = count($resArr);
2165                if ($numResults == 0) {
2166                    $docresult = array('totalDocs'=>$totalDocs, 'docs'=>array());
2167                } else {
2168                    foreach ($resArr as $docArr) {
2169                        $docs[] = $this->getDocument($docArr['id']);
2170                    }
2171                    /** @noinspection PhpUndefinedVariableInspection */
2172                    $docresult = array('totalDocs'=>$totalDocs, 'docs'=>$docs);
2173                }
2174            } else {
2175                $docresult = array('totalDocs'=>0, 'docs'=>array());
2176            }
2177        } else {
2178            $docresult = array('totalDocs'=>0, 'docs'=>array());
2179        }
2180
2181        if ($limit) {
2182            $totalPages = (integer)(($totalDocs+$totalFolders)/$limit);
2183            if ((($totalDocs+$totalFolders)%$limit) > 0) {
2184                $totalPages++;
2185            }
2186        } else {
2187            $totalPages = 1;
2188        }
2189
2190        return array_merge($docresult, $folderresult, array('totalPages'=>$totalPages));
2191    } /* }}} */
2192
2193    /**
2194     * Return a folder by its id
2195     *
2196     * This method retrieves a folder from the database by its id.
2197     *
2198     * @param integer $id internal id of folder
2199     * @return SeedDMS_Core_Folder instance of SeedDMS_Core_Folder or false
2200     */
2201    public function getFolder($id) { /* {{{ */
2202        if ($this->usecache && isset($this->cache['folders'][$id])) {
2203            return $this->cache['folders'][$id];
2204        }
2205        $classname = $this->classnames['folder'];
2206        $folder = $classname::getInstance($id, $this);
2207        if ($this->usecache)
2208            $this->cache['folders'][$id] = $folder;
2209        return $folder;
2210    } /* }}} */
2211
2212    /**
2213     * Return a folder by its name
2214     *
2215     * This method retrieves a folder from the database by its name. The
2216     * search covers the whole database. If
2217     * the parameter $folder is not null, it will search for the name
2218     * only within this parent folder. It will not be done recursively.
2219     *
2220     * @param string $name name of the folder
2221     * @param SeedDMS_Core_Folder $folder parent folder
2222     * @return SeedDMS_Core_Folder|boolean found folder or false
2223     */
2224    public function getFolderByName($name, $folder = null) { /* {{{ */
2225        $name = trim($name);
2226        $classname = $this->classnames['folder'];
2227        return $classname::getInstanceByName($name, $folder, $this);
2228    } /* }}} */
2229
2230    /**
2231     * Returns a list of folders and error message not linked in the tree
2232     *
2233     * This method checks all folders in the database.
2234     *
2235     * @return array|bool
2236     */
2237    public function checkFolders() { /* {{{ */
2238        $queryStr = "SELECT * FROM `tblFolders`";
2239        $resArr = $this->db->getResultArray($queryStr);
2240
2241        if (is_bool($resArr) && $resArr === false)
2242            return false;
2243
2244        $cache = array();
2245        foreach ($resArr as $rec) {
2246            $cache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['parent'], 'folderList'=>$rec['folderList']);
2247        }
2248        $errors = array();
2249        foreach ($cache as $id => $rec) {
2250            if (!array_key_exists($rec['parent'], $cache) && $rec['parent'] != 0) {
2251                $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Missing parent');
2252            }
2253            if (!isset($errors[$id]))    {
2254                /* Create the real folderList and compare it with the stored folderList */
2255                $parent = $rec['parent'];
2256                $fl = [];
2257                while($parent) {
2258                    array_unshift($fl, $parent);
2259                    $parent = $cache[$parent]['parent'];
2260                }
2261                if ($fl)
2262                    $flstr = ':'.implode(':', $fl).':';
2263                else
2264                    $flstr = '';
2265                if ($flstr != $rec['folderList'])
2266                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Wrong folder list '.$flstr.'!='.$rec['folderList']);
2267            }
2268            if (!isset($errors[$id]))    {
2269                /* This is the old insufficient test which will most likely not be called
2270                 * anymore, because the check for a wrong folder list will cache a folder
2271                 * list problem anyway.
2272                 */
2273                $tmparr = explode(':', $rec['folderList']);
2274                array_shift($tmparr);
2275                if (count($tmparr) != count(array_unique($tmparr))) {
2276                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Duplicate entry in folder list ('.$rec['folderList'].')');
2277                }
2278            }
2279        }
2280
2281        return $errors;
2282    } /* }}} */
2283
2284    /**
2285     * Returns a list of documents and error message not linked in the tree
2286     *
2287     * This method checks all documents in the database.
2288     *
2289     * @return array|bool
2290     */
2291    public function checkDocuments() { /* {{{ */
2292        $queryStr = "SELECT * FROM `tblFolders`";
2293        $resArr = $this->db->getResultArray($queryStr);
2294
2295        if (is_bool($resArr) && $resArr === false)
2296            return false;
2297
2298        $fcache = array();
2299        foreach ($resArr as $rec) {
2300            $fcache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['parent'], 'folderList'=>$rec['folderList']);
2301        }
2302
2303        $queryStr = "SELECT * FROM `tblDocuments`";
2304        $resArr = $this->db->getResultArray($queryStr);
2305
2306        if (is_bool($resArr) && $resArr === false)
2307            return false;
2308
2309        $dcache = array();
2310        foreach ($resArr as $rec) {
2311            $dcache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['folder'], 'folderList'=>$rec['folderList']);
2312        }
2313        $errors = array();
2314        foreach ($dcache as $id => $rec) {
2315            if (!array_key_exists($rec['parent'], $fcache) && $rec['parent'] != 0) {
2316                $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Missing parent');
2317            }
2318            if (!isset($errors[$id]))    {
2319                /* Create the real folderList and compare it with the stored folderList */
2320                $parent = $rec['parent'];
2321                $fl = [];
2322                while($parent) {
2323                    array_unshift($fl, $parent);
2324                    $parent = $fcache[$parent]['parent'];
2325                }
2326                if ($fl)
2327                    $flstr = ':'.implode(':', $fl).':';
2328                if ($flstr != $rec['folderList'])
2329                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Wrong folder list '.$flstr.'!='.$rec['folderList']);
2330            }
2331            if (!isset($errors[$id]))    {
2332                $tmparr = explode(':', $rec['folderList']);
2333                array_shift($tmparr);
2334                if (count($tmparr) != count(array_unique($tmparr))) {
2335                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Duplicate entry in folder list ('.$rec['folderList'].'');
2336                }
2337            }
2338        }
2339
2340        return $errors;
2341    } /* }}} */
2342
2343    /**
2344     * Return a user by its id
2345     *
2346     * This method retrieves a user from the database by its id.
2347     *
2348     * @param integer $id internal id of user
2349     * @return SeedDMS_Core_User|boolean instance of {@see SeedDMS_Core_User} or false
2350     */
2351    public function getUser($id) { /* {{{ */
2352        if ($this->usecache && isset($this->cache['users'][$id])) {
2353            return $this->cache['users'][$id];
2354        }
2355        $classname = $this->classnames['user'];
2356        $user = $classname::getInstance($id, $this);
2357        if ($this->usecache)
2358            $this->cache['users'][$id] = $user;
2359        return $user;
2360    } /* }}} */
2361
2362    /**
2363     * Return a user by its login
2364     *
2365     * This method retrieves a user from the database by its login.
2366     * If the second optional parameter $email is not empty, the user must
2367     * also have the given email.
2368     *
2369     * @param string $login internal login of user
2370     * @param string $email email of user
2371     * @return object instance of {@see SeedDMS_Core_User} or false
2372     */
2373    public function getUserByLogin($login, $email = '') { /* {{{ */
2374        $classname = $this->classnames['user'];
2375        return $classname::getInstance($login, $this, 'name', $email);
2376    } /* }}} */
2377
2378    /**
2379     * Return a user by its email
2380     *
2381     * This method retrieves a user from the database by its email.
2382     * It is needed when the user requests a new password.
2383     *
2384     * @param integer $email email address of user
2385     * @return object instance of {@see SeedDMS_Core_User} or false in case of an error
2386     */
2387    public function getUserByEmail($email) { /* {{{ */
2388        $classname = $this->classnames['user'];
2389        return $classname::getInstance($email, $this, 'email');
2390    } /* }}} */
2391
2392    /**
2393     * Return list of all users
2394     *
2395     * @param string $orderby
2396     * @return array list of instances of {@see SeedDMS_Core_User} or false in case of an error
2397     */
2398    public function getAllUsers($orderby = '') { /* {{{ */
2399        $classname = $this->classnames['user'];
2400        return $classname::getAllInstances($orderby, $this);
2401    } /* }}} */
2402
2403    /**
2404     * Add a new user
2405     *
2406     * This method calls the hook `onPostAddUser` after the user has been
2407     * added successfully.
2408     *
2409     * @param string $login login name
2410     * @param string $pwd hashed password of new user
2411     * @param string $fullName full name of user
2412     * @param string $email Email of new user
2413     * @param string $language language of new user
2414     * @param string $theme theme
2415     * @param string $comment comment of new user
2416     * @param int|string $role role of new user (can be 0=normal, 1=admin, 2=guest)
2417     * @param integer $isHidden hide user in all lists, if this is set login
2418     *        is still allowed
2419     * @param integer $isDisabled disable user and prevent login
2420     * @param string $pwdexpiration
2421     * @param int $quota
2422     * @param null $homefolder
2423     * @return bool|SeedDMS_Core_User or false if the user already exists or in case of an error
2424     */
2425    public function addUser($login, $pwd, $fullName, $email, $language, $theme, $comment, $role = '0', $isHidden = 0, $isDisabled = 0, $pwdexpiration = '', $quota = 0, $homefolder = null) { /* {{{ */
2426        $db = $this->db;
2427        if (is_object($this->getUserByLogin($login))) {
2428            return false;
2429        }
2430        if ($role == '')
2431            $role = '0';
2432        if (trim($pwdexpiration) == '' || trim($pwdexpiration) == 'never') {
2433            $pwdexpiration = 'NULL';
2434        } elseif (trim($pwdexpiration) == 'now') {
2435            $pwdexpiration = $db->qstr(date('Y-m-d H:i:s'));
2436        } else {
2437            $pwdexpiration = $db->qstr($pwdexpiration);
2438        }
2439        $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").")";
2440        $res = $this->db->getResult($queryStr);
2441        if (!$res)
2442            return false;
2443
2444        $user = $this->getUser($this->db->getInsertID('tblUsers'));
2445
2446        /* Check if 'onPostAddUser' callback is set */
2447        if (isset($this->callbacks['onPostAddUser'])) {
2448            foreach ($this->callbacks['onPostAddUser'] as $callback) {
2449                /** @noinspection PhpStatementHasEmptyBodyInspection */
2450                if (!call_user_func($callback[0], $callback[1], $user)) {
2451                }
2452            }
2453        }
2454
2455        return $user;
2456    } /* }}} */
2457
2458    /**
2459     * Get a group by its id
2460     *
2461     * @param integer $id id of group
2462     * @return SeedDMS_Core_Group|boolean group or false if no group was found
2463     */
2464    public function getGroup($id) { /* {{{ */
2465        if ($this->usecache && isset($this->cache['groups'][$id])) {
2466            return $this->cache['groups'][$id];
2467        }
2468        $classname = $this->classnames['group'];
2469        $group = $classname::getInstance($id, $this, '');
2470        if ($this->usecache)
2471            $this->cache['groups'][$id] = $group;
2472        return $group;
2473    } /* }}} */
2474
2475    /**
2476     * Get a group by its name
2477     *
2478     * @param string $name name of group
2479     * @return SeedDMS_Core_Group|boolean group or false if no group was found
2480     */
2481    public function getGroupByName($name) { /* {{{ */
2482        $name = trim($name);
2483        $classname = $this->classnames['group'];
2484        return $classname::getInstance($name, $this, 'name');
2485    } /* }}} */
2486
2487    /**
2488     * Get a list of all groups
2489     *
2490     * @return SeedDMS_Core_Group[] array of instances of {@see SeedDMS_Core_Group}
2491     */
2492    public function getAllGroups() { /* {{{ */
2493        $classname = $this->classnames['group'];
2494        return $classname::getAllInstances('name', $this);
2495    } /* }}} */
2496
2497    /**
2498     * Create a new user group
2499     *
2500     * @param string $name name of group
2501     * @param string $comment comment of group
2502     * @return SeedDMS_Core_Group|boolean instance of {@see SeedDMS_Core_Group} or false in
2503     *         case of an error.
2504     */
2505    public function addGroup($name, $comment) { /* {{{ */
2506        $name = trim($name);
2507        if (is_object($this->getGroupByName($name))) {
2508            return false;
2509        }
2510
2511        $queryStr = "INSERT INTO `tblGroups` (`name`, `comment`) VALUES (".$this->db->qstr($name).", ".$this->db->qstr($comment).")";
2512        if (!$this->db->getResult($queryStr))
2513            return false;
2514
2515        $group = $this->getGroup($this->db->getInsertID('tblGroups'));
2516
2517        /* Check if 'onPostAddGroup' callback is set */
2518        if (isset($this->callbacks['onPostAddGroup'])) {
2519            foreach ($this->callbacks['onPostAddGroup'] as $callback) {
2520                /** @noinspection PhpStatementHasEmptyBodyInspection */
2521                if (!call_user_func($callback[0], $callback[1], $group)) {
2522                }
2523            }
2524        }
2525
2526        return $group;
2527    } /* }}} */
2528
2529    public function getKeywordCategory($id) { /* {{{ */
2530        if (!is_numeric($id) || $id < 1)
2531            return false;
2532
2533        $queryStr = "SELECT * FROM `tblKeywordCategories` WHERE `id` = " . (int) $id;
2534        $resArr = $this->db->getResultArray($queryStr);
2535        if (is_bool($resArr) && !$resArr)
2536            return false;
2537        if (count($resArr) != 1)
2538            return null;
2539
2540        $resArr = $resArr[0];
2541        $cat = new SeedDMS_Core_KeywordCategory($resArr["id"], $resArr["owner"], $resArr["name"]);
2542        $cat->setDMS($this);
2543        return $cat;
2544    } /* }}} */
2545
2546    public function getKeywordCategoryByName($name, $userID) { /* {{{ */
2547        if (!is_numeric($userID) || $userID < 1)
2548            return false;
2549        $name = trim($name);
2550        $queryStr = "SELECT * FROM `tblKeywordCategories` WHERE `name` = " . $this->db->qstr($name) . " AND `owner` = " . (int) $userID;
2551        $resArr = $this->db->getResultArray($queryStr);
2552        if (is_bool($resArr) && !$resArr)
2553            return false;
2554        if (count($resArr) != 1)
2555            return null;
2556
2557        $resArr = $resArr[0];
2558        $cat = new SeedDMS_Core_KeywordCategory($resArr["id"], $resArr["owner"], $resArr["name"]);
2559        $cat->setDMS($this);
2560        return $cat;
2561    } /* }}} */
2562
2563    public function getAllKeywordCategories($userIDs = array()) { /* {{{ */
2564        $queryStr = "SELECT * FROM `tblKeywordCategories`";
2565        /* Ensure $userIDs() will only contain integers > 0 */
2566        $userIDs = array_filter(array_unique(array_map('intval', $userIDs)), function($a) {return $a > 0;});
2567        if ($userIDs) {
2568            $queryStr .= " WHERE `owner` IN (".implode(',', $userIDs).")";
2569        }
2570
2571        $resArr = $this->db->getResultArray($queryStr);
2572        if (is_bool($resArr) && !$resArr)
2573            return false;
2574
2575        $categories = array();
2576        foreach ($resArr as $row) {
2577            $cat = new SeedDMS_Core_KeywordCategory($row["id"], $row["owner"], $row["name"]);
2578            $cat->setDMS($this);
2579            array_push($categories, $cat);
2580        }
2581
2582        return $categories;
2583    } /* }}} */
2584
2585    /**
2586     * This method should be replaced by getAllKeywordCategories()
2587     *
2588     * @param $userID
2589     * @return SeedDMS_Core_KeywordCategory[]|bool
2590     */
2591    public function getAllUserKeywordCategories($userID) { /* {{{ */
2592        if (!is_numeric($userID) || $userID < 1)
2593            return false;
2594        return self::getAllKeywordCategories([$userID]);
2595    } /* }}} */
2596
2597    public function addKeywordCategory($userID, $name) { /* {{{ */
2598        if (!is_numeric($userID) || $userID < 1)
2599            return false;
2600        $name = trim($name);
2601        if (!$name)
2602            return false;
2603        if (is_object($this->getKeywordCategoryByName($name, $userID))) {
2604            return false;
2605        }
2606        $queryStr = "INSERT INTO `tblKeywordCategories` (`owner`, `name`) VALUES (".(int) $userID.", ".$this->db->qstr($name).")";
2607        if (!$this->db->getResult($queryStr))
2608            return false;
2609
2610        $category = $this->getKeywordCategory($this->db->getInsertID('tblKeywordCategories'));
2611
2612        /* Check if 'onPostAddKeywordCategory' callback is set */
2613        if (isset($this->callbacks['onPostAddKeywordCategory'])) {
2614            foreach ($this->callbacks['onPostAddKeywordCategory'] as $callback) {
2615                /** @noinspection PhpStatementHasEmptyBodyInspection */
2616                if (!call_user_func($callback[0], $callback[1], $category)) {
2617                }
2618            }
2619        }
2620
2621        return $category;
2622    } /* }}} */
2623
2624    public function getDocumentCategory($id) { /* {{{ */
2625        if (!is_numeric($id) || $id < 1)
2626            return false;
2627
2628        $queryStr = "SELECT * FROM `tblCategory` WHERE `id` = " . (int) $id;
2629        $resArr = $this->db->getResultArray($queryStr);
2630        if (is_bool($resArr) && !$resArr)
2631            return false;
2632        if (count($resArr) != 1)
2633            return null;
2634
2635        $resArr = $resArr[0];
2636        $cat = new SeedDMS_Core_DocumentCategory($resArr["id"], $resArr["name"]);
2637        $cat->setDMS($this);
2638        return $cat;
2639    } /* }}} */
2640
2641    public function getDocumentCategories() { /* {{{ */
2642        $queryStr = "SELECT * FROM `tblCategory` order by `name`";
2643
2644        $resArr = $this->db->getResultArray($queryStr);
2645        if (is_bool($resArr) && !$resArr)
2646            return false;
2647
2648        $categories = array();
2649        foreach ($resArr as $row) {
2650            $cat = new SeedDMS_Core_DocumentCategory($row["id"], $row["name"]);
2651            $cat->setDMS($this);
2652            array_push($categories, $cat);
2653        }
2654
2655        return $categories;
2656    } /* }}} */
2657
2658    /**
2659     * Get a category by its name
2660     *
2661     * The name of a category is by default unique.
2662     *
2663     * @param string $name human readable name of category
2664     * @return SeedDMS_Core_DocumentCategory|boolean instance of {@see SeedDMS_Core_DocumentCategory}
2665     */
2666    public function getDocumentCategoryByName($name) { /* {{{ */
2667        $name = trim($name);
2668        if (!$name) return false;
2669
2670        $queryStr = "SELECT * FROM `tblCategory` WHERE `name`=".$this->db->qstr($name);
2671        $resArr = $this->db->getResultArray($queryStr);
2672        if (!$resArr)
2673            return false;
2674
2675        $row = $resArr[0];
2676        $cat = new SeedDMS_Core_DocumentCategory($row["id"], $row["name"]);
2677        $cat->setDMS($this);
2678
2679        return $cat;
2680    } /* }}} */
2681
2682    /**
2683     * Add a new document category
2684     *
2685     * This method calls the hook `onPostAddDocumentCategory` if the new
2686     * category was added successfully.
2687     *
2688     * @param string $name name of category
2689     * @return SeedDMS_Core_DocumentCategory|boolean instance of {@see SeedDMS_Core_DocumentCategory} or false if the category already exists or in case of an error.
2690     */
2691    public function addDocumentCategory($name) { /* {{{ */
2692        $name = trim($name);
2693        if (!$name)
2694            return false;
2695        if (is_object($this->getDocumentCategoryByName($name))) {
2696            return false;
2697        }
2698        $queryStr = "INSERT INTO `tblCategory` (`name`) VALUES (".$this->db->qstr($name).")";
2699        if (!$this->db->getResult($queryStr))
2700            return false;
2701
2702        $category = $this->getDocumentCategory($this->db->getInsertID('tblCategory'));
2703
2704        /* Check if 'onPostAddDocumentCategory' callback is set */
2705        if (isset($this->callbacks['onPostAddDocumentCategory'])) {
2706            foreach ($this->callbacks['onPostAddDocumentCategory'] as $callback) {
2707                /** @noinspection PhpStatementHasEmptyBodyInspection */
2708                if (!call_user_func($callback[0], $callback[1], $category)) {
2709                }
2710            }
2711        }
2712
2713        return $category;
2714    } /* }}} */
2715
2716    /**
2717     * Get all notifications for a group
2718     *
2719     * deprecated: User {@see SeedDMS_Core_Group::getNotifications()}
2720     *
2721     * @param object $group group for which notifications are to be retrieved
2722     * @param integer $type type of item (T_DOCUMENT or T_FOLDER)
2723     * @return array array of notifications
2724     */
2725    public function getNotificationsByGroup($group, $type = 0) { /* {{{ */
2726        return $group->getNotifications($type);
2727    } /* }}} */
2728
2729    /**
2730     * Get all notifications for a user
2731     *
2732     * deprecated: User {@see SeedDMS_Core_User::getNotifications()}
2733     *
2734     * @param object $user user for which notifications are to be retrieved
2735     * @param integer $type type of item (T_DOCUMENT or T_FOLDER)
2736     * @return array array of notifications
2737     */
2738    public function getNotificationsByUser($user, $type = 0) { /* {{{ */
2739        return $user->getNotifications($type);
2740    } /* }}} */
2741
2742    /**
2743     * Create a token to request a new password.
2744     *
2745     * This method will not delete the password but just creates an entry
2746     * in `tblUserRequestPassword` indicating a password request.
2747     *
2748     * @param SeedDMS_Core_User $user
2749     * @return string|boolean hash value of false in case of an error
2750     */
2751    public function createPasswordRequest($user) { /* {{{ */
2752        $lenght = 32;
2753        if (function_exists("random_bytes")) {
2754            $bytes = random_bytes((int) ceil($lenght / 2));
2755        } elseif (function_exists("openssl_random_pseudo_bytes")) {
2756            $bytes = openssl_random_pseudo_bytes(ceil($lenght / 2));
2757        } else {
2758            return false;
2759        }
2760        $hash = bin2hex($bytes);
2761        $queryStr = "INSERT INTO `tblUserPasswordRequest` (`userID`, `hash`, `date`) VALUES (" . $user->getId() . ", " . $this->db->qstr($hash) .", ".$this->db->getCurrentDatetime().")";
2762        $resArr = $this->db->getResult($queryStr);
2763        if (is_bool($resArr) && !$resArr) return false;
2764        return $hash;
2765    } /* }}} */
2766
2767    /**
2768     * Check if hash for a password request is valid.
2769     *
2770     * This method searches a previously created password request and
2771     * returns the user.
2772     *
2773     * @param string $hash
2774     * @return bool|SeedDMS_Core_User
2775     */
2776    public function checkPasswordRequest($hash) { /* {{{ */
2777        /* Get the password request from the database */
2778        $queryStr = "SELECT * FROM `tblUserPasswordRequest` WHERE `hash`=".$this->db->qstr($hash);
2779        $resArr = $this->db->getResultArray($queryStr);
2780        if (is_bool($resArr) && !$resArr)
2781            return false;
2782
2783        if (count($resArr) != 1)
2784            return false;
2785        $resArr = $resArr[0];
2786
2787        return $this->getUser($resArr['userID']);
2788
2789    } /* }}} */
2790
2791    /**
2792     * Delete a password request
2793     *
2794     * @param string $hash
2795     * @return bool
2796     */
2797    public function deletePasswordRequest($hash) { /* {{{ */
2798        /* Delete the request, so nobody can use it a second time */
2799        $queryStr = "DELETE FROM `tblUserPasswordRequest` WHERE `hash`=".$this->db->qstr($hash);
2800        if (!$this->db->getResult($queryStr))
2801            return false;
2802        return true;
2803    } /* }}} */
2804
2805    /**
2806     * Return a attribute definition by its id
2807     *
2808     * This method retrieves a attribute definitionr from the database by
2809     * its id.
2810     *
2811     * @param integer $id internal id of attribute defintion
2812     * @return bool|SeedDMS_Core_AttributeDefinition or false
2813     */
2814    public function getAttributeDefinition($id) { /* {{{ */
2815        if (!is_numeric($id) || $id < 1)
2816            return false;
2817
2818        $queryStr = "SELECT * FROM `tblAttributeDefinitions` WHERE `id` = " . (int) $id;
2819        $resArr = $this->db->getResultArray($queryStr);
2820
2821        if (is_bool($resArr) && $resArr == false)
2822            return false;
2823        if (count($resArr) != 1)
2824            return null;
2825
2826        $resArr = $resArr[0];
2827
2828        $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"]);
2829        $attrdef->setDMS($this);
2830        return $attrdef;
2831    } /* }}} */
2832
2833    /**
2834     * Return a attribute definition by its name
2835     *
2836     * This method retrieves an attribute def. from the database by its name.
2837     *
2838     * @param string $name internal name of attribute def.
2839     * @return SeedDMS_Core_AttributeDefinition|boolean instance of {@see SeedDMS_Core_AttributeDefinition} or false
2840     */
2841    public function getAttributeDefinitionByName($name) { /* {{{ */
2842        $name = trim($name);
2843        if (!$name) return false;
2844
2845        $queryStr = "SELECT * FROM `tblAttributeDefinitions` WHERE `name` = " . $this->db->qstr($name);
2846        $resArr = $this->db->getResultArray($queryStr);
2847
2848        if (is_bool($resArr) && $resArr == false)
2849            return false;
2850        if (count($resArr) != 1)
2851            return null;
2852
2853        $resArr = $resArr[0];
2854
2855        $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"]);
2856        $attrdef->setDMS($this);
2857        return $attrdef;
2858    } /* }}} */
2859
2860    /**
2861     * Return list of all attribute definitions
2862     *
2863     * @param integer|array $objtype select those attribute definitions defined for an object type
2864     * @param integer|array $type select those attribute definitions defined for a type
2865     * @return bool|SeedDMS_Core_AttributeDefinition[] of instances of {@see SeedDMS_Core_AttributeDefinition} or false
2866     * or false
2867     */
2868    public function getAllAttributeDefinitions($objtype = 0, $type = 0) { /* {{{ */
2869        $queryStr = "SELECT * FROM `tblAttributeDefinitions`";
2870        if ($objtype || $type) {
2871            $queryStr .= ' WHERE ';
2872            if ($objtype) {
2873                if (is_array($objtype))
2874                    $queryStr .= '`objtype` in (\''.implode("','", $objtype).'\')';
2875                else
2876                    $queryStr .= '`objtype`='.intval($objtype);
2877            }
2878            if ($objtype && $type) {
2879                $queryStr .= ' AND ';
2880            }
2881            if ($type) {
2882                if (is_array($type))
2883                    $queryStr .= '`type` in (\''.implode("','", $type).'\')';
2884                else
2885                    $queryStr .= '`type`='.intval($type);
2886            }
2887        }
2888        $queryStr .= ' ORDER BY `name`';
2889        $resArr = $this->db->getResultArray($queryStr);
2890
2891        if (is_bool($resArr) && $resArr == false)
2892            return false;
2893
2894        /** @var SeedDMS_Core_AttributeDefinition[] $attrdefs */
2895        $attrdefs = array();
2896
2897        for ($i = 0; $i < count($resArr); $i++) {
2898            $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"]);
2899            $attrdef->setDMS($this);
2900            $attrdefs[$i] = $attrdef;
2901        }
2902
2903        return $attrdefs;
2904    } /* }}} */
2905
2906    /**
2907     * Add a new attribute definition
2908     *
2909     * @param string $name name of attribute
2910     * @param $objtype
2911     * @param string $type type of attribute
2912     * @param bool|int $multiple set to 1 if attribute has multiple attributes
2913     * @param integer $minvalues minimum number of values
2914     * @param integer $maxvalues maximum number of values if multiple is set
2915     * @param string $valueset list of allowed values (csv format)
2916     * @param string $regex
2917     * @return bool|SeedDMS_Core_User
2918     */
2919    public function addAttributeDefinition($name, $objtype, $type, $multiple = 0, $minvalues = 0, $maxvalues = 1, $valueset = '', $regex = '') { /* {{{ */
2920        $name = trim($name);
2921        if (!$name)
2922            return false;
2923        if (is_object($this->getAttributeDefinitionByName($name))) {
2924            return false;
2925        }
2926        if ($objtype < SeedDMS_Core_AttributeDefinition::objtype_all || $objtype > SeedDMS_Core_AttributeDefinition::objtype_documentcontent)
2927            return false;
2928        if (!$type)
2929            return false;
2930        if (trim($valueset)) {
2931            $valuesetarr = array_map('trim', explode($valueset[0], substr($valueset, 1)));
2932            $valueset = $valueset[0].implode($valueset[0], $valuesetarr);
2933        } else {
2934            $valueset = '';
2935        }
2936        $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).")";
2937        $res = $this->db->getResult($queryStr);
2938        if (!$res)
2939            return false;
2940
2941        return $this->getAttributeDefinition($this->db->getInsertID('tblAttributeDefinitions'));
2942    } /* }}} */
2943
2944    /**
2945     * Return list of all workflows
2946     *
2947     * @return SeedDMS_Core_Workflow[]|bool of instances of {@see SeedDMS_Core_Workflow} or false
2948     */
2949    public function getAllWorkflows() { /* {{{ */
2950        $queryStr = "SELECT * FROM `tblWorkflows` ORDER BY `name`";
2951        $resArr = $this->db->getResultArray($queryStr);
2952
2953        if (is_bool($resArr) && $resArr == false)
2954            return false;
2955
2956        $queryStr = "SELECT * FROM `tblWorkflowStates` ORDER BY `name`";
2957        $ressArr = $this->db->getResultArray($queryStr);
2958
2959        if (is_bool($ressArr) && $ressArr == false)
2960            return false;
2961
2962        for ($i = 0; $i < count($ressArr); $i++) {
2963            $wkfstates[$ressArr[$i]["id"]] = new SeedDMS_Core_Workflow_State($ressArr[$i]["id"], $ressArr[$i]["name"], $ressArr[$i]["maxtime"], $ressArr[$i]["precondfunc"], $ressArr[$i]["documentstatus"]);
2964        }
2965
2966        /** @var SeedDMS_Core_Workflow[] $workflows */
2967        $workflows = array();
2968        for ($i = 0; $i < count($resArr); $i++) {
2969            /** @noinspection PhpUndefinedVariableInspection */
2970            $workflow = new SeedDMS_Core_Workflow($resArr[$i]["id"], $resArr[$i]["name"], $wkfstates[$resArr[$i]["initstate"]]);
2971            $workflow->setDMS($this);
2972            $workflows[$i] = $workflow;
2973        }
2974
2975        return $workflows;
2976    } /* }}} */
2977
2978    /**
2979     * Return workflow by its Id
2980     *
2981     * @param integer $id internal id of workflow
2982     * @return SeedDMS_Core_Workflow|bool of instances of {@see SeedDMS_Core_Workflow}, null if no workflow was found or false
2983     */
2984    public function getWorkflow($id) { /* {{{ */
2985        if (!is_numeric($id) || $id < 1)
2986            return false;
2987
2988        $queryStr = "SELECT * FROM `tblWorkflows` WHERE `id`=".intval($id);
2989        $resArr = $this->db->getResultArray($queryStr);
2990
2991        if (is_bool($resArr) && $resArr == false)
2992            return false;
2993
2994        if (!$resArr)
2995            return null;
2996
2997        $initstate = $this->getWorkflowState($resArr[0]['initstate']);
2998
2999        $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate);
3000        $workflow->setDMS($this);
3001
3002        return $workflow;
3003    } /* }}} */
3004
3005    /**
3006     * Return workflow by its name
3007     *
3008     * @param string $name name of workflow
3009     * @return SeedDMS_Core_Workflow|bool of instances of {@see SeedDMS_Core_Workflow} or null if no workflow was found or false
3010     */
3011    public function getWorkflowByName($name) { /* {{{ */
3012        $name = trim($name);
3013        if (!$name) return false;
3014
3015        $queryStr = "SELECT * FROM `tblWorkflows` WHERE `name`=".$this->db->qstr($name);
3016        $resArr = $this->db->getResultArray($queryStr);
3017
3018        if (is_bool($resArr) && $resArr == false)
3019            return false;
3020
3021        if (!$resArr)
3022            return null;
3023
3024        $initstate = $this->getWorkflowState($resArr[0]['initstate']);
3025
3026        $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate);
3027        $workflow->setDMS($this);
3028
3029        return $workflow;
3030    } /* }}} */
3031
3032    /**
3033     * Add a new workflow
3034     *
3035     * @param string $name name of workflow
3036     * @param SeedDMS_Core_Workflow_State $initstate initial state of workflow
3037     * @return bool|SeedDMS_Core_Workflow
3038     */
3039    public function addWorkflow($name, $initstate) { /* {{{ */
3040        $db = $this->db;
3041        $name = trim($name);
3042        if (!$name)
3043            return false;
3044        if (is_object($this->getWorkflowByName($name))) {
3045            return false;
3046        }
3047        $queryStr = "INSERT INTO `tblWorkflows` (`name`, `initstate`) VALUES (".$db->qstr($name).", ".$initstate->getID().")";
3048        $res = $db->getResult($queryStr);
3049        if (!$res)
3050            return false;
3051
3052        return $this->getWorkflow($db->getInsertID('tblWorkflows'));
3053    } /* }}} */
3054
3055    /**
3056     * Return a workflow state by its id
3057     *
3058     * This method retrieves a workflow state from the database by its id.
3059     *
3060     * @param integer $id internal id of workflow state
3061     * @return bool|SeedDMS_Core_Workflow_State or false
3062     */
3063    public function getWorkflowState($id) { /* {{{ */
3064        if (!is_numeric($id) || $id < 1)
3065            return false;
3066
3067        $queryStr = "SELECT * FROM `tblWorkflowStates` WHERE `id` = " . (int) $id;
3068        $resArr = $this->db->getResultArray($queryStr);
3069
3070        if (is_bool($resArr) && $resArr == false)
3071            return false;
3072
3073        if (count($resArr) != 1)
3074             return null;
3075
3076        $resArr = $resArr[0];
3077
3078        $state = new SeedDMS_Core_Workflow_State($resArr["id"], $resArr["name"], $resArr["maxtime"], $resArr["precondfunc"], $resArr["documentstatus"]);
3079        $state->setDMS($this);
3080        return $state;
3081    } /* }}} */
3082
3083    /**
3084     * Return workflow state by its name
3085     *
3086     * @param string $name name of workflow state
3087     * @return bool|SeedDMS_Core_Workflow_State or false
3088     */
3089    public function getWorkflowStateByName($name) { /* {{{ */
3090        $name = trim($name);
3091        if (!$name) return false;
3092
3093        $queryStr = "SELECT * FROM `tblWorkflowStates` WHERE `name`=".$this->db->qstr($name);
3094        $resArr = $this->db->getResultArray($queryStr);
3095
3096        if (is_bool($resArr) && $resArr == false)
3097            return false;
3098
3099        if (!$resArr)
3100            return null;
3101
3102        $resArr = $resArr[0];
3103
3104        $state = new SeedDMS_Core_Workflow_State($resArr["id"], $resArr["name"], $resArr["maxtime"], $resArr["precondfunc"], $resArr["documentstatus"]);
3105        $state->setDMS($this);
3106
3107        return $state;
3108    } /* }}} */
3109
3110    /**
3111     * Return list of all workflow states
3112     *
3113     * @return SeedDMS_Core_Workflow_State[]|bool of instances of {@see SeedDMS_Core_Workflow_State} or false
3114     */
3115    public function getAllWorkflowStates() { /* {{{ */
3116        $queryStr = "SELECT * FROM `tblWorkflowStates` ORDER BY `name`";
3117        $ressArr = $this->db->getResultArray($queryStr);
3118
3119        if (is_bool($ressArr) && $ressArr == false)
3120            return false;
3121
3122        $wkfstates = array();
3123        for ($i = 0; $i < count($ressArr); $i++) {
3124            $wkfstate = new SeedDMS_Core_Workflow_State($ressArr[$i]["id"], $ressArr[$i]["name"], $ressArr[$i]["maxtime"], $ressArr[$i]["precondfunc"], $ressArr[$i]["documentstatus"]);
3125            $wkfstate->setDMS($this);
3126            $wkfstates[$i] = $wkfstate;
3127        }
3128
3129        return $wkfstates;
3130    } /* }}} */
3131
3132    /**
3133     * Add new workflow state
3134     *
3135     * @param string $name name of workflow state
3136     * @param integer $docstatus document status when this state is reached
3137     * @return bool|SeedDMS_Core_Workflow_State
3138     */
3139    public function addWorkflowState($name, $docstatus) { /* {{{ */
3140        $db = $this->db;
3141        $name = trim($name);
3142        if (!$name)
3143            return false;
3144        if (is_object($this->getWorkflowStateByName($name))) {
3145            return false;
3146        }
3147        $queryStr = "INSERT INTO `tblWorkflowStates` (`name`, `documentstatus`) VALUES (".$db->qstr($name).", ".(int) $docstatus.")";
3148        $res = $db->getResult($queryStr);
3149        if (!$res)
3150            return false;
3151
3152        return $this->getWorkflowState($db->getInsertID('tblWorkflowStates'));
3153    } /* }}} */
3154
3155    /**
3156     * Return a workflow action by its id
3157     *
3158     * This method retrieves a workflow action from the database by its id.
3159     *
3160     * @param integer $id internal id of workflow action
3161     * @return SeedDMS_Core_Workflow_Action|bool instance of {@see SeedDMS_Core_Workflow_Action} or false
3162     */
3163    public function getWorkflowAction($id) { /* {{{ */
3164        if (!is_numeric($id) || $id < 1)
3165            return false;
3166
3167        $queryStr = "SELECT * FROM `tblWorkflowActions` WHERE `id` = " . (int) $id;
3168        $resArr = $this->db->getResultArray($queryStr);
3169
3170        if (is_bool($resArr) && $resArr == false)
3171            return false;
3172
3173        if (count($resArr) != 1)
3174             return null;
3175
3176        $resArr = $resArr[0];
3177
3178        $action = new SeedDMS_Core_Workflow_Action($resArr["id"], $resArr["name"]);
3179        $action->setDMS($this);
3180        return $action;
3181    } /* }}} */
3182
3183    /**
3184     * Return a workflow action by its name
3185     *
3186     * This method retrieves a workflow action from the database by its name.
3187     *
3188     * @param string $name name of workflow action
3189     * @return SeedDMS_Core_Workflow_Action|bool instance of {@see SeedDMS_Core_Workflow_Action} or false
3190     */
3191    public function getWorkflowActionByName($name) { /* {{{ */
3192        $name = trim($name);
3193        if (!$name) return false;
3194
3195        $queryStr = "SELECT * FROM `tblWorkflowActions` WHERE `name` = " . $this->db->qstr($name);
3196        $resArr = $this->db->getResultArray($queryStr);
3197
3198        if (is_bool($resArr) && $resArr == false)
3199            return false;
3200
3201        if (count($resArr) != 1)
3202             return null;
3203
3204        $resArr = $resArr[0];
3205
3206        $action = new SeedDMS_Core_Workflow_Action($resArr["id"], $resArr["name"]);
3207        $action->setDMS($this);
3208        return $action;
3209    } /* }}} */
3210
3211    /**
3212     * Return list of workflow action
3213     *
3214     * @return SeedDMS_Core_Workflow_Action[]|bool list of instances of {@see SeedDMS_Core_Workflow_Action} or false
3215     */
3216    public function getAllWorkflowActions() { /* {{{ */
3217        $queryStr = "SELECT * FROM `tblWorkflowActions`";
3218        $resArr = $this->db->getResultArray($queryStr);
3219
3220        if (is_bool($resArr) && $resArr == false)
3221            return false;
3222
3223        /** @var SeedDMS_Core_Workflow_Action[] $wkfactions */
3224        $wkfactions = array();
3225        for ($i = 0; $i < count($resArr); $i++) {
3226            $action = new SeedDMS_Core_Workflow_Action($resArr[$i]["id"], $resArr[$i]["name"]);
3227            $action->setDMS($this);
3228            $wkfactions[$i] = $action;
3229        }
3230
3231        return $wkfactions;
3232    } /* }}} */
3233
3234    /**
3235     * Add new workflow action
3236     *
3237     * @param string $name name of workflow action
3238     * @return SeedDMS_Core_Workflow_Action|bool
3239     */
3240    public function addWorkflowAction($name) { /* {{{ */
3241        $db = $this->db;
3242        $name = trim($name);
3243        if (!$name)
3244            return false;
3245        if (is_object($this->getWorkflowActionByName($name))) {
3246            return false;
3247        }
3248        $queryStr = "INSERT INTO `tblWorkflowActions` (`name`) VALUES (".$db->qstr($name).")";
3249        $res = $db->getResult($queryStr);
3250        if (!$res)
3251            return false;
3252
3253        return $this->getWorkflowAction($db->getInsertID('tblWorkflowActions'));
3254    } /* }}} */
3255
3256    /**
3257     * Return a workflow transition by its id
3258     *
3259     * This method retrieves a workflow transition from the database by its id.
3260     *
3261     * @param integer $id internal id of workflow transition
3262     * @return SeedDMS_Core_Workflow_Transition|bool instance of {@see SeedDMS_Core_Workflow_Transition} or false
3263     */
3264    public function getWorkflowTransition($id) { /* {{{ */
3265        if (!is_numeric($id))
3266            return false;
3267
3268        $queryStr = "SELECT * FROM `tblWorkflowTransitions` WHERE `id` = " . (int) $id;
3269        $resArr = $this->db->getResultArray($queryStr);
3270
3271        if (is_bool($resArr) && $resArr == false) return false;
3272        if (count($resArr) != 1) return false;
3273
3274        $resArr = $resArr[0];
3275
3276        $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"]);
3277        $transition->setDMS($this);
3278        return $transition;
3279    } /* }}} */
3280
3281    /**
3282     * Returns document content which is not linked to a document
3283     *
3284     * This method is for finding straying document content without
3285     * a parent document. In normal operation this should not happen
3286     * but little checks for database consistency and possible errors
3287     * in the application may have left over document content though
3288     * the document is gone already.
3289     *
3290     * @return array|bool
3291     */
3292    public function getUnlinkedDocumentContent() { /* {{{ */
3293        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` NOT IN (SELECT id FROM `tblDocuments`)";
3294        $resArr = $this->db->getResultArray($queryStr);
3295        if ($resArr === false)
3296            return false;
3297
3298        $versions = array();
3299        foreach ($resArr as $row) {
3300            /** @var SeedDMS_Core_Document $document */
3301            $document = new $this->classnames['document']($row['document'], '', '', '', '', '', '', '', '', '', '', '');
3302            $document->setDMS($this);
3303            $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']);
3304            $versions[] = $version;
3305        }
3306        return $versions;
3307
3308    } /* }}} */
3309
3310    /**
3311     * Returns document content which has no file size set
3312     *
3313     * This method is for finding document content without a file size
3314     * set in the database. The file size of a document content was introduced
3315     * in version 4.0.0 of SeedDMS for implementation of user quotas.
3316     *
3317     * @return SeedDMS_Core_Document[]|bool
3318     */
3319    public function getNoFileSizeDocumentContent() { /* {{{ */
3320        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `fileSize` = 0 OR `fileSize` is null";
3321        $resArr = $this->db->getResultArray($queryStr);
3322        if ($resArr === false)
3323            return false;
3324
3325        /** @var SeedDMS_Core_Document[] $versions */
3326        $versions = array();
3327        foreach ($resArr as $row) {
3328            $document = $this->getDocument($row['document']);
3329            /* getting the document can fail if it is outside the root folder
3330             * and checkWithinRootDir is enabled.
3331             */
3332            if ($document) {
3333                $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']);
3334                $versions[] = $version;
3335            }
3336        }
3337        return $versions;
3338
3339    } /* }}} */
3340
3341    /**
3342     * Returns document content which has no checksum set
3343     *
3344     * This method is for finding document content without a checksum
3345     * set in the database. The checksum of a document content was introduced
3346     * in version 4.0.0 of SeedDMS for finding duplicates.
3347     * @return bool|SeedDMS_Core_Document[]
3348     */
3349    public function getNoChecksumDocumentContent() { /* {{{ */
3350        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `checksum` = '' OR `checksum` is null";
3351        $resArr = $this->db->getResultArray($queryStr);
3352        if ($resArr === false)
3353            return false;
3354
3355        /** @var SeedDMS_Core_Document[] $versions */
3356        $versions = array();
3357        foreach ($resArr as $row) {
3358            $document = $this->getDocument($row['document']);
3359            /* getting the document can fail if it is outside the root folder
3360             * and checkWithinRootDir is enabled.
3361             */
3362            if ($document) {
3363                $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']);
3364                $versions[] = $version;
3365            }
3366        }
3367        return $versions;
3368
3369    } /* }}} */
3370
3371    /**
3372     * Returns document content which is duplicated
3373     *
3374     * This method is for finding document content which is available twice
3375     * in the database. The checksum of a document content was introduced
3376     * in version 4.0.0 of SeedDMS for finding duplicates.
3377     * @return array|bool
3378     */
3379    public function getDuplicateDocumentContent() { /* {{{ */
3380        $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";
3381        $resArr = $this->db->getResultArray($queryStr);
3382        if ($resArr === false)
3383            return false;
3384
3385        /** @var SeedDMS_Core_Document[] $versions */
3386        $versions = array();
3387        foreach ($resArr as $row) {
3388            $document = $this->getDocument($row['document']);
3389            /* getting the document can fail if it is outside the root folder
3390             * and checkWithinRootDir is enabled.
3391             */
3392            if ($document) {
3393                $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']);
3394                if (!isset($versions[$row['dupid']])) {
3395                    $versions[$row['id']]['content'] = $version;
3396                    $versions[$row['id']]['duplicates'] = array();
3397                } else
3398                    $versions[$row['dupid']]['duplicates'][] = $version;
3399            }
3400        }
3401        return $versions;
3402
3403    } /* }}} */
3404
3405    /**
3406     * Returns folders which contain documents with none unique sequence number
3407     *
3408     * This method is for finding folders with documents not having a
3409     * unique sequence number. Those documents cannot propperly be sorted
3410     * by sequence and changing their position is impossible if more than
3411     * two documents with the same sequence number exists, e.g.
3412     * doc 1: 3
3413     * doc 2: 5
3414     * doc 3: 5
3415     * doc 4: 5
3416     * doc 5: 7
3417     * If document 4 was to be moved between doc 1 and 2 it get sequence
3418     * number 4 ((5+3)/2).
3419     * But if document 4 was to be moved between doc 2 and 3 it will again
3420     * have sequence number 5.
3421     *
3422     * @return array|bool
3423     */
3424    public function getDuplicateSequenceNo() { /* {{{ */
3425        $queryStr = "SELECT DISTINCT `folder` FROM (SELECT `folder`, `sequence` FROM `tblDocuments` GROUP BY `folder`, `sequence` HAVING count(*) > 1) a";
3426        $resArr = $this->db->getResultArray($queryStr);
3427        if ($resArr === false)
3428            return false;
3429
3430        $folders = array();
3431        foreach ($resArr as $row) {
3432            $folder = $this->getFolder($row['folder']);
3433            if ($folder)
3434                $folders[] = $folder;
3435        }
3436        return $folders;
3437
3438    } /* }}} */
3439
3440    /**
3441     * Returns documents which have link to themselves
3442     *
3443     * @return array|bool
3444     */
3445    public function getLinksToItself() { /* {{{ */
3446        $queryStr = "SELECT * FROM `tblDocumentLinks` WHERE `document`=`target`";
3447        $resArr = $this->db->getResultArray($queryStr);
3448        if ($resArr === false)
3449            return false;
3450
3451        $documents = array();
3452        foreach ($resArr as $row) {
3453            $document = $this->getDocument($row['document']);
3454            if ($document)
3455                $documents[] = $document;
3456        }
3457        return $documents;
3458
3459    } /* }}} */
3460
3461    /**
3462     * Returns a list of reviews, approvals, receipts, revisions which are not
3463     * linked to a user, group anymore
3464     *
3465     * This method is for finding reviews or approvals whose user
3466     * or group  was deleted and not just removed from the process.
3467     *
3468     * @param string $process
3469     * @param string $usergroup
3470     * @return array
3471     */
3472    public function getProcessWithoutUserGroup($process, $usergroup) { /* {{{ */
3473        switch ($process) {
3474        case 'review':
3475            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentReviewers`";
3476            break;
3477        case 'approval':
3478            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentApprovers`";
3479            break;
3480        }
3481        /** @noinspection PhpUndefinedVariableInspection */
3482        $queryStr .= " a LEFT JOIN `tblDocuments` b ON a.`documentID`=b.`id` WHERE";
3483        switch ($usergroup) {
3484        case 'user':
3485            $queryStr .= " a.`type`=0 and a.`required` not in (SELECT `id` FROM `tblUsers`) ORDER BY b.`id`";
3486            break;
3487        case 'group':
3488            $queryStr .= " a.`type`=1 and a.`required` not in (SELECT `id` FROM `tblGroups`) ORDER BY b.`id`";
3489            break;
3490        }
3491        return $this->db->getResultArray($queryStr);
3492    } /* }}} */
3493
3494    /**
3495     * Removes all reviews, approvals which are not linked
3496     * to a user, group anymore
3497     *
3498     * This method is for removing all reviews or approvals whose user
3499     * or group  was deleted and not just removed from the process.
3500     * If the optional parameter $id is set, only this user/group id is removed.
3501     * @param string $process
3502     * @param string $usergroup
3503     * @param int $id
3504     * @return array
3505     */
3506    public function removeProcessWithoutUserGroup($process, $usergroup, $id = 0) { /* {{{ */
3507        /* Entries of tblDocumentReviewLog or tblDocumentApproveLog are deleted
3508         * because of CASCADE ON
3509         */
3510        switch ($process) {
3511        case 'review':
3512            $queryStr = "DELETE FROM tblDocumentReviewers";
3513            break;
3514        case 'approval':
3515            $queryStr = "DELETE FROM tblDocumentApprovers";
3516            break;
3517        }
3518        /** @noinspection PhpUndefinedVariableInspection */
3519        $queryStr .= " WHERE";
3520        switch ($usergroup) {
3521        case 'user':
3522            $queryStr .= " type=0 AND";
3523            if ($id)
3524                $queryStr .= " required=".((int) $id)." AND";
3525            $queryStr .= " required NOT IN (SELECT id FROM tblUsers)";
3526            break;
3527        case 'group':
3528            $queryStr .= " type=1 AND";
3529            if ($id)
3530                $queryStr .= " required=".((int) $id)." AND";
3531            $queryStr .= " required NOT IN (SELECT id FROM tblGroups)";
3532            break;
3533        }
3534        return $this->db->getResultArray($queryStr);
3535    } /* }}} */
3536
3537    /**
3538     * Returns statitical information
3539     *
3540     * This method returns all kind of statistical information like
3541     * documents or used space per user, recent activity, etc.
3542     *
3543     * @param string $type type of statistic
3544     * @return array|bool returns false if the sql statement fails, returns an empty
3545     * array if no documents or folder where found, otherwise returns a non empty
3546     * array with statistical data
3547     */
3548    public function getStatisticalData($type = '') { /* {{{ */
3549        switch ($type) {
3550            case 'docsperuser':
3551                $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`";
3552                $resArr = $this->db->getResultArray($queryStr);
3553                if (is_bool($resArr) && $resArr == false)
3554                    return false;
3555
3556                return $resArr;
3557            case 'foldersperuser':
3558                $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`";
3559                $resArr = $this->db->getResultArray($queryStr);
3560                if (is_bool($resArr) && $resArr == false)
3561                    return false;
3562
3563                return $resArr;
3564            case 'docspermimetype':
3565                $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`";
3566                $resArr = $this->db->getResultArray($queryStr);
3567                if (is_bool($resArr) && $resArr == false)
3568                    return false;
3569
3570                return $resArr;
3571            case 'docspercategory':
3572                $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`";
3573                $resArr = $this->db->getResultArray($queryStr);
3574                if (is_bool($resArr) && $resArr == false)
3575                    return false;
3576
3577                return $resArr;
3578            case 'docsperstatus':
3579                /** @noinspection PhpUnusedLocalVariableInspection */
3580                $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`";
3581                $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`";
3582                $resArr = $this->db->getResultArray($queryStr);
3583                if (is_bool($resArr) && $resArr == false)
3584                    return false;
3585
3586                return $resArr;
3587            case 'docspermonth':
3588                $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`";
3589                $resArr = $this->db->getResultArray($queryStr);
3590                if (is_bool($resArr) && $resArr == false)
3591                    return false;
3592
3593                return $resArr;
3594            case 'docsaccumulated':
3595                $queryStr = "SELECT *, count(`key`) AS total FROM (SELECT ".$this->db->getDateExtract("date")." AS `key` FROM `tblDocuments`) a GROUP BY `key` ORDER BY `key`";
3596                $resArr = $this->db->getResultArray($queryStr);
3597                if (is_bool($resArr) && $resArr == false)
3598                    return false;
3599
3600                $sum = 0;
3601                foreach ($resArr as &$res) {
3602                    $sum += $res['total'];
3603                    /* auxially variable $key is need because sqlite returns
3604                     * a key '`key`'
3605                     */
3606                    $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;
3607                    $res['total'] = $sum;
3608                }
3609                return $resArr;
3610            case 'docstotal':
3611                $queryStr = "SELECT count(*) AS total FROM `tblDocuments`";
3612                $resArr = $this->db->getResultArray($queryStr);
3613                if (is_bool($resArr) && $resArr == false)
3614                    return false;
3615                return (int) $resArr[0]['total'];
3616            case 'folderstotal':
3617                $queryStr = "SELECT count(*) AS total FROM `tblFolders`";
3618                $resArr = $this->db->getResultArray($queryStr);
3619                if (is_bool($resArr) && $resArr == false)
3620                    return false;
3621                return (int) $resArr[0]['total'];
3622            case 'userstotal':
3623                $queryStr = "SELECT count(*) AS total FROM `tblUsers`";
3624                $resArr = $this->db->getResultArray($queryStr);
3625                if (is_bool($resArr) && $resArr == false)
3626                    return false;
3627                return (int) $resArr[0]['total'];
3628            case 'sizeperuser':
3629                $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`";
3630                $resArr = $this->db->getResultArray($queryStr);
3631                if (is_bool($resArr) && $resArr == false)
3632                    return false;
3633
3634                return $resArr;
3635            case 'sizepermonth':
3636                $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`";
3637                $resArr = $this->db->getResultArray($queryStr);
3638                if (is_bool($resArr) && $resArr == false)
3639                    return false;
3640
3641                return $resArr;
3642            default:
3643                return array();
3644        }
3645    } /* }}} */
3646
3647    /**
3648     * Returns changes with a period of time
3649     *
3650     * This method returns a list of all changes happened in the database
3651     * within a given period of time. It currently just checks for
3652     * entries in the database tables tblDocumentContent, tblDocumentFiles,
3653     * and tblDocumentStatusLog
3654     *
3655     * @param string $startts
3656     * @param string $endts
3657     * @return array|bool
3658     * @internal param string $start start date, defaults to start of current day
3659     * @internal param string $end end date, defaults to end of start day
3660     */
3661    public function getTimeline($startts = '', $endts = '') { /* {{{ */
3662        if (!$startts)
3663            $startts = mktime(0, 0, 0);
3664        if (!$endts)
3665            $endts = $startts+86400;
3666
3667        /** @var SeedDMS_Core_Document[] $timeline */
3668        $timeline = array();
3669
3670        if (0) {
3671        $queryStr = "SELECT DISTINCT `document` FROM `tblDocumentContent` WHERE `date` > ".$startts." AND `date` < ".$endts." UNION SELECT DISTINCT `document` FROM `tblDocumentFiles` WHERE `date` > ".$startts." AND `date` < ".$endts;
3672        } else {
3673        $startdate = date('Y-m-d H:i:s', $startts);
3674        $enddate = date('Y-m-d H:i:s', $endts);
3675        $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;
3676        }
3677        $resArr = $this->db->getResultArray($queryStr);
3678        if ($resArr === false)
3679            return false;
3680        foreach ($resArr as $rec) {
3681            $document = $this->getDocument($rec['document']);
3682            $timeline = array_merge($timeline, $document->getTimeline());
3683        }
3684        return $timeline;
3685
3686    } /* }}} */
3687
3688    /**
3689     * Returns changes with a period of time
3690     *
3691     * This method is similar to getTimeline() but returns more dedicated lists
3692     * of documents or folders which has change in various ways.
3693     *
3694     * @param string $mode
3695     * @param string $startts
3696     * @param string $endts
3697     * @return array|bool
3698     * @internal param string $start start date, defaults to start of current day
3699     * @internal param string $end end date, defaults to end of start day
3700     */
3701    public function getLatestChanges($mode, $startts = '', $endts = '') { /* {{{ */
3702        if (!$startts)
3703            $startts = mktime(0, 0, 0);
3704        if (!$endts)
3705            $endts = $startts+86400;
3706
3707        $startdate = date('Y-m-d H:i:s', $startts);
3708        $enddate = date('Y-m-d H:i:s', $endts);
3709
3710        $objects = [];
3711        switch ($mode) {
3712        case 'statuschange':
3713            /* Count entries in tblDocumentStatusLog for each tblDocumentStatus and
3714             * take only those into account with at least 2 log entries. For the
3715             * document id do a left join with tblDocumentStatus
3716             * This is similar to ttstatid + the count + the join
3717             * c > 1 is required to find only those documents with a changed status
3718             * This sql statement appears to be much to complicated.
3719             */
3720            //$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)." ";
3721            $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";
3722            $resArr = $this->db->getResultArray($queryStr);
3723            if ($resArr === false)
3724                return false;
3725            foreach ($resArr as $rec) {
3726                if ($object = $this->getDocument($rec['document']))
3727                    $objects[] = $object;
3728            }
3729            break;
3730        case 'newdocuments':
3731            $queryStr = "SELECT `id` AS `document` FROM `tblDocuments` WHERE `date` > ".$startts." AND `date` < ".$endts." ORDER BY `date` DESC";
3732            $resArr = $this->db->getResultArray($queryStr);
3733            if ($resArr === false)
3734                return false;
3735            foreach ($resArr as $rec) {
3736                if ($object = $this->getDocument($rec['document']))
3737                    $objects[] = $object;
3738            }
3739            break;
3740        case 'updateddocuments':
3741            /* DISTINCT is need if there is more than 1 update of the document in the
3742             * given period of time. Without it, the query will return the document
3743             * more than once.
3744             */
3745            $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";
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 'newfolders':
3755            $queryStr = "SELECT `id` AS `folder` FROM `tblFolders` WHERE `date` > ".$startts." AND `date` < ".$endts." ORDER BY `date` DESC";
3756            $resArr = $this->db->getResultArray($queryStr);
3757            if ($resArr === false)
3758                return false;
3759            foreach ($resArr as $rec) {
3760                if ($object = $this->getFolder($rec['folder']))
3761                    $objects[] = $object;
3762            }
3763            break;
3764        }
3765        return $objects;
3766    } /* }}} */
3767
3768    public function getMimeTypes() { /* {{{ */
3769        $queryStr = "SELECT `mimeType`, COUNT(`mimeType`) AS `c` FROM `tblDocumentContent` GROUP BY `mimeType` ORDER BY `mimeType`";
3770
3771        $resArr = $this->db->getResultArray($queryStr);
3772        if (is_bool($resArr) && !$resArr)
3773            return false;
3774
3775        return $resArr;
3776    } /* }}} */
3777
3778    /**
3779     * Set a callback function
3780     *
3781     * The function passed in $func must be a callable and $name must not be empty.
3782     *
3783     * Setting a callback with this method will remove all previously
3784     * set callbacks. Use {@see SeedDMS_Core_DMS::addCallback()} to register
3785     * additional callbacks.
3786     * This method does not check if there is a callback with the given name.
3787     *
3788     * @param string $name internal name of callback
3789     * @param mixed $func function name as expected by {call_user_method}
3790     * @param mixed $params parameter passed as the first argument to the
3791     *        callback
3792     * @return bool true if adding the callback succeeds otherwise false
3793     */
3794    public function setCallback($name, $func, $params = null) { /* {{{ */
3795        if ($name && $func && is_callable($func)) {
3796            $this->callbacks[$name] = array(array($func, $params));
3797            return true;
3798        } else {
3799            return false;
3800        }
3801    } /* }}} */
3802
3803    /**
3804     * Add a callback function
3805     *
3806     * The function passed in $func must be a callable and $name must not be empty.
3807     * This method does not check if there is a callback with the given name.
3808     *
3809     * @param string $name internal name of callback
3810     * @param mixed $func function name as expected by {call_user_method}
3811     * @param mixed $params parameter passed as the first argument to the
3812     *        callback
3813     * @return bool true if adding the callback succeeds otherwise false
3814     */
3815    public function addCallback($name, $func, $params = null) { /* {{{ */
3816        if ($name && $func && is_callable($func)) {
3817            $this->callbacks[$name][] = array($func, $params);
3818            return true;
3819        } else {
3820            return false;
3821        }
3822    } /* }}} */
3823
3824    /**
3825     * Check if a callback with the given name has been set
3826     *
3827     * @param string $name internal name of callback
3828     * @return bool true if callback exists otherwise false
3829     */
3830    public function hasCallback($name) { /* {{{ */
3831        if ($name && !empty($this->callbacks[$name]))
3832            return true;
3833        return false;
3834    } /* }}} */
3835
3836}