Passed
Push — master ( a034e0...fb1aac )
by
unknown
03:23
created

GSync::GetBackend()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

887
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::CommandNeedsPlainInput(%d): %s", $commandCode, Utils::PrintAsString(/** @scrutinizer ignore-type */ $stat)));
Loading history...
888
889
		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...
890
	}
891
892
	/**
893
	 * Indicates if the command to be executed operates on the hierarchy.
894
	 *
895
	 * @param int $commandCode
896
	 *
897
	 * @return bool
898
	 */
899
	public static function HierarchyCommand($commandCode) {
900
		$stat = self::checkCommandOptions($commandCode, self::HIERARCHYCOMMAND);
901
		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

901
		SLog::Write(LOGLEVEL_DEBUG, sprintf("GSync::HierarchyCommand(%d): %s", $commandCode, Utils::PrintAsString(/** @scrutinizer ignore-type */ $stat)));
Loading history...
902
903
		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...
904
	}
905
906
	/**
907
	 * Checks access types of a command.
908
	 *
909
	 * @param string $commandCode a commandCode
910
	 * @param string $option      e.g. self::UNAUTHENTICATED
911
	 *
912
	 * @return object StateMachine
913
	 *
914
	 * @throws FatalNotImplementedException
915
	 */
916
	private static function checkCommandOptions($commandCode, $option) {
917
		if ($commandCode === false) {
0 ignored issues
show
introduced by
The condition $commandCode === false is always false.
Loading history...
918
			return false;
919
		}
920
921
		if (!array_key_exists($commandCode, self::$supportedCommands)) {
922
			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

922
			throw new FatalNotImplementedException(sprintf("Command '%s' is not supported", Utils::GetCommandFromCode(/** @scrutinizer ignore-type */ $commandCode)));
Loading history...
923
		}
924
925
		$capa = self::$supportedCommands[$commandCode];
926
		$defcapa = in_array($option, $capa, true);
927
928
		// if not looking for a default capability, check if the command is supported since a previous AS version
929
		if (!$defcapa) {
930
			$verkey = array_search($option, self::$supportedASVersions, true);
931
			if ($verkey !== false && ($verkey >= array_search($capa[0], self::$supportedASVersions))) {
932
				$defcapa = true;
933
			}
934
		}
935
936
		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...
937
	}
938
}
939