Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.64% covered (warning)
68.64%
1753 / 2554
43.55% covered (danger)
43.55%
81 / 186
CRAP
0.00% covered (danger)
0.00%
0 / 5
SeedDMS_Core_Document
72.10% covered (warning)
72.10%
907 / 1258
43.75% covered (danger)
43.75%
35 / 80
7295.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 clearCache
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 isType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSearchFields
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 getInstanceByData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getInstance
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 applyDecorators
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 getDir
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setName
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
14.67
 getComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setComment
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
14.67
 getKeywords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setKeywords
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
14.67
 hasCategory
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getCategories
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 setCategories
68.00% covered (warning)
68.00%
17 / 25
0.00% covered (danger)
0.00%
0 / 1
13.28
 addCategories
72.41% covered (warning)
72.41%
21 / 29
0.00% covered (danger)
0.00%
0 / 1
15.02
 removeCategories
60.00% covered (warning)
60.00%
12 / 20
0.00% covered (danger)
0.00%
0 / 1
14.18
 getDate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 isDescendant
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFolder
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFolder
71.43% covered (warning)
71.43%
25 / 35
0.00% covered (danger)
0.00%
0 / 1
16.94
 getOwner
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setOwner
63.64% covered (warning)
63.64%
14 / 22
0.00% covered (danger)
0.00%
0 / 1
14.81
 getDefaultAccess
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setDefaultAccess
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 inheritsAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInheritAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInheritAccess
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 expires
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getExpires
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setExpires
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 hasExpired
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 verifyLastestContentExpriry
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
9.89
 isLocked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLocked
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getLockingUser
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getSequence
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSequence
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 clearAccessList
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getAccessList
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 addAccess
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
8.10
 changeAccess
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
6.09
 removeAccess
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 getAccessMode
83.33% covered (warning)
83.33%
25 / 30
0.00% covered (danger)
0.00%
0 / 1
23.04
 getGroupAccessMode
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
7.12
 getNotifyList
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
10.02
 cleanNotifyList
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 addNotify
55.00% covered (warning)
55.00%
22 / 40
0.00% covered (danger)
0.00%
0 / 1
43.34
 removeNotify
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
8.51
 addContent
52.22% covered (warning)
52.22%
47 / 90
0.00% covered (danger)
0.00%
0 / 1
177.35
 replaceContent
76.60% covered (warning)
76.60%
36 / 47
0.00% covered (danger)
0.00%
0 / 1
19.28
 getContent
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
7.10
 getContentByVersion
71.43% covered (warning)
71.43%
15 / 21
0.00% covered (danger)
0.00%
0 / 1
13.82
 isLatestContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __getLatestContent
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getLatestContent
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
8.12
 _removeContent
40.70% covered (danger)
40.70%
35 / 86
0.00% covered (danger)
0.00%
0 / 1
231.42
 removeContent
62.96% covered (warning)
62.96%
17 / 27
0.00% covered (danger)
0.00%
0 / 1
23.96
 getDocumentLink
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
6.24
 getDocumentLinks
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
9
 getReverseDocumentLinks
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
8.01
 addDocumentLink
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
10.01
 removeDocumentLink
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 getDocumentFile
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 getDocumentFiles
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
9
 addDocumentFile
69.57% covered (warning)
69.57%
16 / 23
0.00% covered (danger)
0.00%
0 / 1
9.80
 removeDocumentFile
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
8.83
 remove
60.32% covered (warning)
60.32%
38 / 63
0.00% covered (danger)
0.00%
0 / 1
64.06
 __getApproversList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getReadAccessList
57.97% covered (warning)
57.97%
40 / 69
0.00% covered (danger)
0.00%
0 / 1
102.35
 getFolderList
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 repair
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 getUsedDiskSpace
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getTimeline
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
8.03
 transferToUser
62.50% covered (warning)
62.50%
15 / 24
0.00% covered (danger)
0.00%
0 / 1
7.90
SeedDMS_Core_DocumentContent
64.87% covered (warning)
64.87%
735 / 1133
23.88% covered (danger)
23.88%
16 / 67
12011.72
0.00% covered (danger)
0.00%
0 / 1
 verifyStatus
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
20
 __construct
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 isType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOriginalFileName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFileType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFileName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __getDir
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDate
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
6.13
 getFileSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFileSize
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 getChecksum
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setChecksum
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
3.10
 setFileType
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
5.20
 setMimeType
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
4.32
 setComment
43.75% covered (danger)
43.75%
7 / 16
0.00% covered (danger)
0.00%
0 / 1
19.39
 getStatus
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
6.04
 getStatusLog
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
5.01
 setStatus
91.18% covered (success)
91.18%
31 / 34
0.00% covered (danger)
0.00%
0 / 1
16.18
 rewriteStatusLog
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 getAccessMode
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
198.00
 getReviewers
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
7.01
 getReviewStatus
87.88% covered (warning)
87.88%
29 / 33
0.00% covered (danger)
0.00%
0 / 1
12.26
 getReviewLog
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 rewriteReviewLog
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
240
 getApprovers
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
7.01
 getApprovalStatus
87.88% covered (warning)
87.88%
29 / 33
0.00% covered (danger)
0.00%
0 / 1
12.26
 getApproveLog
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 rewriteApprovalLog
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
240
 addIndReviewer
84.38% covered (warning)
84.38%
27 / 32
0.00% covered (danger)
0.00%
0 / 1
18.10
 addGrpReviewer
86.11% covered (warning)
86.11%
31 / 36
0.00% covered (danger)
0.00%
0 / 1
21.07
 setReviewByInd
77.78% covered (warning)
77.78%
21 / 27
0.00% covered (danger)
0.00%
0 / 1
13.58
 removeReview
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
10.69
 setReviewByGrp
76.92% covered (warning)
76.92%
20 / 26
0.00% covered (danger)
0.00%
0 / 1
13.77
 addIndApprover
84.38% covered (warning)
84.38%
27 / 32
0.00% covered (danger)
0.00%
0 / 1
18.10
 addGrpApprover
86.11% covered (warning)
86.11%
31 / 36
0.00% covered (danger)
0.00%
0 / 1
21.07
 setApprovalByInd
77.78% covered (warning)
77.78%
21 / 27
0.00% covered (danger)
0.00%
0 / 1
13.58
 removeApproval
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
10.69
 setApprovalByGrp
76.92% covered (warning)
76.92%
20 / 26
0.00% covered (danger)
0.00%
0 / 1
13.77
 delIndReviewer
70.59% covered (warning)
70.59%
12 / 17
0.00% covered (danger)
0.00%
0 / 1
9.63
 delGrpReviewer
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 delIndApprover
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
9.37
 delGrpApprover
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 setWorkflowState
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 getWorkflowState
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
5.01
 setWorkflow
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
6.77
 getWorkflow
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 rewriteWorkflowLog
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 rewindWorkflow
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
3.14
 removeWorkflow
86.21% covered (warning)
86.21%
25 / 29
0.00% covered (danger)
0.00%
0 / 1
8.17
 getParentWorkflow
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 runSubWorkflow
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 returnFromSubWorkflow
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
90
 triggerWorkflowTransitionIsAllowed
66.67% covered (warning)
66.67%
18 / 27
0.00% covered (danger)
0.00%
0 / 1
19.26
 executeWorkflowTransitionIsAllowed
52.78% covered (warning)
52.78%
19 / 36
0.00% covered (danger)
0.00%
0 / 1
42.96
 triggerWorkflowTransition
65.00% covered (warning)
65.00%
13 / 20
0.00% covered (danger)
0.00%
0 / 1
12.47
 enterNextState
79.17% covered (warning)
79.17%
19 / 24
0.00% covered (danger)
0.00%
0 / 1
12.09
 getWorkflowLog
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
5.01
 getLastWorkflowLog
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 needsWorkflowAction
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 repair
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
132
SeedDMS_Core_DocumentLink
85.00% covered (warning)
85.00%
17 / 20
87.50% covered (warning)
87.50%
7 / 8
13.57
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 isType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getID
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isPublic
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAccessMode
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
SeedDMS_Core_DocumentFile
94.37% covered (success)
94.37%
67 / 71
90.91% covered (success)
90.91%
20 / 22
38.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 isType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getID
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserID
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setComment
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getDate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getDir
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFileType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOriginalFileName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setName
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setVersion
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 isPublic
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPublic
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getAccessMode
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
SeedDMS_Core_AddContentResultSet
22.41% covered (danger)
22.41%
13 / 58
33.33% covered (danger)
33.33%
3 / 9
865.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setDMS
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addReviewer
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
132
 addApprover
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
132
 setStatus
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 getStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReviewers
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
 getApprovers
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2declare(strict_types=1);
3/**
4 * Implementation of a document in the document management system
5 *
6 * @category   DMS
7 * @package    SeedDMS_Core
8 * @license    GPL2
9 * @author     Markus Westphal, Malcolm Cowe, Matteo Lucarelli,
10 *             Uwe Steinmann <uwe@steinmann.cx>
11 * @copyright  Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe,
12 *             2010 Matteo Lucarelli, 2010 Uwe Steinmann
13 * @version    Release: @package_version@
14 */
15
16/**
17 * The different states a document can be in
18 */
19/*
20 * Document is in review state. A document is in review state when
21 * it needs to be reviewed by a user or group.
22 */
23define("S_DRAFT_REV", 0);
24
25/*
26 * Document is in approval state. A document is in approval state when
27 * it needs to be approved by a user or group.
28 */
29define("S_DRAFT_APP", 1);
30
31/*
32 * Document is released. A document is in release state either when
33 * it needs no review or approval after uploaded or has been reviewed
34 * and/or approved.
35 */
36define("S_RELEASED",  2);
37
38/*
39 * Document is in workflow. A document is in workflow if a workflow
40 * has been started and has not reached a final state.
41 */
42define("S_IN_WORKFLOW",  3);
43
44/*
45 * Document was rejected. A document is in rejected state when
46 * the review failed or approval was not given.
47 */
48define("S_REJECTED", -1);
49
50/*
51 * Document is obsolete. A document can be obsoleted once it was
52 * released.
53 */
54define("S_OBSOLETE", -2);
55
56/*
57 * Document is expired. A document expires when the expiration date
58 * is reached
59 */
60define("S_EXPIRED",  -3);
61
62/*
63 * Lowest and highest status that may be set
64 */
65define("S_LOWEST_STATUS",  -3);
66define("S_HIGHEST_STATUS",  3);
67
68/**
69 * The different states a workflow log can be in. This is used in
70 * all tables tblDocumentXXXLog
71 */
72/*
73 * workflow is in a neutral status waiting for action of user
74 */
75define("S_LOG_WAITING",  0);
76
77/*
78 * workflow has been successful ended. The document content has been
79 * approved, reviewed, aknowledged or revised
80 */
81define("S_LOG_ACCEPTED",  1);
82
83/*
84 * workflow has been unsuccessful ended. The document content has been
85 * rejected
86 */
87define("S_LOG_REJECTED",  -1);
88
89/*
90 * user has been removed from workflow. This can be for different reasons
91 * 1. the user has been actively removed from the workflow, 2. the user has
92 * been deleted.
93 */
94define("S_LOG_USER_REMOVED",  -2);
95
96/*
97 * workflow is sleeping until reactivation. The workflow has been set up
98 * but not started. This is only valid for the revision workflow, which
99 * may run over and over again.
100 */
101define("S_LOG_SLEEPING",  -3);
102
103/**
104 * Class to represent a document in the document management system
105 *
106 * A document in SeedDMS is similar to a file in a regular file system.
107 * Documents may have any number of content elements
108 * ({@link SeedDMS_Core_DocumentContent}). These content elements are often
109 * called versions ordered in a timely manner. The most recent content element
110 * is the current version.
111 *
112 * Documents can be linked to other documents and can have attached files.
113 * The document content can be anything that can be stored in a regular
114 * file.
115 *
116 * @category   DMS
117 * @package    SeedDMS_Core
118 * @author     Markus Westphal, Malcolm Cowe, Matteo Lucarelli,
119 *             Uwe Steinmann <uwe@steinmann.cx>
120 * @copyright  Copyright (C) 2002-2005 Markus Westphal, 2006-2008 Malcolm Cowe,
121 *             2010 Matteo Lucarelli, 2010-2022 Uwe Steinmann
122 * @version    Release: @package_version@
123 */
124class SeedDMS_Core_Document extends SeedDMS_Core_Object { /* {{{ */
125    /**
126     * @var string name of document
127     */
128    protected $_name;
129
130    /**
131     * @var string comment of document
132     */
133    protected $_comment;
134
135    /**
136     * @var integer unix timestamp of creation date
137     */
138    protected $_date;
139
140    /**
141     * @var integer id of user who is the owner
142     */
143    protected $_ownerID;
144
145    /**
146     * @var object user who is the owner
147     */
148    protected $_owner;
149
150    /**
151     * @var integer id of folder this document belongs to
152     */
153    protected $_folderID;
154
155    /**
156     * @var object parent folder this document belongs to
157     */
158    protected $_parent;
159
160    /**
161     * @var integer timestamp of expiration date
162     */
163    protected $_expires;
164
165    /**
166     * @var boolean true if access is inherited, otherwise false
167     */
168    protected $_inheritAccess;
169
170    /**
171     * @var integer default access if access rights are not inherited
172     */
173    protected $_defaultAccess;
174
175    /**
176     * @var array list of notifications for users and groups
177     */
178    protected $_readAccessList;
179
180    /**
181     * @var array list of notifications for users and groups
182     */
183    public $_notifyList;
184
185    /**
186     * @var boolean true if document is locked, otherwise false
187     */
188    protected $_locked;
189
190    /**
191     * @var string list of keywords
192     */
193    protected $_keywords;
194
195    /**
196     * @var SeedDMS_Core_DocumentCategory[] list of categories
197     */
198    protected $_categories;
199
200    /**
201     * @var integer position of document within the parent folder
202     */
203    protected $_sequence;
204
205    /**
206     * @var SeedDMS_Core_DocumentContent temp. storage for latestcontent
207     */
208    protected $_latestContent;
209
210    /**
211     * @var array temp. storage for content
212     */
213    protected $_content;
214
215    /**
216     * @var SeedDMS_Core_Folder
217     */
218    protected $_folder;
219
220    /** @var array of SeedDMS_Core_UserAccess and SeedDMS_Core_GroupAccess */
221    protected $_accessList;
222
223    /**
224     * @var array
225     */
226    protected $_documentLinks;
227
228    /**
229     * @var array
230     */
231    protected $_documentFiles;
232
233    function __construct($id, $name, $comment, $date, $expires, $ownerID, $folderID, $inheritAccess, $defaultAccess, $locked, $keywords, $sequence) { /* {{{ */
234        parent::__construct($id);
235        $this->_name = $name;
236        $this->_comment = $comment;
237        $this->_date = $date;
238        $this->_expires = $expires;
239        $this->_ownerID = $ownerID;
240        $this->_folderID = $folderID;
241        $this->_inheritAccess = $inheritAccess ? true : false;
242        $this->_defaultAccess = $defaultAccess;
243        $this->_locked = ($locked == null || $locked == '' ? -1 : $locked);
244        $this->_keywords = $keywords;
245        $this->_sequence = $sequence;
246        $this->_categories = array();
247        $this->_notifyList = array();
248        $this->_latestContent = null;
249        $this->_content = null;
250        /* Cache */
251        $this->clearCache();
252    } /* }}} */
253
254    /**
255     * Clear cache of this instance.
256     *
257     * The result of some expensive database actions (e.g. get all subfolders
258     * or documents) will be saved in a class variable to speed up consecutive
259     * calls of the same method. If a second call of the same method shall not
260     * use the cache, then it must be cleared.
261     *
262     */
263    public function clearCache() { /* {{{ */
264        $this->_parent = null;
265        $this->_owner = null;
266        $this->_documentLinks = null;
267        $this->_documentFiles = null;
268        $this->_content = null;
269        $this->_accessList = null;
270        $this->_notifyList = null;
271    } /* }}} */
272
273    /**
274     * Check if this object is of type 'document'.
275     *
276     * @param string $type type of object
277     */
278    public function isType($type) { /* {{{ */
279        return $type == 'document';
280    } /* }}} */
281
282    /**
283     * Return an array of database fields which are used for searching
284     * a term entered in the database search form
285     *
286     * @param SeedDMS_Core_DMS $dms
287     * @param array $searchin integer list of search scopes (2=name, 3=comment,
288     * 4=attributes)
289     * @return array list of database fields
290     */
291    public static function getSearchFields($dms, $searchin) { /* {{{ */
292        $db = $dms->getDB();
293
294        $searchFields = array();
295        if (in_array(1, $searchin)) {
296            $searchFields[] = "`tblDocuments`.`keywords`";
297        }
298        if (in_array(2, $searchin)) {
299            $searchFields[] = "`tblDocuments`.`name`";
300        }
301        if (in_array(3, $searchin)) {
302            $searchFields[] = "`tblDocuments`.`comment`";
303            $searchFields[] = "`tblDocumentContent`.`comment`";
304        }
305        if (in_array(4, $searchin)) {
306            $searchFields[] = "`tblDocumentAttributes`.`value`";
307            $searchFields[] = "`tblDocumentContentAttributes`.`value`";
308        }
309        if (in_array(5, $searchin)) {
310            $searchFields[] = $db->castToText("`tblDocuments`.`id`");
311        }
312
313        return $searchFields;
314    } /* }}} */
315
316    /**
317     * Return a folder by its database record
318     *
319     * @param array $resArr array of folder data as returned by database
320     * @param SeedDMS_Core_DMS $dms
321     * @return SeedDMS_Core_Folder|bool instance of SeedDMS_Core_Folder if document exists
322     */
323    public static function getInstanceByData($resArr, $dms) { /* {{{ */
324        $classname = $dms->getClassname('document');
325        /** @var SeedDMS_Core_Document $document */
326        $document = new $classname($resArr["id"], $resArr["name"], $resArr["comment"], $resArr["date"], $resArr["expires"], $resArr["owner"], $resArr["folder"], $resArr["inheritAccess"], $resArr["defaultAccess"], $resArr['lock'], $resArr["keywords"], $resArr["sequence"]);
327        $document->setDMS($dms);
328        $document = $document->applyDecorators();
329        return $document;
330    } /* }}} */
331
332    /**
333     * Return an document by its id
334     *
335     * @param integer $id id of document
336     * @param SeedDMS_Core_DMS $dms
337     * @return bool|SeedDMS_Core_Document instance of SeedDMS_Core_Document if document exists, null
338     * if document does not exist, false in case of error
339     */
340    public static function getInstance($id, $dms) { /* {{{ */
341        $db = $dms->getDB();
342
343//        $queryStr = "SELECT * FROM `tblDocuments` WHERE `id` = " . (int) $id;
344        $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lock` FROM `tblDocuments` LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id` = `tblDocumentLocks`.`document` WHERE `id` = " . (int) $id;
345        if($dms->checkWithinRootDir)
346            $queryStr .= " AND `folderList` LIKE '%:".$dms->rootFolderID.":%'";
347        $resArr = $db->getResultArray($queryStr);
348        if (is_bool($resArr) && $resArr == false)
349            return false;
350        if (count($resArr) != 1)
351            return null;
352        $resArr = $resArr[0];
353
354        $resArr['lock'] = !$resArr['lock'] ? -1 : $resArr['lock'];
355
356        return self::getInstanceByData($resArr, $dms);
357    } /* }}} */
358
359    /**
360     * Apply decorators
361     *
362     * @return object final object after all decorators has been applied
363     */
364    function applyDecorators() { /* {{{ */
365        if($decorators = $this->_dms->getDecorators('document')) {
366            $s = $this;
367            foreach($decorators as $decorator) {
368                $s = new $decorator($s);
369            }
370            return $s;
371        } else {
372            return $this;
373        }
374    } /* }}} */
375
376    /**
377     * Return the directory of the document in the file system relativ
378     * to the contentDir
379     *
380     * @return string directory of document
381     */
382    function getDir() { /* {{{ */
383        if($this->_dms->maxDirID) {
384            $dirid = (int) (($this->_id-1) / $this->_dms->maxDirID) + 1;
385            return $dirid."/".$this->_id."/";
386        } else {
387            return $this->_id."/";
388        }
389    } /* }}} */
390
391    /**
392     * Return the name of the document
393     *
394     * @return string name of document
395     */
396    function getName() { return $this->_name; }
397
398    /**
399     * Set the name of the document
400     *
401     * @param $newName string new name of document
402     * @return bool
403     */
404    function setName($newName) { /* {{{ */
405        $db = $this->_dms->getDB();
406
407        /* Check if 'onPreSetName' callback is set */
408        if(isset($this->_dms->callbacks['onPreSetName'])) {
409            foreach($this->_dms->callbacks['onPreSetName'] as $callback) {
410                $ret = call_user_func($callback[0], $callback[1], $this, $newName);
411                if(is_bool($ret))
412                    return $ret;
413            }
414        }
415
416        $queryStr = "UPDATE `tblDocuments` SET `name` = ".$db->qstr($newName)." WHERE `id` = ". $this->_id;
417        if (!$db->getResult($queryStr))
418            return false;
419
420        $oldName = $this->_name;
421        $this->_name = $newName;
422
423        /* Check if 'onPostSetName' callback is set */
424        if(isset($this->_dms->callbacks['onPostSetName'])) {
425            foreach($this->_dms->callbacks['onPostSetName'] as $callback) {
426                $ret = call_user_func($callback[0], $callback[1], $this, $oldName);
427                if(is_bool($ret))
428                    return $ret;
429            }
430        }
431
432        return true;
433    } /* }}} */
434
435    /**
436     * Return the comment of the document
437     *
438     * @return string comment of document
439     */
440    function getComment() { return $this->_comment; }
441
442    /**
443     * Set the comment of the document
444     *
445     * @param $newComment string new comment of document
446     * @return bool
447     */
448    function setComment($newComment) { /* {{{ */
449        $db = $this->_dms->getDB();
450
451        /* Check if 'onPreSetComment' callback is set */
452        if(isset($this->_dms->callbacks['onPreSetComment'])) {
453            foreach($this->_dms->callbacks['onPreSetComment'] as $callback) {
454                $ret = call_user_func($callback[0], $callback[1], $this, $newComment);
455                if(is_bool($ret))
456                    return $ret;
457            }
458        }
459
460        $queryStr = "UPDATE `tblDocuments` SET `comment` = ".$db->qstr($newComment)." WHERE `id` = ". $this->_id;
461        if (!$db->getResult($queryStr))
462            return false;
463
464        $oldComment = $this->_comment;
465        $this->_comment = $newComment;
466
467        /* Check if 'onPostSetComment' callback is set */
468        if(isset($this->_dms->callbacks['onPostSetComment'])) {
469            foreach($this->_dms->callbacks['onPostSetComment'] as $callback) {
470                $ret = call_user_func($callback[0], $callback[1], $this, $oldComment);
471                if(is_bool($ret))
472                    return $ret;
473            }
474        }
475
476        return true;
477    } /* }}} */
478
479    /**
480     * @return string
481     */
482    function getKeywords() { return $this->_keywords; }
483
484    /**
485     * @param string $newKeywords
486     * @return bool
487     */
488    function setKeywords($newKeywords) { /* {{{ */
489        $db = $this->_dms->getDB();
490
491        /* Check if 'onPreSetKeywords' callback is set */
492        if(isset($this->_dms->callbacks['onPreSetKeywords'])) {
493            foreach($this->_dms->callbacks['onPreSetKeywords'] as $callback) {
494                $ret = call_user_func($callback[0], $callback[1], $this, $newKeywords);
495                if(is_bool($ret))
496                    return $ret;
497            }
498        }
499
500        $queryStr = "UPDATE `tblDocuments` SET `keywords` = ".$db->qstr($newKeywords)." WHERE `id` = ". $this->_id;
501        if (!$db->getResult($queryStr))
502            return false;
503
504        $oldKeywords = $this->_keywords;
505        $this->_keywords = $newKeywords;
506
507        /* Check if 'onPostSetKeywords' callback is set */
508        if(isset($this->_dms->callbacks['onPostSetKeywords'])) {
509            foreach($this->_dms->callbacks['onPostSetKeywords'] as $callback) {
510                $ret = call_user_func($callback[0], $callback[1], $this, $oldKeywords);
511                if(is_bool($ret))
512                    return $ret;
513            }
514        }
515
516        return true;
517    } /* }}} */
518
519    /**
520     * Check if document has a given category
521     *
522     * @param SeedDMS_Core_DocumentCategory $cat
523     * @return bool true if document has category, otherwise false
524     */
525    function hasCategory($cat) { /* {{{ */
526        $db = $this->_dms->getDB();
527
528        if(!$cat)
529            return false;
530
531        $queryStr = "SELECT * FROM `tblDocumentCategory` WHERE `documentID` = ".$this->_id." AND `categoryID`=".$cat->getId();
532        $resArr = $db->getResultArray($queryStr);
533        if (!$resArr)
534            return false;
535
536        return true;
537    } /* }}} */
538
539    /**
540     * Retrieve a list of all categories this document belongs to
541     *
542     * @return bool|SeedDMS_Core_DocumentCategory[]
543     */
544    function getCategories() { /* {{{ */
545        $db = $this->_dms->getDB();
546
547        if(!$this->_categories) {
548            $queryStr = "SELECT * FROM `tblCategory` WHERE `id` IN (SELECT `categoryID` FROM `tblDocumentCategory` WHERE `documentID` = ".$this->_id.")";
549            $resArr = $db->getResultArray($queryStr);
550            if (is_bool($resArr) && !$resArr)
551                return false;
552
553            $this->_categories = [];
554            foreach ($resArr as $row) {
555                $cat = new SeedDMS_Core_DocumentCategory($row['id'], $row['name']);
556                $cat->setDMS($this->_dms);
557                $this->_categories[] = $cat;
558            }
559        }
560        return $this->_categories;
561    } /* }}} */
562
563    /**
564     * Set a list of categories for the document
565     * This function will delete currently assigned categories and sets new
566     * categories.
567     *
568     * @param SeedDMS_Core_DocumentCategory[] $newCategories list of category objects
569     * @return bool
570     */
571    function setCategories($newCategories) { /* {{{ */
572        $db = $this->_dms->getDB();
573
574        /* Check if 'onPreSetCategories' callback is set */
575        if(isset($this->_dms->callbacks['onPreSetCategories'])) {
576            foreach($this->_dms->callbacks['onPreSetCategories'] as $callback) {
577                $ret = call_user_func($callback[0], $callback[1], $this, $newCategories);
578                if(is_bool($ret))
579                    return $ret;
580            }
581        }
582
583        $db->startTransaction();
584        $queryStr = "DELETE FROM `tblDocumentCategory` WHERE `documentID` = ". $this->_id;
585        if (!$db->getResult($queryStr)) {
586            $db->rollbackTransaction();
587            return false;
588        }
589
590        foreach($newCategories as $cat) {
591            $queryStr = "INSERT INTO `tblDocumentCategory` (`categoryID`, `documentID`) VALUES (". $cat->getId() .", ". $this->_id .")";
592            if (!$db->getResult($queryStr)) {
593                $db->rollbackTransaction();
594                return false;
595            }
596        }
597
598        $db->commitTransaction();
599
600        $oldCategories = $this->_categories;
601        $this->_categories = $newCategories;
602
603        /* Check if 'onPostSetCategories' callback is set */
604        if(isset($this->_dms->callbacks['onPostSetCategories'])) {
605            foreach($this->_dms->callbacks['onPostSetCategories'] as $callback) {
606                $ret = call_user_func($callback[0], $callback[1], $this, $oldCategories);
607                if(is_bool($ret))
608                    return $ret;
609            }
610        }
611
612        return true;
613    } /* }}} */
614
615    /**
616     * Add a list of categories to the document
617     * This function will add a list of new categories to the document.
618     *
619     * @param array $newCategories list of category objects
620     */
621    function addCategories($newCategories) { /* {{{ */
622        $db = $this->_dms->getDB();
623
624        /* Check if 'onPreAddCategories' callback is set */
625        if(isset($this->_dms->callbacks['onPreAddCategories'])) {
626            foreach($this->_dms->callbacks['onPreAddCategories'] as $callback) {
627                $ret = call_user_func($callback[0], $callback[1], $this, $newCategories);
628                if(is_bool($ret))
629                    return $ret;
630            }
631        }
632
633        if(!$this->_categories)
634            $this->getCategories();
635
636        $catids = array();
637        foreach($this->_categories as $cat)
638            $catids[] = $cat->getID();
639
640        $db->startTransaction();
641        $ncat = array(); // Array containing actually added new categories
642        foreach($newCategories as $cat) {
643            if(!in_array($cat->getID(), $catids)) {
644                $queryStr = "INSERT INTO `tblDocumentCategory` (`categoryID`, `documentID`) VALUES (". $cat->getId() .", ". $this->_id .")";
645                if (!$db->getResult($queryStr)) {
646                    $db->rollbackTransaction();
647                    return false;
648                }
649                $ncat[] = $cat;
650            }
651        }
652        $db->commitTransaction();
653
654        $oldCategories = $this->_categories;
655        $this->_categories = array_merge($this->_categories, $ncat);
656
657        /* Check if 'onPostAddCategories' callback is set */
658        if(isset($this->_dms->callbacks['onPostAddCategories'])) {
659            foreach($this->_dms->callbacks['onPostAddCategories'] as $callback) {
660                $ret = call_user_func($callback[0], $callback[1], $this, $oldCategories);
661                if(is_bool($ret))
662                    return $ret;
663            }
664        }
665
666        return true;
667    } /* }}} */
668
669    /**
670     * Remove a list of categories from the document
671     * This function will remove a list of assigned categories to the document.
672     *
673     * @param array $newCategories list of category objects
674     */
675    function removeCategories($categories) { /* {{{ */
676        $db = $this->_dms->getDB();
677
678        /* Check if 'onPreRemoveCategories' callback is set */
679        if(isset($this->_dms->callbacks['onPreRemoveCategories'])) {
680            foreach($this->_dms->callbacks['onPreRemoveCategories'] as $callback) {
681                $ret = call_user_func($callback[0], $callback[1], $this, $categories);
682                if(is_bool($ret))
683                    return $ret;
684            }
685        }
686
687        $catids = array();
688        foreach($categories as $cat)
689            $catids[] = $cat->getID();
690
691        $queryStr = "DELETE FROM `tblDocumentCategory` WHERE `documentID` = ". $this->_id ." AND `categoryID` IN (".implode(',', $catids).")";
692        if (!$db->getResult($queryStr)) {
693            return false;
694        }
695
696        $oldCategories = $this->_categories;
697        $this->_categories = null;
698
699        /* Check if 'onPostRemoveCategories' callback is set */
700        if(isset($this->_dms->callbacks['onPostRemoveCategories'])) {
701            foreach($this->_dms->callbacks['onPostRemoveCategories'] as $callback) {
702                $ret = call_user_func($callback[0], $callback[1], $this, $oldCategories);
703                if(is_bool($ret))
704                    return $ret;
705            }
706        }
707
708        return true;
709    } /* }}} */
710
711    /**
712     * Return creation date of the document
713     *
714     * @return integer unix timestamp of creation date
715     */
716    function getDate() { /* {{{ */
717        return $this->_date;
718    } /* }}} */
719
720    /**
721     * Set creation date of the document
722     *
723     * @param integer $date timestamp of creation date. If false then set it
724     * to the current timestamp
725     * @return boolean true on success
726     */
727    function setDate($date) { /* {{{ */
728        $db = $this->_dms->getDB();
729
730        if(!$date)
731            $date = time();
732        else {
733            if(!is_numeric($date))
734                return false;
735        }
736
737        $queryStr = "UPDATE `tblDocuments` SET `date` = " . (int) $date . " WHERE `id` = ". $this->_id;
738        if (!$db->getResult($queryStr))
739            return false;
740        $this->_date = $date;
741        return true;
742    } /* }}} */
743
744    /**
745     * Check, if this document is a child of a given folder
746     *
747     * @param object $folder parent folder
748     * @return boolean true if document is a direct child of the given folder
749     */
750    function isDescendant($folder) { /* {{{ */
751        /* First check if the parent folder is folder looking for */
752        if ($this->getFolder()->getID() == $folder->getID())
753            return true;
754        /* Second, check for the parent folder of this document to be
755         * below the given folder
756         */
757        if($this->getFolder()->isDescendant($folder))
758            return true;
759        return false;
760    } /* }}} */
761
762    /**
763     * Return the parent folder of the document
764     *
765     * @return SeedDMS_Core_Folder parent folder
766     */
767    function getParent() { /* {{{ */
768        return $this->getFolder();
769    } /* }}} */
770
771    function getFolder() { /* {{{ */
772        if (!isset($this->_folder))
773            $this->_folder = $this->_dms->getFolder($this->_folderID);
774        return $this->_folder;
775    } /* }}} */
776
777    /**
778     * Set folder of a document
779     *
780     * This function basically moves a document from a folder to another
781     * folder.
782     *
783     * @param SeedDMS_Core_Folder $newFolder
784     * @return boolean false in case of an error, otherwise true
785     */
786    function setParent($newFolder) { /* {{{ */
787        return $this->setFolder($newFolder);
788    } /* }}} */
789
790    /**
791     * Set folder of a document
792     *
793     * This function basically moves a document from a folder to another
794     * folder.
795     *
796     * @param SeedDMS_Core_Folder $newFolder
797     * @return boolean false in case of an error, otherwise true
798     */
799    function setFolder($newFolder) { /* {{{ */
800        $db = $this->_dms->getDB();
801
802        if(!$newFolder)
803            return false;
804
805        if(!$newFolder->isType('folder'))
806            return false;
807
808        /* Check if 'onPreSetFolder' callback is set */
809        if(isset($this->_dms->callbacks['onPreSetFolder'])) {
810            foreach($this->_dms->callbacks['onPreSetFolder'] as $callback) {
811                $ret = call_user_func($callback[0], $callback[1], $this, $newFolder);
812                if(is_bool($ret))
813                    return $ret;
814            }
815        }
816
817        $db->startTransaction();
818
819        $queryStr = "UPDATE `tblDocuments` SET `folder` = " . $newFolder->getID() . " WHERE `id` = ". $this->_id;
820        if (!$db->getResult($queryStr)) {
821            $db->rollbackTransaction();
822            return false;
823        }
824
825        // Make sure that the folder search path is also updated.
826        $path = $newFolder->getPath();
827        $flist = "";
828        /** @var SeedDMS_Core_Folder[] $path */
829        foreach ($path as $f) {
830            $flist .= ":".$f->getID();
831        }
832        if (strlen($flist)>1) {
833            $flist .= ":";
834        }
835        $queryStr = "UPDATE `tblDocuments` SET `folderList` = '" . $flist . "' WHERE `id` = ". $this->_id;
836        if (!$db->getResult($queryStr)) {
837            $db->rollbackTransaction();
838            return false;
839        }
840
841        $db->commitTransaction();
842
843        $oldFolder = $this->_folder;
844        $this->_folderID = $newFolder->getID();
845        $this->_folder = $newFolder;
846
847        /* Check if 'onPostSetFolder' callback is set */
848        if(isset($this->_dms->callbacks['onPostSetFolder'])) {
849            foreach($this->_dms->callbacks['onPostSetFolder'] as $callback) {
850                $ret = call_user_func($callback[0], $callback[1], $this, $oldFolder);
851                if(is_bool($ret))
852                    return $ret;
853            }
854        }
855
856        return true;
857    } /* }}} */
858
859    /**
860     * Return owner of document
861     *
862     * @return SeedDMS_Core_User owner of document as an instance of {@link SeedDMS_Core_User}
863     */
864    function getOwner() { /* {{{ */
865        if (!isset($this->_owner))
866            $this->_owner = $this->_dms->getUser($this->_ownerID);
867        return $this->_owner;
868    } /* }}} */
869
870    /**
871     * Set owner of a document
872     *
873     * @param SeedDMS_Core_User $newOwner new owner
874     * @return boolean true if successful otherwise false
875     */
876    function setOwner($newOwner) { /* {{{ */
877        $db = $this->_dms->getDB();
878
879        if(!$newOwner)
880            return false;
881
882        if(!$newOwner->isType('user'))
883            return false;
884
885        /* Check if 'onPreSetOwner' callback is set */
886        if(isset($this->_dms->callbacks['onPreSetOwner'])) {
887            foreach($this->_dms->callbacks['onPreSetOwner'] as $callback) {
888                $ret = call_user_func($callback[0], $callback[1], $this, $newOwner);
889                if(is_bool($ret))
890                    return $ret;
891            }
892        }
893
894        $queryStr = "UPDATE `tblDocuments` set `owner` = " . $newOwner->getID() . " WHERE `id` = " . $this->_id;
895        if (!$db->getResult($queryStr))
896            return false;
897
898        $oldOwner = $this->_owner;
899        $this->_ownerID = $newOwner->getID();
900        $this->_owner = $newOwner;
901
902        /* Check if 'onPostSetOwner' callback is set */
903        if(isset($this->_dms->callbacks['onPostSetOwner'])) {
904            foreach($this->_dms->callbacks['onPostSetOwner'] as $callback) {
905                $ret = call_user_func($callback[0], $callback[1], $this, $oldOwner);
906                if(is_bool($ret))
907                    return $ret;
908            }
909        }
910
911        return true;
912    } /* }}} */
913
914    /**
915     * @return bool|int
916     */
917    function getDefaultAccess() { /* {{{ */
918        if ($this->inheritsAccess()) {
919            $res = $this->getFolder();
920            if (!$res) return false;
921            return $this->_folder->getDefaultAccess();
922        }
923        return $this->_defaultAccess;
924    } /* }}} */
925
926    /**
927     * Set default access mode
928     *
929     * This method sets the default access mode and also removes all notifiers which
930     * will not have read access anymore. Setting a default access mode will only
931     * have an immediate effect if the access rights are not inherited, otherwise
932     * it just updates the database record of the document and once the
933     * inheritance is turn off the default access mode will take effect.
934     *
935     * @param integer     $mode    access mode
936     * @param bool|string $noclean set to true if notifier list shall not be clean up
937     *
938     * @return bool
939     */
940    function setDefaultAccess($mode, $noclean=false) { /* {{{ */
941        $db = $this->_dms->getDB();
942
943        if($mode < M_LOWEST_RIGHT || $mode > M_HIGHEST_RIGHT)
944            return false;
945
946        $queryStr = "UPDATE `tblDocuments` set `defaultAccess` = " . (int) $mode . " WHERE `id` = " . $this->_id;
947        if (!$db->getResult($queryStr))
948            return false;
949
950        $this->_defaultAccess = $mode;
951
952        /* Setting the default access mode does not have any effect if access
953         * is still inherited. In that case there is no need to clean the
954         * notification list.
955         */
956        if(!$noclean && !$this->_inheritAccess)
957            $this->cleanNotifyList();
958
959        return true;
960    } /* }}} */
961
962    /**
963     * @return bool
964     */
965    function inheritsAccess() { return $this->_inheritAccess; }
966
967    /**
968     * This is supposed to be a replacement for inheritsAccess()
969     *
970     * @return bool
971     */
972    function getInheritAccess() { return $this->_inheritAccess; }
973
974    /**
975     * Set inherited access mode
976     * Setting inherited access mode will set or unset the internal flag which
977     * controls if the access mode is inherited from the parent folder or not.
978     * It will not modify the
979     * access control list for the current object. It will remove all
980     * notifications of users which do not even have read access anymore
981     * after setting or unsetting inherited access.
982     *
983     * @param boolean $inheritAccess set to true for setting and false for
984     *        unsetting inherited access mode
985     * @param boolean $noclean set to true if notifier list shall not be clean up
986     * @return boolean true if operation was successful otherwise false
987     */
988    function setInheritAccess($inheritAccess, $noclean=false) { /* {{{ */
989        $db = $this->_dms->getDB();
990
991        $queryStr = "UPDATE `tblDocuments` SET `inheritAccess` = " . ($inheritAccess ? "1" : "0") . " WHERE `id` = " . $this->_id;
992        if (!$db->getResult($queryStr))
993            return false;
994
995        $this->_inheritAccess = ($inheritAccess ? true : false);
996
997        if(!$noclean)
998            $this->cleanNotifyList();
999
1000        return true;
1001    } /* }}} */
1002
1003    /**
1004     * Check if document expires
1005     *
1006     * @return boolean true if document has expiration date set, otherwise false
1007     */
1008    function expires() { /* {{{ */
1009        if (intval($this->_expires) == 0)
1010            return false;
1011        else
1012            return true;
1013    } /* }}} */
1014
1015    /**
1016     * Get expiration time of document
1017     *
1018     * @return integer/boolean expiration date as unix timestamp or false
1019     */
1020    function getExpires() { /* {{{ */
1021        if (intval($this->_expires) == 0)
1022            return false;
1023        else
1024            return $this->_expires;
1025    } /* }}} */
1026
1027    /**
1028     * Set expiration date as unix timestamp
1029     *
1030     * @param integer $expires unix timestamp of expiration date
1031     * @return bool
1032     */
1033    function setExpires($expires) { /* {{{ */
1034        $db = $this->_dms->getDB();
1035
1036        $expires = (!$expires) ? 0 : $expires;
1037
1038        if ($expires == $this->_expires) {
1039            // No change is necessary.
1040            return true;
1041        }
1042
1043        $queryStr = "UPDATE `tblDocuments` SET `expires` = " . (int) $expires . " WHERE `id` = " . $this->_id;
1044        if (!$db->getResult($queryStr))
1045            return false;
1046
1047        $this->_expires = $expires;
1048        return true;
1049    } /* }}} */
1050
1051    /**
1052     * Check if the document has expired
1053     *
1054     * The method expects to database field 'expired' to hold the timestamp
1055     * of the start of day at which end the document expires. The document will
1056     * expire if that day is over. Hence, a document will *not* 
1057     * be expired during the day of expiration but at the end of that day
1058     *
1059     * @return boolean true if document has expired otherwise false
1060     */
1061    function hasExpired() { /* {{{ */
1062        if (intval($this->_expires) == 0) return false;
1063        if (time()>=$this->_expires+24*60*60) return true;
1064        return false;
1065    } /* }}} */
1066
1067    /**
1068     * Check if the document has expired and set the status accordingly
1069     * It will also recalculate the status if the current status is
1070     * set to S_EXPIRED but the document isn't actually expired.
1071     * The method will update the document status log database table
1072     * if needed.
1073     * FIXME: some left over reviewers/approvers are in the way if
1074     * no workflow is set and traditional workflow mode is on. In that
1075     * case the status is set to S_DRAFT_REV or S_DRAFT_APP
1076     *
1077     * @return boolean true if status has changed
1078     */
1079    function verifyLastestContentExpriry(){ /* {{{ */
1080        $lc=$this->getLatestContent();
1081        if($lc) {
1082            $st=$lc->getStatus();
1083
1084            if (($st["status"]==S_DRAFT_REV || $st["status"]==S_DRAFT_APP || $st["status"]==S_IN_WORKFLOW || $st["status"]==S_RELEASED) && $this->hasExpired()){
1085                return $lc->setStatus(S_EXPIRED,"", $this->getOwner());
1086            }
1087            elseif ($st["status"]==S_EXPIRED && !$this->hasExpired() ){
1088                $lc->verifyStatus(true, $this->getOwner());
1089                return true;
1090            }
1091        }
1092        return false;
1093    } /* }}} */
1094
1095    /**
1096     * Check if document is locked
1097     *
1098     * @return boolean true if locked otherwise false
1099     */
1100    function isLocked() { return $this->_locked != -1; }
1101
1102    /**
1103     * Lock or unlock document
1104     *
1105     * @param SeedDMS_Core_User|bool $falseOrUser user object for locking or false for unlocking
1106     * @return boolean true if operation was successful otherwise false
1107     */
1108    function setLocked($falseOrUser) { /* {{{ */
1109        $db = $this->_dms->getDB();
1110
1111        $lockUserID = -1;
1112        if (is_bool($falseOrUser) && !$falseOrUser) {
1113            $queryStr = "DELETE FROM `tblDocumentLocks` WHERE `document` = ".$this->_id;
1114        }
1115        else if (is_object($falseOrUser)) {
1116            $queryStr = "INSERT INTO `tblDocumentLocks` (`document`, `userID`) VALUES (".$this->_id.", ".$falseOrUser->getID().")";
1117            $lockUserID = $falseOrUser->getID();
1118        }
1119        else {
1120            return false;
1121        }
1122        if (!$db->getResult($queryStr)) {
1123            return false;
1124        }
1125        unset($this->_lockingUser);
1126        $this->_locked = $lockUserID;
1127        return true;
1128    } /* }}} */
1129
1130    /**
1131     * Get the user currently locking the document
1132     *
1133     * @return SeedDMS_Core_User|bool user have a lock
1134     */
1135    function getLockingUser() { /* {{{ */
1136        if (!$this->isLocked())
1137            return false;
1138
1139        if (!isset($this->_lockingUser))
1140            $this->_lockingUser = $this->_dms->getUser($this->_locked);
1141        return $this->_lockingUser;
1142    } /* }}} */
1143
1144    /**
1145     * @return float
1146     */
1147    function getSequence() { return $this->_sequence; }
1148
1149    /**
1150     * @param float $seq
1151     * @return bool
1152     */
1153    function setSequence($seq) { /* {{{ */
1154        $db = $this->_dms->getDB();
1155
1156        $queryStr = "UPDATE `tblDocuments` SET `sequence` = " . $seq . " WHERE `id` = " . $this->_id;
1157        if (!$db->getResult($queryStr))
1158            return false;
1159
1160        $this->_sequence = $seq;
1161        return true;
1162    } /* }}} */
1163
1164    /**
1165     * Delete all entries for this document from the access control list
1166     *
1167     * @param boolean $noclean set to true if notifier list shall not be clean up
1168     * @return boolean true if operation was successful otherwise false
1169     */
1170    function clearAccessList($noclean=false) { /* {{{ */
1171        $db = $this->_dms->getDB();
1172
1173        $queryStr = "DELETE FROM `tblACLs` WHERE `targetType` = " . T_DOCUMENT . " AND `target` = " . $this->_id;
1174        if (!$db->getResult($queryStr))
1175            return false;
1176
1177        unset($this->_accessList);
1178
1179        if(!$noclean)
1180            $this->cleanNotifyList();
1181
1182        return true;
1183    } /* }}} */
1184
1185    /**
1186     * Returns a list of access privileges
1187     *
1188     * If the document inherits the access privileges from the parent folder
1189     * those will be returned.
1190     * $mode and $op can be set to restrict the list of returned access
1191     * privileges. If $mode is set to M_ANY no restriction will apply
1192     * regardless of the value of $op. The returned array contains a list
1193     * of {@link SeedDMS_Core_UserAccess} and
1194     * {@link SeedDMS_Core_GroupAccess} objects. Even if the document
1195     * has no access list the returned array contains the two elements
1196     * 'users' and 'groups' which are than empty. The methode returns false
1197     * if the function fails.
1198     *
1199     * @param int $mode access mode (defaults to M_ANY)
1200     * @param int|string $op operation (defaults to O_EQ)
1201     * @return bool|array
1202     */
1203    function getAccessList($mode = M_ANY, $op = O_EQ) { /* {{{ */
1204        $db = $this->_dms->getDB();
1205
1206        if ($this->inheritsAccess()) {
1207            $res = $this->getFolder();
1208            if (!$res) return false;
1209            return $this->_folder->getAccessList($mode, $op);
1210        }
1211
1212        if (!isset($this->_accessList[$mode])) {
1213            if ($op!=O_GTEQ && $op!=O_LTEQ && $op!=O_EQ) {
1214                return false;
1215            }
1216            $modeStr = "";
1217            if ($mode!=M_ANY) {
1218                $modeStr = " AND `mode`".$op.(int)$mode;
1219            }
1220            $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_DOCUMENT.
1221                " AND `target` = " . $this->_id . $modeStr . " ORDER BY `targetType`";
1222            $resArr = $db->getResultArray($queryStr);
1223            if (is_bool($resArr) && !$resArr)
1224                return false;
1225
1226            $this->_accessList[$mode] = array("groups" => array(), "users" => array());
1227            foreach ($resArr as $row) {
1228                if ($row["userID"] != -1)
1229                    array_push($this->_accessList[$mode]["users"], new SeedDMS_Core_UserAccess($this->_dms->getUser($row["userID"]), (int) $row["mode"]));
1230                else //if ($row["groupID"] != -1)
1231                    array_push($this->_accessList[$mode]["groups"], new SeedDMS_Core_GroupAccess($this->_dms->getGroup($row["groupID"]), (int) $row["mode"]));
1232            }
1233        }
1234
1235        return $this->_accessList[$mode];
1236    } /* }}} */
1237
1238    /**
1239     * Add access right to folder
1240     * This function may change in the future. Instead of passing a flag
1241     * and a user/group id a user or group object will be expected.
1242     * Starting with version 5.1.25 this method will first check if there
1243     * is already an access right for the user/group.
1244     *
1245     * @param integer $mode access mode
1246     * @param integer $userOrGroupID id of user or group
1247     * @param integer $isUser set to 1 if $userOrGroupID is the id of a
1248     *        user
1249     * @return bool
1250     */
1251    function addAccess($mode, $userOrGroupID, $isUser) { /* {{{ */
1252        $db = $this->_dms->getDB();
1253
1254        if($mode < M_NONE || $mode > M_ALL)
1255            return false;
1256
1257        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1258
1259        /* Adding a second access right will return false */
1260        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_DOCUMENT.
1261                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ".$userOrGroupID;
1262        $resArr = $db->getResultArray($queryStr);
1263        if (is_bool($resArr) || $resArr)
1264                return false;
1265
1266        $queryStr = "INSERT INTO `tblACLs` (`target`, `targetType`, ".$userOrGroup.", `mode`) VALUES
1267                    (".$this->_id.", ".T_DOCUMENT.", " . (int) $userOrGroupID . ", " .(int) $mode. ")";
1268        if (!$db->getResult($queryStr))
1269            return false;
1270
1271        unset($this->_accessList);
1272
1273        // Update the notify list, if necessary.
1274        if ($mode == M_NONE) {
1275            $this->removeNotify($userOrGroupID, $isUser);
1276        }
1277
1278        return true;
1279    } /* }}} */
1280
1281    /**
1282     * Change access right of document
1283     * This function may change in the future. Instead of passing the a flag
1284     * and a user/group id a user or group object will be expected.
1285     *
1286     * @param integer $newMode access mode
1287     * @param integer $userOrGroupID id of user or group
1288     * @param integer $isUser set to 1 if $userOrGroupID is the id of a
1289     *        user
1290     * @return bool
1291     */
1292    function changeAccess($newMode, $userOrGroupID, $isUser) { /* {{{ */
1293        $db = $this->_dms->getDB();
1294
1295        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1296
1297        /* Get the old access right */
1298        $queryStr = "SELECT * FROM `tblACLs` WHERE `targetType` = ".T_DOCUMENT.
1299                " AND `target` = " . $this->_id . " AND ". $userOrGroup . " = ". (int) $userOrGroupID;
1300        $resArr = $db->getResultArray($queryStr);
1301        if (is_bool($resArr) && $resArr == false)
1302            return false;
1303
1304        $oldmode = $resArr[0]['mode'];
1305
1306        $queryStr = "UPDATE `tblACLs` SET `mode` = " . (int) $newMode . " WHERE `targetType` = ".T_DOCUMENT." AND `target` = " . $this->_id . " AND " . $userOrGroup . " = " . (int) $userOrGroupID;
1307        if (!$db->getResult($queryStr))
1308            return false;
1309
1310        unset($this->_accessList);
1311
1312        // Update the notify list, if necessary.
1313        if ($newMode == M_NONE) {
1314            $this->removeNotify($userOrGroupID, $isUser);
1315        }
1316
1317        return $oldmode;
1318    } /* }}} */
1319
1320    /**
1321     * Remove access rights for a user or group
1322     *
1323     * @param integer $userOrGroupID ID of user or group
1324     * @param boolean $isUser true if $userOrGroupID is a user id, false if it
1325     *        is a group id.
1326     * @return boolean true on success, otherwise false
1327     */
1328    function removeAccess($userOrGroupID, $isUser) { /* {{{ */
1329        $db = $this->_dms->getDB();
1330
1331        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1332
1333        $queryStr = "DELETE FROM `tblACLs` WHERE `targetType` = ".T_DOCUMENT." AND `target` = ".$this->_id." AND ".$userOrGroup." = " . (int) $userOrGroupID;
1334        if (!$db->getResult($queryStr))
1335            return false;
1336
1337        unset($this->_accessList);
1338
1339        // Update the notify list, if the user looses access rights.
1340        $mode = ($isUser ? $this->getAccessMode($this->_dms->getUser($userOrGroupID)) : $this->getGroupAccessMode($this->_dms->getGroup($userOrGroupID)));
1341        if ($mode == M_NONE) {
1342            $this->removeNotify($userOrGroupID, $isUser);
1343        }
1344
1345        return true;
1346    } /* }}} */
1347
1348    /**
1349     * Returns the greatest access privilege for a given user
1350     *
1351     * This function returns the access mode for a given user. An administrator
1352     * and the owner of the folder has unrestricted access. A guest user has
1353     * read only access or no access if access rights are further limited
1354     * by access control lists. All other users have access rights according
1355     * to the access control lists or the default access. This function will
1356     * recursive check for access rights of parent folders if access rights
1357     * are inherited.
1358     *
1359     * The function searches the access control list for entries of
1360     * user $user. If it finds more than one entry it will return the
1361     * one allowing the greatest privileges, but user rights will always
1362     * precede group rights. If there is no entry in the
1363     * access control list, it will return the default access mode.
1364     * The function takes inherited access rights into account.
1365     * For a list of possible access rights see @file inc.AccessUtils.php
1366     *
1367     * Having access on a document does not necessarily mean the document
1368     * content is accessible too. Accessing the content is checked by
1369     * {@link SeedDMS_Core_DocumentContent::getAccessMode()} which calls
1370     * a callback function defined by the application. If the callback
1371     * function is not set, access on the content is always granted.
1372     *
1373     * Before checking the access in the method itself a callback 'onCheckAccessDocument'
1374     * is called. If it returns a value > 0, then this will be returned by this
1375     * method without any further checks. The optional paramater $context
1376     * will be passed as a third parameter to the callback. It contains
1377     * the operation for which the access mode is retrieved. It is for example
1378     * set to 'removeDocument' if the access mode is used to check for sufficient
1379     * permission on deleting a document.
1380     *
1381     * @param $user object instance of class SeedDMS_Core_User
1382     * @param string $context context in which the access mode is requested
1383     * @return integer access mode
1384     */
1385    function getAccessMode($user, $context='') { /* {{{ */
1386        if(!$user)
1387            return M_NONE;
1388
1389        /* Check if 'onCheckAccessDocument' callback is set */
1390        if(isset($this->_dms->callbacks['onCheckAccessDocument'])) {
1391            foreach($this->_dms->callbacks['onCheckAccessDocument'] as $callback) {
1392                if(($ret = call_user_func($callback[0], $callback[1], $this, $user, $context)) > 0) {
1393                    return $ret;
1394                }
1395            }
1396        }
1397
1398        /* Administrators have unrestricted access */
1399        if ($user->isAdmin()) return M_ALL;
1400
1401        /* The owner of the document has unrestricted access */
1402        if ($user->getID() == $this->_ownerID) return M_ALL;
1403
1404        /* Check ACLs */
1405        $accessList = $this->getAccessList();
1406        if (!$accessList) return false;
1407
1408        /** @var SeedDMS_Core_UserAccess $userAccess */
1409        foreach ($accessList["users"] as $userAccess) {
1410            if ($userAccess->getUserID() == $user->getID()) {
1411                $mode = $userAccess->getMode();
1412                if ($user->isGuest()) {
1413                    if ($mode >= M_READ) $mode = M_READ;
1414                }
1415                return $mode;
1416            }
1417        }
1418
1419        /* Get the highest right defined by a group */
1420        if($accessList['groups']) {
1421            $mode = 0;
1422            /** @var SeedDMS_Core_GroupAccess $groupAccess */
1423            foreach ($accessList["groups"] as $groupAccess) {
1424                if ($user->isMemberOfGroup($groupAccess->getGroup())) {
1425                    if ($groupAccess->getMode() > $mode)
1426                        $mode = $groupAccess->getMode();
1427                }
1428            }
1429            if($mode) {
1430                if ($user->isGuest()) {
1431                    if ($mode >= M_READ) $mode = M_READ;
1432                }
1433                return $mode;
1434            }
1435        }
1436
1437        $mode = $this->getDefaultAccess();
1438        if ($user->isGuest()) {
1439            if ($mode >= M_READ) $mode = M_READ;
1440        }
1441        return $mode;
1442    } /* }}} */
1443
1444    /**
1445     * Returns the greatest access privilege for a given group
1446     *
1447     * This function searches the access control list for entries of
1448     * group $group. If it finds more than one entry it will return the
1449     * one allowing the greatest privileges. If there is no entry in the
1450     * access control list, it will return the default access mode.
1451     * The function takes inherited access rights into account.
1452     * For a list of possible access rights see @file inc.AccessUtils.php
1453     *
1454     * @param SeedDMS_Core_Group $group object instance of class SeedDMS_Core_Group
1455     * @return integer access mode
1456     */
1457    function getGroupAccessMode($group) { /* {{{ */
1458        $highestPrivileged = M_NONE;
1459
1460        //ACLs durchforsten
1461        $foundInACL = false;
1462        $accessList = $this->getAccessList();
1463        if (!$accessList)
1464            return false;
1465
1466        /** @var SeedDMS_Core_GroupAccess $groupAccess */
1467        foreach ($accessList["groups"] as $groupAccess) {
1468            if ($groupAccess->getGroupID() == $group->getID()) {
1469                $foundInACL = true;
1470                if ($groupAccess->getMode() > $highestPrivileged)
1471                    $highestPrivileged = $groupAccess->getMode();
1472                if ($highestPrivileged == M_ALL) // max access right -> skip the rest
1473                    return $highestPrivileged;
1474            }
1475        }
1476
1477        if ($foundInACL)
1478            return $highestPrivileged;
1479
1480        //Standard-Berechtigung verwenden
1481        return $this->getDefaultAccess();
1482    } /* }}} */
1483
1484    /**
1485     * Returns a list of all notifications
1486     *
1487     * The returned list has two elements called 'users' and 'groups'. Each one
1488     * is an array itself countaining objects of class SeedDMS_Core_User and
1489     * SeedDMS_Core_Group.
1490     *
1491     * @param integer $type type of notification (not yet used)
1492     * @param bool $incdisabled set to true if disabled user shall be included
1493     * @return array|bool
1494     */
1495    function getNotifyList($type=0, $incdisabled=false) { /* {{{ */
1496        if (empty($this->_notifyList)) {
1497            $db = $this->_dms->getDB();
1498
1499            $queryStr ="SELECT * FROM `tblNotify` WHERE `targetType` = " . T_DOCUMENT . " AND `target` = " . $this->_id;
1500            $resArr = $db->getResultArray($queryStr);
1501            if (is_bool($resArr) && $resArr == false)
1502                return false;
1503
1504            $this->_notifyList = array("groups" => array(), "users" => array());
1505            foreach ($resArr as $row)
1506            {
1507                if ($row["userID"] != -1) {
1508                    $u = $this->_dms->getUser($row["userID"]);
1509                    if($u && (!$u->isDisabled() || $incdisabled))
1510                        array_push($this->_notifyList["users"], $u);
1511                } else { //if ($row["groupID"] != -1)
1512                    $g = $this->_dms->getGroup($row["groupID"]);
1513                    if($g)
1514                        array_push($this->_notifyList["groups"], $g);
1515                }
1516            }
1517        }
1518        return $this->_notifyList;
1519    } /* }}} */
1520
1521    /**
1522     * Make sure only users/groups with read access are in the notify list
1523     *
1524     */
1525    function cleanNotifyList() { /* {{{ */
1526        // If any of the notification subscribers no longer have read access,
1527        // remove their subscription.
1528        if (empty($this->_notifyList))
1529            $this->getNotifyList();
1530
1531        /* Make a copy of both notifier lists because removeNotify will empty
1532         * $this->_notifyList and the second foreach will not work anymore.
1533         */
1534        /** @var SeedDMS_Core_User[] $nusers */
1535        $nusers = $this->_notifyList["users"];
1536        /** @var SeedDMS_Core_Group[] $ngroups */
1537        $ngroups = $this->_notifyList["groups"];
1538        foreach ($nusers as $u) {
1539            if ($this->getAccessMode($u) < M_READ) {
1540                $this->removeNotify($u->getID(), true);
1541            }
1542        }
1543        foreach ($ngroups as $g) {
1544            if ($this->getGroupAccessMode($g) < M_READ) {
1545                $this->removeNotify($g->getID(), false);
1546            }
1547        }
1548    } /* }}} */
1549
1550    /**
1551     * Add a user/group to the notification list
1552     * This function does not check if the currently logged in user
1553     * is allowed to add a notification. This must be checked by the calling
1554     * application.
1555     *
1556     * @param $userOrGroupID integer id of user or group to add
1557     * @param $isUser integer 1 if $userOrGroupID is a user,
1558     *                0 if $userOrGroupID is a group
1559     * @return integer  0: Update successful.
1560     *                 -1: Invalid User/Group ID.
1561     *                 -2: Target User / Group does not have read access.
1562     *                 -3: User is already subscribed.
1563     *                 -4: Database / internal error.
1564     */
1565    function addNotify($userOrGroupID, $isUser) { /* {{{ */
1566        $db = $this->_dms->getDB();
1567
1568        $userOrGroup = ($isUser ? "`userID`" : "`groupID`");
1569
1570        /* Verify that user / group exists. */
1571        $obj = ($isUser ? $this->_dms->getUser($userOrGroupID) : $this->_dms->getGroup($userOrGroupID));
1572        if (!is_object($obj)) {
1573            return -1;
1574        }
1575
1576        /* Verify that the requesting user has permission to add the target to
1577         * the notification system.
1578         */
1579        /*
1580         * The calling application should enforce the policy on who is allowed
1581         * to add someone to the notification system. If is shall remain here
1582         * the currently logged in user should be passed to this function
1583         *
1584        GLOBAL $user;
1585        if ($user->isGuest()) {
1586            return -2;
1587        }
1588        if (!$user->isAdmin()) {
1589            if ($isUser) {
1590                if ($user->getID() != $obj->getID()) {
1591                    return -2;
1592                }
1593            }
1594            else {
1595                if (!$obj->isMember($user)) {
1596                    return -2;
1597                }
1598            }
1599        }
1600         */
1601
1602        /* Verify that target user / group has read access to the document. */
1603        if ($isUser) {
1604            // Users are straightforward to check.
1605            if ($this->getAccessMode($obj) < M_READ) {
1606                return -2;
1607            }
1608        }
1609        else {
1610            // Groups are a little more complex.
1611            if ($this->getDefaultAccess() >= M_READ) {
1612                // If the default access is at least READ-ONLY, then just make sure
1613                // that the current group has not been explicitly excluded.
1614                $acl = $this->getAccessList(M_NONE, O_EQ);
1615                $found = false;
1616                /** @var SeedDMS_Core_GroupAccess $group */
1617                foreach ($acl["groups"] as $group) {
1618                    if ($group->getGroupID() == $userOrGroupID) {
1619                        $found = true;
1620                        break;
1621                    }
1622                }
1623                if ($found) {
1624                    return -2;
1625                }
1626            }
1627            else {
1628                // The default access is restricted. Make sure that the group has
1629                // been explicitly allocated access to the document.
1630                $acl = $this->getAccessList(M_READ, O_GTEQ);
1631                if (is_bool($acl)) {
1632                    return -4;
1633                }
1634                $found = false;
1635                /** @var SeedDMS_Core_GroupAccess $group */
1636                foreach ($acl["groups"] as $group) {
1637                    if ($group->getGroupID() == $userOrGroupID) {
1638                        $found = true;
1639                        break;
1640                    }
1641                }
1642                if (!$found) {
1643                    return -2;
1644                }
1645            }
1646        }
1647        /* Check to see if user/group is already on the list. */
1648        $queryStr = "SELECT * FROM `tblNotify` WHERE `tblNotify`.`target` = '".$this->_id."' ".
1649            "AND `tblNotify`.`targetType` = '".T_DOCUMENT."' ".
1650            "AND `tblNotify`.".$userOrGroup." = '".(int) $userOrGroupID."'";
1651        $resArr = $db->getResultArray($queryStr);
1652        if (is_bool($resArr)) {
1653            return -4;
1654        }
1655        if (count($resArr)>0) {
1656            return -3;
1657        }
1658
1659        $queryStr = "INSERT INTO `tblNotify` (`target`, `targetType`, " . $userOrGroup . ") VALUES (" . $this->_id . ", " . T_DOCUMENT . ", " . (int) $userOrGroupID . ")";
1660        if (!$db->getResult($queryStr))
1661            return -4;
1662
1663        unset($this->_notifyList);
1664        return 0;
1665    } /* }}} */
1666
1667    /**
1668     * Remove a user or group from the notification list
1669     * This function does not check if the currently logged in user
1670     * is allowed to remove a notification. This must be checked by the calling
1671     * application.
1672     *
1673     * @param integer $userOrGroupID id of user or group
1674     * @param boolean $isUser boolean true if a user is passed in $userOrGroupID, false
1675     *        if a group is passed in $userOrGroupID
1676     * @param integer $type type of notification (0 will delete all) Not used yet!
1677     * @return integer 0 if operation was succesful
1678     *                 -1 if the userid/groupid is invalid
1679     *                 -3 if the user/group is already subscribed
1680     *                 -4 in case of an internal database error
1681     */
1682    function removeNotify($userOrGroupID, $isUser, $type=0) { /* {{{ */
1683        $db = $this->_dms->getDB();
1684
1685        /* Verify that user / group exists. */
1686        /** @var SeedDMS_Core_Group|SeedDMS_Core_User $obj */
1687        $obj = ($isUser ? $this->_dms->getUser($userOrGroupID) : $this->_dms->getGroup($userOrGroupID));
1688        if (!is_object($obj)) {
1689            return -1;
1690        }
1691
1692        $userOrGroup = ($isUser) ? "`userID`" : "`groupID`";
1693
1694        /* Verify that the requesting user has permission to add the target to
1695         * the notification system.
1696         */
1697        /*
1698         * The calling application should enforce the policy on who is allowed
1699         * to add someone to the notification system. If is shall remain here
1700         * the currently logged in user should be passed to this function
1701         *
1702        GLOBAL $user;
1703        if ($user->isGuest()) {
1704            return -2;
1705        }
1706        if (!$user->isAdmin()) {
1707            if ($isUser) {
1708                if ($user->getID() != $obj->getID()) {
1709                    return -2;
1710                }
1711            }
1712            else {
1713                if (!$obj->isMember($user)) {
1714                    return -2;
1715                }
1716            }
1717        }
1718         */
1719
1720        /* Check to see if the target is in the database. */
1721        $queryStr = "SELECT * FROM `tblNotify` WHERE `tblNotify`.`target` = '".$this->_id."' ".
1722            "AND `tblNotify`.`targetType` = '".T_DOCUMENT."' ".
1723            "AND `tblNotify`.".$userOrGroup." = '".(int) $userOrGroupID."'";
1724        $resArr = $db->getResultArray($queryStr);
1725        if (is_bool($resArr)) {
1726            return -4;
1727        }
1728        if (count($resArr)==0) {
1729            return -3;
1730        }
1731
1732        $queryStr = "DELETE FROM `tblNotify` WHERE `target` = " . $this->_id . " AND `targetType` = " . T_DOCUMENT . " AND " . $userOrGroup . " = " . (int) $userOrGroupID;
1733        /* If type is given then delete only those notifications */
1734        if($type)
1735            $queryStr .= " AND `type` = ".(int) $type;
1736        if (!$db->getResult($queryStr))
1737            return -4;
1738
1739        unset($this->_notifyList);
1740        return 0;
1741    } /* }}} */
1742
1743    /**
1744     * Add content to a document
1745     *
1746     * Each document may have any number of content elements attached to it.
1747     * Each content element has a version number. Newer versions (greater
1748     * version number) replace older versions.
1749     *
1750     * @param string $comment comment
1751     * @param object $user user who shall be the owner of this content
1752     * @param string $tmpFile file containing the actuall content
1753     * @param string $orgFileName original file name
1754     * @param string $fileType
1755     * @param string $mimeType MimeType of the content
1756     * @param array $reviewers list of reviewers
1757     * @param array $approvers list of approvers
1758     * @param integer $version version number of content or 0 if next higher version shall be used.
1759     * @param array $attributes list of version attributes. The element key
1760     *        must be the id of the attribute definition.
1761     * @param object $workflow
1762     * @return bool|SeedDMS_Core_AddContentResultSet
1763     */
1764    function addContent($comment, $user, $tmpFile, $orgFileName, $fileType, $mimeType, $reviewers=array(), $approvers=array(), $version=0, $attributes=array(), $workflow=null) { /* {{{ */
1765        $db = $this->_dms->getDB();
1766
1767        // the doc path is id/version.filetype
1768        $dir = $this->getDir();
1769
1770        /* The version field in table tblDocumentContent used to be auto
1771         * increment but that requires the field to be primary as well if
1772         * innodb is used. That's why the version is now determined here.
1773         */
1774        if ((int)$version<1) {
1775            $queryStr = "SELECT MAX(`version`) AS m FROM `tblDocumentContent` WHERE `document` = ".$this->_id;
1776            $resArr = $db->getResultArray($queryStr);
1777            if (is_bool($resArr) && !$resArr)
1778                return false;
1779
1780            $version = $resArr[0]['m']+1;
1781        }
1782
1783        if($fileType == '.')
1784            $fileType = '';
1785        $filesize = SeedDMS_Core_File::fileSize($tmpFile);
1786        $checksum = SeedDMS_Core_File::checksum($tmpFile);
1787
1788        $db->startTransaction();
1789        $queryStr = "INSERT INTO `tblDocumentContent` (`document`, `version`, `comment`, `date`, `createdBy`, `dir`, `orgFileName`, `fileType`, `mimeType`, `fileSize`, `checksum`) VALUES ".
1790                        "(".$this->_id.", ".(int)$version.",".$db->qstr($comment).", ".$db->getCurrentTimestamp().", ".$user->getID().", ".$db->qstr($dir).", ".$db->qstr($orgFileName).", ".$db->qstr($fileType).", ".$db->qstr($mimeType).", ".$filesize.", ".$db->qstr($checksum).")";
1791        if (!$db->getResult($queryStr)) {
1792            $db->rollbackTransaction();
1793            return false;
1794        }
1795
1796        $contentID = $db->getInsertID('tblDocumentContent');
1797
1798        // copy file
1799        if (!SeedDMS_Core_File::makeDir($this->_dms->contentDir . $dir)) {
1800            $db->rollbackTransaction();
1801            return false;
1802        }
1803        if($this->_dms->forceRename)
1804            $err = SeedDMS_Core_File::renameFile($tmpFile, $this->_dms->contentDir . $dir . $version . $fileType);
1805        else
1806            $err = SeedDMS_Core_File::copyFile($tmpFile, $this->_dms->contentDir . $dir . $version . $fileType);
1807        if (!$err) {
1808            $db->rollbackTransaction();
1809            return false;
1810        }
1811
1812        $this->_content = null;
1813        $this->_latestContent = null;
1814        $content = $this->getLatestContent($contentID); /** @todo: Parameter not defined in Funktion */
1815        $docResultSet = new SeedDMS_Core_AddContentResultSet($content);
1816        $docResultSet->setDMS($this->_dms);
1817
1818        if($attributes) {
1819            foreach($attributes as $attrdefid=>$attribute) {
1820                /* $attribute can be a string or an array */
1821                if($attribute) {
1822                    if($attrdef = $this->_dms->getAttributeDefinition($attrdefid)) {
1823                        if(!$content->setAttributeValue($attrdef, $attribute)) {
1824                            $this->_removeContent($content);
1825                            $db->rollbackTransaction();
1826                            return false;
1827                        }
1828                    } else {
1829                        $this->_removeContent($content);
1830                        $db->rollbackTransaction();
1831                        return false;
1832                    }
1833                }
1834            }
1835        }
1836
1837        $queryStr = "INSERT INTO `tblDocumentStatus` (`documentID`, `version`) ".
1838            "VALUES (". $this->_id .", ". (int) $version .")";
1839        if (!$db->getResult($queryStr)) {
1840            $this->_removeContent($content);
1841            $db->rollbackTransaction();
1842            return false;
1843        }
1844
1845        $statusID = $db->getInsertID('tblDocumentStatus', 'statusID');
1846
1847        if($workflow)
1848            $content->setWorkflow($workflow, $user);
1849
1850        // Add reviewers into the database. Reviewers must review the document
1851        // and submit comments, if appropriate. Reviewers can also recommend that
1852        // a document be rejected.
1853        $pendingReview=false;
1854        /** @noinspection PhpUnusedLocalVariableInspection */
1855        foreach (array("i", "g") as $i){
1856            if (isset($reviewers[$i])) {
1857                foreach ($reviewers[$i] as $reviewerID) {
1858                    $reviewer=($i=="i" ?$this->_dms->getUser($reviewerID) : $this->_dms->getGroup($reviewerID));
1859                    $res = ($i=="i" ? $docResultSet->getContent()->addIndReviewer($reviewer, $user, true) : $docResultSet->getContent()->addGrpReviewer($reviewer, $user, true));
1860                    $docResultSet->addReviewer($reviewer, $i, $res);
1861                    // If no error is returned, or if the error is just due to email
1862                    // failure, mark the state as "pending review".
1863                    // FIXME: There seems to be no error code -4 anymore
1864                    if ($res==0 || $res=-3 || $res=-4) {
1865                        $pendingReview=true;
1866                    }
1867                }
1868            }
1869        }
1870        // Add approvers to the database. Approvers must also review the document
1871        // and make a recommendation on its release as an approved version.
1872        $pendingApproval=false;
1873        /** @noinspection PhpUnusedLocalVariableInspection */
1874        foreach (array("i", "g") as $i){
1875            if (isset($approvers[$i])) {
1876                foreach ($approvers[$i] as $approverID) {
1877                    $approver=($i=="i" ? $this->_dms->getUser($approverID) : $this->_dms->getGroup($approverID));
1878                    $res=($i=="i" ? $docResultSet->getContent()->addIndApprover($approver, $user, true) : $docResultSet->getContent()->addGrpApprover($approver, $user, !$pendingReview));
1879                    $docResultSet->addApprover($approver, $i, $res);
1880                    // FIXME: There seems to be no error code -4 anymore
1881                    if ($res==0 || $res=-3 || $res=-4) {
1882                        $pendingApproval=true;
1883                    }
1884                }
1885            }
1886        }
1887
1888        // If there are no reviewers or approvers, the document is automatically
1889        // promoted to the released state.
1890        if ($pendingReview) {
1891            $status = S_DRAFT_REV;
1892            $comment = "";
1893        }
1894        elseif ($pendingApproval) {
1895            $status = S_DRAFT_APP;
1896            $comment = "";
1897        }
1898        elseif($workflow) {
1899            $status = S_IN_WORKFLOW;
1900            $comment = ", workflow: ".$workflow->getName();
1901        } else {
1902            $status = S_RELEASED;
1903            $comment = "";
1904        }
1905        $queryStr = "INSERT INTO `tblDocumentStatusLog` (`statusID`, `status`, `comment`, `date`, `userID`) ".
1906            "VALUES ('". $statusID ."', '". $status."', 'New document content submitted". $comment ."', ".$db->getCurrentDatetime().", '". $user->getID() ."')";
1907        if (!$db->getResult($queryStr)) {
1908            $db->rollbackTransaction();
1909            return false;
1910        }
1911
1912        /** @noinspection PhpMethodParametersCountMismatchInspection */
1913        $docResultSet->setStatus($status);
1914
1915        $db->commitTransaction();
1916        return $docResultSet;
1917    } /* }}} */
1918
1919    /**
1920     * Replace a version of a document
1921     *
1922     * Each document may have any number of content elements attached to it.
1923     * This function replaces the file content of a given version.
1924     * Using this function is highly discourage, because it undermines the
1925     * idea of keeping all versions of a document as originally saved.
1926     * Content will only be replaced if the mimetype, filetype, user and
1927     * original filename are identical to the version being updated.
1928     *
1929     * This function was introduced for the webdav server because any saving
1930     * of a document created a new version.
1931     *
1932     * @param object $user user who shall be the owner of this content
1933     * @param string $tmpFile file containing the actuall content
1934     * @param string $orgFileName original file name
1935     * @param string $fileType
1936     * @param string $mimeType MimeType of the content
1937     * @param integer $version version number of content or 0 if latest version shall be replaced.
1938     * @return bool/array false in case of an error or a result set
1939     */
1940    function replaceContent($version, $user, $tmpFile, $orgFileName, $fileType, $mimeType, $allowoverride=[]) { /* {{{ */
1941        $db = $this->_dms->getDB();
1942
1943        // the doc path is id/version.filetype
1944        $dir = $this->getDir();
1945
1946        /* If $version < 1 than replace the content of the latest version.
1947         */
1948        if ((int) $version<1) {
1949            $queryStr = "SELECT MAX(`version`) AS m FROM `tblDocumentContent` WHERE `document` = ".$this->_id;
1950            $resArr = $db->getResultArray($queryStr);
1951            if (is_bool($resArr) && !$resArr)
1952                return false;
1953
1954            $version = $resArr[0]['m'];
1955        }
1956
1957        $content = $this->getContentByVersion($version);
1958        if(!$content)
1959            return false;
1960
1961        if($fileType == '.')
1962            $fileType = '';
1963
1964        $sql = [];
1965        /* Check if $user, $orgFileName, $fileType and $mimeType are the same */
1966        if($user->getID() != $content->getUser()->getID()) {
1967            if(!empty($allowoverride['user']))
1968                $sql[] = "`createdBy`=".$user->getID();
1969            else
1970                return false;
1971        }
1972        if($orgFileName != $content->getOriginalFileName()) {
1973            if(!empty($allowoverride['orgfilename']))
1974                $sql[] = "`orgFileName`=".$db->qstr($orgFileName);
1975            else
1976                return false;
1977        }
1978        if($fileType != $content->getFileType()) {
1979            if(!empty($allowoverride['filetype']))
1980                $sql[] = "`fileType`=".$db->qstr($fileType);
1981            else
1982                return false;
1983        }
1984        if($mimeType != $content->getMimeType()) {
1985            if(!empty($allowoverride['mimetype']))
1986                $sql[] = "`mimeType`=".$db->qstr($mimeType);
1987            else
1988                return false;
1989        }
1990
1991        $filesize = SeedDMS_Core_File::fileSize($tmpFile);
1992        $checksum = SeedDMS_Core_File::checksum($tmpFile);
1993
1994        $db->startTransaction();
1995        $sql[] = "`date`=".$db->getCurrentTimestamp();
1996        $sql[] = "`fileSize`=".$filesize;
1997        $sql[] = "`checksum`=".$db->qstr($checksum);
1998        $queryStr = "UPDATE `tblDocumentContent` set ".implode(", ", $sql)." WHERE `id`=".$content->getID();
1999        if (!$db->getResult($queryStr)) {
2000            $db->rollbackTransaction();
2001            return false;
2002        }
2003
2004        // copy file
2005        if (!SeedDMS_Core_File::copyFile($tmpFile, $this->_dms->contentDir . $dir . $version . $fileType)) {
2006            $db->rollbackTransaction();
2007            return false;
2008        }
2009
2010        $this->_content = null;
2011        $this->_latestContent = null;
2012        $db->commitTransaction();
2013
2014        return true;
2015    } /* }}} */
2016
2017    /**
2018     * Return all content elements of a document
2019     *
2020     * This functions returns an array of content elements ordered by version.
2021     * Version which are not accessible because of its status, will be filtered
2022     * out. Access rights based on the document status are calculated for the
2023     * currently logged in user.
2024     *
2025     * @return bool|SeedDMS_Core_DocumentContent[]
2026     */
2027    function getContent() { /* {{{ */
2028        $db = $this->_dms->getDB();
2029
2030        if (!isset($this->_content)) {
2031            $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = ".$this->_id." ORDER BY `version`";
2032            $resArr = $db->getResultArray($queryStr);
2033            if (is_bool($resArr) && !$resArr)
2034                return false;
2035
2036            $this->_content = array();
2037            $classname = $this->_dms->getClassname('documentcontent');
2038            $user = $this->_dms->getLoggedInUser();
2039            foreach ($resArr as $row) {
2040                /** @var SeedDMS_Core_DocumentContent $content */
2041                $content = new $classname($row["id"], $this, $row["version"], $row["comment"], $row["date"], $row["createdBy"], $row["dir"], $row["orgFileName"], $row["fileType"], $row["mimeType"], $row['fileSize'], $row['checksum']);
2042                /* TODO: Better use content id as key in $this->_content. This
2043                 * would allow to remove a single content object in removeContent().
2044                 * Currently removeContent() must clear $this->_content completely
2045                 */
2046                if($user) {
2047                    if($content->getAccessMode($user) >= M_READ)
2048                        array_push($this->_content, $content);
2049                } else {
2050                    array_push($this->_content, $content);
2051                }
2052            }
2053        }
2054
2055        return $this->_content;
2056    } /* }}} */
2057
2058    /**
2059     * Return the content element of a document with a given version number
2060     *
2061     * This function will check if the version is accessible and return false
2062     * if not. Access rights based on the document status are calculated for the
2063     * currently logged in user.
2064     *
2065     * @param integer $version version number of content element
2066     * @return SeedDMS_Core_DocumentContent|null|boolean object of class
2067     * {@link SeedDMS_Core_DocumentContent}, null if not content was found,
2068     * false in case of an error
2069     */
2070    function getContentByVersion($version) { /* {{{ */
2071        if (!is_numeric($version)) return false;
2072
2073        if (isset($this->_content)) {
2074            foreach ($this->_content as $revision) {
2075                if ($revision->getVersion() == $version)
2076                    return $revision;
2077            }
2078            return null;
2079        }
2080
2081        $db = $this->_dms->getDB();
2082        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = ".$this->_id." AND `version` = " . (int) $version;
2083        $resArr = $db->getResultArray($queryStr);
2084        if (is_bool($resArr) && !$resArr)
2085            return false;
2086        if (count($resArr) != 1)
2087            return null;
2088
2089        $resArr = $resArr[0];
2090        $classname = $this->_dms->getClassname('documentcontent');
2091        /** @var SeedDMS_Core_DocumentContent $content */
2092        if($content = new $classname($resArr["id"], $this, $resArr["version"], $resArr["comment"], $resArr["date"], $resArr["createdBy"], $resArr["dir"], $resArr["orgFileName"], $resArr["fileType"], $resArr["mimeType"], $resArr['fileSize'], $resArr['checksum'])) {
2093            $user = $this->_dms->getLoggedInUser();
2094            /* A user with write access on the document may always see the version */
2095            if($user && $content->getAccessMode($user) == M_NONE)
2096                return null;
2097            else
2098                return $content;
2099        } else {
2100            return false;
2101        }
2102    } /* }}} */
2103
2104    /**
2105     * Check if a given version is the latest version of the document
2106     *
2107     * @param integer $version version number of content element
2108     * @return SeedDMS_Core_DocumentContent|boolean object of class {@link SeedDMS_Core_DocumentContent}
2109     * or false
2110     */
2111    function isLatestContent($version) { /* {{{ */
2112        return $this->getLatestContent()->getVersion() == $version;
2113    } /* }}} */
2114
2115    /**
2116     * @return bool|null|SeedDMS_Core_DocumentContent
2117     */
2118    function __getLatestContent() { /* {{{ */
2119        if (!$this->_latestContent) {
2120            $db = $this->_dms->getDB();
2121            $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = ".$this->_id." ORDER BY `version` DESC LIMIT 1";
2122            $resArr = $db->getResultArray($queryStr);
2123            if (is_bool($resArr) && !$resArr)
2124                return false;
2125            if (count($resArr) != 1)
2126                return false;
2127
2128            $resArr = $resArr[0];
2129            $classname = $this->_dms->getClassname('documentcontent');
2130            $this->_latestContent = new $classname($resArr["id"], $this, $resArr["version"], $resArr["comment"], $resArr["date"], $resArr["createdBy"], $resArr["dir"], $resArr["orgFileName"], $resArr["fileType"], $resArr["mimeType"], $resArr['fileSize'], $resArr['checksum']);
2131        }
2132        return $this->_latestContent;
2133    } /* }}} */
2134
2135    /**
2136     * Get the latest version of document
2137     *
2138     * This function returns the latest accessible version of a document.
2139     * If content access has been restricted by setting
2140     * {@link SeedDMS_Core_DMS::noReadForStatus} the function will go
2141     * backwards in history until an accessible version is found. If none
2142     * is found null will be returned.
2143     * Access rights based on the document status are calculated for the
2144     * currently logged in user.
2145     *
2146     * @return bool|SeedDMS_Core_DocumentContent object of class {@link SeedDMS_Core_DocumentContent}
2147     */
2148    function getLatestContent() { /* {{{ */
2149        if (!$this->_latestContent) {
2150            $db = $this->_dms->getDB();
2151            $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = ".$this->_id." ORDER BY `version` DESC";
2152            $resArr = $db->getResultArray($queryStr);
2153            if (is_bool($resArr) && !$resArr)
2154                return false;
2155
2156            $classname = $this->_dms->getClassname('documentcontent');
2157            $user = $this->_dms->getLoggedInUser();
2158            foreach ($resArr as $row) {
2159                if (!$this->_latestContent) {
2160                    /** @var SeedDMS_Core_DocumentContent $content */
2161                    $content = new $classname($row["id"], $this, $row["version"], $row["comment"], $row["date"], $row["createdBy"], $row["dir"], $row["orgFileName"], $row["fileType"], $row["mimeType"], $row['fileSize'], $row['checksum']);
2162                    if($user) {
2163                        /* If the user may even write the document, then also allow to see all content.
2164                         * This is needed because the user could upload a new version
2165                         */
2166                        if($content->getAccessMode($user) >= M_READ) {
2167                            $this->_latestContent = $content;
2168                        }
2169                    } else {
2170                        $this->_latestContent = $content;
2171                    }
2172                }
2173            }
2174        }
2175
2176        return $this->_latestContent;
2177    } /* }}} */
2178
2179    /**
2180     * Remove version of document
2181     *
2182     * @param SeedDMS_Core_DocumentContent $version version number of content
2183     * @return boolean true if successful, otherwise false
2184     */
2185    private function _removeContent($version) { /* {{{ */
2186        $db = $this->_dms->getDB();
2187
2188        $db->startTransaction();
2189
2190        $status = $version->getStatus();
2191        $stID = $status["statusID"];
2192
2193        $queryStr = "DELETE FROM `tblDocumentContent` WHERE `document` = " . $this->getID() . " AND `version` = " . $version->getVersion();
2194        if (!$db->getResult($queryStr)) {
2195            $db->rollbackTransaction();
2196            return false;
2197        }
2198
2199        $queryStr = "DELETE FROM `tblDocumentContentAttributes` WHERE `content` = " . $version->getId();
2200        if (!$db->getResult($queryStr)) {
2201            $db->rollbackTransaction();
2202            return false;
2203        }
2204
2205        $queryStr = "DELETE FROM `tblDocumentStatusLog` WHERE `statusID` = '".$stID."'";
2206        if (!$db->getResult($queryStr)) {
2207            $db->rollbackTransaction();
2208            return false;
2209        }
2210
2211        $queryStr = "DELETE FROM `tblDocumentStatus` WHERE `documentID` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'";
2212        if (!$db->getResult($queryStr)) {
2213            $db->rollbackTransaction();
2214            return false;
2215        }
2216
2217        $status = $version->getReviewStatus();
2218        $stList = "";
2219        foreach ($status as $st) {
2220            $stList .= (strlen($stList)==0 ? "" : ", "). "'".$st["reviewID"]."'";
2221            $queryStr = "SELECT * FROM `tblDocumentReviewLog` WHERE `reviewID` = " . $st['reviewID'];
2222            $resArr = $db->getResultArray($queryStr);
2223            if ((is_bool($resArr) && !$resArr)) {
2224                $db->rollbackTransaction();
2225                return false;
2226            }
2227            foreach($resArr as $res) {
2228                $file = $this->_dms->contentDir . $this->getDir().'r'.$res['reviewLogID'];
2229                if(SeedDMS_Core_File::file_exists($file))
2230                    SeedDMS_Core_File::removeFile($file);
2231            }
2232        }
2233
2234        if (strlen($stList)>0) {
2235            $queryStr = "DELETE FROM `tblDocumentReviewLog` WHERE `tblDocumentReviewLog`.`reviewID` IN (".$stList.")";
2236            if (!$db->getResult($queryStr)) {
2237                $db->rollbackTransaction();
2238                return false;
2239            }
2240        }
2241        $queryStr = "DELETE FROM `tblDocumentReviewers` WHERE `documentID` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'";
2242        if (!$db->getResult($queryStr)) {
2243            $db->rollbackTransaction();
2244            return false;
2245        }
2246        $status = $version->getApprovalStatus();
2247        $stList = "";
2248        foreach ($status as $st) {
2249            $stList .= (strlen($stList)==0 ? "" : ", "). "'".$st["approveID"]."'";
2250            $queryStr = "SELECT * FROM `tblDocumentApproveLog` WHERE `approveID` = " . $st['approveID'];
2251            $resArr = $db->getResultArray($queryStr);
2252            if ((is_bool($resArr) && !$resArr)) {
2253                $db->rollbackTransaction();
2254                return false;
2255            }
2256            foreach($resArr as $res) {
2257                $file = $this->_dms->contentDir . $this->getDir().'a'.$res['approveLogID'];
2258                if(SeedDMS_Core_File::file_exists($file))
2259                    SeedDMS_Core_File::removeFile($file);
2260            }
2261        }
2262
2263        if (strlen($stList)>0) {
2264            $queryStr = "DELETE FROM `tblDocumentApproveLog` WHERE `tblDocumentApproveLog`.`approveID` IN (".$stList.")";
2265            if (!$db->getResult($queryStr)) {
2266                $db->rollbackTransaction();
2267                return false;
2268            }
2269        }
2270        $queryStr = "DELETE FROM `tblDocumentApprovers` WHERE `documentID` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'";
2271        if (!$db->getResult($queryStr)) {
2272            $db->rollbackTransaction();
2273            return false;
2274        }
2275
2276        $queryStr = "DELETE FROM `tblWorkflowDocumentContent` WHERE `document` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'";
2277        if (!$db->getResult($queryStr)) {
2278            $db->rollbackTransaction();
2279            return false;
2280        }
2281
2282        $queryStr = "DELETE FROM `tblWorkflowLog` WHERE `document` = '". $this->getID() ."' AND `version` = '" . $version->getVersion()."'";
2283        if (!$db->getResult($queryStr)) {
2284            $db->rollbackTransaction();
2285            return false;
2286        }
2287
2288        // remove only those document files attached to version
2289        $res = $this->getDocumentFiles($version->getVersion(), false);
2290        if (is_bool($res) && !$res) {
2291            $db->rollbackTransaction();
2292            return false;
2293        }
2294
2295        foreach ($res as $documentfile)
2296            if(!$this->removeDocumentFile($documentfile->getId())) {
2297                $db->rollbackTransaction();
2298                return false;
2299            }
2300
2301        if (SeedDMS_Core_File::file_exists( $this->_dms->contentDir.$version->getPath() ))
2302            if (!SeedDMS_Core_File::removeFile( $this->_dms->contentDir.$version->getPath() )) {
2303                $db->rollbackTransaction();
2304                return false;
2305            }
2306
2307        $db->commitTransaction();
2308        return true;
2309    } /* }}} */
2310
2311    /**
2312     * Call callback onPreRemoveDocument before deleting content
2313     *
2314     * @param SeedDMS_Core_DocumentContent $version version number of content
2315     * @return bool|mixed
2316     */
2317    function removeContent($version) { /* {{{ */
2318        $this->_dms->lasterror = '';
2319        $db = $this->_dms->getDB();
2320
2321        /* Make sure the version exists */
2322        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = " . $this->getID() . " AND `version` = " . $version->getVersion();
2323        $resArr = $db->getResultArray($queryStr);
2324        if (is_bool($resArr) && !$resArr)
2325            return false;
2326        if (count($resArr)==0)
2327            return false;
2328
2329        /* Make sure this is not the last version */
2330        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = " . $this->getID();
2331        $resArr = $db->getResultArray($queryStr);
2332        if (is_bool($resArr) && !$resArr)
2333            return false;
2334        if (count($resArr)==1)
2335            return false;
2336
2337        /* Check if 'onPreRemoveDocument' callback is set */
2338        if(isset($this->_dms->callbacks['onPreRemoveContent'])) {
2339            foreach($this->_dms->callbacks['onPreRemoveContent'] as $callback) {
2340                $ret = call_user_func($callback[0], $callback[1], $this, $version);
2341                if(is_bool($ret))
2342                    return $ret;
2343            }
2344        }
2345
2346        if(false === ($ret = self::_removeContent($version))) {
2347            return false;
2348        }
2349
2350        /* Invalidate the content list and the latest content of this document,
2351         * otherwise getContent() and getLatestContent()
2352         * will still return the content just deleted.
2353         */
2354        $this->_latestContent = null;
2355        $this->_content = null;
2356
2357        /* Check if 'onPostRemoveDocument' callback is set */
2358        if(isset($this->_dms->callbacks['onPostRemoveContent'])) {
2359            foreach($this->_dms->callbacks['onPostRemoveContent'] as $callback) {
2360                if(!call_user_func($callback[0], $callback[1], $version)) {
2361                }
2362            }
2363        }
2364
2365        return $ret;
2366    } /* }}} */
2367
2368    /**
2369     * Return a certain document link
2370     *
2371     * @param integer $linkID id of link
2372     * @return SeedDMS_Core_DocumentLink|bool of SeedDMS_Core_DocumentLink or false in case of
2373     *         an error.
2374     */
2375    function getDocumentLink($linkID) { /* {{{ */
2376        $db = $this->_dms->getDB();
2377
2378        if (!is_numeric($linkID)) return false;
2379
2380        $queryStr = "SELECT * FROM `tblDocumentLinks` WHERE `document` = " . $this->_id ." AND `id` = " . (int) $linkID;
2381        $resArr = $db->getResultArray($queryStr);
2382        if (is_bool($resArr) && !$resArr)
2383            return false;
2384        if (count($resArr)==0)
2385            return null;
2386
2387        $resArr = $resArr[0];
2388        $document = $this->_dms->getDocument($resArr["document"]);
2389        $target = $this->_dms->getDocument($resArr["target"]);
2390        $link = new SeedDMS_Core_DocumentLink($resArr["id"], $document, $target, $resArr["userID"], $resArr["public"]);
2391        $user = $this->_dms->getLoggedInUser();
2392        if($link->getAccessMode($user, $document, $target) >= M_READ)
2393            return $link;
2394        return null;
2395    } /* }}} */
2396
2397    /**
2398     * Return all document links
2399     *
2400     * The list may contain all links to other documents, even those which
2401     * may not be visible by certain users, unless you pass appropriate
2402     * parameters to filter out public links and those created by
2403     * the given user. The two parameters are or'ed. If $publiconly
2404     * is set the method will return all public links disregarding the
2405     * user. If $publiconly is not set but a user is set, the method
2406     * will return all links of that user (public and none public).
2407     * Setting a user and $publiconly to true will *not* return the
2408     * public links of that user but all links which are public or
2409     * owned by that user.
2410     *
2411     * The application must call
2412     * SeedDMS_Core_DMS::filterDocumentLinks() afterwards to filter out
2413     * those links pointing to a document not accessible by a given user.
2414     *
2415     * @param boolean           $publiconly return all publically visible links
2416     * @param SeedDMS_Core_User $user       return also private links of this user
2417     *
2418     * @return array list of objects of class {@see SeedDMS_Core_DocumentLink}
2419     */
2420    function getDocumentLinks($publiconly=false, $user=null) { /* {{{ */
2421        if (!isset($this->_documentLinks)) {
2422            $db = $this->_dms->getDB();
2423
2424            $queryStr = "SELECT * FROM `tblDocumentLinks` WHERE `document` = " . $this->_id;
2425            $tmp = array();
2426            if($publiconly)
2427                $tmp[] = "`public`=1";
2428            if($user)
2429                $tmp[] = "`userID`=".$user->getID();
2430            if($tmp) {
2431                $queryStr .= " AND (".implode(" OR ", $tmp).")";
2432            }
2433
2434            $resArr = $db->getResultArray($queryStr);
2435            if (is_bool($resArr) && !$resArr)
2436                return false;
2437            $this->_documentLinks = array();
2438
2439            $user = $this->_dms->getLoggedInUser();
2440            foreach ($resArr as $row) {
2441                $target = $this->_dms->getDocument($row["target"]);
2442                $link = new SeedDMS_Core_DocumentLink($row["id"], $this, $target, $row["userID"], $row["public"]);
2443                if($link->getAccessMode($user, $this, $target) >= M_READ)
2444                    array_push($this->_documentLinks, $link);
2445            }
2446        }
2447        return $this->_documentLinks;
2448    } /* }}} */
2449
2450    /**
2451     * Return all document having a link on this document
2452     *
2453     * The list contains all documents which have a link to the current
2454     * document. The list contains even those documents which
2455     * may not be accessible by the user, unless you pass appropriate
2456     * parameters to filter out public links and those created by
2457     * the given user.
2458     * This functions is basically the reverse of
2459     * {@see SeedDMS_Core_Document::getDocumentLinks()}
2460     *
2461     * The application must call
2462     * SeedDMS_Core_DMS::filterDocumentLinks() afterwards to filter out
2463     * those links pointing to a document not accessible by a given user.
2464     *
2465     * @param boolean           $publiconly return all publically visible links
2466     * @param SeedDMS_Core_User $user       return also private links of this user
2467     *
2468     * @return array list of objects of class SeedDMS_Core_DocumentLink
2469     */
2470    function getReverseDocumentLinks($publiconly=false, $user=null) { /* {{{ */
2471        $db = $this->_dms->getDB();
2472
2473        $queryStr = "SELECT * FROM `tblDocumentLinks` WHERE `target` = " . $this->_id;
2474        $tmp = array();
2475        if($publiconly)
2476            $tmp[] = "`public`=1";
2477        if($user)
2478            $tmp[] = "`userID`=".$user->getID();
2479        if($tmp) {
2480            $queryStr .= " AND (".implode(" OR ", $tmp).")";
2481        }
2482
2483        $resArr = $db->getResultArray($queryStr);
2484        if (is_bool($resArr) && !$resArr)
2485            return false;
2486
2487        $links = array();
2488        foreach ($resArr as $row) {
2489            $document = $this->_dms->getDocument($row["document"]);
2490            $link = new SeedDMS_Core_DocumentLink($row["id"], $document, $this, $row["userID"], $row["public"]);
2491            if($link->getAccessMode($user, $document, $this) >= M_READ)
2492                array_push($links, $link);
2493        }
2494
2495        return $links;
2496    } /* }}} */
2497
2498    function addDocumentLink($targetID, $userID, $public) { /* {{{ */
2499        $db = $this->_dms->getDB();
2500
2501        $public = ($public) ? 1 : 0;
2502
2503        if (!is_numeric($targetID) || $targetID < 1)
2504            return false;
2505
2506        if ($targetID == $this->_id)
2507            return false;
2508
2509        if (!is_numeric($userID) || $userID < 1)
2510            return false;
2511
2512        if(!($target = $this->_dms->getDocument($targetID)))
2513            return false;
2514
2515        if(!($user = $this->_dms->getUser($userID)))
2516            return false;
2517
2518        $queryStr = "INSERT INTO `tblDocumentLinks` (`document`, `target`, `userID`, `public`) VALUES (".$this->_id.", ".(int)$targetID.", ".(int)$userID.", ".$public.")";
2519        if (!$db->getResult($queryStr))
2520            return false;
2521
2522        unset($this->_documentLinks);
2523
2524        $id = $db->getInsertID('tblDocumentLinks');
2525        $link = new SeedDMS_Core_DocumentLink($id, $this, $target, $user->getId(), $public);
2526        return $link;
2527    } /* }}} */
2528
2529    function removeDocumentLink($linkID) { /* {{{ */
2530        $db = $this->_dms->getDB();
2531
2532        if (!is_numeric($linkID) || $linkID < 1)
2533            return false;
2534
2535        $queryStr = "DELETE FROM `tblDocumentLinks` WHERE `document` = " . $this->_id ." AND `id` = " . (int) $linkID;
2536        if (!$db->getResult($queryStr)) return false;
2537        unset ($this->_documentLinks);
2538        return true;
2539    } /* }}} */
2540
2541    /**
2542     * Get attached file by its id
2543     *
2544     * @return object instance of SeedDMS_Core_DocumentFile, null if file is not
2545     * accessible, false in case of an sql error
2546     */
2547    function getDocumentFile($ID) { /* {{{ */
2548        $db = $this->_dms->getDB();
2549
2550        if (!is_numeric($ID)) return false;
2551
2552        $queryStr = "SELECT * FROM `tblDocumentFiles` WHERE `document` = " . $this->_id ." AND `id` = " . (int) $ID;
2553        $resArr = $db->getResultArray($queryStr);
2554        if ((is_bool($resArr) && !$resArr) || count($resArr)==0) return false;
2555
2556        $resArr = $resArr[0];
2557        $classname = $this->_dms->getClassname('documentfile');
2558        $file = new $classname($resArr["id"], $this, $resArr["userID"], $resArr["comment"], $resArr["date"], $resArr["dir"], $resArr["fileType"], $resArr["mimeType"], $resArr["orgFileName"], $resArr["name"],$resArr["version"],$resArr["public"]);
2559        $user = $this->_dms->getLoggedInUser();
2560        if($file->getAccessMode($user) >= M_READ)
2561            return $file;
2562        return null;
2563    } /* }}} */
2564
2565    /**
2566     * Get list of files attached to document
2567     *
2568     * @param integer $version      get only attachments for this version
2569     * @param boolean $incnoversion include attachments without a version
2570     *
2571     * @return array list of files, false in case of an sql error
2572     */
2573    function getDocumentFiles($version=0, $incnoversion=true) { /* {{{ */
2574        /* use a smarter caching because removing a document will call this function
2575         * for each version and the document itself.
2576         */
2577        $hash = substr(md5($version.$incnoversion), 0, 4);
2578        if (!isset($this->_documentFiles[$hash])) {
2579            $db = $this->_dms->getDB();
2580
2581            $queryStr = "SELECT * FROM `tblDocumentFiles` WHERE `document` = " . $this->_id;
2582            if($version) {
2583                if($incnoversion)
2584                    $queryStr .= " AND (`version`=0 OR `version`=".(int) $version.")";
2585                else
2586                    $queryStr .= " AND (`version`=".(int) $version.")";
2587            }
2588            $queryStr .= " ORDER BY ";
2589            if($version) {
2590                $queryStr .= "`version` DESC,";
2591            }
2592            $queryStr .= "`date` DESC";
2593            $resArr = $db->getResultArray($queryStr);
2594            if (is_bool($resArr) && !$resArr) return false;
2595
2596            $this->_documentFiles = array($hash=>array());
2597
2598            $user = $this->_dms->getLoggedInUser();
2599            $classname = $this->_dms->getClassname('documentfile');
2600            foreach ($resArr as $row) {
2601                $file = new $classname($row["id"], $this, $row["userID"], $row["comment"], $row["date"], $row["dir"], $row["fileType"], $row["mimeType"], $row["orgFileName"], $row["name"], $row["version"], $row["public"]);
2602                if($file->getAccessMode($user) >= M_READ)
2603                    array_push($this->_documentFiles[$hash], $file);
2604            }
2605        }
2606        return $this->_documentFiles[$hash];
2607    } /* }}} */
2608
2609    function addDocumentFile($name, $comment, $user, $tmpFile, $orgFileName, $fileType, $mimeType, $version=0, $public=1) { /* {{{ */
2610        $db = $this->_dms->getDB();
2611
2612        $dir = $this->getDir();
2613
2614        $db->startTransaction();
2615        $queryStr = "INSERT INTO `tblDocumentFiles` (`comment`, `date`, `dir`, `document`, `fileType`, `mimeType`, `orgFileName`, `userID`, `name`, `version`, `public`) VALUES ".
2616            "(".$db->qstr($comment).", ".$db->getCurrentTimestamp().", ".$db->qstr($dir).", ".$this->_id.", ".$db->qstr($fileType).", ".$db->qstr($mimeType).", ".$db->qstr($orgFileName).",".$user->getID().",".$db->qstr($name).", ".((int) $version).", ".($public ? 1 : 0).")";
2617        if (!$db->getResult($queryStr)) {
2618            $db->rollbackTransaction();
2619            return false;
2620        }
2621
2622        $id = $db->getInsertID('tblDocumentFiles');
2623
2624        $file = $this->getDocumentFile($id);
2625        if (is_bool($file) && !$file) {
2626            $db->rollbackTransaction();
2627            return false;
2628        }
2629
2630        // copy file
2631        if (!SeedDMS_Core_File::makeDir($this->_dms->contentDir . $dir)) return false;
2632        if($this->_dms->forceRename)
2633            $err = SeedDMS_Core_File::renameFile($tmpFile, $this->_dms->contentDir . $file->getPath());
2634        else
2635            $err = SeedDMS_Core_File::copyFile($tmpFile, $this->_dms->contentDir . $file->getPath());
2636        if (!$err) {
2637            $db->rollbackTransaction();
2638            return false;
2639        }
2640
2641        $db->commitTransaction();
2642        unset ($this->_documentFiles);
2643        return $file;
2644    } /* }}} */
2645
2646    function removeDocumentFile($ID) { /* {{{ */
2647        $db = $this->_dms->getDB();
2648
2649        if (!is_numeric($ID) || $ID < 1)
2650            return false;
2651
2652        $file = $this->getDocumentFile($ID);
2653        if (is_bool($file) && !$file) return false;
2654
2655        $db->startTransaction();
2656        /* First delete the database record, because that can be undone
2657         * if deletion of the file fails.
2658         */
2659        $queryStr = "DELETE FROM `tblDocumentFiles` WHERE `document` = " . $this->getID() . " AND `id` = " . (int) $ID;
2660        if (!$db->getResult($queryStr)) {
2661            $db->rollbackTransaction();
2662            return false;
2663        }
2664
2665        if (SeedDMS_Core_File::file_exists( $this->_dms->contentDir . $file->getPath() )){
2666            if (!SeedDMS_Core_File::removeFile( $this->_dms->contentDir . $file->getPath() )) {
2667                $db->rollbackTransaction();
2668                return false;
2669            }
2670        }
2671
2672        $db->commitTransaction();
2673        unset ($this->_documentFiles);
2674
2675        return true;
2676    } /* }}} */
2677
2678    /**
2679     * Remove a document completly
2680     *
2681     * This methods calls the callback 'onPreRemoveDocument' before removing
2682     * the document. The current document will be passed as the second
2683     * parameter to the callback function. After successful deletion the
2684     * 'onPostRemoveDocument' callback will be used. The current document id
2685     * will be passed as the second parameter. If onPreRemoveDocument fails
2686     * the whole function will fail and the document will not be deleted.
2687     * The return value of 'onPostRemoveDocument' will be disregarded.
2688     *
2689     * @return boolean true on success, otherwise false
2690     */
2691    function remove() { /* {{{ */
2692        $db = $this->_dms->getDB();
2693        $this->_dms->lasterror = '';
2694
2695        /* Check if 'onPreRemoveDocument' callback is set */
2696        if(isset($this->_dms->callbacks['onPreRemoveDocument'])) {
2697            foreach($this->_dms->callbacks['onPreRemoveDocument'] as $callback) {
2698                $ret = call_user_func($callback[0], $callback[1], $this);
2699                if(is_bool($ret))
2700                    return $ret;
2701            }
2702        }
2703
2704        $res = $this->getContent();
2705        if (is_bool($res) && !$res) return false;
2706
2707        $db->startTransaction();
2708
2709        // remove content of document
2710        foreach ($this->_content as $version) {
2711            if (!$this->_removeContent($version)) {
2712                $db->rollbackTransaction();
2713                return false;
2714            }
2715        }
2716
2717        // remove all document files
2718        $res = $this->getDocumentFiles();
2719        if (is_bool($res) && !$res) {
2720            $db->rollbackTransaction();
2721            return false;
2722        }
2723
2724        foreach ($res as $documentfile)
2725            if(!$this->removeDocumentFile($documentfile->getId())) {
2726                $db->rollbackTransaction();
2727                return false;
2728            }
2729
2730        // TODO: versioning file?
2731
2732        if (SeedDMS_Core_File::file_exists( $this->_dms->contentDir . $this->getDir() ))
2733            if (!SeedDMS_Core_File::removeDir( $this->_dms->contentDir . $this->getDir() )) {
2734                $db->rollbackTransaction();
2735                return false;
2736            }
2737
2738        $queryStr = "DELETE FROM `tblDocuments` WHERE `id` = " . $this->_id;
2739        if (!$db->getResult($queryStr)) {
2740            $db->rollbackTransaction();
2741            return false;
2742        }
2743        $queryStr = "DELETE FROM `tblDocumentAttributes` WHERE `document` = " . $this->_id;
2744        if (!$db->getResult($queryStr)) {
2745            $db->rollbackTransaction();
2746            return false;
2747        }
2748        $queryStr = "DELETE FROM `tblACLs` WHERE `target` = " . $this->_id . " AND `targetType` = " . T_DOCUMENT;
2749        if (!$db->getResult($queryStr)) {
2750            $db->rollbackTransaction();
2751            return false;
2752        }
2753        $queryStr = "DELETE FROM `tblDocumentLinks` WHERE `document` = " . $this->_id . " OR `target` = " . $this->_id;
2754        if (!$db->getResult($queryStr)) {
2755            $db->rollbackTransaction();
2756            return false;
2757        }
2758        $queryStr = "DELETE FROM `tblDocumentLocks` WHERE `document` = " . $this->_id;
2759        if (!$db->getResult($queryStr)) {
2760            $db->rollbackTransaction();
2761            return false;
2762        }
2763        $queryStr = "DELETE FROM `tblDocumentFiles` WHERE `document` = " . $this->_id;
2764        if (!$db->getResult($queryStr)) {
2765            $db->rollbackTransaction();
2766            return false;
2767        }
2768        $queryStr = "DELETE FROM `tblDocumentCategory` WHERE `documentID` = " . $this->_id;
2769        if (!$db->getResult($queryStr)) {
2770            $db->rollbackTransaction();
2771            return false;
2772        }
2773
2774        // Delete the notification list.
2775        $queryStr = "DELETE FROM `tblNotify` WHERE `target` = " . $this->_id . " AND `targetType` = " . T_DOCUMENT;
2776        if (!$db->getResult($queryStr)) {
2777            $db->rollbackTransaction();
2778            return false;
2779        }
2780
2781        $db->commitTransaction();
2782
2783        /* Check if 'onPostRemoveDocument' callback is set */
2784        if(isset($this->_dms->callbacks['onPostRemoveDocument'])) {
2785            foreach($this->_dms->callbacks['onPostRemoveDocument'] as $callback) {
2786                if(!call_user_func($callback[0], $callback[1], $this)) {
2787                }
2788            }
2789        }
2790
2791        return true;
2792    } /* }}} */
2793
2794    /**
2795     * Get List of users and groups which have read access on the document
2796     * The list will not include any guest users,
2797     * administrators and the owner of the folder unless $listadmin resp.
2798     * $listowner is set to true.
2799     *
2800     * This function is deprecated. Use
2801     * {@see SeedDMS_Core_Document::getReadAccessList()} instead.
2802     */
2803    protected function __getApproversList() { /* {{{ */
2804        return $this->getReadAccessList(0, 0, 0);
2805    } /* }}} */
2806
2807    /**
2808     * Returns a list of groups and users with read access on the document
2809     *
2810     * @param boolean $listadmin if set to true any admin will be listed too
2811     * @param boolean $listowner if set to true the owner will be listed too
2812     * @param boolean $listguest if set to true any guest will be listed too
2813     *
2814     * @return array list of users and groups
2815     */
2816    function getReadAccessList($listadmin=0, $listowner=0, $listguest=0) { /* {{{ */
2817        $db = $this->_dms->getDB();
2818
2819        if (!isset($this->_readAccessList)) {
2820            $this->_readAccessList = array("groups" => array(), "users" => array());
2821            $userIDs = "";
2822            $groupIDs = "";
2823            $defAccess  = $this->getDefaultAccess();
2824
2825            /* Check if the default access is < read access or >= read access.
2826             * If default access is less than read access, then create a list
2827             * of users and groups with read access.
2828             * If default access is equal or greater then read access, then
2829             * create a list of users and groups without read access.
2830             */
2831            if ($defAccess<M_READ) {
2832                // Get the list of all users and groups that are listed in the ACL as
2833                // having read access to the document.
2834                $tmpList = $this->getAccessList(M_READ, O_GTEQ);
2835            }
2836            else {
2837                // Get the list of all users and groups that DO NOT have read access
2838                // to the document.
2839                $tmpList = $this->getAccessList(M_NONE, O_LTEQ);
2840            }
2841            /** @var SeedDMS_Core_GroupAccess $groupAccess */
2842            foreach ($tmpList["groups"] as $groupAccess) {
2843                $groupIDs .= (strlen($groupIDs)==0 ? "" : ", ") . $groupAccess->getGroupID();
2844            }
2845
2846            /** @var SeedDMS_Core_UserAccess $userAccess */
2847            foreach ($tmpList["users"] as $userAccess) {
2848                $user = $userAccess->getUser();
2849                if (!$listadmin && $user->isAdmin()) continue;
2850                if (!$listowner && $user->getID() == $this->_ownerID) continue;
2851                if (!$listguest && $user->isGuest()) continue;
2852                $userIDs .= (strlen($userIDs)==0 ? "" : ", ") . $userAccess->getUserID();
2853            }
2854
2855            // Construct a query against the users table to identify those users
2856            // that have read access to this document, either directly through an
2857            // ACL entry, by virtue of ownership or by having administrative rights
2858            // on the database.
2859            $queryStr="";
2860            /* If default access is less then read, $userIDs and $groupIDs contains
2861             * a list of user with read access
2862             */
2863            if ($defAccess < M_READ) {
2864                if (strlen($groupIDs)>0) {
2865                    $queryStr = "SELECT `tblUsers`.* FROM `tblUsers` ".
2866                        "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`userID`=`tblUsers`.`id` ".
2867                        "WHERE `tblGroupMembers`.`groupID` IN (". $groupIDs .") ".
2868                        "AND `tblUsers`.`role` != ".SeedDMS_Core_User::role_guest." UNION ";
2869                }
2870                $queryStr .=
2871                    "SELECT `tblUsers`.* FROM `tblUsers` ".
2872                    "WHERE (`tblUsers`.`role` != ".SeedDMS_Core_User::role_guest.") ".
2873                    "AND ((`tblUsers`.`id` = ". $this->_ownerID . ") ".
2874                    "OR (`tblUsers`.`role` = ".SeedDMS_Core_User::role_admin.")".
2875                    (strlen($userIDs) == 0 ? "" : " OR (`tblUsers`.`id` IN (". $userIDs ."))").
2876                    ") ORDER BY `login`";
2877            }
2878            /* If default access is equal or greater than M_READ, $userIDs and
2879             * $groupIDs contains a list of user without read access
2880             */
2881            else {
2882                if (strlen($groupIDs)>0) {
2883                    $queryStr = "SELECT `tblUsers`.* FROM `tblUsers` ".
2884                        "LEFT JOIN `tblGroupMembers` ON `tblGroupMembers`.`userID`=`tblUsers`.`id` ".
2885                        "WHERE `tblGroupMembers`.`groupID` NOT IN (". $groupIDs .")".
2886                        "AND `tblUsers`.`role` != ".SeedDMS_Core_User::role_guest." ".
2887                        (strlen($userIDs) == 0 ? "" : " AND (`tblUsers`.`id` NOT IN (". $userIDs ."))")." UNION ";
2888                } else {
2889                    $queryStr .=
2890                        "SELECT `tblUsers`.* FROM `tblUsers` ".
2891                        "WHERE `tblUsers`.`role` != ".SeedDMS_Core_User::role_guest." ".
2892                        (strlen($userIDs) == 0 ? "" : " AND (`tblUsers`.`id` NOT IN (". $userIDs ."))")." UNION ";
2893                }
2894                $queryStr .=
2895                    "SELECT `tblUsers`.* FROM `tblUsers` ".
2896                    "WHERE (`tblUsers`.`id` = ". $this->_ownerID . ") ".
2897                    "OR (`tblUsers`.`role` = ".SeedDMS_Core_User::role_admin.") ".
2898//                    "UNION ".
2899//                    "SELECT `tblUsers`.* FROM `tblUsers` ".
2900//                    "WHERE `tblUsers`.`role` != ".SeedDMS_Core_User::role_guest." ".
2901//                    (strlen($userIDs) == 0 ? "" : " AND (`tblUsers`.`id` NOT IN (". $userIDs ."))").
2902                    " ORDER BY `login`";
2903            }
2904            $resArr = $db->getResultArray($queryStr);
2905            if (!is_bool($resArr)) {
2906                foreach ($resArr as $row) {
2907                    $user = $this->_dms->getUser($row['id']);
2908                    if (!$listadmin && $user->isAdmin()) continue;
2909                    if (!$listowner && $user->getID() == $this->_ownerID) continue;
2910                    $this->_readAccessList["users"][] = $user;
2911                }
2912            }
2913
2914            // Assemble the list of groups that have read access to the document.
2915            $queryStr="";
2916            if ($defAccess < M_READ) {
2917                if (strlen($groupIDs)>0) {
2918                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ".
2919                        "WHERE `tblGroups`.`id` IN (". $groupIDs .") ORDER BY `name`";
2920                }
2921            }
2922            else {
2923                if (strlen($groupIDs)>0) {
2924                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ".
2925                        "WHERE `tblGroups`.`id` NOT IN (". $groupIDs .") ORDER BY `name`";
2926                }
2927                else {
2928                    $queryStr = "SELECT `tblGroups`.* FROM `tblGroups` ORDER BY `name`";
2929                }
2930            }
2931            if (strlen($queryStr)>0) {
2932                $resArr = $db->getResultArray($queryStr);
2933                if (!is_bool($resArr)) {
2934                    foreach ($resArr as $row) {
2935                        $group = $this->_dms->getGroup($row["id"]);
2936                        $this->_readAccessList["groups"][] = $group;
2937                    }
2938                }
2939            }
2940        }
2941        return $this->_readAccessList;
2942    } /* }}} */
2943
2944    /**
2945     * Get the internally used folderList which stores the ids of folders from
2946     * the root folder to the parent folder.
2947     *
2948     * @return string column separated list of folder ids
2949     */
2950    function getFolderList() { /* {{{ */
2951        $db = $this->_dms->getDB();
2952
2953        $queryStr = "SELECT `folderList` FROM `tblDocuments` WHERE id = ".$this->_id;
2954        $resArr = $db->getResultArray($queryStr);
2955        if (is_bool($resArr) && !$resArr)
2956            return false;
2957
2958        return $resArr[0]['folderList'];
2959    } /* }}} */
2960
2961    /**
2962     * Checks the internal data of the document and repairs it.
2963     * Currently, this function only repairs an incorrect folderList
2964     *
2965     * @return boolean true on success, otherwise false
2966     */
2967    function repair() { /* {{{ */
2968        $db = $this->_dms->getDB();
2969
2970        $curfolderlist = $this->getFolderList();
2971
2972        // calculate the folderList of the folder
2973        $parent = $this->getFolder();
2974        $pathPrefix="";
2975        $path = $parent->getPath();
2976        foreach ($path as $f) {
2977            $pathPrefix .= ":".$f->getID();
2978        }
2979        if (strlen($pathPrefix)>1) {
2980            $pathPrefix .= ":";
2981        }
2982        if($curfolderlist != $pathPrefix) {
2983            $queryStr = "UPDATE `tblDocuments` SET `folderList`='".$pathPrefix."' WHERE `id` = ". $this->_id;
2984            $res = $db->getResult($queryStr);
2985            if (!$res)
2986                return false;
2987        }
2988        return true;
2989    } /* }}} */
2990
2991    /**
2992     * Calculate the disk space including all versions of the document
2993     *
2994     * This is done by using the internal database field storing the
2995     * filesize of a document version.
2996     *
2997     * @return integer total disk space in Bytes
2998     */
2999    function getUsedDiskSpace() { /* {{{ */
3000        $db = $this->_dms->getDB();
3001
3002        $queryStr = "SELECT SUM(`fileSize`) sum FROM `tblDocumentContent` WHERE `document` = " . $this->_id;
3003        $resArr = $db->getResultArray($queryStr);
3004        if (is_bool($resArr) && $resArr == false)
3005            return false;
3006
3007        return $resArr[0]['sum'];
3008    } /* }}} */
3009
3010    /**
3011     * Returns a list of events happend during the life of the document
3012     *
3013     * This includes the creation of new versions, approval and reviews, etc.
3014     *
3015     * @return array list of events
3016     */
3017    function getTimeline() { /* {{{ */
3018        $db = $this->_dms->getDB();
3019
3020        $timeline = array();
3021
3022        /* No need to add entries for new version because the status log
3023         * will generate an entry as well.
3024        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` = " . $this->_id;
3025        $resArr = $db->getResultArray($queryStr);
3026        if (is_bool($resArr) && $resArr == false)
3027            return false;
3028
3029        foreach ($resArr as $row) {
3030            $date = date('Y-m-d H:i:s', $row['date']);
3031            $timeline[] = array('date'=>$date, 'msg'=>'Added version '.$row['version'], 'type'=>'add_version', 'version'=>$row['version'], 'document'=>$this, 'params'=>array($row['version']));
3032        }
3033         */
3034
3035        $queryStr = "SELECT * FROM `tblDocumentFiles` WHERE `document` = " . $this->_id;
3036        $resArr = $db->getResultArray($queryStr);
3037        if (is_bool($resArr) && $resArr == false)
3038            return false;
3039
3040        foreach ($resArr as $row) {
3041            $date = date('Y-m-d H:i:s', (int) $row['date']);
3042            $timeline[] = array('date'=>$date, 'msg'=>'Added attachment "'.$row['name'].'"', 'document'=>$this, 'type'=>'add_file', 'fileid'=>$row['id']);
3043        }
3044
3045        $queryStr=
3046            "SELECT `tblDocumentStatus`.*, `tblDocumentStatusLog`.`statusLogID`,`tblDocumentStatusLog`.`status`, ".
3047            "`tblDocumentStatusLog`.`comment`, `tblDocumentStatusLog`.`date`, ".
3048            "`tblDocumentStatusLog`.`userID` ".
3049            "FROM `tblDocumentStatus` ".
3050            "LEFT JOIN `tblDocumentStatusLog` USING (`statusID`) ".
3051            "WHERE `tblDocumentStatus`.`documentID` = '". $this->_id ."' ".
3052            "ORDER BY `tblDocumentStatusLog`.`statusLogID` DESC";
3053        $resArr = $db->getResultArray($queryStr);
3054        if (is_bool($resArr) && !$resArr)
3055            return false;
3056
3057        /* The above query will also contain entries where a document status exists
3058         * but no status log entry. Those records will have no date and must be
3059         * skipped.
3060         */
3061        foreach ($resArr as $row) {
3062            if($row['date']) {
3063                $date = $row['date'];
3064                $timeline[] = array('date'=>$date, 'msg'=>'Version '.$row['version'].': Status change to '.$row['status'], 'type'=>'status_change', 'version'=>$row['version'], 'document'=>$this, 'status'=>$row['status'], 'statusid'=>$row['statusID'], 'statuslogid'=>$row['statusLogID']);
3065            }
3066        }
3067        return $timeline;
3068    } /* }}} */
3069
3070    /**
3071     * Transfers the document to a new user
3072     * 
3073     * This method not just sets a new owner of the document but also
3074     * transfers the document links, attachments and locks to the new user.
3075     *
3076     * @return boolean true if successful, otherwise false
3077     */
3078    function transferToUser($newuser) { /* {{{ */
3079        $db = $this->_dms->getDB();
3080
3081        if($newuser->getId() == $this->_ownerID)
3082            return true;
3083
3084        $db->startTransaction();
3085        $queryStr = "UPDATE `tblDocuments` SET `owner` = ".$newuser->getId()." WHERE `id` = " . $this->_id;
3086        if (!$db->getResult($queryStr)) {
3087            $db->rollbackTransaction();
3088            return false;
3089        }
3090
3091        $queryStr = "UPDATE `tblDocumentLocks` SET `userID` = ".$newuser->getId()." WHERE `document` = " . $this->_id . " AND `userID` = ".$this->_ownerID;
3092        if (!$db->getResult($queryStr)) {
3093            $db->rollbackTransaction();
3094            return false;
3095        }
3096
3097        $queryStr = "UPDATE `tblDocumentLinks` SET `userID` = ".$newuser->getId()." WHERE `document` = " . $this->_id . " AND `userID` = ".$this->_ownerID;
3098        if (!$db->getResult($queryStr)) {
3099            $db->rollbackTransaction();
3100            return false;
3101        }
3102
3103        $queryStr = "UPDATE `tblDocumentFiles` SET `userID` = ".$newuser->getId()." WHERE `document` = " . $this->_id . " AND `userID` = ".$this->_ownerID;
3104        if (!$db->getResult($queryStr)) {
3105            $db->rollbackTransaction();
3106            return false;
3107        }
3108
3109        $this->_ownerID = $newuser->getID();
3110        $this->_owner = $newuser;
3111
3112        $db->commitTransaction();
3113        return true;
3114    } /* }}} */
3115
3116} /* }}} */
3117
3118
3119/**
3120 * Class to represent content of a document
3121 *
3122 * Each document has content attached to it, often called a 'version' of the
3123 * document. The document content represents a file on the disk with some
3124 * meta data stored in the database. A document content has a version number
3125 * which is incremented with each replacement of the old content. Old versions
3126 * are kept unless they are explicitly deleted by
3127 * {@link SeedDMS_Core_Document::removeContent()}.
3128 *
3129 * @category   DMS
3130 * @package    SeedDMS_Core
3131 * @author     Markus Westphal, Malcolm Cowe, Matteo Lucarelli,
3132 *             Uwe Steinmann <uwe@steinmann.cx>
3133 * @copyright  Copyright (C) 2002-2005 Markus Westphal,
3134 *             2006-2008 Malcolm Cowe, 2010 Matteo Lucarelli,
3135 *             2010-2022 Uwe Steinmann
3136 * @version    Release: @package_version@
3137 */
3138class SeedDMS_Core_DocumentContent extends SeedDMS_Core_Object { /* {{{ */
3139    /**
3140     * @var object document
3141     */
3142    protected $_document;
3143
3144    /**
3145     * @var integer version
3146     */
3147    protected $_version;
3148
3149    /**
3150     * @var string comment
3151     */
3152    protected $_comment;
3153
3154    /**
3155     * @var string date
3156     */
3157    protected $_date;
3158
3159    /**
3160     * @var integer $_userID
3161     */
3162    protected $_userID;
3163
3164    /**
3165     * @var object $_user
3166     */
3167    protected $_user;
3168
3169    /**
3170     * @var string dir on disk (deprecated)
3171     */
3172    protected $_dir;
3173
3174    /**
3175     * @var string original file name
3176     */
3177    protected $_orgFileName;
3178
3179    /**
3180     * @var string file type (actually the extension without the leading dot)
3181     */
3182    protected $_fileType;
3183
3184    /**
3185     * @var string mime type
3186     */
3187    protected $_mimeType;
3188
3189    /**
3190     * @var string checksum of content
3191     */
3192    protected $_checksum;
3193
3194    /**
3195     * @var int size of content file
3196     */
3197    protected $_fileSize;
3198
3199    /**
3200     * @var object workflow
3201     */
3202    protected $_workflow;
3203
3204    /**
3205     * @var object workflow state
3206     */
3207    protected $_workflowState;
3208
3209    /**
3210     * @var int $_status state
3211     */
3212    protected $_status;
3213
3214    /**
3215     * @var int $_reviewStatus state
3216     */
3217    protected $_reviewStatus;
3218
3219    /**
3220     * @var int $_approvalStatus state
3221     */
3222    protected $_approvalStatus;
3223
3224    /**
3225     * @var object dms
3226     */
3227    public $_dms;
3228
3229    /**
3230     * Recalculate the status of a document
3231     *
3232     * The methods checks the review and approval status and sets the
3233     * status of the document accordingly.
3234     *
3235     * If status is S_RELEASED and the version has a workflow, then set
3236     * the status to S_IN_WORKFLOW
3237     * If status is S_RELEASED and there are reviewers => set status S_DRAFT_REV
3238     * If status is S_RELEASED or S_DRAFT_REV and there are approvers => set
3239     * status S_DRAFT_APP
3240     * If status is draft and there are no approver and no reviewers => set
3241     * status to S_RELEASED
3242     * The status of a document with the current status S_OBSOLETE, S_REJECTED,
3243     * or S_EXPIRED will not be changed unless the parameter
3244     * $ignorecurrentstatus is set to true.
3245     *
3246     * This method may not be called after a negative approval or review to
3247     * recalculated the status, because
3248     * it doesn't take a defeating approval or review into account. This method
3249     * does not set the status to S_REJECTED! It will
3250     * just check for a pending workflow, approval or review and set the status
3251     * accordingly, e.g. after the list of reviewers or appovers has been
3252     * modified. If there is not pending workflow, approval or review the
3253     * status will be set to S_RELEASED.
3254     *
3255     * This method will call {@see SeedDMS_Core_DocumentContent::setStatus()}
3256     * which checks if the status has actually changed. This is, why this
3257     * function can be called at any time without harm to the status log.
3258     *
3259     * @param boolean $ignorecurrentstatus ignore the current status and
3260     *        recalculate a new status in any case
3261     * @param object $user the user initiating this method
3262     * @param string $msg message stored in status log when status is set
3263     */
3264    function verifyStatus($ignorecurrentstatus=false, $user=null, $msg='') { /* {{{ */
3265
3266        unset($this->_status);
3267        $st=$this->getStatus();
3268
3269        if (!$ignorecurrentstatus && ($st["status"]==S_OBSOLETE || $st["status"]==S_REJECTED || $st["status"]==S_EXPIRED )) return $st['status'];
3270
3271        $this->_workflow = null; // force to be reloaded from DB
3272        $hasworkflow =  $this->getWorkflow() ? true : false;
3273
3274        /* $pendingReview will be set when there are still open reviews */
3275        $pendingReview=false;
3276        /* $hasReview will be set if there is at least one positiv review */
3277        $hasReview=false;
3278        unset($this->_reviewStatus);  // force to be reloaded from DB
3279        $reviewStatus=$this->getReviewStatus();
3280        if (is_array($reviewStatus) && count($reviewStatus)>0) {
3281            foreach ($reviewStatus as $r){
3282                if ($r["status"]==0){
3283                    $pendingReview=true;
3284                    break;
3285                } elseif($r["status"]==1){
3286                    $hasReview=true;
3287                }
3288            }
3289        }
3290
3291        /* $pendingApproval will be set when there are still open approvals */
3292        $pendingApproval=false;
3293        /* $hasApproval will be set if there is at least one positiv review */
3294        $hasApproval=false;
3295        unset($this->_approvalStatus);  // force to be reloaded from DB
3296        $approvalStatus=$this->getApprovalStatus();
3297        if (is_array($approvalStatus) && count($approvalStatus)>0) {
3298            foreach ($approvalStatus as $a){
3299                if ($a["status"]==0){
3300                    $pendingApproval=true;
3301                    break;
3302                } elseif($a["status"]==1){
3303                    $hasApproval=true;
3304                }
3305            }
3306        }
3307
3308        /* First check for a running workflow or open reviews or approvals. */
3309        if ($hasworkflow) { $newstatus = S_IN_WORKFLOW; $ret = $this->setStatus(S_IN_WORKFLOW,$msg,$user); }
3310        elseif ($pendingReview) { $newstatus = S_DRAFT_REV; $ret = $this->setStatus(S_DRAFT_REV,$msg,$user); }
3311        elseif ($pendingApproval) { $newstatus = S_DRAFT_APP; $ret = $this->setStatus(S_DRAFT_APP,$msg,$user); }
3312        else { $newstatus = S_RELEASED; $ret = $this->setStatus(S_RELEASED,$msg,$user); }
3313        return $ret ? $newstatus : $ret;
3314    } /* }}} */
3315
3316    function __construct($id, $document, $version, $comment, $date, $userID, $dir, $orgFileName, $fileType, $mimeType, $fileSize=0, $checksum='') { /* {{{ */
3317        parent::__construct($id);
3318        $this->_document = $document;
3319        $this->_version = (int) $version;
3320        $this->_comment = $comment;
3321        $this->_date = (int) $date;
3322        $this->_userID = (int) $userID;
3323        $this->_user = null;
3324        $this->_dir = $dir;
3325        $this->_orgFileName = $orgFileName;
3326        $this->_fileType = $fileType;
3327        $this->_mimeType = $mimeType;
3328        $this->_dms = $document->getDMS();
3329        if(!$fileSize) {
3330            $this->_fileSize = SeedDMS_Core_File::fileSize($this->_dms->contentDir . $this->getPath());
3331        } else {
3332            $this->_fileSize = $fileSize;
3333        }
3334        $this->_checksum = $checksum;
3335        $this->_workflow = null;
3336        $this->_workflowState = null;
3337    } /* }}} */
3338
3339    /**
3340     * Check if this object is of type 'documentcontent'.
3341     *
3342     * @param string $type type of object
3343     */
3344    public function isType($type) { /* {{{ */
3345        return $type == 'documentcontent';
3346    } /* }}} */
3347
3348    function getVersion() { return $this->_version; }
3349    function getComment() { return $this->_comment; }
3350    function getDate() { return $this->_date; }
3351    function getOriginalFileName() { return $this->_orgFileName; }
3352    function getFileType() { return $this->_fileType; }
3353    function getFileName(){ return $this->_version . $this->_fileType; }
3354    /**
3355     * getDir and the corresponding database table field are deprecated
3356     */
3357    function __getDir() { return $this->_dir; }
3358    function getMimeType() { return $this->_mimeType; }
3359    function getDocument() { return $this->_document; }
3360
3361    function getUser() { /* {{{ */
3362        if (!isset($this->_user))
3363            $this->_user = $this->_document->getDMS()->getUser($this->_userID);
3364        return $this->_user;
3365    } /* }}} */
3366
3367    /**
3368     * Return path of file on disk relative to the content directory
3369     *
3370     * Since version 5.1.13 a single '.' in the fileType will be skipped.
3371     * On Windows a file named 'name.' will be saved as 'name' but the fileType
3372     * will contain the a single '.'.
3373     *
3374     * @return string path of file on disc
3375     */
3376    function getPath() { return $this->_document->getDir() . $this->_version . $this->_fileType; }
3377
3378    /**
3379     * Set upload date of document content
3380     *
3381     * @param string $date date must be a timestamp or in the format 'Y-m-d H:i:s'
3382     *
3383     * @return boolean true on success, otherwise false
3384     */
3385    function setDate($date = false) { /* {{{ */
3386        $db = $this->_document->getDMS()->getDB();
3387
3388        if(!$date)
3389            $date = time();
3390        else {
3391            if(is_string($date) && SeedDMS_Core_DMS::checkDate($date, 'Y-m-d H:i:s')) {
3392                $date = strtotime($date);
3393            } elseif(is_numeric($date))
3394                $date = (int) $date;
3395            else
3396                return false;
3397        }
3398
3399        $queryStr = "UPDATE `tblDocumentContent` SET `date` = ". $date." WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version;
3400        if (!$db->getResult($queryStr))
3401            return false;
3402
3403        $this->_date = $date;
3404
3405        return true;
3406    } /* }}} */
3407
3408    function getFileSize() { /* {{{ */
3409        return $this->_fileSize;
3410    } /* }}} */
3411
3412    /**
3413     * Set file size by reading the file
3414     */
3415    function setFileSize() { /* {{{ */
3416        $filesize = SeedDMS_Core_File::fileSize($this->_dms->contentDir . $this->_document->getDir() . $this->getFileName());
3417        if($filesize === false)
3418            return false;
3419
3420        $db = $this->_document->getDMS()->getDB();
3421        $queryStr = "UPDATE `tblDocumentContent` SET `fileSize` = ".$filesize." WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version;
3422        if (!$db->getResult($queryStr))
3423            return false;
3424        $this->_fileSize = $filesize;
3425
3426        return true;
3427    } /* }}} */
3428
3429    function getChecksum() { /* {{{ */
3430        return $this->_checksum;
3431    } /* }}} */
3432
3433    /**
3434     * Set checksum by reading the file
3435     */
3436    function setChecksum() { /* {{{ */
3437        $checksum = SeedDMS_Core_File::checksum($this->_dms->contentDir . $this->_document->getDir() . $this->getFileName());
3438        if($checksum === false)
3439            return false;
3440
3441        $db = $this->_document->getDMS()->getDB();
3442        $queryStr = "UPDATE `tblDocumentContent` SET `checksum` = ".$db->qstr($checksum)." WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version;
3443        if (!$db->getResult($queryStr))
3444            return false;
3445        $this->_checksum = $checksum;
3446
3447        return true;
3448    } /* }}} */
3449
3450    /**
3451     * Set file type by evaluating the mime type
3452     */
3453    function setFileType() { /* {{{ */
3454        $mimetype = $this->getMimeType();
3455
3456        $expect = SeedDMS_Core_File::fileExtension($mimetype);
3457        if($expect && '.'.$expect != $this->_fileType) {
3458            $db = $this->_document->getDMS()->getDB();
3459            $db->startTransaction();
3460            $queryStr = "UPDATE `tblDocumentContent` SET `fileType`='.".$expect."' WHERE `id` =   ". $this->_id;
3461            $res = $db->getResult($queryStr);
3462            if ($res) {
3463                if(!SeedDMS_Core_File::renameFile($this->_dms->contentDir.$this->_document->getDir() . $this->_version . $this->_fileType, $this->_dms->contentDir.$this->_document->getDir() . $this->_version . '.' . $expect)) {
3464                    $db->rollbackTransaction();
3465                } else {
3466                    $this->_fileType = '.'.$expect;
3467                    $db->commitTransaction();
3468                    return true;
3469                }
3470            } else {
3471                $db->rollbackTransaction();
3472            }
3473        }
3474
3475        return false;
3476    } /* }}} */
3477
3478    function setMimeType($newMimetype) { /* {{{ */
3479        $db = $this->_document->getDMS()->getDB();
3480
3481        if(!$newMimetype)
3482            return false;
3483
3484        $newMimetype = trim($newMimetype);
3485
3486        if(!$newMimetype)
3487            return false;
3488
3489        $queryStr = "UPDATE `tblDocumentContent` SET `mimeType` = ".$db->qstr($newMimetype)." WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version;
3490        if (!$db->getResult($queryStr))
3491            return false;
3492
3493        $this->_mimeType = $newMimetype;
3494
3495        return true;
3496    } /* }}} */
3497
3498    function setComment($newComment) { /* {{{ */
3499        $db = $this->_document->getDMS()->getDB();
3500
3501        /* Check if 'onPreSetVersionComment' callback is set */
3502        if(isset($this->_dms->callbacks['onPreSetVersionComment'])) {
3503            foreach($this->_dms->callbacks['onPreSetVersionComment'] as $callback) {
3504                $ret = call_user_func($callback[0], $callback[1], $this, $newComment);
3505                if(is_bool($ret))
3506                    return $ret;
3507            }
3508        }
3509
3510        $queryStr = "UPDATE `tblDocumentContent` SET `comment` = ".$db->qstr($newComment)." WHERE `document` = " . $this->_document->getID() . " AND `version` = " . $this->_version;
3511        if (!$db->getResult($queryStr))
3512            return false;
3513
3514        $this->_comment = $newComment;
3515
3516        /* Check if 'onPostSetVersionComment' callback is set */
3517        if(isset($this->_dms->callbacks['onPostSetVersionComment'])) {
3518            foreach($this->_dms->callbacks['onPostSetVersionComment'] as $callback) {
3519                $ret = call_user_func($callback[0], $callback[1], $this, $oldComment);
3520                if(is_bool($ret))
3521                    return $ret;
3522            }
3523        }
3524
3525        return true;
3526    } /* }}} */
3527
3528    /**
3529     * Get the latest status of the content
3530     *
3531     * The status of the content reflects its current review, approval or workflow
3532     * state. A status can be a negative or positive number or 0. A negative
3533     * numbers indicate a missing approval, review or an obsolete content.
3534     * Positive numbers indicate some kind of approval or workflow being
3535     * active, but not necessarily a release.
3536     * S_DRAFT_REV, 0
3537     * S_DRAFT_APP, 1
3538     * S_RELEASED, 2
3539     * S_IN_WORKFLOW, 3
3540     * S_REJECTED, -1
3541     * S_OBSOLETE, -2
3542     * S_EXPIRED, -3
3543     * When a content is inserted and does not need approval nor review,
3544     * then its status is set to S_RELEASED immediately. Any change of
3545     * the status is monitored in the table tblDocumentStatusLog. This
3546     * function will always return the latest entry for the content.
3547     *
3548     * @return array latest record from tblDocumentStatusLog
3549     */
3550    function getStatus($limit=1) { /* {{{ */
3551        $db = $this->_document->getDMS()->getDB();
3552
3553        if (!is_numeric($limit)) return false;
3554
3555        // Retrieve the current overall status of the content represented by
3556        // this object.
3557        if (!isset($this->_status)) {
3558            $queryStr=
3559                "SELECT `tblDocumentStatus`.*, `tblDocumentStatusLog`.`status`, ".
3560                "`tblDocumentStatusLog`.`comment`, `tblDocumentStatusLog`.`date`, ".
3561                "`tblDocumentStatusLog`.`userID` ".
3562                "FROM `tblDocumentStatus` ".
3563                "LEFT JOIN `tblDocumentStatusLog` USING (`statusID`) ".
3564                "WHERE `tblDocumentStatus`.`documentID` = '". $this->_document->getID() ."' ".
3565                "AND `tblDocumentStatus`.`version` = '". $this->_version ."' ".
3566                "ORDER BY `tblDocumentStatusLog`.`statusLogID` DESC LIMIT ".(int) $limit;
3567
3568            $res = $db->getResultArray($queryStr);
3569            if (is_bool($res) && !$res)
3570                return false;
3571            if (count($res)!=1)
3572                return false;
3573            $this->_status = $res[0];
3574        }
3575        return $this->_status;
3576    } /* }}} */
3577
3578    /**
3579     * Get current and former states of the document content
3580     *
3581     * @param integer $limit if not set all log entries will be returned
3582     * @return array list of status changes
3583     */
3584    function getStatusLog($limit=0) { /* {{{ */
3585        $db = $this->_document->getDMS()->getDB();
3586
3587        if (!is_numeric($limit)) return false;
3588
3589        $queryStr=
3590            "SELECT `tblDocumentStatus`.*, `tblDocumentStatusLog`.`status`, ".
3591            "`tblDocumentStatusLog`.`comment`, `tblDocumentStatusLog`.`date`, ".
3592            "`tblDocumentStatusLog`.`userID` ".
3593            "FROM `tblDocumentStatus` ".
3594            "LEFT JOIN `tblDocumentStatusLog` USING (`statusID`) ".
3595            "WHERE `tblDocumentStatus`.`documentID` = '". $this->_document->getID() ."' ".
3596            "AND `tblDocumentStatus`.`version` = '". $this->_version ."' ".
3597            "ORDER BY `tblDocumentStatusLog`.`statusLogID` DESC ";
3598        if($limit)
3599            $queryStr .= "LIMIT ".(int) $limit;
3600
3601        $res = $db->getResultArray($queryStr);
3602        if (is_bool($res) && !$res)
3603            return false;
3604
3605        return $res;
3606    } /* }}} */
3607
3608    /**
3609     * Set the status of the content
3610     * Setting the status means to add another entry into the table
3611     * tblDocumentStatusLog. The method returns also false if the status
3612     * is already set on the value passed to the method.
3613     *
3614     * @param integer $status     new status of content
3615     * @param string  $comment    comment for this status change
3616     * @param object  $updateUser user initiating the status change
3617     * @param string  $date       date in the format 'Y-m-d H:i:s'
3618     *
3619     * @return boolean true on success, otherwise false
3620     */
3621    function setStatus($status, $comment, $updateUser, $date='') { /* {{{ */
3622        $db = $this->_document->getDMS()->getDB();
3623
3624        if (!is_numeric($status)) return false;
3625
3626        /* return an error if $updateuser is not set */
3627        if(!$updateUser || !$updateUser->isType('user'))
3628            return false;
3629
3630        // If the supplied value lies outside of the accepted range, return an
3631        // error.
3632        if ($status < S_LOWEST_STATUS || $status > S_HIGHEST_STATUS) {
3633            return false;
3634        }
3635
3636        // Retrieve the current overall status of the content represented by
3637        // this object, if it hasn't been done already.
3638        if (!isset($this->_status)) {
3639            $this->getStatus();
3640        }
3641        if ($this->_status["status"]==$status) {
3642            return true;
3643        }
3644        if($date) {
3645            if(!SeedDMS_Core_DMS::checkDate($date, 'Y-m-d H:i:s'))
3646                return false;
3647            $ddate = $db->qstr($date);
3648        } else
3649            $ddate = $db->getCurrentDatetime();
3650        $db->startTransaction();
3651        $queryStr = "INSERT INTO `tblDocumentStatusLog` (`statusID`, `status`, `comment`, `date`, `userID`) ".
3652            "VALUES ('". $this->_status["statusID"] ."', '". (int) $status ."', ".$db->qstr($comment).", ".$ddate.", '". $updateUser->getID() ."')";
3653        $res = $db->getResult($queryStr);
3654        if (is_bool($res) && !$res) {
3655            $db->rollbackTransaction();
3656            return false;
3657        }
3658
3659        /* Check if 'onSetStatus' callback is set */
3660        if(isset($this->_dms->callbacks['onSetStatus'])) {
3661            foreach($this->_dms->callbacks['onSetStatus'] as $callback) {
3662                $ret = call_user_func($callback[0], $callback[1], $this, $updateUser, $this->_status["status"], $status);
3663                if(is_bool($ret)) {
3664                    unset($this->_status);
3665                    if($ret)
3666                        $db->commitTransaction();
3667                    else
3668                        $db->rollbackTransaction();
3669                    return $ret;
3670                }
3671            }
3672        }
3673
3674        $db->commitTransaction();
3675        unset($this->_status);
3676        return true;
3677    } /* }}} */
3678
3679    /**
3680     * Rewrites the complete status log
3681     *
3682     * Attention: this function is highly dangerous.
3683     * It removes an existing status log and rewrites it.
3684     * This method was added for importing an xml dump.
3685     *
3686     * @param array $statuslog new status log with the newest log entry first.
3687     * @return boolean true on success, otherwise false
3688     */
3689    function rewriteStatusLog($statuslog) { /* {{{ */
3690        $db = $this->_document->getDMS()->getDB();
3691
3692        $queryStr= "SELECT `tblDocumentStatus`.* FROM `tblDocumentStatus` WHERE `tblDocumentStatus`.`documentID` = '". $this->_document->getID() ."' AND `tblDocumentStatus`.`version` = '". $this->_version ."' ";
3693        $res = $db->getResultArray($queryStr);
3694        if (is_bool($res) && !$res)
3695            return false;
3696
3697        $statusID = $res[0]['statusID'];
3698
3699        $db->startTransaction();
3700
3701        /* First, remove the old entries */
3702        $queryStr = "DELETE FROM `tblDocumentStatusLog` WHERE `statusID`=".$statusID;
3703        if (!$db->getResult($queryStr)) {
3704            $db->rollbackTransaction();
3705            return false;
3706        }
3707
3708        /* Second, insert the new entries */
3709        $statuslog = array_reverse($statuslog);
3710        foreach($statuslog as $log) {
3711            if(!SeedDMS_Core_DMS::checkDate($log['date'], 'Y-m-d H:i:s')) {
3712                $db->rollbackTransaction();
3713                return false;
3714            }
3715            $queryStr = "INSERT INTO `tblDocumentStatusLog` (`statusID`, `status`, `comment`, `date`, `userID`) ".
3716                "VALUES ('".$statusID ."', '".(int) $log['status']."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".$log['user']->getID().")";
3717            if (!$db->getResult($queryStr)) {
3718                $db->rollbackTransaction();
3719                return false;
3720            }
3721        }
3722
3723        $db->commitTransaction();
3724        return true;
3725    } /* }}} */
3726
3727
3728    /**
3729     * Returns the access mode similar to a document
3730     *
3731     * There is no real access mode for document content, so this is more
3732     * like a virtual access mode, derived from the status of the document
3733     * content. The function checks if {@link SeedDMS_Core_DMS::noReadForStatus}
3734     * contains the status of the version and returns M_NONE if it exists and
3735     * the user is not involved in a workflow or review/approval/revision.
3736     * This method is called by all functions that returns the content e.g.
3737     * {@link SeedDMS_Core_Document::getLatestContent()}
3738     * It is also used by {@link SeedDMS_Core_Document::getAccessMode()} to
3739     * prevent access on the whole document if there is no accessible version.
3740     *
3741     * FIXME: This function only works propperly if $u is the currently logged in
3742     * user, because noReadForStatus will be set for this user.
3743     * FIXED: instead of using $dms->noReadForStatus it is take from the user's role
3744     *
3745     * @param object $u user
3746     * @return integer either M_NONE or M_READ
3747     */
3748    function getAccessMode($u) { /* {{{ */
3749        $dms = $this->_document->getDMS();
3750
3751        /* Check if 'onCheckAccessDocumentContent' callback is set */
3752        if(isset($this->_dms->callbacks['onCheckAccessDocumentContent'])) {
3753            foreach($this->_dms->callbacks['onCheckAccessDocumentContent'] as $callback) {
3754                if(($ret = call_user_func($callback[0], $callback[1], $this, $u)) > 0) {
3755                    return $ret;
3756                }
3757            }
3758        }
3759
3760        return M_READ;
3761
3762        if(!$u)
3763            return M_NONE;
3764
3765        /* If read access isn't further restricted by status, than grant read access */
3766        if(!$dms->noReadForStatus)
3767            return M_READ;
3768        $noReadForStatus = $dms->noReadForStatus;
3769
3770        /* If the current status is not in list of status without read access, then grant read access */
3771        if(!in_array($this->getStatus()['status'], $noReadForStatus))
3772            return M_READ;
3773
3774        /* Administrators have unrestricted access */
3775        if ($u->isAdmin()) return M_READ;
3776
3777        /* The owner of the document has unrestricted access */
3778        $owner = $this->_document->getOwner();
3779        if ($u->getID() == $owner->getID()) return M_READ;
3780
3781        /* Read/Write access on the document will also grant access on the version */
3782        if($this->_document->getAccessMode($u) >= M_READWRITE) return M_READ;
3783
3784        /* At this point the current status is in the list of status without read access.
3785         * The only way to still gain read access is, if the user is involved in the
3786         * process, e.g. is a reviewer, approver or an active person in the workflow.
3787         */
3788        $s = $this->getStatus();
3789        switch($s['status']) {
3790        case S_DRAFT_REV:
3791            $status = $this->getReviewStatus();
3792            foreach ($status as $r) {
3793                if($r['status'] != -2) // Check if reviewer was removed
3794                    switch ($r["type"]) {
3795                    case 0: // Reviewer is an individual.
3796                        if($u->getId() == $r["required"])
3797                            return M_READ;
3798                        break;
3799                    case 1: // Reviewer is a group.
3800                        $required = $dms->getGroup($r["required"]);
3801                        if (is_object($required) && $required->isMember($u))
3802                            return M_READ;
3803                        break;
3804                    }
3805            }
3806            break;
3807        case S_DRAFT_APP:
3808            $status = $this->getApprovalStatus();
3809            foreach ($status as $r) {
3810                if($r['status'] != -2) // Check if approver was removed
3811                    switch ($r["type"]) {
3812                    case 0: // Reviewer is an individual.
3813                        if($u->getId() == $r["required"])
3814                            return M_READ;
3815                        break;
3816                    case 1: // Reviewer is a group.
3817                        $required = $dms->getGroup($r["required"]);
3818                        if (is_object($required) && $required->isMember($u))
3819                            return M_READ;
3820                        break;
3821                    }
3822            }
3823            break;
3824        case S_RELEASED:
3825            break;
3826        case S_IN_WORKFLOW:
3827            if(!$this->_workflow)
3828                $this->getWorkflow();
3829
3830            if($this->_workflow) {
3831                if (!$this->_workflowState)
3832                    $this->getWorkflowState();
3833                $transitions = $this->_workflow->getNextTransitions($this->_workflowState);
3834                foreach($transitions as $transition) {
3835                    if($this->triggerWorkflowTransitionIsAllowed($u, $transition))
3836                        return M_READ;
3837                }
3838            }
3839            break;
3840        case S_REJECTED:
3841            break;
3842        case S_OBSOLETE:
3843            break;
3844        case S_EXPIRED:
3845            break;
3846        }
3847
3848        return M_NONE;
3849    } /* }}} */
3850
3851    /**
3852     * Return a list of all reviewers separated by individuals and groups
3853     * This list will not take the review log into account. Therefore it
3854     * can contain reviewers which has actually been deleted as a reviewer.
3855     *
3856     * @return array|bool|null
3857     */
3858    function getReviewers() { /* {{{ */
3859        $dms = $this->_document->getDMS();
3860        $db = $dms->getDB();
3861
3862        $queryStr=
3863            "SELECT * FROM `tblDocumentReviewers` WHERE `version`='".$this->_version
3864            ."' AND `documentID` = '". $this->_document->getID() ."' ";
3865
3866        $recs = $db->getResultArray($queryStr);
3867        if (is_bool($recs))
3868            return false;
3869        $reviewers = array('i'=>array(), 'g'=>array());
3870        foreach($recs as $rec) {
3871            if($rec['type'] == 0) {
3872                if($u = $dms->getUser($rec['required']))
3873                    $reviewers['i'][] = $u;
3874            } elseif($rec['type'] == 1) {
3875                if($g = $dms->getGroup($rec['required']))
3876                    $reviewers['g'][] = $g;
3877            }
3878        }
3879        return $reviewers;
3880    } /* }}} */
3881
3882    /**
3883     * Get the current review status of the document content
3884     * The review status is a list of reviewers and its current status
3885     *
3886     * @param integer $limit the number of recent status changes per reviewer
3887     * @return array list of review status
3888     */
3889    function getReviewStatus($limit=1) { /* {{{ */
3890        $db = $this->_document->getDMS()->getDB();
3891
3892        if (!is_numeric($limit)) return false;
3893
3894        // Retrieve the current status of each assigned reviewer for the content
3895        // represented by this object.
3896        // FIXME: caching was turned off to make list of review log in ViewDocument
3897        // possible
3898        if (1 || !isset($this->_reviewStatus)) {
3899            /* First get a list of all reviews for this document content */
3900            $queryStr=
3901                "SELECT `reviewID` FROM `tblDocumentReviewers` WHERE `version`='".$this->_version
3902                ."' AND `documentID` = '". $this->_document->getID() ."' ";
3903            $recs = $db->getResultArray($queryStr);
3904            if (is_bool($recs) && !$recs)
3905                return false;
3906            $this->_reviewStatus = array();
3907            if($recs) {
3908                foreach($recs as $rec) {
3909                    $queryStr=
3910                        "SELECT `tblDocumentReviewers`.*, `tblDocumentReviewLog`.`reviewLogID`, `tblDocumentReviewLog`.`status`, ".
3911                        "`tblDocumentReviewLog`.`comment`, `tblDocumentReviewLog`.`date`, ".
3912                        "`tblDocumentReviewLog`.`userID`, `tblUsers`.`fullName`, `tblGroups`.`name` AS `groupName` ".
3913                        "FROM `tblDocumentReviewers` ".
3914                        "LEFT JOIN `tblDocumentReviewLog` USING (`reviewID`) ".
3915                        "LEFT JOIN `tblUsers` on `tblUsers`.`id` = `tblDocumentReviewers`.`required`".
3916                        "LEFT JOIN `tblGroups` on `tblGroups`.`id` = `tblDocumentReviewers`.`required`".
3917                        "WHERE `tblDocumentReviewers`.`reviewID` = '". $rec['reviewID'] ."' ".
3918                        "ORDER BY `tblDocumentReviewLog`.`reviewLogID` DESC LIMIT ".(int) $limit;
3919
3920                    $res = $db->getResultArray($queryStr);
3921                    if (is_bool($res) && !$res) {
3922                        unset($this->_reviewStatus);
3923                        return false;
3924                    }
3925                    foreach($res as &$t) {
3926                        $filename = $this->_dms->contentDir . $this->_document->getDir().'r'.$t['reviewLogID'];
3927                        if(SeedDMS_Core_File::file_exists($filename))
3928                            $t['file'] = $filename;
3929                        else
3930                            $t['file'] = '';
3931                    }
3932                    $this->_reviewStatus = array_merge($this->_reviewStatus, $res);
3933                }
3934            }
3935        }
3936        return $this->_reviewStatus;
3937    } /* }}} */
3938
3939    /**
3940     * Get the latest entries from the review log of the document content
3941     *
3942     * @param integer $limit the number of log entries returned, defaults to 1
3943     * @return array list of review log entries
3944     */
3945    function getReviewLog($limit=1) { /* {{{ */
3946        $db = $this->_document->getDMS()->getDB();
3947
3948        if (!is_numeric($limit)) return false;
3949
3950        $queryStr=
3951            "SELECT * FROM `tblDocumentReviewLog` LEFT JOIN `tblDocumentReviewers` ON  `tblDocumentReviewLog`.`reviewID` = `tblDocumentReviewers`.`reviewID` WHERE `version`='".$this->_version
3952            ."' AND `documentID` = '". $this->_document->getID() ."' "
3953            ."ORDER BY `tblDocumentReviewLog`.`reviewLogID` DESC LIMIT ".(int) $limit;
3954        $recs = $db->getResultArray($queryStr);
3955        if (is_bool($recs) && !$recs)
3956            return false;
3957        return($recs);
3958    } /* }}} */
3959
3960    /**
3961     * Rewrites the complete review log
3962     *
3963     * Attention: this function is highly dangerous.
3964     * It removes an existing review log and rewrites it.
3965     * This method was added for importing an xml dump.
3966     *
3967     * @param array $reviewlog new status log with the newest log entry first.
3968     * @return boolean true on success, otherwise false
3969     */
3970    function rewriteReviewLog($reviewers) { /* {{{ */
3971        $db = $this->_document->getDMS()->getDB();
3972
3973        $queryStr= "SELECT `tblDocumentReviewers`.* FROM `tblDocumentReviewers` WHERE `tblDocumentReviewers`.`documentID` = '". $this->_document->getID() ."' AND `tblDocumentReviewers`.`version` = '". $this->_version ."' ";
3974        $res = $db->getResultArray($queryStr);
3975        if (is_bool($res) && !$res)
3976            return false;
3977
3978        $db->startTransaction();
3979
3980        if($res) {
3981            foreach($res as $review) {
3982                $reviewID = $review['reviewID'];
3983
3984                /* First, remove the old entries */
3985                $queryStr = "DELETE FROM `tblDocumentReviewLog` WHERE `reviewID`=".$reviewID;
3986                if (!$db->getResult($queryStr)) {
3987                    $db->rollbackTransaction();
3988                    return false;
3989                }
3990
3991                $queryStr = "DELETE FROM `tblDocumentReviewers` WHERE `reviewID`=".$reviewID;
3992                if (!$db->getResult($queryStr)) {
3993                    $db->rollbackTransaction();
3994                    return false;
3995                }
3996            }
3997        }
3998
3999        /* Second, insert the new entries */
4000        foreach($reviewers as $review) {
4001            $queryStr = "INSERT INTO `tblDocumentReviewers` (`documentID`, `version`, `type`, `required`) ".
4002                "VALUES ('".$this->_document->getID()."', '".$this->_version."', ".$review['type'] .", ".(is_object($review['required']) ? $review['required']->getID() : (int) $review['required']).")";
4003            if (!$db->getResult($queryStr)) {
4004                $db->rollbackTransaction();
4005                return false;
4006            }
4007            $reviewID = $db->getInsertID('tblDocumentReviewers', 'reviewID');
4008            $reviewlog = array_reverse($review['logs']);
4009            foreach($reviewlog as $log) {
4010                if(!SeedDMS_Core_DMS::checkDate($log['date'], 'Y-m-d H:i:s')) {
4011                    $db->rollbackTransaction();
4012                    return false;
4013                }
4014                $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`, `comment`, `date`, `userID`) ".
4015                    "VALUES ('".$reviewID ."', '".(int) $log['status']."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".(is_object($log['user']) ? $log['user']->getID() : (int) $log['user']).")";
4016                if (!$db->getResult($queryStr)) {
4017                    $db->rollbackTransaction();
4018                    return false;
4019                }
4020                $reviewLogID = $db->getInsertID('tblDocumentReviewLog', 'reviewLogID');
4021                if(!empty($log['file'])) {
4022                    SeedDMS_Core_File::copyFile($log['file'], $this->_dms->contentDir . $this->_document->getDir() . 'r' . $reviewLogID);
4023                }
4024            }
4025        }
4026
4027        $db->commitTransaction();
4028        return true;
4029    } /* }}} */
4030
4031    /**
4032     * Return a list of all approvers separated by individuals and groups
4033     * This list will not take the approval log into account. Therefore it
4034     * can contain approvers which has actually been deleted as an approver.
4035     *
4036     * @return array|bool|null
4037     */
4038    function getApprovers() { /* {{{ */
4039        $dms = $this->_document->getDMS();
4040        $db = $dms->getDB();
4041
4042        $queryStr=
4043            "SELECT * FROM `tblDocumentApprovers` WHERE `version`='".$this->_version
4044            ."' AND `documentID` = '". $this->_document->getID() ."' ";
4045
4046        $recs = $db->getResultArray($queryStr);
4047        if (is_bool($recs))
4048            return false;
4049        $approvers = array('i'=>array(), 'g'=>array());
4050        foreach($recs as $rec) {
4051            if($rec['type'] == 0) {
4052                if($u = $dms->getUser($rec['required']))
4053                    $approvers['i'][] = $u;
4054            } elseif($rec['type'] == 1) {
4055                if($g = $dms->getGroup($rec['required']))
4056                    $approvers['g'][] = $g;
4057            }
4058        }
4059        return $approvers;
4060    } /* }}} */
4061
4062    /**
4063     * Get the current approval status of the document content
4064     * The approval status is a list of approvers and its current status
4065     *
4066     * @param integer $limit the number of recent status changes per approver
4067     * @return array list of approval status
4068     */
4069    function getApprovalStatus($limit=1) { /* {{{ */
4070        $db = $this->_document->getDMS()->getDB();
4071
4072        if (!is_numeric($limit)) return false;
4073
4074        // Retrieve the current status of each assigned approver for the content
4075        // represented by this object.
4076        // FIXME: caching was turned off to make list of approval log in ViewDocument
4077        // possible
4078        if (1 || !isset($this->_approvalStatus)) {
4079            /* First get a list of all approvals for this document content */
4080            $queryStr=
4081                "SELECT `approveID` FROM `tblDocumentApprovers` WHERE `version`='".$this->_version
4082                ."' AND `documentID` = '". $this->_document->getID() ."' ";
4083            $recs = $db->getResultArray($queryStr);
4084            if (is_bool($recs) && !$recs)
4085                return false;
4086            $this->_approvalStatus = array();
4087            if($recs) {
4088                foreach($recs as $rec) {
4089                    $queryStr=
4090                        "SELECT `tblDocumentApprovers`.*, `tblDocumentApproveLog`.`approveLogID`, `tblDocumentApproveLog`.`status`, ".
4091                        "`tblDocumentApproveLog`.`comment`, `tblDocumentApproveLog`.`date`, ".
4092                        "`tblDocumentApproveLog`.`userID`, `tblUsers`.`fullName`, `tblGroups`.`name` AS `groupName` ".
4093                        "FROM `tblDocumentApprovers` ".
4094                        "LEFT JOIN `tblDocumentApproveLog` USING (`approveID`) ".
4095                        "LEFT JOIN `tblUsers` on `tblUsers`.`id` = `tblDocumentApprovers`.`required` ".
4096                        "LEFT JOIN `tblGroups` on `tblGroups`.`id` = `tblDocumentApprovers`.`required`".
4097                        "WHERE `tblDocumentApprovers`.`approveID` = '". $rec['approveID'] ."' ".
4098                        "ORDER BY `tblDocumentApproveLog`.`approveLogID` DESC LIMIT ".(int) $limit;
4099
4100                    $res = $db->getResultArray($queryStr);
4101                    if (is_bool($res) && !$res) {
4102                        unset($this->_approvalStatus);
4103                        return false;
4104                    }
4105                    foreach($res as &$t) {
4106                        $filename = $this->_dms->contentDir . $this->_document->getDir().'a'.$t['approveLogID'];
4107                        if(SeedDMS_Core_File::file_exists($filename))
4108                            $t['file'] = $filename;
4109                        else
4110                            $t['file'] = '';
4111                    }
4112                    $this->_approvalStatus = array_merge($this->_approvalStatus, $res);
4113                }
4114            }
4115        }
4116        return $this->_approvalStatus;
4117    } /* }}} */
4118
4119    /**
4120     * Get the latest entries from the approval log of the document content
4121     *
4122     * @param integer $limit the number of log entries returned, defaults to 1
4123     * @return array list of approval log entries
4124     */
4125    function getApproveLog($limit=1) { /* {{{ */
4126        $db = $this->_document->getDMS()->getDB();
4127
4128        if (!is_numeric($limit)) return false;
4129
4130        $queryStr=
4131            "SELECT * FROM `tblDocumentApproveLog` LEFT JOIN `tblDocumentApprovers` ON  `tblDocumentApproveLog`.`approveID` = `tblDocumentApprovers`.`approveID` WHERE `version`='".$this->_version
4132            ."' AND `documentID` = '". $this->_document->getID() ."' "
4133            ."ORDER BY `tblDocumentApproveLog`.`approveLogID` DESC LIMIT ".(int) $limit;
4134        $recs = $db->getResultArray($queryStr);
4135        if (is_bool($recs) && !$recs)
4136            return false;
4137        return($recs);
4138    } /* }}} */
4139
4140    /**
4141     * Rewrites the complete approval log
4142     *
4143     * Attention: this function is highly dangerous.
4144     * It removes an existing review log and rewrites it.
4145     * This method was added for importing an xml dump.
4146     *
4147     * @param array $reviewlog new status log with the newest log entry first.
4148     * @return boolean true on success, otherwise false
4149     */
4150    function rewriteApprovalLog($reviewers) { /* {{{ */
4151        $db = $this->_document->getDMS()->getDB();
4152
4153        $queryStr= "SELECT `tblDocumentApprovers`.* FROM `tblDocumentApprovers` WHERE `tblDocumentApprovers`.`documentID` = '". $this->_document->getID() ."' AND `tblDocumentApprovers`.`version` = '". $this->_version ."' ";
4154        $res = $db->getResultArray($queryStr);
4155        if (is_bool($res) && !$res)
4156            return false;
4157
4158        $db->startTransaction();
4159
4160        if($res) {
4161            foreach($res as $review) {
4162                $reviewID = $review['reviewID'];
4163
4164                /* First, remove the old entries */
4165                $queryStr = "DELETE FROM `tblDocumentApproveLog` WHERE `approveID`=".$reviewID;
4166                if (!$db->getResult($queryStr)) {
4167                    $db->rollbackTransaction();
4168                    return false;
4169                }
4170
4171                $queryStr = "DELETE FROM `tblDocumentApprovers` WHERE `approveID`=".$reviewID;
4172                if (!$db->getResult($queryStr)) {
4173                    $db->rollbackTransaction();
4174                    return false;
4175                }
4176            }
4177        }
4178
4179        /* Second, insert the new entries */
4180        foreach($reviewers as $review) {
4181            $queryStr = "INSERT INTO `tblDocumentApprovers` (`documentID`, `version`, `type`, `required`) ".
4182                "VALUES ('".$this->_document->getID()."', '".$this->_version."', ".$review['type'] .", ".(is_object($review['required']) ? $review['required']->getID() : (int) $review['required']).")";
4183            if (!$db->getResult($queryStr)) {
4184                $db->rollbackTransaction();
4185                return false;
4186            }
4187            $reviewID = $db->getInsertID('tblDocumentApprovers', 'approveID');
4188            $reviewlog = array_reverse($review['logs']);
4189            foreach($reviewlog as $log) {
4190                if(!SeedDMS_Core_DMS::checkDate($log['date'], 'Y-m-d H:i:s')) {
4191                    $db->rollbackTransaction();
4192                    return false;
4193                }
4194                $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`, `comment`, `date`, `userID`) ".
4195                    "VALUES ('".$reviewID ."', '".(int) $log['status']."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".(is_object($log['user']) ? $log['user']->getID() : (int) $log['user']).")";
4196                if (!$db->getResult($queryStr)) {
4197                    $db->rollbackTransaction();
4198                    return false;
4199                }
4200                $approveLogID = $db->getInsertID('tblDocumentApproveLog', 'approveLogID');
4201                if(!empty($log['file'])) {
4202                    SeedDMS_Core_File::copyFile($log['file'], $this->_dms->contentDir . $this->_document->getDir() . 'a' . $approveLogID);
4203                }
4204            }
4205        }
4206
4207        $db->commitTransaction();
4208        return true;
4209    } /* }}} */
4210
4211    /**
4212     * Add user as new reviewer
4213     *
4214     * @param object $user user in charge for the review
4215     * @param object $requestUser user requesting the operation (usually the
4216     * currently logged in user)
4217     *
4218     * @return integer|false if > 0 the id of the review log, if < 0 the error
4219     * code, false in case of an sql error
4220     */
4221    function addIndReviewer($user, $requestUser) { /* {{{ */
4222        if(!$user || !$requestUser)
4223            return -1;
4224
4225        $db = $this->_document->getDMS()->getDB();
4226
4227        if(!$user->isType('user'))
4228            return -1;
4229
4230        $userID = $user->getID();
4231
4232        // Get the list of users and groups with read access to this document.
4233        if($this->_document->getAccessMode($user) < M_READ) {
4234            return -2;
4235        }
4236
4237        // Check to see if the user has already been added to the review list.
4238        $reviewStatus = $user->getReviewStatus($this->_document->getID(), $this->_version);
4239        if (is_bool($reviewStatus) && !$reviewStatus) {
4240            return false;
4241        }
4242        $indstatus = false;
4243        if (count($reviewStatus["indstatus"]) > 0) {
4244            $indstatus = array_pop($reviewStatus["indstatus"]);
4245            if($indstatus["status"]!=-2) {
4246                // User is already on the list of reviewers; return an error.
4247                return -3;
4248            }
4249        }
4250
4251        // Add the user into the review database.
4252        if (!$indstatus || ($indstatus && $indstatus["status"]!=-2)) {
4253            $queryStr = "INSERT INTO `tblDocumentReviewers` (`documentID`, `version`, `type`, `required`) ".
4254                "VALUES ('". $this->_document->getID() ."', '". $this->_version ."', '0', '". $userID ."')";
4255            $res = $db->getResult($queryStr);
4256            if (is_bool($res) && !$res) {
4257                return false;
4258            }
4259            $reviewID = $db->getInsertID('tblDocumentReviewers', 'reviewID');
4260        }
4261        else {
4262            $reviewID = isset($indstatus["reviewID"]) ? $indstatus["reviewID"] : NULL;
4263        }
4264
4265        $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`, `comment`, `date`, `userID`) ".
4266            "VALUES ('". $reviewID ."', '0', '', ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4267        $res = $db->getResult($queryStr);
4268        if (is_bool($res) && !$res) {
4269            return false;
4270        }
4271
4272        $reviewLogID = $db->getInsertID('tblDocumentReviewLog', 'reviewLogID');
4273        $db->dropTemporaryTable('ttreviewid');
4274        return $reviewLogID;
4275    } /* }}} */
4276
4277    /**
4278     * Add group as new reviewer
4279     *
4280     * @param object $group group in charge for the review
4281     * @param object $requestUser user requesting the operation (usually the
4282     * currently logged in user)
4283     *
4284     * @return integer|false if > 0 the id of the review log, if < 0 the error
4285     * code, false in case of an sql error
4286     */
4287    function addGrpReviewer($group, $requestUser) { /* {{{ */
4288        if(!$group || !$requestUser)
4289            return -1;
4290
4291        $db = $this->_document->getDMS()->getDB();
4292
4293        if(!$group->isType('group'))
4294            return -1;
4295
4296        $groupID = $group->getID();
4297
4298        // Get the list of users and groups with read access to this document.
4299        if (!isset($this->_readAccessList)) {
4300            // TODO: error checking.
4301            $this->_readAccessList = $this->_document->getReadAccessList();
4302        }
4303        $approved = false;
4304        foreach ($this->_readAccessList["groups"] as $appGroup) {
4305            if ($groupID == $appGroup->getID()) {
4306                $approved = true;
4307                break;
4308            }
4309        }
4310        if (!$approved) {
4311            return -2;
4312        }
4313
4314        // Check to see if the group has already been added to the review list.
4315        $reviewStatus = $group->getReviewStatus($this->_document->getID(), $this->_version);
4316        if (is_bool($reviewStatus) && !$reviewStatus) {
4317            return false;
4318        }
4319        if (count($reviewStatus) > 0 && $reviewStatus[0]["status"]!=-2) {
4320            // Group is already on the list of reviewers; return an error.
4321            return -3;
4322        }
4323
4324        // Add the group into the review database.
4325        if (!isset($reviewStatus[0]["status"]) || (isset($reviewStatus[0]["status"]) && $reviewStatus[0]["status"]!=-2)) {
4326            $queryStr = "INSERT INTO `tblDocumentReviewers` (`documentID`, `version`, `type`, `required`) ".
4327                "VALUES ('". $this->_document->getID() ."', '". $this->_version ."', '1', '". $groupID ."')";
4328            $res = $db->getResult($queryStr);
4329            if (is_bool($res) && !$res) {
4330                return false;
4331            }
4332            $reviewID = $db->getInsertID('tblDocumentReviewers', 'reviewID');
4333        }
4334        else {
4335            $reviewID = isset($reviewStatus[0]["reviewID"])?$reviewStatus[0]["reviewID"]:NULL;
4336        }
4337
4338        $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`, `comment`, `date`, `userID`) ".
4339            "VALUES ('". $reviewID ."', '0', '', ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4340        $res = $db->getResult($queryStr);
4341        if (is_bool($res) && !$res) {
4342            return false;
4343        }
4344
4345        $reviewLogID = $db->getInsertID('tblDocumentReviewLog', 'reviewLogID');
4346        $db->dropTemporaryTable('ttreviewid');
4347        return $reviewLogID;
4348    } /* }}} */
4349
4350    /**
4351     * Add a review to the document content
4352     *
4353     * This method will add an entry to the table tblDocumentReviewLog.
4354     * It will first check if the user is ment to review the document version.
4355     * It not the return value is -3.
4356     * Next it will check if the users has been removed from the list of
4357     * reviewers. In that case -4 will be returned.
4358     * If the given review status has been set by the user before, it cannot
4359     * be set again and 0 will be returned. Ð†f the review could be succesfully
4360     * added, the review log id will be returned.
4361     *
4362     * @see SeedDMS_Core_DocumentContent::setApprovalByInd()
4363     *
4364     * @param object  $user user doing the review
4365     * @param object  $requestUser user asking for the review, this is mostly
4366     * the user currently logged in.
4367     * @param integer $status status of review
4368     * @param string  $comment comment for review
4369     *
4370     * @return integer|bool new review log id, error code 0 till -4,
4371     * false in case of an sql error
4372     */
4373    function setReviewByInd($user, $requestUser, $status, $comment, $file='') { /* {{{ */
4374        if(!$user || !$requestUser)
4375            return -1;
4376
4377        $db = $this->_document->getDMS()->getDB();
4378
4379        if(!$user->isType('user'))
4380            return -1;
4381
4382        // Check if the user is on the review list at all.
4383        $reviewStatus = $user->getReviewStatus($this->_document->getID(), $this->_version);
4384        if (is_bool($reviewStatus) && !$reviewStatus) {
4385            return false;
4386        }
4387        if (count($reviewStatus["indstatus"])==0) {
4388            // User is not assigned to review this document. No action required.
4389            // Return an error.
4390            return -3;
4391        }
4392        $indstatus = array_pop($reviewStatus["indstatus"]);
4393        if ($indstatus["status"]==-2) {
4394            // User has been deleted from reviewers
4395            return -4;
4396        }
4397        // Check if the status is really different from the current status
4398        if ($indstatus["status"] == $status)
4399            return 0;
4400
4401        $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`,
4402            `comment`, `date`, `userID`) ".
4403            "VALUES ('". $indstatus["reviewID"] ."', '".
4404            (int) $status ."', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '".
4405            $requestUser->getID() ."')";
4406        $res=$db->getResult($queryStr);
4407        if (is_bool($res) && !$res)
4408            return false;
4409
4410        $reviewLogID = $db->getInsertID('tblDocumentReviewLog', 'reviewLogID');
4411        if($file) {
4412            SeedDMS_Core_File::copyFile($file, $this->_dms->contentDir . $this->_document->getDir() . 'r' . $reviewLogID);
4413        }
4414        return $reviewLogID;
4415    } /* }}} */
4416
4417    /**
4418     * Add another entry to review log which resets the status
4419     *
4420     * This method will not delete anything from the database, but will add
4421     * a new review log entry which sets the status to 0. This is only allowed
4422     * if the current status is either 1 (reviewed) or -1 (rejected).
4423     *
4424     * After calling this method SeedDMS_Core_DocumentCategory::verifyStatus()
4425     * should be called to recalculate the document status.
4426     *
4427     * @param integer $reviewid id of review
4428     * @param SeedDMS_Core_User $requestUser user requesting the removal
4429     * @param string $comment comment
4430     *
4431     * @return integer|bool true if successful, error code < 0,
4432     * false in case of an sql error
4433     */
4434    public function removeReview($reviewid, $requestUser, $comment='') { /* {{{ */
4435        $db = $this->_document->getDMS()->getDB();
4436
4437        // Check to see if the user can be removed from the review list.
4438        $reviews = $this->getReviewStatus();
4439        if (is_bool($reviews) && !$reviews) {
4440            return false;
4441        }
4442        $reviewStatus = null;
4443        foreach($reviews as $review) {
4444            if($review['reviewID'] == $reviewid) {
4445                $reviewStatus = $review;
4446                break;
4447            }
4448        }
4449        if(!$reviewStatus)
4450            return -2;
4451
4452        // The review log entry may only be removed if the status is 1 or -1
4453        if ($reviewStatus["status"] != 1 && $reviewStatus["status"] != -1)
4454            return -3;
4455
4456        $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`,
4457            `comment`, `date`, `userID`) ".
4458            "VALUES ('". $reviewStatus["reviewID"] ."', '0', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '".
4459            $requestUser->getID() ."')";
4460        $res=$db->getResult($queryStr);
4461        if (is_bool($res) && !$res)
4462            return false;
4463
4464        return true;
4465    } /* }}} */
4466
4467    /**
4468     * Add a review to the document content
4469     *
4470     * This method is similar to
4471     * {@see SeedDMS_Core_DocumentContent::setReviewByInd()} but adds a review
4472     * for a group instead of a user.
4473     *
4474     * @param object  $group group doing the review
4475     * @param object  $requestUser user asking for the review, this is mostly
4476     * the user currently logged in.
4477     * @param integer $status status of review
4478     * @param string  $comment comment for review
4479     *
4480     * @return integer|bool new review log id, error code 0 till -4,
4481     * false in case of an sql error
4482     */
4483    function setReviewByGrp($group, $requestUser, $status, $comment, $file='') { /* {{{ */
4484        if(!$group || !$requestUser)
4485            return -1;
4486
4487        $db = $this->_document->getDMS()->getDB();
4488
4489        if(!$group->isType('group'))
4490                return -1;
4491
4492        // Check if the group is on the review list at all.
4493        $reviewStatus = $group->getReviewStatus($this->_document->getID(), $this->_version);
4494        if (is_bool($reviewStatus) && !$reviewStatus) {
4495            return false;
4496        }
4497        if (count($reviewStatus)==0) {
4498            // User is not assigned to review this document. No action required.
4499            // Return an error.
4500            return -3;
4501        }
4502        if ((int) $reviewStatus[0]["status"]==-2) {
4503            // Group has been deleted from reviewers
4504            return -4;
4505        }
4506
4507        // Check if the status is really different from the current status
4508        if ($reviewStatus[0]["status"] == $status)
4509            return 0;
4510
4511        $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`,
4512            `comment`, `date`, `userID`) ".
4513            "VALUES ('". $reviewStatus[0]["reviewID"] ."', '".
4514            (int) $status ."', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '".
4515            $requestUser->getID() ."')";
4516        $res=$db->getResult($queryStr);
4517        if (is_bool($res) && !$res)
4518            return false;
4519
4520        $reviewLogID = $db->getInsertID('tblDocumentReviewLog', 'reviewLogID');
4521        if($file) {
4522            SeedDMS_Core_File::copyFile($file, $this->_dms->contentDir . $this->_document->getDir() . 'r' . $reviewLogID);
4523        }
4524        return $reviewLogID;
4525 } /* }}} */
4526
4527    /**
4528     * Add user as new approver
4529     *
4530     * @param object $user user in charge for the approval
4531     * @param object $requestUser user requesting the operation (usually the
4532     * currently logged in user)
4533     *
4534     * @return integer|false if > 0 the id of the approval log, if < 0 the error
4535     * code, false in case of an sql error
4536     */
4537    function addIndApprover($user, $requestUser) { /* {{{ */
4538        if(!$user || !$requestUser)
4539            return -1;
4540
4541        $db = $this->_document->getDMS()->getDB();
4542
4543        if(!$user->isType('user'))
4544            return -1;
4545
4546        $userID = $user->getID();
4547
4548        // Get the list of users and groups with read access to this document.
4549        if($this->_document->getAccessMode($user) < M_READ) {
4550            return -2;
4551        }
4552
4553        // Check if the user has already been added to the approvers list.
4554        $approvalStatus = $user->getApprovalStatus($this->_document->getID(), $this->_version);
4555        if (is_bool($approvalStatus) && !$approvalStatus) {
4556            return false;
4557        }
4558        $indstatus = false;
4559        if (count($approvalStatus["indstatus"]) > 0) {
4560            $indstatus = array_pop($approvalStatus["indstatus"]);
4561            if($indstatus["status"]!=-2) {
4562                // User is already on the list of approverss; return an error.
4563                return -3;
4564            }
4565        }
4566
4567        if ( !$indstatus || (isset($indstatus["status"]) && $indstatus["status"]!=-2)) {
4568            // Add the user into the approvers database.
4569            $queryStr = "INSERT INTO `tblDocumentApprovers` (`documentID`, `version`, `type`, `required`) ".
4570                "VALUES ('". $this->_document->getID() ."', '". $this->_version ."', '0', '". $userID ."')";
4571            $res = $db->getResult($queryStr);
4572            if (is_bool($res) && !$res) {
4573                return false;
4574            }
4575            $approveID = $db->getInsertID('tblDocumentApprovers', 'approveID');
4576        }
4577        else {
4578            $approveID = isset($indstatus["approveID"]) ? $indstatus["approveID"] : NULL;
4579        }
4580
4581        $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`, `comment`, `date`, `userID`) ".
4582            "VALUES ('". $approveID ."', '0', '', ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4583        $res = $db->getResult($queryStr);
4584        if (is_bool($res) && !$res) {
4585            return false;
4586        }
4587
4588        $approveLogID = $db->getInsertID('tblDocumentApproveLog', 'approveLogID');
4589        $db->dropTemporaryTable('ttapproveid');
4590        return $approveLogID;
4591    } /* }}} */
4592
4593    /**
4594     * Add group as new approver
4595     *
4596     * @param object $group group in charge for the approval
4597     * @param object $requestUser user requesting the operation (usually the
4598     * currently logged in user)
4599     *
4600     * @return integer|false if > 0 the id of the approval log, if < 0 the error
4601     * code, false in case of an sql error
4602     */
4603    function addGrpApprover($group, $requestUser) { /* {{{ */
4604        if(!$group || !$requestUser)
4605            return -1;
4606
4607        $db = $this->_document->getDMS()->getDB();
4608
4609        if(!$group->isType('group'))
4610            return -1;
4611
4612        $groupID = $group->getID();
4613
4614        // Get the list of users and groups with read access to this document.
4615        if (!isset($this->_readAccessList)) {
4616            // TODO: error checking.
4617            $this->_readAccessList = $this->_document->getReadAccessList();
4618        }
4619        $approved = false;
4620        foreach ($this->_readAccessList["groups"] as $appGroup) {
4621            if ($groupID == $appGroup->getID()) {
4622                $approved = true;
4623                break;
4624            }
4625        }
4626        if (!$approved) {
4627            return -2;
4628        }
4629
4630        // Check if the group has already been added to the approver list.
4631        $approvalStatus = $group->getApprovalStatus($this->_document->getID(), $this->_version);
4632        if (is_bool($approvalStatus) && !$approvalStatus) {
4633            return false;
4634        }
4635        if (count($approvalStatus) > 0 && $approvalStatus[0]["status"]!=-2) {
4636            // Group is already on the list of approvers; return an error.
4637            return -3;
4638        }
4639
4640        // Add the group into the approver database.
4641        if (!isset($approvalStatus[0]["status"]) || (isset($approvalStatus[0]["status"]) && $approvalStatus[0]["status"]!=-2)) {
4642            $queryStr = "INSERT INTO `tblDocumentApprovers` (`documentID`, `version`, `type`, `required`) ".
4643                "VALUES ('". $this->_document->getID() ."', '". $this->_version ."', '1', '". $groupID ."')";
4644            $res = $db->getResult($queryStr);
4645            if (is_bool($res) && !$res) {
4646                return false;
4647            }
4648            $approveID = $db->getInsertID('tblDocumentApprovers', 'approveID');
4649        }
4650        else {
4651            $approveID = isset($approvalStatus[0]["approveID"])?$approvalStatus[0]["approveID"]:NULL;
4652        }
4653
4654        $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`, `comment`, `date`, `userID`) ".
4655            "VALUES ('". $approveID ."', '0', '', ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4656        $res = $db->getResult($queryStr);
4657        if (is_bool($res) && !$res) {
4658            return false;
4659        }
4660
4661        $approveLogID = $db->getInsertID('tblDocumentApproveLog', 'approveLogID');
4662        $db->dropTemporaryTable('ttapproveid');
4663        return $approveLogID;
4664    } /* }}} */
4665
4666    /**
4667     * Sets approval status of a document content for a user
4668     *
4669     * This function can be used to approve or reject a document content, or
4670     * to reset its approval state. In most cases this function will be
4671     * called by an user, but  an admin may set the approval for
4672     * somebody else.
4673     * It is first checked if the user is in the list of approvers at all.
4674     * Then it is check if the approval status is already -2. In both cases
4675     * the function returns with an error.
4676     *
4677     * @see SeedDMS_Core_DocumentContent::setReviewByInd()
4678     *
4679     * @param object  $user user in charge for doing the approval
4680     * @param object  $requestUser user actually calling this function
4681     * @param integer $status the status of the approval, possible values are
4682     *        0=unprocessed (maybe used to reset a status)
4683     *        1=approved,
4684     *       -1=rejected,
4685     *       -2=user is deleted (use {link
4686     *       SeedDMS_Core_DocumentContent::delIndApprover} instead)
4687     * @param string $comment approval comment
4688     *
4689     * @return integer|bool new review log id, error code 0 till -4,
4690     * false in case of an sql error
4691     */
4692    function setApprovalByInd($user, $requestUser, $status, $comment, $file='') { /* {{{ */
4693        if(!$user || !$requestUser)
4694            return -1;
4695
4696        $db = $this->_document->getDMS()->getDB();
4697
4698        if(!$user->isType('user'))
4699            return -1;
4700
4701        // Check if the user is on the approval list at all.
4702        $approvalStatus = $user->getApprovalStatus($this->_document->getID(), $this->_version);
4703        if (is_bool($approvalStatus) && !$approvalStatus) {
4704            return false;
4705        }
4706        if (count($approvalStatus["indstatus"])==0) {
4707            // User is not assigned to approve this document. No action required.
4708            // Return an error.
4709            return -3;
4710        }
4711        $indstatus = array_pop($approvalStatus["indstatus"]);
4712        if ($indstatus["status"]==-2) {
4713            // User has been deleted from approvers
4714            return -4;
4715        }
4716        // Check if the status is really different from the current status
4717        if ($indstatus["status"] == $status)
4718            return 0;
4719
4720        $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`,
4721            `comment`, `date`, `userID`) ".
4722            "VALUES ('". $indstatus["approveID"] ."', '".
4723            (int) $status ."', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '".
4724            $requestUser->getID() ."')";
4725        $res=$db->getResult($queryStr);
4726        if (is_bool($res) && !$res)
4727            return false;
4728
4729        $approveLogID = $db->getInsertID('tblDocumentApproveLog', 'approveLogID');
4730        if($file) {
4731            SeedDMS_Core_File::copyFile($file, $this->_dms->contentDir . $this->_document->getDir() . 'a' . $approveLogID);
4732        }
4733        return $approveLogID;
4734    } /* }}} */
4735
4736    /**
4737     * Add another entry to approval log which resets the status
4738     *
4739     * This method will not delete anything from the database, but will add
4740     * a new approval log entry which sets the status to 0. This is only allowed
4741     * if the current status is either 1 (approved) or -1 (rejected).
4742     *
4743     * After calling this method SeedDMS_Core_DocumentCategory::verifyStatus()
4744     * should be called to recalculate the document status.
4745     *
4746     * @param integer $approveid id of approval
4747     * @param SeedDMS_Core_User $requestUser user requesting the removal
4748     * @param string $comment comment
4749     *
4750     * @return integer|bool true if successful, error code < 0,
4751     * false in case of an sql error
4752     */
4753    public function removeApproval($approveid, $requestUser, $comment='') { /* {{{ */
4754        $db = $this->_document->getDMS()->getDB();
4755
4756        // Check to see if the user can be removed from the approval list.
4757        $approvals = $this->getApprovalStatus();
4758        if (is_bool($approvals) && !$approvals) {
4759            return false;
4760        }
4761        $approvalStatus = null;
4762        foreach($approvals as $approval) {
4763            if($approval['approveID'] == $approveid) {
4764                $approvalStatus = $approval;
4765                break;
4766            }
4767        }
4768        if(!$approvalStatus)
4769            return -2;
4770
4771        // The approval log entry may only be removed if the status is 1 or -1
4772        if ($approvalStatus["status"] != 1 && $approvalStatus["status"] != -1)
4773            return -3;
4774
4775        $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`,
4776            `comment`, `date`, `userID`) ".
4777            "VALUES ('". $approvalStatus["approveID"] ."', '0', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '".
4778            $requestUser->getID() ."')";
4779        $res=$db->getResult($queryStr);
4780        if (is_bool($res) && !$res)
4781            return false;
4782
4783        return true;
4784    } /* }}} */
4785
4786    /**
4787     * Sets approval status of a document content for a group
4788     *
4789     * The functions behaves like
4790     * {link SeedDMS_Core_DocumentContent::setApprovalByInd} but does it for
4791     * a group instead of a user
4792     */
4793    function setApprovalByGrp($group, $requestUser, $status, $comment, $file='') { /* {{{ */
4794        if(!$group || !$requestUser)
4795            return -1;
4796
4797        $db = $this->_document->getDMS()->getDB();
4798
4799        if(!$group->isType('group'))
4800            return -1;
4801
4802        // Check if the group is on the approval list at all.
4803        $approvalStatus = $group->getApprovalStatus($this->_document->getID(), $this->_version);
4804        if (is_bool($approvalStatus) && !$approvalStatus) {
4805            return false;
4806        }
4807        if (count($approvalStatus)==0) {
4808            // User is not assigned to approve this document. No action required.
4809            // Return an error.
4810            return -3;
4811        }
4812        if ($approvalStatus[0]["status"]==-2) {
4813            // Group has been deleted from approvers
4814            return -4;
4815        }
4816
4817        // Check if the status is really different from the current status
4818        if ($approvalStatus[0]["status"] == $status)
4819            return 0;
4820
4821        $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`,
4822            `comment`, `date`, `userID`) ".
4823            "VALUES ('". $approvalStatus[0]["approveID"] ."', '".
4824            (int) $status ."', ".$db->qstr($comment).", ".$db->getCurrentDatetime().", '".
4825            $requestUser->getID() ."')";
4826        $res=$db->getResult($queryStr);
4827        if (is_bool($res) && !$res)
4828            return false;
4829
4830        $approveLogID = $db->getInsertID('tblDocumentApproveLog', 'approveLogID');
4831        if($file) {
4832            SeedDMS_Core_File::copyFile($file, $this->_dms->contentDir . $this->_document->getDir() . 'a' . $approveLogID);
4833        }
4834        return $approveLogID;
4835    } /* }}} */
4836
4837    function delIndReviewer($user, $requestUser, $msg='') { /* {{{ */
4838        $db = $this->_document->getDMS()->getDB();
4839
4840        if(!$user->isType('user'))
4841            return -1;
4842
4843        // Check to see if the user can be removed from the review list.
4844        $reviewStatus = $user->getReviewStatus($this->_document->getID(), $this->_version);
4845        if (is_bool($reviewStatus) && !$reviewStatus) {
4846            return false;
4847        }
4848        if (count($reviewStatus["indstatus"])==0) {
4849            // User is not assigned to review this document. No action required.
4850            // Return an error.
4851            return -2;
4852        }
4853        $indstatus = array_pop($reviewStatus["indstatus"]);
4854        if ($indstatus["status"]!=0) {
4855            // User has already submitted a review or has already been deleted;
4856            // return an error.
4857            return -3;
4858        }
4859
4860        $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`, `comment`, `date`, `userID`) ".
4861            "VALUES ('". $indstatus["reviewID"] ."', '".S_LOG_USER_REMOVED."', ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4862        $res = $db->getResult($queryStr);
4863        if (is_bool($res) && !$res) {
4864            return false;
4865        }
4866
4867        return 0;
4868    } /* }}} */
4869
4870    function delGrpReviewer($group, $requestUser, $msg='') { /* {{{ */
4871        $db = $this->_document->getDMS()->getDB();
4872
4873        if(!$group->isType('group'))
4874            return -1;
4875
4876        $groupID = $group->getID();
4877
4878        // Check to see if the user can be removed from the review list.
4879        $reviewStatus = $group->getReviewStatus($this->_document->getID(), $this->_version);
4880        if (is_bool($reviewStatus) && !$reviewStatus) {
4881            return false;
4882        }
4883        if (count($reviewStatus)==0) {
4884            // User is not assigned to review this document. No action required.
4885            // Return an error.
4886            return -2;
4887        }
4888        if ($reviewStatus[0]["status"]!=0) {
4889            // User has already submitted a review or has already been deleted;
4890            // return an error.
4891            return -3;
4892        }
4893
4894        $queryStr = "INSERT INTO `tblDocumentReviewLog` (`reviewID`, `status`, `comment`, `date`, `userID`) ".
4895            "VALUES ('". $reviewStatus[0]["reviewID"] ."', '".S_LOG_USER_REMOVED."', ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4896        $res = $db->getResult($queryStr);
4897        if (is_bool($res) && !$res) {
4898            return false;
4899        }
4900
4901        return 0;
4902    } /* }}} */
4903
4904    function delIndApprover($user, $requestUser, $msg='') { /* {{{ */
4905        $db = $this->_document->getDMS()->getDB();
4906
4907        if(!$user->isType('user'))
4908            return -1;
4909
4910        $userID = $user->getID();
4911
4912        // Check if the user is on the approval list at all.
4913        $approvalStatus = $user->getApprovalStatus($this->_document->getID(), $this->_version);
4914        if (is_bool($approvalStatus) && !$approvalStatus) {
4915            return false;
4916        }
4917        if (count($approvalStatus["indstatus"])==0) {
4918            // User is not assigned to approve this document. No action required.
4919            // Return an error.
4920            return -2;
4921        }
4922        $indstatus = array_pop($approvalStatus["indstatus"]);
4923        if ($indstatus["status"]!=0) {
4924            // User has already submitted an approval or has already been deleted;
4925            // return an error.
4926            return -3;
4927        }
4928
4929        $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`, `comment`, `date`, `userID`) ".
4930            "VALUES ('". $indstatus["approveID"] ."', '".S_LOG_USER_REMOVED."', ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4931        $res = $db->getResult($queryStr);
4932        if (is_bool($res) && !$res) {
4933            return false;
4934        }
4935
4936        return 0;
4937    } /* }}} */
4938
4939    function delGrpApprover($group, $requestUser, $msg='') { /* {{{ */
4940        $db = $this->_document->getDMS()->getDB();
4941
4942        if(!$group->isType('group'))
4943            return -1;
4944
4945        $groupID = $group->getID();
4946
4947        // Check if the group is on the approval list at all.
4948        $approvalStatus = $group->getApprovalStatus($this->_document->getID(), $this->_version);
4949        if (is_bool($approvalStatus) && !$approvalStatus) {
4950            return false;
4951        }
4952        if (count($approvalStatus)==0) {
4953            // User is not assigned to approve this document. No action required.
4954            // Return an error.
4955            return -2;
4956        }
4957        if ($approvalStatus[0]["status"]!=0) {
4958            // User has already submitted an approval or has already been deleted;
4959            // return an error.
4960            return -3;
4961        }
4962
4963        $queryStr = "INSERT INTO `tblDocumentApproveLog` (`approveID`, `status`, `comment`, `date`, `userID`) ".
4964            "VALUES ('". $approvalStatus[0]["approveID"] ."', '".S_LOG_USER_REMOVED."', ".$db->qstr($msg).", ".$db->getCurrentDatetime().", '". $requestUser->getID() ."')";
4965        $res = $db->getResult($queryStr);
4966        if (is_bool($res) && !$res) {
4967            return false;
4968        }
4969
4970        return 0;
4971    } /* }}} */
4972
4973    /**
4974     * Set state of workflow assigned to the document content
4975     *
4976     * @param object $state
4977     */
4978    function setWorkflowState($state) { /* {{{ */
4979        $db = $this->_document->getDMS()->getDB();
4980
4981        if($this->_workflow) {
4982            $queryStr = "UPDATE `tblWorkflowDocumentContent` set `state`=". $state->getID() ." WHERE `workflow`=". intval($this->_workflow->getID()). " AND `document`=". intval($this->_document->getID()) ." AND version=". intval($this->_version) ."";
4983            if (!$db->getResult($queryStr)) {
4984                return false;
4985            }
4986            $this->_workflowState = $state;
4987            return true;
4988        }
4989        return false;
4990    } /* }}} */
4991
4992    /**
4993     * Get state of workflow assigned to the document content
4994     *
4995     * @return object/boolean an object of class SeedDMS_Core_Workflow_State
4996     *         or false in case of error, e.g. the version has not a workflow
4997     */
4998    function getWorkflowState() { /* {{{ */
4999        $db = $this->_document->getDMS()->getDB();
5000
5001        if(!$this->_workflow)
5002            $this->getWorkflow();
5003
5004        if(!$this->_workflow)
5005            return false;
5006
5007        if (!$this->_workflowState) {
5008            $queryStr=
5009                "SELECT b.* FROM `tblWorkflowDocumentContent` a LEFT JOIN `tblWorkflowStates` b ON a.`state` = b.`id` WHERE a.`state` IS NOT NULL AND `workflow`=". intval($this->_workflow->getID())
5010                ." AND a.`version`='".$this->_version
5011                ."' AND a.`document` = '". $this->_document->getID() ."' ";
5012            $recs = $db->getResultArray($queryStr);
5013            if (!$recs)
5014                return false;
5015            $this->_workflowState = new SeedDMS_Core_Workflow_State($recs[0]['id'], $recs[0]['name'], $recs[0]['maxtime'], $recs[0]['precondfunc'], $recs[0]['documentstatus']);
5016            $this->_workflowState->setDMS($this->_document->getDMS());
5017        }
5018        return $this->_workflowState;
5019    } /* }}} */
5020
5021    /**
5022     * Assign a workflow to a document content
5023     *
5024     * @param object $workflow
5025     */
5026    function setWorkflow($workflow, $user) { /* {{{ */
5027        $db = $this->_document->getDMS()->getDB();
5028
5029        $this->getWorkflow();
5030        if($this->_workflow)
5031            return false;
5032
5033        if($workflow && is_object($workflow)) {
5034            $db->startTransaction();
5035            $initstate = $workflow->getInitState();
5036            $queryStr = "INSERT INTO `tblWorkflowDocumentContent` (`workflow`, `document`, `version`, `state`, `date`) VALUES (". $workflow->getID(). ", ". $this->_document->getID() .", ". $this->_version .", ".$initstate->getID().", ".$db->getCurrentDatetime().")";
5037            if (!$db->getResult($queryStr)) {
5038                $db->rollbackTransaction();
5039                return false;
5040            }
5041            $this->_workflow = $workflow;
5042            if(!$this->setStatus(S_IN_WORKFLOW, "Added workflow '".$workflow->getName()."'", $user)) {
5043                $db->rollbackTransaction();
5044                return false;
5045            }
5046            $db->commitTransaction();
5047            return true;
5048        }
5049        return false;
5050    } /* }}} */
5051
5052    /**
5053     * Get workflow assigned to the document content
5054     *
5055     * The method returns the last workflow if one was assigned.
5056     * If the document version is in a sub workflow, it will have
5057     * a never date and therefore will be found first.
5058     *
5059     * @return object/boolean an object of class SeedDMS_Core_Workflow
5060     *         or false in case of error, e.g. the version has not a workflow
5061     */
5062    function getWorkflow() { /* {{{ */
5063        $db = $this->_document->getDMS()->getDB();
5064
5065        if (!$this->_workflow) {
5066            $queryStr=
5067                "SELECT b.* FROM `tblWorkflowDocumentContent` a LEFT JOIN `tblWorkflows` b ON a.`workflow` = b.`id` WHERE a.`version`='".$this->_version
5068                ."' AND a.`document` = '". $this->_document->getID() ."' "
5069                ." ORDER BY `date` DESC LIMIT 1";
5070            $recs = $db->getResultArray($queryStr);
5071            if (is_bool($recs) && !$recs)
5072                return false;
5073            if(!$recs)
5074                return false;
5075            $this->_workflow = new SeedDMS_Core_Workflow($recs[0]['id'], $recs[0]['name'], $this->_document->getDMS()->getWorkflowState($recs[0]['initstate']));
5076            $this->_workflow->setDMS($this->_document->getDMS());
5077        }
5078        return $this->_workflow;
5079    } /* }}} */
5080
5081    /**
5082     * Rewrites the complete workflow log
5083     *
5084     * Attention: this function is highly dangerous.
5085     * It removes an existing workflow log and rewrites it.
5086     * This method was added for importing an xml dump.
5087     *
5088     * @param array $workflowlog new workflow log with the newest log entry first.
5089     * @return boolean true on success, otherwise false
5090     */
5091    function rewriteWorkflowLog($workflowlog) { /* {{{ */
5092        $db = $this->_document->getDMS()->getDB();
5093
5094        $db->startTransaction();
5095
5096        /* First, remove the old entries */
5097        $queryStr = "DELETE FROM `tblWorkflowLog` WHERE `tblWorkflowLog`.`document` = '". $this->_document->getID() ."' AND `tblWorkflowLog`.`version` = '". $this->_version ."'";
5098        if (!$db->getResult($queryStr)) {
5099            $db->rollbackTransaction();
5100            return false;
5101        }
5102
5103        /* Second, insert the new entries */
5104        $workflowlog = array_reverse($workflowlog);
5105        foreach($workflowlog as $log) {
5106            if(!SeedDMS_Core_DMS::checkDate($log['date'], 'Y-m-d H:i:s')) {
5107                $db->rollbackTransaction();
5108                return false;
5109            }
5110            $queryStr = "INSERT INTO `tblWorkflowLog` (`document`, `version`, `workflow`, `transition`, `comment`, `date`, `userid`) ".
5111                "VALUES ('".$this->_document->getID() ."', '".(int) $this->_version."', '".(int) $log['workflow']->getID()."', '".(int) $log['transition']->getID()."', ".$db->qstr($log['comment']) .", ".$db->qstr($log['date']).", ".$log['user']->getID().")";
5112            if (!$db->getResult($queryStr)) {
5113                $db->rollbackTransaction();
5114                return false;
5115            }
5116        }
5117
5118        $db->commitTransaction();
5119        return true;
5120    } /* }}} */
5121
5122    /**
5123     * Restart workflow from its initial state
5124     *
5125     * @return boolean true if workflow could be restarted
5126     *         or false in case of error
5127     */
5128    function rewindWorkflow() { /* {{{ */
5129        $db = $this->_document->getDMS()->getDB();
5130
5131        $this->getWorkflow();
5132
5133        if (!$this->_workflow) {
5134            return true;
5135        }
5136
5137        $db->startTransaction();
5138        $queryStr = "DELETE from `tblWorkflowLog` WHERE `document` = ". $this->_document->getID() ." AND `version` = ".$this->_version." AND `workflow` = ".$this->_workflow->getID();
5139        if (!$db->getResult($queryStr)) {
5140            $db->rollbackTransaction();
5141            return false;
5142        }
5143
5144        $this->setWorkflowState($this->_workflow->getInitState());
5145        $db->commitTransaction();
5146
5147        return true;
5148    } /* }}} */
5149
5150    /**
5151     * Remove workflow
5152     *
5153     * Fully removing a workflow including entries in the workflow log is
5154     * only allowed if the workflow is still its initial state.
5155     * At a later point of time only unlinking the document from the
5156     * workflow is allowed. It will keep any log entries.
5157     * A workflow is unlinked from a document when enterNextState()
5158     * succeeds.
5159     *
5160     * @param object $user user doing initiating the removal
5161     * @param boolean $unlink if true, just unlink the workflow from the
5162     *        document but do not remove the workflow log. The $unlink
5163     *        flag has been added to detach the workflow from the document
5164     *        when it has reached a valid end state
5165              (see SeedDMS_Core_DocumentContent::enterNextState())
5166     * @return boolean true if workflow could be removed
5167     *         or false in case of error
5168     */
5169    function removeWorkflow($user, $unlink=false) { /* {{{ */
5170        $db = $this->_document->getDMS()->getDB();
5171
5172        $this->getWorkflow();
5173
5174        if (!$this->_workflow) {
5175            return true;
5176        }
5177
5178        /* A workflow should always be in a state, but in case it isn't, the
5179         * at least allow to remove the workflow.
5180         */
5181        $currentstate = $this->getWorkflowState();
5182        if(!$currentstate || SeedDMS_Core_DMS::checkIfEqual($this->_workflow->getInitState(), $currentstate) || $unlink == true) {
5183            $db->startTransaction();
5184            if(!$unlink) {
5185                $queryStr=
5186                    "DELETE FROM `tblWorkflowLog` WHERE "
5187                    ."`version`='".$this->_version."' "
5188                    ." AND `document` = '". $this->_document->getID() ."' "
5189                    ." AND `workflow` = '". $this->_workflow->getID() ."' ";
5190                if (!$db->getResult($queryStr)) {
5191                    $db->rollbackTransaction();
5192                    return false;
5193                }
5194            }
5195            $queryStr=
5196                "DELETE FROM `tblWorkflowDocumentContent` WHERE "
5197                ."`version`='".$this->_version."' "
5198                ." AND `document` = '". $this->_document->getID() ."' "
5199                ." AND `workflow` = '". $this->_workflow->getID() ."' ";
5200            if (!$db->getResult($queryStr)) {
5201                $db->rollbackTransaction();
5202                return false;
5203            }
5204            $this->_workflow = null;
5205            $this->_workflowState = null;
5206            $this->verifyStatus(false, $user, 'Workflow removed');
5207            $db->commitTransaction();
5208        }
5209
5210        return true;
5211    } /* }}} */
5212
5213    /**
5214     * Run a sub workflow
5215     *
5216     * @param object $subworkflow
5217     */
5218    function getParentWorkflow() { /* {{{ */
5219        $db = $this->_document->getDMS()->getDB();
5220
5221        /* document content must be in a workflow */
5222        $this->getWorkflow();
5223        if(!$this->_workflow)
5224            return false;
5225
5226        $queryStr=
5227            "SELECT * FROM `tblWorkflowDocumentContent` WHERE "
5228            ."`version`='".$this->_version."' "
5229            ." AND `document` = '". $this->_document->getID() ."' "
5230            ." AND `workflow` = '". $this->_workflow->getID() ."' ";
5231        $recs = $db->getResultArray($queryStr);
5232        if (is_bool($recs) && !$recs)
5233            return false;
5234        if(!$recs)
5235            return false;
5236
5237        if($recs[0]['parentworkflow'])
5238            return $this->_document->getDMS()->getWorkflow($recs[0]['parentworkflow']);
5239
5240        return false;
5241    } /* }}} */
5242
5243    /**
5244     * Run a sub workflow
5245     *
5246     * @param object $subworkflow
5247     */
5248    function runSubWorkflow($subworkflow) { /* {{{ */
5249        $db = $this->_document->getDMS()->getDB();
5250
5251        /* document content must be in a workflow */
5252        $this->getWorkflow();
5253        if(!$this->_workflow)
5254            return false;
5255
5256        /* The current workflow state must match the sub workflows initial state */
5257        if($subworkflow->getInitState()->getID() != $this->_workflowState->getID())
5258            return false;
5259
5260        if($subworkflow) {
5261            $initstate = $subworkflow->getInitState();
5262            $queryStr = "INSERT INTO `tblWorkflowDocumentContent` (`parentworkflow`, `workflow`, `document`, `version`, `state`, `date`) VALUES (". $this->_workflow->getID(). ", ". $subworkflow->getID(). ", ". $this->_document->getID() .", ". $this->_version .", ".$initstate->getID().", ".$db->getCurrentDatetime().")";
5263            if (!$db->getResult($queryStr)) {
5264                return false;
5265            }
5266            $this->_workflow = $subworkflow;
5267            return true;
5268        }
5269        return true;
5270    } /* }}} */
5271
5272    /**
5273     * Return from sub workflow to parent workflow.
5274     * The method will trigger the given transition
5275     *
5276     * FIXME: Needs much better checking if this is allowed
5277     *
5278     * @param object $user intiating the return
5279     * @param object $transtion to trigger
5280     * @param string comment for the transition trigger
5281     */
5282    function returnFromSubWorkflow($user, $transition=null, $comment='') { /* {{{ */
5283        $db = $this->_document->getDMS()->getDB();
5284
5285        /* document content must be in a workflow */
5286        $this->getWorkflow();
5287        if(!$this->_workflow)
5288            return false;
5289
5290        if ($this->_workflow) {
5291            $db->startTransaction();
5292
5293            $queryStr=
5294                "SELECT * FROM `tblWorkflowDocumentContent` WHERE `workflow`=". intval($this->_workflow->getID())
5295                . " AND `version`='".$this->_version
5296                ."' AND `document` = '". $this->_document->getID() ."' ";
5297            $recs = $db->getResultArray($queryStr);
5298            if (is_bool($recs) && !$recs) {
5299                $db->rollbackTransaction();
5300                return false;
5301            }
5302            if(!$recs) {
5303                $db->rollbackTransaction();
5304                return false;
5305            }
5306
5307            $queryStr = "DELETE FROM `tblWorkflowDocumentContent` WHERE `workflow` =". intval($this->_workflow->getID())." AND `document` = '". $this->_document->getID() ."' AND `version` = '" . $this->_version."'";
5308            if (!$db->getResult($queryStr)) {
5309                $db->rollbackTransaction();
5310                return false;
5311            }
5312
5313            $this->_workflow = $this->_document->getDMS()->getWorkflow($recs[0]['parentworkflow']);
5314            $this->_workflow->setDMS($this->_document->getDMS());
5315
5316            if($transition) {
5317                if(false === $this->triggerWorkflowTransition($user, $transition, $comment)) {
5318                    $db->rollbackTransaction();
5319                    return false;
5320                }
5321            }
5322
5323            $db->commitTransaction();
5324        }
5325        return $this->_workflow;
5326    } /* }}} */
5327
5328    /**
5329     * Check if the user is allowed to trigger the transition
5330     * A user is allowed if either the user itself or
5331     * a group of which the user is a member of is registered for
5332     * triggering a transition. This method does not change the workflow
5333     * state of the document content.
5334     *
5335     * @param object $user
5336     * @return boolean true if user may trigger transaction
5337     */
5338    function triggerWorkflowTransitionIsAllowed($user, $transition) { /* {{{ */
5339        $db = $this->_document->getDMS()->getDB();
5340
5341        if(!$this->_workflow)
5342            $this->getWorkflow();
5343
5344        if(!$this->_workflow)
5345            return false;
5346
5347        if(!$this->_workflowState)
5348            $this->getWorkflowState();
5349
5350        /* Check if the user has already triggered the transition */
5351        $queryStr=
5352            "SELECT * FROM `tblWorkflowLog` WHERE `version`='".$this->_version ."' AND `document` = '". $this->_document->getID() ."' AND `workflow` = ". $this->_workflow->getID(). " AND userid = ".$user->getID();
5353        $queryStr .= " AND `transition` = ".$transition->getID();
5354        $resArr = $db->getResultArray($queryStr);
5355        if (is_bool($resArr) && !$resArr)
5356            return false;
5357
5358        if(count($resArr))
5359            return false;
5360
5361        /* Get all transition users allowed to trigger the transition */
5362        $transusers = $transition->getUsers();
5363        if($transusers) {
5364            foreach($transusers as $transuser) {
5365                if($user->getID() == $transuser->getUser()->getID())
5366                    return true;
5367            }
5368        }
5369
5370        /* Get all transition groups whose members are allowed to trigger
5371         * the transition */
5372        $transgroups = $transition->getGroups();
5373        if($transgroups) {
5374            foreach($transgroups as $transgroup) {
5375                $group = $transgroup->getGroup();
5376                if($group->isMember($user))
5377                    return true;
5378            }
5379        }
5380
5381        return false;
5382    } /* }}} */
5383
5384    /**
5385     * Check if all conditions are met to change the workflow state
5386     * of a document content (run the transition).
5387     * The conditions are met if all explicitly set users and a sufficient
5388     * number of users of the groups have acknowledged the content.
5389     *
5390     * @return boolean true if transaction maybe executed
5391     */
5392    function executeWorkflowTransitionIsAllowed($transition) { /* {{{ */
5393        if(!$this->_workflow)
5394            $this->getWorkflow();
5395
5396        if(!$this->_workflow)
5397            return false;
5398
5399        if(!$this->_workflowState)
5400            $this->getWorkflowState();
5401
5402        /* Get the Log of transition triggers */
5403        $entries = $this->getWorkflowLog($transition);
5404        if(!$entries)
5405            return false;
5406
5407        /* Get all transition users allowed to trigger the transition
5408         * $allowedusers is a list of all users allowed to trigger the
5409         * transition
5410         */
5411        $transusers = $transition->getUsers();
5412        $allowedusers = array();
5413        foreach($transusers as $transuser) {
5414            $a = $transuser->getUser();
5415            $allowedusers[$a->getID()] = $a;
5416        }
5417
5418        /* Get all transition groups whose members are allowed to trigger
5419         * the transition */
5420        $transgroups = $transition->getGroups();
5421        foreach($entries as $entry) {
5422            $loguser = $entry->getUser();
5423            /* Unset each allowed user if it was found in the log */
5424            if(isset($allowedusers[$loguser->getID()]))
5425                unset($allowedusers[$loguser->getID()]);
5426            /* Also check groups if required. Count the group membership of
5427             * each user in the log in the array $gg
5428             */
5429            if($transgroups) {
5430                $loggroups = $loguser->getGroups();
5431                foreach($loggroups as $loggroup) {
5432                    if(!isset($gg[$loggroup->getID()]))
5433                        $gg[$loggroup->getID()] = 1;
5434                    else
5435                        $gg[$loggroup->getID()]++;
5436                }
5437            }
5438        }
5439        /* If there are allowed users left, then there some users still
5440         * need to trigger the transition.
5441         */
5442        if($allowedusers)
5443            return false;
5444
5445        if($transgroups) {
5446            foreach($transgroups as $transgroup) {
5447                $group = $transgroup->getGroup();
5448                $minusers = $transgroup->getNumOfUsers();
5449                if(!isset($gg[$group->getID()]))
5450                    return false;
5451                if($gg[$group->getID()] < $minusers)
5452                    return false;
5453            }
5454        }
5455        return true;
5456    } /* }}} */
5457
5458    /**
5459     * Trigger transition
5460     *
5461     * This method will be deprecated
5462     *
5463     * The method will first check if the user is allowed to trigger the
5464     * transition. If the user is allowed, an entry in the workflow log
5465     * will be added, which is later used to check if the transition
5466     * can actually be processed. The method will finally call
5467     * executeWorkflowTransitionIsAllowed() which checks all log entries
5468     * and does the transitions post function if all users and groups have
5469     * triggered the transition. Finally enterNextState() is called which
5470     * will try to enter the next state.
5471     *
5472     * @param object $user
5473     * @param object $transition
5474     * @param string $comment user comment
5475     * @return boolean/object next state if transition could be triggered and
5476     *         then next state could be entered,
5477     *         true if the transition could just be triggered or
5478     *         false in case of an error
5479     */
5480    function triggerWorkflowTransition($user, $transition, $comment='') { /* {{{ */
5481        $db = $this->_document->getDMS()->getDB();
5482
5483        if(!$this->_workflow)
5484            $this->getWorkflow();
5485
5486        if(!$this->_workflow)
5487            return false;
5488
5489        if(!$this->_workflowState)
5490            $this->getWorkflowState();
5491
5492        if(!$this->_workflowState)
5493            return false;
5494
5495        /* Check if the user is allowed to trigger the transition.
5496         */
5497        if(!$this->triggerWorkflowTransitionIsAllowed($user, $transition))
5498            return false;
5499
5500        $state = $this->_workflowState;
5501        $queryStr = "INSERT INTO `tblWorkflowLog` (`document`, `version`, `workflow`, `userid`, `transition`, `date`, `comment`) VALUES (".$this->_document->getID().", ".$this->_version.", " . (int) $this->_workflow->getID() . ", " .(int) $user->getID(). ", ".(int) $transition->getID().", ".$db->getCurrentDatetime().", ".$db->qstr($comment).")";
5502        if (!$db->getResult($queryStr))
5503            return false;
5504
5505        /* Check if this transition is processed. Run the post function in
5506         * that case. A transition is processed when all users and groups
5507         * have triggered it.
5508         */
5509        if($this->executeWorkflowTransitionIsAllowed($transition)) {
5510            /* run post function of transition */
5511//            echo "run post function of transition ".$transition->getID()."<br />";
5512        }
5513
5514        /* Go into the next state. This will only succeed if the pre condition
5515         * function of that states succeeds.
5516         */
5517        $nextstate = $transition->getNextState();
5518        if($this->enterNextState($user, $nextstate)) {
5519            return $nextstate;
5520        }
5521        return true;
5522
5523    } /* }}} */
5524
5525    /**
5526     * Enter next state of workflow if possible
5527     *
5528     * The method will check if one of the following states in the workflow
5529     * can be reached.
5530     * It does it by running
5531     * the precondition function of that state. The precondition function
5532     * gets a list of all transitions leading to the state. It will
5533     * determine, whether the transitions has been triggered and if that
5534     * is sufficient to enter the next state. If no pre condition function
5535     * is set, then 1 of n transtions are enough to enter the next state.
5536     *
5537     * If moving in the next state is possible and this state has a
5538     * corresponding document state, then the document state will be
5539     * updated and the workflow will be detached from the document.
5540     *
5541     * @param object $user
5542     * @param object $nextstate
5543     * @return boolean true if the state could be reached
5544     *         false if not
5545     */
5546    function enterNextState($user, $nextstate) { /* {{{ */
5547
5548            /* run the pre condition of the next state. If it is not set
5549             * the next state will be reached if one of the transitions
5550             * leading to the given state can be processed.
5551             */
5552            if($nextstate->getPreCondFunc() == '') {
5553                $transitions = $this->_workflow->getPreviousTransitions($nextstate);
5554                foreach($transitions as $transition) {
5555//                echo "transition ".$transition->getID()." led to state ".$nextstate->getName()."<br />";
5556                    if($this->executeWorkflowTransitionIsAllowed($transition)) {
5557//                    echo "stepping into next state<br />";
5558                        $this->setWorkflowState($nextstate);
5559
5560                        /* Check if the new workflow state has a mapping into a
5561                         * document state. If yes, set the document state will
5562                         * be updated and the workflow will be removed from the
5563                         * document.
5564                         */
5565                        $docstate = $nextstate->getDocumentStatus();
5566                        if($docstate == S_RELEASED || $docstate == S_REJECTED) {
5567                            $this->setStatus($docstate, "Workflow has ended", $user);
5568                            /* Detach the workflow from the document, but keep the
5569                             * workflow log
5570                             */
5571                            $this->removeWorkflow($user, true);
5572                            return true ;
5573                        }
5574
5575                        /* make sure the users and groups allowed to trigger the next
5576                         * transitions are also allowed to read the document
5577                         */
5578                        $transitions = $this->_workflow->getNextTransitions($nextstate);
5579                        foreach($transitions as $tran) {
5580//                            echo "checking access for users/groups allowed to trigger transition ".$tran->getID()."<br />";
5581                            $transusers = $tran->getUsers();
5582                            foreach($transusers as $transuser) {
5583                                $u = $transuser->getUser();
5584//                                echo $u->getFullName()."<br />";
5585                                if($this->_document->getAccessMode($u) < M_READ) {
5586                                    $this->_document->addAccess(M_READ, $u->getID(), 1);
5587//                                    echo "granted read access<br />";
5588                                } else {
5589//                                    echo "has already access<br />";
5590                                }
5591                            }
5592                            $transgroups = $tran->getGroups();
5593                            foreach($transgroups as $transgroup) {
5594                                $g = $transgroup->getGroup();
5595//                                echo $g->getName()."<br />";
5596                                if ($this->_document->getGroupAccessMode($g) < M_READ) {
5597                                    $this->_document->addAccess(M_READ, $g->getID(), 0);
5598//                                    echo "granted read access<br />";
5599                                } else {
5600//                                    echo "has already access<br />";
5601                                }
5602                            }
5603                        }
5604                        return(true);
5605                    } else {
5606//                        echo "transition not ready for process now<br />";
5607                    }
5608                }
5609                return false;
5610            } else {
5611            }
5612
5613    } /* }}} */
5614
5615    /**
5616     * Get the so far logged operations on the document content within the
5617     * workflow. Even after finishing the workflow (when the document content
5618     * does not have workflow set anymore) this function returns the list of all
5619     * log entries.
5620     *
5621     * @return array list of objects
5622     */
5623    function getWorkflowLog($transition = null) { /* {{{ */
5624        $db = $this->_document->getDMS()->getDB();
5625
5626/*
5627        if(!$this->_workflow)
5628            $this->getWorkflow();
5629
5630        if(!$this->_workflow)
5631            return false;
5632*/
5633        $queryStr=
5634            "SELECT * FROM `tblWorkflowLog` WHERE `version`='".$this->_version ."' AND `document` = '". $this->_document->getID() ."'"; // AND `workflow` = ". $this->_workflow->getID();
5635        if($transition)
5636            $queryStr .= " AND `transition` = ".$transition->getID();
5637        $queryStr .= " ORDER BY `date`";
5638        $resArr = $db->getResultArray($queryStr);
5639        if (is_bool($resArr) && !$resArr)
5640            return false;
5641
5642        $workflowlogs = array();
5643        for ($i = 0; $i < count($resArr); $i++) {
5644            $workflow = $this->_document->getDMS()->getWorkflow($resArr[$i]["workflow"]);
5645            $workflowlog = new SeedDMS_Core_Workflow_Log($resArr[$i]["id"], $this->_document->getDMS()->getDocument($resArr[$i]["document"]), $resArr[$i]["version"], $workflow, $this->_document->getDMS()->getUser($resArr[$i]["userid"]), $workflow->getTransition($resArr[$i]["transition"]), $resArr[$i]["date"], $resArr[$i]["comment"]);
5646            $workflowlog->setDMS($this);
5647            $workflowlogs[$i] = $workflowlog;
5648        }
5649
5650        return $workflowlogs;
5651    } /* }}} */
5652
5653    /**
5654     * Get the latest workflow log entry for the document content within the
5655     * workflow. Even after finishing the workflow (when the document content
5656     * does not have workflow set anymore) this function returns the last
5657     * log entry.
5658     *
5659     * @return object
5660     */
5661    function getLastWorkflowLog() { /* {{{ */
5662        $db = $this->_document->getDMS()->getDB();
5663
5664/*
5665        if(!$this->_workflow)
5666            $this->getWorkflow();
5667
5668        if(!$this->_workflow)
5669            return false;
5670 */
5671        $queryStr=
5672            "SELECT * FROM `tblWorkflowLog` WHERE `version`='".$this->_version ."' AND `document` = '". $this->_document->getID() ."'"; // AND `workflow` = ". $this->_workflow->getID();
5673        $queryStr .= " ORDER BY `id` DESC LIMIT 1";
5674        $resArr = $db->getResultArray($queryStr);
5675        if (is_bool($resArr) && !$resArr)
5676            return false;
5677
5678        $i = 0;
5679        $workflow = $this->_document->getDMS()->getWorkflow($resArr[$i]["workflow"]);
5680        $workflowlog = new SeedDMS_Core_Workflow_Log($resArr[$i]["id"], $this->_document->getDMS()->getDocument($resArr[$i]["document"]), $resArr[$i]["version"], $workflow, $this->_document->getDMS()->getUser($resArr[$i]["userid"]), $workflow->getTransition($resArr[$i]["transition"]), $resArr[$i]["date"], $resArr[$i]["comment"]);
5681        $workflowlog->setDMS($this);
5682
5683        return $workflowlog;
5684    } /* }}} */
5685
5686    /**
5687     * Check if the document content needs an action by a user
5688     *
5689     * This method will return true if document content is in a transition
5690     * which can be triggered by the given user.
5691     *
5692     * @param SeedDMS_Core_User $user
5693     * @return boolean true is action is needed
5694     */
5695    function needsWorkflowAction($user) { /* {{{ */
5696        $needwkflaction = false;
5697        if($this->_workflow) {
5698            if (!$this->_workflowState)
5699                $this->getWorkflowState();
5700            $workflowstate = $this->_workflowState;
5701            if($transitions = $this->_workflow->getNextTransitions($workflowstate)) {
5702                foreach($transitions as $transition) {
5703                    if($this->triggerWorkflowTransitionIsAllowed($user, $transition)) {
5704                        $needwkflaction = true;
5705                    }
5706                }
5707            }
5708        }
5709        return $needwkflaction;
5710    } /* }}} */
5711
5712    /**
5713     * Checks the internal data of the document version and repairs it.
5714     * Currently, this function only repairs a missing filetype
5715     *
5716     * @return boolean true on success, otherwise false
5717     */
5718    function repair() { /* {{{ */
5719        $dms = $this->_document->getDMS();
5720        $db = $this->_dms->getDB();
5721
5722        if(SeedDMS_Core_File::file_exists($this->_dms->contentDir.$this->_document->getDir() . $this->_version . $this->_fileType)) {
5723            if(strlen($this->_fileType) < 2) {
5724                switch($this->_mimeType) {
5725                case "application/pdf":
5726                case "image/png":
5727                case "image/gif":
5728                case "image/jpg":
5729                    $expect = substr($this->_mimeType, -3, 3);
5730                    if($this->_fileType != '.'.$expect) {
5731                        $db->startTransaction();
5732                        $queryStr = "UPDATE `tblDocumentContent` SET `fileType`='.".$expect."' WHERE `id` = ". $this->_id;
5733                        $res = $db->getResult($queryStr);
5734                        if ($res) {
5735                            if(!SeedDMS_Core_File::renameFile($this->_dms->contentDir.$this->_document->getDir() . $this->_version . $this->_fileType, $this->_dms->contentDir.$this->_document->getDir() . $this->_version . '.' . $expect)) {
5736                                $db->rollbackTransaction();
5737                            } else {
5738                                $db->commitTransaction();
5739                            }
5740                        } else {
5741                            $db->rollbackTransaction();
5742                        }
5743                    }
5744                    break;
5745                }
5746            }
5747        } elseif(SeedDMS_Core_File::file_exists($this->_document->getDir() . $this->_version . '.')) {
5748            echo "no file";
5749        } else {
5750            echo $this->_dms->contentDir.$this->_document->getDir() . $this->_version . $this->_fileType;
5751        }
5752        return true;
5753    } /* }}} */
5754
5755} /* }}} */
5756
5757
5758/**
5759 * Class to represent a link between two document
5760 *
5761 * Document links are to establish a reference from one document to
5762 * another document. The owner of the document link may not be the same
5763 * as the owner of one of the documents.
5764 * Use {@link SeedDMS_Core_Document::addDocumentLink()} to add a reference
5765 * to another document.
5766 *
5767 * @category   DMS
5768 * @package    SeedDMS_Core
5769 * @author     Markus Westphal, Malcolm Cowe, Matteo Lucarelli,
5770 *             Uwe Steinmann <uwe@steinmann.cx>
5771 * @copyright  Copyright (C) 2002-2005 Markus Westphal,
5772 *             2006-2008 Malcolm Cowe, 2010 Matteo Lucarelli,
5773 *             2010-2022 Uwe Steinmann
5774 * @version    Release: @package_version@
5775 */
5776class SeedDMS_Core_DocumentLink { /* {{{ */
5777    /**
5778     * @var integer internal id of document link
5779     */
5780    protected $_id;
5781
5782    /**
5783     * @var SeedDMS_Core_Document reference to document this link belongs to
5784     */
5785    protected $_document;
5786
5787    /**
5788     * @var object reference to target document this link points to
5789     */
5790    protected $_target;
5791
5792    /**
5793     * @var integer id of user who is the owner of this link
5794     */
5795    protected $_userID;
5796
5797    /**
5798     * @var object $_user user who is the owner of this link
5799     */
5800    protected $_user;
5801
5802    /**
5803     * @var integer 1 if this link is public, or 0 if is only visible to the owner
5804     */
5805    protected $_public;
5806
5807    /**
5808     * SeedDMS_Core_DocumentLink constructor.
5809     * @param $id
5810     * @param $document
5811     * @param $target
5812     * @param $userID
5813     * @param $public
5814     */
5815    function __construct($id, $document, $target, $userID, $public) {
5816        $this->_id = $id;
5817        $this->_document = $document;
5818        $this->_target = $target;
5819        $this->_userID = $userID;
5820        $this->_user = null;
5821        $this->_public = $public ? true : false;
5822    }
5823
5824    /**
5825     * Check if this object is of type 'documentlink'.
5826     *
5827     * @param string $type type of object
5828     */
5829    public function isType($type) { /* {{{ */
5830        return $type == 'documentlink';
5831    } /* }}} */
5832
5833    /**
5834     * @return int
5835     */
5836    function getID() { return $this->_id; }
5837
5838    /**
5839     * @return SeedDMS_Core_Document
5840     */
5841    function getDocument() {
5842        return $this->_document;
5843    }
5844
5845    /**
5846     * @return object
5847     */
5848    function getTarget() {
5849        return $this->_target;
5850    }
5851
5852    /**
5853     * @return bool|SeedDMS_Core_User
5854     */
5855    function getUser() {
5856        if (!isset($this->_user)) {
5857            $this->_user = $this->_document->getDMS()->getUser($this->_userID);
5858        }
5859        return $this->_user;
5860    }
5861
5862    /**
5863     * @return int
5864     */
5865    function isPublic() { return $this->_public; }
5866
5867    /**
5868     * Returns the access mode similar to a document
5869     *
5870     * There is no real access mode for document links, so this is just
5871     * another way to add more access restrictions than the default restrictions.
5872     * It is only called for public document links, not accessed by the owner
5873     * or the administrator.
5874     *
5875     * @param SeedDMS_Core_User $u user
5876     * @param $source
5877     * @param $target
5878     * @return int either M_NONE or M_READ
5879     */
5880    function getAccessMode($u, $source, $target) { /* {{{ */
5881        $dms = $this->_document->getDMS();
5882
5883        /* Check if 'onCheckAccessDocumentLink' callback is set */
5884        if(isset($dms->callbacks['onCheckAccessDocumentLink'])) {
5885            foreach($dms->callbacks['onCheckAccessDocumentLink'] as $callback) {
5886                if(($ret = call_user_func($callback[0], $callback[1], $this, $u, $source, $target)) > 0) {
5887                    return $ret;
5888                }
5889            }
5890        }
5891
5892        return M_READ;
5893    } /* }}} */
5894
5895} /* }}} */
5896
5897/**
5898 * Class to represent a file attached to a document
5899 *
5900 * Beside the regular document content arbitrary files can be attached
5901 * to a document. This is a similar concept as attaching files to emails.
5902 * The owner of the attached file and the document may not be the same.
5903 * Use {@link SeedDMS_Core_Document::addDocumentFile()} to attach a file.
5904 *
5905 * @category   DMS
5906 * @package    SeedDMS_Core
5907 * @author     Markus Westphal, Malcolm Cowe, Matteo Lucarelli,
5908 *             Uwe Steinmann <uwe@steinmann.cx>
5909 * @copyright  Copyright (C) 2002-2005 Markus Westphal,
5910 *             2006-2008 Malcolm Cowe, 2010 Matteo Lucarelli,
5911 *             2010-2022 Uwe Steinmann
5912 * @version    Release: @package_version@
5913 */
5914class SeedDMS_Core_DocumentFile { /* {{{ */
5915    /**
5916     * @var integer internal id of document file
5917     */
5918    protected $_id;
5919
5920    /**
5921     * @var SeedDMS_Core_Document reference to document this file belongs to
5922     */
5923    protected $_document;
5924
5925    /**
5926     * @var integer id of user who is the owner of this link
5927     */
5928    protected $_userID;
5929
5930    /**
5931     * @var string comment for the attached file
5932     */
5933    protected $_comment;
5934
5935    /**
5936     * @var string date when the file was attached
5937     */
5938    protected $_date;
5939
5940    /**
5941     * @var integer version of document this file is attached to
5942     */
5943    protected $_version;
5944
5945    /**
5946     * @var integer 1 if this link is public, or 0 if is only visible to the owner
5947     */
5948    protected $_public;
5949
5950    /**
5951     * @var string directory where the file is stored. This is the
5952     * document id with a proceding '/'.
5953     * FIXME: looks like this isn't used anymore. The file path is
5954     * constructed by getPath()
5955     */
5956    protected $_dir;
5957
5958    /**
5959     * @var string extension of the original file name with a leading '.'
5960     */
5961    protected $_fileType;
5962
5963    /**
5964     * @var string mime type of the file
5965     */
5966    protected $_mimeType;
5967
5968    /**
5969     * @var string name of the file that was originally uploaded
5970     */
5971    protected $_orgFileName;
5972
5973    /**
5974     * @var string name of the file as given by the user
5975     */
5976    protected $_name;
5977
5978    /**
5979     * SeedDMS_Core_DocumentFile constructor.
5980     * @param $id
5981     * @param $document
5982     * @param $userID
5983     * @param $comment
5984     * @param $date
5985     * @param $dir
5986     * @param $fileType
5987     * @param $mimeType
5988     * @param $orgFileName
5989     * @param $name
5990     * @param $version
5991     * @param $public
5992     */
5993    function __construct($id, $document, $userID, $comment, $date, $dir, $fileType, $mimeType, $orgFileName,$name,$version,$public) {
5994        $this->_id = $id;
5995        $this->_document = $document;
5996        $this->_userID = $userID;
5997        $this->_comment = $comment;
5998        $this->_date = $date;
5999        $this->_dir = $dir;
6000        $this->_fileType = $fileType;
6001        $this->_mimeType = $mimeType;
6002        $this->_orgFileName = $orgFileName;
6003        $this->_name = $name;
6004        $this->_version = $version;
6005        $this->_public = $public ? true : false;
6006    }
6007
6008    /**
6009     * Check if this object is of type 'documentfile'.
6010     *
6011     * @param string $type type of object
6012     */
6013    public function isType($type) { /* {{{ */
6014        return $type == 'documentfile';
6015    } /* }}} */
6016
6017    /**
6018     * @return int
6019     */
6020    function getID() { return $this->_id; }
6021
6022    /**
6023     * @return SeedDMS_Core_Document
6024     */
6025    function getDocument() { return $this->_document; }
6026
6027    /**
6028     * @return int
6029     */
6030    function getUserID() { return $this->_userID; }
6031
6032    /**
6033     * @return string
6034     */
6035    function getComment() { return $this->_comment; }
6036
6037    /*
6038     * Set the comment of the document file
6039     *
6040     * @param string $newComment string new comment of document
6041     */
6042    function setComment($newComment) { /* {{{ */
6043        $db = $this->_document->getDMS()->getDB();
6044
6045        $queryStr = "UPDATE `tblDocumentFiles` SET `comment` = ".$db->qstr($newComment)." WHERE `document` = ".$this->_document->getId()." AND `id` = ". $this->_id;
6046        if (!$db->getResult($queryStr))
6047            return false;
6048
6049        $this->_comment = $newComment;
6050        return true;
6051    } /* }}} */
6052
6053    /**
6054     * @return string
6055     */
6056    function getDate() { return $this->_date; }
6057
6058    /**
6059     * Set creation date of the document file
6060     *
6061     * @param integer $date timestamp of creation date. If false then set it
6062     * to the current timestamp
6063     * @return boolean true on success
6064     */
6065    function setDate($date=null) { /* {{{ */
6066        $db = $this->_document->getDMS()->getDB();
6067
6068        if(!$date)
6069            $date = time();
6070        else {
6071            if(!is_numeric($date))
6072                return false;
6073        }
6074
6075        $queryStr = "UPDATE `tblDocumentFiles` SET `date` = " . (int) $date . " WHERE `id` = ". $this->_id;
6076        if (!$db->getResult($queryStr))
6077            return false;
6078        $this->_date = $date;
6079        return true;
6080    } /* }}} */
6081
6082    /**
6083     * @return string
6084     */
6085    function getDir() { return $this->_dir; }
6086
6087    /**
6088     * @return string
6089     */
6090    function getFileType() { return $this->_fileType; }
6091
6092    /**
6093     * @return string
6094     */
6095    function getMimeType() { return $this->_mimeType; }
6096
6097    /**
6098     * @return string
6099     */
6100    function getOriginalFileName() { return $this->_orgFileName; }
6101
6102    /**
6103     * @return string
6104     */
6105    function getName() { return $this->_name; }
6106
6107    /*
6108     * Set the name of the document file
6109     *
6110     * @param $newComment string new name of document
6111     */
6112    function setName($newName) { /* {{{ */
6113        $db = $this->_document->getDMS()->getDB();
6114
6115        $queryStr = "UPDATE `tblDocumentFiles` SET `name` = ".$db->qstr($newName)." WHERE `document` = ".$this->_document->getId()." AND `id` = ". $this->_id;
6116        if (!$db->getResult($queryStr))
6117            return false;
6118
6119        $this->_name = $newName;
6120
6121        return true;
6122    } /* }}} */
6123
6124    /**
6125     * @return bool|SeedDMS_Core_User
6126     */
6127    function getUser() {
6128        if (!isset($this->_user))
6129            $this->_user = $this->_document->getDMS()->getUser($this->_userID);
6130        return $this->_user;
6131    }
6132
6133    /**
6134     * @return string
6135     */
6136    function getPath() {
6137        return $this->_document->getDir() . "f" .$this->_id . $this->_fileType;
6138    }
6139
6140    /**
6141     * @return int
6142     */
6143    function getVersion() { return $this->_version; }
6144
6145    /*
6146     * Set the version of the document file
6147     *
6148     * @param $newComment string new version of document
6149     */
6150    function setVersion($newVersion) { /* {{{ */
6151        $db = $this->_document->getDMS()->getDB();
6152
6153        if(!is_numeric($newVersion) && $newVersion != '')
6154            return false;
6155
6156        $queryStr = "UPDATE `tblDocumentFiles` SET `version` = ".(int) $newVersion." WHERE `document` = ".$this->_document->getId()." AND `id` = ". $this->_id;
6157        if (!$db->getResult($queryStr))
6158            return false;
6159
6160        $this->_version = (int) $newVersion;
6161        return true;
6162    } /* }}} */
6163
6164    /**
6165     * @return int
6166     */
6167    function isPublic() { return $this->_public; }
6168
6169    /*
6170     * Set the public flag of the document file
6171     *
6172     * @param $newComment string new comment of document
6173     */
6174    function setPublic($newPublic) { /* {{{ */
6175        $db = $this->_document->getDMS()->getDB();
6176
6177        $queryStr = "UPDATE `tblDocumentFiles` SET `public` = ".($newPublic ? 1 : 0)." WHERE `document` = ".$this->_document->getId()." AND `id` = ". $this->_id;
6178        if (!$db->getResult($queryStr))
6179            return false;
6180
6181        $this->_public = $newPublic ? true : false;
6182        return true;
6183    } /* }}} */
6184
6185    /**
6186     * Returns the access mode similar to a document
6187     *
6188     * There is no real access mode for document files, so this is just
6189     * another way to add more access restrictions than the default restrictions.
6190     * It is only called for public document files, not accessed by the owner
6191     * or the administrator.
6192     *
6193     * @param object $u user
6194     * @return integer either M_NONE or M_READ
6195     */
6196    function getAccessMode($u) { /* {{{ */
6197        $dms = $this->_document->getDMS();
6198
6199        /* Check if 'onCheckAccessDocumentLink' callback is set */
6200        if(isset($this->_dms->callbacks['onCheckAccessDocumentFile'])) {
6201            foreach($this->_dms->callbacks['onCheckAccessDocumentFile'] as $callback) {
6202                if(($ret = call_user_func($callback[0], $callback[1], $this, $u)) > 0) {
6203                    return $ret;
6204                }
6205            }
6206        }
6207
6208        return M_READ;
6209    } /* }}} */
6210
6211} /* }}} */
6212
6213//
6214// Perhaps not the cleanest object ever devised, it exists to encapsulate all
6215// of the data generated during the addition of new content to the database.
6216// The object stores a copy of the new DocumentContent object, the newly assigned
6217// reviewers and approvers and the status.
6218//
6219/**
6220 * Class to represent a list of document contents
6221 *
6222 * @category   DMS
6223 * @package    SeedDMS_Core
6224 * @author     Markus Westphal, Malcolm Cowe, Matteo Lucarelli,
6225 *             Uwe Steinmann <uwe@steinmann.cx>
6226 * @copyright  Copyright (C) 2002-2005 Markus Westphal,
6227 *             2006-2008 Malcolm Cowe, 2010 Matteo Lucarelli,
6228 *             2010-2022 Uwe Steinmann
6229 * @version    Release: @package_version@
6230 */
6231class SeedDMS_Core_AddContentResultSet { /* {{{ */
6232
6233    /**
6234     * @var null
6235     */
6236    protected $_indReviewers;
6237
6238    /**
6239     * @var null
6240     */
6241    protected $_grpReviewers;
6242
6243    /**
6244     * @var null
6245     */
6246    protected $_indApprovers;
6247
6248    /**
6249     * @var null
6250     */
6251    protected $_grpApprovers;
6252
6253    /**
6254     * @var
6255     */
6256    protected $_content;
6257
6258    /**
6259     * @var null
6260     */
6261    protected $_status;
6262
6263    /**
6264     * @var SeedDMS_Core_DMS back reference to document management system
6265     */
6266    protected $_dms;
6267
6268    /**
6269     * SeedDMS_Core_AddContentResultSet constructor.
6270     * @param $content
6271     */
6272    function __construct($content) { /* {{{ */
6273        $this->_content = $content;
6274        $this->_indReviewers = null;
6275        $this->_grpReviewers = null;
6276        $this->_indApprovers = null;
6277        $this->_grpApprovers = null;
6278        $this->_status = null;
6279        $this->_dms = null;
6280    } /* }}} */
6281
6282    /**
6283     * Set dms this object belongs to.
6284     *
6285     * Each object needs a reference to the dms it belongs to. It will be
6286     * set when the object is created.
6287     * The dms has a references to the currently logged in user
6288     * and the database connection.
6289     *
6290     * @param SeedDMS_Core_DMS $dms reference to dms
6291     */
6292    function setDMS($dms) { /* {{{ */
6293        $this->_dms = $dms;
6294    } /* }}} */
6295
6296    /**
6297     * @param $reviewer
6298     * @param $type
6299     * @param $status
6300     * @return bool
6301     */
6302    function addReviewer($reviewer, $type, $status) { /* {{{ */
6303        $dms = $this->_dms;
6304
6305        if (!is_object($reviewer) || (strcasecmp($type, "i") && strcasecmp($type, "g")) && !is_integer($status)){
6306            return false;
6307        }
6308        if (!strcasecmp($type, "i")) {
6309            if (strcasecmp(get_class($reviewer), $dms->getClassname("user"))) {
6310                return false;
6311            }
6312            if ($this->_indReviewers == null) {
6313                $this->_indReviewers = array();
6314            }
6315            $this->_indReviewers[$status][] = $reviewer;
6316        }
6317        if (!strcasecmp($type, "g")) {
6318            if (strcasecmp(get_class($reviewer), $dms->getClassname("group"))) {
6319                return false;
6320            }
6321            if ($this->_grpReviewers == null) {
6322                $this->_grpReviewers = array();
6323            }
6324            $this->_grpReviewers[$status][] = $reviewer;
6325        }
6326        return true;
6327    } /* }}} */
6328
6329    /**
6330     * @param $approver
6331     * @param $type
6332     * @param $status
6333     * @return bool
6334     */
6335    function addApprover($approver, $type, $status) { /* {{{ */
6336        $dms = $this->_dms;
6337
6338        if (!is_object($approver) || (strcasecmp($type, "i") && strcasecmp($type, "g")) && !is_integer($status)){
6339            return false;
6340        }
6341        if (!strcasecmp($type, "i")) {
6342            if (strcasecmp(get_class($approver), $dms->getClassname("user"))) {
6343                return false;
6344            }
6345            if ($this->_indApprovers == null) {
6346                $this->_indApprovers = array();
6347            }
6348            $this->_indApprovers[$status][] = $approver;
6349        }
6350        if (!strcasecmp($type, "g")) {
6351            if (strcasecmp(get_class($approver), $dms->getClassname("group"))) {
6352                return false;
6353            }
6354            if ($this->_grpApprovers == null) {
6355                $this->_grpApprovers = array();
6356            }
6357            $this->_grpApprovers[$status][] = $approver;
6358        }
6359        return true;
6360    } /* }}} */
6361
6362    /**
6363     * @param $status
6364     * @return bool
6365     */
6366    function setStatus($status) { /* {{{ */
6367        if (!is_integer($status)) {
6368            return false;
6369        }
6370        if ($status<-3 || $status>3) {
6371            return false;
6372        }
6373        $this->_status = $status;
6374        return true;
6375    } /* }}} */
6376
6377    /**
6378     * @return null
6379     */
6380    function getStatus() { /* {{{ */
6381        return $this->_status;
6382    } /* }}} */
6383
6384    /**
6385     * @return mixed
6386     */
6387    function getContent() { /* {{{ */
6388        return $this->_content;
6389    } /* }}} */
6390
6391    /**
6392     * @param $type
6393     * @return array|bool|null
6394     */
6395    function getReviewers($type) { /* {{{ */
6396        if (strcasecmp($type, "i") && strcasecmp($type, "g")) {
6397            return false;
6398        }
6399        if (!strcasecmp($type, "i")) {
6400            return ($this->_indReviewers == null ? array() : $this->_indReviewers);
6401        }
6402        else {
6403            return ($this->_grpReviewers == null ? array() : $this->_grpReviewers);
6404        }
6405    } /* }}} */
6406
6407    /**
6408     * @param $type
6409     * @return array|bool|null
6410     */
6411    function getApprovers($type) { /* {{{ */
6412        if (strcasecmp($type, "i") && strcasecmp($type, "g")) {
6413            return false;
6414        }
6415        if (!strcasecmp($type, "i")) {
6416            return ($this->_indApprovers == null ? array() : $this->_indApprovers);
6417        }
6418        else {
6419            return ($this->_grpApprovers == null ? array() : $this->_grpApprovers);
6420        }
6421    } /* }}} */
6422} /* }}} */