Completed
Pull Request — master (#551)
by Maxence
02:13
created

ConfigService::getTrustedDomains()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * Circles - Bring cloud-users closer together.
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Maxence Lange <[email protected]>
9
 * @copyright 2017
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OCA\Circles\Service;
28
29
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21Request;
30
use daita\MySmallPhpTools\Traits\TStringTools;
31
use OCA\Circles\AppInfo\Application;
32
use OCA\Circles\Exceptions\GSStatusException;
33
use OCA\Circles\Model\DeprecatedCircle;
34
use OCP\IConfig;
35
use OCP\IRequest;
36
use OCP\IURLGenerator;
37
use OCP\PreConditionNotMetException;
38
use OCP\Util;
39
40
class ConfigService {
41
42
43
	use TStringTools;
44
45
46
	const CIRCLES_ALLOW_CIRCLES = 'allow_circles';
47
	const CIRCLES_CONTACT_BACKEND = 'contact_backend';
48
	const CIRCLES_STILL_FRONTEND = 'still_frontend';
49
	const CIRCLES_SWAP_TO_TEAMS = 'swap_to_teams';
50
	const CIRCLES_ALLOW_FEDERATED_CIRCLES = 'allow_federated';
51
	const CIRCLES_GS_ENABLED = 'gs_enabled';
52
	const CIRCLES_MEMBERS_LIMIT = 'members_limit';
53
	const CIRCLES_ACCOUNTS_ONLY = 'accounts_only';
54
	const CIRCLES_ALLOW_LINKED_GROUPS = 'allow_linked_groups';
55
	const CIRCLES_ALLOW_NON_SSL_LINKS = 'allow_non_ssl_links';
56
	const CIRCLES_NON_SSL_LOCAL = 'local_is_non_ssl';
57
	const CIRCLES_SELF_SIGNED = 'self_signed_cert';
58
	const CIRCLES_LOCAL_GSKEY = 'local_gskey';
59
	const CIRCLES_ACTIVITY_ON_CREATION = 'creation_activity';
60
	const CIRCLES_SKIP_INVITATION_STEP = 'skip_invitation_to_closed_circles';
61
	const CIRCLES_SEARCH_FROM_COLLABORATOR = 'search_from_collaborator';
62
	const CIRCLES_TEST_ASYNC_LOCK = 'test_async_lock';
63
	const CIRCLES_TEST_ASYNC_INIT = 'test_async_init';
64
	const CIRCLES_TEST_ASYNC_HAND = 'test_async_hand';
65
	const CIRCLES_TEST_ASYNC_COUNT = 'test_async_count';
66
67
	const FRONTAL_CLOUD_ID = 'frontal_cloud_id';
68
	const FRONTAL_CLOUD_SCHEME = 'frontal_cloud_scheme';
69
	const INTERNAL_CLOUD_ID = 'internal_cloud_id';
70
	const INTERNAL_CLOUD_SCHEME = 'internal_cloud_scheme';
71
72
73
74
	const FORCE_NC_BASE = 'force_nc_base';
75
	const TEST_NC_BASE = 'test_nc_base';
76
77
	const GS_ENABLED = 'enabled';
78
	const GS_MODE = 'mode';
79
	const GS_KEY = 'key';
80
	const GS_LOOKUP = 'lookup';
81
	const GS_MOCKUP = 'mockup';
82
83
	const GS_LOOKUP_INSTANCES = '/instances';
84
	const GS_LOOKUP_USERS = '/users';
85
86
87
	private $defaults = [
88
		self::CIRCLES_ALLOW_CIRCLES            => DeprecatedCircle::CIRCLES_ALL,
89
		self::CIRCLES_CONTACT_BACKEND          => '0',
90
		self::CIRCLES_STILL_FRONTEND           => '0',
91
		self::CIRCLES_TEST_ASYNC_INIT          => '0',
92
		self::CIRCLES_SWAP_TO_TEAMS            => '0',
93
		self::CIRCLES_ACCOUNTS_ONLY            => '0',
94
		self::CIRCLES_MEMBERS_LIMIT            => '50',
95
		self::CIRCLES_ALLOW_LINKED_GROUPS      => '0',
96
		self::CIRCLES_ALLOW_FEDERATED_CIRCLES  => '0',
97
		self::CIRCLES_GS_ENABLED               => '0',
98
		self::CIRCLES_LOCAL_GSKEY              => '',
99
		self::CIRCLES_ALLOW_NON_SSL_LINKS      => '0',
100
		self::CIRCLES_NON_SSL_LOCAL            => '0',
101
		self::CIRCLES_SELF_SIGNED              => '0',
102
		self::FRONTAL_CLOUD_ID                 => '',
103
		self::FRONTAL_CLOUD_SCHEME             => 'https',
104
		self::INTERNAL_CLOUD_ID                 => '',
105
		self::INTERNAL_CLOUD_SCHEME             => 'https',
106
		self::FORCE_NC_BASE                    => '',
107
		self::TEST_NC_BASE                     => '',
108
		self::CIRCLES_ACTIVITY_ON_CREATION     => '1',
109
		self::CIRCLES_SKIP_INVITATION_STEP     => '0',
110
		self::CIRCLES_SEARCH_FROM_COLLABORATOR => '0'
111
	];
112
113
	/** @var string */
114
	private $appName;
115
116
	/** @var IConfig */
117
	private $config;
118
119
	/** @var string */
120
	private $userId;
121
122
	/** @var IRequest */
123
	private $request;
124
125
	/** @var IURLGenerator */
126
	private $urlGenerator;
127
128
	/** @var MiscService */
129
	private $miscService;
130
131
	/** @var int */
132
	private $allowedCircle = -1;
133
134
	/** @var int */
135
	private $allowedLinkedGroups = -1;
136
137
	/** @var int */
138
	private $allowedFederatedCircles = -1;
139
140
	/** @var int */
141
	private $allowedNonSSLLinks = -1;
142
143
	/** @var int */
144
	private $localNonSSL = -1;
145
146
	/**
147
	 * ConfigService constructor.
148
	 *
149
	 * @param string $appName
150
	 * @param IConfig $config
151
	 * @param IRequest $request
152
	 * @param string $userId
153
	 * @param IURLGenerator $urlGenerator
154
	 * @param MiscService $miscService
155
	 */
156
	public function __construct(
157
		$appName, IConfig $config, IRequest $request, $userId, IURLGenerator $urlGenerator,
158
		MiscService $miscService
159
	) {
160
		$this->appName = $appName;
161
		$this->config = $config;
162
		$this->request = $request;
163
		$this->userId = $userId;
164
		$this->urlGenerator = $urlGenerator;
165
		$this->miscService = $miscService;
166
	}
167
168
169
	/**
170
	 * @return string
171
	 * @deprecated
172
	 */
173
	public function getLocalAddress() {
174
		return (($this->isLocalNonSSL()) ? 'http://' : '')
175
			   . $this->request->getServerHost();
176
	}
177
178
179
	/**
180
	 * returns if this type of circle is allowed by the current configuration.
181
	 *
182
	 * @param $type
183
	 *
184
	 * @return int
185
	 */
186
	public function isCircleAllowed($type) {
187
		if ($this->allowedCircle === -1) {
188
			$this->allowedCircle = (int)$this->getAppValue(self::CIRCLES_ALLOW_CIRCLES);
189
		}
190
191
		return ((int)$type & (int)$this->allowedCircle);
192
	}
193
194
195
	/**
196
	 * @return bool
197
	 * @throws GSStatusException
198
	 */
199
	public function isLinkedGroupsAllowed() {
200
		if ($this->allowedLinkedGroups === -1) {
201
			$allowed = ($this->getAppValue(self::CIRCLES_ALLOW_LINKED_GROUPS) === '1'
202
						&& !$this->getGSStatus(self::GS_ENABLED));
203
			$this->allowedLinkedGroups = ($allowed) ? 1 : 0;
204
		}
205
206
		return ($this->allowedLinkedGroups === 1);
207
	}
208
209
210
	/**
211
	 * @return bool
212
	 */
213
	public function isFederatedCirclesAllowed() {
214
		if ($this->allowedFederatedCircles === -1) {
215
			$this->allowedFederatedCircles =
216
				(int)$this->getAppValue(self::CIRCLES_ALLOW_FEDERATED_CIRCLES);
217
		}
218
219
		return ($this->allowedFederatedCircles === 1);
220
	}
221
222
	/**
223
	 * @return bool
224
	 */
225
	public function isInvitationSkipped() {
226
		return (int)$this->getAppValue(self::CIRCLES_SKIP_INVITATION_STEP) === 1;
227
	}
228
229
	/**
230
	 * @return bool
231
	 */
232
	public function isLocalNonSSL() {
233
		if ($this->localNonSSL === -1) {
234
			$this->localNonSSL =
235
				(int)$this->getAppValue(self::CIRCLES_NON_SSL_LOCAL);
236
		}
237
238
		return ($this->localNonSSL === 1);
239
	}
240
241
242
	/**
243
	 * @return bool
244
	 */
245
	public function isNonSSLLinksAllowed() {
246
		if ($this->allowedNonSSLLinks === -1) {
247
			$this->allowedNonSSLLinks =
248
				(int)$this->getAppValue(self::CIRCLES_ALLOW_NON_SSL_LINKS);
249
		}
250
251
		return ($this->allowedNonSSLLinks === 1);
252
	}
253
254
255
	/**
256
	 * @param string $remote
257
	 *
258
	 * @return string
259
	 */
260
	public function generateRemoteHost($remote) {
261
		if ((!$this->isNonSSLLinksAllowed() || strpos($remote, 'http://') !== 0)
262
			&& strpos($remote, 'https://') !== 0
263
		) {
264
			$remote = 'https://' . $remote;
265
		}
266
267
		return rtrim($remote, '/');
268
	}
269
270
271
	/**
272
	 * Get a value by key
273
	 *
274
	 * @param string $key
275
	 *
276
	 * @return string
277
	 */
278
	public function getCoreValue($key) {
279
		$defaultValue = null;
280
281
		return $this->config->getAppValue('core', $key, $defaultValue);
282
	}
283
284
	/**
285
	 * Get a value by key
286
	 *
287
	 * @param string $key
288
	 *
289
	 * @return string
290
	 */
291
	public function getSystemValue($key) {
292
		$defaultValue = null;
293
294
		return $this->config->getSystemValue($key, $defaultValue);
295
	}
296
297
298
	/**
299
	 * Get available hosts
300
	 *
301
	 * @return array
302
	 */
303
	public function getAvailableHosts(): array {
304
		return $this->config->getSystemValue('trusted_domains', []);
305
	}
306
307
308
	/**
309
	 * Get a value by key
310
	 *
311
	 * @param string $key
312
	 *
313
	 * @return string
314
	 */
315
	public function getAppValue($key) {
316
		$defaultValue = null;
317
318
		if (array_key_exists($key, $this->defaults)) {
319
			$defaultValue = $this->defaults[$key];
320
		}
321
322
		return $this->config->getAppValue($this->appName, $key, $defaultValue);
323
	}
324
325
	/**
326
	 * Set a value by key
327
	 *
328
	 * @param string $key
329
	 * @param string $value
330
	 *
331
	 * @return void
332
	 */
333
	public function setAppValue($key, $value) {
334
		$this->config->setAppValue($this->appName, $key, $value);
335
	}
336
337
	/**
338
	 * remove a key
339
	 *
340
	 * @param string $key
341
	 *
342
	 * @return string
343
	 */
344
	public function deleteAppValue($key): string {
345
		return $this->config->deleteAppValue($this->appName, $key);
346
	}
347
348
349
	/**
350
	 *
351
	 */
352
	public function unsetAppConfig() {
353
		$this->config->deleteAppValues(Application::APP_ID);
354
	}
355
356
357
	/**
358
	 * Get a user value by key
359
	 *
360
	 * @param string $key
361
	 *
362
	 * @return string
363
	 */
364
	public function getUserValue($key) {
365
		return $this->config->getUserValue($this->userId, $this->appName, $key);
366
	}
367
368
	/**
369
	 * Set a user value by key
370
	 *
371
	 * @param string $key
372
	 * @param string $value
373
	 *
374
	 * @return string
375
	 * @throws PreConditionNotMetException
376
	 */
377
	public function setUserValue($key, $value) {
378
		return $this->config->setUserValue($this->userId, $this->appName, $key, $value);
379
	}
380
381
382
	/**
383
	 * Get a user value by key and user
384
	 *
385
	 * @param string $userId
386
	 * @param string $key
387
	 *
388
	 * @param string $default
389
	 *
390
	 * @return string
391
	 */
392
	public function getCoreValueForUser($userId, $key, $default = '') {
393
		return $this->config->getUserValue($userId, 'core', $key, $default);
394
	}
395
396
397
	/**
398
	 * Get a user value by key and user
399
	 *
400
	 * @param string $userId
401
	 * @param string $key
402
	 *
403
	 * @return string
404
	 */
405
	public function getValueForUser($userId, $key) {
406
		return $this->config->getUserValue($userId, $this->appName, $key);
407
	}
408
409
	/**
410
	 * Set a user value by key
411
	 *
412
	 * @param string $userId
413
	 * @param string $key
414
	 * @param string $value
415
	 *
416
	 * @return string
417
	 * @throws PreConditionNotMetException
418
	 */
419
	public function setValueForUser($userId, $key, $value) {
420
		return $this->config->setUserValue($userId, $this->appName, $key, $value);
421
	}
422
423
	/**
424
	 * return the cloud version.
425
	 * if $complete is true, return a string x.y.z
426
	 *
427
	 * @param boolean $complete
428
	 *
429
	 * @return string|integer
430
	 */
431
	public function getCloudVersion($complete = false) {
432
		$ver = Util::getVersion();
433
434
		if ($complete) {
435
			return implode('.', $ver);
436
		}
437
438
		return $ver[0];
439
	}
440
441
442
	/**
443
	 * @return bool
444
	 */
445
	public function isAccountOnly() {
446
		return ($this->getAppValue(self::CIRCLES_ACCOUNTS_ONLY) === '1');
447
	}
448
449
450
	/**
451
	 * @return bool
452
	 */
453
	public function isContactsBackend(): bool {
454
		return ($this->getAppValue(ConfigService::CIRCLES_CONTACT_BACKEND) !== '0'
455
				&& $this->getAppValue(ConfigService::CIRCLES_CONTACT_BACKEND) !== '');
456
	}
457
458
459
	/**
460
	 * @return int
461
	 */
462
	public function contactsBackendType(): int {
463
		return (int)$this->getAppValue(ConfigService::CIRCLES_CONTACT_BACKEND);
464
	}
465
466
467
	/**
468
	 * @return bool
469
	 */
470
	public function stillFrontEnd(): bool {
471
		if (!$this->isContactsBackend()) {
472
			return true;
473
		}
474
475
		if ($this->getAppValue(self::CIRCLES_STILL_FRONTEND) === '1') {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $this->getAppValu...TILL_FRONTEND) === '1';.
Loading history...
476
			return true;
477
		}
478
479
		return false;
480
	}
481
482
483
	/**
484
	 * should the password for a mail share be send to the recipient
485
	 *
486
	 * @return bool
487
	 */
488
	public function sendPasswordByMail() {
489
		if ($this->isContactsBackend()) {
490
			return false;
491
		}
492
493
		return ($this->config->getAppValue('sharebymail', 'sendpasswordmail', 'yes') === 'yes');
494
	}
495
496
	/**
497
	 * do we require a share by mail to be password protected
498
	 *
499
	 * @param DeprecatedCircle $circle
500
	 *
501
	 * @return bool
502
	 */
503
	public function enforcePasswordProtection(DeprecatedCircle $circle) {
504
		if ($this->isContactsBackend()) {
505
			return false;
506
		}
507
508
		if ($circle->getSetting('password_enforcement') === 'true') {
509
			return true;
510
		}
511
512
		return ($this->config->getAppValue('sharebymail', 'enforcePasswordProtection', 'no') === 'yes');
513
	}
514
515
516
	/**
517
	 * @param string $type
518
	 *
519
	 * @return array|bool|mixed
520
	 * @throws GSStatusException
521
	 */
522
	public function getGSStatus(string $type = '') {
523
		$enabled = $this->config->getSystemValueBool('gs.enabled', false);
524
		$lookup = $this->config->getSystemValue('lookup_server', '');
525
		$mockup = $this->config->getSystemValue('gss.mockup', []);
526
527
		if ($lookup === '' || !$enabled) {
528
			if ($type === self::GS_ENABLED) {
529
				return false;
530
			}
531
532
			if ($type !== self::GS_MOCKUP) {
533
				throw new GSStatusException(
534
					'GS and lookup are not configured : ' . $lookup . ', ' . $enabled
535
				);
536
			}
537
		}
538
539
		$clef = $this->config->getSystemValue('gss.jwt.key', '');
540
		$mode = $this->config->getSystemValue('gss.mode', '');
541
542
		switch ($type) {
543
			case self::GS_ENABLED:
544
				return $enabled;
545
546
			case self::GS_MODE:
547
				return $mode;
548
549
			case self::GS_KEY:
550
				return $clef;
551
552
			case self::GS_LOOKUP:
553
				return $lookup;
554
555
			case self::GS_MOCKUP:
556
				return $mockup;
557
		}
558
559
		return [
560
			self::GS_ENABLED => $enabled,
561
			self::GS_LOOKUP  => $lookup,
562
			self::GS_MODE    => $clef,
563
			self::GS_KEY     => $mode,
564
		];
565
	}
566
567
568
	/**
569
	 * @return array
570
	 */
571
	public function getTrustedDomains(): array {
572
		return array_values($this->config->getSystemValue('trusted_domains', []));
573
	}
574
575
576
	/**
577
	 * - returns host+port, does not specify any protocol
578
	 * - can be forced using FRONTAL_CLOUD_ID
579
	 * - use 'overwrite.cli.url'
580
	 * - can use the first entry from trusted_domains if FRONTAL_CLOUD_ID = 'use-trusted-domain'
581
	 * - used mainly to assign instance and source to a request
582
	 * - important only in remote environment; can be totally random in a jailed environment
583
	 *
584
	 * @return string
585
	 */
586
	public function getFrontalInstance(): string {
587
		$frontalCloudId = $this->getAppValue(self::FRONTAL_CLOUD_ID);
588
589
		// using old settings - Deprecated in NC25
590
		if ($frontalCloudId === '') {
591
			$frontalCloudId = $this->config->getAppValue($this->appName, 'local_cloud_id', '');
592
			$this->setAppValue(self::FRONTAL_CLOUD_ID, $frontalCloudId);
593
		}
594
595
		if ($frontalCloudId === '') {
596
			$cliUrl = $this->config->getSystemValue('overwrite.cli.url', '');
597
			$frontal = parse_url($cliUrl);
598
			if (!is_array($frontal) || !array_key_exists('host', $frontal)) {
599
				if ($cliUrl !== '') {
600
					return $cliUrl;
601
				}
602
603
				$randomCloudId = $this->uuid();
604
				$this->setAppValue(self::FRONTAL_CLOUD_ID, $randomCloudId);
605
606
				return $randomCloudId;
607
			}
608
609
			if (array_key_exists('port', $frontal)) {
610
				return $frontal['host'] . ':' . $frontal['port'];
611
			} else {
612
				return $frontal['host'];
613
			}
614
		} else if ($frontalCloudId === 'use-trusted-domain') {
615
			return $this->getTrustedDomains()[0];
616
		} else {
617
			return $frontalCloudId;
618
		}
619
	}
620
621
622
	/**
623
	 * returns address based on FRONTAL_CLOUD_ID, FRONTAL_CLOUD_SCHEME and a routeName
624
	 * perfect for urlId in ActivityPub env.
625
	 *
626
	 * @param string $route
627
	 * @param array $args
628
	 *
629
	 * @return string
630
	 */
631
	public function getFrontalPath(string $route = 'circles.Remote.appService', array $args = []): string {
632
		$base = $this->getAppValue(self::FRONTAL_CLOUD_SCHEME) . '://' . $this->getFrontalInstance();
633
634
		if ($route === '') {
635
			return $base;
636
		}
637
638
		return $base . $this->urlGenerator->linkToRoute($route, $args);
639
	}
640
641
	/**
642
	 * @param string $instance
643
	 *
644
	 * @return bool
645
	 */
646
	public function isLocalInstance(string $instance): bool {
647
		if ($instance === $this->getFrontalInstance()) {
648
			return true;
649
		}
650
651
		if ($this->getAppValue(self::FRONTAL_CLOUD_ID) === 'use-trusted-domain') {
652
			return (in_array($instance, $this->getTrustedDomains()));
653
		}
654
655
		return false;
656
	}
657
658
659
	/**
660
	 * @param NC21Request $request
661
	 * @param string $routeName
662
	 * @param array $args
663
	 */
664
	public function configureRequest(NC21Request $request, string $routeName = '', array $args = []) {
665
		$this->configureRequestAddress($request, $routeName, $args);
666
667
		if ($this->getForcedNcBase() === '') {
668
			$request->setProtocols(['https', 'http']);
669
		}
670
671
		$request->setVerifyPeer($this->getAppValue(ConfigService::CIRCLES_SELF_SIGNED) !== '1');
672
		$request->setLocalAddressAllowed(true);
673
		$request->setFollowLocation(true);
674
		$request->setTimeout(5);
675
	}
676
677
	/**
678
	 * - Create route using overwrite.cli.url.
679
	 * - can be forced using FORCE_NC_BASE or TEST_BC_BASE (temporary)
680
	 * - can also be overwritten in config/config.php: 'circles.force_nc_base'
681
	 * - perfect for loopback request.
682
	 *
683
	 * @param NC21Request $request
684
	 * @param string $routeName
685
	 * @param array $args
686
	 *
687
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
688
	 */
689
	private function configureRequestAddress(NC21Request $request, string $routeName, array $args = []) {
690
		if ($routeName === '') {
691
			return;
692
		}
693
694
		$ncBase = $this->getForcedNcBase();
695
		if ($ncBase !== '') {
696
			$absolute = $this->cleanLinkToRoute($ncBase, $routeName, $args);
697
		} else {
698
			$absolute = $this->urlGenerator->linkToRouteAbsolute($routeName, $args);
699
		}
700
701
		$request->basedOnUrl($absolute);
702
	}
703
704
705
	/**
706
	 * - return force_nc_base from config/config.php, then from FORCE_NC_BASE.
707
	 *
708
	 * @return string
709
	 */
710
	private function getForcedNcBase(): string {
711
		if ($this->getAppValue(self::TEST_NC_BASE) !== '') {
712
			return $this->getAppValue(self::TEST_NC_BASE);
713
		}
714
715
		$fromConfig = $this->config->getSystemValue('circles.force_nc_base', '');
716
		if ($fromConfig !== '') {
717
			return $fromConfig;
718
		}
719
720
		return $this->getAppValue(self::FORCE_NC_BASE);
721
	}
722
723
724
	/**
725
	 * sometimes, linkToRoute will include the base path to the nextcloud which will be duplicate with ncBase
726
	 *
727
	 * @param string $ncBase
728
	 * @param string $routeName
729
	 * @param array $args
730
	 *
731
	 * @return string
732
	 */
733
	private function cleanLinkToRoute(string $ncBase, string $routeName, array $args): string {
734
		$link = $this->urlGenerator->linkToRoute($routeName, $args);
735
		$forcedPath = rtrim(parse_url($ncBase, PHP_URL_PATH), '/');
736
737
		if ($forcedPath !== '' && strpos($link, $forcedPath) === 0) {
738
			$ncBase = substr($ncBase, 0, -strlen($forcedPath));
739
		}
740
741
		return rtrim($ncBase, '/') . $link;
742
	}
743
744
}
745
746