ImportChangesICS   F
last analyzed

Complexity

Total Complexity 140

Size/Duplication

Total Lines 832
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 374
c 1
b 1
f 0
dl 0
loc 832
rs 2
wmc 140

13 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 48 7
A Config() 0 33 5
C lazyLoadConflicts() 0 51 12
A ImportMessageDeletion() 0 36 6
B ConfigContentParameters() 0 25 8
B GetState() 0 30 11
D ImportMessageMove() 0 93 20
A LoadConflicts() 0 13 4
D ImportFolderChange() 0 144 30
A ImportFolderDeletion() 0 23 6
C isModificationAllowed() 0 42 13
B ImportMessageReadFlag() 0 57 9
B ImportMessageChange() 0 60 9

How to fix   Complexity   

Complex Class

Complex classes like ImportChangesICS often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ImportChangesICS, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH
6
 *
7
 * This is a generic class that is used by both the proxy importer (for
8
 * outgoing messages) and our local importer (for incoming messages). Basically
9
 * all shared conversion data for converting to and from MAPI objects is in
10
 * here.
11
 */
12
13
/**
14
 * This is our local importer. Tt receives data from the PDA, for contents and hierarchy changes.
15
 * It must therefore receive the incoming data and convert it into MAPI objects, and then send
16
 * them to the ICS importer to do the actual writing of the object.
17
 * The creation of folders is fairly trivial, because folders that are created on
18
 * the PDA are always e-mail folders.
19
 */
20
class ImportChangesICS implements IImportChanges {
21
	private $folderid;
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, $folderid = false) {
47
		$this->session = $session;
48
		$this->store = $store;
49
		$this->folderid = $folderid;
50
		$this->folderidHex = bin2hex($folderid);
0 ignored issues
show
Bug introduced by
It seems like $folderid can also be of type false; however, parameter $string of bin2hex() does only seem to accept 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

50
		$this->folderidHex = bin2hex(/** @scrutinizer ignore-type */ $folderid);
Loading history...
51
		$this->conflictsLoaded = false;
52
		$this->cutoffdate = false;
53
		$this->contentClass = false;
54
		$this->prefix = '';
55
56
		if ($folderid) {
57
			$entryid = mapi_msgstore_entryidfromsourcekey($store, $folderid);
58
			$folderidForBackendId = GSync::GetDeviceManager()->GetFolderIdForBackendId($this->folderidHex);
59
			// Only append backend id if the mapping backendid<->folderid is available.
60
			if ($folderidForBackendId != $this->folderidHex) {
61
				$this->prefix = $folderidForBackendId . ':';
62
			}
63
		}
64
		else {
65
			$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...
66
			if (GSync::GetBackend()->GetImpersonatedUser() == 'system') {
67
				$entryid = $storeprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
68
			}
69
			else {
70
				$entryid = $storeprops[PR_IPM_SUBTREE_ENTRYID];
71
			}
72
		}
73
74
		$folder = false;
75
		if ($entryid) {
76
			$folder = mapi_msgstore_openentry($store, $entryid);
77
		}
78
79
		if (!$folder) {
80
			$this->importer = false;
81
82
			// We throw an general error SYNC_FSSTATUS_CODEUNKNOWN (12) which is also SYNC_STATUS_FOLDERHIERARCHYCHANGED (12)
83
			// if this happened while doing content sync, the mobile will try to resync the folderhierarchy
84
			throw new StatusException(sprintf("ImportChangesICS('%s','%s'): Error, unable to open folder: 0x%X", $session, bin2hex($folderid), mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN);
85
		}
86
87
		$this->mapiprovider = new MAPIProvider($this->session, $this->store);
0 ignored issues
show
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

87
		$this->mapiprovider = new MAPIProvider(/** @scrutinizer ignore-type */ $this->session, $this->store);
Loading history...
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

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

116
		$stream = /** @scrutinizer ignore-call */ mapi_stream_create();
Loading history...
117
		if (strlen($state) == 0) {
118
			$state = hex2bin("0000000000000000");
119
		}
120
121
		SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->Config(): initializing importer with state: 0x%s", bin2hex($state)));
122
123
		mapi_stream_write($stream, $state);
124
		$this->statestream = $stream;
125
126
		if ($this->folderid !== false) {
127
			// possible conflicting messages will be cached here
128
			$this->memChanges = new ChangesMemoryWrapper();
129
			$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

129
			$stat = /** @scrutinizer ignore-call */ mapi_importcontentschanges_config($this->importer, $stream, $flags);
Loading history...
130
		}
131
		else {
132
			$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

132
			$stat = /** @scrutinizer ignore-call */ mapi_importhierarchychanges_config($this->importer, $stream, $flags);
Loading history...
133
		}
134
135
		if (!$stat) {
136
			throw new StatusException(sprintf("ImportChangesICS->Config(): Error, mapi_import_*_changes_config() failed: 0x%X", mapi_last_hresult()), SYNC_FSSTATUS_CODEUNKNOWN, null, LOGLEVEL_WARN);
137
		}
138
139
		return $stat;
140
	}
141
142
	/**
143
	 * Configures additional parameters for content selection.
144
	 *
145
	 * @param ContentParameters $contentparameters
146
	 *
147
	 * @return bool
148
	 *
149
	 * @throws StatusException
150
	 */
151
	public function ConfigContentParameters($contentparameters) {
152
		$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

152
		/** @scrutinizer ignore-call */ 
153
  $filtertype = $contentparameters->GetFilterType();
Loading history...
153
154
		if ($filtertype == SYNC_FILTERTYPE_DISABLE) {
155
			$filtertype = false;
156
		}
157
158
		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

158
		switch ($contentparameters->/** @scrutinizer ignore-call */ GetContentClass()) {
Loading history...
159
			case "Email":
160
				$this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
161
				break;
162
163
			case "Calendar":
164
				$this->cutoffdate = ($filtertype) ? Utils::GetCutOffDate($filtertype) : false;
165
				break;
166
167
			default:
168
			case "Contacts":
169
			case "Tasks":
170
				$this->cutoffdate = false;
171
				break;
172
		}
173
		$this->contentClass = $contentparameters->GetContentClass();
174
175
		return true;
176
	}
177
178
	/**
179
	 * Reads state from the Importer.
180
	 *
181
	 * @return string
182
	 *
183
	 * @throws StatusException
184
	 */
185
	public function GetState() {
186
		$error = false;
187
		if (!isset($this->statestream) || $this->importer === false) {
188
			$error = true;
189
		}
190
191
		if ($error === false && $this->folderid !== false && function_exists("mapi_importcontentschanges_updatestate")) {
192
			if (mapi_importcontentschanges_updatestate($this->importer, $this->statestream) != true) {
193
				$error = true;
194
			}
195
		}
196
197
		if ($error === true) {
198
			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);
199
		}
200
201
		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

201
		/** @scrutinizer ignore-call */ 
202
  mapi_stream_seek($this->statestream, 0, STREAM_SEEK_SET);
Loading history...
202
203
		$state = "";
204
		while (true) {
205
			$data = mapi_stream_read($this->statestream, 4096);
206
			if (strlen($data)) {
207
				$state .= $data;
208
			}
209
			else {
210
				break;
211
			}
212
		}
213
214
		return $state;
215
	}
216
217
	/**
218
	 * Checks if a message may be modified. This involves checking:
219
	 * - if there is a synchronization interval and if so, if the message is in it (sync window).
220
	 *   These checks only apply to Emails and Appointments only, Contacts, Tasks and Notes do not have time restrictions.
221
	 * - if the message is not marked as private in a shared folder.
222
	 *
223
	 * @param string $messageid the message id to be checked
224
	 *
225
	 * @return bool
226
	 */
227
	private function isModificationAllowed($messageid) {
228
		$sharedUser = GSync::GetAdditionalSyncFolderStore(bin2hex($this->folderid));
0 ignored issues
show
Bug introduced by
It seems like $this->folderid can also be of type boolean; however, parameter $string of bin2hex() does only seem to accept 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

228
		$sharedUser = GSync::GetAdditionalSyncFolderStore(bin2hex(/** @scrutinizer ignore-type */ $this->folderid));
Loading history...
229
		// if this is either a user folder or SYSTEM and no restriction is set, we don't need to check
230
		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...
231
			return true;
232
		}
233
234
		// open the existing object
235
		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($messageid));
236
		if (!$entryid) {
237
			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to resolve message id: 0x%X", $messageid, mapi_last_hresult()));
238
239
			return false;
240
		}
241
242
		$mapimessage = mapi_msgstore_openentry($this->store, $entryid);
243
		if (!$mapimessage) {
244
			SLog::Write(LOGLEVEL_WARN, sprintf("ImportChangesICS->isModificationAllowed('%s'): Error, unable to open entry id: 0x%X", $messageid, mapi_last_hresult()));
245
246
			return false;
247
		}
248
249
		// check the sync interval
250
		if ($this->cutoffdate !== false) {
251
			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

251
			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

251
			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...
252
			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

252
			if (($this->contentClass == "Email" && !MAPIUtils::IsInEmailSyncInterval($this->store, $mapimessage, /** @scrutinizer ignore-type */ $this->cutoffdate)) ||
Loading history...
253
				  ($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

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

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

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

526
			if (!/** @scrutinizer ignore-call */ mapi_importcontentschanges_importperuserreadstatechange($this->importer, [$readstate])) {
Loading history...
527
				throw new StatusException(sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): Error setting read state: 0x%X", $id, $flags, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND);
528
			}
529
		}
530
		// yeah OL sucks
531
		else {
532
			if (!$fsk) {
533
				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);
534
			}
535
			$store = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($fsk), $fsk);
536
			$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($fsk), hex2bin($sk));
537
			$realMessage = mapi_msgstore_openentry($store, $entryid);
538
			$flag = 0;
539
			if ($flags == 0) {
540
				$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...
541
			}
542
			$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...
543
			SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult()));
544
		}
545
546
		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...
547
	}
548
549
	/**
550
	 * Imports a move of a message. This occurs when a user moves an item to another folder.
551
	 *
552
	 * Normally, we would implement this via the 'offical' importmessagemove() function on the ICS importer,
553
	 * but the grommunio importer does not support this. Therefore we currently implement it via a standard mapi
554
	 * call. This causes a mirror 'add/delete' to be sent to the PDA at the next sync.
555
	 * Manfred, 2010-10-21. For some mobiles import was causing duplicate messages in the destination folder
556
	 * (Mantis #202). Therefore we will create a new message in the destination folder, copy properties
557
	 * of the source message to the new one and then delete the source message.
558
	 *
559
	 * @param string $id
560
	 * @param string $newfolder destination folder
561
	 *
562
	 * @return bool|string
563
	 *
564
	 * @throws StatusException
565
	 */
566
	public function ImportMessageMove($id, $newfolder) {
567
		list(, $sk) = Utils::SplitMessageId($id);
568
		if (strtolower($newfolder) == strtolower(bin2hex($this->folderid))) {
0 ignored issues
show
Bug introduced by
It seems like $this->folderid can also be of type boolean; however, parameter $string of bin2hex() does only seem to accept 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

568
		if (strtolower($newfolder) == strtolower(bin2hex(/** @scrutinizer ignore-type */ $this->folderid))) {
Loading history...
569
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, source and destination are equal", $id, $newfolder), SYNC_MOVEITEMSSTATUS_SAMESOURCEANDDEST);
570
		}
571
572
		// Get the entryid of the message we're moving
573
		$entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk));
574
		$srcmessage = false;
575
576
		if ($entryid) {
577
			// open the source message
578
			$srcmessage = mapi_msgstore_openentry($this->store, $entryid);
579
		}
580
581
		if (!$entryid || !$srcmessage) {
582
			$code = SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID;
583
			$mapiLastHresult = mapi_last_hresult();
584
			// 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
585
			if ($newfolder == GSync::GetBackend()->GetWasteBasket()) {
586
				$code = SYNC_MOVEITEMSSTATUS_SUCCESS;
587
			}
588
			$errorCase = !$entryid ? "resolve source message id" : "open source message";
589
590
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to %s: 0x%X", $sk, $newfolder, $errorCase, $mapiLastHresult), $code);
591
		}
592
593
		// check if it is in the synchronization interval and/or shared+private
594
		if (!$this->isModificationAllowed($sk)) {
595
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Source message move is not allowed. Move not performed.", $id, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
596
		}
597
598
		// get correct mapi store for the destination folder
599
		$dststore = GSync::GetBackend()->GetMAPIStoreForFolderId(GSync::GetAdditionalSyncFolderStore($newfolder), $newfolder);
600
		if ($dststore === false) {
601
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open store of destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
602
		}
603
604
		$dstentryid = mapi_msgstore_entryidfromsourcekey($dststore, hex2bin($newfolder));
605
		if (!$dstentryid) {
606
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
607
		}
608
609
		$dstfolder = mapi_msgstore_openentry($dststore, $dstentryid);
610
		if (!$dstfolder) {
611
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open destination folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDDESTID);
612
		}
613
614
		$newmessage = mapi_folder_createmessage($dstfolder);
615
		if (!$newmessage) {
616
			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);
617
		}
618
619
		// Copy message
620
		mapi_copyto($srcmessage, [], [], $newmessage);
621
		if (mapi_last_hresult()) {
622
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, copy to destination message failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
623
		}
624
625
		$srcfolderentryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid);
626
		if (!$srcfolderentryid) {
627
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to resolve source folder", $sk, $newfolder), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
628
		}
629
630
		$srcfolder = mapi_msgstore_openentry($this->store, $srcfolderentryid);
631
		if (!$srcfolder) {
632
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, unable to open source folder: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_INVALIDSOURCEID);
633
		}
634
635
		// Save changes
636
		mapi_savechanges($newmessage);
637
		if (mapi_last_hresult()) {
638
			throw new StatusException(sprintf("ImportChangesICS->ImportMessageMove('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $sk, $newfolder, mapi_last_hresult()), SYNC_MOVEITEMSSTATUS_CANNOTMOVE);
639
		}
640
641
		// Delete the old message
642
		if (!mapi_folder_deletemessages($srcfolder, [$entryid])) {
643
			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);
644
		}
645
646
		$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...
647
		if (isset($sourcekeyprops[PR_SOURCE_KEY]) && $sourcekeyprops[PR_SOURCE_KEY]) {
648
			$prefix = "";
649
			// prepend the destination short folderid, if it exists
650
			$destShortId = GSync::GetDeviceManager()->GetFolderIdForBackendId($newfolder);
651
			if ($destShortId !== $newfolder) {
652
				$prefix = $destShortId . ":";
653
			}
654
655
			return $prefix . bin2hex($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...
656
		}
657
658
		return false;
659
	}
660
661
	/*----------------------------------------------------------------------------------------------------------
662
	 * Methods for HierarchyExporter
663
	 */
664
665
	/**
666
	 * Imports a change on a folder.
667
	 *
668
	 * @param object $folder SyncFolder
669
	 *
670
	 * @return bool|SyncFolder false on error or a SyncFolder object with serverid and BackendId set (if available)
671
	 *
672
	 * @throws StatusException
673
	 */
674
	public function ImportFolderChange($folder) {
675
		$id = isset($folder->BackendId) ? $folder->BackendId : false;
676
		$parent = $folder->parentid;
677
		$parent_org = $folder->parentid;
678
		$displayname = $folder->displayname;
679
		$type = $folder->type;
680
681
		if (Utils::IsSystemFolder($type)) {
682
			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);
683
		}
684
685
		// create a new folder if $id is not set
686
		if (!$id) {
687
			// the root folder is "0" - get IPM_SUBTREE
688
			if ($parent == "0") {
689
				$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_SUBTREE_ENTRYID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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...
690
				if (GSync::GetBackend()->GetImpersonatedUser() == 'system' && isset($parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID])) {
691
					$parentfentryid = $parentprops[PR_IPM_PUBLIC_FOLDERS_ENTRYID];
692
				}
693
				elseif (isset($parentprops[PR_IPM_SUBTREE_ENTRYID])) {
694
					$parentfentryid = $parentprops[PR_IPM_SUBTREE_ENTRYID];
695
				}
696
			}
697
			else {
698
				$parentfentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($parent));
699
			}
700
701
			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...
702
				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

702
				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...
703
			}
704
705
			$parentfolder = mapi_msgstore_openentry($this->store, $parentfentryid);
706
			if (!$parentfolder) {
707
				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);
708
			}
709
710
			//  mapi_folder_createfolder() fails if a folder with this name already exists -> MAPI_E_COLLISION
711
			$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

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

794
			if (!/** @scrutinizer ignore-call */ mapi_folder_copyfolder($sourceparentfolder, $entryid, $destfolder, $displayname, FOLDER_MOVE)) {
Loading history...
Bug introduced by
The constant FOLDER_MOVE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
795
				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);
796
			}
797
798
			// 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
799
			if ($folder->parentid != 0) {
800
				$folder->parentid = GSync::GetDeviceManager()->GetFolderIdForBackendId($parent);
801
			}
802
			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($props[PR_PARENT_SOURCE_KEY]), $folder->parentid, $parent_org));
803
804
			return $folder;
805
		}
806
807
		// update the display name
808
		$props = [PR_DISPLAY_NAME => $displayname];
809
		mapi_setprops($mfolder, $props);
810
		mapi_savechanges($mfolder);
811
		if (mapi_last_hresult()) {
812
			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);
813
		}
814
815
		SLog::Write(LOGLEVEL_DEBUG, "Imported changes for folder: {$id}");
816
817
		return true;
818
	}
819
820
	/**
821
	 * Imports a folder deletion.
822
	 *
823
	 * @param SyncFolder $folder at least "serverid" needs to be set
824
	 *
825
	 * @return int SYNC_FOLDERHIERARCHY_STATUS
826
	 *
827
	 * @throws StatusException
828
	 */
829
	public function ImportFolderDeletion($folder) {
830
		$id = $folder->BackendId;
831
		$parent = isset($folder->parentid) ? $folder->parentid : false;
832
		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

832
		SLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): importing folder deletetion", $id, /** @scrutinizer ignore-type */ $parent));
Loading history...
833
834
		$folderentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($id));
835
		if (!$folderentryid) {
836
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error, unable to resolve folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_FOLDERDOESNOTEXIST);
837
		}
838
839
		// get the folder type from the MAPIProvider
840
		$type = $this->mapiprovider->GetFolderType($folderentryid);
841
842
		if (Utils::IsSystemFolder($type) || $this->mapiprovider->IsMAPIDefaultFolder($folderentryid)) {
843
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting system/default folder", $id, $parent), SYNC_FSSTATUS_SYSTEMFOLDER);
844
		}
845
846
		$ret = mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin($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

846
		$ret = /** @scrutinizer ignore-call */ mapi_importhierarchychanges_importfolderdeletion($this->importer, 0, [PR_SOURCE_KEY => hex2bin($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...
847
		if (!$ret) {
848
			throw new StatusException(sprintf("ImportChangesICS->ImportFolderDeletion('%s','%s'): Error deleting folder: 0x%X", $id, $parent, mapi_last_hresult()), SYNC_FSSTATUS_SERVERERROR);
849
		}
850
851
		return $ret;
852
	}
853
}
854