@@ -10,74 +10,74 @@ |
||
10 | 10 | namespace grommunio\DAV; |
11 | 11 | |
12 | 12 | class GrommunioIMipPlugin extends \Sabre\CalDAV\Schedule\IMipPlugin { |
13 | - /** |
|
14 | - * Constructor. |
|
15 | - */ |
|
16 | - public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) { |
|
17 | - $this->gDavBackend = $gDavBackend; |
|
18 | - $this->logger = $glogger; |
|
19 | - } |
|
13 | + /** |
|
14 | + * Constructor. |
|
15 | + */ |
|
16 | + public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) { |
|
17 | + $this->gDavBackend = $gDavBackend; |
|
18 | + $this->logger = $glogger; |
|
19 | + } |
|
20 | 20 | |
21 | - /** |
|
22 | - * Sends out meeting invitation. |
|
23 | - * |
|
24 | - * Using the information in iTipMessage to send out a meeting |
|
25 | - * invitation. |
|
26 | - */ |
|
27 | - public function schedule(\Sabre\VObject\ITip\Message $iTipMessage) { |
|
28 | - $this->logger->trace("method: %s - recipient: %s - significantChange: %d - scheduleStatus: %s - message: %s", $iTipMessage->method, $iTipMessage->recipient, $iTipMessage->significantChange, $iTipMessage->scheduleStatus, $iTipMessage->message->serialize()); |
|
21 | + /** |
|
22 | + * Sends out meeting invitation. |
|
23 | + * |
|
24 | + * Using the information in iTipMessage to send out a meeting |
|
25 | + * invitation. |
|
26 | + */ |
|
27 | + public function schedule(\Sabre\VObject\ITip\Message $iTipMessage) { |
|
28 | + $this->logger->trace("method: %s - recipient: %s - significantChange: %d - scheduleStatus: %s - message: %s", $iTipMessage->method, $iTipMessage->recipient, $iTipMessage->significantChange, $iTipMessage->scheduleStatus, $iTipMessage->message->serialize()); |
|
29 | 29 | |
30 | - if (!$iTipMessage->significantChange) { |
|
31 | - if (!$iTipMessage->scheduleStatus) { |
|
32 | - $iTipMessage->scheduleStatus = "1.0;We got the message, but it's not significant enough to warrant an email"; |
|
33 | - } |
|
30 | + if (!$iTipMessage->significantChange) { |
|
31 | + if (!$iTipMessage->scheduleStatus) { |
|
32 | + $iTipMessage->scheduleStatus = "1.0;We got the message, but it's not significant enough to warrant an email"; |
|
33 | + } |
|
34 | 34 | |
35 | - return; |
|
36 | - } |
|
35 | + return; |
|
36 | + } |
|
37 | 37 | |
38 | - $recipient = preg_replace('!^mailto:!i', '', $iTipMessage->recipient); |
|
39 | - $session = $this->gDavBackend->GetSession(); |
|
40 | - $addrbook = $this->gDavBackend->GetAddressBook(); |
|
41 | - $store = $this->gDavBackend->GetStore($this->gDavBackend->GetUser()); |
|
42 | - $storeprops = mapi_getprops($store, [PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]); |
|
43 | - if (!isset($storeprops[PR_IPM_OUTBOX_ENTRYID]) || !isset($storeprops[PR_IPM_SENTMAIL_ENTRYID])) { |
|
44 | - /* handle error */ |
|
45 | - $this->logger->error("no outbox found aborting user: %s", $this->gDavBackend->GetUser()); |
|
38 | + $recipient = preg_replace('!^mailto:!i', '', $iTipMessage->recipient); |
|
39 | + $session = $this->gDavBackend->GetSession(); |
|
40 | + $addrbook = $this->gDavBackend->GetAddressBook(); |
|
41 | + $store = $this->gDavBackend->GetStore($this->gDavBackend->GetUser()); |
|
42 | + $storeprops = mapi_getprops($store, [PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID]); |
|
43 | + if (!isset($storeprops[PR_IPM_OUTBOX_ENTRYID]) || !isset($storeprops[PR_IPM_SENTMAIL_ENTRYID])) { |
|
44 | + /* handle error */ |
|
45 | + $this->logger->error("no outbox found aborting user: %s", $this->gDavBackend->GetUser()); |
|
46 | 46 | |
47 | - return; |
|
48 | - } |
|
47 | + return; |
|
48 | + } |
|
49 | 49 | |
50 | - /* create message and convert */ |
|
51 | - $outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]); |
|
52 | - $newmessage = mapi_folder_createmessage($outbox); |
|
53 | - mapi_icaltomapi($session, $store, $addrbook, $newmessage, $iTipMessage->message->serialize(), false); |
|
54 | - mapi_setprops($newmessage, [PR_SENTMAIL_ENTRYID => $storeprops[PR_IPM_SENTMAIL_ENTRYID], PR_DELETE_AFTER_SUBMIT => false]); |
|
50 | + /* create message and convert */ |
|
51 | + $outbox = mapi_msgstore_openentry($store, $storeprops[PR_IPM_OUTBOX_ENTRYID]); |
|
52 | + $newmessage = mapi_folder_createmessage($outbox); |
|
53 | + mapi_icaltomapi($session, $store, $addrbook, $newmessage, $iTipMessage->message->serialize(), false); |
|
54 | + mapi_setprops($newmessage, [PR_SENTMAIL_ENTRYID => $storeprops[PR_IPM_SENTMAIL_ENTRYID], PR_DELETE_AFTER_SUBMIT => false]); |
|
55 | 55 | |
56 | - /* clean the recipients (needed since mapi_icaltomapi does not take IC2M_NO_ORGANIZER) */ |
|
57 | - $recipientTable = mapi_message_getrecipienttable($newmessage); |
|
58 | - $recipientRows = mapi_table_queryallrows($recipientTable, [PR_SMTP_ADDRESS, PR_ROWID]); |
|
59 | - $removeRecipients = []; |
|
60 | - foreach ($recipientRows as $key => $recip) { |
|
61 | - if (!isset($recip[PR_SMTP_ADDRESS])) { |
|
62 | - continue; |
|
63 | - } |
|
64 | - if (strcasecmp($recip[PR_SMTP_ADDRESS], $recipient) != 0) { |
|
65 | - $removeRecipients[] = $recip; |
|
66 | - } |
|
67 | - } |
|
68 | - if (count($removeRecipients) == count($recipientRows)) { |
|
69 | - $this->logger->error("message will have no recipients. List to remove: %s - recipientRows: %s", $removeRecipients, $recipientRows); |
|
56 | + /* clean the recipients (needed since mapi_icaltomapi does not take IC2M_NO_ORGANIZER) */ |
|
57 | + $recipientTable = mapi_message_getrecipienttable($newmessage); |
|
58 | + $recipientRows = mapi_table_queryallrows($recipientTable, [PR_SMTP_ADDRESS, PR_ROWID]); |
|
59 | + $removeRecipients = []; |
|
60 | + foreach ($recipientRows as $key => $recip) { |
|
61 | + if (!isset($recip[PR_SMTP_ADDRESS])) { |
|
62 | + continue; |
|
63 | + } |
|
64 | + if (strcasecmp($recip[PR_SMTP_ADDRESS], $recipient) != 0) { |
|
65 | + $removeRecipients[] = $recip; |
|
66 | + } |
|
67 | + } |
|
68 | + if (count($removeRecipients) == count($recipientRows)) { |
|
69 | + $this->logger->error("message will have no recipients. List to remove: %s - recipientRows: %s", $removeRecipients, $recipientRows); |
|
70 | 70 | |
71 | - return; |
|
72 | - } |
|
73 | - if (count($removeRecipients) > 0) { |
|
74 | - mapi_message_modifyrecipients($newmessage, MODRECIP_REMOVE, $removeRecipients); |
|
75 | - } |
|
71 | + return; |
|
72 | + } |
|
73 | + if (count($removeRecipients) > 0) { |
|
74 | + mapi_message_modifyrecipients($newmessage, MODRECIP_REMOVE, $removeRecipients); |
|
75 | + } |
|
76 | 76 | |
77 | - /* save message and send */ |
|
78 | - mapi_savechanges($newmessage); |
|
79 | - mapi_message_submitmessage($newmessage); |
|
80 | - $this->logger->info("email sent, recipient: %s", $recipient); |
|
81 | - $iTipMessage->scheduleStatus = '1.1;Scheduling message sent via iMip'; |
|
82 | - } |
|
77 | + /* save message and send */ |
|
78 | + mapi_savechanges($newmessage); |
|
79 | + mapi_message_submitmessage($newmessage); |
|
80 | + $this->logger->info("email sent, recipient: %s", $recipient); |
|
81 | + $iTipMessage->scheduleStatus = '1.1;Scheduling message sent via iMip'; |
|
82 | + } |
|
83 | 83 | } |
@@ -10,105 +10,105 @@ |
||
10 | 10 | namespace grommunio\DAV; |
11 | 11 | |
12 | 12 | class GrommunioSyncState { |
13 | - private $db; |
|
13 | + private $db; |
|
14 | 14 | |
15 | - /** |
|
16 | - * Constructor. |
|
17 | - * |
|
18 | - * @param GLogger $logger |
|
19 | - * @param string $dbstring |
|
20 | - */ |
|
21 | - public function __construct($logger, $dbstring) { |
|
22 | - $this->logger = $logger; |
|
23 | - $this->logger->trace("Using db %s", $dbstring); |
|
24 | - $this->db = new \PDO($dbstring); |
|
15 | + /** |
|
16 | + * Constructor. |
|
17 | + * |
|
18 | + * @param GLogger $logger |
|
19 | + * @param string $dbstring |
|
20 | + */ |
|
21 | + public function __construct($logger, $dbstring) { |
|
22 | + $this->logger = $logger; |
|
23 | + $this->logger->trace("Using db %s", $dbstring); |
|
24 | + $this->db = new \PDO($dbstring); |
|
25 | 25 | |
26 | - $query = "CREATE TABLE IF NOT EXISTS gdav_sync_state ( |
|
26 | + $query = "CREATE TABLE IF NOT EXISTS gdav_sync_state ( |
|
27 | 27 | id VARCHAR(255), folderid VARCHAR(255), value TEXT, |
28 | 28 | PRIMARY KEY (id, folderid)); |
29 | 29 | CREATE TABLE IF NOT EXISTS gdav_sync_appttsref ( |
30 | 30 | sourcekey VARCHAR(255), folderid VARCHAR(255), appttsref VARCHAR(255), |
31 | 31 | PRIMARY KEY (sourcekey, folderid));"; |
32 | 32 | |
33 | - $this->db->exec($query); |
|
34 | - } |
|
33 | + $this->db->exec($query); |
|
34 | + } |
|
35 | 35 | |
36 | - /** |
|
37 | - * Fetch state information for a folderId (e.g. calenderId) and an id (uuid). |
|
38 | - * |
|
39 | - * @param string $folderid |
|
40 | - * @param string $id |
|
41 | - * |
|
42 | - * @return string |
|
43 | - */ |
|
44 | - public function getState($folderid, $id) { |
|
45 | - $query = "SELECT value FROM gdav_sync_state WHERE folderid = :folderid AND id = :id"; |
|
46 | - $statement = $this->db->prepare($query); |
|
47 | - $statement->bindParam(":folderid", $folderid); |
|
48 | - $statement->bindParam(":id", $id); |
|
49 | - $statement->execute(); |
|
50 | - $result = $statement->fetch(); |
|
51 | - if (!$result) { |
|
52 | - return null; |
|
53 | - } |
|
36 | + /** |
|
37 | + * Fetch state information for a folderId (e.g. calenderId) and an id (uuid). |
|
38 | + * |
|
39 | + * @param string $folderid |
|
40 | + * @param string $id |
|
41 | + * |
|
42 | + * @return string |
|
43 | + */ |
|
44 | + public function getState($folderid, $id) { |
|
45 | + $query = "SELECT value FROM gdav_sync_state WHERE folderid = :folderid AND id = :id"; |
|
46 | + $statement = $this->db->prepare($query); |
|
47 | + $statement->bindParam(":folderid", $folderid); |
|
48 | + $statement->bindParam(":id", $id); |
|
49 | + $statement->execute(); |
|
50 | + $result = $statement->fetch(); |
|
51 | + if (!$result) { |
|
52 | + return null; |
|
53 | + } |
|
54 | 54 | |
55 | - return $result['value']; |
|
56 | - } |
|
55 | + return $result['value']; |
|
56 | + } |
|
57 | 57 | |
58 | - /** |
|
59 | - * Set state information for a folderId (e.g. calenderId) and an id (uuid). |
|
60 | - * The state information is the sync token for ICS. |
|
61 | - * |
|
62 | - * @param string $folderid |
|
63 | - * @param string $id |
|
64 | - * @param string $value |
|
65 | - */ |
|
66 | - public function setState($folderid, $id, $value) { |
|
67 | - $query = "REPLACE INTO gdav_sync_state (id, folderid, value) VALUES(:id, :folderid, :value)"; |
|
68 | - $statement = $this->db->prepare($query); |
|
69 | - $statement->bindParam(":folderid", $folderid); |
|
70 | - $statement->bindParam(":id", $id); |
|
71 | - $statement->bindParam(":value", $value); |
|
72 | - $statement->execute(); |
|
73 | - } |
|
58 | + /** |
|
59 | + * Set state information for a folderId (e.g. calenderId) and an id (uuid). |
|
60 | + * The state information is the sync token for ICS. |
|
61 | + * |
|
62 | + * @param string $folderid |
|
63 | + * @param string $id |
|
64 | + * @param string $value |
|
65 | + */ |
|
66 | + public function setState($folderid, $id, $value) { |
|
67 | + $query = "REPLACE INTO gdav_sync_state (id, folderid, value) VALUES(:id, :folderid, :value)"; |
|
68 | + $statement = $this->db->prepare($query); |
|
69 | + $statement->bindParam(":folderid", $folderid); |
|
70 | + $statement->bindParam(":id", $id); |
|
71 | + $statement->bindParam(":value", $value); |
|
72 | + $statement->execute(); |
|
73 | + } |
|
74 | 74 | |
75 | - /** |
|
76 | - * Set the APPTTSREF (custom URL) for a folderId and source key. |
|
77 | - * This is needed for detecting the URL of deleted items reported by ICS. |
|
78 | - * |
|
79 | - * @param string $folderid |
|
80 | - * @param string $sourcekey |
|
81 | - * @param string $appttsref |
|
82 | - */ |
|
83 | - public function rememberAppttsref($folderid, $sourcekey, $appttsref) { |
|
84 | - $query = "REPLACE INTO gdav_sync_appttsref (folderid, sourcekey, appttsref) VALUES(:folderid, :sourcekey, :appttsref)"; |
|
85 | - $statement = $this->db->prepare($query); |
|
86 | - $statement->bindParam(":folderid", $folderid); |
|
87 | - $statement->bindParam(":sourcekey", $sourcekey); |
|
88 | - $statement->bindParam(":appttsref", $appttsref); |
|
89 | - $statement->execute(); |
|
90 | - } |
|
75 | + /** |
|
76 | + * Set the APPTTSREF (custom URL) for a folderId and source key. |
|
77 | + * This is needed for detecting the URL of deleted items reported by ICS. |
|
78 | + * |
|
79 | + * @param string $folderid |
|
80 | + * @param string $sourcekey |
|
81 | + * @param string $appttsref |
|
82 | + */ |
|
83 | + public function rememberAppttsref($folderid, $sourcekey, $appttsref) { |
|
84 | + $query = "REPLACE INTO gdav_sync_appttsref (folderid, sourcekey, appttsref) VALUES(:folderid, :sourcekey, :appttsref)"; |
|
85 | + $statement = $this->db->prepare($query); |
|
86 | + $statement->bindParam(":folderid", $folderid); |
|
87 | + $statement->bindParam(":sourcekey", $sourcekey); |
|
88 | + $statement->bindParam(":appttsref", $appttsref); |
|
89 | + $statement->execute(); |
|
90 | + } |
|
91 | 91 | |
92 | - /** |
|
93 | - * Get the APPTTSREF (custom URL) for a folderId and source key. |
|
94 | - * This is needed for detecting the URL of deleted items reported by ICS. |
|
95 | - * |
|
96 | - * @param string $folderid |
|
97 | - * @param string $sourcekey |
|
98 | - * |
|
99 | - * @return string |
|
100 | - */ |
|
101 | - public function getAppttsref($folderid, $sourcekey) { |
|
102 | - $query = "SELECT appttsref FROM gdav_sync_appttsref WHERE folderid = :folderid AND sourcekey = :sourcekey"; |
|
103 | - $statement = $this->db->prepare($query); |
|
104 | - $statement->bindParam(":folderid", $folderid); |
|
105 | - $statement->bindParam(":sourcekey", $sourcekey); |
|
106 | - $statement->execute(); |
|
107 | - $result = $statement->fetch(); |
|
108 | - if (!$result) { |
|
109 | - return null; |
|
110 | - } |
|
92 | + /** |
|
93 | + * Get the APPTTSREF (custom URL) for a folderId and source key. |
|
94 | + * This is needed for detecting the URL of deleted items reported by ICS. |
|
95 | + * |
|
96 | + * @param string $folderid |
|
97 | + * @param string $sourcekey |
|
98 | + * |
|
99 | + * @return string |
|
100 | + */ |
|
101 | + public function getAppttsref($folderid, $sourcekey) { |
|
102 | + $query = "SELECT appttsref FROM gdav_sync_appttsref WHERE folderid = :folderid AND sourcekey = :sourcekey"; |
|
103 | + $statement = $this->db->prepare($query); |
|
104 | + $statement->bindParam(":folderid", $folderid); |
|
105 | + $statement->bindParam(":sourcekey", $sourcekey); |
|
106 | + $statement->execute(); |
|
107 | + $result = $statement->fetch(); |
|
108 | + if (!$result) { |
|
109 | + return null; |
|
110 | + } |
|
111 | 111 | |
112 | - return $result['appttsref']; |
|
113 | - } |
|
112 | + return $result['appttsref']; |
|
113 | + } |
|
114 | 114 | } |
@@ -10,117 +10,117 @@ |
||
10 | 10 | namespace grommunio\DAV; |
11 | 11 | |
12 | 12 | class GPSR3Logger implements \Psr\Log\LoggerInterface { |
13 | - /** |
|
14 | - * log4php. |
|
15 | - * |
|
16 | - * @var Logger |
|
17 | - */ |
|
18 | - private $logger; |
|
13 | + /** |
|
14 | + * log4php. |
|
15 | + * |
|
16 | + * @var Logger |
|
17 | + */ |
|
18 | + private $logger; |
|
19 | 19 | |
20 | - /** |
|
21 | - * Wraps a log4php logger into a PSR-3 compatible interface. |
|
22 | - * |
|
23 | - * @param Logger $logger |
|
24 | - */ |
|
25 | - public function __construct($logger) { |
|
26 | - $this->logger = $logger; |
|
27 | - } |
|
20 | + /** |
|
21 | + * Wraps a log4php logger into a PSR-3 compatible interface. |
|
22 | + * |
|
23 | + * @param Logger $logger |
|
24 | + */ |
|
25 | + public function __construct($logger) { |
|
26 | + $this->logger = $logger; |
|
27 | + } |
|
28 | 28 | |
29 | - /** |
|
30 | - * Emergency message, like system down. |
|
31 | - * |
|
32 | - * @param string $message |
|
33 | - */ |
|
34 | - public function emergency($message, array $context = []) { |
|
35 | - $this->logger->fatal($this->interpret($message, $context)); |
|
36 | - } |
|
29 | + /** |
|
30 | + * Emergency message, like system down. |
|
31 | + * |
|
32 | + * @param string $message |
|
33 | + */ |
|
34 | + public function emergency($message, array $context = []) { |
|
35 | + $this->logger->fatal($this->interpret($message, $context)); |
|
36 | + } |
|
37 | 37 | |
38 | - /** |
|
39 | - * Immediate Action required. |
|
40 | - * |
|
41 | - * @param string $message |
|
42 | - */ |
|
43 | - public function alert($message, array $context = []) { |
|
44 | - $this->logger->fatal($this->interpret($message, $context)); |
|
45 | - } |
|
38 | + /** |
|
39 | + * Immediate Action required. |
|
40 | + * |
|
41 | + * @param string $message |
|
42 | + */ |
|
43 | + public function alert($message, array $context = []) { |
|
44 | + $this->logger->fatal($this->interpret($message, $context)); |
|
45 | + } |
|
46 | 46 | |
47 | - /** |
|
48 | - * Critical messages. |
|
49 | - * |
|
50 | - * @param string $message |
|
51 | - */ |
|
52 | - public function critical($message, array $context = []) { |
|
53 | - $this->logger->fatal($this->interpret($message, $context)); |
|
54 | - } |
|
47 | + /** |
|
48 | + * Critical messages. |
|
49 | + * |
|
50 | + * @param string $message |
|
51 | + */ |
|
52 | + public function critical($message, array $context = []) { |
|
53 | + $this->logger->fatal($this->interpret($message, $context)); |
|
54 | + } |
|
55 | 55 | |
56 | - /** |
|
57 | - * Errors happening on runtime that need to be logged. |
|
58 | - * |
|
59 | - * @param string $message |
|
60 | - */ |
|
61 | - public function error($message, array $context = []) { |
|
62 | - $this->logger->error($this->interpret($message, $context)); |
|
63 | - } |
|
56 | + /** |
|
57 | + * Errors happening on runtime that need to be logged. |
|
58 | + * |
|
59 | + * @param string $message |
|
60 | + */ |
|
61 | + public function error($message, array $context = []) { |
|
62 | + $this->logger->error($this->interpret($message, $context)); |
|
63 | + } |
|
64 | 64 | |
65 | - /** |
|
66 | - * Warnings (not necessarily errors). |
|
67 | - * |
|
68 | - * @param string $message |
|
69 | - */ |
|
70 | - public function warning($message, array $context = []) { |
|
71 | - $this->logger->warn($this->interpret($message, $context)); |
|
72 | - } |
|
65 | + /** |
|
66 | + * Warnings (not necessarily errors). |
|
67 | + * |
|
68 | + * @param string $message |
|
69 | + */ |
|
70 | + public function warning($message, array $context = []) { |
|
71 | + $this->logger->warn($this->interpret($message, $context)); |
|
72 | + } |
|
73 | 73 | |
74 | - /** |
|
75 | - * Significant events (still normal). |
|
76 | - * |
|
77 | - * @param string $message |
|
78 | - */ |
|
79 | - public function notice($message, array $context = []) { |
|
80 | - $this->logger->info($this->interpret($message, $context)); |
|
81 | - } |
|
74 | + /** |
|
75 | + * Significant events (still normal). |
|
76 | + * |
|
77 | + * @param string $message |
|
78 | + */ |
|
79 | + public function notice($message, array $context = []) { |
|
80 | + $this->logger->info($this->interpret($message, $context)); |
|
81 | + } |
|
82 | 82 | |
83 | - /** |
|
84 | - * Events with informational value. |
|
85 | - * |
|
86 | - * @param string $message |
|
87 | - */ |
|
88 | - public function info($message, array $context = []) { |
|
89 | - $this->logger->info($this->interpret($message, $context)); |
|
90 | - } |
|
83 | + /** |
|
84 | + * Events with informational value. |
|
85 | + * |
|
86 | + * @param string $message |
|
87 | + */ |
|
88 | + public function info($message, array $context = []) { |
|
89 | + $this->logger->info($this->interpret($message, $context)); |
|
90 | + } |
|
91 | 91 | |
92 | - /** |
|
93 | - * Debug data. |
|
94 | - * |
|
95 | - * @param string $message |
|
96 | - */ |
|
97 | - public function debug($message, array $context = []) { |
|
98 | - $this->logger->debug($this->interpret($message, $context)); |
|
99 | - } |
|
92 | + /** |
|
93 | + * Debug data. |
|
94 | + * |
|
95 | + * @param string $message |
|
96 | + */ |
|
97 | + public function debug($message, array $context = []) { |
|
98 | + $this->logger->debug($this->interpret($message, $context)); |
|
99 | + } |
|
100 | 100 | |
101 | - /** |
|
102 | - * Logs at a loglevel. |
|
103 | - * |
|
104 | - * @param mixed $level |
|
105 | - * @param string $message |
|
106 | - */ |
|
107 | - public function log($level, $message, array $context = []) { |
|
108 | - throw new \Exception('Please call specific logging message'); |
|
109 | - } |
|
101 | + /** |
|
102 | + * Logs at a loglevel. |
|
103 | + * |
|
104 | + * @param mixed $level |
|
105 | + * @param string $message |
|
106 | + */ |
|
107 | + public function log($level, $message, array $context = []) { |
|
108 | + throw new \Exception('Please call specific logging message'); |
|
109 | + } |
|
110 | 110 | |
111 | - /** |
|
112 | - * Interprets context values as string like in the PSR-3 example implementation. |
|
113 | - * |
|
114 | - * @param string $message |
|
115 | - * |
|
116 | - * @return string |
|
117 | - */ |
|
118 | - protected function interpret($message, array $context = []) { |
|
119 | - $replace = []; |
|
120 | - foreach ($context as $key => $val) { |
|
121 | - $replace['{' . $key . '}'] = $val; |
|
122 | - } |
|
111 | + /** |
|
112 | + * Interprets context values as string like in the PSR-3 example implementation. |
|
113 | + * |
|
114 | + * @param string $message |
|
115 | + * |
|
116 | + * @return string |
|
117 | + */ |
|
118 | + protected function interpret($message, array $context = []) { |
|
119 | + $replace = []; |
|
120 | + foreach ($context as $key => $val) { |
|
121 | + $replace['{' . $key . '}'] = $val; |
|
122 | + } |
|
123 | 123 | |
124 | - return strtr($message, $replace); |
|
125 | - } |
|
124 | + return strtr($message, $replace); |
|
125 | + } |
|
126 | 126 | } |
@@ -10,353 +10,353 @@ |
||
10 | 10 | namespace grommunio\DAV; |
11 | 11 | |
12 | 12 | class GrommunioCardDavBackend extends \Sabre\CardDAV\Backend\AbstractBackend implements \Sabre\CardDAV\Backend\SyncSupport { |
13 | - private $logger; |
|
14 | - protected $gDavBackend; |
|
15 | - |
|
16 | - public const FILE_EXTENSION = '.vcf'; |
|
17 | - public const CONTAINER_CLASS = 'IPF.Contact'; |
|
18 | - public const CONTAINER_CLASSES = ['IPF.Contact']; |
|
19 | - |
|
20 | - /** |
|
21 | - * Constructor. |
|
22 | - */ |
|
23 | - public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) { |
|
24 | - $this->gDavBackend = $gDavBackend; |
|
25 | - $this->logger = $glogger; |
|
26 | - } |
|
27 | - |
|
28 | - /** |
|
29 | - * Returns the list of addressbooks for a specific user. |
|
30 | - * |
|
31 | - * Every addressbook should have the following properties: |
|
32 | - * id - an arbitrary unique id |
|
33 | - * uri - the 'basename' part of the url |
|
34 | - * principaluri - Same as the passed parameter |
|
35 | - * |
|
36 | - * Any additional clark-notation property may be passed besides this. Some |
|
37 | - * common ones are : |
|
38 | - * {DAV:}displayname |
|
39 | - * {urn:ietf:params:xml:ns:carddav}addressbook-description |
|
40 | - * {http://calendarserver.org/ns/}getctag |
|
41 | - * |
|
42 | - * @param string $principalUri |
|
43 | - * |
|
44 | - * @return array |
|
45 | - */ |
|
46 | - public function getAddressBooksForUser($principalUri) { |
|
47 | - $this->logger->trace("principalUri: %s", $principalUri); |
|
48 | - |
|
49 | - return $this->gDavBackend->GetFolders($principalUri, static::CONTAINER_CLASSES); |
|
50 | - } |
|
51 | - |
|
52 | - /** |
|
53 | - * Updates properties for an address book. |
|
54 | - * |
|
55 | - * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
56 | - * To do the actual updates, you must tell this object which properties |
|
57 | - * you're going to process with the handle() method. |
|
58 | - * |
|
59 | - * Calling the handle method is like telling the PropPatch object "I |
|
60 | - * promise I can handle updating this property". |
|
61 | - * |
|
62 | - * Read the PropPatch documentation for more info and examples. |
|
63 | - * |
|
64 | - * @param string $addressBookId |
|
65 | - */ |
|
66 | - public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { |
|
67 | - // TODO is our logger able to log this object? It probably needs to be adapted. |
|
68 | - $this->logger->trace("addressBookId: %s - proppatch: %s", $addressBookId, $propPatch); |
|
69 | - } |
|
70 | - |
|
71 | - /** |
|
72 | - * Creates a new address book. |
|
73 | - * |
|
74 | - * This method should return the id of the new address book. The id can be |
|
75 | - * in any format, including ints, strings, arrays or objects. |
|
76 | - * |
|
77 | - * @param string $principalUri |
|
78 | - * @param string $url just the 'basename' of the url |
|
79 | - * |
|
80 | - * @return mixed |
|
81 | - */ |
|
82 | - public function createAddressBook($principalUri, $url, array $properties) { |
|
83 | - $this->logger->trace("principalUri: %s - url: %s - properties: %s", $principalUri, $url, $properties); |
|
84 | - // TODO Add displayname |
|
85 | - return $this->gDavBackend->CreateFolder($principalUri, $url, static::CONTAINER_CLASS, ""); |
|
86 | - } |
|
87 | - |
|
88 | - /** |
|
89 | - * Deletes an entire addressbook and all its contents. |
|
90 | - * |
|
91 | - * @param mixed $addressBookId |
|
92 | - */ |
|
93 | - public function deleteAddressBook($addressBookId) { |
|
94 | - $this->logger->trace("addressBookId: %s", $addressBookId); |
|
95 | - $success = $this->gDavBackend->DeleteFolder($addressBookId); |
|
96 | - // TODO evaluate $success |
|
97 | - } |
|
98 | - |
|
99 | - /** |
|
100 | - * Returns all cards for a specific addressbook id. |
|
101 | - * |
|
102 | - * This method should return the following properties for each card: |
|
103 | - * * carddata - raw vcard data |
|
104 | - * * uri - Some unique url |
|
105 | - * * lastmodified - A unix timestamp |
|
106 | - * |
|
107 | - * It's recommended to also return the following properties: |
|
108 | - * * etag - A unique etag. This must change every time the card changes. |
|
109 | - * * size - The size of the card in bytes. |
|
110 | - * |
|
111 | - * If these last two properties are provided, less time will be spent |
|
112 | - * calculating them. If they are specified, you can also omit carddata. |
|
113 | - * This may speed up certain requests, especially with large cards. |
|
114 | - * |
|
115 | - * @param mixed $addressbookId |
|
116 | - * |
|
117 | - * @return array |
|
118 | - */ |
|
119 | - public function getCards($addressbookId) { |
|
120 | - $result = $this->gDavBackend->GetObjects($addressbookId, static::FILE_EXTENSION); |
|
121 | - $this->logger->trace("addressbookId: %s found %d objects", $addressbookId, count($result)); |
|
122 | - |
|
123 | - return $result; |
|
124 | - } |
|
125 | - |
|
126 | - /** |
|
127 | - * Returns a specific card. |
|
128 | - * |
|
129 | - * The same set of properties must be returned as with getCards. The only |
|
130 | - * exception is that 'carddata' is absolutely required. |
|
131 | - * |
|
132 | - * If the card does not exist, you must return false. |
|
133 | - * |
|
134 | - * @param mixed $addressBookId |
|
135 | - * @param string $cardUri |
|
136 | - * @param resource $mapifolder optional mapifolder resource, used if available |
|
137 | - * |
|
138 | - * @return array |
|
139 | - */ |
|
140 | - public function getCard($addressBookId, $cardUri, $mapifolder = null) { |
|
141 | - $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
|
142 | - |
|
143 | - if (!$mapifolder) { |
|
144 | - $mapifolder = $this->gDavBackend->GetMapiFolder($addressBookId); |
|
145 | - } |
|
146 | - |
|
147 | - $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, $mapifolder, static::FILE_EXTENSION); |
|
148 | - if (!$mapimessage) { |
|
149 | - $this->logger->debug("Object NOT FOUND"); |
|
150 | - |
|
151 | - return null; |
|
152 | - } |
|
153 | - |
|
154 | - $realId = $this->gDavBackend->GetIdOfMapiMessage($addressBookId, $mapimessage); |
|
155 | - |
|
156 | - $session = $this->gDavBackend->GetSession(); |
|
157 | - $ab = $this->gDavBackend->GetAddressBook(); |
|
158 | - |
|
159 | - $vcf = mapi_mapitovcf($session, $ab, $mapimessage, []); |
|
160 | - $this->logger->trace("vcf generated by mapi_mapitovcf: %s%s", PHP_EOL, $vcf); |
|
161 | - $props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]); |
|
162 | - $r = [ |
|
163 | - 'id' => $realId, |
|
164 | - 'uri' => $realId . static::FILE_EXTENSION, |
|
165 | - 'etag' => '"' . $props[PR_LAST_MODIFICATION_TIME] . '"', |
|
166 | - 'lastmodified' => $props[PR_LAST_MODIFICATION_TIME], |
|
167 | - 'carddata' => $vcf, |
|
168 | - 'size' => strlen($vcf), |
|
169 | - 'addressbookid' => $addressBookId, |
|
170 | - ]; |
|
171 | - |
|
172 | - $this->logger->trace("returned data id: %s - size: %d - etag: %s", $r['id'], $r['size'], $r['etag']); |
|
173 | - |
|
174 | - return $r; |
|
175 | - } |
|
176 | - |
|
177 | - /** |
|
178 | - * Creates a new card. |
|
179 | - * |
|
180 | - * The addressbook id will be passed as the first argument. This is the |
|
181 | - * same id as it is returned from the getAddressBooksForUser method. |
|
182 | - * |
|
183 | - * The cardUri is a base uri, and doesn't include the full path. The |
|
184 | - * cardData argument is the vcard body, and is passed as a string. |
|
185 | - * |
|
186 | - * It is possible to return an ETag from this method. This ETag is for the |
|
187 | - * newly created resource, and must be enclosed with double quotes (that |
|
188 | - * is, the string itself must contain the double quotes). |
|
189 | - * |
|
190 | - * You should only return the ETag if you store the carddata as-is. If a |
|
191 | - * subsequent GET request on the same card does not have the same body, |
|
192 | - * byte-by-byte and you did return an ETag here, clients tend to get |
|
193 | - * confused. |
|
194 | - * |
|
195 | - * If you don't return an ETag, you can just return null. |
|
196 | - * |
|
197 | - * @param mixed $addressBookId |
|
198 | - * @param string $cardUri |
|
199 | - * @param string $cardData |
|
200 | - * |
|
201 | - * @return null|string |
|
202 | - */ |
|
203 | - public function createCard($addressBookId, $cardUri, $cardData) { |
|
204 | - $this->logger->trace("addressBookId: %s - cardUri: %s - cardData: %s", $addressBookId, $cardUri, $cardData); |
|
205 | - $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($cardUri, static::FILE_EXTENSION); |
|
206 | - $folder = $this->gDavBackend->GetMapiFolder($addressBookId); |
|
207 | - $mapimessage = $this->gDavBackend->CreateObject($addressBookId, $folder, $objectId); |
|
208 | - $retval = $this->setData($addressBookId, $mapimessage, $cardData); |
|
209 | - if (!$retval) { |
|
210 | - return null; |
|
211 | - } |
|
212 | - |
|
213 | - return '"' . $retval . '"'; |
|
214 | - } |
|
215 | - |
|
216 | - /** |
|
217 | - * Updates a card. |
|
218 | - * |
|
219 | - * The addressbook id will be passed as the first argument. This is the |
|
220 | - * same id as it is returned from the getAddressBooksForUser method. |
|
221 | - * |
|
222 | - * The cardUri is a base uri, and doesn't include the full path. The |
|
223 | - * cardData argument is the vcard body, and is passed as a string. |
|
224 | - * |
|
225 | - * It is possible to return an ETag from this method. This ETag should |
|
226 | - * match that of the updated resource, and must be enclosed with double |
|
227 | - * quotes (that is: the string itself must contain the actual quotes). |
|
228 | - * |
|
229 | - * You should only return the ETag if you store the carddata as-is. If a |
|
230 | - * subsequent GET request on the same card does not have the same body, |
|
231 | - * byte-by-byte and you did return an ETag here, clients tend to get |
|
232 | - * confused. |
|
233 | - * |
|
234 | - * If you don't return an ETag, you can just return null. |
|
235 | - * |
|
236 | - * @param mixed $addressBookId |
|
237 | - * @param string $cardUri |
|
238 | - * @param string $cardData |
|
239 | - * |
|
240 | - * @return null|string |
|
241 | - */ |
|
242 | - public function updateCard($addressBookId, $cardUri, $cardData) { |
|
243 | - $this->logger->trace("addressBookId: %s - cardUri: %s - cardData: %s", $addressBookId, $cardUri, $cardData); |
|
244 | - |
|
245 | - $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, null, static::FILE_EXTENSION); |
|
246 | - $retval = $this->setData($addressBookId, $mapimessage, $cardData); |
|
247 | - if (!$retval) { |
|
248 | - return null; |
|
249 | - } |
|
250 | - |
|
251 | - return '"' . $retval . '"'; |
|
252 | - } |
|
253 | - |
|
254 | - /** |
|
255 | - * Sets data for a contact. |
|
256 | - * |
|
257 | - * @param mixed $addressBookId |
|
258 | - * @param MAPIMessage $mapimessage |
|
259 | - * @param string $vcf |
|
260 | - * |
|
261 | - * @return null|string |
|
262 | - */ |
|
263 | - private function setData($addressBookId, $mapimessage, $vcf) { |
|
264 | - $this->logger->trace("mapimessage: %s - vcf: %s", $mapimessage, $vcf); |
|
265 | - $store = $this->gDavBackend->GetStoreById($addressBookId); |
|
266 | - $session = $this->gDavBackend->GetSession(); |
|
267 | - |
|
268 | - $ok = mapi_vcftomapi($session, $store, $mapimessage, $vcf); |
|
269 | - if ($ok) { |
|
270 | - mapi_savechanges($mapimessage); |
|
271 | - // $props = mapi_getprops($mapimessage, array(PR_LAST_MODIFICATION_TIME)); |
|
272 | - $props = mapi_getprops($mapimessage); |
|
273 | - |
|
274 | - return $props[PR_LAST_MODIFICATION_TIME]; |
|
275 | - } |
|
276 | - |
|
277 | - return null; |
|
278 | - } |
|
279 | - |
|
280 | - /** |
|
281 | - * Deletes a card. |
|
282 | - * |
|
283 | - * @param mixed $addressBookId |
|
284 | - * @param string $cardUri |
|
285 | - * |
|
286 | - * @return bool |
|
287 | - */ |
|
288 | - public function deleteCard($addressBookId, $cardUri) { |
|
289 | - $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
|
290 | - $mapifolder = $this->gDavBackend->GetMapiFolder($addressBookId); |
|
291 | - $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($cardUri, static::FILE_EXTENSION); |
|
292 | - |
|
293 | - // to delete we need the PR_ENTRYID of the message |
|
294 | - // TODO move this part to GrommunioDavBackend |
|
295 | - $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, $mapifolder, static::FILE_EXTENSION); |
|
296 | - $props = mapi_getprops($mapimessage, [PR_ENTRYID]); |
|
297 | - mapi_folder_deletemessages($mapifolder, [$props[PR_ENTRYID]]); |
|
298 | - } |
|
299 | - |
|
300 | - /** |
|
301 | - * The getChanges method returns all the changes that have happened, since |
|
302 | - * the specified syncToken in the specified address book. |
|
303 | - * |
|
304 | - * This function should return an array, such as the following: |
|
305 | - * |
|
306 | - * [ |
|
307 | - * 'syncToken' => 'The current synctoken', |
|
308 | - * 'added' => [ |
|
309 | - * 'new.txt', |
|
310 | - * ], |
|
311 | - * 'modified' => [ |
|
312 | - * 'modified.txt', |
|
313 | - * ], |
|
314 | - * 'deleted' => [ |
|
315 | - * 'foo.php.bak', |
|
316 | - * 'old.txt' |
|
317 | - * ] |
|
318 | - * ]; |
|
319 | - * |
|
320 | - * The returned syncToken property should reflect the *current* syncToken |
|
321 | - * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
322 | - * property. This is needed here too, to ensure the operation is atomic. |
|
323 | - * |
|
324 | - * If the $syncToken argument is specified as null, this is an initial |
|
325 | - * sync, and all members should be reported. |
|
326 | - * |
|
327 | - * The modified property is an array of nodenames that have changed since |
|
328 | - * the last token. |
|
329 | - * |
|
330 | - * The deleted property is an array with nodenames, that have been deleted |
|
331 | - * from collection. |
|
332 | - * |
|
333 | - * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
334 | - * 1, you only have to report changes that happened only directly in |
|
335 | - * immediate descendants. If it's 2, it should also include changes from |
|
336 | - * the nodes below the child collections. (grandchildren) |
|
337 | - * |
|
338 | - * The $limit argument allows a client to specify how many results should |
|
339 | - * be returned at most. If the limit is not specified, it should be treated |
|
340 | - * as infinite. |
|
341 | - * |
|
342 | - * If the limit (infinite or not) is higher than you're willing to return, |
|
343 | - * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
344 | - * |
|
345 | - * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
346 | - * return null. |
|
347 | - * |
|
348 | - * The limit is 'suggestive'. You are free to ignore it. |
|
349 | - * |
|
350 | - * @param string $addressBookId |
|
351 | - * @param string $syncToken |
|
352 | - * @param int $syncLevel |
|
353 | - * @param int $limit |
|
354 | - * |
|
355 | - * @return array |
|
356 | - */ |
|
357 | - public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { |
|
358 | - $this->logger->trace("addressBookId: %s - syncToken: %s - syncLevel: %d - limit: %d", $addressBookId, $syncToken, $syncLevel, $limit); |
|
359 | - |
|
360 | - return $this->gDavBackend->Sync($addressBookId, $syncToken, static::FILE_EXTENSION, $limit); |
|
361 | - } |
|
13 | + private $logger; |
|
14 | + protected $gDavBackend; |
|
15 | + |
|
16 | + public const FILE_EXTENSION = '.vcf'; |
|
17 | + public const CONTAINER_CLASS = 'IPF.Contact'; |
|
18 | + public const CONTAINER_CLASSES = ['IPF.Contact']; |
|
19 | + |
|
20 | + /** |
|
21 | + * Constructor. |
|
22 | + */ |
|
23 | + public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) { |
|
24 | + $this->gDavBackend = $gDavBackend; |
|
25 | + $this->logger = $glogger; |
|
26 | + } |
|
27 | + |
|
28 | + /** |
|
29 | + * Returns the list of addressbooks for a specific user. |
|
30 | + * |
|
31 | + * Every addressbook should have the following properties: |
|
32 | + * id - an arbitrary unique id |
|
33 | + * uri - the 'basename' part of the url |
|
34 | + * principaluri - Same as the passed parameter |
|
35 | + * |
|
36 | + * Any additional clark-notation property may be passed besides this. Some |
|
37 | + * common ones are : |
|
38 | + * {DAV:}displayname |
|
39 | + * {urn:ietf:params:xml:ns:carddav}addressbook-description |
|
40 | + * {http://calendarserver.org/ns/}getctag |
|
41 | + * |
|
42 | + * @param string $principalUri |
|
43 | + * |
|
44 | + * @return array |
|
45 | + */ |
|
46 | + public function getAddressBooksForUser($principalUri) { |
|
47 | + $this->logger->trace("principalUri: %s", $principalUri); |
|
48 | + |
|
49 | + return $this->gDavBackend->GetFolders($principalUri, static::CONTAINER_CLASSES); |
|
50 | + } |
|
51 | + |
|
52 | + /** |
|
53 | + * Updates properties for an address book. |
|
54 | + * |
|
55 | + * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
56 | + * To do the actual updates, you must tell this object which properties |
|
57 | + * you're going to process with the handle() method. |
|
58 | + * |
|
59 | + * Calling the handle method is like telling the PropPatch object "I |
|
60 | + * promise I can handle updating this property". |
|
61 | + * |
|
62 | + * Read the PropPatch documentation for more info and examples. |
|
63 | + * |
|
64 | + * @param string $addressBookId |
|
65 | + */ |
|
66 | + public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch) { |
|
67 | + // TODO is our logger able to log this object? It probably needs to be adapted. |
|
68 | + $this->logger->trace("addressBookId: %s - proppatch: %s", $addressBookId, $propPatch); |
|
69 | + } |
|
70 | + |
|
71 | + /** |
|
72 | + * Creates a new address book. |
|
73 | + * |
|
74 | + * This method should return the id of the new address book. The id can be |
|
75 | + * in any format, including ints, strings, arrays or objects. |
|
76 | + * |
|
77 | + * @param string $principalUri |
|
78 | + * @param string $url just the 'basename' of the url |
|
79 | + * |
|
80 | + * @return mixed |
|
81 | + */ |
|
82 | + public function createAddressBook($principalUri, $url, array $properties) { |
|
83 | + $this->logger->trace("principalUri: %s - url: %s - properties: %s", $principalUri, $url, $properties); |
|
84 | + // TODO Add displayname |
|
85 | + return $this->gDavBackend->CreateFolder($principalUri, $url, static::CONTAINER_CLASS, ""); |
|
86 | + } |
|
87 | + |
|
88 | + /** |
|
89 | + * Deletes an entire addressbook and all its contents. |
|
90 | + * |
|
91 | + * @param mixed $addressBookId |
|
92 | + */ |
|
93 | + public function deleteAddressBook($addressBookId) { |
|
94 | + $this->logger->trace("addressBookId: %s", $addressBookId); |
|
95 | + $success = $this->gDavBackend->DeleteFolder($addressBookId); |
|
96 | + // TODO evaluate $success |
|
97 | + } |
|
98 | + |
|
99 | + /** |
|
100 | + * Returns all cards for a specific addressbook id. |
|
101 | + * |
|
102 | + * This method should return the following properties for each card: |
|
103 | + * * carddata - raw vcard data |
|
104 | + * * uri - Some unique url |
|
105 | + * * lastmodified - A unix timestamp |
|
106 | + * |
|
107 | + * It's recommended to also return the following properties: |
|
108 | + * * etag - A unique etag. This must change every time the card changes. |
|
109 | + * * size - The size of the card in bytes. |
|
110 | + * |
|
111 | + * If these last two properties are provided, less time will be spent |
|
112 | + * calculating them. If they are specified, you can also omit carddata. |
|
113 | + * This may speed up certain requests, especially with large cards. |
|
114 | + * |
|
115 | + * @param mixed $addressbookId |
|
116 | + * |
|
117 | + * @return array |
|
118 | + */ |
|
119 | + public function getCards($addressbookId) { |
|
120 | + $result = $this->gDavBackend->GetObjects($addressbookId, static::FILE_EXTENSION); |
|
121 | + $this->logger->trace("addressbookId: %s found %d objects", $addressbookId, count($result)); |
|
122 | + |
|
123 | + return $result; |
|
124 | + } |
|
125 | + |
|
126 | + /** |
|
127 | + * Returns a specific card. |
|
128 | + * |
|
129 | + * The same set of properties must be returned as with getCards. The only |
|
130 | + * exception is that 'carddata' is absolutely required. |
|
131 | + * |
|
132 | + * If the card does not exist, you must return false. |
|
133 | + * |
|
134 | + * @param mixed $addressBookId |
|
135 | + * @param string $cardUri |
|
136 | + * @param resource $mapifolder optional mapifolder resource, used if available |
|
137 | + * |
|
138 | + * @return array |
|
139 | + */ |
|
140 | + public function getCard($addressBookId, $cardUri, $mapifolder = null) { |
|
141 | + $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
|
142 | + |
|
143 | + if (!$mapifolder) { |
|
144 | + $mapifolder = $this->gDavBackend->GetMapiFolder($addressBookId); |
|
145 | + } |
|
146 | + |
|
147 | + $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, $mapifolder, static::FILE_EXTENSION); |
|
148 | + if (!$mapimessage) { |
|
149 | + $this->logger->debug("Object NOT FOUND"); |
|
150 | + |
|
151 | + return null; |
|
152 | + } |
|
153 | + |
|
154 | + $realId = $this->gDavBackend->GetIdOfMapiMessage($addressBookId, $mapimessage); |
|
155 | + |
|
156 | + $session = $this->gDavBackend->GetSession(); |
|
157 | + $ab = $this->gDavBackend->GetAddressBook(); |
|
158 | + |
|
159 | + $vcf = mapi_mapitovcf($session, $ab, $mapimessage, []); |
|
160 | + $this->logger->trace("vcf generated by mapi_mapitovcf: %s%s", PHP_EOL, $vcf); |
|
161 | + $props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]); |
|
162 | + $r = [ |
|
163 | + 'id' => $realId, |
|
164 | + 'uri' => $realId . static::FILE_EXTENSION, |
|
165 | + 'etag' => '"' . $props[PR_LAST_MODIFICATION_TIME] . '"', |
|
166 | + 'lastmodified' => $props[PR_LAST_MODIFICATION_TIME], |
|
167 | + 'carddata' => $vcf, |
|
168 | + 'size' => strlen($vcf), |
|
169 | + 'addressbookid' => $addressBookId, |
|
170 | + ]; |
|
171 | + |
|
172 | + $this->logger->trace("returned data id: %s - size: %d - etag: %s", $r['id'], $r['size'], $r['etag']); |
|
173 | + |
|
174 | + return $r; |
|
175 | + } |
|
176 | + |
|
177 | + /** |
|
178 | + * Creates a new card. |
|
179 | + * |
|
180 | + * The addressbook id will be passed as the first argument. This is the |
|
181 | + * same id as it is returned from the getAddressBooksForUser method. |
|
182 | + * |
|
183 | + * The cardUri is a base uri, and doesn't include the full path. The |
|
184 | + * cardData argument is the vcard body, and is passed as a string. |
|
185 | + * |
|
186 | + * It is possible to return an ETag from this method. This ETag is for the |
|
187 | + * newly created resource, and must be enclosed with double quotes (that |
|
188 | + * is, the string itself must contain the double quotes). |
|
189 | + * |
|
190 | + * You should only return the ETag if you store the carddata as-is. If a |
|
191 | + * subsequent GET request on the same card does not have the same body, |
|
192 | + * byte-by-byte and you did return an ETag here, clients tend to get |
|
193 | + * confused. |
|
194 | + * |
|
195 | + * If you don't return an ETag, you can just return null. |
|
196 | + * |
|
197 | + * @param mixed $addressBookId |
|
198 | + * @param string $cardUri |
|
199 | + * @param string $cardData |
|
200 | + * |
|
201 | + * @return null|string |
|
202 | + */ |
|
203 | + public function createCard($addressBookId, $cardUri, $cardData) { |
|
204 | + $this->logger->trace("addressBookId: %s - cardUri: %s - cardData: %s", $addressBookId, $cardUri, $cardData); |
|
205 | + $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($cardUri, static::FILE_EXTENSION); |
|
206 | + $folder = $this->gDavBackend->GetMapiFolder($addressBookId); |
|
207 | + $mapimessage = $this->gDavBackend->CreateObject($addressBookId, $folder, $objectId); |
|
208 | + $retval = $this->setData($addressBookId, $mapimessage, $cardData); |
|
209 | + if (!$retval) { |
|
210 | + return null; |
|
211 | + } |
|
212 | + |
|
213 | + return '"' . $retval . '"'; |
|
214 | + } |
|
215 | + |
|
216 | + /** |
|
217 | + * Updates a card. |
|
218 | + * |
|
219 | + * The addressbook id will be passed as the first argument. This is the |
|
220 | + * same id as it is returned from the getAddressBooksForUser method. |
|
221 | + * |
|
222 | + * The cardUri is a base uri, and doesn't include the full path. The |
|
223 | + * cardData argument is the vcard body, and is passed as a string. |
|
224 | + * |
|
225 | + * It is possible to return an ETag from this method. This ETag should |
|
226 | + * match that of the updated resource, and must be enclosed with double |
|
227 | + * quotes (that is: the string itself must contain the actual quotes). |
|
228 | + * |
|
229 | + * You should only return the ETag if you store the carddata as-is. If a |
|
230 | + * subsequent GET request on the same card does not have the same body, |
|
231 | + * byte-by-byte and you did return an ETag here, clients tend to get |
|
232 | + * confused. |
|
233 | + * |
|
234 | + * If you don't return an ETag, you can just return null. |
|
235 | + * |
|
236 | + * @param mixed $addressBookId |
|
237 | + * @param string $cardUri |
|
238 | + * @param string $cardData |
|
239 | + * |
|
240 | + * @return null|string |
|
241 | + */ |
|
242 | + public function updateCard($addressBookId, $cardUri, $cardData) { |
|
243 | + $this->logger->trace("addressBookId: %s - cardUri: %s - cardData: %s", $addressBookId, $cardUri, $cardData); |
|
244 | + |
|
245 | + $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, null, static::FILE_EXTENSION); |
|
246 | + $retval = $this->setData($addressBookId, $mapimessage, $cardData); |
|
247 | + if (!$retval) { |
|
248 | + return null; |
|
249 | + } |
|
250 | + |
|
251 | + return '"' . $retval . '"'; |
|
252 | + } |
|
253 | + |
|
254 | + /** |
|
255 | + * Sets data for a contact. |
|
256 | + * |
|
257 | + * @param mixed $addressBookId |
|
258 | + * @param MAPIMessage $mapimessage |
|
259 | + * @param string $vcf |
|
260 | + * |
|
261 | + * @return null|string |
|
262 | + */ |
|
263 | + private function setData($addressBookId, $mapimessage, $vcf) { |
|
264 | + $this->logger->trace("mapimessage: %s - vcf: %s", $mapimessage, $vcf); |
|
265 | + $store = $this->gDavBackend->GetStoreById($addressBookId); |
|
266 | + $session = $this->gDavBackend->GetSession(); |
|
267 | + |
|
268 | + $ok = mapi_vcftomapi($session, $store, $mapimessage, $vcf); |
|
269 | + if ($ok) { |
|
270 | + mapi_savechanges($mapimessage); |
|
271 | + // $props = mapi_getprops($mapimessage, array(PR_LAST_MODIFICATION_TIME)); |
|
272 | + $props = mapi_getprops($mapimessage); |
|
273 | + |
|
274 | + return $props[PR_LAST_MODIFICATION_TIME]; |
|
275 | + } |
|
276 | + |
|
277 | + return null; |
|
278 | + } |
|
279 | + |
|
280 | + /** |
|
281 | + * Deletes a card. |
|
282 | + * |
|
283 | + * @param mixed $addressBookId |
|
284 | + * @param string $cardUri |
|
285 | + * |
|
286 | + * @return bool |
|
287 | + */ |
|
288 | + public function deleteCard($addressBookId, $cardUri) { |
|
289 | + $this->logger->trace("addressBookId: %s - cardUri: %s", $addressBookId, $cardUri); |
|
290 | + $mapifolder = $this->gDavBackend->GetMapiFolder($addressBookId); |
|
291 | + $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($cardUri, static::FILE_EXTENSION); |
|
292 | + |
|
293 | + // to delete we need the PR_ENTRYID of the message |
|
294 | + // TODO move this part to GrommunioDavBackend |
|
295 | + $mapimessage = $this->gDavBackend->GetMapiMessageForId($addressBookId, $cardUri, $mapifolder, static::FILE_EXTENSION); |
|
296 | + $props = mapi_getprops($mapimessage, [PR_ENTRYID]); |
|
297 | + mapi_folder_deletemessages($mapifolder, [$props[PR_ENTRYID]]); |
|
298 | + } |
|
299 | + |
|
300 | + /** |
|
301 | + * The getChanges method returns all the changes that have happened, since |
|
302 | + * the specified syncToken in the specified address book. |
|
303 | + * |
|
304 | + * This function should return an array, such as the following: |
|
305 | + * |
|
306 | + * [ |
|
307 | + * 'syncToken' => 'The current synctoken', |
|
308 | + * 'added' => [ |
|
309 | + * 'new.txt', |
|
310 | + * ], |
|
311 | + * 'modified' => [ |
|
312 | + * 'modified.txt', |
|
313 | + * ], |
|
314 | + * 'deleted' => [ |
|
315 | + * 'foo.php.bak', |
|
316 | + * 'old.txt' |
|
317 | + * ] |
|
318 | + * ]; |
|
319 | + * |
|
320 | + * The returned syncToken property should reflect the *current* syncToken |
|
321 | + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
322 | + * property. This is needed here too, to ensure the operation is atomic. |
|
323 | + * |
|
324 | + * If the $syncToken argument is specified as null, this is an initial |
|
325 | + * sync, and all members should be reported. |
|
326 | + * |
|
327 | + * The modified property is an array of nodenames that have changed since |
|
328 | + * the last token. |
|
329 | + * |
|
330 | + * The deleted property is an array with nodenames, that have been deleted |
|
331 | + * from collection. |
|
332 | + * |
|
333 | + * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
334 | + * 1, you only have to report changes that happened only directly in |
|
335 | + * immediate descendants. If it's 2, it should also include changes from |
|
336 | + * the nodes below the child collections. (grandchildren) |
|
337 | + * |
|
338 | + * The $limit argument allows a client to specify how many results should |
|
339 | + * be returned at most. If the limit is not specified, it should be treated |
|
340 | + * as infinite. |
|
341 | + * |
|
342 | + * If the limit (infinite or not) is higher than you're willing to return, |
|
343 | + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
344 | + * |
|
345 | + * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
346 | + * return null. |
|
347 | + * |
|
348 | + * The limit is 'suggestive'. You are free to ignore it. |
|
349 | + * |
|
350 | + * @param string $addressBookId |
|
351 | + * @param string $syncToken |
|
352 | + * @param int $syncLevel |
|
353 | + * @param int $limit |
|
354 | + * |
|
355 | + * @return array |
|
356 | + */ |
|
357 | + public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { |
|
358 | + $this->logger->trace("addressBookId: %s - syncToken: %s - syncLevel: %d - limit: %d", $addressBookId, $syncToken, $syncLevel, $limit); |
|
359 | + |
|
360 | + return $this->gDavBackend->Sync($addressBookId, $syncToken, static::FILE_EXTENSION, $limit); |
|
361 | + } |
|
362 | 362 | } |
@@ -10,423 +10,423 @@ discard block |
||
10 | 10 | namespace grommunio\DAV; |
11 | 11 | |
12 | 12 | class GrommunioDavBackend { |
13 | - private $logger; |
|
14 | - protected $session; |
|
15 | - protected $stores; |
|
16 | - protected $user; |
|
17 | - protected $customprops; |
|
18 | - protected $syncstate; |
|
19 | - |
|
20 | - /** |
|
21 | - * Constructor. |
|
22 | - */ |
|
23 | - public function __construct(GLogger $glogger) { |
|
24 | - $this->logger = $glogger; |
|
25 | - $this->syncstate = new GrommunioSyncState($glogger, SYNC_DB); |
|
26 | - } |
|
27 | - |
|
28 | - /** |
|
29 | - * Connect to grommunio and create session. |
|
30 | - * |
|
31 | - * @param string $user |
|
32 | - * @param string $pass |
|
33 | - * |
|
34 | - * @return bool |
|
35 | - */ |
|
36 | - public function Logon($user, $pass) { |
|
37 | - $this->logger->trace('%s / password', $user); |
|
38 | - |
|
39 | - $gDavVersion = 'grommunio-dav' . @constant('GDAV_VERSION'); |
|
40 | - $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown'; |
|
41 | - $this->session = mapi_logon_zarafa($user, $pass, MAPI_SERVER, null, null, 1, $gDavVersion, $userAgent); |
|
42 | - if (!$this->session) { |
|
43 | - $this->logger->info("Auth: ERROR - logon failed for user %s", $user); |
|
44 | - |
|
45 | - return false; |
|
46 | - } |
|
47 | - |
|
48 | - $this->user = $user; |
|
49 | - $this->logger->debug("Auth: OK - user %s - session %s", $this->user, $this->session); |
|
50 | - |
|
51 | - return true; |
|
52 | - } |
|
53 | - |
|
54 | - /** |
|
55 | - * Returns the authenticated user. |
|
56 | - * |
|
57 | - * @return string |
|
58 | - */ |
|
59 | - public function GetUser() { |
|
60 | - $this->logger->trace($this->user); |
|
61 | - |
|
62 | - return $this->user; |
|
63 | - } |
|
64 | - |
|
65 | - /** |
|
66 | - * Create a folder with MAPI class. |
|
67 | - * |
|
68 | - * @param mixed $principalUri |
|
69 | - * @param string $url |
|
70 | - * @param string $class |
|
71 | - * @param string $displayname |
|
72 | - * |
|
73 | - * @return string |
|
74 | - */ |
|
75 | - public function CreateFolder($principalUri, $url, $class, $displayname) { |
|
76 | - $props = mapi_getprops($this->GetStore($principalUri), [PR_IPM_SUBTREE_ENTRYID]); |
|
77 | - $folder = mapi_msgstore_openentry($this->GetStore($principalUri), $props[PR_IPM_SUBTREE_ENTRYID]); |
|
78 | - $newfolder = mapi_folder_createfolder($folder, $url, $displayname); |
|
79 | - mapi_setprops($newfolder, [PR_CONTAINER_CLASS => $class]); |
|
80 | - |
|
81 | - return $url; |
|
82 | - } |
|
83 | - |
|
84 | - /** |
|
85 | - * Delete a folder with MAPI class. |
|
86 | - * |
|
87 | - * @param mixed $id |
|
88 | - * |
|
89 | - * @return bool |
|
90 | - */ |
|
91 | - public function DeleteFolder($id) { |
|
92 | - $folder = $this->GetMapiFolder($id); |
|
93 | - if (!$folder) { |
|
94 | - return false; |
|
95 | - } |
|
96 | - |
|
97 | - $props = mapi_getprops($folder, [PR_ENTRYID, PR_PARENT_ENTRYID]); |
|
98 | - $parentfolder = mapi_msgstore_openentry($this->GetStoreById($id), $props[PR_PARENT_ENTRYID]); |
|
99 | - mapi_folder_deletefolder($parentfolder, $props[PR_ENTRYID]); |
|
100 | - |
|
101 | - return true; |
|
102 | - } |
|
103 | - |
|
104 | - /** |
|
105 | - * Returns a list of folders for a MAPI class. |
|
106 | - * |
|
107 | - * @param string $principalUri |
|
108 | - * @param string $class |
|
109 | - * @param mixed $classes |
|
110 | - * |
|
111 | - * @return array |
|
112 | - */ |
|
113 | - public function GetFolders($principalUri, $classes) { |
|
114 | - $this->logger->trace("principal '%s', classes '%s'", $principalUri, $classes); |
|
115 | - $folders = []; |
|
116 | - |
|
117 | - // TODO limit the output to subfolders of the principalUri? |
|
118 | - |
|
119 | - $rootfolder = mapi_msgstore_openentry($this->GetStore($principalUri)); |
|
120 | - $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS); |
|
121 | - // TODO also filter hidden folders |
|
122 | - $restrictions = []; |
|
123 | - foreach ($classes as $class) { |
|
124 | - $restrictions[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => PR_CONTAINER_CLASS, VALUE => $class]]; |
|
125 | - } |
|
126 | - mapi_table_restrict($hierarchy, [RES_OR, $restrictions]); |
|
127 | - |
|
128 | - // TODO how to handle hierarchies? |
|
129 | - $rows = mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_FOLDER_TYPE, PR_LOCAL_COMMIT_TIME_MAX]); |
|
130 | - |
|
131 | - $rootprops = mapi_getprops($rootfolder, [PR_IPM_CONTACT_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID]); |
|
132 | - foreach ($rows as $row) { |
|
133 | - if ($row[PR_FOLDER_TYPE] == FOLDER_SEARCH) { |
|
134 | - continue; |
|
135 | - } |
|
136 | - |
|
137 | - $folder = [ |
|
138 | - 'id' => $principalUri . ":" . bin2hex($row[PR_SOURCE_KEY]), |
|
139 | - 'uri' => $row[PR_DISPLAY_NAME], |
|
140 | - 'principaluri' => $principalUri, |
|
141 | - '{http://sabredav.org/ns}sync-token' => '0000000000', |
|
142 | - '{DAV:}displayname' => $row[PR_DISPLAY_NAME], |
|
143 | - '{http://calendarserver.org/ns/}getctag' => isset($row[PR_LOCAL_COMMIT_TIME_MAX]) ? strval($row[PR_LOCAL_COMMIT_TIME_MAX]) : '0000000000', |
|
144 | - ]; |
|
145 | - |
|
146 | - // ensure default contacts folder is put first, some clients |
|
147 | - // i.e. Apple Addressbook only supports one contact folder, |
|
148 | - // therefore it is desired that folder is the default one. |
|
149 | - if (in_array("IPF.Contact", $classes) && isset($rootprops[PR_IPM_CONTACT_ENTRYID]) && $row[PR_ENTRYID] == $rootprops[PR_IPM_CONTACT_ENTRYID]) { |
|
150 | - array_unshift($folders, $folder); |
|
151 | - } |
|
152 | - // ensure default calendar folder is put first, |
|
153 | - // before the tasks folder. |
|
154 | - elseif (in_array('IPF.Appointment', $classes) && isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID]) && $row[PR_ENTRYID] == $rootprops[PR_IPM_APPOINTMENT_ENTRYID]) { |
|
155 | - array_unshift($folders, $folder); |
|
156 | - } |
|
157 | - else { |
|
158 | - array_push($folders, $folder); |
|
159 | - } |
|
160 | - } |
|
161 | - $this->logger->trace('found %d folders: %s', count($folders), $folders); |
|
162 | - |
|
163 | - return $folders; |
|
164 | - } |
|
165 | - |
|
166 | - /** |
|
167 | - * Returns a list of objects for a folder given by the id. |
|
168 | - * |
|
169 | - * @param string $id |
|
170 | - * @param string $fileExtension |
|
171 | - * @param array $filters |
|
172 | - * |
|
173 | - * @return array |
|
174 | - */ |
|
175 | - public function GetObjects($id, $fileExtension, $filters = []) { |
|
176 | - $folder = $this->GetMapiFolder($id); |
|
177 | - $properties = $this->GetCustomProperties($id); |
|
178 | - $table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS); |
|
179 | - |
|
180 | - $restrictions = []; |
|
181 | - if (isset($filters['start'], $filters['end'])) { |
|
182 | - $this->logger->trace("got start: %d and end: %d", $filters['start'], $filters['end']); |
|
183 | - $subrestriction = $this->GetCalendarRestriction($this->GetStoreById($id), $filters['start'], $filters['end']); |
|
184 | - $restrictions[] = $subrestriction; |
|
185 | - } |
|
186 | - if (isset($filters['types'])) { |
|
187 | - $this->logger->trace("got types: %s", $filters['types']); |
|
188 | - $arr = []; |
|
189 | - foreach ($filters['types'] as $filter) { |
|
190 | - $arr[] = [RES_PROPERTY, |
|
191 | - [RELOP => RELOP_EQ, |
|
192 | - ULPROPTAG => PR_MESSAGE_CLASS, |
|
193 | - VALUE => $filter, |
|
194 | - ], |
|
195 | - ]; |
|
196 | - } |
|
197 | - $restrictions[] = [RES_OR, $arr]; |
|
198 | - } |
|
199 | - if (!empty($restrictions)) { |
|
200 | - $restriction = [RES_AND, $restrictions]; |
|
201 | - $this->logger->trace("Got restriction: %s", $restriction); |
|
202 | - mapi_table_restrict($table, $restriction); |
|
203 | - } |
|
204 | - |
|
205 | - $rows = mapi_table_queryallrows($table, [PR_SOURCE_KEY, PR_LAST_MODIFICATION_TIME, PR_MESSAGE_SIZE, $properties['goid']]); |
|
206 | - |
|
207 | - $results = []; |
|
208 | - foreach ($rows as $row) { |
|
209 | - $realId = ""; |
|
210 | - if (isset($row[$properties['goid']])) { |
|
211 | - $realId = getUidFromGoid($row[$properties['goid']]); |
|
212 | - } |
|
213 | - if (!$realId) { |
|
214 | - $realId = bin2hex($row[PR_SOURCE_KEY]); |
|
215 | - } |
|
216 | - |
|
217 | - $result = [ |
|
218 | - 'id' => $realId, |
|
219 | - 'uri' => $realId . $fileExtension, |
|
220 | - 'etag' => '"' . $row[PR_LAST_MODIFICATION_TIME] . '"', |
|
221 | - 'lastmodified' => $row[PR_LAST_MODIFICATION_TIME], |
|
222 | - 'size' => $row[PR_MESSAGE_SIZE], // only approximation |
|
223 | - ]; |
|
224 | - |
|
225 | - if ($fileExtension == GrommunioCalDavBackend::FILE_EXTENSION) { |
|
226 | - $result['calendarid'] = $id; |
|
227 | - } |
|
228 | - elseif ($fileExtension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
229 | - $result['addressbookid'] = $id; |
|
230 | - } |
|
231 | - $results[] = $result; |
|
232 | - } |
|
233 | - |
|
234 | - return $results; |
|
235 | - } |
|
236 | - |
|
237 | - /** |
|
238 | - * Create the object and set appttsref. |
|
239 | - * |
|
240 | - * @param mixed $folderId |
|
241 | - * @param string $folder |
|
242 | - * @param string $objectId |
|
243 | - * |
|
244 | - * @return mapiresource |
|
245 | - */ |
|
246 | - public function CreateObject($folderId, $folder, $objectId) { |
|
247 | - $mapimessage = mapi_folder_createmessage($folder); |
|
248 | - // we save the objectId in PROP_APPTTSREF so we find it by this id |
|
249 | - $properties = $this->GetCustomProperties($folderId); |
|
250 | - // FIXME: uid for contacts |
|
251 | - $goid = getGoidFromUid($objectId); |
|
252 | - mapi_setprops($mapimessage, [$properties['goid'] => $goid]); |
|
253 | - |
|
254 | - return $mapimessage; |
|
255 | - } |
|
256 | - |
|
257 | - /** |
|
258 | - * Returns a mapi folder resource for a folderid (PR_SOURCE_KEY). |
|
259 | - * |
|
260 | - * @param string $folderid |
|
261 | - * |
|
262 | - * @return mapiresource |
|
263 | - */ |
|
264 | - public function GetMapiFolder($folderid) { |
|
265 | - $this->logger->trace('Id: %s', $folderid); |
|
266 | - $arr = explode(':', $folderid); |
|
267 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->GetStore($arr[0]), hex2bin($arr[1])); |
|
268 | - |
|
269 | - return mapi_msgstore_openentry($this->GetStore($arr[0]), $entryid); |
|
270 | - } |
|
271 | - |
|
272 | - /** |
|
273 | - * Returns MAPI addressbook. |
|
274 | - * |
|
275 | - * @return MAPIAddressbook |
|
276 | - */ |
|
277 | - public function GetAddressBook() { |
|
278 | - // TODO should be a singleton |
|
279 | - return mapi_openaddressbook($this->session); |
|
280 | - } |
|
281 | - |
|
282 | - /** |
|
283 | - * Opens MAPI store for the user. |
|
284 | - * |
|
285 | - * @param string $username |
|
286 | - * |
|
287 | - * @return false|MAPIStore if store not available |
|
288 | - */ |
|
289 | - public function OpenMapiStore($username = null) { |
|
290 | - $msgstorestable = mapi_getmsgstorestable($this->session); |
|
291 | - $msgstores = mapi_table_queryallrows($msgstorestable, [PR_DEFAULT_STORE, PR_ENTRYID, PR_MDB_PROVIDER]); |
|
292 | - |
|
293 | - $defaultstore = null; |
|
294 | - $publicstore = null; |
|
295 | - foreach ($msgstores as $row) { |
|
296 | - if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) { |
|
297 | - $defaultstore = $row[PR_ENTRYID]; |
|
298 | - } |
|
299 | - if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == KOPANO_STORE_PUBLIC_GUID) { |
|
300 | - $publicstore = $row[PR_ENTRYID]; |
|
301 | - } |
|
302 | - } |
|
303 | - |
|
304 | - /* user's own store or public store */ |
|
305 | - if ($username == $this->GetUser() && $defaultstore != null) { |
|
306 | - return mapi_openmsgstore($this->session, $defaultstore); |
|
307 | - } |
|
308 | - if ($username == 'public' && $publicstore != null) { |
|
309 | - return mapi_openmsgstore($this->session, $publicstore); |
|
310 | - } |
|
311 | - |
|
312 | - /* otherwise other user's store */ |
|
313 | - $store = mapi_openmsgstore($this->session, $defaultstore); |
|
314 | - if (!$store) { |
|
315 | - return false; |
|
316 | - } |
|
317 | - $otherstore = mapi_msgstore_createentryid($store, $username); |
|
318 | - |
|
319 | - return mapi_openmsgstore($this->session, $otherstore); |
|
320 | - } |
|
321 | - |
|
322 | - /** |
|
323 | - * Returns store for the user. |
|
324 | - * |
|
325 | - * @param string $storename |
|
326 | - * |
|
327 | - * @return false|MAPIStore if the store is not available |
|
328 | - */ |
|
329 | - public function GetStore($storename) { |
|
330 | - if ($storename == null) { |
|
331 | - $storename = $this->GetUser(); |
|
332 | - } |
|
333 | - else { |
|
334 | - $storename = str_replace('principals/', '', $storename); |
|
335 | - } |
|
336 | - $this->logger->trace("storename %s", $storename); |
|
337 | - |
|
338 | - /* We already got the store */ |
|
339 | - if (isset($this->stores[$storename]) && $this->stores[$storename] != null) { |
|
340 | - return $this->stores[$storename]; |
|
341 | - } |
|
342 | - |
|
343 | - $this->stores[$storename] = $this->OpenMapiStore($storename); |
|
344 | - if (!$this->stores[$storename]) { |
|
345 | - $this->logger->info("Auth: ERROR - unable to open store for %s (0x%08X)", $storename, mapi_last_hresult()); |
|
346 | - |
|
347 | - return false; |
|
348 | - } |
|
349 | - |
|
350 | - return $this->stores[$storename]; |
|
351 | - } |
|
352 | - |
|
353 | - /** |
|
354 | - * Returns store from the id. |
|
355 | - * |
|
356 | - * @param mixed $id |
|
357 | - * |
|
358 | - * @return false|\grommunio\DAV\MAPIStore on error |
|
359 | - */ |
|
360 | - public function GetStoreById($id) { |
|
361 | - $arr = explode(':', $id); |
|
362 | - |
|
363 | - return $this->GetStore($arr[0]); |
|
364 | - } |
|
365 | - |
|
366 | - /** |
|
367 | - * Returns logon session. |
|
368 | - * |
|
369 | - * @return MAPISession |
|
370 | - */ |
|
371 | - public function GetSession() { |
|
372 | - return $this->session; |
|
373 | - } |
|
374 | - |
|
375 | - /** |
|
376 | - * Returns an object ID of a mapi object. |
|
377 | - * If set, goid will be preferred. If not the PR_SOURCE_KEY of the message (as hex) will be returned. |
|
378 | - * |
|
379 | - * This order is reflected as well when searching for a message with these ids in GrommunioDavBackend->GetMapiMessageForId(). |
|
380 | - * |
|
381 | - * @param string $folderId |
|
382 | - * @param mapiresource $mapimessage |
|
383 | - * |
|
384 | - * @return string |
|
385 | - */ |
|
386 | - public function GetIdOfMapiMessage($folderId, $mapimessage) { |
|
387 | - $this->logger->trace("Finding ID of %s", $mapimessage); |
|
388 | - $properties = $this->GetCustomProperties($folderId); |
|
389 | - |
|
390 | - // It's one of these, order: |
|
391 | - // - GOID (if set) |
|
392 | - // - PROP_VCARDUID (if set) |
|
393 | - // - PR_SOURCE_KEY |
|
394 | - $props = mapi_getprops($mapimessage, [$properties['goid'], PR_SOURCE_KEY]); |
|
395 | - if (isset($props[$properties['goid']])) { |
|
396 | - $id = getUidFromGoid($props[$properties['goid']]); |
|
397 | - $this->logger->debug("Found uid %s from goid: %s", $id, bin2hex($props[$properties['goid']])); |
|
398 | - if ($id != null) { |
|
399 | - return $id; |
|
400 | - } |
|
401 | - } |
|
402 | - // PR_SOURCE_KEY is always available |
|
403 | - $id = bin2hex($props[PR_SOURCE_KEY]); |
|
404 | - $this->logger->debug("Found PR_SOURCE_KEY: %s", $id); |
|
405 | - |
|
406 | - return $id; |
|
407 | - } |
|
408 | - |
|
409 | - /** |
|
410 | - * Finds and opens a MapiMessage from an objectId. |
|
411 | - * The id can be a PROP_APPTTSREF or a PR_SOURCE_KEY (as hex). |
|
412 | - * |
|
413 | - * @param string $folderId |
|
414 | - * @param string $objectUri |
|
415 | - * @param mapiresource $mapifolder optional |
|
416 | - * @param string $extension optional |
|
417 | - * |
|
418 | - * @return null|mapiresource |
|
419 | - */ |
|
420 | - public function GetMapiMessageForId($folderId, $objectUri, $mapifolder = null, $extension = null) { |
|
421 | - $this->logger->trace("Searching for '%s' in '%s' (%s) (%s)", $objectUri, $folderId, $mapifolder, $extension); |
|
422 | - |
|
423 | - if (!$mapifolder) { |
|
424 | - $mapifolder = $this->GetMapiFolder($folderId); |
|
425 | - } |
|
426 | - |
|
427 | - $id = $this->GetObjectIdFromObjectUri($objectUri, $extension); |
|
428 | - |
|
429 | - /* The ID can be several different things: |
|
13 | + private $logger; |
|
14 | + protected $session; |
|
15 | + protected $stores; |
|
16 | + protected $user; |
|
17 | + protected $customprops; |
|
18 | + protected $syncstate; |
|
19 | + |
|
20 | + /** |
|
21 | + * Constructor. |
|
22 | + */ |
|
23 | + public function __construct(GLogger $glogger) { |
|
24 | + $this->logger = $glogger; |
|
25 | + $this->syncstate = new GrommunioSyncState($glogger, SYNC_DB); |
|
26 | + } |
|
27 | + |
|
28 | + /** |
|
29 | + * Connect to grommunio and create session. |
|
30 | + * |
|
31 | + * @param string $user |
|
32 | + * @param string $pass |
|
33 | + * |
|
34 | + * @return bool |
|
35 | + */ |
|
36 | + public function Logon($user, $pass) { |
|
37 | + $this->logger->trace('%s / password', $user); |
|
38 | + |
|
39 | + $gDavVersion = 'grommunio-dav' . @constant('GDAV_VERSION'); |
|
40 | + $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown'; |
|
41 | + $this->session = mapi_logon_zarafa($user, $pass, MAPI_SERVER, null, null, 1, $gDavVersion, $userAgent); |
|
42 | + if (!$this->session) { |
|
43 | + $this->logger->info("Auth: ERROR - logon failed for user %s", $user); |
|
44 | + |
|
45 | + return false; |
|
46 | + } |
|
47 | + |
|
48 | + $this->user = $user; |
|
49 | + $this->logger->debug("Auth: OK - user %s - session %s", $this->user, $this->session); |
|
50 | + |
|
51 | + return true; |
|
52 | + } |
|
53 | + |
|
54 | + /** |
|
55 | + * Returns the authenticated user. |
|
56 | + * |
|
57 | + * @return string |
|
58 | + */ |
|
59 | + public function GetUser() { |
|
60 | + $this->logger->trace($this->user); |
|
61 | + |
|
62 | + return $this->user; |
|
63 | + } |
|
64 | + |
|
65 | + /** |
|
66 | + * Create a folder with MAPI class. |
|
67 | + * |
|
68 | + * @param mixed $principalUri |
|
69 | + * @param string $url |
|
70 | + * @param string $class |
|
71 | + * @param string $displayname |
|
72 | + * |
|
73 | + * @return string |
|
74 | + */ |
|
75 | + public function CreateFolder($principalUri, $url, $class, $displayname) { |
|
76 | + $props = mapi_getprops($this->GetStore($principalUri), [PR_IPM_SUBTREE_ENTRYID]); |
|
77 | + $folder = mapi_msgstore_openentry($this->GetStore($principalUri), $props[PR_IPM_SUBTREE_ENTRYID]); |
|
78 | + $newfolder = mapi_folder_createfolder($folder, $url, $displayname); |
|
79 | + mapi_setprops($newfolder, [PR_CONTAINER_CLASS => $class]); |
|
80 | + |
|
81 | + return $url; |
|
82 | + } |
|
83 | + |
|
84 | + /** |
|
85 | + * Delete a folder with MAPI class. |
|
86 | + * |
|
87 | + * @param mixed $id |
|
88 | + * |
|
89 | + * @return bool |
|
90 | + */ |
|
91 | + public function DeleteFolder($id) { |
|
92 | + $folder = $this->GetMapiFolder($id); |
|
93 | + if (!$folder) { |
|
94 | + return false; |
|
95 | + } |
|
96 | + |
|
97 | + $props = mapi_getprops($folder, [PR_ENTRYID, PR_PARENT_ENTRYID]); |
|
98 | + $parentfolder = mapi_msgstore_openentry($this->GetStoreById($id), $props[PR_PARENT_ENTRYID]); |
|
99 | + mapi_folder_deletefolder($parentfolder, $props[PR_ENTRYID]); |
|
100 | + |
|
101 | + return true; |
|
102 | + } |
|
103 | + |
|
104 | + /** |
|
105 | + * Returns a list of folders for a MAPI class. |
|
106 | + * |
|
107 | + * @param string $principalUri |
|
108 | + * @param string $class |
|
109 | + * @param mixed $classes |
|
110 | + * |
|
111 | + * @return array |
|
112 | + */ |
|
113 | + public function GetFolders($principalUri, $classes) { |
|
114 | + $this->logger->trace("principal '%s', classes '%s'", $principalUri, $classes); |
|
115 | + $folders = []; |
|
116 | + |
|
117 | + // TODO limit the output to subfolders of the principalUri? |
|
118 | + |
|
119 | + $rootfolder = mapi_msgstore_openentry($this->GetStore($principalUri)); |
|
120 | + $hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH | MAPI_DEFERRED_ERRORS); |
|
121 | + // TODO also filter hidden folders |
|
122 | + $restrictions = []; |
|
123 | + foreach ($classes as $class) { |
|
124 | + $restrictions[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => PR_CONTAINER_CLASS, VALUE => $class]]; |
|
125 | + } |
|
126 | + mapi_table_restrict($hierarchy, [RES_OR, $restrictions]); |
|
127 | + |
|
128 | + // TODO how to handle hierarchies? |
|
129 | + $rows = mapi_table_queryallrows($hierarchy, [PR_DISPLAY_NAME, PR_ENTRYID, PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_FOLDER_TYPE, PR_LOCAL_COMMIT_TIME_MAX]); |
|
130 | + |
|
131 | + $rootprops = mapi_getprops($rootfolder, [PR_IPM_CONTACT_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID]); |
|
132 | + foreach ($rows as $row) { |
|
133 | + if ($row[PR_FOLDER_TYPE] == FOLDER_SEARCH) { |
|
134 | + continue; |
|
135 | + } |
|
136 | + |
|
137 | + $folder = [ |
|
138 | + 'id' => $principalUri . ":" . bin2hex($row[PR_SOURCE_KEY]), |
|
139 | + 'uri' => $row[PR_DISPLAY_NAME], |
|
140 | + 'principaluri' => $principalUri, |
|
141 | + '{http://sabredav.org/ns}sync-token' => '0000000000', |
|
142 | + '{DAV:}displayname' => $row[PR_DISPLAY_NAME], |
|
143 | + '{http://calendarserver.org/ns/}getctag' => isset($row[PR_LOCAL_COMMIT_TIME_MAX]) ? strval($row[PR_LOCAL_COMMIT_TIME_MAX]) : '0000000000', |
|
144 | + ]; |
|
145 | + |
|
146 | + // ensure default contacts folder is put first, some clients |
|
147 | + // i.e. Apple Addressbook only supports one contact folder, |
|
148 | + // therefore it is desired that folder is the default one. |
|
149 | + if (in_array("IPF.Contact", $classes) && isset($rootprops[PR_IPM_CONTACT_ENTRYID]) && $row[PR_ENTRYID] == $rootprops[PR_IPM_CONTACT_ENTRYID]) { |
|
150 | + array_unshift($folders, $folder); |
|
151 | + } |
|
152 | + // ensure default calendar folder is put first, |
|
153 | + // before the tasks folder. |
|
154 | + elseif (in_array('IPF.Appointment', $classes) && isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID]) && $row[PR_ENTRYID] == $rootprops[PR_IPM_APPOINTMENT_ENTRYID]) { |
|
155 | + array_unshift($folders, $folder); |
|
156 | + } |
|
157 | + else { |
|
158 | + array_push($folders, $folder); |
|
159 | + } |
|
160 | + } |
|
161 | + $this->logger->trace('found %d folders: %s', count($folders), $folders); |
|
162 | + |
|
163 | + return $folders; |
|
164 | + } |
|
165 | + |
|
166 | + /** |
|
167 | + * Returns a list of objects for a folder given by the id. |
|
168 | + * |
|
169 | + * @param string $id |
|
170 | + * @param string $fileExtension |
|
171 | + * @param array $filters |
|
172 | + * |
|
173 | + * @return array |
|
174 | + */ |
|
175 | + public function GetObjects($id, $fileExtension, $filters = []) { |
|
176 | + $folder = $this->GetMapiFolder($id); |
|
177 | + $properties = $this->GetCustomProperties($id); |
|
178 | + $table = mapi_folder_getcontentstable($folder, MAPI_DEFERRED_ERRORS); |
|
179 | + |
|
180 | + $restrictions = []; |
|
181 | + if (isset($filters['start'], $filters['end'])) { |
|
182 | + $this->logger->trace("got start: %d and end: %d", $filters['start'], $filters['end']); |
|
183 | + $subrestriction = $this->GetCalendarRestriction($this->GetStoreById($id), $filters['start'], $filters['end']); |
|
184 | + $restrictions[] = $subrestriction; |
|
185 | + } |
|
186 | + if (isset($filters['types'])) { |
|
187 | + $this->logger->trace("got types: %s", $filters['types']); |
|
188 | + $arr = []; |
|
189 | + foreach ($filters['types'] as $filter) { |
|
190 | + $arr[] = [RES_PROPERTY, |
|
191 | + [RELOP => RELOP_EQ, |
|
192 | + ULPROPTAG => PR_MESSAGE_CLASS, |
|
193 | + VALUE => $filter, |
|
194 | + ], |
|
195 | + ]; |
|
196 | + } |
|
197 | + $restrictions[] = [RES_OR, $arr]; |
|
198 | + } |
|
199 | + if (!empty($restrictions)) { |
|
200 | + $restriction = [RES_AND, $restrictions]; |
|
201 | + $this->logger->trace("Got restriction: %s", $restriction); |
|
202 | + mapi_table_restrict($table, $restriction); |
|
203 | + } |
|
204 | + |
|
205 | + $rows = mapi_table_queryallrows($table, [PR_SOURCE_KEY, PR_LAST_MODIFICATION_TIME, PR_MESSAGE_SIZE, $properties['goid']]); |
|
206 | + |
|
207 | + $results = []; |
|
208 | + foreach ($rows as $row) { |
|
209 | + $realId = ""; |
|
210 | + if (isset($row[$properties['goid']])) { |
|
211 | + $realId = getUidFromGoid($row[$properties['goid']]); |
|
212 | + } |
|
213 | + if (!$realId) { |
|
214 | + $realId = bin2hex($row[PR_SOURCE_KEY]); |
|
215 | + } |
|
216 | + |
|
217 | + $result = [ |
|
218 | + 'id' => $realId, |
|
219 | + 'uri' => $realId . $fileExtension, |
|
220 | + 'etag' => '"' . $row[PR_LAST_MODIFICATION_TIME] . '"', |
|
221 | + 'lastmodified' => $row[PR_LAST_MODIFICATION_TIME], |
|
222 | + 'size' => $row[PR_MESSAGE_SIZE], // only approximation |
|
223 | + ]; |
|
224 | + |
|
225 | + if ($fileExtension == GrommunioCalDavBackend::FILE_EXTENSION) { |
|
226 | + $result['calendarid'] = $id; |
|
227 | + } |
|
228 | + elseif ($fileExtension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
229 | + $result['addressbookid'] = $id; |
|
230 | + } |
|
231 | + $results[] = $result; |
|
232 | + } |
|
233 | + |
|
234 | + return $results; |
|
235 | + } |
|
236 | + |
|
237 | + /** |
|
238 | + * Create the object and set appttsref. |
|
239 | + * |
|
240 | + * @param mixed $folderId |
|
241 | + * @param string $folder |
|
242 | + * @param string $objectId |
|
243 | + * |
|
244 | + * @return mapiresource |
|
245 | + */ |
|
246 | + public function CreateObject($folderId, $folder, $objectId) { |
|
247 | + $mapimessage = mapi_folder_createmessage($folder); |
|
248 | + // we save the objectId in PROP_APPTTSREF so we find it by this id |
|
249 | + $properties = $this->GetCustomProperties($folderId); |
|
250 | + // FIXME: uid for contacts |
|
251 | + $goid = getGoidFromUid($objectId); |
|
252 | + mapi_setprops($mapimessage, [$properties['goid'] => $goid]); |
|
253 | + |
|
254 | + return $mapimessage; |
|
255 | + } |
|
256 | + |
|
257 | + /** |
|
258 | + * Returns a mapi folder resource for a folderid (PR_SOURCE_KEY). |
|
259 | + * |
|
260 | + * @param string $folderid |
|
261 | + * |
|
262 | + * @return mapiresource |
|
263 | + */ |
|
264 | + public function GetMapiFolder($folderid) { |
|
265 | + $this->logger->trace('Id: %s', $folderid); |
|
266 | + $arr = explode(':', $folderid); |
|
267 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->GetStore($arr[0]), hex2bin($arr[1])); |
|
268 | + |
|
269 | + return mapi_msgstore_openentry($this->GetStore($arr[0]), $entryid); |
|
270 | + } |
|
271 | + |
|
272 | + /** |
|
273 | + * Returns MAPI addressbook. |
|
274 | + * |
|
275 | + * @return MAPIAddressbook |
|
276 | + */ |
|
277 | + public function GetAddressBook() { |
|
278 | + // TODO should be a singleton |
|
279 | + return mapi_openaddressbook($this->session); |
|
280 | + } |
|
281 | + |
|
282 | + /** |
|
283 | + * Opens MAPI store for the user. |
|
284 | + * |
|
285 | + * @param string $username |
|
286 | + * |
|
287 | + * @return false|MAPIStore if store not available |
|
288 | + */ |
|
289 | + public function OpenMapiStore($username = null) { |
|
290 | + $msgstorestable = mapi_getmsgstorestable($this->session); |
|
291 | + $msgstores = mapi_table_queryallrows($msgstorestable, [PR_DEFAULT_STORE, PR_ENTRYID, PR_MDB_PROVIDER]); |
|
292 | + |
|
293 | + $defaultstore = null; |
|
294 | + $publicstore = null; |
|
295 | + foreach ($msgstores as $row) { |
|
296 | + if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) { |
|
297 | + $defaultstore = $row[PR_ENTRYID]; |
|
298 | + } |
|
299 | + if (isset($row[PR_MDB_PROVIDER]) && $row[PR_MDB_PROVIDER] == KOPANO_STORE_PUBLIC_GUID) { |
|
300 | + $publicstore = $row[PR_ENTRYID]; |
|
301 | + } |
|
302 | + } |
|
303 | + |
|
304 | + /* user's own store or public store */ |
|
305 | + if ($username == $this->GetUser() && $defaultstore != null) { |
|
306 | + return mapi_openmsgstore($this->session, $defaultstore); |
|
307 | + } |
|
308 | + if ($username == 'public' && $publicstore != null) { |
|
309 | + return mapi_openmsgstore($this->session, $publicstore); |
|
310 | + } |
|
311 | + |
|
312 | + /* otherwise other user's store */ |
|
313 | + $store = mapi_openmsgstore($this->session, $defaultstore); |
|
314 | + if (!$store) { |
|
315 | + return false; |
|
316 | + } |
|
317 | + $otherstore = mapi_msgstore_createentryid($store, $username); |
|
318 | + |
|
319 | + return mapi_openmsgstore($this->session, $otherstore); |
|
320 | + } |
|
321 | + |
|
322 | + /** |
|
323 | + * Returns store for the user. |
|
324 | + * |
|
325 | + * @param string $storename |
|
326 | + * |
|
327 | + * @return false|MAPIStore if the store is not available |
|
328 | + */ |
|
329 | + public function GetStore($storename) { |
|
330 | + if ($storename == null) { |
|
331 | + $storename = $this->GetUser(); |
|
332 | + } |
|
333 | + else { |
|
334 | + $storename = str_replace('principals/', '', $storename); |
|
335 | + } |
|
336 | + $this->logger->trace("storename %s", $storename); |
|
337 | + |
|
338 | + /* We already got the store */ |
|
339 | + if (isset($this->stores[$storename]) && $this->stores[$storename] != null) { |
|
340 | + return $this->stores[$storename]; |
|
341 | + } |
|
342 | + |
|
343 | + $this->stores[$storename] = $this->OpenMapiStore($storename); |
|
344 | + if (!$this->stores[$storename]) { |
|
345 | + $this->logger->info("Auth: ERROR - unable to open store for %s (0x%08X)", $storename, mapi_last_hresult()); |
|
346 | + |
|
347 | + return false; |
|
348 | + } |
|
349 | + |
|
350 | + return $this->stores[$storename]; |
|
351 | + } |
|
352 | + |
|
353 | + /** |
|
354 | + * Returns store from the id. |
|
355 | + * |
|
356 | + * @param mixed $id |
|
357 | + * |
|
358 | + * @return false|\grommunio\DAV\MAPIStore on error |
|
359 | + */ |
|
360 | + public function GetStoreById($id) { |
|
361 | + $arr = explode(':', $id); |
|
362 | + |
|
363 | + return $this->GetStore($arr[0]); |
|
364 | + } |
|
365 | + |
|
366 | + /** |
|
367 | + * Returns logon session. |
|
368 | + * |
|
369 | + * @return MAPISession |
|
370 | + */ |
|
371 | + public function GetSession() { |
|
372 | + return $this->session; |
|
373 | + } |
|
374 | + |
|
375 | + /** |
|
376 | + * Returns an object ID of a mapi object. |
|
377 | + * If set, goid will be preferred. If not the PR_SOURCE_KEY of the message (as hex) will be returned. |
|
378 | + * |
|
379 | + * This order is reflected as well when searching for a message with these ids in GrommunioDavBackend->GetMapiMessageForId(). |
|
380 | + * |
|
381 | + * @param string $folderId |
|
382 | + * @param mapiresource $mapimessage |
|
383 | + * |
|
384 | + * @return string |
|
385 | + */ |
|
386 | + public function GetIdOfMapiMessage($folderId, $mapimessage) { |
|
387 | + $this->logger->trace("Finding ID of %s", $mapimessage); |
|
388 | + $properties = $this->GetCustomProperties($folderId); |
|
389 | + |
|
390 | + // It's one of these, order: |
|
391 | + // - GOID (if set) |
|
392 | + // - PROP_VCARDUID (if set) |
|
393 | + // - PR_SOURCE_KEY |
|
394 | + $props = mapi_getprops($mapimessage, [$properties['goid'], PR_SOURCE_KEY]); |
|
395 | + if (isset($props[$properties['goid']])) { |
|
396 | + $id = getUidFromGoid($props[$properties['goid']]); |
|
397 | + $this->logger->debug("Found uid %s from goid: %s", $id, bin2hex($props[$properties['goid']])); |
|
398 | + if ($id != null) { |
|
399 | + return $id; |
|
400 | + } |
|
401 | + } |
|
402 | + // PR_SOURCE_KEY is always available |
|
403 | + $id = bin2hex($props[PR_SOURCE_KEY]); |
|
404 | + $this->logger->debug("Found PR_SOURCE_KEY: %s", $id); |
|
405 | + |
|
406 | + return $id; |
|
407 | + } |
|
408 | + |
|
409 | + /** |
|
410 | + * Finds and opens a MapiMessage from an objectId. |
|
411 | + * The id can be a PROP_APPTTSREF or a PR_SOURCE_KEY (as hex). |
|
412 | + * |
|
413 | + * @param string $folderId |
|
414 | + * @param string $objectUri |
|
415 | + * @param mapiresource $mapifolder optional |
|
416 | + * @param string $extension optional |
|
417 | + * |
|
418 | + * @return null|mapiresource |
|
419 | + */ |
|
420 | + public function GetMapiMessageForId($folderId, $objectUri, $mapifolder = null, $extension = null) { |
|
421 | + $this->logger->trace("Searching for '%s' in '%s' (%s) (%s)", $objectUri, $folderId, $mapifolder, $extension); |
|
422 | + |
|
423 | + if (!$mapifolder) { |
|
424 | + $mapifolder = $this->GetMapiFolder($folderId); |
|
425 | + } |
|
426 | + |
|
427 | + $id = $this->GetObjectIdFromObjectUri($objectUri, $extension); |
|
428 | + |
|
429 | + /* The ID can be several different things: |
|
430 | 430 | * - a UID that is saved in goid |
431 | 431 | * - a PROP_VCARDUID |
432 | 432 | * - a PR_SOURCE_KEY |
@@ -438,306 +438,306 @@ discard block |
||
438 | 438 | * if it's vcf: |
439 | 439 | * - search PROP_VCARDUID value |
440 | 440 | */ |
441 | - $entryid = false; |
|
442 | - $restriction = false; |
|
443 | - |
|
444 | - if (ctype_xdigit($id) && strlen($id) % 2 == 0) { |
|
445 | - $this->logger->trace("Try PR_SOURCE_KEY %s", $id); |
|
446 | - $arr = explode(':', $folderId); |
|
447 | - $entryid = mapi_msgstore_entryidfromsourcekey($this->GetStoreById($arr[0]), hex2bin($arr[1]), hex2bin($id)); |
|
448 | - } |
|
449 | - |
|
450 | - if (!$entryid) { |
|
451 | - $this->logger->trace("Entryid not found. Try goid/vcarduid %s", $id); |
|
452 | - |
|
453 | - $properties = $this->GetCustomProperties($folderId); |
|
454 | - if (strpos($id, '%40') !== false) { |
|
455 | - $this->logger->debug("The id contains '%40'. Use urldecode."); |
|
456 | - $id = urldecode($id); |
|
457 | - } |
|
458 | - $restriction = []; |
|
459 | - |
|
460 | - if ($extension) { |
|
461 | - if ($extension == GrommunioCalDavBackend::FILE_EXTENSION) { |
|
462 | - $this->logger->trace("Try goid %s", $id); |
|
463 | - $goid = getGoidFromUid($id); |
|
464 | - $this->logger->trace("Try goid 0x%08X => %s", $properties["goid"], bin2hex($goid)); |
|
465 | - $restriction[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["goid"], VALUE => $goid]]; |
|
466 | - } |
|
467 | - elseif ($extension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
468 | - $this->logger->trace("Try vcarduid %s", $id); |
|
469 | - $restriction[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["vcarduid"], VALUE => $id]]; |
|
470 | - } |
|
471 | - } |
|
472 | - } |
|
473 | - |
|
474 | - // find the message if we have a restriction |
|
475 | - if ($restriction) { |
|
476 | - $table = mapi_folder_getcontentstable($mapifolder, MAPI_DEFERRED_ERRORS); |
|
477 | - mapi_table_restrict($table, [RES_OR, $restriction]); |
|
478 | - // Get requested properties, plus whatever we need |
|
479 | - $proplist = [PR_ENTRYID]; |
|
480 | - $rows = mapi_table_queryallrows($table, $proplist); |
|
481 | - if (count($rows) > 1) { |
|
482 | - $this->logger->warn("Found %d entries for id '%s' searching for message", count($rows), $id); |
|
483 | - } |
|
484 | - if (isset($rows[0], $rows[0][PR_ENTRYID])) { |
|
485 | - $entryid = $rows[0][PR_ENTRYID]; |
|
486 | - } |
|
487 | - } |
|
488 | - if ($entryid) { |
|
489 | - $mapimessage = mapi_msgstore_openentry($this->GetStoreById($folderId), $entryid); |
|
490 | - if (!$mapimessage) { |
|
491 | - $this->logger->warn("Error, unable to open entry id: 0x%X", $entryid, mapi_last_hresult()); |
|
492 | - |
|
493 | - return null; |
|
494 | - } |
|
495 | - |
|
496 | - return $mapimessage; |
|
497 | - } |
|
498 | - $this->logger->debug("Nothing found for %s", $id); |
|
499 | - |
|
500 | - return null; |
|
501 | - } |
|
502 | - |
|
503 | - /** |
|
504 | - * Returns the objectId from an objectUri. It strips the file extension |
|
505 | - * if it matches the passed one. |
|
506 | - * |
|
507 | - * @param string $objectUri |
|
508 | - * @param string $extension |
|
509 | - * |
|
510 | - * @return string |
|
511 | - */ |
|
512 | - public function GetObjectIdFromObjectUri($objectUri, $extension) { |
|
513 | - if (!$extension) { |
|
514 | - return $objectUri; |
|
515 | - } |
|
516 | - $extLength = strlen($extension); |
|
517 | - if (substr($objectUri, -$extLength) === $extension) { |
|
518 | - return substr($objectUri, 0, -$extLength); |
|
519 | - } |
|
520 | - |
|
521 | - return $objectUri; |
|
522 | - } |
|
523 | - |
|
524 | - /** |
|
525 | - * Checks if the PHP-MAPI extension is available and in a requested version. |
|
526 | - * |
|
527 | - * @param string $version the version to be checked ("6.30.10-18495", parts or build number) |
|
528 | - * |
|
529 | - * @return bool installed version is superior to the checked string |
|
530 | - */ |
|
531 | - protected function checkMapiExtVersion($version = "") { |
|
532 | - if (!extension_loaded("mapi")) { |
|
533 | - return false; |
|
534 | - } |
|
535 | - // compare build number if requested |
|
536 | - if (preg_match('/^\d+$/', $version) && strlen($version) > 3) { |
|
537 | - $vs = preg_split('/-/', phpversion("mapi")); |
|
538 | - |
|
539 | - return $version <= $vs[1]; |
|
540 | - } |
|
541 | - if (version_compare(phpversion("mapi"), $version) == -1) { |
|
542 | - return false; |
|
543 | - } |
|
544 | - |
|
545 | - return true; |
|
546 | - } |
|
547 | - |
|
548 | - /** |
|
549 | - * Get named (custom) properties. Currently only PROP_APPTTSREF. |
|
550 | - * |
|
551 | - * @param string $id the folder id |
|
552 | - * |
|
553 | - * @return mixed |
|
554 | - */ |
|
555 | - protected function GetCustomProperties($id) { |
|
556 | - if (!isset($this->customprops[$id])) { |
|
557 | - $this->logger->trace("Fetching properties id:%s", $id); |
|
558 | - $store = $this->GetStoreById($id); |
|
559 | - $properties = getPropIdsFromStrings($store, ["goid" => "PT_BINARY:PSETID_Meeting:0x3", "vcarduid" => MapiProps::PROP_VCARDUID]); |
|
560 | - $this->customprops[$id] = $properties; |
|
561 | - } |
|
562 | - |
|
563 | - return $this->customprops[$id]; |
|
564 | - } |
|
565 | - |
|
566 | - /** |
|
567 | - * Create a MAPI restriction to use in the calendar which will |
|
568 | - * return future calendar items (until $end), plus those since $start. |
|
569 | - * Origins: Z-Push. |
|
570 | - * |
|
571 | - * @param MAPIStore $store the MAPI store |
|
572 | - * @param long $start Timestamp since when to include messages |
|
573 | - * @param long $end Ending timestamp |
|
574 | - * |
|
575 | - * @return array |
|
576 | - */ |
|
577 | - // TODO getting named properties |
|
578 | - public function GetCalendarRestriction($store, $start, $end) { |
|
579 | - $props = MapiProps::GetAppointmentProperties(); |
|
580 | - $props = getPropIdsFromStrings($store, $props); |
|
581 | - |
|
582 | - return [RES_OR, |
|
583 | - [ |
|
584 | - // OR |
|
585 | - // item.end > window.start && item.start < window.end |
|
586 | - [RES_AND, |
|
587 | - [ |
|
588 | - [RES_PROPERTY, |
|
589 | - [RELOP => RELOP_LE, |
|
590 | - ULPROPTAG => $props["starttime"], |
|
591 | - VALUE => $end, |
|
592 | - ], |
|
593 | - ], |
|
594 | - [RES_PROPERTY, |
|
595 | - [RELOP => RELOP_GE, |
|
596 | - ULPROPTAG => $props["endtime"], |
|
597 | - VALUE => $start, |
|
598 | - ], |
|
599 | - ], |
|
600 | - ], |
|
601 | - ], |
|
602 | - // OR |
|
603 | - [RES_OR, |
|
604 | - [ |
|
605 | - // OR |
|
606 | - // (EXIST(recurrence_enddate_property) && item[isRecurring] == true && recurrence_enddate_property >= start) |
|
607 | - [RES_AND, |
|
608 | - [ |
|
609 | - [RES_EXIST, |
|
610 | - [ULPROPTAG => $props["recurrenceend"], |
|
611 | - ], |
|
612 | - ], |
|
613 | - [RES_PROPERTY, |
|
614 | - [RELOP => RELOP_EQ, |
|
615 | - ULPROPTAG => $props["isrecurring"], |
|
616 | - VALUE => true, |
|
617 | - ], |
|
618 | - ], |
|
619 | - [RES_PROPERTY, |
|
620 | - [RELOP => RELOP_GE, |
|
621 | - ULPROPTAG => $props["recurrenceend"], |
|
622 | - VALUE => $start, |
|
623 | - ], |
|
624 | - ], |
|
625 | - ], |
|
626 | - ], |
|
627 | - // OR |
|
628 | - // (!EXIST(recurrence_enddate_property) && item[isRecurring] == true && item[start] <= end) |
|
629 | - [RES_AND, |
|
630 | - [ |
|
631 | - [RES_NOT, |
|
632 | - [ |
|
633 | - [RES_EXIST, |
|
634 | - [ULPROPTAG => $props["recurrenceend"], |
|
635 | - ], |
|
636 | - ], |
|
637 | - ], |
|
638 | - ], |
|
639 | - [RES_PROPERTY, |
|
640 | - [RELOP => RELOP_LE, |
|
641 | - ULPROPTAG => $props["starttime"], |
|
642 | - VALUE => $end, |
|
643 | - ], |
|
644 | - ], |
|
645 | - [RES_PROPERTY, |
|
646 | - [RELOP => RELOP_EQ, |
|
647 | - ULPROPTAG => $props["isrecurring"], |
|
648 | - VALUE => true, |
|
649 | - ], |
|
650 | - ], |
|
651 | - ], |
|
652 | - ], |
|
653 | - ], |
|
654 | - ], // EXISTS OR |
|
655 | - ], |
|
656 | - ]; // global OR |
|
657 | - } |
|
658 | - |
|
659 | - /** |
|
660 | - * Performs ICS based sync used from getChangesForAddressBook |
|
661 | - * / getChangesForCalendar. |
|
662 | - * |
|
663 | - * @param string $folderId |
|
664 | - * @param string $syncToken |
|
665 | - * @param string $fileExtension |
|
666 | - * @param int $limit |
|
667 | - * |
|
668 | - * @return array |
|
669 | - */ |
|
670 | - public function Sync($folderId, $syncToken, $fileExtension, $limit = null) { |
|
671 | - $arr = explode(':', $folderId); |
|
672 | - $phpwrapper = new PHPWrapper($this->GetStoreById($folderId), $this->logger, $this->GetCustomProperties($folderId), $fileExtension, $this->syncstate, $arr[1]); |
|
673 | - $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper); |
|
674 | - |
|
675 | - $mapifolder = $this->GetMapiFolder($folderId); |
|
676 | - $exporter = mapi_openproperty($mapifolder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
|
677 | - if (!$exporter) { |
|
678 | - $this->logger->error("Unable to get exporter"); |
|
679 | - |
|
680 | - return null; |
|
681 | - } |
|
682 | - |
|
683 | - $stream = mapi_stream_create(); |
|
684 | - if ($syncToken == null) { |
|
685 | - mapi_stream_write($stream, hex2bin("0000000000000000")); |
|
686 | - } |
|
687 | - else { |
|
688 | - $value = $this->syncstate->getState($arr[1], $syncToken); |
|
689 | - if ($value == null) { |
|
690 | - $this->logger->error("Unable to get value from token: %s - folderId: %s", $syncToken, $folderId); |
|
691 | - |
|
692 | - return null; |
|
693 | - } |
|
694 | - mapi_stream_write($stream, hex2bin($value)); |
|
695 | - } |
|
696 | - |
|
697 | - // The last parameter in mapi_exportchanges_config is buffer size for mapi_exportchanges_synchronize - how many |
|
698 | - // changes will be processed in its call. Setting it to MAX_SYNC_ITEMS won't export more items than is set in |
|
699 | - // the config. If there are more changes than MAX_SYNC_ITEMS the client will eventually catch up and sync |
|
700 | - // the rest on the subsequent sync request(s). |
|
701 | - $bufferSize = ($limit !== null && $limit > 0) ? $limit : MAX_SYNC_ITEMS; |
|
702 | - mapi_exportchanges_config($exporter, $stream, SYNC_NORMAL | SYNC_UNICODE, $mapiimporter, null, false, false, $bufferSize); |
|
703 | - $changesCount = mapi_exportchanges_getchangecount($exporter); |
|
704 | - $this->logger->debug("Exporter found %d changes, buffer size for mapi_exportchanges_synchronize %d", $changesCount, $bufferSize); |
|
705 | - while ((is_array(mapi_exportchanges_synchronize($exporter)))) { |
|
706 | - if ($changesCount > $bufferSize) { |
|
707 | - $this->logger->info("There were too many changes to be exported in this request. Total changes %d, exported %d.", $changesCount, $phpwrapper->Total()); |
|
708 | - |
|
709 | - break; |
|
710 | - } |
|
711 | - } |
|
712 | - $exportedChanges = $phpwrapper->Total(); |
|
713 | - $this->logger->debug("Exported %d changes, pending %d", $exportedChanges, $changesCount - $exportedChanges); |
|
714 | - |
|
715 | - mapi_exportchanges_updatestate($exporter, $stream); |
|
716 | - mapi_stream_seek($stream, 0, STREAM_SEEK_SET); |
|
717 | - $state = ""; |
|
718 | - while (true) { |
|
719 | - $data = mapi_stream_read($stream, 4096); |
|
720 | - if (strlen($data) > 0) { |
|
721 | - $state .= $data; |
|
722 | - } |
|
723 | - else { |
|
724 | - break; |
|
725 | - } |
|
726 | - } |
|
727 | - |
|
728 | - $newtoken = ($phpwrapper->Total() > 0) ? uniqid() : $syncToken; |
|
729 | - |
|
730 | - $this->syncstate->setState($arr[1], $newtoken, bin2hex($state)); |
|
731 | - |
|
732 | - $result = [ |
|
733 | - "syncToken" => $newtoken, |
|
734 | - "added" => $phpwrapper->GetAdded(), |
|
735 | - "modified" => $phpwrapper->GetModified(), |
|
736 | - "deleted" => $phpwrapper->GetDeleted(), |
|
737 | - ]; |
|
738 | - |
|
739 | - $this->logger->trace("Returning %s", $result); |
|
740 | - |
|
741 | - return $result; |
|
742 | - } |
|
441 | + $entryid = false; |
|
442 | + $restriction = false; |
|
443 | + |
|
444 | + if (ctype_xdigit($id) && strlen($id) % 2 == 0) { |
|
445 | + $this->logger->trace("Try PR_SOURCE_KEY %s", $id); |
|
446 | + $arr = explode(':', $folderId); |
|
447 | + $entryid = mapi_msgstore_entryidfromsourcekey($this->GetStoreById($arr[0]), hex2bin($arr[1]), hex2bin($id)); |
|
448 | + } |
|
449 | + |
|
450 | + if (!$entryid) { |
|
451 | + $this->logger->trace("Entryid not found. Try goid/vcarduid %s", $id); |
|
452 | + |
|
453 | + $properties = $this->GetCustomProperties($folderId); |
|
454 | + if (strpos($id, '%40') !== false) { |
|
455 | + $this->logger->debug("The id contains '%40'. Use urldecode."); |
|
456 | + $id = urldecode($id); |
|
457 | + } |
|
458 | + $restriction = []; |
|
459 | + |
|
460 | + if ($extension) { |
|
461 | + if ($extension == GrommunioCalDavBackend::FILE_EXTENSION) { |
|
462 | + $this->logger->trace("Try goid %s", $id); |
|
463 | + $goid = getGoidFromUid($id); |
|
464 | + $this->logger->trace("Try goid 0x%08X => %s", $properties["goid"], bin2hex($goid)); |
|
465 | + $restriction[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["goid"], VALUE => $goid]]; |
|
466 | + } |
|
467 | + elseif ($extension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
468 | + $this->logger->trace("Try vcarduid %s", $id); |
|
469 | + $restriction[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["vcarduid"], VALUE => $id]]; |
|
470 | + } |
|
471 | + } |
|
472 | + } |
|
473 | + |
|
474 | + // find the message if we have a restriction |
|
475 | + if ($restriction) { |
|
476 | + $table = mapi_folder_getcontentstable($mapifolder, MAPI_DEFERRED_ERRORS); |
|
477 | + mapi_table_restrict($table, [RES_OR, $restriction]); |
|
478 | + // Get requested properties, plus whatever we need |
|
479 | + $proplist = [PR_ENTRYID]; |
|
480 | + $rows = mapi_table_queryallrows($table, $proplist); |
|
481 | + if (count($rows) > 1) { |
|
482 | + $this->logger->warn("Found %d entries for id '%s' searching for message", count($rows), $id); |
|
483 | + } |
|
484 | + if (isset($rows[0], $rows[0][PR_ENTRYID])) { |
|
485 | + $entryid = $rows[0][PR_ENTRYID]; |
|
486 | + } |
|
487 | + } |
|
488 | + if ($entryid) { |
|
489 | + $mapimessage = mapi_msgstore_openentry($this->GetStoreById($folderId), $entryid); |
|
490 | + if (!$mapimessage) { |
|
491 | + $this->logger->warn("Error, unable to open entry id: 0x%X", $entryid, mapi_last_hresult()); |
|
492 | + |
|
493 | + return null; |
|
494 | + } |
|
495 | + |
|
496 | + return $mapimessage; |
|
497 | + } |
|
498 | + $this->logger->debug("Nothing found for %s", $id); |
|
499 | + |
|
500 | + return null; |
|
501 | + } |
|
502 | + |
|
503 | + /** |
|
504 | + * Returns the objectId from an objectUri. It strips the file extension |
|
505 | + * if it matches the passed one. |
|
506 | + * |
|
507 | + * @param string $objectUri |
|
508 | + * @param string $extension |
|
509 | + * |
|
510 | + * @return string |
|
511 | + */ |
|
512 | + public function GetObjectIdFromObjectUri($objectUri, $extension) { |
|
513 | + if (!$extension) { |
|
514 | + return $objectUri; |
|
515 | + } |
|
516 | + $extLength = strlen($extension); |
|
517 | + if (substr($objectUri, -$extLength) === $extension) { |
|
518 | + return substr($objectUri, 0, -$extLength); |
|
519 | + } |
|
520 | + |
|
521 | + return $objectUri; |
|
522 | + } |
|
523 | + |
|
524 | + /** |
|
525 | + * Checks if the PHP-MAPI extension is available and in a requested version. |
|
526 | + * |
|
527 | + * @param string $version the version to be checked ("6.30.10-18495", parts or build number) |
|
528 | + * |
|
529 | + * @return bool installed version is superior to the checked string |
|
530 | + */ |
|
531 | + protected function checkMapiExtVersion($version = "") { |
|
532 | + if (!extension_loaded("mapi")) { |
|
533 | + return false; |
|
534 | + } |
|
535 | + // compare build number if requested |
|
536 | + if (preg_match('/^\d+$/', $version) && strlen($version) > 3) { |
|
537 | + $vs = preg_split('/-/', phpversion("mapi")); |
|
538 | + |
|
539 | + return $version <= $vs[1]; |
|
540 | + } |
|
541 | + if (version_compare(phpversion("mapi"), $version) == -1) { |
|
542 | + return false; |
|
543 | + } |
|
544 | + |
|
545 | + return true; |
|
546 | + } |
|
547 | + |
|
548 | + /** |
|
549 | + * Get named (custom) properties. Currently only PROP_APPTTSREF. |
|
550 | + * |
|
551 | + * @param string $id the folder id |
|
552 | + * |
|
553 | + * @return mixed |
|
554 | + */ |
|
555 | + protected function GetCustomProperties($id) { |
|
556 | + if (!isset($this->customprops[$id])) { |
|
557 | + $this->logger->trace("Fetching properties id:%s", $id); |
|
558 | + $store = $this->GetStoreById($id); |
|
559 | + $properties = getPropIdsFromStrings($store, ["goid" => "PT_BINARY:PSETID_Meeting:0x3", "vcarduid" => MapiProps::PROP_VCARDUID]); |
|
560 | + $this->customprops[$id] = $properties; |
|
561 | + } |
|
562 | + |
|
563 | + return $this->customprops[$id]; |
|
564 | + } |
|
565 | + |
|
566 | + /** |
|
567 | + * Create a MAPI restriction to use in the calendar which will |
|
568 | + * return future calendar items (until $end), plus those since $start. |
|
569 | + * Origins: Z-Push. |
|
570 | + * |
|
571 | + * @param MAPIStore $store the MAPI store |
|
572 | + * @param long $start Timestamp since when to include messages |
|
573 | + * @param long $end Ending timestamp |
|
574 | + * |
|
575 | + * @return array |
|
576 | + */ |
|
577 | + // TODO getting named properties |
|
578 | + public function GetCalendarRestriction($store, $start, $end) { |
|
579 | + $props = MapiProps::GetAppointmentProperties(); |
|
580 | + $props = getPropIdsFromStrings($store, $props); |
|
581 | + |
|
582 | + return [RES_OR, |
|
583 | + [ |
|
584 | + // OR |
|
585 | + // item.end > window.start && item.start < window.end |
|
586 | + [RES_AND, |
|
587 | + [ |
|
588 | + [RES_PROPERTY, |
|
589 | + [RELOP => RELOP_LE, |
|
590 | + ULPROPTAG => $props["starttime"], |
|
591 | + VALUE => $end, |
|
592 | + ], |
|
593 | + ], |
|
594 | + [RES_PROPERTY, |
|
595 | + [RELOP => RELOP_GE, |
|
596 | + ULPROPTAG => $props["endtime"], |
|
597 | + VALUE => $start, |
|
598 | + ], |
|
599 | + ], |
|
600 | + ], |
|
601 | + ], |
|
602 | + // OR |
|
603 | + [RES_OR, |
|
604 | + [ |
|
605 | + // OR |
|
606 | + // (EXIST(recurrence_enddate_property) && item[isRecurring] == true && recurrence_enddate_property >= start) |
|
607 | + [RES_AND, |
|
608 | + [ |
|
609 | + [RES_EXIST, |
|
610 | + [ULPROPTAG => $props["recurrenceend"], |
|
611 | + ], |
|
612 | + ], |
|
613 | + [RES_PROPERTY, |
|
614 | + [RELOP => RELOP_EQ, |
|
615 | + ULPROPTAG => $props["isrecurring"], |
|
616 | + VALUE => true, |
|
617 | + ], |
|
618 | + ], |
|
619 | + [RES_PROPERTY, |
|
620 | + [RELOP => RELOP_GE, |
|
621 | + ULPROPTAG => $props["recurrenceend"], |
|
622 | + VALUE => $start, |
|
623 | + ], |
|
624 | + ], |
|
625 | + ], |
|
626 | + ], |
|
627 | + // OR |
|
628 | + // (!EXIST(recurrence_enddate_property) && item[isRecurring] == true && item[start] <= end) |
|
629 | + [RES_AND, |
|
630 | + [ |
|
631 | + [RES_NOT, |
|
632 | + [ |
|
633 | + [RES_EXIST, |
|
634 | + [ULPROPTAG => $props["recurrenceend"], |
|
635 | + ], |
|
636 | + ], |
|
637 | + ], |
|
638 | + ], |
|
639 | + [RES_PROPERTY, |
|
640 | + [RELOP => RELOP_LE, |
|
641 | + ULPROPTAG => $props["starttime"], |
|
642 | + VALUE => $end, |
|
643 | + ], |
|
644 | + ], |
|
645 | + [RES_PROPERTY, |
|
646 | + [RELOP => RELOP_EQ, |
|
647 | + ULPROPTAG => $props["isrecurring"], |
|
648 | + VALUE => true, |
|
649 | + ], |
|
650 | + ], |
|
651 | + ], |
|
652 | + ], |
|
653 | + ], |
|
654 | + ], // EXISTS OR |
|
655 | + ], |
|
656 | + ]; // global OR |
|
657 | + } |
|
658 | + |
|
659 | + /** |
|
660 | + * Performs ICS based sync used from getChangesForAddressBook |
|
661 | + * / getChangesForCalendar. |
|
662 | + * |
|
663 | + * @param string $folderId |
|
664 | + * @param string $syncToken |
|
665 | + * @param string $fileExtension |
|
666 | + * @param int $limit |
|
667 | + * |
|
668 | + * @return array |
|
669 | + */ |
|
670 | + public function Sync($folderId, $syncToken, $fileExtension, $limit = null) { |
|
671 | + $arr = explode(':', $folderId); |
|
672 | + $phpwrapper = new PHPWrapper($this->GetStoreById($folderId), $this->logger, $this->GetCustomProperties($folderId), $fileExtension, $this->syncstate, $arr[1]); |
|
673 | + $mapiimporter = mapi_wrap_importcontentschanges($phpwrapper); |
|
674 | + |
|
675 | + $mapifolder = $this->GetMapiFolder($folderId); |
|
676 | + $exporter = mapi_openproperty($mapifolder, PR_CONTENTS_SYNCHRONIZER, IID_IExchangeExportChanges, 0, 0); |
|
677 | + if (!$exporter) { |
|
678 | + $this->logger->error("Unable to get exporter"); |
|
679 | + |
|
680 | + return null; |
|
681 | + } |
|
682 | + |
|
683 | + $stream = mapi_stream_create(); |
|
684 | + if ($syncToken == null) { |
|
685 | + mapi_stream_write($stream, hex2bin("0000000000000000")); |
|
686 | + } |
|
687 | + else { |
|
688 | + $value = $this->syncstate->getState($arr[1], $syncToken); |
|
689 | + if ($value == null) { |
|
690 | + $this->logger->error("Unable to get value from token: %s - folderId: %s", $syncToken, $folderId); |
|
691 | + |
|
692 | + return null; |
|
693 | + } |
|
694 | + mapi_stream_write($stream, hex2bin($value)); |
|
695 | + } |
|
696 | + |
|
697 | + // The last parameter in mapi_exportchanges_config is buffer size for mapi_exportchanges_synchronize - how many |
|
698 | + // changes will be processed in its call. Setting it to MAX_SYNC_ITEMS won't export more items than is set in |
|
699 | + // the config. If there are more changes than MAX_SYNC_ITEMS the client will eventually catch up and sync |
|
700 | + // the rest on the subsequent sync request(s). |
|
701 | + $bufferSize = ($limit !== null && $limit > 0) ? $limit : MAX_SYNC_ITEMS; |
|
702 | + mapi_exportchanges_config($exporter, $stream, SYNC_NORMAL | SYNC_UNICODE, $mapiimporter, null, false, false, $bufferSize); |
|
703 | + $changesCount = mapi_exportchanges_getchangecount($exporter); |
|
704 | + $this->logger->debug("Exporter found %d changes, buffer size for mapi_exportchanges_synchronize %d", $changesCount, $bufferSize); |
|
705 | + while ((is_array(mapi_exportchanges_synchronize($exporter)))) { |
|
706 | + if ($changesCount > $bufferSize) { |
|
707 | + $this->logger->info("There were too many changes to be exported in this request. Total changes %d, exported %d.", $changesCount, $phpwrapper->Total()); |
|
708 | + |
|
709 | + break; |
|
710 | + } |
|
711 | + } |
|
712 | + $exportedChanges = $phpwrapper->Total(); |
|
713 | + $this->logger->debug("Exported %d changes, pending %d", $exportedChanges, $changesCount - $exportedChanges); |
|
714 | + |
|
715 | + mapi_exportchanges_updatestate($exporter, $stream); |
|
716 | + mapi_stream_seek($stream, 0, STREAM_SEEK_SET); |
|
717 | + $state = ""; |
|
718 | + while (true) { |
|
719 | + $data = mapi_stream_read($stream, 4096); |
|
720 | + if (strlen($data) > 0) { |
|
721 | + $state .= $data; |
|
722 | + } |
|
723 | + else { |
|
724 | + break; |
|
725 | + } |
|
726 | + } |
|
727 | + |
|
728 | + $newtoken = ($phpwrapper->Total() > 0) ? uniqid() : $syncToken; |
|
729 | + |
|
730 | + $this->syncstate->setState($arr[1], $newtoken, bin2hex($state)); |
|
731 | + |
|
732 | + $result = [ |
|
733 | + "syncToken" => $newtoken, |
|
734 | + "added" => $phpwrapper->GetAdded(), |
|
735 | + "modified" => $phpwrapper->GetModified(), |
|
736 | + "deleted" => $phpwrapper->GetDeleted(), |
|
737 | + ]; |
|
738 | + |
|
739 | + $this->logger->trace("Returning %s", $result); |
|
740 | + |
|
741 | + return $result; |
|
742 | + } |
|
743 | 743 | } |
@@ -653,7 +653,7 @@ |
||
653 | 653 | ], |
654 | 654 | ], // EXISTS OR |
655 | 655 | ], |
656 | - ]; // global OR |
|
656 | + ]; // global OR |
|
657 | 657 | } |
658 | 658 | |
659 | 659 | /** |
@@ -153,8 +153,7 @@ discard block |
||
153 | 153 | // before the tasks folder. |
154 | 154 | elseif (in_array('IPF.Appointment', $classes) && isset($rootprops[PR_IPM_APPOINTMENT_ENTRYID]) && $row[PR_ENTRYID] == $rootprops[PR_IPM_APPOINTMENT_ENTRYID]) { |
155 | 155 | array_unshift($folders, $folder); |
156 | - } |
|
157 | - else { |
|
156 | + } else { |
|
158 | 157 | array_push($folders, $folder); |
159 | 158 | } |
160 | 159 | } |
@@ -224,8 +223,7 @@ discard block |
||
224 | 223 | |
225 | 224 | if ($fileExtension == GrommunioCalDavBackend::FILE_EXTENSION) { |
226 | 225 | $result['calendarid'] = $id; |
227 | - } |
|
228 | - elseif ($fileExtension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
226 | + } elseif ($fileExtension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
229 | 227 | $result['addressbookid'] = $id; |
230 | 228 | } |
231 | 229 | $results[] = $result; |
@@ -329,8 +327,7 @@ discard block |
||
329 | 327 | public function GetStore($storename) { |
330 | 328 | if ($storename == null) { |
331 | 329 | $storename = $this->GetUser(); |
332 | - } |
|
333 | - else { |
|
330 | + } else { |
|
334 | 331 | $storename = str_replace('principals/', '', $storename); |
335 | 332 | } |
336 | 333 | $this->logger->trace("storename %s", $storename); |
@@ -463,8 +460,7 @@ discard block |
||
463 | 460 | $goid = getGoidFromUid($id); |
464 | 461 | $this->logger->trace("Try goid 0x%08X => %s", $properties["goid"], bin2hex($goid)); |
465 | 462 | $restriction[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["goid"], VALUE => $goid]]; |
466 | - } |
|
467 | - elseif ($extension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
463 | + } elseif ($extension == GrommunioCardDavBackend::FILE_EXTENSION) { |
|
468 | 464 | $this->logger->trace("Try vcarduid %s", $id); |
469 | 465 | $restriction[] = [RES_PROPERTY, [RELOP => RELOP_EQ, ULPROPTAG => $properties["vcarduid"], VALUE => $id]]; |
470 | 466 | } |
@@ -683,8 +679,7 @@ discard block |
||
683 | 679 | $stream = mapi_stream_create(); |
684 | 680 | if ($syncToken == null) { |
685 | 681 | mapi_stream_write($stream, hex2bin("0000000000000000")); |
686 | - } |
|
687 | - else { |
|
682 | + } else { |
|
688 | 683 | $value = $this->syncstate->getState($arr[1], $syncToken); |
689 | 684 | if ($value == null) { |
690 | 685 | $this->logger->error("Unable to get value from token: %s - folderId: %s", $syncToken, $folderId); |
@@ -719,8 +714,7 @@ discard block |
||
719 | 714 | $data = mapi_stream_read($stream, 4096); |
720 | 715 | if (strlen($data) > 0) { |
721 | 716 | $state .= $data; |
722 | - } |
|
723 | - else { |
|
717 | + } else { |
|
724 | 718 | break; |
725 | 719 | } |
726 | 720 | } |
@@ -10,7 +10,7 @@ discard block |
||
10 | 10 | namespace grommunio\DAV; |
11 | 11 | |
12 | 12 | class GrommunioCalDavBackend extends \Sabre\CalDAV\Backend\AbstractBackend implements \Sabre\CalDAV\Backend\SchedulingSupport, \Sabre\CalDAV\Backend\SyncSupport { |
13 | - /* |
|
13 | + /* |
|
14 | 14 | * TODO IMPLEMENT |
15 | 15 | * |
16 | 16 | * SubscriptionSupport, |
@@ -18,584 +18,584 @@ discard block |
||
18 | 18 | * |
19 | 19 | */ |
20 | 20 | |
21 | - private $logger; |
|
22 | - protected $gDavBackend; |
|
23 | - |
|
24 | - public const FILE_EXTENSION = '.ics'; |
|
25 | - public const CONTAINER_CLASS = 'IPF.Appointment'; |
|
26 | - public const CONTAINER_CLASSES = ['IPF.Appointment', 'IPF.Task']; |
|
27 | - |
|
28 | - /** |
|
29 | - * Constructor. |
|
30 | - */ |
|
31 | - public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) { |
|
32 | - $this->gDavBackend = $gDavBackend; |
|
33 | - $this->logger = $glogger; |
|
34 | - } |
|
35 | - |
|
36 | - /** |
|
37 | - * Publish free/busy information. |
|
38 | - * |
|
39 | - * Uses the FreeBusyPublish class to publish the information |
|
40 | - * about free/busy status. |
|
41 | - * |
|
42 | - * @param string $calendarId |
|
43 | - * @param mapiresource $calendar |
|
44 | - */ |
|
45 | - private function UpdateFB($calendarId, $calendar) { |
|
46 | - $session = $this->gDavBackend->GetSession(); |
|
47 | - $store = $this->gDavBackend->GetStoreById($calendarId); |
|
48 | - $weekUnixTime = 7 * 24 * 60 * 60; |
|
49 | - $start = time() - $weekUnixTime; |
|
50 | - $range = strtotime("+7 weeks"); |
|
51 | - $storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
|
52 | - if (!isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) { |
|
53 | - return; |
|
54 | - } |
|
55 | - $pub = new \FreeBusyPublish($session, $store, $calendar, $storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
|
56 | - $pub->publishFB($start, $range); |
|
57 | - } |
|
58 | - |
|
59 | - /** |
|
60 | - * Returns a list of calendars for a principal. |
|
61 | - * |
|
62 | - * Every project is an array with the following keys: |
|
63 | - * * id, a unique id that will be used by other functions to modify the |
|
64 | - * calendar. This can be the same as the uri or a database key. |
|
65 | - * * uri. This is just the 'base uri' or 'filename' of the calendar. |
|
66 | - * * principaluri. The owner of the calendar. Almost always the same as |
|
67 | - * principalUri passed to this method. |
|
68 | - * |
|
69 | - * Furthermore it can contain webdav properties in clark notation. A very |
|
70 | - * common one is '{DAV:}displayname'. |
|
71 | - * |
|
72 | - * Many clients also require: |
|
73 | - * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set |
|
74 | - * For this property, you can just return an instance of |
|
75 | - * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. |
|
76 | - * |
|
77 | - * If you return {http://sabredav.org/ns}read-only and set the value to 1, |
|
78 | - * ACL will automatically be put in read-only mode. |
|
79 | - * |
|
80 | - * @param string $principalUri |
|
81 | - * |
|
82 | - * @return array |
|
83 | - */ |
|
84 | - public function getCalendarsForUser($principalUri) { |
|
85 | - $this->logger->trace("principalUri: %s", $principalUri); |
|
86 | - |
|
87 | - return $this->gDavBackend->GetFolders($principalUri, static::CONTAINER_CLASSES); |
|
88 | - } |
|
89 | - |
|
90 | - /** |
|
91 | - * Creates a new calendar for a principal. |
|
92 | - * |
|
93 | - * If the creation was a success, an id must be returned that can be used |
|
94 | - * to reference this calendar in other methods, such as updateCalendar. |
|
95 | - * |
|
96 | - * @param string $principalUri |
|
97 | - * @param string $calendarUri |
|
98 | - * |
|
99 | - * @return string |
|
100 | - */ |
|
101 | - public function createCalendar($principalUri, $calendarUri, array $properties) { |
|
102 | - $this->logger->trace("principalUri: %s - calendarUri: %s - properties: %s", $principalUri, $calendarUri, $properties); |
|
103 | - // TODO Add displayname |
|
104 | - return $this->gDavBackend->CreateFolder($principalUri, $calendarUri, static::CONTAINER_CLASS, ""); |
|
105 | - } |
|
106 | - |
|
107 | - /** |
|
108 | - * Delete a calendar and all its objects. |
|
109 | - * |
|
110 | - * @param string $calendarId |
|
111 | - */ |
|
112 | - public function deleteCalendar($calendarId) { |
|
113 | - $this->logger->trace("calendarId: %s", $calendarId); |
|
114 | - $success = $this->gDavBackend->DeleteFolder($calendarId); |
|
115 | - // TODO evaluate $success |
|
116 | - } |
|
117 | - |
|
118 | - /** |
|
119 | - * Returns all calendar objects within a calendar. |
|
120 | - * |
|
121 | - * Every item contains an array with the following keys: |
|
122 | - * * calendardata - The iCalendar-compatible calendar data |
|
123 | - * * uri - a unique key which will be used to construct the uri. This can |
|
124 | - * be any arbitrary string, but making sure it ends with '.ics' is a |
|
125 | - * good idea. This is only the basename, or filename, not the full |
|
126 | - * path. |
|
127 | - * * lastmodified - a timestamp of the last modification time |
|
128 | - * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: |
|
129 | - * ' "abcdef"') |
|
130 | - * * size - The size of the calendar objects, in bytes. |
|
131 | - * * component - optional, a string containing the type of object, such |
|
132 | - * as 'vevent' or 'vtodo'. If specified, this will be used to populate |
|
133 | - * the Content-Type header. |
|
134 | - * |
|
135 | - * Note that the etag is optional, but it's highly encouraged to return for |
|
136 | - * speed reasons. |
|
137 | - * |
|
138 | - * The calendardata is also optional. If it's not returned |
|
139 | - * 'getCalendarObject' will be called later, which *is* expected to return |
|
140 | - * calendardata. |
|
141 | - * |
|
142 | - * If neither etag or size are specified, the calendardata will be |
|
143 | - * used/fetched to determine these numbers. If both are specified the |
|
144 | - * amount of times this is needed is reduced by a great degree. |
|
145 | - * |
|
146 | - * @param string $calendarId |
|
147 | - * |
|
148 | - * @return array |
|
149 | - */ |
|
150 | - public function getCalendarObjects($calendarId) { |
|
151 | - $result = $this->gDavBackend->GetObjects($calendarId, static::FILE_EXTENSION); |
|
152 | - $this->logger->trace("calendarId: %s found %d objects", $calendarId, count($result)); |
|
153 | - |
|
154 | - return $result; |
|
155 | - } |
|
156 | - |
|
157 | - /** |
|
158 | - * Performs a calendar-query on the contents of this calendar. |
|
159 | - * |
|
160 | - * The calendar-query is defined in RFC4791 : CalDAV. Using the |
|
161 | - * calendar-query it is possible for a client to request a specific set of |
|
162 | - * object, based on contents of iCalendar properties, date-ranges and |
|
163 | - * iCalendar component types (VTODO, VEVENT). |
|
164 | - * |
|
165 | - * This method should just return a list of (relative) urls that match this |
|
166 | - * query. |
|
167 | - * |
|
168 | - * The list of filters are specified as an array. The exact array is |
|
169 | - * documented by \Sabre\CalDAV\CalendarQueryParser. |
|
170 | - * |
|
171 | - * Note that it is extremely likely that getCalendarObject for every path |
|
172 | - * returned from this method will be called almost immediately after. You |
|
173 | - * may want to anticipate this to speed up these requests. |
|
174 | - * |
|
175 | - * This method provides a default implementation, which parses *all* the |
|
176 | - * iCalendar objects in the specified calendar. |
|
177 | - * |
|
178 | - * This default may well be good enough for personal use, and calendars |
|
179 | - * that aren't very large. But if you anticipate high usage, big calendars |
|
180 | - * or high loads, you are strongly advised to optimize certain paths. |
|
181 | - * |
|
182 | - * The best way to do so is override this method and to optimize |
|
183 | - * specifically for 'common filters'. |
|
184 | - * |
|
185 | - * Requests that are extremely common are: |
|
186 | - * * requests for just VEVENTS |
|
187 | - * * requests for just VTODO |
|
188 | - * * requests with a time-range-filter on either VEVENT or VTODO. |
|
189 | - * |
|
190 | - * ..and combinations of these requests. It may not be worth it to try to |
|
191 | - * handle every possible situation and just rely on the (relatively |
|
192 | - * easy to use) CalendarQueryValidator to handle the rest. |
|
193 | - * |
|
194 | - * Note that especially time-range-filters may be difficult to parse. A |
|
195 | - * time-range filter specified on a VEVENT must for instance also handle |
|
196 | - * recurrence rules correctly. |
|
197 | - * A good example of how to interpret all these filters can also simply |
|
198 | - * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct |
|
199 | - * as possible, so it gives you a good idea on what type of stuff you need |
|
200 | - * to think of. |
|
201 | - * |
|
202 | - * @param mixed $calendarId |
|
203 | - * |
|
204 | - * @return array |
|
205 | - */ |
|
206 | - public function calendarQuery($calendarId, array $filters) { |
|
207 | - $start = $end = null; |
|
208 | - $types = []; |
|
209 | - foreach ($filters['comp-filters'] as $filter) { |
|
210 | - $this->logger->trace("got filter: %s", $filter); |
|
211 | - |
|
212 | - if ($filter['name'] == 'VEVENT') { |
|
213 | - $types[] = 'IPM.Appointment'; |
|
214 | - } |
|
215 | - elseif ($filter['name'] == 'VTODO') { |
|
216 | - $types[] = 'IPM.Task'; |
|
217 | - } |
|
218 | - |
|
219 | - /* will this work on tasks? */ |
|
220 | - if (is_array($filter['time-range']) && isset($filter['time-range']['start'], $filter['time-range']['end'])) { |
|
221 | - $start = $filter['time-range']['start']->getTimestamp(); |
|
222 | - $end = $filter['time-range']['end']->getTimestamp(); |
|
223 | - } |
|
224 | - } |
|
225 | - |
|
226 | - $objfilters = []; |
|
227 | - if ($start != null && $end != null) { |
|
228 | - $objfilters["start"] = $start; |
|
229 | - $objfilters["end"] = $end; |
|
230 | - } |
|
231 | - if (!empty($types)) { |
|
232 | - $objfilters["types"] = $types; |
|
233 | - } |
|
234 | - |
|
235 | - $objects = $this->gDavBackend->GetObjects($calendarId, static::FILE_EXTENSION, $objfilters); |
|
236 | - $result = []; |
|
237 | - foreach ($objects as $object) { |
|
238 | - $result[] = $object['uri']; |
|
239 | - } |
|
240 | - |
|
241 | - return $result; |
|
242 | - } |
|
243 | - |
|
244 | - /** |
|
245 | - * Returns information from a single calendar object, based on its object uri. |
|
246 | - * |
|
247 | - * The object uri is only the basename, or filename and not a full path. |
|
248 | - * |
|
249 | - * The returned array must have the same keys as getCalendarObjects. The |
|
250 | - * 'calendardata' object is required here though, while it's not required |
|
251 | - * for getCalendarObjects. |
|
252 | - * |
|
253 | - * This method must return null if the object did not exist. |
|
254 | - * |
|
255 | - * @param string $calendarId |
|
256 | - * @param string $objectUri |
|
257 | - * @param resource $mapifolder optional mapifolder resource, used if available |
|
258 | - * |
|
259 | - * @return null|array |
|
260 | - */ |
|
261 | - public function getCalendarObject($calendarId, $objectUri, $mapifolder = null) { |
|
262 | - $this->logger->trace("calendarId: %s - objectUri: %s - mapifolder: %s", $calendarId, $objectUri, $mapifolder); |
|
263 | - |
|
264 | - if (!$mapifolder) { |
|
265 | - $mapifolder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
266 | - } |
|
267 | - |
|
268 | - $mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, $mapifolder, static::FILE_EXTENSION); |
|
269 | - if (!$mapimessage) { |
|
270 | - $this->logger->error("Object NOT FOUND"); |
|
271 | - |
|
272 | - return null; |
|
273 | - } |
|
274 | - |
|
275 | - $realId = $this->gDavBackend->GetIdOfMapiMessage($calendarId, $mapimessage); |
|
276 | - |
|
277 | - // this should be cached or moved to gDavBackend |
|
278 | - $session = $this->gDavBackend->GetSession(); |
|
279 | - $ab = $this->gDavBackend->GetAddressBook(); |
|
280 | - |
|
281 | - $ics = mapi_mapitoical($session, $ab, $mapimessage, []); |
|
282 | - if (!$ics && mapi_last_hresult()) { |
|
283 | - $this->logger->error("Error generating ical, error code: 0x%08X", mapi_last_hresult()); |
|
284 | - $ics = null; |
|
285 | - } |
|
286 | - elseif (!$ics) { |
|
287 | - $this->logger->error("Error generating ical, unknown error"); |
|
288 | - $ics = null; |
|
289 | - } |
|
290 | - $this->logger->trace("ics generated by mapi_mapitoical: %s%s", PHP_EOL, $ics); |
|
291 | - |
|
292 | - $props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]); |
|
293 | - |
|
294 | - $r = [ |
|
295 | - 'id' => $realId, |
|
296 | - 'uri' => $realId . static::FILE_EXTENSION, |
|
297 | - 'etag' => '"' . $props[PR_LAST_MODIFICATION_TIME] . '"', |
|
298 | - 'lastmodified' => $props[PR_LAST_MODIFICATION_TIME], |
|
299 | - 'calendarid' => $calendarId, |
|
300 | - 'size' => strlen($ics), |
|
301 | - 'calendardata' => $ics, |
|
302 | - ]; |
|
303 | - $this->logger->trace("returned data id: %s - size: %d - etag: %s", $r['id'], $r['size'], $r['etag']); |
|
304 | - |
|
305 | - return $r; |
|
306 | - } |
|
307 | - |
|
308 | - /** |
|
309 | - * Creates a new calendar object. |
|
310 | - * |
|
311 | - * The object uri is only the basename, or filename and not a full path. |
|
312 | - * |
|
313 | - * It is possible return an etag from this function, which will be used in |
|
314 | - * the response to this PUT request. Note that the ETag must be surrounded |
|
315 | - * by double-quotes. |
|
316 | - * |
|
317 | - * However, you should only really return this ETag if you don't mangle the |
|
318 | - * calendar-data. If the result of a subsequent GET to this object is not |
|
319 | - * the exact same as this request body, you should omit the ETag. |
|
320 | - * |
|
321 | - * @param mixed $calendarId |
|
322 | - * @param string $objectUri |
|
323 | - * @param string $calendarData |
|
324 | - * |
|
325 | - * @return null|string |
|
326 | - */ |
|
327 | - public function createCalendarObject($calendarId, $objectUri, $calendarData) { |
|
328 | - $this->logger->trace("calendarId: %s - objectUri: %s - calendarData: %s", $calendarId, $objectUri, $calendarData); |
|
329 | - $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($objectUri, static::FILE_EXTENSION); |
|
330 | - $folder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
331 | - $mapimessage = $this->gDavBackend->CreateObject($calendarId, $folder, $objectId); |
|
332 | - $retval = $this->setData($calendarId, $mapimessage, $calendarData); |
|
333 | - if (!$retval) { |
|
334 | - return null; |
|
335 | - } |
|
336 | - // $this->UpdateFB($calendarId, $folder); |
|
337 | - return '"' . $retval . '"'; |
|
338 | - } |
|
339 | - |
|
340 | - /** |
|
341 | - * Updates an existing calendarobject, based on its uri. |
|
342 | - * |
|
343 | - * The object uri is only the basename, or filename and not a full path. |
|
344 | - * |
|
345 | - * It is possible return an etag from this function, which will be used in |
|
346 | - * the response to this PUT request. Note that the ETag must be surrounded |
|
347 | - * by double-quotes. |
|
348 | - * |
|
349 | - * However, you should only really return this ETag if you don't mangle the |
|
350 | - * calendar-data. If the result of a subsequent GET to this object is not |
|
351 | - * the exact same as this request body, you should omit the ETag. |
|
352 | - * |
|
353 | - * @param mixed $calendarId |
|
354 | - * @param string $objectUri |
|
355 | - * @param string $calendarData |
|
356 | - * |
|
357 | - * @return null|string |
|
358 | - */ |
|
359 | - public function updateCalendarObject($calendarId, $objectUri, $calendarData) { |
|
360 | - $this->logger->trace("calendarId: %s - objectUri: %s - calendarData: %s", $calendarId, $objectUri, $calendarData); |
|
361 | - |
|
362 | - $folder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
363 | - $mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, null, static::FILE_EXTENSION); |
|
364 | - $retval = $this->setData($calendarId, $mapimessage, $calendarData); |
|
365 | - if (!$retval) { |
|
366 | - return null; |
|
367 | - } |
|
368 | - // $this->UpdateFB($calendarId, $folder); |
|
369 | - return '"' . $retval . '"'; |
|
370 | - } |
|
371 | - |
|
372 | - /** |
|
373 | - * Sets data for a calendar item. |
|
374 | - * |
|
375 | - * @param mixed $calendarId |
|
376 | - * @param MAPIMessage $mapimessage |
|
377 | - * @param string $ics |
|
378 | - * |
|
379 | - * @return null|string |
|
380 | - */ |
|
381 | - private function setData($calendarId, $mapimessage, $ics) { |
|
382 | - $this->logger->trace("mapimessage: %s - ics: %s", $mapimessage, $ics); |
|
383 | - // this should be cached or moved to gDavBackend |
|
384 | - $store = $this->gDavBackend->GetStoreById($calendarId); |
|
385 | - $session = $this->gDavBackend->GetSession(); |
|
386 | - $ab = $this->gDavBackend->GetAddressBook(); |
|
387 | - |
|
388 | - // Evolution sends daylight/standard information in the ical data |
|
389 | - // and some values are not supported by Outlook/Exchange. |
|
390 | - // Strip that data and leave only the last occurrences of |
|
391 | - // daylight/standard information. |
|
392 | - // @see GRAM-52 |
|
393 | - |
|
394 | - $xLicLocation = stripos($ics, 'X-LIC-LOCATION:'); |
|
395 | - if (($xLicLocation !== false) && |
|
396 | - ( |
|
397 | - substr_count($ics, 'BEGIN:DAYLIGHT', $xLicLocation) > 0 || |
|
398 | - substr_count($ics, 'BEGIN:STANDARD', $xLicLocation) > 0 |
|
399 | - )) { |
|
400 | - $firstDaytime = stripos($ics, 'BEGIN:DAYLIGHT', $xLicLocation); |
|
401 | - $firstStandard = stripos($ics, 'BEGIN:STANDARD', $xLicLocation); |
|
402 | - |
|
403 | - $lastDaytime = strripos($ics, 'BEGIN:DAYLIGHT', $xLicLocation); |
|
404 | - $lastStandard = strripos($ics, 'BEGIN:STANDARD', $xLicLocation); |
|
405 | - |
|
406 | - // the first part of ics until the first piece of standard/daytime information |
|
407 | - $cutStart = $firstDaytime < $firstStandard ? $firstDaytime : $firstStandard; |
|
408 | - |
|
409 | - if ($lastDaytime > $lastStandard) { |
|
410 | - // the part of the ics with the last piece of standard/daytime information |
|
411 | - $cutEnd = $lastDaytime; |
|
412 | - |
|
413 | - // the positions of the last piece of standard information |
|
414 | - $cut1 = $lastStandard; |
|
415 | - $cut2 = strripos($ics, 'END:STANDARD', $lastStandard) + 14; // strlen('END:STANDARD') |
|
416 | - } |
|
417 | - else { |
|
418 | - // the part of the ics with the last piece of standard/daytime information |
|
419 | - $cutEnd = $lastStandard; |
|
420 | - |
|
421 | - // the positions of the last piece of daylight information |
|
422 | - $cut1 = $lastDaytime; |
|
423 | - $cut2 = strripos($ics, 'END:DAYLIGHT', $lastDaytime) + 14; // strlen('END:DAYLIGHT') |
|
424 | - } |
|
425 | - |
|
426 | - $ics = substr($ics, 0, $cutStart) . substr($ics, $cut1, ($cut2 - $cut1)) . substr($ics, $cutEnd); |
|
427 | - $this->logger->trace("newics: %s", $ics); |
|
428 | - } |
|
429 | - |
|
430 | - $ok = mapi_icaltomapi($session, $store, $ab, $mapimessage, $ics, false); |
|
431 | - if (!$ok && mapi_last_hresult()) { |
|
432 | - $this->logger->error("Error updating mapi object, error code: 0x%08X", mapi_last_hresult()); |
|
433 | - |
|
434 | - return null; |
|
435 | - } |
|
436 | - if (!$ok) { |
|
437 | - $this->logger->error("Error updating mapi object, unknown error"); |
|
438 | - |
|
439 | - return null; |
|
440 | - } |
|
441 | - |
|
442 | - mapi_savechanges($mapimessage); |
|
443 | - $props = mapi_getprops($mapimessage); |
|
444 | - |
|
445 | - return $props[PR_LAST_MODIFICATION_TIME]; |
|
446 | - } |
|
447 | - |
|
448 | - /** |
|
449 | - * Deletes an existing calendar object. |
|
450 | - * |
|
451 | - * The object uri is only the basename, or filename and not a full path. |
|
452 | - * |
|
453 | - * @param string $calendarId |
|
454 | - * @param string $objectUri |
|
455 | - */ |
|
456 | - public function deleteCalendarObject($calendarId, $objectUri) { |
|
457 | - $this->logger->trace("calendarId: %s - objectUri: %s", $calendarId, $objectUri); |
|
458 | - |
|
459 | - $mapifolder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
460 | - |
|
461 | - // to delete we need the PR_ENTRYID of the message |
|
462 | - // TODO move this part to GrommunioDavBackend |
|
463 | - $mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, $mapifolder, static::FILE_EXTENSION); |
|
464 | - $props = mapi_getprops($mapimessage, [PR_ENTRYID]); |
|
465 | - mapi_folder_deletemessages($mapifolder, [$props[PR_ENTRYID]]); |
|
466 | - } |
|
467 | - |
|
468 | - /** |
|
469 | - * Return a single scheduling object. |
|
470 | - * |
|
471 | - * TODO: Add implementation. |
|
472 | - * |
|
473 | - * @param string $principalUri |
|
474 | - * @param string $objectUri |
|
475 | - * |
|
476 | - * @return array |
|
477 | - */ |
|
478 | - public function getSchedulingObject($principalUri, $objectUri) { |
|
479 | - $this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri); |
|
480 | - |
|
481 | - return []; |
|
482 | - } |
|
483 | - |
|
484 | - /** |
|
485 | - * Returns scheduling objects for the principal URI. |
|
486 | - * |
|
487 | - * TODO: Add implementation. |
|
488 | - * |
|
489 | - * @param string $principalUri |
|
490 | - * |
|
491 | - * @return array |
|
492 | - */ |
|
493 | - public function getSchedulingObjects($principalUri) { |
|
494 | - $this->logger->trace("principalUri: %s", $principalUri); |
|
495 | - |
|
496 | - return []; |
|
497 | - } |
|
498 | - |
|
499 | - /** |
|
500 | - * Delete scheduling object. |
|
501 | - * |
|
502 | - * TODO: Add implementation. |
|
503 | - * |
|
504 | - * @param string $principalUri |
|
505 | - * @param string $objectUri |
|
506 | - */ |
|
507 | - public function deleteSchedulingObject($principalUri, $objectUri) { |
|
508 | - $this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri); |
|
509 | - } |
|
510 | - |
|
511 | - /** |
|
512 | - * Create a new scheduling object. |
|
513 | - * |
|
514 | - * TODO: Add implementation. |
|
515 | - * |
|
516 | - * @param string $principalUri |
|
517 | - * @param string $objectUri |
|
518 | - * @param string $objectData |
|
519 | - */ |
|
520 | - public function createSchedulingObject($principalUri, $objectUri, $objectData) { |
|
521 | - $this->logger->trace("principalUri: %s - objectUri: %s - objectData: %s", $principalUri, $objectUri, $objectData); |
|
522 | - } |
|
523 | - |
|
524 | - /** |
|
525 | - * Return CTAG for scheduling inbox. |
|
526 | - * |
|
527 | - * TODO: Add implementation. |
|
528 | - * |
|
529 | - * @param string $principalUri |
|
530 | - * |
|
531 | - * @return string |
|
532 | - */ |
|
533 | - public function getSchedulingInboxCtag($principalUri) { |
|
534 | - $this->logger->trace("principalUri: %s", $principalUri); |
|
535 | - |
|
536 | - return "empty"; |
|
537 | - } |
|
538 | - |
|
539 | - /** |
|
540 | - * The getChanges method returns all the changes that have happened, since |
|
541 | - * the specified syncToken in the specified calendar. |
|
542 | - * |
|
543 | - * This function should return an array, such as the following: |
|
544 | - * |
|
545 | - * [ |
|
546 | - * 'syncToken' => 'The current synctoken', |
|
547 | - * 'added' => [ |
|
548 | - * 'new.txt', |
|
549 | - * ], |
|
550 | - * 'modified' => [ |
|
551 | - * 'modified.txt', |
|
552 | - * ], |
|
553 | - * 'deleted' => [ |
|
554 | - * 'foo.php.bak', |
|
555 | - * 'old.txt' |
|
556 | - * ] |
|
557 | - * ); |
|
558 | - * |
|
559 | - * The returned syncToken property should reflect the *current* syncToken |
|
560 | - * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
561 | - * property This is * needed here too, to ensure the operation is atomic. |
|
562 | - * |
|
563 | - * If the $syncToken argument is specified as null, this is an initial |
|
564 | - * sync, and all members should be reported. |
|
565 | - * |
|
566 | - * The modified property is an array of nodenames that have changed since |
|
567 | - * the last token. |
|
568 | - * |
|
569 | - * The deleted property is an array with nodenames, that have been deleted |
|
570 | - * from collection. |
|
571 | - * |
|
572 | - * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
573 | - * 1, you only have to report changes that happened only directly in |
|
574 | - * immediate descendants. If it's 2, it should also include changes from |
|
575 | - * the nodes below the child collections. (grandchildren) |
|
576 | - * |
|
577 | - * The $limit argument allows a client to specify how many results should |
|
578 | - * be returned at most. If the limit is not specified, it should be treated |
|
579 | - * as infinite. |
|
580 | - * |
|
581 | - * If the limit (infinite or not) is higher than you're willing to return, |
|
582 | - * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
583 | - * |
|
584 | - * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
585 | - * return null. |
|
586 | - * |
|
587 | - * The limit is 'suggestive'. You are free to ignore it. |
|
588 | - * |
|
589 | - * @param string $calendarId |
|
590 | - * @param string $syncToken |
|
591 | - * @param int $syncLevel |
|
592 | - * @param int $limit |
|
593 | - * |
|
594 | - * @return array |
|
595 | - */ |
|
596 | - public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { |
|
597 | - $this->logger->trace("calendarId: %s - syncToken: %s - syncLevel: %d - limit: %d", $calendarId, $syncToken, $syncLevel, $limit); |
|
598 | - |
|
599 | - return $this->gDavBackend->Sync($calendarId, $syncToken, static::FILE_EXTENSION, $limit); |
|
600 | - } |
|
21 | + private $logger; |
|
22 | + protected $gDavBackend; |
|
23 | + |
|
24 | + public const FILE_EXTENSION = '.ics'; |
|
25 | + public const CONTAINER_CLASS = 'IPF.Appointment'; |
|
26 | + public const CONTAINER_CLASSES = ['IPF.Appointment', 'IPF.Task']; |
|
27 | + |
|
28 | + /** |
|
29 | + * Constructor. |
|
30 | + */ |
|
31 | + public function __construct(GrommunioDavBackend $gDavBackend, GLogger $glogger) { |
|
32 | + $this->gDavBackend = $gDavBackend; |
|
33 | + $this->logger = $glogger; |
|
34 | + } |
|
35 | + |
|
36 | + /** |
|
37 | + * Publish free/busy information. |
|
38 | + * |
|
39 | + * Uses the FreeBusyPublish class to publish the information |
|
40 | + * about free/busy status. |
|
41 | + * |
|
42 | + * @param string $calendarId |
|
43 | + * @param mapiresource $calendar |
|
44 | + */ |
|
45 | + private function UpdateFB($calendarId, $calendar) { |
|
46 | + $session = $this->gDavBackend->GetSession(); |
|
47 | + $store = $this->gDavBackend->GetStoreById($calendarId); |
|
48 | + $weekUnixTime = 7 * 24 * 60 * 60; |
|
49 | + $start = time() - $weekUnixTime; |
|
50 | + $range = strtotime("+7 weeks"); |
|
51 | + $storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID]); |
|
52 | + if (!isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) { |
|
53 | + return; |
|
54 | + } |
|
55 | + $pub = new \FreeBusyPublish($session, $store, $calendar, $storeProps[PR_MAILBOX_OWNER_ENTRYID]); |
|
56 | + $pub->publishFB($start, $range); |
|
57 | + } |
|
58 | + |
|
59 | + /** |
|
60 | + * Returns a list of calendars for a principal. |
|
61 | + * |
|
62 | + * Every project is an array with the following keys: |
|
63 | + * * id, a unique id that will be used by other functions to modify the |
|
64 | + * calendar. This can be the same as the uri or a database key. |
|
65 | + * * uri. This is just the 'base uri' or 'filename' of the calendar. |
|
66 | + * * principaluri. The owner of the calendar. Almost always the same as |
|
67 | + * principalUri passed to this method. |
|
68 | + * |
|
69 | + * Furthermore it can contain webdav properties in clark notation. A very |
|
70 | + * common one is '{DAV:}displayname'. |
|
71 | + * |
|
72 | + * Many clients also require: |
|
73 | + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set |
|
74 | + * For this property, you can just return an instance of |
|
75 | + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. |
|
76 | + * |
|
77 | + * If you return {http://sabredav.org/ns}read-only and set the value to 1, |
|
78 | + * ACL will automatically be put in read-only mode. |
|
79 | + * |
|
80 | + * @param string $principalUri |
|
81 | + * |
|
82 | + * @return array |
|
83 | + */ |
|
84 | + public function getCalendarsForUser($principalUri) { |
|
85 | + $this->logger->trace("principalUri: %s", $principalUri); |
|
86 | + |
|
87 | + return $this->gDavBackend->GetFolders($principalUri, static::CONTAINER_CLASSES); |
|
88 | + } |
|
89 | + |
|
90 | + /** |
|
91 | + * Creates a new calendar for a principal. |
|
92 | + * |
|
93 | + * If the creation was a success, an id must be returned that can be used |
|
94 | + * to reference this calendar in other methods, such as updateCalendar. |
|
95 | + * |
|
96 | + * @param string $principalUri |
|
97 | + * @param string $calendarUri |
|
98 | + * |
|
99 | + * @return string |
|
100 | + */ |
|
101 | + public function createCalendar($principalUri, $calendarUri, array $properties) { |
|
102 | + $this->logger->trace("principalUri: %s - calendarUri: %s - properties: %s", $principalUri, $calendarUri, $properties); |
|
103 | + // TODO Add displayname |
|
104 | + return $this->gDavBackend->CreateFolder($principalUri, $calendarUri, static::CONTAINER_CLASS, ""); |
|
105 | + } |
|
106 | + |
|
107 | + /** |
|
108 | + * Delete a calendar and all its objects. |
|
109 | + * |
|
110 | + * @param string $calendarId |
|
111 | + */ |
|
112 | + public function deleteCalendar($calendarId) { |
|
113 | + $this->logger->trace("calendarId: %s", $calendarId); |
|
114 | + $success = $this->gDavBackend->DeleteFolder($calendarId); |
|
115 | + // TODO evaluate $success |
|
116 | + } |
|
117 | + |
|
118 | + /** |
|
119 | + * Returns all calendar objects within a calendar. |
|
120 | + * |
|
121 | + * Every item contains an array with the following keys: |
|
122 | + * * calendardata - The iCalendar-compatible calendar data |
|
123 | + * * uri - a unique key which will be used to construct the uri. This can |
|
124 | + * be any arbitrary string, but making sure it ends with '.ics' is a |
|
125 | + * good idea. This is only the basename, or filename, not the full |
|
126 | + * path. |
|
127 | + * * lastmodified - a timestamp of the last modification time |
|
128 | + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: |
|
129 | + * ' "abcdef"') |
|
130 | + * * size - The size of the calendar objects, in bytes. |
|
131 | + * * component - optional, a string containing the type of object, such |
|
132 | + * as 'vevent' or 'vtodo'. If specified, this will be used to populate |
|
133 | + * the Content-Type header. |
|
134 | + * |
|
135 | + * Note that the etag is optional, but it's highly encouraged to return for |
|
136 | + * speed reasons. |
|
137 | + * |
|
138 | + * The calendardata is also optional. If it's not returned |
|
139 | + * 'getCalendarObject' will be called later, which *is* expected to return |
|
140 | + * calendardata. |
|
141 | + * |
|
142 | + * If neither etag or size are specified, the calendardata will be |
|
143 | + * used/fetched to determine these numbers. If both are specified the |
|
144 | + * amount of times this is needed is reduced by a great degree. |
|
145 | + * |
|
146 | + * @param string $calendarId |
|
147 | + * |
|
148 | + * @return array |
|
149 | + */ |
|
150 | + public function getCalendarObjects($calendarId) { |
|
151 | + $result = $this->gDavBackend->GetObjects($calendarId, static::FILE_EXTENSION); |
|
152 | + $this->logger->trace("calendarId: %s found %d objects", $calendarId, count($result)); |
|
153 | + |
|
154 | + return $result; |
|
155 | + } |
|
156 | + |
|
157 | + /** |
|
158 | + * Performs a calendar-query on the contents of this calendar. |
|
159 | + * |
|
160 | + * The calendar-query is defined in RFC4791 : CalDAV. Using the |
|
161 | + * calendar-query it is possible for a client to request a specific set of |
|
162 | + * object, based on contents of iCalendar properties, date-ranges and |
|
163 | + * iCalendar component types (VTODO, VEVENT). |
|
164 | + * |
|
165 | + * This method should just return a list of (relative) urls that match this |
|
166 | + * query. |
|
167 | + * |
|
168 | + * The list of filters are specified as an array. The exact array is |
|
169 | + * documented by \Sabre\CalDAV\CalendarQueryParser. |
|
170 | + * |
|
171 | + * Note that it is extremely likely that getCalendarObject for every path |
|
172 | + * returned from this method will be called almost immediately after. You |
|
173 | + * may want to anticipate this to speed up these requests. |
|
174 | + * |
|
175 | + * This method provides a default implementation, which parses *all* the |
|
176 | + * iCalendar objects in the specified calendar. |
|
177 | + * |
|
178 | + * This default may well be good enough for personal use, and calendars |
|
179 | + * that aren't very large. But if you anticipate high usage, big calendars |
|
180 | + * or high loads, you are strongly advised to optimize certain paths. |
|
181 | + * |
|
182 | + * The best way to do so is override this method and to optimize |
|
183 | + * specifically for 'common filters'. |
|
184 | + * |
|
185 | + * Requests that are extremely common are: |
|
186 | + * * requests for just VEVENTS |
|
187 | + * * requests for just VTODO |
|
188 | + * * requests with a time-range-filter on either VEVENT or VTODO. |
|
189 | + * |
|
190 | + * ..and combinations of these requests. It may not be worth it to try to |
|
191 | + * handle every possible situation and just rely on the (relatively |
|
192 | + * easy to use) CalendarQueryValidator to handle the rest. |
|
193 | + * |
|
194 | + * Note that especially time-range-filters may be difficult to parse. A |
|
195 | + * time-range filter specified on a VEVENT must for instance also handle |
|
196 | + * recurrence rules correctly. |
|
197 | + * A good example of how to interpret all these filters can also simply |
|
198 | + * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct |
|
199 | + * as possible, so it gives you a good idea on what type of stuff you need |
|
200 | + * to think of. |
|
201 | + * |
|
202 | + * @param mixed $calendarId |
|
203 | + * |
|
204 | + * @return array |
|
205 | + */ |
|
206 | + public function calendarQuery($calendarId, array $filters) { |
|
207 | + $start = $end = null; |
|
208 | + $types = []; |
|
209 | + foreach ($filters['comp-filters'] as $filter) { |
|
210 | + $this->logger->trace("got filter: %s", $filter); |
|
211 | + |
|
212 | + if ($filter['name'] == 'VEVENT') { |
|
213 | + $types[] = 'IPM.Appointment'; |
|
214 | + } |
|
215 | + elseif ($filter['name'] == 'VTODO') { |
|
216 | + $types[] = 'IPM.Task'; |
|
217 | + } |
|
218 | + |
|
219 | + /* will this work on tasks? */ |
|
220 | + if (is_array($filter['time-range']) && isset($filter['time-range']['start'], $filter['time-range']['end'])) { |
|
221 | + $start = $filter['time-range']['start']->getTimestamp(); |
|
222 | + $end = $filter['time-range']['end']->getTimestamp(); |
|
223 | + } |
|
224 | + } |
|
225 | + |
|
226 | + $objfilters = []; |
|
227 | + if ($start != null && $end != null) { |
|
228 | + $objfilters["start"] = $start; |
|
229 | + $objfilters["end"] = $end; |
|
230 | + } |
|
231 | + if (!empty($types)) { |
|
232 | + $objfilters["types"] = $types; |
|
233 | + } |
|
234 | + |
|
235 | + $objects = $this->gDavBackend->GetObjects($calendarId, static::FILE_EXTENSION, $objfilters); |
|
236 | + $result = []; |
|
237 | + foreach ($objects as $object) { |
|
238 | + $result[] = $object['uri']; |
|
239 | + } |
|
240 | + |
|
241 | + return $result; |
|
242 | + } |
|
243 | + |
|
244 | + /** |
|
245 | + * Returns information from a single calendar object, based on its object uri. |
|
246 | + * |
|
247 | + * The object uri is only the basename, or filename and not a full path. |
|
248 | + * |
|
249 | + * The returned array must have the same keys as getCalendarObjects. The |
|
250 | + * 'calendardata' object is required here though, while it's not required |
|
251 | + * for getCalendarObjects. |
|
252 | + * |
|
253 | + * This method must return null if the object did not exist. |
|
254 | + * |
|
255 | + * @param string $calendarId |
|
256 | + * @param string $objectUri |
|
257 | + * @param resource $mapifolder optional mapifolder resource, used if available |
|
258 | + * |
|
259 | + * @return null|array |
|
260 | + */ |
|
261 | + public function getCalendarObject($calendarId, $objectUri, $mapifolder = null) { |
|
262 | + $this->logger->trace("calendarId: %s - objectUri: %s - mapifolder: %s", $calendarId, $objectUri, $mapifolder); |
|
263 | + |
|
264 | + if (!$mapifolder) { |
|
265 | + $mapifolder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
266 | + } |
|
267 | + |
|
268 | + $mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, $mapifolder, static::FILE_EXTENSION); |
|
269 | + if (!$mapimessage) { |
|
270 | + $this->logger->error("Object NOT FOUND"); |
|
271 | + |
|
272 | + return null; |
|
273 | + } |
|
274 | + |
|
275 | + $realId = $this->gDavBackend->GetIdOfMapiMessage($calendarId, $mapimessage); |
|
276 | + |
|
277 | + // this should be cached or moved to gDavBackend |
|
278 | + $session = $this->gDavBackend->GetSession(); |
|
279 | + $ab = $this->gDavBackend->GetAddressBook(); |
|
280 | + |
|
281 | + $ics = mapi_mapitoical($session, $ab, $mapimessage, []); |
|
282 | + if (!$ics && mapi_last_hresult()) { |
|
283 | + $this->logger->error("Error generating ical, error code: 0x%08X", mapi_last_hresult()); |
|
284 | + $ics = null; |
|
285 | + } |
|
286 | + elseif (!$ics) { |
|
287 | + $this->logger->error("Error generating ical, unknown error"); |
|
288 | + $ics = null; |
|
289 | + } |
|
290 | + $this->logger->trace("ics generated by mapi_mapitoical: %s%s", PHP_EOL, $ics); |
|
291 | + |
|
292 | + $props = mapi_getprops($mapimessage, [PR_LAST_MODIFICATION_TIME]); |
|
293 | + |
|
294 | + $r = [ |
|
295 | + 'id' => $realId, |
|
296 | + 'uri' => $realId . static::FILE_EXTENSION, |
|
297 | + 'etag' => '"' . $props[PR_LAST_MODIFICATION_TIME] . '"', |
|
298 | + 'lastmodified' => $props[PR_LAST_MODIFICATION_TIME], |
|
299 | + 'calendarid' => $calendarId, |
|
300 | + 'size' => strlen($ics), |
|
301 | + 'calendardata' => $ics, |
|
302 | + ]; |
|
303 | + $this->logger->trace("returned data id: %s - size: %d - etag: %s", $r['id'], $r['size'], $r['etag']); |
|
304 | + |
|
305 | + return $r; |
|
306 | + } |
|
307 | + |
|
308 | + /** |
|
309 | + * Creates a new calendar object. |
|
310 | + * |
|
311 | + * The object uri is only the basename, or filename and not a full path. |
|
312 | + * |
|
313 | + * It is possible return an etag from this function, which will be used in |
|
314 | + * the response to this PUT request. Note that the ETag must be surrounded |
|
315 | + * by double-quotes. |
|
316 | + * |
|
317 | + * However, you should only really return this ETag if you don't mangle the |
|
318 | + * calendar-data. If the result of a subsequent GET to this object is not |
|
319 | + * the exact same as this request body, you should omit the ETag. |
|
320 | + * |
|
321 | + * @param mixed $calendarId |
|
322 | + * @param string $objectUri |
|
323 | + * @param string $calendarData |
|
324 | + * |
|
325 | + * @return null|string |
|
326 | + */ |
|
327 | + public function createCalendarObject($calendarId, $objectUri, $calendarData) { |
|
328 | + $this->logger->trace("calendarId: %s - objectUri: %s - calendarData: %s", $calendarId, $objectUri, $calendarData); |
|
329 | + $objectId = $this->gDavBackend->GetObjectIdFromObjectUri($objectUri, static::FILE_EXTENSION); |
|
330 | + $folder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
331 | + $mapimessage = $this->gDavBackend->CreateObject($calendarId, $folder, $objectId); |
|
332 | + $retval = $this->setData($calendarId, $mapimessage, $calendarData); |
|
333 | + if (!$retval) { |
|
334 | + return null; |
|
335 | + } |
|
336 | + // $this->UpdateFB($calendarId, $folder); |
|
337 | + return '"' . $retval . '"'; |
|
338 | + } |
|
339 | + |
|
340 | + /** |
|
341 | + * Updates an existing calendarobject, based on its uri. |
|
342 | + * |
|
343 | + * The object uri is only the basename, or filename and not a full path. |
|
344 | + * |
|
345 | + * It is possible return an etag from this function, which will be used in |
|
346 | + * the response to this PUT request. Note that the ETag must be surrounded |
|
347 | + * by double-quotes. |
|
348 | + * |
|
349 | + * However, you should only really return this ETag if you don't mangle the |
|
350 | + * calendar-data. If the result of a subsequent GET to this object is not |
|
351 | + * the exact same as this request body, you should omit the ETag. |
|
352 | + * |
|
353 | + * @param mixed $calendarId |
|
354 | + * @param string $objectUri |
|
355 | + * @param string $calendarData |
|
356 | + * |
|
357 | + * @return null|string |
|
358 | + */ |
|
359 | + public function updateCalendarObject($calendarId, $objectUri, $calendarData) { |
|
360 | + $this->logger->trace("calendarId: %s - objectUri: %s - calendarData: %s", $calendarId, $objectUri, $calendarData); |
|
361 | + |
|
362 | + $folder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
363 | + $mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, null, static::FILE_EXTENSION); |
|
364 | + $retval = $this->setData($calendarId, $mapimessage, $calendarData); |
|
365 | + if (!$retval) { |
|
366 | + return null; |
|
367 | + } |
|
368 | + // $this->UpdateFB($calendarId, $folder); |
|
369 | + return '"' . $retval . '"'; |
|
370 | + } |
|
371 | + |
|
372 | + /** |
|
373 | + * Sets data for a calendar item. |
|
374 | + * |
|
375 | + * @param mixed $calendarId |
|
376 | + * @param MAPIMessage $mapimessage |
|
377 | + * @param string $ics |
|
378 | + * |
|
379 | + * @return null|string |
|
380 | + */ |
|
381 | + private function setData($calendarId, $mapimessage, $ics) { |
|
382 | + $this->logger->trace("mapimessage: %s - ics: %s", $mapimessage, $ics); |
|
383 | + // this should be cached or moved to gDavBackend |
|
384 | + $store = $this->gDavBackend->GetStoreById($calendarId); |
|
385 | + $session = $this->gDavBackend->GetSession(); |
|
386 | + $ab = $this->gDavBackend->GetAddressBook(); |
|
387 | + |
|
388 | + // Evolution sends daylight/standard information in the ical data |
|
389 | + // and some values are not supported by Outlook/Exchange. |
|
390 | + // Strip that data and leave only the last occurrences of |
|
391 | + // daylight/standard information. |
|
392 | + // @see GRAM-52 |
|
393 | + |
|
394 | + $xLicLocation = stripos($ics, 'X-LIC-LOCATION:'); |
|
395 | + if (($xLicLocation !== false) && |
|
396 | + ( |
|
397 | + substr_count($ics, 'BEGIN:DAYLIGHT', $xLicLocation) > 0 || |
|
398 | + substr_count($ics, 'BEGIN:STANDARD', $xLicLocation) > 0 |
|
399 | + )) { |
|
400 | + $firstDaytime = stripos($ics, 'BEGIN:DAYLIGHT', $xLicLocation); |
|
401 | + $firstStandard = stripos($ics, 'BEGIN:STANDARD', $xLicLocation); |
|
402 | + |
|
403 | + $lastDaytime = strripos($ics, 'BEGIN:DAYLIGHT', $xLicLocation); |
|
404 | + $lastStandard = strripos($ics, 'BEGIN:STANDARD', $xLicLocation); |
|
405 | + |
|
406 | + // the first part of ics until the first piece of standard/daytime information |
|
407 | + $cutStart = $firstDaytime < $firstStandard ? $firstDaytime : $firstStandard; |
|
408 | + |
|
409 | + if ($lastDaytime > $lastStandard) { |
|
410 | + // the part of the ics with the last piece of standard/daytime information |
|
411 | + $cutEnd = $lastDaytime; |
|
412 | + |
|
413 | + // the positions of the last piece of standard information |
|
414 | + $cut1 = $lastStandard; |
|
415 | + $cut2 = strripos($ics, 'END:STANDARD', $lastStandard) + 14; // strlen('END:STANDARD') |
|
416 | + } |
|
417 | + else { |
|
418 | + // the part of the ics with the last piece of standard/daytime information |
|
419 | + $cutEnd = $lastStandard; |
|
420 | + |
|
421 | + // the positions of the last piece of daylight information |
|
422 | + $cut1 = $lastDaytime; |
|
423 | + $cut2 = strripos($ics, 'END:DAYLIGHT', $lastDaytime) + 14; // strlen('END:DAYLIGHT') |
|
424 | + } |
|
425 | + |
|
426 | + $ics = substr($ics, 0, $cutStart) . substr($ics, $cut1, ($cut2 - $cut1)) . substr($ics, $cutEnd); |
|
427 | + $this->logger->trace("newics: %s", $ics); |
|
428 | + } |
|
429 | + |
|
430 | + $ok = mapi_icaltomapi($session, $store, $ab, $mapimessage, $ics, false); |
|
431 | + if (!$ok && mapi_last_hresult()) { |
|
432 | + $this->logger->error("Error updating mapi object, error code: 0x%08X", mapi_last_hresult()); |
|
433 | + |
|
434 | + return null; |
|
435 | + } |
|
436 | + if (!$ok) { |
|
437 | + $this->logger->error("Error updating mapi object, unknown error"); |
|
438 | + |
|
439 | + return null; |
|
440 | + } |
|
441 | + |
|
442 | + mapi_savechanges($mapimessage); |
|
443 | + $props = mapi_getprops($mapimessage); |
|
444 | + |
|
445 | + return $props[PR_LAST_MODIFICATION_TIME]; |
|
446 | + } |
|
447 | + |
|
448 | + /** |
|
449 | + * Deletes an existing calendar object. |
|
450 | + * |
|
451 | + * The object uri is only the basename, or filename and not a full path. |
|
452 | + * |
|
453 | + * @param string $calendarId |
|
454 | + * @param string $objectUri |
|
455 | + */ |
|
456 | + public function deleteCalendarObject($calendarId, $objectUri) { |
|
457 | + $this->logger->trace("calendarId: %s - objectUri: %s", $calendarId, $objectUri); |
|
458 | + |
|
459 | + $mapifolder = $this->gDavBackend->GetMapiFolder($calendarId); |
|
460 | + |
|
461 | + // to delete we need the PR_ENTRYID of the message |
|
462 | + // TODO move this part to GrommunioDavBackend |
|
463 | + $mapimessage = $this->gDavBackend->GetMapiMessageForId($calendarId, $objectUri, $mapifolder, static::FILE_EXTENSION); |
|
464 | + $props = mapi_getprops($mapimessage, [PR_ENTRYID]); |
|
465 | + mapi_folder_deletemessages($mapifolder, [$props[PR_ENTRYID]]); |
|
466 | + } |
|
467 | + |
|
468 | + /** |
|
469 | + * Return a single scheduling object. |
|
470 | + * |
|
471 | + * TODO: Add implementation. |
|
472 | + * |
|
473 | + * @param string $principalUri |
|
474 | + * @param string $objectUri |
|
475 | + * |
|
476 | + * @return array |
|
477 | + */ |
|
478 | + public function getSchedulingObject($principalUri, $objectUri) { |
|
479 | + $this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri); |
|
480 | + |
|
481 | + return []; |
|
482 | + } |
|
483 | + |
|
484 | + /** |
|
485 | + * Returns scheduling objects for the principal URI. |
|
486 | + * |
|
487 | + * TODO: Add implementation. |
|
488 | + * |
|
489 | + * @param string $principalUri |
|
490 | + * |
|
491 | + * @return array |
|
492 | + */ |
|
493 | + public function getSchedulingObjects($principalUri) { |
|
494 | + $this->logger->trace("principalUri: %s", $principalUri); |
|
495 | + |
|
496 | + return []; |
|
497 | + } |
|
498 | + |
|
499 | + /** |
|
500 | + * Delete scheduling object. |
|
501 | + * |
|
502 | + * TODO: Add implementation. |
|
503 | + * |
|
504 | + * @param string $principalUri |
|
505 | + * @param string $objectUri |
|
506 | + */ |
|
507 | + public function deleteSchedulingObject($principalUri, $objectUri) { |
|
508 | + $this->logger->trace("principalUri: %s - objectUri: %s", $principalUri, $objectUri); |
|
509 | + } |
|
510 | + |
|
511 | + /** |
|
512 | + * Create a new scheduling object. |
|
513 | + * |
|
514 | + * TODO: Add implementation. |
|
515 | + * |
|
516 | + * @param string $principalUri |
|
517 | + * @param string $objectUri |
|
518 | + * @param string $objectData |
|
519 | + */ |
|
520 | + public function createSchedulingObject($principalUri, $objectUri, $objectData) { |
|
521 | + $this->logger->trace("principalUri: %s - objectUri: %s - objectData: %s", $principalUri, $objectUri, $objectData); |
|
522 | + } |
|
523 | + |
|
524 | + /** |
|
525 | + * Return CTAG for scheduling inbox. |
|
526 | + * |
|
527 | + * TODO: Add implementation. |
|
528 | + * |
|
529 | + * @param string $principalUri |
|
530 | + * |
|
531 | + * @return string |
|
532 | + */ |
|
533 | + public function getSchedulingInboxCtag($principalUri) { |
|
534 | + $this->logger->trace("principalUri: %s", $principalUri); |
|
535 | + |
|
536 | + return "empty"; |
|
537 | + } |
|
538 | + |
|
539 | + /** |
|
540 | + * The getChanges method returns all the changes that have happened, since |
|
541 | + * the specified syncToken in the specified calendar. |
|
542 | + * |
|
543 | + * This function should return an array, such as the following: |
|
544 | + * |
|
545 | + * [ |
|
546 | + * 'syncToken' => 'The current synctoken', |
|
547 | + * 'added' => [ |
|
548 | + * 'new.txt', |
|
549 | + * ], |
|
550 | + * 'modified' => [ |
|
551 | + * 'modified.txt', |
|
552 | + * ], |
|
553 | + * 'deleted' => [ |
|
554 | + * 'foo.php.bak', |
|
555 | + * 'old.txt' |
|
556 | + * ] |
|
557 | + * ); |
|
558 | + * |
|
559 | + * The returned syncToken property should reflect the *current* syncToken |
|
560 | + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
561 | + * property This is * needed here too, to ensure the operation is atomic. |
|
562 | + * |
|
563 | + * If the $syncToken argument is specified as null, this is an initial |
|
564 | + * sync, and all members should be reported. |
|
565 | + * |
|
566 | + * The modified property is an array of nodenames that have changed since |
|
567 | + * the last token. |
|
568 | + * |
|
569 | + * The deleted property is an array with nodenames, that have been deleted |
|
570 | + * from collection. |
|
571 | + * |
|
572 | + * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
573 | + * 1, you only have to report changes that happened only directly in |
|
574 | + * immediate descendants. If it's 2, it should also include changes from |
|
575 | + * the nodes below the child collections. (grandchildren) |
|
576 | + * |
|
577 | + * The $limit argument allows a client to specify how many results should |
|
578 | + * be returned at most. If the limit is not specified, it should be treated |
|
579 | + * as infinite. |
|
580 | + * |
|
581 | + * If the limit (infinite or not) is higher than you're willing to return, |
|
582 | + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
583 | + * |
|
584 | + * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
585 | + * return null. |
|
586 | + * |
|
587 | + * The limit is 'suggestive'. You are free to ignore it. |
|
588 | + * |
|
589 | + * @param string $calendarId |
|
590 | + * @param string $syncToken |
|
591 | + * @param int $syncLevel |
|
592 | + * @param int $limit |
|
593 | + * |
|
594 | + * @return array |
|
595 | + */ |
|
596 | + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { |
|
597 | + $this->logger->trace("calendarId: %s - syncToken: %s - syncLevel: %d - limit: %d", $calendarId, $syncToken, $syncLevel, $limit); |
|
598 | + |
|
599 | + return $this->gDavBackend->Sync($calendarId, $syncToken, static::FILE_EXTENSION, $limit); |
|
600 | + } |
|
601 | 601 | } |
@@ -211,8 +211,7 @@ discard block |
||
211 | 211 | |
212 | 212 | if ($filter['name'] == 'VEVENT') { |
213 | 213 | $types[] = 'IPM.Appointment'; |
214 | - } |
|
215 | - elseif ($filter['name'] == 'VTODO') { |
|
214 | + } elseif ($filter['name'] == 'VTODO') { |
|
216 | 215 | $types[] = 'IPM.Task'; |
217 | 216 | } |
218 | 217 | |
@@ -282,8 +281,7 @@ discard block |
||
282 | 281 | if (!$ics && mapi_last_hresult()) { |
283 | 282 | $this->logger->error("Error generating ical, error code: 0x%08X", mapi_last_hresult()); |
284 | 283 | $ics = null; |
285 | - } |
|
286 | - elseif (!$ics) { |
|
284 | + } elseif (!$ics) { |
|
287 | 285 | $this->logger->error("Error generating ical, unknown error"); |
288 | 286 | $ics = null; |
289 | 287 | } |
@@ -413,8 +411,7 @@ discard block |
||
413 | 411 | // the positions of the last piece of standard information |
414 | 412 | $cut1 = $lastStandard; |
415 | 413 | $cut2 = strripos($ics, 'END:STANDARD', $lastStandard) + 14; // strlen('END:STANDARD') |
416 | - } |
|
417 | - else { |
|
414 | + } else { |
|
418 | 415 | // the part of the ics with the last piece of standard/daytime information |
419 | 416 | $cutEnd = $lastStandard; |
420 | 417 |
@@ -10,185 +10,185 @@ |
||
10 | 10 | namespace grommunio\DAV; |
11 | 11 | |
12 | 12 | class PrincipalsBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface { |
13 | - protected $gDavBackend; |
|
13 | + protected $gDavBackend; |
|
14 | 14 | |
15 | - /** |
|
16 | - * Constructor. |
|
17 | - */ |
|
18 | - public function __construct(GrommunioDavBackend $gDavBackend) { |
|
19 | - file_put_contents('/tmp/grommunio-dav.log', "construct principals\n", FILE_APPEND); |
|
15 | + /** |
|
16 | + * Constructor. |
|
17 | + */ |
|
18 | + public function __construct(GrommunioDavBackend $gDavBackend) { |
|
19 | + file_put_contents('/tmp/grommunio-dav.log', "construct principals\n", FILE_APPEND); |
|
20 | 20 | |
21 | - $this->gDavBackend = $gDavBackend; |
|
22 | - } |
|
21 | + $this->gDavBackend = $gDavBackend; |
|
22 | + } |
|
23 | 23 | |
24 | - /** |
|
25 | - * Returns a list of principals based on a prefix. |
|
26 | - * |
|
27 | - * This prefix will often contain something like 'principals'. You are only |
|
28 | - * expected to return principals that are in this base path. |
|
29 | - * |
|
30 | - * You are expected to return at least a 'uri' for every user, you can |
|
31 | - * return any additional properties if you wish so. Common properties are: |
|
32 | - * {DAV:}displayname |
|
33 | - * {http://sabredav.org/ns}email-address - This is a custom SabreDAV |
|
34 | - * field that's actually injected in a number of other properties. If |
|
35 | - * you have an email address, use this property. |
|
36 | - * |
|
37 | - * @param string $prefixPath |
|
38 | - * |
|
39 | - * @return array |
|
40 | - */ |
|
41 | - public function getPrincipalsByPrefix($prefixPath) { |
|
42 | - $principals = []; |
|
43 | - if ($prefixPath === 'principals') { |
|
44 | - $principals[] = $this->getPrincipalByPath($prefixPath); |
|
45 | - $principals[] = $this->getPrincipalByPath('principals/public'); |
|
46 | - } |
|
24 | + /** |
|
25 | + * Returns a list of principals based on a prefix. |
|
26 | + * |
|
27 | + * This prefix will often contain something like 'principals'. You are only |
|
28 | + * expected to return principals that are in this base path. |
|
29 | + * |
|
30 | + * You are expected to return at least a 'uri' for every user, you can |
|
31 | + * return any additional properties if you wish so. Common properties are: |
|
32 | + * {DAV:}displayname |
|
33 | + * {http://sabredav.org/ns}email-address - This is a custom SabreDAV |
|
34 | + * field that's actually injected in a number of other properties. If |
|
35 | + * you have an email address, use this property. |
|
36 | + * |
|
37 | + * @param string $prefixPath |
|
38 | + * |
|
39 | + * @return array |
|
40 | + */ |
|
41 | + public function getPrincipalsByPrefix($prefixPath) { |
|
42 | + $principals = []; |
|
43 | + if ($prefixPath === 'principals') { |
|
44 | + $principals[] = $this->getPrincipalByPath($prefixPath); |
|
45 | + $principals[] = $this->getPrincipalByPath('principals/public'); |
|
46 | + } |
|
47 | 47 | |
48 | - return $principals; |
|
49 | - } |
|
48 | + return $principals; |
|
49 | + } |
|
50 | 50 | |
51 | - /** |
|
52 | - * Returns a specific principal, specified by it's path. |
|
53 | - * The returned structure should be the exact same as from |
|
54 | - * getPrincipalsByPrefix. |
|
55 | - * |
|
56 | - * @param string $path |
|
57 | - * |
|
58 | - * @return array |
|
59 | - */ |
|
60 | - public function getPrincipalByPath($path) { |
|
61 | - if ($path === 'principals/public') { |
|
62 | - return [ |
|
63 | - 'id' => 'public', |
|
64 | - 'uri' => 'principals/public', |
|
65 | - '{DAV:}displayname' => 'Public', |
|
66 | - '{http://sabredav.org/ns}email-address' => 'postmaster@localhost', |
|
67 | - ]; |
|
68 | - } |
|
69 | - if ($path === 'principals') { |
|
70 | - $username = $this->gDavBackend->GetUser(); |
|
71 | - } |
|
72 | - else { |
|
73 | - $username = str_replace('principals/', '', $path); |
|
74 | - } |
|
75 | - file_put_contents('/tmp/grommunio-dav.log', 'before mapi_zarafa_getuser_by_name', FILE_APPEND); |
|
76 | - $userinfo = mapi_zarafa_getuser_by_name($this->gDavBackend->GetStore($username), $username); |
|
77 | - file_put_contents('/tmp/grommunio-dav.log', 'AFTER mapi_zarafa_getuser_by_name', FILE_APPEND); |
|
78 | - if (!$userinfo) { |
|
79 | - return false; |
|
80 | - } |
|
81 | - $emailaddress = (isset($userinfo['emailaddress']) && $userinfo['emailaddress']) ? $userinfo['emailaddress'] : false; |
|
82 | - $fullname = (isset($userinfo['fullname']) && $userinfo['fullname']) ? $userinfo['fullname'] : false; |
|
51 | + /** |
|
52 | + * Returns a specific principal, specified by it's path. |
|
53 | + * The returned structure should be the exact same as from |
|
54 | + * getPrincipalsByPrefix. |
|
55 | + * |
|
56 | + * @param string $path |
|
57 | + * |
|
58 | + * @return array |
|
59 | + */ |
|
60 | + public function getPrincipalByPath($path) { |
|
61 | + if ($path === 'principals/public') { |
|
62 | + return [ |
|
63 | + 'id' => 'public', |
|
64 | + 'uri' => 'principals/public', |
|
65 | + '{DAV:}displayname' => 'Public', |
|
66 | + '{http://sabredav.org/ns}email-address' => 'postmaster@localhost', |
|
67 | + ]; |
|
68 | + } |
|
69 | + if ($path === 'principals') { |
|
70 | + $username = $this->gDavBackend->GetUser(); |
|
71 | + } |
|
72 | + else { |
|
73 | + $username = str_replace('principals/', '', $path); |
|
74 | + } |
|
75 | + file_put_contents('/tmp/grommunio-dav.log', 'before mapi_zarafa_getuser_by_name', FILE_APPEND); |
|
76 | + $userinfo = mapi_zarafa_getuser_by_name($this->gDavBackend->GetStore($username), $username); |
|
77 | + file_put_contents('/tmp/grommunio-dav.log', 'AFTER mapi_zarafa_getuser_by_name', FILE_APPEND); |
|
78 | + if (!$userinfo) { |
|
79 | + return false; |
|
80 | + } |
|
81 | + $emailaddress = (isset($userinfo['emailaddress']) && $userinfo['emailaddress']) ? $userinfo['emailaddress'] : false; |
|
82 | + $fullname = (isset($userinfo['fullname']) && $userinfo['fullname']) ? $userinfo['fullname'] : false; |
|
83 | 83 | |
84 | - return [ |
|
85 | - 'id' => $username, |
|
86 | - 'uri' => 'principals/' . $username, |
|
87 | - '{DAV:}displayname' => $fullname, |
|
88 | - '{http://sabredav.org/ns}email-address' => $emailaddress, |
|
89 | - // TODO 'vcardurl' should be set, see here: http://sabre.io/dav/principals/ |
|
90 | - ]; |
|
91 | - } |
|
84 | + return [ |
|
85 | + 'id' => $username, |
|
86 | + 'uri' => 'principals/' . $username, |
|
87 | + '{DAV:}displayname' => $fullname, |
|
88 | + '{http://sabredav.org/ns}email-address' => $emailaddress, |
|
89 | + // TODO 'vcardurl' should be set, see here: http://sabre.io/dav/principals/ |
|
90 | + ]; |
|
91 | + } |
|
92 | 92 | |
93 | - /** |
|
94 | - * Updates one ore more webdav properties on a principal. |
|
95 | - * |
|
96 | - * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
97 | - * To do the actual updates, you must tell this object which properties |
|
98 | - * you're going to process with the handle() method. |
|
99 | - * |
|
100 | - * Calling the handle method is like telling the PropPatch object "I |
|
101 | - * promise I can handle updating this property". |
|
102 | - * |
|
103 | - * Read the PropPatch documentation for more info and examples. |
|
104 | - * |
|
105 | - * @param string $path |
|
106 | - */ |
|
107 | - public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { |
|
108 | - } |
|
93 | + /** |
|
94 | + * Updates one ore more webdav properties on a principal. |
|
95 | + * |
|
96 | + * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
97 | + * To do the actual updates, you must tell this object which properties |
|
98 | + * you're going to process with the handle() method. |
|
99 | + * |
|
100 | + * Calling the handle method is like telling the PropPatch object "I |
|
101 | + * promise I can handle updating this property". |
|
102 | + * |
|
103 | + * Read the PropPatch documentation for more info and examples. |
|
104 | + * |
|
105 | + * @param string $path |
|
106 | + */ |
|
107 | + public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) { |
|
108 | + } |
|
109 | 109 | |
110 | - /** |
|
111 | - * This method is used to search for principals matching a set of |
|
112 | - * properties. |
|
113 | - * |
|
114 | - * This search is specifically used by RFC3744's principal-property-search |
|
115 | - * REPORT. |
|
116 | - * |
|
117 | - * The actual search should be a unicode-non-case-sensitive search. The |
|
118 | - * keys in searchProperties are the WebDAV property names, while the values |
|
119 | - * are the property values to search on. |
|
120 | - * |
|
121 | - * By default, if multiple properties are submitted to this method, the |
|
122 | - * various properties should be combined with 'AND'. If $test is set to |
|
123 | - * 'anyof', it should be combined using 'OR'. |
|
124 | - * |
|
125 | - * This method should simply return an array with full principal uri's. |
|
126 | - * |
|
127 | - * If somebody attempted to search on a property the backend does not |
|
128 | - * support, you should simply return 0 results. |
|
129 | - * |
|
130 | - * You can also just return 0 results if you choose to not support |
|
131 | - * searching at all, but keep in mind that this may stop certain features |
|
132 | - * from working. |
|
133 | - * |
|
134 | - * @param string $prefixPath |
|
135 | - * @param string $test |
|
136 | - * |
|
137 | - * @return array |
|
138 | - */ |
|
139 | - public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { |
|
140 | - } |
|
110 | + /** |
|
111 | + * This method is used to search for principals matching a set of |
|
112 | + * properties. |
|
113 | + * |
|
114 | + * This search is specifically used by RFC3744's principal-property-search |
|
115 | + * REPORT. |
|
116 | + * |
|
117 | + * The actual search should be a unicode-non-case-sensitive search. The |
|
118 | + * keys in searchProperties are the WebDAV property names, while the values |
|
119 | + * are the property values to search on. |
|
120 | + * |
|
121 | + * By default, if multiple properties are submitted to this method, the |
|
122 | + * various properties should be combined with 'AND'. If $test is set to |
|
123 | + * 'anyof', it should be combined using 'OR'. |
|
124 | + * |
|
125 | + * This method should simply return an array with full principal uri's. |
|
126 | + * |
|
127 | + * If somebody attempted to search on a property the backend does not |
|
128 | + * support, you should simply return 0 results. |
|
129 | + * |
|
130 | + * You can also just return 0 results if you choose to not support |
|
131 | + * searching at all, but keep in mind that this may stop certain features |
|
132 | + * from working. |
|
133 | + * |
|
134 | + * @param string $prefixPath |
|
135 | + * @param string $test |
|
136 | + * |
|
137 | + * @return array |
|
138 | + */ |
|
139 | + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { |
|
140 | + } |
|
141 | 141 | |
142 | - /** |
|
143 | - * Finds a principal by its URI. |
|
144 | - * |
|
145 | - * This method may receive any type of uri, but mailto: addresses will be |
|
146 | - * the most common. |
|
147 | - * |
|
148 | - * Implementation of this API is optional. It is currently used by the |
|
149 | - * CalDAV system to find principals based on their email addresses. If this |
|
150 | - * API is not implemented, some features may not work correctly. |
|
151 | - * |
|
152 | - * This method must return a relative principal path, or null, if the |
|
153 | - * principal was not found or you refuse to find it. |
|
154 | - * |
|
155 | - * @param string $uri |
|
156 | - * @param string $principalPrefix |
|
157 | - * |
|
158 | - * @return string |
|
159 | - */ |
|
160 | - public function findByUri($uri, $principalPrefix) { |
|
161 | - } |
|
142 | + /** |
|
143 | + * Finds a principal by its URI. |
|
144 | + * |
|
145 | + * This method may receive any type of uri, but mailto: addresses will be |
|
146 | + * the most common. |
|
147 | + * |
|
148 | + * Implementation of this API is optional. It is currently used by the |
|
149 | + * CalDAV system to find principals based on their email addresses. If this |
|
150 | + * API is not implemented, some features may not work correctly. |
|
151 | + * |
|
152 | + * This method must return a relative principal path, or null, if the |
|
153 | + * principal was not found or you refuse to find it. |
|
154 | + * |
|
155 | + * @param string $uri |
|
156 | + * @param string $principalPrefix |
|
157 | + * |
|
158 | + * @return string |
|
159 | + */ |
|
160 | + public function findByUri($uri, $principalPrefix) { |
|
161 | + } |
|
162 | 162 | |
163 | - /** |
|
164 | - * Returns the list of members for a group-principal. |
|
165 | - * |
|
166 | - * @param string $principal |
|
167 | - * |
|
168 | - * @return array |
|
169 | - */ |
|
170 | - public function getGroupMemberSet($principal) { |
|
171 | - return []; |
|
172 | - } |
|
163 | + /** |
|
164 | + * Returns the list of members for a group-principal. |
|
165 | + * |
|
166 | + * @param string $principal |
|
167 | + * |
|
168 | + * @return array |
|
169 | + */ |
|
170 | + public function getGroupMemberSet($principal) { |
|
171 | + return []; |
|
172 | + } |
|
173 | 173 | |
174 | - /** |
|
175 | - * Returns the list of groups a principal is a member of. |
|
176 | - * |
|
177 | - * @param string $principal |
|
178 | - * |
|
179 | - * @return array |
|
180 | - */ |
|
181 | - public function getGroupMembership($principal) { |
|
182 | - return []; |
|
183 | - } |
|
174 | + /** |
|
175 | + * Returns the list of groups a principal is a member of. |
|
176 | + * |
|
177 | + * @param string $principal |
|
178 | + * |
|
179 | + * @return array |
|
180 | + */ |
|
181 | + public function getGroupMembership($principal) { |
|
182 | + return []; |
|
183 | + } |
|
184 | 184 | |
185 | - /** |
|
186 | - * Updates the list of group members for a group principal. |
|
187 | - * |
|
188 | - * The principals should be passed as a list of uri's. |
|
189 | - * |
|
190 | - * @param string $principal |
|
191 | - */ |
|
192 | - public function setGroupMemberSet($principal, array $members) { |
|
193 | - } |
|
185 | + /** |
|
186 | + * Updates the list of group members for a group principal. |
|
187 | + * |
|
188 | + * The principals should be passed as a list of uri's. |
|
189 | + * |
|
190 | + * @param string $principal |
|
191 | + */ |
|
192 | + public function setGroupMemberSet($principal, array $members) { |
|
193 | + } |
|
194 | 194 | } |
@@ -68,8 +68,7 @@ |
||
68 | 68 | } |
69 | 69 | if ($path === 'principals') { |
70 | 70 | $username = $this->gDavBackend->GetUser(); |
71 | - } |
|
72 | - else { |
|
71 | + } else { |
|
73 | 72 | $username = str_replace('principals/', '', $path); |
74 | 73 | } |
75 | 74 | file_put_contents('/tmp/grommunio-dav.log', 'before mapi_zarafa_getuser_by_name', FILE_APPEND); |