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