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