Test Setup Failed
Branch master (463395)
by Mike
02:40
created
lib/GrommunioIMipPlugin.php 1 patch
Indentation   +61 added lines, -61 removed lines patch added patch discarded remove patch
@@ -10,74 +10,74 @@
 block discarded – undo
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
 }
Please login to merge, or discard this patch.
lib/GrommunioSyncState.php 1 patch
Indentation   +87 added lines, -87 removed lines patch added patch discarded remove patch
@@ -10,105 +10,105 @@
 block discarded – undo
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
 }
Please login to merge, or discard this patch.
lib/GPSR3Logger.php 1 patch
Indentation   +101 added lines, -101 removed lines patch added patch discarded remove patch
@@ -10,117 +10,117 @@
 block discarded – undo
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
 }
Please login to merge, or discard this patch.
lib/GrommunioCardDavBackend.php 1 patch
Indentation   +349 added lines, -349 removed lines patch added patch discarded remove patch
@@ -10,353 +10,353 @@
 block discarded – undo
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
 }
Please login to merge, or discard this patch.
lib/GrommunioDavBackend.php 3 patches
Indentation   +719 added lines, -719 removed lines patch added patch discarded remove patch
@@ -10,423 +10,423 @@  discard block
 block discarded – undo
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
 block discarded – undo
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
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -653,7 +653,7 @@
 block discarded – undo
653 653
 					],
654 654
 				], // EXISTS OR
655 655
 			],
656
-		];        // global OR
656
+		]; // global OR
657 657
 	}
658 658
 
659 659
 	/**
Please login to merge, or discard this patch.
Braces   +6 added lines, -12 removed lines patch added patch discarded remove patch
@@ -153,8 +153,7 @@  discard block
 block discarded – undo
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
 block discarded – undo
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
 block discarded – undo
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
 block discarded – undo
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
 block discarded – undo
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
 block discarded – undo
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
 		}
Please login to merge, or discard this patch.
lib/GrommunioCalDavBackend.php 2 patches
Indentation   +581 added lines, -581 removed lines patch added patch discarded remove patch
@@ -10,7 +10,7 @@  discard block
 block discarded – undo
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
 block discarded – undo
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
 }
Please login to merge, or discard this patch.
Braces   +3 added lines, -6 removed lines patch added patch discarded remove patch
@@ -211,8 +211,7 @@  discard block
 block discarded – undo
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
 block discarded – undo
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
 block discarded – undo
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
 
Please login to merge, or discard this patch.
lib/PrincipalsBackend.php 2 patches
Indentation   +169 added lines, -169 removed lines patch added patch discarded remove patch
@@ -10,185 +10,185 @@
 block discarded – undo
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
 }
Please login to merge, or discard this patch.
Braces   +1 added lines, -2 removed lines patch added patch discarded remove patch
@@ -68,8 +68,7 @@
 block discarded – undo
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);
Please login to merge, or discard this patch.