GSync   F
last analyzed

Complexity

Total Complexity 157

Size/Duplication

Total Lines 914
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 394
c 1
b 0
f 0
dl 0
loc 914
rs 2
wmc 157

30 Methods

Rating   Name   Duplication   Size   Complexity  
A GetAdditionalSyncFolderStore() 0 13 3
A GetServerHeader() 0 6 2
A ReplyCatchHasChange() 0 9 2
A getDefaultFolderTypeFromFolderClass() 0 4 1
A GetLatestStateVersion() 0 2 1
C getAddSyncFolders() 0 41 16
A GetStateMachine() 0 15 4
A GetDeviceManager() 0 6 3
A ReplyCatchMark() 0 5 2
A GetBackend() 0 8 2
A CommandNeedsPlainInput() 0 5 1
A GetProvisioningManager() 0 6 2
A PrintGrommunioSyncLegal() 0 20 3
A CommandNeedsProvisioning() 0 5 1
A HierarchyCommand() 0 5 1
A GetLatestSupportedASVersion() 0 2 1
A GetSupportedCommands() 0 14 4
A GetAdditionalSyncFolders() 0 16 4
A GetRequestHandlerForCommand() 0 18 5
A getSyncObjectFromFolderClass() 0 11 3
A GetSupportedASVersion() 0 7 2
F CheckAdvancedConfig() 0 83 38
A CommandNeedsAuthentication() 0 5 1
A GetRedis() 0 6 2
F CheckConfig() 0 109 34
A GetFolderClassFromFolderType() 0 12 4
A IncludeBackend() 0 28 5
A checkCommandOptions() 0 21 6
A GetSupportedProtocolVersions() 0 9 2
A GetTopCollector() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like GSync 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 GSync, 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
 * Core functionalities
8
 */
9
10
class GSync {
11
	public const UNAUTHENTICATED = 1;
12
	public const UNPROVISIONED = 2;
13
	public const NOACTIVESYNCCOMMAND = 3;
14
	public const WEBSERVICECOMMAND = 4;    // DEPRECATED
15
	public const HIERARCHYCOMMAND = 5;
16
	public const PLAININPUT = 6;
17
	public const REQUESTHANDLER = 7;
18
	public const CLASS_NAME = 1;
19
	public const CLASS_REQUIRESPROTOCOLVERSION = 2;
20
	public const CLASS_DEFAULTTYPE = 3;
21
	public const CLASS_OTHERTYPES = 4;
22
23
	// AS versions
24
	public const ASV_1 = "1.0";
25
	public const ASV_2 = "2.0";
26
	public const ASV_21 = "2.1";
27
	public const ASV_25 = "2.5";
28
	public const ASV_12 = "12.0";
29
	public const ASV_121 = "12.1";
30
	public const ASV_14 = "14.0";
31
	public const ASV_141 = "14.1";
32
	public const ASV_16 = "16.0";
33
	public const ASV_161 = "16.1";
34
35
	/**
36
	 * Command codes for base64 encoded requests (AS >= 12.1).
37
	 */
38
	public const COMMAND_SYNC = 0;
39
	public const COMMAND_SENDMAIL = 1;
40
	public const COMMAND_SMARTFORWARD = 2;
41
	public const COMMAND_SMARTREPLY = 3;
42
	public const COMMAND_GETATTACHMENT = 4;
43
	public const COMMAND_FOLDERSYNC = 9;
44
	public const COMMAND_FOLDERCREATE = 10;
45
	public const COMMAND_FOLDERDELETE = 11;
46
	public const COMMAND_FOLDERUPDATE = 12;
47
	public const COMMAND_MOVEITEMS = 13;
48
	public const COMMAND_GETITEMESTIMATE = 14;
49
	public const COMMAND_MEETINGRESPONSE = 15;
50
	public const COMMAND_SEARCH = 16;
51
	public const COMMAND_SETTINGS = 17;
52
	public const COMMAND_PING = 18;
53
	public const COMMAND_ITEMOPERATIONS = 19;
54
	public const COMMAND_PROVISION = 20;
55
	public const COMMAND_RESOLVERECIPIENTS = 21;
56
	public const COMMAND_VALIDATECERT = 22;
57
	public const COMMAND_FIND = 23;
58
59
	// Deprecated commands
60
	public const COMMAND_GETHIERARCHY = -1;
61
	public const COMMAND_CREATECOLLECTION = -2;
62
	public const COMMAND_DELETECOLLECTION = -3;
63
	public const COMMAND_MOVECOLLECTION = -4;
64
	public const COMMAND_NOTIFY = -5;
65
66
	// Latest supported State version
67
	public const STATE_VERSION = IStateMachine::STATEVERSION_02;
68
69
	// Versions 1.0, 2.0, 2.1 and 2.5 are deprecated
70
	private static $supportedASVersions = [
71
		self::ASV_12,
72
		self::ASV_121,
73
		self::ASV_14,
74
		self::ASV_141,
75
		self::ASV_16,
76
		self::ASV_161,
77
	];
78
79
	private static $supportedCommands = [
80
		// COMMAND             AS VERSION   REQUESTHANDLER                                  OTHER SETTINGS
81
		self::COMMAND_SYNC => [self::ASV_1, self::REQUESTHANDLER => "Sync"],
82
		self::COMMAND_SENDMAIL => [self::ASV_1, self::REQUESTHANDLER => "SendMail"],
83
		self::COMMAND_SMARTFORWARD => [self::ASV_1, self::REQUESTHANDLER => "SendMail"],
84
		self::COMMAND_SMARTREPLY => [self::ASV_1, self::REQUESTHANDLER => "SendMail"],
85
		self::COMMAND_GETATTACHMENT => [self::ASV_1, self::REQUESTHANDLER => "GetAttachment"],
86
		self::COMMAND_GETHIERARCHY => [self::ASV_1, self::REQUESTHANDLER => "GetHierarchy", self::HIERARCHYCOMMAND], // deprecated but implemented
87
		self::COMMAND_CREATECOLLECTION => [self::ASV_1], // deprecated & not implemented
88
		self::COMMAND_DELETECOLLECTION => [self::ASV_1], // deprecated & not implemented
89
		self::COMMAND_MOVECOLLECTION => [self::ASV_1], // deprecated & not implemented
90
		self::COMMAND_FOLDERSYNC => [self::ASV_2, self::REQUESTHANDLER => "FolderSync", self::HIERARCHYCOMMAND],
91
		self::COMMAND_FOLDERCREATE => [self::ASV_2, self::REQUESTHANDLER => "FolderChange", self::HIERARCHYCOMMAND],
92
		self::COMMAND_FOLDERDELETE => [self::ASV_2, self::REQUESTHANDLER => "FolderChange", self::HIERARCHYCOMMAND],
93
		self::COMMAND_FOLDERUPDATE => [self::ASV_2, self::REQUESTHANDLER => "FolderChange", self::HIERARCHYCOMMAND],
94
		self::COMMAND_MOVEITEMS => [self::ASV_1, self::REQUESTHANDLER => "MoveItems"],
95
		self::COMMAND_GETITEMESTIMATE => [self::ASV_1, self::REQUESTHANDLER => "GetItemEstimate"],
96
		self::COMMAND_MEETINGRESPONSE => [self::ASV_1, self::REQUESTHANDLER => "MeetingResponse"],
97
		self::COMMAND_RESOLVERECIPIENTS => [self::ASV_1, self::REQUESTHANDLER => "ResolveRecipients"],
98
		self::COMMAND_VALIDATECERT => [self::ASV_1, self::REQUESTHANDLER => "ValidateCert"],
99
		self::COMMAND_PROVISION => [self::ASV_25, self::REQUESTHANDLER => "Provisioning", self::UNAUTHENTICATED, self::UNPROVISIONED],
100
		self::COMMAND_SEARCH => [self::ASV_1, self::REQUESTHANDLER => "Search"],
101
		self::COMMAND_PING => [self::ASV_2, self::REQUESTHANDLER => "Ping", self::UNPROVISIONED],
102
		self::COMMAND_NOTIFY => [self::ASV_1, self::REQUESTHANDLER => "Notify"], // deprecated & not implemented
103
		self::COMMAND_ITEMOPERATIONS => [self::ASV_12, self::REQUESTHANDLER => "ItemOperations"],
104
		self::COMMAND_SETTINGS => [self::ASV_12, self::REQUESTHANDLER => "Settings"],
105
		self::COMMAND_FIND => [self::ASV_161, self::REQUESTHANDLER => "Find"],
106
	];
107
108
	private static $classes = [
109
		"Email" => [
110
			self::CLASS_NAME => "SyncMail",
111
			self::CLASS_REQUIRESPROTOCOLVERSION => false,
112
			self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_INBOX,
113
			self::CLASS_OTHERTYPES => [
114
				SYNC_FOLDER_TYPE_OTHER,
115
				SYNC_FOLDER_TYPE_DRAFTS,
116
				SYNC_FOLDER_TYPE_WASTEBASKET,
117
				SYNC_FOLDER_TYPE_SENTMAIL,
118
				SYNC_FOLDER_TYPE_OUTBOX,
119
				SYNC_FOLDER_TYPE_USER_MAIL,
120
				SYNC_FOLDER_TYPE_JOURNAL,
121
				SYNC_FOLDER_TYPE_USER_JOURNAL,
122
			],
123
		],
124
		"Contacts" => [
125
			self::CLASS_NAME => "SyncContact",
126
			self::CLASS_REQUIRESPROTOCOLVERSION => true,
127
			self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_CONTACT,
128
			self::CLASS_OTHERTYPES => [SYNC_FOLDER_TYPE_USER_CONTACT, SYNC_FOLDER_TYPE_UNKNOWN],
129
		],
130
		"Calendar" => [
131
			self::CLASS_NAME => "SyncAppointment",
132
			self::CLASS_REQUIRESPROTOCOLVERSION => false,
133
			self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_APPOINTMENT,
134
			self::CLASS_OTHERTYPES => [SYNC_FOLDER_TYPE_USER_APPOINTMENT],
135
		],
136
		"Tasks" => [
137
			self::CLASS_NAME => "SyncTask",
138
			self::CLASS_REQUIRESPROTOCOLVERSION => false,
139
			self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_TASK,
140
			self::CLASS_OTHERTYPES => [SYNC_FOLDER_TYPE_USER_TASK],
141
		],
142
		"Notes" => [
143
			self::CLASS_NAME => "SyncNote",
144
			self::CLASS_REQUIRESPROTOCOLVERSION => false,
145
			self::CLASS_DEFAULTTYPE => SYNC_FOLDER_TYPE_NOTE,
146
			self::CLASS_OTHERTYPES => [SYNC_FOLDER_TYPE_USER_NOTE],
147
		],
148
	];
149
150
	private static $stateMachine;
151
	private static $deviceManager;
152
	private static $provisioningManager;
153
	private static $topCollector;
154
	private static $backend;
155
	private static $addSyncFolders;
156
	private static $policies;
0 ignored issues
show
introduced by
The private property $policies is not used, and could be removed.
Loading history...
157
	private static $redis;
158
159
	/**
160
	 * Verifies configuration.
161
	 *
162
	 * @return bool
163
	 *
164
	 * @throws FatalMisconfigurationException
165
	 */
166
	public static function CheckConfig() {
167
		// check the php version
168
		if (version_compare(phpversion(), '5.4.0') < 0) {
169
			throw new FatalException("The configured PHP version is too old. Please make sure at least PHP 5.4 is used.");
170
		}
171
172
		// some basic checks
173
		if (!defined('BASE_PATH')) {
174
			throw new FatalMisconfigurationException("The BASE_PATH is not configured. Check if the config.php file is in place.");
175
		}
176
177
		if (substr(BASE_PATH, -1, 1) != "/") {
178
			throw new FatalMisconfigurationException("The BASE_PATH should terminate with a '/'");
179
		}
180
181
		if (!file_exists(BASE_PATH)) {
182
			throw new FatalMisconfigurationException("The configured BASE_PATH does not exist or can not be accessed.");
183
		}
184
185
		if (defined('BASE_PATH_CLI') && file_exists(BASE_PATH_CLI)) {
186
			define('REAL_BASE_PATH', BASE_PATH_CLI);
187
		}
188
		else {
189
			define('REAL_BASE_PATH', BASE_PATH);
190
		}
191
192
		if (!defined('LOGBACKEND')) {
193
			define('LOGBACKEND', 'filelog');
194
		}
195
196
		if (strtolower(LOGBACKEND) == 'syslog') {
0 ignored issues
show
introduced by
The condition strtolower(LOGBACKEND) == 'syslog' is always false.
Loading history...
197
			define('LOGBACKEND_CLASS', 'Syslog');
198
			if (!defined('LOG_SYSLOG_FACILITY')) {
199
				define('LOG_SYSLOG_FACILITY', LOG_LOCAL0);
200
			}
201
202
			if (!defined('LOG_SYSLOG_HOST')) {
203
				define('LOG_SYSLOG_HOST', false);
204
			}
205
206
			if (!defined('LOG_SYSLOG_PORT')) {
207
				define('LOG_SYSLOG_PORT', 514);
208
			}
209
210
			if (!defined('LOG_SYSLOG_PROGRAM')) {
211
				define('LOG_SYSLOG_PROGRAM', 'grommunio-sync');
212
			}
213
214
			if (!is_numeric(LOG_SYSLOG_PORT)) {
215
				throw new FatalMisconfigurationException("The LOG_SYSLOG_PORT must a be a number.");
216
			}
217
218
			if (LOG_SYSLOG_HOST && LOG_SYSLOG_PORT <= 0) {
219
				throw new FatalMisconfigurationException("LOG_SYSLOG_HOST is defined but the LOG_SYSLOG_PORT does not seem to be valid.");
220
			}
221
		}
222
		elseif (strtolower(LOGBACKEND) == 'filelog') {
0 ignored issues
show
introduced by
The condition strtolower(LOGBACKEND) == 'filelog' is always true.
Loading history...
223
			define('LOGBACKEND_CLASS', 'FileLog');
224
			if (!defined('LOGFILEDIR')) {
225
				throw new FatalMisconfigurationException("The LOGFILEDIR is not configured. Check if the config.php file is in place.");
226
			}
227
228
			if (substr(LOGFILEDIR, -1, 1) != "/") {
229
				throw new FatalMisconfigurationException("The LOGFILEDIR should terminate with a '/'");
230
			}
231
232
			if (!file_exists(LOGFILEDIR)) {
233
				throw new FatalMisconfigurationException("The configured LOGFILEDIR does not exist or can not be accessed.");
234
			}
235
236
			if ((!file_exists(LOGFILE) && !touch(LOGFILE)) || !is_writable(LOGFILE)) {
237
				throw new FatalMisconfigurationException("The configured LOGFILE can not be modified.");
238
			}
239
240
			if ((!file_exists(LOGERRORFILE) && !touch(LOGERRORFILE)) || !is_writable(LOGERRORFILE)) {
241
				throw new FatalMisconfigurationException("The configured LOGERRORFILE can not be modified.");
242
			}
243
		}
244
		else {
245
			define('LOGBACKEND_CLASS', LOGBACKEND);
246
		}
247
248
		// set time zone
249
		// code contributed by Robert Scheck (rsc)
250
		if (defined('TIMEZONE') ? constant('TIMEZONE') : false) {
251
			if (!@date_default_timezone_set(TIMEZONE)) {
252
				throw new FatalMisconfigurationException(sprintf("The configured TIMEZONE '%s' is not valid. Please check supported timezones at http://www.php.net/manual/en/timezones.php", constant('TIMEZONE')));
253
			}
254
		}
255
		elseif (!ini_get('date.timezone')) {
256
			date_default_timezone_set('Europe/Vienna');
257
		}
258
259
		if (defined('USE_X_FORWARDED_FOR_HEADER')) {
260
			SLog::Write(LOGLEVEL_INFO, "The configuration parameter 'USE_X_FORWARDED_FOR_HEADER' was deprecated in favor of 'USE_CUSTOM_REMOTE_IP_HEADER'. Please update your configuration.");
261
		}
262
263
		// check redis configuration - set defaults
264
		if (!defined('REDIS_HOST')) {
265
			define('REDIS_HOST', 'localhost');
266
		}
267
		if (!defined('REDIS_PORT')) {
268
			define('REDIS_PORT', 6379);
269
		}
270
		if (!defined('REDIS_AUTH')) {
271
			define('REDIS_AUTH', '');
272
		}
273
274
		return true;
275
	}
276
277
	/**
278
	 * Verifies Timezone, StateMachine and Backend configuration.
279
	 *
280
	 * @return bool
281
	 *
282
	 * @throws FatalMisconfigurationException
283
	 */
284
	public static function CheckAdvancedConfig() {
285
		global $specialLogUsers, $additionalFolders;
286
287
		if (!is_array($specialLogUsers)) {
288
			throw new FatalMisconfigurationException("The WBXML log users is not an array.");
289
		}
290
291
		if (!defined('SYNC_CONTACTS_MAXPICTURESIZE')) {
292
			define('SYNC_CONTACTS_MAXPICTURESIZE', 49152);
293
		}
294
		elseif (!is_int(SYNC_CONTACTS_MAXPICTURESIZE) || SYNC_CONTACTS_MAXPICTURESIZE < 1) {
0 ignored issues
show
introduced by
The condition is_int(SYNC_CONTACTS_MAXPICTURESIZE) is always true.
Loading history...
295
			throw new FatalMisconfigurationException("The SYNC_CONTACTS_MAXPICTURESIZE value must be a number higher than 0.");
296
		}
297
298
		if (!defined('USE_PARTIAL_FOLDERSYNC')) {
299
			define('USE_PARTIAL_FOLDERSYNC', false);
300
		}
301
302
		if (!defined('PING_LOWER_BOUND_LIFETIME')) {
303
			define('PING_LOWER_BOUND_LIFETIME', false);
304
		}
305
		elseif (PING_LOWER_BOUND_LIFETIME !== false && (!is_int(PING_LOWER_BOUND_LIFETIME) || PING_LOWER_BOUND_LIFETIME < 1 || PING_LOWER_BOUND_LIFETIME > 3540)) {
0 ignored issues
show
introduced by
The condition PING_LOWER_BOUND_LIFETIME !== false is always false.
Loading history...
306
			throw new FatalMisconfigurationException("The PING_LOWER_BOUND_LIFETIME value must be 'false' or a number between 1 and 3540 inclusively.");
307
		}
308
		if (!defined('PING_HIGHER_BOUND_LIFETIME')) {
309
			define('PING_HIGHER_BOUND_LIFETIME', false);
310
		}
311
		elseif (PING_HIGHER_BOUND_LIFETIME !== false && (!is_int(PING_HIGHER_BOUND_LIFETIME) || PING_HIGHER_BOUND_LIFETIME < 1 || PING_HIGHER_BOUND_LIFETIME > 3540)) {
0 ignored issues
show
introduced by
The condition PING_HIGHER_BOUND_LIFETIME !== false is always false.
Loading history...
312
			throw new FatalMisconfigurationException("The PING_HIGHER_BOUND_LIFETIME value must be 'false' or a number between 1 and 3540 inclusively.");
313
		}
314
		if (PING_HIGHER_BOUND_LIFETIME !== false && PING_LOWER_BOUND_LIFETIME !== false && PING_HIGHER_BOUND_LIFETIME < PING_LOWER_BOUND_LIFETIME) {
0 ignored issues
show
introduced by
The condition PING_HIGHER_BOUND_LIFETIME !== false is always false.
Loading history...
315
			throw new FatalMisconfigurationException("The PING_HIGHER_BOUND_LIFETIME value must be greater or equal to PING_LOWER_BOUND_LIFETIME.");
316
		}
317
318
		if (!defined('RETRY_AFTER_DELAY')) {
319
			define('RETRY_AFTER_DELAY', 300);
320
		}
321
		elseif (RETRY_AFTER_DELAY !== false && (!is_int(RETRY_AFTER_DELAY) || RETRY_AFTER_DELAY < 1)) {
0 ignored issues
show
introduced by
The condition is_int(RETRY_AFTER_DELAY) is always true.
Loading history...
322
			throw new FatalMisconfigurationException("The RETRY_AFTER_DELAY value must be 'false' or a number greater than 0.");
323
		}
324
325
		// set Grommunio backend defaults if not set
326
		if (!defined('MAPI_SERVER')) {
327
			define('MAPI_SERVER', 'default:');
328
		}
329
		if (!defined('STORE_STATE_FOLDER')) {
330
			define('STORE_STATE_FOLDER', 'GS-SyncState');
331
		}
332
333
		// the check on additional folders will not throw hard errors, as this is probably changed on live systems
334
		if (isset($additionalFolders) && !is_array($additionalFolders)) {
335
			SLog::Write(LOGLEVEL_ERROR, "GSync::CheckConfig(): The additional folders synchronization not available as array.");
336
		}
337
		else {
338
			// check configured data
339
			foreach ($additionalFolders as $af) {
340
				if (!is_array($af) || !isset($af['store']) || !isset($af['folderid']) || !isset($af['name']) || !isset($af['type'])) {
341
					SLog::Write(LOGLEVEL_ERROR, "GSync::CheckConfig(): the additional folder synchronization is not configured correctly. Missing parameters. Entry will be ignored.");
342
343
					continue;
344
				}
345
346
				if ($af['store'] == "" || $af['folderid'] == "" || $af['name'] == "" || $af['type'] == "") {
347
					SLog::Write(LOGLEVEL_WARN, "GSync::CheckConfig(): the additional folder synchronization is not configured correctly. Empty parameters. Entry will be ignored.");
348
349
					continue;
350
				}
351
352
				if (!in_array($af['type'], [SYNC_FOLDER_TYPE_USER_NOTE, SYNC_FOLDER_TYPE_USER_CONTACT, SYNC_FOLDER_TYPE_USER_APPOINTMENT, SYNC_FOLDER_TYPE_USER_TASK, SYNC_FOLDER_TYPE_USER_MAIL])) {
353
					SLog::Write(LOGLEVEL_ERROR, sprintf("GSync::CheckConfig(): the type of the additional synchronization folder '%s is not permitted.", $af['name']));
354
355
					continue;
356
				}
357
				// the data will be initialized when used via self::getAddFolders()
358
			}
359
		}
360
361
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Used timezone '%s'", date_default_timezone_get()));
362
363
		// get the statemachine, which will also try to load the backend.. This could throw errors
364
		self::GetStateMachine();
365
366
		return true;
367
	}
368
369
	/**
370
	 * Returns the StateMachine object
371
	 * which has to be an IStateMachine implementation.
372
	 *
373
	 * @return object implementation of IStateMachine
374
	 *
375
	 * @throws FatalNotImplementedException
376
	 * @throws HTTPReturnCodeException
377
	 */
378
	public static function GetStateMachine() {
379
		if (!isset(GSync::$stateMachine)) {
380
			// the backend could also return an own IStateMachine implementation
381
			GSync::$stateMachine = self::GetBackend()->GetStateMachine();
382
383
			if (GSync::$stateMachine->GetStateVersion() !== GSync::GetLatestStateVersion()) {
384
				if (class_exists("TopCollector")) {
385
					self::GetTopCollector()->AnnounceInformation("Run migration script!", true);
386
				}
387
388
				throw new ServiceUnavailableException(sprintf("The state version available to the %s is not the latest version - please run the state upgrade script. See release notes for more information.", get_class(GSync::$stateMachine)));
389
			}
390
		}
391
392
		return GSync::$stateMachine;
393
	}
394
395
	/**
396
	 * Marks an arbitrary ID for a user and device for 2 seconds.
397
	 *
398
	 * @param $id $mixed
0 ignored issues
show
Documentation Bug introduced by
The doc comment $id at position 0 could not be parsed: Unknown type name '$id' at position 0 in $id.
Loading history...
399
	 */
400
	public static function ReplyCatchMark($id) {
401
		$k = sprintf("grommunio-sync:rb-%s-%s-%s", Request::GetAuthUserString(), Request::GetDeviceID(), $id);
402
		$r = GSync::GetRedis();
403
		if ($r) {
0 ignored issues
show
introduced by
$r is of type object, thus it always evaluated to true.
Loading history...
404
			$r->setKey($k, true, 2); // 2 sec timeout!
405
		}
406
	}
407
408
	/**
409
	 * Returns if an arbitrary ID for a user and device was marked in the last 2 seconds.
410
	 *
411
	 * @param $id $mixed
0 ignored issues
show
Documentation Bug introduced by
The doc comment $id at position 0 could not be parsed: Unknown type name '$id' at position 0 in $id.
Loading history...
412
	 *
413
	 * @return bool
414
	 */
415
	public static function ReplyCatchHasChange($id) {
416
		$k = sprintf("grommunio-sync:rb-%s-%s-%s", Request::GetAuthUserString(), Request::GetDeviceID(), $id);
417
418
		$r = GSync::GetRedis();
419
		if ($r) {
0 ignored issues
show
introduced by
$r is of type object, thus it always evaluated to true.
Loading history...
420
			return $r->get()->exists($k);
421
		}
422
423
		return false;
424
	}
425
426
	/**
427
	 * Returns the Redis object.
428
	 *
429
	 * @return object Redis
430
	 */
431
	public static function GetRedis() {
432
		if (!isset(GSync::$redis)) {
433
			GSync::$redis = new RedisConnection();
434
		}
435
436
		return GSync::$redis;
437
	}
438
439
	/**
440
	 * Returns the latest version of supported states.
441
	 *
442
	 * @return int
443
	 */
444
	public static function GetLatestStateVersion() {
445
		return self::STATE_VERSION;
446
	}
447
448
	/**
449
	 * Returns the ProvisioningManager object.
450
	 *
451
	 * @return object ProvisioningManager
452
	 */
453
	public static function GetProvisioningManager() {
454
		if (!isset(self::$provisioningManager)) {
455
			self::$provisioningManager = new ProvisioningManager();
456
		}
457
458
		return self::$provisioningManager;
459
	}
460
461
	/**
462
	 * Returns the DeviceManager object.
463
	 *
464
	 * @param bool $initialize (opt) default true: initializes the DeviceManager if not already done
465
	 *
466
	 * @return object DeviceManager
467
	 */
468
	public static function GetDeviceManager($initialize = true) {
469
		if (!isset(GSync::$deviceManager) && $initialize) {
470
			GSync::$deviceManager = new DeviceManager();
471
		}
472
473
		return GSync::$deviceManager;
474
	}
475
476
	/**
477
	 * Returns the Top data collector object.
478
	 *
479
	 * @return object TopCollector
480
	 */
481
	public static function GetTopCollector() {
482
		if (!isset(GSync::$topCollector)) {
483
			GSync::$topCollector = new TopCollector();
484
		}
485
486
		return GSync::$topCollector;
487
	}
488
489
	/**
490
	 * Loads a backend file.
491
	 *
492
	 * @param string $backendname
493
	 *
494
	 * @return bool
495
	 *
496
	 * @throws FatalNotImplementedException
497
	 */
498
	public static function IncludeBackend($backendname) {
499
		if ($backendname == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $backendname of type string to the boolean false. If you are specifically checking for an empty string, consider using the more explicit === '' instead.
Loading history...
500
			return false;
501
		}
502
503
		$backendname = strtolower($backendname);
504
		if (substr($backendname, 0, 7) !== 'backend') {
505
			throw new FatalNotImplementedException(sprintf("Backend '%s' is not allowed", $backendname));
506
		}
507
508
		$rbn = substr($backendname, 7);
509
510
		$subdirbackend = REAL_BASE_PATH . "backend/" . $rbn . "/" . $rbn . ".php";
511
		$stdbackend = REAL_BASE_PATH . "backend/" . $rbn . ".php";
512
513
		if (is_file($subdirbackend)) {
514
			$toLoad = $subdirbackend;
515
		}
516
		elseif (is_file($stdbackend)) {
517
			$toLoad = $stdbackend;
518
		}
519
		else {
520
			return false;
521
		}
522
523
		SLog::Write(LOGLEVEL_DEBUG, sprintf("Including backend file: '%s'", $toLoad));
524
525
		return include_once $toLoad;
526
	}
527
528
	/**
529
	 * Returns the Backend for this request
530
	 * the backend has to be an IBackend implementation.
531
	 *
532
	 * @return object IBackend implementation
533
	 */
534
	public static function GetBackend() {
535
		// if the backend is not yet loaded, load backend drivers and instantiate it
536
		if (!isset(GSync::$backend)) {
537
			// Initialize Grommunio
538
			GSync::$backend = new Grommunio();
539
		}
540
541
		return GSync::$backend;
542
	}
543
544
	/**
545
	 * Returns additional folder objects which should be synchronized to the device.
546
	 *
547
	 * @param bool $backendIdsAsKeys if true the keys are backendids else folderids, default: true
548
	 *
549
	 * @return array
550
	 */
551
	public static function GetAdditionalSyncFolders($backendIdsAsKeys = true) {
552
		// get user based folders which should be synchronized
553
		$userFolder = self::GetDeviceManager()->GetAdditionalUserSyncFolders();
554
		$addfolders = self::getAddSyncFolders() + $userFolder;
555
		// if requested, we rewrite the backendids to folderids here
556
		if ($backendIdsAsKeys === false && !empty($addfolders)) {
557
			SLog::Write(LOGLEVEL_DEBUG, "GSync::GetAdditionalSyncFolders(): Requested AS folderids as keys for additional folders array, converting");
558
			$faddfolders = [];
559
			foreach ($addfolders as $backendId => $addFolder) {
560
				$fid = self::GetDeviceManager()->GetFolderIdForBackendId($backendId);
561
				$faddfolders[$fid] = $addFolder;
562
			}
563
			$addfolders = $faddfolders;
564
		}
565
566
		return $addfolders;
567
	}
568
569
	/**
570
	 * Returns the store for an additional folder.
571
	 *
572
	 * @param string $backendid
573
	 * @param bool   $noDebug   (opt) by default, debug message is shown
574
	 *
575
	 * @return string
576
	 */
577
	public static function GetAdditionalSyncFolderStore($backendid, $noDebug = false) {
578
		if (isset(self::getAddSyncFolders()[$backendid]->Store)) {
579
			$val = self::getAddSyncFolders()[$backendid]->Store;
580
		}
581
		else {
582
			$val = self::GetDeviceManager()->GetAdditionalUserSyncFolderStore($backendid);
583
		}
584
585
		if (!$noDebug) {
586
			SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::GetAdditionalSyncFolderStore('%s'): '%s'", $backendid, Utils::PrintAsString($val)));
587
		}
588
589
		return $val;
590
	}
591
592
	/**
593
	 * Returns a SyncObject class name for a folder class.
594
	 *
595
	 * @param string $folderclass
596
	 *
597
	 * @return string
598
	 *
599
	 * @throws FatalNotImplementedException
600
	 */
601
	public static function getSyncObjectFromFolderClass($folderclass) {
602
		if (!isset(self::$classes[$folderclass])) {
603
			throw new FatalNotImplementedException("Class '{$folderclass}' is not supported");
604
		}
605
606
		$class = self::$classes[$folderclass][self::CLASS_NAME];
607
		if (self::$classes[$folderclass][self::CLASS_REQUIRESPROTOCOLVERSION]) {
608
			return new $class(Request::GetProtocolVersion());
0 ignored issues
show
Bug Best Practice introduced by
The expression return new $class(Request::GetProtocolVersion()) returns the type object which is incompatible with the documented return type string.
Loading history...
609
		}
610
611
		return new $class();
0 ignored issues
show
Bug Best Practice introduced by
The expression return new $class() returns the type object which is incompatible with the documented return type string.
Loading history...
612
	}
613
614
	/**
615
	 * Initializes the SyncObjects for additional folders on demand.
616
	 * Uses DeviceManager->BuildSyncFolderObject() to do patching.
617
	 *
618
	 * @return array
619
	 */
620
	private static function getAddSyncFolders() {
621
		global $additionalFolders;
622
		if (!isset(self::$addSyncFolders)) {
623
			self::$addSyncFolders = [];
624
625
			if (isset($additionalFolders) && !is_array($additionalFolders)) {
626
				SLog::Write(LOGLEVEL_ERROR, "GSync::getAddSyncFolders() : The additional folders synchronization not available as array.");
627
			}
628
			else {
629
				foreach ($additionalFolders as $af) {
630
					if (!is_array($af) || !isset($af['store']) || !isset($af['folderid']) || !isset($af['name']) || !isset($af['type'])) {
631
						SLog::Write(LOGLEVEL_ERROR, "GSync::getAddSyncFolders() : the additional folder synchronization is not configured correctly. Missing parameters. Entry will be ignored.");
632
633
						continue;
634
					}
635
636
					if ($af['store'] == "" || $af['folderid'] == "" || $af['name'] == "" || $af['type'] == "") {
637
						SLog::Write(LOGLEVEL_WARN, "GSync::getAddSyncFolders() : the additional folder synchronization is not configured correctly. Empty parameters. Entry will be ignored.");
638
639
						continue;
640
					}
641
642
					if (!in_array($af['type'], [SYNC_FOLDER_TYPE_USER_NOTE, SYNC_FOLDER_TYPE_USER_CONTACT, SYNC_FOLDER_TYPE_USER_APPOINTMENT, SYNC_FOLDER_TYPE_USER_TASK, SYNC_FOLDER_TYPE_USER_MAIL])) {
643
						SLog::Write(LOGLEVEL_ERROR, sprintf("GSync::getAddSyncFolders() : the type of the additional synchronization folder '%s is not permitted.", $af['name']));
644
645
						continue;
646
					}
647
648
					// don't fail hard if no flags are set, but we at least warn about it
649
					if (!isset($af['flags'])) {
650
						SLog::Write(LOGLEVEL_WARN, sprintf("GSync::getAddSyncFolders() : the additional folder '%s' is not configured completely. Missing 'flags' parameter, defaulting to DeviceManager::FLD_FLAGS_NONE.", $af['name']));
651
						$af['flags'] = DeviceManager::FLD_FLAGS_NONE;
652
					}
653
654
					$folder = self::GetDeviceManager()->BuildSyncFolderObject($af['store'], $af['folderid'], '0', $af['name'], $af['type'], $af['flags'], DeviceManager::FLD_ORIGIN_CONFIG);
655
					self::$addSyncFolders[$folder->BackendId] = $folder;
656
				}
657
			}
658
		}
659
660
		return self::$addSyncFolders;
661
	}
662
663
	/**
664
	 * Returns the default foldertype for a folder class.
665
	 *
666
	 * @param string $folderclass folderclass sent by the mobile
667
	 *
668
	 * @return string
669
	 */
670
	public static function getDefaultFolderTypeFromFolderClass($folderclass) {
671
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::getDefaultFolderTypeFromFolderClass('%s'): '%d'", $folderclass, self::$classes[$folderclass][self::CLASS_DEFAULTTYPE]));
672
673
		return self::$classes[$folderclass][self::CLASS_DEFAULTTYPE];
674
	}
675
676
	/**
677
	 * Returns the folder class for a foldertype.
678
	 *
679
	 * @param string $foldertype
680
	 *
681
	 * @return false|string false if no class for this type is available
682
	 */
683
	public static function GetFolderClassFromFolderType($foldertype) {
684
		$class = false;
685
		foreach (self::$classes as $aClass => $cprops) {
686
			if ($cprops[self::CLASS_DEFAULTTYPE] == $foldertype || in_array($foldertype, $cprops[self::CLASS_OTHERTYPES])) {
687
				$class = $aClass;
688
689
				break;
690
			}
691
		}
692
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::GetFolderClassFromFolderType('%s'): %s", $foldertype, Utils::PrintAsString($class)));
693
694
		return $class;
695
	}
696
697
	/**
698
	 * Prints the grommunio-sync legal header to STDOUT
699
	 * Using this breaks ActiveSync synchronization if wbxml is expected.
700
	 *
701
	 * @param string $message           (opt) message to be displayed
702
	 * @param string $additionalMessage (opt) additional message to be displayed
703
	 */
704
	public static function PrintGrommunioSyncLegal($message = "", $additionalMessage = "") {
705
		SLog::Write(LOGLEVEL_DEBUG, "GSync::PrintGrommunioSyncLegal()");
706
707
		if ($message) {
708
			$message = "<h3>" . $message . "</h3>";
709
		}
710
		if ($additionalMessage) {
711
			$additionalMessage .= "<br>";
712
		}
713
714
		header("Content-type: text/html");
715
		echo <<<END
716
        <html>
717
        <header>
718
        <title>grommunio-sync ActiveSync</title>
719
        </header>
720
        <body>
721
        <font face="verdana">
722
        <h2>grommunio-sync - Open Source ActiveSync</h2>
723
        {$message} {$additionalMessage}
724
        <br><br>
725
        More information about grommunio can be found
726
        <a href="https://grommunio.com/">at the grommunio homepage</a><br>
727
        </font>
728
        </body>
729
        </html>
730
END;
731
	}
732
733
	/**
734
	 * Indicates the latest AS version supported by grommunio-sync.
735
	 *
736
	 * @return string
737
	 */
738
	public static function GetLatestSupportedASVersion() {
739
		return end(self::$supportedASVersions);
740
	}
741
742
	/**
743
	 * Indicates which is the highest AS version supported by the backend.
744
	 *
745
	 * @return string
746
	 *
747
	 * @throws FatalNotImplementedException if the backend returns an invalid version
748
	 */
749
	public static function GetSupportedASVersion() {
750
		$version = self::GetBackend()->GetSupportedASVersion();
751
		if (!in_array($version, self::$supportedASVersions)) {
752
			throw new FatalNotImplementedException(sprintf("AS version '%s' reported by the backend is not supported", $version));
753
		}
754
755
		return $version;
756
	}
757
758
	/**
759
	 * Returns AS server header.
760
	 *
761
	 * @return string
762
	 */
763
	public static function GetServerHeader() {
764
		if (self::GetSupportedASVersion() == self::ASV_25) {
765
			return "MS-Server-ActiveSync: 6.5.7638.1";
766
		}
767
768
		return "MS-Server-ActiveSync: " . self::GetSupportedASVersion();
769
	}
770
771
	/**
772
	 * Returns AS protocol versions which are supported.
773
	 *
774
	 * @param bool $valueOnly (opt) default: false (also returns the header name)
775
	 *
776
	 * @return string
777
	 */
778
	public static function GetSupportedProtocolVersions($valueOnly = false) {
779
		$versions = implode(',', array_slice(self::$supportedASVersions, 0, array_search(self::GetSupportedASVersion(), self::$supportedASVersions) + 1));
780
		SLog::Write(LOGLEVEL_DEBUG, "GSync::GetSupportedProtocolVersions(): " . $versions);
781
782
		if ($valueOnly === true) {
783
			return $versions;
784
		}
785
786
		return "MS-ASProtocolVersions: " . $versions;
787
	}
788
789
	/**
790
	 * Returns AS commands which are supported.
791
	 *
792
	 * @return string
793
	 */
794
	public static function GetSupportedCommands() {
795
		$asCommands = [];
796
		// filter all non-activesync commands
797
		foreach (self::$supportedCommands as $c => $v) {
798
			if (!self::checkCommandOptions($c, self::NOACTIVESYNCCOMMAND) &&
799
					self::checkCommandOptions($c, self::GetSupportedASVersion())) {
800
				$asCommands[] = Utils::GetCommandFromCode($c);
801
			}
802
		}
803
804
		$commands = implode(',', $asCommands);
805
		SLog::Write(LOGLEVEL_DEBUG, "GSync::GetSupportedCommands(): " . $commands);
806
807
		return "MS-ASProtocolCommands: " . $commands;
808
	}
809
810
	/**
811
	 * Loads and instantiates a request processor for a command.
812
	 *
813
	 * @param int $commandCode
814
	 *
815
	 * @return RequestProcessor sub-class
816
	 */
817
	public static function GetRequestHandlerForCommand($commandCode) {
818
		if (!array_key_exists($commandCode, self::$supportedCommands) ||
819
				!array_key_exists(self::REQUESTHANDLER, self::$supportedCommands[$commandCode])) {
820
			throw new FatalNotImplementedException(sprintf("Command '%s' has no request handler or class", Utils::GetCommandFromCode($commandCode)));
821
		}
822
823
		$class = self::$supportedCommands[$commandCode][self::REQUESTHANDLER];
824
		$handlerclass = REAL_BASE_PATH . "lib/request/" . strtolower($class) . ".php";
825
826
		if (is_file($handlerclass)) {
827
			include $handlerclass;
828
		}
829
830
		if (class_exists($class)) {
831
			return new $class();
832
		}
833
834
		throw new FatalNotImplementedException(sprintf("Request handler '%s' can not be loaded", $class));
835
	}
836
837
	/**
838
	 * Indicates if a commands requires authentication or not.
839
	 *
840
	 * @param int $commandCode
841
	 *
842
	 * @return bool
843
	 */
844
	public static function CommandNeedsAuthentication($commandCode) {
845
		$stat = !self::checkCommandOptions($commandCode, self::UNAUTHENTICATED);
846
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::CommandNeedsAuthentication(%d): %s", $commandCode, Utils::PrintAsString($stat)));
847
848
		return $stat;
849
	}
850
851
	/**
852
	 * Indicates if the Provisioning check has to be forced on these commands.
853
	 *
854
	 * @param string $commandCode
855
	 *
856
	 * @return bool
857
	 */
858
	public static function CommandNeedsProvisioning($commandCode) {
859
		$stat = !self::checkCommandOptions($commandCode, self::UNPROVISIONED);
860
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::CommandNeedsProvisioning(%s): %s", $commandCode, Utils::PrintAsString($stat)));
861
862
		return $stat;
863
	}
864
865
	/**
866
	 * Indicates if these commands expect plain text input instead of wbxml.
867
	 *
868
	 * @param string $commandCode
869
	 *
870
	 * @return bool
871
	 */
872
	public static function CommandNeedsPlainInput($commandCode) {
873
		$stat = self::checkCommandOptions($commandCode, self::PLAININPUT);
874
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::CommandNeedsPlainInput(%d): %s", $commandCode, Utils::PrintAsString($stat)));
0 ignored issues
show
Bug introduced by
$stat of type object 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

874
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::CommandNeedsPlainInput(%d): %s", $commandCode, Utils::PrintAsString(/** @scrutinizer ignore-type */ $stat)));
Loading history...
875
876
		return $stat;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $stat returns the type object which is incompatible with the documented return type boolean.
Loading history...
877
	}
878
879
	/**
880
	 * Indicates if the command to be executed operates on the hierarchy.
881
	 *
882
	 * @param int $commandCode
883
	 *
884
	 * @return bool
885
	 */
886
	public static function HierarchyCommand($commandCode) {
887
		$stat = self::checkCommandOptions($commandCode, self::HIERARCHYCOMMAND);
888
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::HierarchyCommand(%d): %s", $commandCode, Utils::PrintAsString($stat)));
0 ignored issues
show
Bug introduced by
$stat of type object 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

888
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::HierarchyCommand(%d): %s", $commandCode, Utils::PrintAsString(/** @scrutinizer ignore-type */ $stat)));
Loading history...
889
890
		return $stat;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $stat returns the type object which is incompatible with the documented return type boolean.
Loading history...
891
	}
892
893
	/**
894
	 * Checks access types of a command.
895
	 *
896
	 * @param string $commandCode a commandCode
897
	 * @param string $option      e.g. self::UNAUTHENTICATED
898
	 *
899
	 * @return object StateMachine
900
	 *
901
	 * @throws FatalNotImplementedException
902
	 */
903
	private static function checkCommandOptions($commandCode, $option) {
904
		if ($commandCode === false) {
0 ignored issues
show
introduced by
The condition $commandCode === false is always false.
Loading history...
905
			return false;
906
		}
907
908
		if (!array_key_exists($commandCode, self::$supportedCommands)) {
909
			throw new FatalNotImplementedException(sprintf("Command '%s' is not supported", Utils::GetCommandFromCode($commandCode)));
0 ignored issues
show
Bug introduced by
$commandCode of type string is incompatible with the type integer expected by parameter $code of Utils::GetCommandFromCode(). ( Ignorable by Annotation )

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

909
			throw new FatalNotImplementedException(sprintf("Command '%s' is not supported", Utils::GetCommandFromCode(/** @scrutinizer ignore-type */ $commandCode)));
Loading history...
910
		}
911
912
		$capa = self::$supportedCommands[$commandCode];
913
		$defcapa = in_array($option, $capa, true);
914
915
		// if not looking for a default capability, check if the command is supported since a previous AS version
916
		if (!$defcapa) {
917
			$verkey = array_search($option, self::$supportedASVersions, true);
918
			if ($verkey !== false && ($verkey >= array_search($capa[0], self::$supportedASVersions))) {
919
				$defcapa = true;
920
			}
921
		}
922
923
		return $defcapa;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $defcapa returns the type boolean which is incompatible with the documented return type object.
Loading history...
924
	}
925
}
926