ImportChangesICS::ImportMessageReadFlag()   B
last analyzed

Complexity

Conditions 9
Paths 16

Size

Total Lines 57
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 25
c 0
b 0
f 0
nc 16
nop 3
dl 0
loc 57
rs 8.0555

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * SPDX-License-Identifier: AGPL-3.0-only
5
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
6
 * SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH
7
 *
8
 * This is a generic class that is used by both the proxy importer (for
9
 * outgoing messages) and our local importer (for incoming messages). Basically
10
 * all shared conversion data for converting to and from MAPI objects is in
11
 * here.
12
 */
13
14
/**
15
 * This is our local importer. Tt receives data from the PDA, for contents and hierarchy changes.
16
 * It must therefore receive the incoming data and convert it into MAPI objects, and then send
17
 * them to the ICS importer to do the actual writing of the object.
18
 * The creation of folders is fairly trivial, because folders that are created on
19
 * the PDA are always e-mail folders.
20
 */
21
class ImportChangesICS implements IImportChanges {
22
	private $folderidHex;
23
	private $store;
24
	private $session;
25
	private $flags;
26
	private $statestream;
27
	private $importer;
28
	private $memChanges;
29
	private $mapiprovider;
30
	private $conflictsLoaded;
31
	private $conflictsContentParameters;
32
	private $conflictsState;
33
	private $cutoffdate;
34
	private $contentClass;
35
	private $prefix;
36
37
	/**
38
	 * Constructor.
39
	 *
40
	 * @param mapisession $session
0 ignored issues
show
Bug introduced by
The type mapisession was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
41
	 * @param mapistore   $store
0 ignored issues
show
Bug introduced by
The type mapistore was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
42
	 * @param string      $folderid (opt)
43
	 *
44
	 * @throws StatusException
45
	 */
46
	public function __construct($session, $store, private $folderid = false) {
47
		$this->session = $session;
48
		$this->store = $store;
49
		$this->folderidHex = bin2hex($this->folderid);
50
		$this->conflictsLoaded = false;
51
		$this->cutoffdate = false;
52
		$this->contentClass = false;
53
		$this->prefix = '';
54
55
		if ($this->folderid) {
56
			$entryid = mapi_msgstore_entryidfromsourcekey($store, $this->folderid);
57
			$folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex);
58
			// Only append backend id if the mapping backendid<->folderid is available.
59
			if ($folderidForBackendId != $this->folderidHex) {
60
				$this->prefix = $folderidForBackendId . ':';
61
			}
62
		}
63
		else {
64
			$storeprops = mapi_getprops($store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
0 ignored issues
show
Bug introduced by
The constant PR_IPM_PUBLIC_FOLDERS_ENTRYID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The constant PR_IPM_SUBTREE_ENTRYID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
65
			if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
66
				$entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
67
			}
68
			else {
69
				$entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
70
			}
71
		}
72
73
		$folder = false;
74
		if ($entryid) {
75
			$folder = mapi_msgstore_openentry($store, $entryid);
76
		}
77
78
		if (!$folder) {
79
			$this->importer = false;
80
81
			// We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
82
			// if this happened while doing content sync, the mobile will try to resync the folderhierarchy
83
			throw new StatusException(sprintf("ImportChangesICS('%s','%s'): Error, unable to open folder: 0x%X", $session, bin2hex($this->folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN);
84
		}
85
86
		$this->mapiprovider = new MAPIProvider($this->session, $this->store);
0 ignored issues
show
Bug introduced by
$this->store of type mapistore is incompatible with the type resource expected by parameter $store of MAPIProvider::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
		$this->mapiprovider = new MAPIProvider($this->session, /** @scrutinizer ignore-type */ $this->store);
Loading history...
Bug introduced by
$this->session of type mapisession is incompatible with the type resource expected by parameter $session of MAPIProvider::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

86
		$this->mapiprovider = new MAPIProvider(/** @scrutinizer ignore-type */ $this->session, $this->store);
Loading history...
87
88
		if ($this->folderid) {
89
			$this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportContentsChanges, 0, 0);
0 ignored issues
show
Bug introduced by
The constant IID_IExchangeImportContentsChanges was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The constant PR_COLLECTOR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
90
		}
91
		else {
92
			$this->importer = mapi_openproperty($folder, PR_COLLECTOR, IID_IExchangeImportHierarchyChanges, 0, 0);
0 ignored issues
show
Bug introduced by
The constant IID_IExchangeImportHierarchyChanges was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
93
		}
94
	}
95
96
	/**
97
	 * Initializes the importer.
98
	 *
99
	 * @param string $state
100
	 * @param int    $flags
101
	 *
102
	 * @return bool
103
	 *
104
	 * @throws StatusException
105
	 */
106
	public function Config($state, $flags = 0) {
107
		$this->flags = $flags;
108
109
		// this should never happen
110
		if ($this->importer === false) {
111
			throw new StatusException("ImportChangesICS->Config(): Error, importer not available", SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_ERROR);
112
		}
113
114
		// Put the state information in a stream that can be used by ICS
115
		$stream = mapi_stream_create();
0 ignored issues
show
Bug introduced by
The function mapi_stream_create was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
		$stream = /** @scrutinizer ignore-call */ mapi_stream_create();
Loading history...
116
		if (strlen($state) == 0) {
117
			$state = hex2bin("0000000000000000");
118
		}
119
120
		SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state)));
121
122
		mapi_stream_write($stream, $state);
123
		$this->statestream = $stream;
124
125
		if ($this->folderid !== false) {
0 ignored issues
show
introduced by
The condition $this->folderid !== false is always true.
Loading history...
126
			// possible conflicting messages will be cached here
127
			$this->memChanges = new ChangesMemoryWrapper();
128
			$stat = mapi_importcontentschanges_config($this->importer, $stream, $flags);
0 ignored issues
show
Bug introduced by
The function mapi_importcontentschanges_config was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

128
			$stat = /** @scrutinizer ignore-call */ mapi_importcontentschanges_config($this->importer, $stream, $flags);
Loading history...
129
		}
130
		else {
131
			$stat = mapi_importhierarchychanges_config($this->importer, $stream, $flags);
0 ignored issues
show
Bug introduced by
The function mapi_importhierarchychanges_config was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

131
			$stat = /** @scrutinizer ignore-call */ mapi_importhierarchychanges_config($this->importer, $stream, $flags);
Loading history...
132
		}
133
134
		if (!$stat) {
135
			throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
136
		}
137
138
		return $stat;
139
	}
140
141
	/**
142
	 * Configures additional parameters for content selection.
143
	 *
144
	 * @param ContentParameters $contentparameters
145
	 *
146
	 * @return bool
147
	 *
148
	 * @throws StatusException
149
	 */
150
	public function ConfigContentParameters($contentparameters) {
151
		$filtertype = $contentparameters->GetFilterType();
0 ignored issues
show
Bug introduced by
The method GetFilterType() does not exist on ContentParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
		/** @scrutinizer ignore-call */ 
152
  $filtertype = $contentparameters->GetFilterType();
Loading history...
152
153
		if ($filtertype == SYNC_FILTERTYPE_DISABLE) {
154
			$filtertype = false;
155
		}
156
157
		switch ($contentparameters->GetContentClass()) {
0 ignored issues
show
Bug introduced by
The method GetContentClass() does not exist on ContentParameters. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

157
		switch ($contentparameters->/** @scrutinizer ignore-call */ GetContentClass()) {
Loading history...
158
			case "Email":
159
				$this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
160
				break;
161
162
			case "Calendar":
163
				$this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
164
				break;
165
166
			default:
167
			case "Contacts":
168
			case "Tasks":
169
				$this->cutoffdate = false;
170
				break;
171
		}
172
		$this->contentClass = $contentparameters->GetContentClass();
173
174
		return true;
175
	}
176
177
	/**
178
	 * Reads state from the Importer.
179
	 *
180
	 * @return string
181
	 *
182
	 * @throws StatusException
183
	 */
184
	public function GetState() {
185
		$error = false;
186
		if (!isset($this->statestream) || $this->importer === false) {
187
			$error = true;
188
		}
189
190
		if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) {
191
			if (mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) {
192
				$error = true;
193
			}
194
		}
195
196
		if ($error === true) {
197
			throw new StatusException(sprintf("ImportChangesICS->GetState(): Error, state not available or unable to update: 0x%X", mapi_last_hresult()), ($this->folderid) ? SYNC_STATUS_FOLDERHIERARCHYCHANGED : SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
198
		}
199
200
		mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET);
0 ignored issues
show
Bug introduced by
The constant STREAM_SEEK_SET was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The function mapi_stream_seek was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

200
		/** @scrutinizer ignore-call */ 
201
  mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET);
Loading history...
201
202
		$state = "";
203
		while (true) {
204
			$data = mapi_stream_read($this->statestream, 4096);
205
			if (strlen($data)) {
206
				$state .= $data;
207
			}
208
			else {
209
				break;
210
			}
211
		}
212
213
		return $state;
214
	}
215
216
	/**
217
	 * Checks if a message may be modified. This involves checking:
218
	 * - if there is a synchronization interval and if so, if the message is in it (sync window).
219
	 *   These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions.
220
	 * - if the message is not marked as private in a shared folder.
221
	 *
222
	 * @param string $messageid the message id to be checked
223
	 *
224
	 * @return bool
225
	 */
226
	private function isModificationAllowed($messageid) {
227
		$sharedUser = GSync::GetAdditionalSyncFolderStore(bin2hex($this->folderid));
228
		// if this is either a user folder or SYSTEM and no restriction is set, we don't need to check
229
		if (($sharedUser == false || $sharedUser == 'SYSTEM') && $this->cutoffdate === false && !GSync::GetBackend()->GetImpersonatedUser()) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $sharedUser of type string to the boolean false. If you are specifically checking for an empty string, consider using the more explicit === '' instead.
Loading history...
230
			return true;
231
		}
232
233
		// open the existing object
234
		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid));
235
		if (!$entryid) {
236
			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult()));
237
238
			return false;
239
		}
240
241
		$mapimessage = mapi_msgstore_openentry($this->store, $entryid);
242
		if (!$mapimessage) {
243
			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult()));
244
245
			return false;
246
		}
247
248
		// check the sync interval
249
		if ($this->cutoffdate !== false) {
250
			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s (%s)", $messageid, Utils::FormatDate($this->cutoffdate), $this->cutoffdate));
0 ignored issues
show
Bug introduced by
$this->cutoffdate of type true is incompatible with the type integer expected by parameter $ts of Utils::FormatDate(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s (%s)", $messageid, Utils::FormatDate(/** @scrutinizer ignore-type */ $this->cutoffdate), $this->cutoffdate));
Loading history...
Bug introduced by
$this->cutoffdate of type true is incompatible with the type double|integer|string expected by parameter $values of sprintf(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->isModificationAllowed('%s'): cut off date is: %s (%s)", $messageid, Utils::FormatDate($this->cutoffdate), /** @scrutinizer ignore-type */ $this->cutoffdate));
Loading history...
251
			if (($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, $this->cutoffdate)) ||
0 ignored issues
show
Bug introduced by
$this->cutoffdate of type true is incompatible with the type long expected by parameter $timestamp of MAPIUtils::IsInEmailSyncInterval(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

251
			if (($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, /** @scrutinizer ignore-type */ $this->cutoffdate)) ||
Loading history...
252
				  ($this->contentClass == "Calendar" && !MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, $this->cutoffdate))) {
0 ignored issues
show
Bug introduced by
$this->cutoffdate of type true is incompatible with the type long expected by parameter $timestamp of MAPIUtils::IsInCalendarSyncInterval(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
				  ($this->contentClass == "Calendar" && !MAPIUtils::IsInCalendarSyncInterval($this->store, $mapimessage, /** @scrutinizer ignore-type */ $this->cutoffdate))) {
Loading history...
253
				SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message in %s is outside the sync interval. Data not saved.", $messageid, $this->contentClass));
254
255
				return false;
256
			}
257
		}
258
259
		// check if not private
260
		if (MAPIUtils::IsMessageSharedAndPrivate($this->folderid, $mapimessage)) {
261
			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Message is shared and marked as private. Data not saved.", $messageid));
262
263
			return false;
264
		}
265
266
		// yes, modification allowed
267
		return true;
268
	}
269
270
	/*----------------------------------------------------------------------------------------------------------
271
	 * Methods for ContentsExporter
272
	 */
273
274
	/**
275
	 * Loads objects which are expected to be exported with the state
276
	 * Before importing/saving the actual message from the mobile, a conflict detection should be done.
277
	 *
278
	 * @param ContentParameters $contentparameters class of objects
279
	 * @param string            $state
280
	 *
281
	 * @return bool
282
	 *
283
	 * @throws StatusException
284
	 */
285
	public function LoadConflicts($contentparameters, $state) {
286
		if (!isset($this->session) || !isset($this->store) || !isset($this->folderid)) {
287
			throw new StatusException("ImportChangesICS->LoadConflicts(): Error, can not load changes for conflict detection. Session, store or folder information not available", SYNC_STATUS_SERVERERROR);
288
		}
289
290
		// save data to load changes later if necessary
291
		$this->conflictsLoaded = false;
292
		$this->conflictsContentParameters = $contentparameters;
293
		$this->conflictsState = $state;
294
295
		SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->LoadConflicts(): will be loaded later if necessary");
296
297
		return true;
298
	}
299
300
	/**
301
	 * Potential conflicts are only loaded when really necessary,
302
	 * e.g. on ADD or MODIFY.
303
	 *
304
	 * @return bool
305
	 */
306
	private function lazyLoadConflicts() {
307
		if (!isset($this->session) || !isset($this->store) || !isset($this->folderid) ||
308
			!isset($this->conflictsContentParameters) || $this->conflictsState === false) {
309
			SLog::Write(LOGLEVEL_WARN, "ImportChangesICS->lazyLoadConflicts(): can not load potential conflicting changes in lazymode for conflict detection. Missing information");
310
311
			return false;
312
		}
313
314
		if (!$this->conflictsLoaded) {
315
			SLog::Write(LOGLEVEL_DEBUG, "ImportChangesICS->lazyLoadConflicts(): loading..");
316
317
			// configure an exporter so we can detect conflicts
318
			$exporter = new ExportChangesICS($this->session, $this->store, $this->folderid);
319
			$exporter->Config($this->conflictsState);
320
			$exporter->ConfigContentParameters($this->conflictsContentParameters);
321
			$exporter->InitializeExporter($this->memChanges);
322
323
			// monitor how long it takes to export potential conflicts
324
			// if this takes "too long" we cancel this operation!
325
			$potConflicts = $exporter->GetChangeCount();
326
			if ($potConflicts > 100) {
327
				SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection abandoned as there are too many (%d) changes to be exported.", $potConflicts));
328
				$this->conflictsLoaded = true;
329
330
				return false;
331
			}
332
			$started = time();
333
			$exported = 0;
334
335
			try {
336
				while (is_array($exporter->Synchronize())) {
337
					++$exported;
338
339
					// stop if this takes more than 15 seconds and there are more than 5 changes still to be exported
340
					// within 20 seconds this should be finished or it will not be performed
341
					if ((time() - $started) > 15 && ($potConflicts - $exported) > 5) {
342
						SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): conflict detection cancelled as operation is too slow. In %d seconds only %d from %d changes were processed.", time() - $started, $exported, $potConflicts));
343
						$this->conflictsLoaded = true;
344
345
						return false;
346
					}
347
				}
348
			}
349
			// something really bad happened while exporting changes
350
			catch (StatusException $stex) {
351
				SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->lazyLoadConflicts(): got StatusException code %d while exporting changes. Ignore and mark conflicts as loaded.", $stex->getCode()));
352
			}
353
			$this->conflictsLoaded = true;
354
		}
355
356
		return true;
357
	}
358
359
	/**
360
	 * Imports a single message.
361
	 *
362
	 * @param string     $id
363
	 * @param SyncObject $message
364
	 *
365
	 * @return bool|SyncObject - failure / response
366
	 *
367
	 * @throws StatusException
368
	 */
369
	public function ImportMessageChange($id, $message) {
370
		$flags = 0;
371
		$props = [];
372
		$props[PR_PARENT_SOURCE_KEY] = $this->folderid;
0 ignored issues
show
Bug introduced by
The constant PR_PARENT_SOURCE_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
373
		$messageClass = $message::class;
374
375
		// set the PR_SOURCE_KEY if available or mark it as new message
376
		if ($id) {
377
			[, $sk] = Utils::SplitMessageId($id);
378
			$props[PR_SOURCE_KEY] = hex2bin((string) $sk);
0 ignored issues
show
Bug introduced by
The constant PR_SOURCE_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
379
380
			// check if message is in the synchronization interval and/or shared+private
381
			if (!$this->isModificationAllowed($sk)) {
382
				throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Message modification is not allowed. Data not saved.", $id, $messageClass), SYNC_STATUS_SYNCCANNOTBECOMPLETED);
383
			}
384
385
			// check for conflicts
386
			$this->lazyLoadConflicts();
387
			if ($this->memChanges->IsChanged($id)) {
388
				if ($this->flags & SYNC_CONFLICT_OVERWRITE_PIM) {
389
					// in these cases the status SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT should be returned, so the mobile client can inform the end user
390
					throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Server overwrites PIM. User is informed.", $id, $messageClass), SYNC_STATUS_CONFLICTCLIENTSERVEROBJECT, null, LOGLEVEL_INFO);
391
				}
392
393
				SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from Server will be dropped! PIM overwrites server.", $id, $messageClass));
394
			}
395
			if ($this->memChanges->IsDeleted($id)) {
396
				SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Conflict detected. Data from PIM will be dropped! Object was deleted on server.", $id, $messageClass));
397
				$response = Utils::GetResponseFromMessageClass($messageClass);
398
				$response->hasResponse = false;
399
400
				return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type object which is incompatible with the return type mandated by IImportChanges::ImportMessageChange() of boolean|string.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
401
			}
402
		}
403
		else {
404
			$flags = SYNC_NEW_MESSAGE;
0 ignored issues
show
Bug introduced by
The constant SYNC_NEW_MESSAGE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
405
		}
406
407
		if (mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mapimessage does not exist. Did you maybe mean $message?
Loading history...
Bug introduced by
The function mapi_importcontentschanges_importmessagechange was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

407
		if (/** @scrutinizer ignore-call */ mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) {
Loading history...
408
			// grommunio-sync #113: workaround blocking notifications on this item
409
			$sourcekeyprops = mapi_getprops($mapimessage, [PR_ENTRYID]);
410
			if (isset($sourcekeyprops[PR_ENTRYID])) {
411
				GSync::ReplyCatchMark(bin2hex((string) $sourcekeyprops[PR_ENTRYID]));
412
			}
413
414
			$response = $this->mapiprovider->SetMessage($mapimessage, $message);
415
			mapi_savechanges($mapimessage);
416
417
			if (mapi_last_hresult()) {
418
				throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, $messageClass, mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED);
419
			}
420
421
			$sourcekeyprops = mapi_getprops($mapimessage, [PR_SOURCE_KEY]);
422
423
			$response->serverid = $this->prefix . bin2hex((string) $sourcekeyprops[PR_SOURCE_KEY]);
424
425
			return $response;
426
		}
427
428
		throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, $messageClass, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
429
	}
430
431
	/**
432
	 * Imports a deletion. This may conflict if the local object has been modified.
433
	 *
434
	 * @param string $id
435
	 * @param bool   $asSoftDelete (opt) if true, the deletion is exported as "SoftDelete", else as "Remove" - default: false
436
	 *
437
	 * @return bool
438
	 */
439
	public function ImportMessageDeletion($id, $asSoftDelete = false) {
440
		[, $sk] = Utils::SplitMessageId($id);
441
442
		// check if message is in the synchronization interval and/or shared+private
443
		if (!$this->isModificationAllowed($sk)) {
444
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Message deletion is not allowed. Deletion not executed.", $id), SYNC_STATUS_OBJECTNOTFOUND);
445
		}
446
447
		// check for conflicts
448
		$this->lazyLoadConflicts();
449
		if ($this->memChanges->IsChanged($id)) {
450
			SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data from Server will be dropped! PIM deleted object.", $id));
451
		}
452
		elseif ($this->memChanges->IsDeleted($id)) {
453
			SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id));
454
455
			return true;
456
		}
457
458
		// check if we need to do actions before deleting this message (e.g. send meeting cancellations to attendees)
459
		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin((string) $sk));
460
		if ($entryid) {
461
			// open the source message
462
			$mapimessage = mapi_msgstore_openentry($this->store, $entryid);
463
			$this->mapiprovider->PreDeleteMessage($mapimessage);
464
			// grommunio-sync #113: workaround blocking notifications on this item
465
			GSync::ReplyCatchMark(bin2hex($entryid));
466
		}
467
468
		// do a 'soft' delete so people can un-delete if necessary
469
		mapi_importcontentschanges_importmessagedeletion($this->importer, 1, [hex2bin((string) $sk)]);
0 ignored issues
show
Bug introduced by
The function mapi_importcontentschanges_importmessagedeletion was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

469
		/** @scrutinizer ignore-call */ 
470
  mapi_importcontentschanges_importmessagedeletion($this->importer, 1, [hex2bin((string) $sk)]);
Loading history...
470
		if (mapi_last_hresult()) {
471
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
472
		}
473
474
		return true;
475
	}
476
477
	/**
478
	 * Imports a change in 'read' flag
479
	 * This can never conflict.
480
	 *
481
	 * @param string $id
482
	 * @param int    $flags      - read/unread
483
	 * @param array  $categories
484
	 *
485
	 * @return bool
486
	 *
487
	 * @throws StatusException
488
	 */
489
	public function ImportMessageReadFlag($id, $flags, $categories = []) {
490
		$response = new SyncMailResponse();
491
		[$fsk, $sk] = Utils::SplitMessageId($id);
492
493
		// if $fsk is set, we convert it into a backend id.
494
		if ($fsk) {
495
			$fsk = GSync::GetDeviceManager()->GetBackendIdForFolderId($fsk);
496
		}
497
498
		// read flag change for our current folder
499
		if ($this->folderidHex == $fsk || empty($fsk)) {
500
			// check if it is in the synchronization interval and/or shared+private
501
			if (!$this->isModificationAllowed($sk)) {
502
				throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Flag update is not allowed. Flags not updated.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND);
503
			}
504
505
			// check for conflicts
506
			/*
507
			 * Checking for conflicts is correct at this point, but is a very expensive operation.
508
			 * If the message was deleted, only an error will be shown.
509
			 *
510
			$this->lazyLoadConflicts();
511
			if($this->memChanges->IsDeleted($id)) {
512
				SLog::Write(LOGLEVEL_INFO, sprintf("ImportChangesICS->ImportMessageReadFlag('%s'): Conflict detected. Data is already deleted. Request will be ignored.", $id));
513
				return true;
514
			}
515
			 */
516
517
			// grommunio-sync #113: workaround blocking notifications on this item
518
			$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($fsk), hex2bin((string) $sk));
519
			if ($entryid !== false) {
520
				GSync::ReplyCatchMark(bin2hex($entryid));
521
			}
522
523
			$readstate = ["sourcekey" => hex2bin((string) $sk), "flags" => $flags];
524
525
			if (!mapi_importcontentschanges_importperuserreadstatechange($this->importer, [$readstate])) {
0 ignored issues
show
Bug introduced by
The function mapi_importcontentschang...tperuserreadstatechange was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

525
			if (!/** @scrutinizer ignore-call */ mapi_importcontentschanges_importperuserreadstatechange($this->importer, [$readstate])) {
Loading history...
526
				throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
527
			}
528
		}
529
		// yeah OL sucks
530
		else {
531
			if (!$fsk) {
532
				throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state. The message is in another folder but id is unknown as no short folder id is available. Please remove your device states to fully resync your device. Operation ignored.", $id, $flags), SYNC_STATUS_OBJECTNOTFOUND);
533
			}
534
			$store = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($fsk), $fsk);
535
			$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin((string) $fsk), hex2bin((string) $sk));
536
			$realMessage = mapi_msgstore_openentry($store, $entryid);
537
			$flag = 0;
538
			if ($flags == 0) {
539
				$flag |= CLEAR_READ_FLAG;
0 ignored issues
show
Bug introduced by
The constant CLEAR_READ_FLAG was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
540
			}
541
			$p = mapi_message_setreadflag($realMessage, $flag);
0 ignored issues
show
Unused Code introduced by
The assignment to $p is dead and can be removed.
Loading history...
542
			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult()));
543
		}
544
545
		return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type SyncMailResponse which is incompatible with the documented return type boolean.
Loading history...
546
	}
547
548
	/**
549
	 * Imports a move of a message. This occurs when a user moves an item to another folder.
550
	 *
551
	 * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer,
552
	 * but the grommunio importer does not support this. Therefore we currently implement it via a standard mapi
553
	 * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync.
554
	 * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder
555
	 * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties
556
	 * of the source message to the new one and then delete the source message.
557
	 *
558
	 * @param string $id
559
	 * @param string $newfolder destination folder
560
	 *
561
	 * @return bool|string
562
	 *
563
	 * @throws StatusException
564
	 */
565
	public function ImportMessageMove($id, $newfolder) {
566
		[, $sk] = Utils::SplitMessageId($id);
567
		if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) {
568
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST);
569
		}
570
571
		// Get the entryid of the message we're moving
572
		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin((string) $sk));
573
		$srcmessage = false;
574
575
		if ($entryid) {
576
			// open the source message
577
			$srcmessage = mapi_msgstore_openentry($this->store, $entryid);
578
		}
579
580
		if (!$entryid || !$srcmessage) {
581
			$code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID;
582
			$mapiLastHresult = mapi_last_hresult();
583
			// if we move to the trash and the source message is not found, we can also just tell the mobile that we successfully moved to avoid errors
584
			if ($newfolder == GSync::GetBackend()->GetWasteBasket()) {
585
				$code = SYNC_MOVEITEMSSTATUS_SUCCESS;
586
			}
587
			$errorCase = !$entryid ? "resolve source message id" : "open source message";
588
589
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, $mapiLastHresult), $code);
590
		}
591
592
		// check if it is in the synchronization interval and/or shared+private
593
		if (!$this->isModificationAllowed($sk)) {
594
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message move is not allowed. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
595
		}
596
597
		// get correct mapi store for the destination folder
598
		$dststore = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($newfolder), $newfolder);
599
		if ($dststore === false) {
600
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
601
		}
602
603
		$dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder));
604
		if (!$dstentryid) {
605
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
606
		}
607
608
		$dstfolder = mapi_msgstore_openentry($dststore, $dstentryid);
609
		if (!$dstfolder) {
610
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
611
		}
612
613
		$newmessage = mapi_folder_createmessage($dstfolder);
614
		if (!$newmessage) {
615
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to create message in destination folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
616
		}
617
618
		// Copy message
619
		mapi_copyto($srcmessage, [], [], $newmessage);
620
		if (mapi_last_hresult()) {
621
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
622
		}
623
624
		$srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid);
625
		if (!$srcfolderentryid) {
626
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
627
		}
628
629
		$srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid);
630
		if (!$srcfolder) {
631
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
632
		}
633
634
		// Save changes
635
		mapi_savechanges($newmessage);
636
		if (mapi_last_hresult()) {
637
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
638
		}
639
640
		// Delete the old message
641
		if (!mapi_folder_deletemessages($srcfolder, [$entryid])) {
642
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, delete of source message failed: 0x%X. Possible duplicates.", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_SOURCEORDESTLOCKED);
643
		}
644
645
		$sourcekeyprops = mapi_getprops($newmessage, [PR_SOURCE_KEY]);
0 ignored issues
show
Bug introduced by
The constant PR_SOURCE_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
646
		if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) {
647
			$prefix = "";
648
			// prepend the destination short folderid, if it exists
649
			$destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder);
650
			if ($destShortId !== $newfolder) {
651
				$prefix = $destShortId . ":";
652
			}
653
654
			return $prefix . bin2hex((string) $sourcekeyprops[PR_SOURCE_KEY]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $prefix . bin2hex...eyprops[PR_SOURCE_KEY]) returns the type string which is incompatible with the return type mandated by IImportChanges::ImportMessageMove() of boolean.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
655
		}
656
657
		return false;
658
	}
659
660
	/*----------------------------------------------------------------------------------------------------------
661
	 * Methods for HierarchyExporter
662
	 */
663
664
	/**
665
	 * Imports a change on a folder.
666
	 *
667
	 * @param object $folder SyncFolder
668
	 *
669
	 * @return bool|SyncFolder false on error or a SyncFolder object with serverid and BackendId set (if available)
670
	 *
671
	 * @throws StatusException
672
	 */
673
	public function ImportFolderChange($folder) {
674
		$id = $folder->BackendId ?? false;
675
		$parent = $folder->parentid;
676
		$parent_org = $folder->parentid;
677
		$displayname = $folder->displayname;
678
		$type = $folder->type;
679
680
		if (Utils::IsSystemFolder($type)) {
681
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, system folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER);
682
		}
683
684
		// create a new folder if $id is not set
685
		if (!$id) {
686
			// the root folder is "0" - get IPM_SUBTREE
687
			if ($parent == "0") {
688
				$parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
0 ignored issues
show
Bug introduced by
The constant PR_IPM_PUBLIC_FOLDERS_ENTRYID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The constant PR_IPM_SUBTREE_ENTRYID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
689
				if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) {
690
					$parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
691
				}
692
				elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) {
693
					$parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
694
				}
695
			}
696
			else {
697
				$parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin((string) $parent));
698
			}
699
700
			if (!$parentfentryid) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $parentfentryid does not seem to be defined for all execution paths leading up to this point.
Loading history...
701
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (no entry id)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $var of Utils::PrintAsString(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

701
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (no entry id)", Utils::PrintAsString(/** @scrutinizer ignore-type */ false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND);
Loading history...
702
			}
703
704
			$parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid);
705
			if (!$parentfolder) {
706
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent folder (open entry)", Utils::PrintAsString(false), $folder->parentid, $displayname), SYNC_FSSTATUS_PARENTNOTFOUND);
707
			}
708
709
			//  mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
710
			$newfolder = mapi_folder_createfolder($parentfolder, $displayname, "");
0 ignored issues
show
Bug introduced by
The function mapi_folder_createfolder was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

710
			$newfolder = /** @scrutinizer ignore-call */ mapi_folder_createfolder($parentfolder, $displayname, "");
Loading history...
711
			if (mapi_last_hresult()) {
712
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_folder_createfolder() failed: 0x%X", Utils::PrintAsString(false), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS);
713
			}
714
715
			mapi_setprops($newfolder, [PR_CONTAINER_CLASS => MAPIUtils::GetContainerClassFromFolderType($type)]);
0 ignored issues
show
Bug introduced by
The constant PR_CONTAINER_CLASS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
716
717
			$props = mapi_getprops($newfolder, [PR_SOURCE_KEY]);
0 ignored issues
show
Bug introduced by
The constant PR_SOURCE_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
718
			if (isset($props[PR_SOURCE_KEY])) {
719
				$folder->BackendId = bin2hex((string) $props[PR_SOURCE_KEY]);
720
				$folderOrigin = DeviceManager::FLD_ORIGIN_USER;
721
				if (GSync::GetBackend()->GetImpersonatedUser()) {
722
					$folderOrigin = DeviceManager::FLD_ORIGIN_IMPERSONATED;
723
				}
724
				$folder->serverid = GSync::GetDeviceManager()->GetFolderIdForBackendId($folder->BackendId, true, $folderOrigin, $folder->displayname);
725
				SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Created folder '%s' with id: '%s' backendid: '%s'", $displayname, $folder->serverid, $folder->BackendId));
726
727
				return $folder;
728
			}
729
730
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder created but PR_SOURCE_KEY not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
731
		}
732
733
		// open folder for update
734
		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin((string) $id));
735
		if (!$entryid) {
736
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
737
		}
738
739
		// check if this is a MAPI default folder
740
		if ($this->mapiprovider->IsMAPIDefaultFolder($entryid)) {
741
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, MAPI default folder can not be created/modified", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname), SYNC_FSSTATUS_SYSTEMFOLDER);
742
		}
743
744
		$mfolder = mapi_msgstore_openentry($this->store, $entryid);
745
		if (!$mfolder) {
746
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
747
		}
748
749
		$props = mapi_getprops($mfolder, [PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY, PR_DISPLAY_NAME, PR_CONTAINER_CLASS]);
0 ignored issues
show
Bug introduced by
The constant PR_DISPLAY_NAME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The constant PR_PARENT_SOURCE_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
750
		if (!isset($props[PR_SOURCE_KEY]) || !isset($props[PR_PARENT_SOURCE_KEY]) || !isset($props[PR_DISPLAY_NAME]) || !isset($props[PR_CONTAINER_CLASS])) {
751
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, folder data not available: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
752
		}
753
754
		// get the real parent source key from mapi
755
		if ($parent == "0") {
756
			$parentprops = mapi_getprops($this->store, [PR_IPM_SUBTREE_ENTRYID, PR_IPM_PUBLIC_FOLDERS_ENTRYID]);
757
			if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
758
				$parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
759
			}
760
			else {
761
				$parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
762
			}
763
			$mapifolder = mapi_msgstore_openentry($this->store, $parentfentryid);
764
765
			$rootfolderprops = mapi_getprops($mapifolder, [PR_SOURCE_KEY]);
766
			$parent = bin2hex((string) $rootfolderprops[PR_SOURCE_KEY]);
767
			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): resolved AS parent '0' to sourcekey '%s'", $parent));
768
		}
769
770
		// a changed parent id means that the folder should be moved
771
		if (bin2hex((string) $props[PR_PARENT_SOURCE_KEY]) !== $parent) {
772
			$sourceparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, $props[PR_PARENT_SOURCE_KEY]);
773
			if (!$sourceparentfentryid) {
774
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
775
			}
776
777
			$sourceparentfolder = mapi_msgstore_openentry($this->store, $sourceparentfentryid);
778
			if (!$sourceparentfolder) {
779
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open parent source folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_PARENTNOTFOUND);
780
			}
781
782
			$destparentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin((string) $parent));
783
			if (!$sourceparentfentryid) {
784
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (no entry id): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
785
			}
786
787
			$destfolder = mapi_msgstore_openentry($this->store, $destparentfentryid);
788
			if (!$destfolder) {
789
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to open destination folder (open entry): 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
790
			}
791
792
			// mapi_folder_copyfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
793
			if (!mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) {
0 ignored issues
show
Bug introduced by
The constant FOLDER_MOVE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The function mapi_folder_copyfolder was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

793
			if (!/** @scrutinizer ignore-call */ mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) {
Loading history...
794
				throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, unable to move folder: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_FOLDEREXISTS);
795
			}
796
797
			// the parent changed, but we got a backendID as parent and have to return an AS folderid - the parent-backendId must be mapped at this point already
798
			if ($folder->parentid != 0) {
799
				$folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId($parent);
800
			}
801
			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderChange(): Moved folder '%s' with id: %s/%s from: %s to: %s/%s", $displayname, $folder->serverid, $folder->BackendId, bin2hex((string) $props[PR_PARENT_SOURCE_KEY]), $folder->parentid, $parent_org));
802
803
			return $folder;
804
		}
805
806
		// update the display name
807
		$props = [PR_DISPLAY_NAME => $displayname];
808
		mapi_setprops($mfolder, $props);
809
		mapi_savechanges($mfolder);
810
		if (mapi_last_hresult()) {
811
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderChange('%s','%s','%s'): Error, mapi_savechanges() failed: 0x%X", Utils::PrintAsString($folder->serverid), $folder->parentid, $displayname, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
812
		}
813
814
		SLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: {$id}");
815
816
		return true;
817
	}
818
819
	/**
820
	 * Imports a folder deletion.
821
	 *
822
	 * @param SyncFolder $folder at least "serverid" needs to be set
823
	 *
824
	 * @return int SYNC_FOLDERHIERARCHY_STATUS
825
	 *
826
	 * @throws StatusException
827
	 */
828
	public function ImportFolderDeletion($folder) {
829
		$id = $folder->BackendId;
830
		$parent = $folder->parentid ?? false;
831
		SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, $parent));
0 ignored issues
show
Bug introduced by
It seems like $parent can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

831
		SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, /** @scrutinizer ignore-type */ $parent));
Loading history...
832
833
		$folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin((string) $id));
834
		if (!$folderentryid) {
835
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST);
836
		}
837
838
		// get the folder type from the MAPIProvider
839
		$type = $this->mapiprovider->GetFolderType($folderentryid);
840
841
		if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) {
842
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER);
843
		}
844
845
		$ret = mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin((string) $id)]);
0 ignored issues
show
Bug introduced by
The function mapi_importhierarchychanges_importfolderdeletion was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

845
		$ret = /** @scrutinizer ignore-call */ mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin((string) $id)]);
Loading history...
Bug introduced by
The constant PR_SOURCE_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
846
		if (!$ret) {
847
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
848
		}
849
850
		return $ret;
851
	}
852
}
853