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