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