Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 104 additions & 59 deletions lib/private/Template/JSConfigHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,70 +39,99 @@
use OCP\User\Backend\IPasswordConfirmationBackend;
use OCP\Util;

/**
* Builds frontend bootstrap configuration for the web UI.
*
* This class collects server, user, sharing, localization, and theme settings,
* provides selected values through the initial state service, and renders the
* JavaScript configuration payload used during page initialization.
*/
class JSConfigHelper {

/** @var array user back-ends excluded from password verification */
private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true];
/**
* Backend class names for which password confirmation should be treated as unavailable.
*
* @var array<string, bool>
*/
private $passwordConfirmationExcludedBackends = [
'user_saml' => true,
'user_globalsiteselector' => true,
];

public function __construct(
protected ServerVersion $serverVersion,
protected IL10N $l,
protected Defaults $defaults,
protected IAppManager $appManager,
protected ISession $session,
protected ?IUser $currentUser,
protected IConfig $config,
protected readonly IAppConfig $appConfig,
protected IGroupManager $groupManager,
protected IniGetWrapper $iniWrapper,
protected IURLGenerator $urlGenerator,
protected CapabilitiesManager $capabilitiesManager,
protected IInitialStateService $initialStateService,
protected IProvider $tokenProvider,
protected FilenameValidator $filenameValidator,
private readonly ServerVersion $serverVersion,
private readonly IL10N $l,
private readonly Defaults $defaults,
private readonly IAppManager $appManager,
private readonly ISession $session,
private readonly ?IUser $currentUser,
private readonly IConfig $config,
private readonly IAppConfig $appConfig,
private readonly IGroupManager $groupManager,
private readonly IniGetWrapper $iniWrapper,
private readonly IURLGenerator $urlGenerator,
private readonly CapabilitiesManager $capabilitiesManager,
private readonly IInitialStateService $initialStateService,
private readonly IProvider $tokenProvider,
private readonly FilenameValidator $filenameValidator,
) {
}

/**
* Builds the JavaScript configuration payload for page initialization.
*
* @return string JavaScript source containing global variable assignments.
*/
public function getConfig(): string {
// Determine whether the current user/session can perform password confirmation.
$userBackendAllowsPasswordConfirmation = true;
if ($this->currentUser !== null) {
$uid = $this->currentUser->getUID();
$canValidatePassword = $this->canUserValidatePassword();
$userBackend = $this->currentUser->getBackend();
$userBackendClassName = $this->currentUser->getBackendClassName();

$backend = $this->currentUser->getBackend();
if ($backend instanceof IPasswordConfirmationBackend) {
$userBackendAllowsPasswordConfirmation = $backend->canConfirmPassword($uid) && $this->canUserValidatePassword();
} elseif (isset($this->excludedUserBackEnds[$this->currentUser->getBackendClassName()])) {
if ($userBackend instanceof IPasswordConfirmationBackend) {
$userBackendAllowsPasswordConfirmation = $userBackend->canConfirmPassword($uid) && $canValidatePassword;
} elseif (isset($this->passwordConfirmationExcludedBackends[$$userBackendClassName])) {
$userBackendAllowsPasswordConfirmation = false;
} else {
$userBackendAllowsPasswordConfirmation = $this->canUserValidatePassword();
$userBackendAllowsPasswordConfirmation = $canValidatePassword;
}
} else {
$uid = null;
}

// Get the config
$apps_paths = [];
$isAdmin = $uid !== null && $this->groupManager->isAdmin($uid);

// Build the map of enabled app IDs to their public web paths for the current context.
/** @var array<string, string|false> $appWebPaths */
$appWebPaths = [];

if ($this->currentUser === null) {
$apps = $this->appManager->getEnabledApps();
$enabledApps = $this->appManager->getEnabledApps();
} else {
$apps = $this->appManager->getEnabledAppsForUser($this->currentUser);
$enabledApps = $this->appManager->getEnabledAppsForUser($this->currentUser);
}

foreach ($apps as $app) {
// Resolve enabled app web paths for frontend bootstrapping.
foreach ($enabledApps as $app) {
try {
$apps_paths[$app] = $this->appManager->getAppWebPath($app);
$appWebPaths[$app] = $this->appManager->getAppWebPath($app);
} catch (AppPathNotFoundException $e) {
$apps_paths[$app] = false;
// If an app's filesystem path cannot be resolved, mark it as unavailable
// instead of aborting JS config generation for all apps.
$appWebPaths[$app] = false;
}
}

// Collect sharing defaults exposed to the frontend.
$enableLinkPasswordByDefault = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_DEFAULT);
$defaultExpireDateEnabled = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_EXPIRE_DATE_DEFAULT);
$defaultExpireDate = $enforceDefaultExpireDate = null;
$defaultExpireDate = $defaultExpireDateEnforced = null;
if ($defaultExpireDateEnabled) {
$defaultExpireDate = (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
$enforceDefaultExpireDate = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_EXPIRE_DATE_ENFORCED);
$defaultExpireDateEnforced = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_EXPIRE_DATE_ENFORCED);
}
$outgoingServer2serverShareEnabled = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes';

Expand All @@ -120,10 +149,12 @@ public function getConfig(): string {
$defaultRemoteExpireDateEnforced = $this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes';
}

$countOfDataLocation = 0;
$dataLocation = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValue('datadirectory', ''), $countOfDataLocation);
if ($countOfDataLocation !== 1 || $uid === null || !$this->groupManager->isAdmin($uid)) {
$dataLocation = false;
// Expose the data directory only when it is a child of the server root and the
// current user is an admin; otherwise keep it hidden from the client.
$dataDirectoryPrefixReplacementCount = 0;
$relativeDataDirectory = str_replace(\OC::$SERVERROOT . '/', '', $this->config->getSystemValue('datadirectory', ''), $dataDirectoryPrefixReplacementCount);
if ($dataDirectoryPrefixReplacementCount !== 1 || $uid === null || !$isAdmin) {
$relativeDataDirectory = false;
}

if ($this->currentUser instanceof IUser) {
Expand All @@ -133,6 +164,8 @@ public function getConfig(): string {
$lastConfirmTimestamp = 0;
}
} else {
// Use a sentinel value so the frontend treats password confirmation as already satisfied
// when this user/session cannot perform password validation.
$lastConfirmTimestamp = PHP_INT_MAX;
}
} else {
Expand All @@ -141,14 +174,14 @@ public function getConfig(): string {

$capabilities = $this->capabilitiesManager->getCapabilities(false, true);

$firstDay = $this->config->getUserValue($uid, 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK, '');
if ($firstDay === '') {
$firstDay = (int)$this->l->l('firstday', null);
$firstDayOfWeek = $this->config->getUserValue($uid, 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK, '');
if ($firstDayOfWeek === '') {
$firstDayOfWeek = (int)$this->l->l('firstday', null);
} else {
$firstDay = (int)$firstDay;
$firstDayOfWeek = (int)$firstDayOfWeek;
}

$config = [
$coreConfig = [
/** @deprecated 30.0.0 - use files capabilities instead */
'blacklist_files_regex' => FileInfo::BLACKLIST_FILES_REGEX,
/** @deprecated 30.0.0 - use files capabilities instead */
Expand All @@ -172,13 +205,16 @@ public function getConfig(): string {

$shareManager = Server::get(IShareManager::class);

$array = [
// Values in this map must already be serialized as JavaScript literals because
// they are concatenated directly into `var <name> = <value>;` statements below.
/** @var array<string, int|string> $legacyJsGlobals */
$legacyJsGlobals = [
'_oc_debug' => $this->config->getSystemValue('debug', false) ? 'true' : 'false',
'_oc_isadmin' => $uid !== null && $this->groupManager->isAdmin($uid) ? 'true' : 'false',
'_oc_isadmin' => $isAdmin ? 'true' : 'false',
'backendAllowsPasswordConfirmation' => $userBackendAllowsPasswordConfirmation ? 'true' : 'false',
'oc_dataURL' => is_string($dataLocation) ? '"' . $dataLocation . '"' : 'false',
'oc_dataURL' => is_string($relativeDataDirectory) ? '"' . $relativeDataDirectory . '"' : 'false',
'_oc_webroot' => '"' . \OC::$WEBROOT . '"',
'_oc_appswebroots' => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution
'_oc_appswebroots' => str_replace('\\/', '/', json_encode($appWebPaths)), // Ugly unescape slashes waiting for better solution
'datepickerFormatDate' => json_encode($this->l->l('jsdate', null)),
'nc_lastLogin' => $lastConfirmTimestamp,
'nc_pageLoad' => time(),
Expand Down Expand Up @@ -237,13 +273,13 @@ public function getConfig(): string {
$this->l->t('Nov.'),
$this->l->t('Dec.')
]),
'firstDay' => json_encode($firstDay),
'_oc_config' => json_encode($config),
'firstDay' => json_encode($firstDayOfWeek),
'_oc_config' => json_encode($coreConfig),
'oc_appconfig' => json_encode([
'core' => [
'defaultExpireDateEnabled' => $defaultExpireDateEnabled,
'defaultExpireDate' => $defaultExpireDate,
'defaultExpireDateEnforced' => $enforceDefaultExpireDate,
'defaultExpireDateEnforced' => $defaultExpireDateEnforced,
'enforcePasswordForPublicLink' => Util::isPublicLinkPasswordRequired(),
'enableLinkPasswordByDefault' => $enableLinkPasswordByDefault,
'sharingDisabledForUser' => $shareManager->sharingDisabledForUser($uid),
Expand Down Expand Up @@ -275,37 +311,46 @@ public function getConfig(): string {
];

if ($this->currentUser !== null) {
$array['oc_userconfig'] = json_encode([
$legacyJsGlobals['oc_userconfig'] = json_encode([
'avatar' => [
'version' => (int)$this->config->getUserValue($uid, 'avatar', 'version', 0),
'generated' => $this->config->getUserValue($uid, 'avatar', 'generated', 'true') === 'true',
]
]);
}

// Provide structured initial state for modern consumers in addition to the legacy JS globals below.
$this->initialStateService->provideInitialState('core', 'projects_enabled', $this->config->getSystemValueBool('projects.enabled', false));

$this->initialStateService->provideInitialState('core', 'config', $config);
$this->initialStateService->provideInitialState('core', 'config', $coreConfig);
$this->initialStateService->provideInitialState('core', 'capabilities', $capabilities);

// Allow hooks to modify the output values
\OC_Hook::emit('\OCP\Config', 'js', ['array' => &$array]);
// Allow legacy hooks to amend the generated JavaScript globals before rendering.
\OC_Hook::emit('\OCP\Config', 'js', ['array' => &$legacyJsGlobals]);

$result = '';
$jsBootstrap = '';

// Echo it
foreach ($array as $setting => $value) {
$result .= 'var ' . $setting . '=' . $value . ';' . PHP_EOL;
// Render the globals as legacy `var` assignments.
foreach ($legacyJsGlobals as $globalName => $serializedValue) {
$jsBootstrap .= 'var ' . $globalName . '=' . $serializedValue . ';' . PHP_EOL;
}

return $result;
return $jsBootstrap;
}

/**
* Returns whether the current session token allows password validation.
*
* If the token cannot be resolved from the current session, this method falls
* back to `true` to avoid incorrectly disabling password confirmation flows.
*
* FIXME: make private / declare @internal?
*/
protected function canUserValidatePassword(): bool {
try {
$token = $this->tokenProvider->getToken($this->session->getId());
$sessionId = $this->session->getId();
$token = $this->tokenProvider->getToken($sessionId);
} catch (ExpiredTokenException|WipeTokenException|InvalidTokenException|SessionNotAvailableException) {
// actually we do not know, so we fall back to this statement
// If the session token cannot be inspected, keep password validation enabled by default.
return true;
}
$scope = $token->getScopeAsArray();
Expand Down
Loading
Loading