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

875
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::CommandNeedsPlainInput(%d): %s", $commandCode, Utils::PrintAsString(/** @scrutinizer ignore-type */ $stat)));
Loading history...
876
877
		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...
878
	}
879
880
	/**
881
	 * Indicates if the command to be executed operates on the hierarchy.
882
	 *
883
	 * @param int $commandCode
884
	 *
885
	 * @return bool
886
	 */
887
	public static function HierarchyCommand($commandCode) {
888
		$stat = self::checkCommandOptions($commandCode, self::HIERARCHYCOMMAND);
889
		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

889
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::HierarchyCommand(%d): %s", $commandCode, Utils::PrintAsString(/** @scrutinizer ignore-type */ $stat)));
Loading history...
890
891
		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...
892
	}
893
894
	/**
895
	 * Checks access types of a command.
896
	 *
897
	 * @param string $commandCode a commandCode
898
	 * @param string $option      e.g. self::UNAUTHENTICATED
899
	 *
900
	 * @return object StateMachine
901
	 *
902
	 * @throws FatalNotImplementedException
903
	 */
904
	private static function checkCommandOptions($commandCode, $option) {
905
		if ($commandCode === false) {
0 ignored issues
show
introduced by
The condition $commandCode === false is always false.
Loading history...
906
			return false;
907
		}
908
909
		if (!array_key_exists($commandCode, self::$supportedCommands)) {
910
			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

910
			throw new FatalNotImplementedException(sprintf("Command '%s' is not supported", Utils::GetCommandFromCode(/** @scrutinizer ignore-type */ $commandCode)));
Loading history...
911
		}
912
913
		$capa = self::$supportedCommands[$commandCode];
914
		$defcapa = in_array($option, $capa, true);
915
916
		// if not looking for a default capability, check if the command is supported since a previous AS version
917
		if (!$defcapa) {
918
			$verkey = array_search($option, self::$supportedASVersions, true);
919
			if ($verkey !== false && ($verkey >= array_search($capa[0], self::$supportedASVersions))) {
920
				$defcapa = true;
921
			}
922
		}
923
924
		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...
925
	}
926
}
927