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