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