Completed
Push — master ( fc3067...d385e0 )
by
unknown
18:57 queued 01:04
created
lib/private/TemplateLayout.php 1 patch
Indentation   +376 added lines, -376 removed lines patch added patch discarded remove patch
@@ -35,380 +35,380 @@
 block discarded – undo
35 35
 use OCP\Util;
36 36
 
37 37
 class TemplateLayout {
38
-	private static string $versionHash = '';
39
-	/** @var string[] */
40
-	private static array $cacheBusterCache = [];
41
-
42
-	public static ?CSSResourceLocator $cssLocator = null;
43
-	public static ?JSResourceLocator $jsLocator = null;
44
-
45
-	public function __construct(
46
-		private IConfig $config,
47
-		private IAppManager $appManager,
48
-		private InitialStateService $initialState,
49
-		private INavigationManager $navigationManager,
50
-		private ITemplateManager $templateManager,
51
-		private ServerVersion $serverVersion,
52
-	) {
53
-	}
54
-
55
-	public function getPageTemplate(string $renderAs, string $appId): ITemplate {
56
-		// Add fallback theming variables if not rendered as user
57
-		if ($renderAs !== TemplateResponse::RENDER_AS_USER) {
58
-			// TODO cache generated default theme if enabled for fallback if server is erroring ?
59
-			Util::addStyle('theming', 'default');
60
-		}
61
-
62
-		// Decide which page we show
63
-		switch ($renderAs) {
64
-			case TemplateResponse::RENDER_AS_USER:
65
-				$page = $this->templateManager->getTemplate('core', 'layout.user');
66
-				if (in_array(\OC_App::getCurrentApp(), ['settings','admin', 'help']) !== false) {
67
-					$page->assign('bodyid', 'body-settings');
68
-				} else {
69
-					$page->assign('bodyid', 'body-user');
70
-				}
71
-
72
-				$this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry());
73
-				$this->initialState->provideInitialState('core', 'apps', array_values($this->navigationManager->getAll()));
74
-
75
-				if ($this->config->getSystemValueBool('unified_search.enabled', false) || !$this->config->getSystemValueBool('enable_non-accessible_features', true)) {
76
-					$this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT));
77
-					$this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1));
78
-					$this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes');
79
-					Util::addScript('core', 'legacy-unified-search', 'core');
80
-				} else {
81
-					Util::addScript('core', 'unified-search', 'core');
82
-				}
83
-
84
-				// Set logo link target
85
-				$logoUrl = $this->config->getSystemValueString('logo_url', '');
86
-				$page->assign('logoUrl', $logoUrl);
87
-
88
-				// Set default entry name
89
-				$defaultEntryId = $this->navigationManager->getDefaultEntryIdForUser();
90
-				$defaultEntry = $this->navigationManager->get($defaultEntryId);
91
-				$page->assign('defaultAppName', $defaultEntry['name'] ?? '');
92
-
93
-				// Add navigation entry
94
-				$page->assign('application', '');
95
-				$page->assign('appid', $appId);
96
-
97
-				$navigation = $this->navigationManager->getAll();
98
-				$page->assign('navigation', $navigation);
99
-				$settingsNavigation = $this->navigationManager->getAll('settings');
100
-				$this->initialState->provideInitialState('core', 'settingsNavEntries', $settingsNavigation);
101
-
102
-				foreach ($navigation as $entry) {
103
-					if ($entry['active']) {
104
-						$page->assign('application', $entry['name']);
105
-						break;
106
-					}
107
-				}
108
-
109
-				foreach ($settingsNavigation as $entry) {
110
-					if ($entry['active']) {
111
-						$page->assign('application', $entry['name']);
112
-						break;
113
-					}
114
-				}
115
-
116
-				$user = Server::get(IUserSession::class)->getUser();
117
-
118
-				if ($user === null) {
119
-					$page->assign('user_uid', false);
120
-					$page->assign('user_displayname', false);
121
-					$page->assign('userAvatarSet', false);
122
-					$page->assign('userStatus', false);
123
-				} else {
124
-					$page->assign('user_uid', $user->getUID());
125
-					$page->assign('user_displayname', $user->getDisplayName());
126
-					$page->assign('userAvatarSet', true);
127
-					$page->assign('userAvatarVersion', $this->config->getUserValue($user->getUID(), 'avatar', 'version', 0));
128
-				}
129
-				break;
130
-			case TemplateResponse::RENDER_AS_ERROR:
131
-				$page = $this->templateManager->getTemplate('core', 'layout.guest', '', false);
132
-				$page->assign('bodyid', 'body-login');
133
-				$page->assign('user_displayname', '');
134
-				$page->assign('user_uid', '');
135
-				break;
136
-			case TemplateResponse::RENDER_AS_GUEST:
137
-				$page = $this->templateManager->getTemplate('core', 'layout.guest');
138
-				Util::addStyle('guest');
139
-				$page->assign('bodyid', 'body-login');
140
-
141
-				$userDisplayName = false;
142
-				$user = Server::get(IUserSession::class)->getUser();
143
-				if ($user) {
144
-					$userDisplayName = $user->getDisplayName();
145
-				}
146
-
147
-				$page->assign('enabledThemes', []);
148
-				if ($this->appManager->isEnabledForUser('theming') && class_exists('\OCA\Theming\Service\ThemesService')) {
149
-					$themesService = Server::get(\OCA\Theming\Service\ThemesService::class);
150
-					$page->assign('enabledThemes', $themesService->getEnabledThemes());
151
-				}
152
-
153
-				$page->assign('user_displayname', $userDisplayName);
154
-				$page->assign('user_uid', \OC_User::getUser());
155
-				break;
156
-			case TemplateResponse::RENDER_AS_PUBLIC:
157
-				$page = $this->templateManager->getTemplate('core', 'layout.public');
158
-				$page->assign('appid', $appId);
159
-				$page->assign('bodyid', 'body-public');
160
-
161
-				// Set logo link target
162
-				$logoUrl = $this->config->getSystemValueString('logo_url', '');
163
-				$page->assign('logoUrl', $logoUrl);
164
-
165
-				$subscription = Server::get(IRegistry::class);
166
-				$showSimpleSignup = $this->config->getSystemValueBool('simpleSignUpLink.shown', true);
167
-				if ($showSimpleSignup && $subscription->delegateHasValidSubscription()) {
168
-					$showSimpleSignup = false;
169
-				}
170
-
171
-				$defaultSignUpLink = 'https://nextcloud.com/signup/';
172
-				$signUpLink = $this->config->getSystemValueString('registration_link', $defaultSignUpLink);
173
-				if ($signUpLink !== $defaultSignUpLink) {
174
-					$showSimpleSignup = true;
175
-				}
176
-
177
-				if ($this->appManager->isEnabledForUser('registration')) {
178
-					$urlGenerator = Server::get(IURLGenerator::class);
179
-					$signUpLink = $urlGenerator->getAbsoluteURL('/index.php/apps/registration/');
180
-				}
181
-
182
-				$page->assign('showSimpleSignUpLink', $showSimpleSignup);
183
-				$page->assign('signUpLink', $signUpLink);
184
-				break;
185
-			default:
186
-				$page = $this->templateManager->getTemplate('core', 'layout.base');
187
-				break;
188
-		}
189
-		// Send the language, locale, and direction to our layouts
190
-		$l10nFactory = Server::get(IFactory::class);
191
-		$lang = $l10nFactory->findLanguage();
192
-		$locale = $l10nFactory->findLocale($lang);
193
-		$direction = $l10nFactory->getLanguageDirection($lang);
194
-
195
-		$lang = str_replace('_', '-', $lang);
196
-		$page->assign('language', $lang);
197
-		$page->assign('locale', $locale);
198
-		$page->assign('direction', $direction);
199
-
200
-		// Set body data-theme
201
-		try {
202
-			$themesService = Server::get(\OCA\Theming\Service\ThemesService::class);
203
-		} catch (\OCP\AppFramework\QueryException) {
204
-			$themesService = null;
205
-		}
206
-
207
-		$page->assign('enabledThemes', $themesService?->getEnabledThemes() ?? []);
208
-
209
-		if ($this->config->getSystemValueBool('installed', false)) {
210
-			if (empty(self::$versionHash)) {
211
-				$v = $this->appManager->getAppInstalledVersions();
212
-				$v['core'] = implode('.', $this->serverVersion->getVersion());
213
-				self::$versionHash = substr(md5(implode(',', $v)), 0, 8);
214
-			}
215
-		} else {
216
-			self::$versionHash = md5('not installed');
217
-		}
218
-
219
-		// Add the js files
220
-		$jsFiles = self::findJavascriptFiles(Util::getScripts());
221
-		$page->assign('jsfiles', []);
222
-		if ($this->config->getSystemValueBool('installed', false) && $renderAs != TemplateResponse::RENDER_AS_ERROR) {
223
-			// this is on purpose outside of the if statement below so that the initial state is prefilled (done in the getConfig() call)
224
-			// see https://github.com/nextcloud/server/pull/22636 for details
225
-			$jsConfigHelper = new JSConfigHelper(
226
-				$this->serverVersion,
227
-				\OCP\Util::getL10N('lib'),
228
-				\OCP\Server::get(Defaults::class),
229
-				$this->appManager,
230
-				\OC::$server->getSession(),
231
-				\OC::$server->getUserSession()->getUser(),
232
-				$this->config,
233
-				\OC::$server->getGroupManager(),
234
-				\OC::$server->get(IniGetWrapper::class),
235
-				\OC::$server->getURLGenerator(),
236
-				\OC::$server->get(CapabilitiesManager::class),
237
-				\OCP\Server::get(IInitialStateService::class),
238
-				\OCP\Server::get(IProvider::class),
239
-				\OCP\Server::get(FilenameValidator::class),
240
-			);
241
-			$config = $jsConfigHelper->getConfig();
242
-			if (\OC::$server->getContentSecurityPolicyNonceManager()->browserSupportsCspV3()) {
243
-				$page->assign('inline_ocjs', $config);
244
-			} else {
245
-				$page->append('jsfiles', \OC::$server->getURLGenerator()->linkToRoute('core.OCJS.getConfig', ['v' => self::$versionHash]));
246
-			}
247
-		}
248
-		foreach ($jsFiles as $info) {
249
-			$web = $info[1];
250
-			$file = $info[2];
251
-			$page->append('jsfiles', $web . '/' . $file . $this->getVersionHashSuffix());
252
-		}
253
-
254
-		$request = \OCP\Server::get(IRequest::class);
255
-
256
-		try {
257
-			$pathInfo = $request->getPathInfo();
258
-		} catch (\Exception $e) {
259
-			$pathInfo = '';
260
-		}
261
-
262
-		// Do not initialise scss appdata until we have a fully installed instance
263
-		// Do not load scss for update, errors, installation or login page
264
-		if ($this->config->getSystemValueBool('installed', false)
265
-			&& !\OCP\Util::needUpgrade()
266
-			&& $pathInfo !== ''
267
-			&& !preg_match('/^\/login/', $pathInfo)
268
-			&& $renderAs !== TemplateResponse::RENDER_AS_ERROR
269
-		) {
270
-			$cssFiles = self::findStylesheetFiles(\OC_Util::$styles);
271
-		} else {
272
-			// If we ignore the scss compiler,
273
-			// we need to load the guest css fallback
274
-			Util::addStyle('guest');
275
-			$cssFiles = self::findStylesheetFiles(\OC_Util::$styles);
276
-		}
277
-
278
-		$page->assign('cssfiles', []);
279
-		$page->assign('printcssfiles', []);
280
-		$this->initialState->provideInitialState('core', 'versionHash', self::$versionHash);
281
-		foreach ($cssFiles as $info) {
282
-			$web = $info[1];
283
-			$file = $info[2];
284
-
285
-			if (str_ends_with($file, 'print.css')) {
286
-				$page->append('printcssfiles', $web . '/' . $file . $this->getVersionHashSuffix());
287
-			} else {
288
-				$suffix = $this->getVersionHashSuffix($web, $file);
289
-
290
-				if (!str_contains($file, '?v=')) {
291
-					$page->append('cssfiles', $web . '/' . $file . $suffix);
292
-				} else {
293
-					$page->append('cssfiles', $web . '/' . $file . '-' . substr($suffix, 3));
294
-				}
295
-			}
296
-		}
297
-
298
-		if ($request->isUserAgent([Request::USER_AGENT_CLIENT_IOS, Request::USER_AGENT_SAFARI, Request::USER_AGENT_SAFARI_MOBILE])) {
299
-			// Prevent auto zoom with iOS but still allow user zoom
300
-			// On chrome (and others) this does not work (will also disable user zoom)
301
-			$page->assign('viewport_maximum_scale', '1.0');
302
-		}
303
-
304
-		$page->assign('initialStates', $this->initialState->getInitialStates());
305
-
306
-		$page->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content');
307
-		$page->assign('id-app-navigation', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-navigation' : null);
308
-
309
-		return $page;
310
-	}
311
-
312
-	protected function getVersionHashSuffix(string $path = '', string $file = ''): string {
313
-		if ($this->config->getSystemValueBool('debug', false)) {
314
-			// allows chrome workspace mapping in debug mode
315
-			return '';
316
-		}
317
-
318
-		if ($this->config->getSystemValueBool('installed', false) === false) {
319
-			// if not installed just return the version hash
320
-			return '?v=' . self::$versionHash;
321
-		}
322
-
323
-		$hash = false;
324
-		// Try the web-root first
325
-		if ($path !== '') {
326
-			$hash = $this->getVersionHashByPath($path);
327
-		}
328
-		// If not found try the file
329
-		if ($hash === false && $file !== '') {
330
-			$hash = $this->getVersionHashByPath($file);
331
-		}
332
-		// As a last resort we use the server version hash
333
-		if ($hash === false) {
334
-			$hash = self::$versionHash;
335
-		}
336
-
337
-		// The theming app is force-enabled thus the cache buster is always available
338
-		$themingSuffix = '-' . $this->config->getAppValue('theming', 'cachebuster', '0');
339
-
340
-		return '?v=' . $hash . $themingSuffix;
341
-	}
342
-
343
-	private function getVersionHashByPath(string $path): string|false {
344
-		if (array_key_exists($path, self::$cacheBusterCache) === false) {
345
-			// Not yet cached, so lets find the cache buster string
346
-			$appId = $this->getAppNamefromPath($path);
347
-			if ($appId === false) {
348
-				// No app Id could be guessed
349
-				return false;
350
-			}
351
-
352
-			if ($appId === 'core') {
353
-				// core is not a real app but the server itself
354
-				$hash = self::$versionHash;
355
-			} else {
356
-				$appVersion = $this->appManager->getAppVersion($appId);
357
-				// For shipped apps the app version is not a single source of truth, we rather also need to consider the Nextcloud version
358
-				if ($this->appManager->isShipped($appId)) {
359
-					$appVersion .= '-' . self::$versionHash;
360
-				}
361
-
362
-				$hash = substr(md5($appVersion), 0, 8);
363
-			}
364
-			self::$cacheBusterCache[$path] = $hash;
365
-		}
366
-
367
-		return self::$cacheBusterCache[$path];
368
-	}
369
-
370
-	public static function findStylesheetFiles(array $styles): array {
371
-		if (!self::$cssLocator) {
372
-			self::$cssLocator = \OCP\Server::get(CSSResourceLocator::class);
373
-		}
374
-		self::$cssLocator->find($styles);
375
-		return self::$cssLocator->getResources();
376
-	}
377
-
378
-	public function getAppNamefromPath(string $path): string|false {
379
-		if ($path !== '') {
380
-			$pathParts = explode('/', $path);
381
-			if ($pathParts[0] === 'css') {
382
-				// This is a scss request
383
-				return $pathParts[1];
384
-			} elseif ($pathParts[0] === 'core') {
385
-				return 'core';
386
-			}
387
-			return end($pathParts);
388
-		}
389
-		return false;
390
-	}
391
-
392
-	public static function findJavascriptFiles(array $scripts): array {
393
-		if (!self::$jsLocator) {
394
-			self::$jsLocator = \OCP\Server::get(JSResourceLocator::class);
395
-		}
396
-		self::$jsLocator->find($scripts);
397
-		return self::$jsLocator->getResources();
398
-	}
399
-
400
-	/**
401
-	 * Converts the absolute file path to a relative path from \OC::$SERVERROOT
402
-	 * @param string $filePath Absolute path
403
-	 * @return string Relative path
404
-	 * @throws \Exception If $filePath is not under \OC::$SERVERROOT
405
-	 */
406
-	public static function convertToRelativePath(string $filePath) {
407
-		$relativePath = explode(\OC::$SERVERROOT, $filePath);
408
-		if (count($relativePath) !== 2) {
409
-			throw new \Exception('$filePath is not under the \OC::$SERVERROOT');
410
-		}
411
-
412
-		return $relativePath[1];
413
-	}
38
+    private static string $versionHash = '';
39
+    /** @var string[] */
40
+    private static array $cacheBusterCache = [];
41
+
42
+    public static ?CSSResourceLocator $cssLocator = null;
43
+    public static ?JSResourceLocator $jsLocator = null;
44
+
45
+    public function __construct(
46
+        private IConfig $config,
47
+        private IAppManager $appManager,
48
+        private InitialStateService $initialState,
49
+        private INavigationManager $navigationManager,
50
+        private ITemplateManager $templateManager,
51
+        private ServerVersion $serverVersion,
52
+    ) {
53
+    }
54
+
55
+    public function getPageTemplate(string $renderAs, string $appId): ITemplate {
56
+        // Add fallback theming variables if not rendered as user
57
+        if ($renderAs !== TemplateResponse::RENDER_AS_USER) {
58
+            // TODO cache generated default theme if enabled for fallback if server is erroring ?
59
+            Util::addStyle('theming', 'default');
60
+        }
61
+
62
+        // Decide which page we show
63
+        switch ($renderAs) {
64
+            case TemplateResponse::RENDER_AS_USER:
65
+                $page = $this->templateManager->getTemplate('core', 'layout.user');
66
+                if (in_array(\OC_App::getCurrentApp(), ['settings','admin', 'help']) !== false) {
67
+                    $page->assign('bodyid', 'body-settings');
68
+                } else {
69
+                    $page->assign('bodyid', 'body-user');
70
+                }
71
+
72
+                $this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry());
73
+                $this->initialState->provideInitialState('core', 'apps', array_values($this->navigationManager->getAll()));
74
+
75
+                if ($this->config->getSystemValueBool('unified_search.enabled', false) || !$this->config->getSystemValueBool('enable_non-accessible_features', true)) {
76
+                    $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT));
77
+                    $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1));
78
+                    $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes');
79
+                    Util::addScript('core', 'legacy-unified-search', 'core');
80
+                } else {
81
+                    Util::addScript('core', 'unified-search', 'core');
82
+                }
83
+
84
+                // Set logo link target
85
+                $logoUrl = $this->config->getSystemValueString('logo_url', '');
86
+                $page->assign('logoUrl', $logoUrl);
87
+
88
+                // Set default entry name
89
+                $defaultEntryId = $this->navigationManager->getDefaultEntryIdForUser();
90
+                $defaultEntry = $this->navigationManager->get($defaultEntryId);
91
+                $page->assign('defaultAppName', $defaultEntry['name'] ?? '');
92
+
93
+                // Add navigation entry
94
+                $page->assign('application', '');
95
+                $page->assign('appid', $appId);
96
+
97
+                $navigation = $this->navigationManager->getAll();
98
+                $page->assign('navigation', $navigation);
99
+                $settingsNavigation = $this->navigationManager->getAll('settings');
100
+                $this->initialState->provideInitialState('core', 'settingsNavEntries', $settingsNavigation);
101
+
102
+                foreach ($navigation as $entry) {
103
+                    if ($entry['active']) {
104
+                        $page->assign('application', $entry['name']);
105
+                        break;
106
+                    }
107
+                }
108
+
109
+                foreach ($settingsNavigation as $entry) {
110
+                    if ($entry['active']) {
111
+                        $page->assign('application', $entry['name']);
112
+                        break;
113
+                    }
114
+                }
115
+
116
+                $user = Server::get(IUserSession::class)->getUser();
117
+
118
+                if ($user === null) {
119
+                    $page->assign('user_uid', false);
120
+                    $page->assign('user_displayname', false);
121
+                    $page->assign('userAvatarSet', false);
122
+                    $page->assign('userStatus', false);
123
+                } else {
124
+                    $page->assign('user_uid', $user->getUID());
125
+                    $page->assign('user_displayname', $user->getDisplayName());
126
+                    $page->assign('userAvatarSet', true);
127
+                    $page->assign('userAvatarVersion', $this->config->getUserValue($user->getUID(), 'avatar', 'version', 0));
128
+                }
129
+                break;
130
+            case TemplateResponse::RENDER_AS_ERROR:
131
+                $page = $this->templateManager->getTemplate('core', 'layout.guest', '', false);
132
+                $page->assign('bodyid', 'body-login');
133
+                $page->assign('user_displayname', '');
134
+                $page->assign('user_uid', '');
135
+                break;
136
+            case TemplateResponse::RENDER_AS_GUEST:
137
+                $page = $this->templateManager->getTemplate('core', 'layout.guest');
138
+                Util::addStyle('guest');
139
+                $page->assign('bodyid', 'body-login');
140
+
141
+                $userDisplayName = false;
142
+                $user = Server::get(IUserSession::class)->getUser();
143
+                if ($user) {
144
+                    $userDisplayName = $user->getDisplayName();
145
+                }
146
+
147
+                $page->assign('enabledThemes', []);
148
+                if ($this->appManager->isEnabledForUser('theming') && class_exists('\OCA\Theming\Service\ThemesService')) {
149
+                    $themesService = Server::get(\OCA\Theming\Service\ThemesService::class);
150
+                    $page->assign('enabledThemes', $themesService->getEnabledThemes());
151
+                }
152
+
153
+                $page->assign('user_displayname', $userDisplayName);
154
+                $page->assign('user_uid', \OC_User::getUser());
155
+                break;
156
+            case TemplateResponse::RENDER_AS_PUBLIC:
157
+                $page = $this->templateManager->getTemplate('core', 'layout.public');
158
+                $page->assign('appid', $appId);
159
+                $page->assign('bodyid', 'body-public');
160
+
161
+                // Set logo link target
162
+                $logoUrl = $this->config->getSystemValueString('logo_url', '');
163
+                $page->assign('logoUrl', $logoUrl);
164
+
165
+                $subscription = Server::get(IRegistry::class);
166
+                $showSimpleSignup = $this->config->getSystemValueBool('simpleSignUpLink.shown', true);
167
+                if ($showSimpleSignup && $subscription->delegateHasValidSubscription()) {
168
+                    $showSimpleSignup = false;
169
+                }
170
+
171
+                $defaultSignUpLink = 'https://nextcloud.com/signup/';
172
+                $signUpLink = $this->config->getSystemValueString('registration_link', $defaultSignUpLink);
173
+                if ($signUpLink !== $defaultSignUpLink) {
174
+                    $showSimpleSignup = true;
175
+                }
176
+
177
+                if ($this->appManager->isEnabledForUser('registration')) {
178
+                    $urlGenerator = Server::get(IURLGenerator::class);
179
+                    $signUpLink = $urlGenerator->getAbsoluteURL('/index.php/apps/registration/');
180
+                }
181
+
182
+                $page->assign('showSimpleSignUpLink', $showSimpleSignup);
183
+                $page->assign('signUpLink', $signUpLink);
184
+                break;
185
+            default:
186
+                $page = $this->templateManager->getTemplate('core', 'layout.base');
187
+                break;
188
+        }
189
+        // Send the language, locale, and direction to our layouts
190
+        $l10nFactory = Server::get(IFactory::class);
191
+        $lang = $l10nFactory->findLanguage();
192
+        $locale = $l10nFactory->findLocale($lang);
193
+        $direction = $l10nFactory->getLanguageDirection($lang);
194
+
195
+        $lang = str_replace('_', '-', $lang);
196
+        $page->assign('language', $lang);
197
+        $page->assign('locale', $locale);
198
+        $page->assign('direction', $direction);
199
+
200
+        // Set body data-theme
201
+        try {
202
+            $themesService = Server::get(\OCA\Theming\Service\ThemesService::class);
203
+        } catch (\OCP\AppFramework\QueryException) {
204
+            $themesService = null;
205
+        }
206
+
207
+        $page->assign('enabledThemes', $themesService?->getEnabledThemes() ?? []);
208
+
209
+        if ($this->config->getSystemValueBool('installed', false)) {
210
+            if (empty(self::$versionHash)) {
211
+                $v = $this->appManager->getAppInstalledVersions();
212
+                $v['core'] = implode('.', $this->serverVersion->getVersion());
213
+                self::$versionHash = substr(md5(implode(',', $v)), 0, 8);
214
+            }
215
+        } else {
216
+            self::$versionHash = md5('not installed');
217
+        }
218
+
219
+        // Add the js files
220
+        $jsFiles = self::findJavascriptFiles(Util::getScripts());
221
+        $page->assign('jsfiles', []);
222
+        if ($this->config->getSystemValueBool('installed', false) && $renderAs != TemplateResponse::RENDER_AS_ERROR) {
223
+            // this is on purpose outside of the if statement below so that the initial state is prefilled (done in the getConfig() call)
224
+            // see https://github.com/nextcloud/server/pull/22636 for details
225
+            $jsConfigHelper = new JSConfigHelper(
226
+                $this->serverVersion,
227
+                \OCP\Util::getL10N('lib'),
228
+                \OCP\Server::get(Defaults::class),
229
+                $this->appManager,
230
+                \OC::$server->getSession(),
231
+                \OC::$server->getUserSession()->getUser(),
232
+                $this->config,
233
+                \OC::$server->getGroupManager(),
234
+                \OC::$server->get(IniGetWrapper::class),
235
+                \OC::$server->getURLGenerator(),
236
+                \OC::$server->get(CapabilitiesManager::class),
237
+                \OCP\Server::get(IInitialStateService::class),
238
+                \OCP\Server::get(IProvider::class),
239
+                \OCP\Server::get(FilenameValidator::class),
240
+            );
241
+            $config = $jsConfigHelper->getConfig();
242
+            if (\OC::$server->getContentSecurityPolicyNonceManager()->browserSupportsCspV3()) {
243
+                $page->assign('inline_ocjs', $config);
244
+            } else {
245
+                $page->append('jsfiles', \OC::$server->getURLGenerator()->linkToRoute('core.OCJS.getConfig', ['v' => self::$versionHash]));
246
+            }
247
+        }
248
+        foreach ($jsFiles as $info) {
249
+            $web = $info[1];
250
+            $file = $info[2];
251
+            $page->append('jsfiles', $web . '/' . $file . $this->getVersionHashSuffix());
252
+        }
253
+
254
+        $request = \OCP\Server::get(IRequest::class);
255
+
256
+        try {
257
+            $pathInfo = $request->getPathInfo();
258
+        } catch (\Exception $e) {
259
+            $pathInfo = '';
260
+        }
261
+
262
+        // Do not initialise scss appdata until we have a fully installed instance
263
+        // Do not load scss for update, errors, installation or login page
264
+        if ($this->config->getSystemValueBool('installed', false)
265
+            && !\OCP\Util::needUpgrade()
266
+            && $pathInfo !== ''
267
+            && !preg_match('/^\/login/', $pathInfo)
268
+            && $renderAs !== TemplateResponse::RENDER_AS_ERROR
269
+        ) {
270
+            $cssFiles = self::findStylesheetFiles(\OC_Util::$styles);
271
+        } else {
272
+            // If we ignore the scss compiler,
273
+            // we need to load the guest css fallback
274
+            Util::addStyle('guest');
275
+            $cssFiles = self::findStylesheetFiles(\OC_Util::$styles);
276
+        }
277
+
278
+        $page->assign('cssfiles', []);
279
+        $page->assign('printcssfiles', []);
280
+        $this->initialState->provideInitialState('core', 'versionHash', self::$versionHash);
281
+        foreach ($cssFiles as $info) {
282
+            $web = $info[1];
283
+            $file = $info[2];
284
+
285
+            if (str_ends_with($file, 'print.css')) {
286
+                $page->append('printcssfiles', $web . '/' . $file . $this->getVersionHashSuffix());
287
+            } else {
288
+                $suffix = $this->getVersionHashSuffix($web, $file);
289
+
290
+                if (!str_contains($file, '?v=')) {
291
+                    $page->append('cssfiles', $web . '/' . $file . $suffix);
292
+                } else {
293
+                    $page->append('cssfiles', $web . '/' . $file . '-' . substr($suffix, 3));
294
+                }
295
+            }
296
+        }
297
+
298
+        if ($request->isUserAgent([Request::USER_AGENT_CLIENT_IOS, Request::USER_AGENT_SAFARI, Request::USER_AGENT_SAFARI_MOBILE])) {
299
+            // Prevent auto zoom with iOS but still allow user zoom
300
+            // On chrome (and others) this does not work (will also disable user zoom)
301
+            $page->assign('viewport_maximum_scale', '1.0');
302
+        }
303
+
304
+        $page->assign('initialStates', $this->initialState->getInitialStates());
305
+
306
+        $page->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content');
307
+        $page->assign('id-app-navigation', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-navigation' : null);
308
+
309
+        return $page;
310
+    }
311
+
312
+    protected function getVersionHashSuffix(string $path = '', string $file = ''): string {
313
+        if ($this->config->getSystemValueBool('debug', false)) {
314
+            // allows chrome workspace mapping in debug mode
315
+            return '';
316
+        }
317
+
318
+        if ($this->config->getSystemValueBool('installed', false) === false) {
319
+            // if not installed just return the version hash
320
+            return '?v=' . self::$versionHash;
321
+        }
322
+
323
+        $hash = false;
324
+        // Try the web-root first
325
+        if ($path !== '') {
326
+            $hash = $this->getVersionHashByPath($path);
327
+        }
328
+        // If not found try the file
329
+        if ($hash === false && $file !== '') {
330
+            $hash = $this->getVersionHashByPath($file);
331
+        }
332
+        // As a last resort we use the server version hash
333
+        if ($hash === false) {
334
+            $hash = self::$versionHash;
335
+        }
336
+
337
+        // The theming app is force-enabled thus the cache buster is always available
338
+        $themingSuffix = '-' . $this->config->getAppValue('theming', 'cachebuster', '0');
339
+
340
+        return '?v=' . $hash . $themingSuffix;
341
+    }
342
+
343
+    private function getVersionHashByPath(string $path): string|false {
344
+        if (array_key_exists($path, self::$cacheBusterCache) === false) {
345
+            // Not yet cached, so lets find the cache buster string
346
+            $appId = $this->getAppNamefromPath($path);
347
+            if ($appId === false) {
348
+                // No app Id could be guessed
349
+                return false;
350
+            }
351
+
352
+            if ($appId === 'core') {
353
+                // core is not a real app but the server itself
354
+                $hash = self::$versionHash;
355
+            } else {
356
+                $appVersion = $this->appManager->getAppVersion($appId);
357
+                // For shipped apps the app version is not a single source of truth, we rather also need to consider the Nextcloud version
358
+                if ($this->appManager->isShipped($appId)) {
359
+                    $appVersion .= '-' . self::$versionHash;
360
+                }
361
+
362
+                $hash = substr(md5($appVersion), 0, 8);
363
+            }
364
+            self::$cacheBusterCache[$path] = $hash;
365
+        }
366
+
367
+        return self::$cacheBusterCache[$path];
368
+    }
369
+
370
+    public static function findStylesheetFiles(array $styles): array {
371
+        if (!self::$cssLocator) {
372
+            self::$cssLocator = \OCP\Server::get(CSSResourceLocator::class);
373
+        }
374
+        self::$cssLocator->find($styles);
375
+        return self::$cssLocator->getResources();
376
+    }
377
+
378
+    public function getAppNamefromPath(string $path): string|false {
379
+        if ($path !== '') {
380
+            $pathParts = explode('/', $path);
381
+            if ($pathParts[0] === 'css') {
382
+                // This is a scss request
383
+                return $pathParts[1];
384
+            } elseif ($pathParts[0] === 'core') {
385
+                return 'core';
386
+            }
387
+            return end($pathParts);
388
+        }
389
+        return false;
390
+    }
391
+
392
+    public static function findJavascriptFiles(array $scripts): array {
393
+        if (!self::$jsLocator) {
394
+            self::$jsLocator = \OCP\Server::get(JSResourceLocator::class);
395
+        }
396
+        self::$jsLocator->find($scripts);
397
+        return self::$jsLocator->getResources();
398
+    }
399
+
400
+    /**
401
+     * Converts the absolute file path to a relative path from \OC::$SERVERROOT
402
+     * @param string $filePath Absolute path
403
+     * @return string Relative path
404
+     * @throws \Exception If $filePath is not under \OC::$SERVERROOT
405
+     */
406
+    public static function convertToRelativePath(string $filePath) {
407
+        $relativePath = explode(\OC::$SERVERROOT, $filePath);
408
+        if (count($relativePath) !== 2) {
409
+            throw new \Exception('$filePath is not under the \OC::$SERVERROOT');
410
+        }
411
+
412
+        return $relativePath[1];
413
+    }
414 414
 }
Please login to merge, or discard this patch.