* @license GNU AGPL version 3 or any later version * * SPDX-FileCopyrightText: 2017 Joas Schilling * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\External; use OCA\External\Exceptions\GroupNotFoundException; use OCA\External\Exceptions\IconNotFoundException; use OCA\External\Exceptions\InvalidDeviceException; use OCA\External\Exceptions\InvalidNameException; use OCA\External\Exceptions\InvalidTypeException; use OCA\External\Exceptions\InvalidURLException; use OCA\External\Exceptions\LanguageNotFoundException; use OCA\External\Exceptions\SiteNotFoundException; use OCP\App\IAppManager; use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\IAppConfig; use OCP\IGroupManager; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; use OCP\L10N\IFactory; class SitesManager { public const TYPE_LINK = 'link'; public const TYPE_SETTING = 'settings'; public const TYPE_LOGIN = 'guest'; public const TYPE_QUOTA = 'quota'; public const DEVICE_ALL = ''; public const DEVICE_ANDROID = 'android'; public const DEVICE_IOS = 'ios'; public const DEVICE_DESKTOP = 'desktop'; public const DEVICE_BROWSER = 'browser'; /** @var IRequest */ protected $request; /** @var IAppConfig */ protected $config; /** @var IFactory */ protected $languageFactory; /** @var IAppManager */ protected $appManager; /** @var IGroupManager */ protected $groupManager; /** @var IUserSession */ protected $userSession; /** @var JWTManager */ protected $JWTManager; /** @var IAppData */ protected $appData; public function __construct(IRequest $request, IAppConfig $config, IAppManager $appManager, IGroupManager $groupManager, IUserSession $userSession, IFactory $languageFactory, JWTManager $JWTManager, IAppData $appData) { $this->request = $request; $this->config = $config; $this->appManager = $appManager; $this->groupManager = $groupManager; $this->userSession = $userSession; $this->languageFactory = $languageFactory; $this->JWTManager = $JWTManager; $this->appData = $appData; } /** * @return array * @throws SiteNotFoundException */ public function getSiteById(int $id) { $sites = $this->getSitesToDisplay(); if (isset($sites[$id])) { return $sites[$id]; } throw new SiteNotFoundException(); } /** * @return array[] */ public function getSitesToDisplay() { $sites = $this->getSites(); $lang = $this->languageFactory->findLanguage(); $locale = $this->languageFactory->findLocale(); $device = $this->getDeviceFromUserAgent(); $user = $this->userSession->getUser(); if ($user instanceof IUser) { $groups = $this->groupManager->getUserGroupIds($user); } else { $groups = []; } $email = $user instanceof IUser ? $user->getEMailAddress() : ''; $uid = $user instanceof IUser ? $user->getUID() : ''; $displayName = $user instanceof IUser ? $user->getDisplayName() : ''; $email = $email ?? ''; $jwt = ''; if ($user instanceof IUser) { $payload = ['email' => $email, 'uid' => $uid, 'displayName' => $displayName]; $jwt = $this->JWTManager->getToken($payload); } $langSites = []; foreach ($sites as $id => $site) { if ($site['lang'] !== '' && $site['lang'] !== $lang) { continue; } if ($site['device'] !== self::DEVICE_ALL && $site['device'] !== $device) { continue; } if (!empty($site['groups']) && empty(array_intersect($site['groups'], $groups))) { continue; } $site['url'] = str_replace( ['{email}', '{uid}', '{displayname}', '{jwt}', '{groups}', '{language}', '{locale}'], array_map('rawurlencode', [$email, $uid, $displayName, $jwt, implode(',', $groups), $lang, $locale]), $site['url'] ); $langSites[$id] = $site; } return $langSites; } /** * @return array[] */ public function getSites() { $jsonEncodedList = $this->config->getValueString('external', 'sites', ''); $sites = json_decode($jsonEncodedList, true); if (!is_array($sites) || empty($sites)) { return []; } if (isset($sites[0][0])) { return $this->getSitesFromOldConfig($sites); } $sites = array_map([$this, 'fillSiteArray'], $sites); return $sites; } /** * Adds default values for new attributes of sites */ protected function fillSiteArray(array $site): array { return array_merge([ 'icon' => 'external.svg', 'lang' => '', 'type' => self::TYPE_LINK, 'device' => self::DEVICE_ALL, 'groups' => [], 'redirect' => false, ], $site ); } /** * @param string[] $groups * @throws InvalidNameException * @throws InvalidURLException * @throws LanguageNotFoundException * @throws InvalidTypeException * @throws InvalidDeviceException * @throws GroupNotFoundException * @throws IconNotFoundException */ public function addSite(string $name, string $url, string $lang, string $type, string $device, string $icon, array $groups, bool $redirect): array { $id = 1 + $this->config->getValueInt('external', 'max_site'); if ($name === '') { throw new InvalidNameException(); } if (filter_var($url, FILTER_VALIDATE_URL) === false || (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0 && strpos($url, 'mailto:') !== 0)) { throw new InvalidURLException(); } if ($lang !== '') { $valid = false; foreach ($this->getAvailableLanguages() as $language) { if ($language['code'] === $lang) { $valid = true; break; } } if (!$valid) { throw new LanguageNotFoundException(); } } if (!in_array($type, [self::TYPE_LINK, self::TYPE_SETTING, self::TYPE_QUOTA, self::TYPE_LOGIN], true)) { throw new InvalidTypeException(); } if (!in_array($device, [self::DEVICE_ALL, self::DEVICE_ANDROID, self::DEVICE_IOS, self::DEVICE_DESKTOP, self::DEVICE_BROWSER], true)) { throw new InvalidDeviceException(); } foreach ($groups as $gid) { if (!$this->groupManager->groupExists($gid)) { throw new GroupNotFoundException(); } } $icons = $this->getAvailableIcons(); if ($icon === '') { $icon = 'external.svg'; } if (!in_array($icon, $icons, true)) { throw new IconNotFoundException(); } if ($type === self::TYPE_LOGIN) { $redirect = true; } $sites = $this->getSites(); $sites[$id] = [ 'id' => $id, 'name' => $name, 'url' => $url, 'lang' => $lang, 'type' => $type, 'device' => $device, 'icon' => $icon, 'groups' => $groups, 'redirect' => $redirect, ]; $this->config->setValueString('external', 'sites', json_encode($sites)); $this->config->setValueInt('external', 'max_site', $id); return $sites[$id]; } /** * @param string[] $groups * @throws SiteNotFoundException * @throws InvalidNameException * @throws InvalidURLException * @throws LanguageNotFoundException * @throws InvalidTypeException * @throws InvalidDeviceException * @throws GroupNotFoundException * @throws IconNotFoundException */ public function updateSite(int $id, string $name, string $url, string $lang, string $type, string $device, string $icon, array $groups, bool $redirect): array { $sites = $this->getSites(); if (!isset($sites[$id])) { throw new SiteNotFoundException(); } if ($name === '') { throw new InvalidNameException(); } if (filter_var($url, FILTER_VALIDATE_URL) === false || (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0 && strpos($url, 'mailto:') !== 0)) { throw new InvalidURLException(); } if ($lang !== '') { $valid = false; foreach ($this->getAvailableLanguages() as $language) { if ($language['code'] === $lang) { $valid = true; break; } } if (!$valid) { throw new LanguageNotFoundException(); } } if (!in_array($type, [self::TYPE_LINK, self::TYPE_SETTING, self::TYPE_QUOTA, self::TYPE_LOGIN], true)) { throw new InvalidTypeException(); } if (!in_array($device, [self::DEVICE_ALL, self::DEVICE_ANDROID, self::DEVICE_IOS, self::DEVICE_DESKTOP, self::DEVICE_BROWSER], true)) { throw new InvalidDeviceException(); } foreach ($groups as $gid) { if (!$this->groupManager->groupExists($gid)) { throw new GroupNotFoundException(); } } $icons = $this->getAvailableIcons(); if ($icon === '') { $icon = 'external.svg'; } if (!in_array($icon, $icons, true)) { throw new IconNotFoundException(); } if ($type === self::TYPE_LOGIN) { $redirect = true; } $sites[$id] = [ 'id' => $id, 'name' => $name, 'url' => $url, 'lang' => $lang, 'type' => $type, 'device' => $device, 'icon' => $icon, 'groups' => $groups, 'redirect' => $redirect, ]; $this->config->setValueString('external', 'sites', json_encode($sites)); return $sites[$id]; } public function deleteSite(int $id): void { $sites = $this->getSites(); if (!isset($sites[$id])) { return; } unset($sites[$id]); $this->config->setValueString('external', 'sites', json_encode($sites)); } /** * @param array $sites * @return array */ protected function getSitesFromOldConfig(array $sites): array { $fixedSites = []; foreach ($sites as $id => $site) { $fixedSites[$id + 1] = $this->fillSiteArray([ 'id' => $id + 1, 'name' => $site[0], 'url' => $site[1], 'icon' => isset($site[2]) ? $site[2] : 'external.svg', ]); } $this->config->setValueString('external', 'sites', json_encode($fixedSites)); $this->config->setValueInt('external', 'max_site', max(array_keys($fixedSites))); return $fixedSites; } /** * @return string[] */ public function getAvailableIcons() { try { $folder = $this->appData->getFolder('icons'); $icons = $folder->getDirectoryListing(); return array_map(function (ISimpleFile $icon) { return $icon->getName(); }, $icons); } catch (NotFoundException $e) { return ['external.svg']; } } /** * @return array[] */ public function getAvailableLanguages() { $languageCodes = $this->languageFactory->findAvailableLanguages(); $languages = []; foreach ($languageCodes as $lang) { $l = $this->languageFactory->get('lib', $lang); $potentialName = $l->t('__language_name__'); $ln = ['code' => $lang, 'name' => $lang]; if ($l->getLanguageCode() === $lang && strpos($potentialName, '_') !== 0) { $ln = ['code' => $lang, 'name' => $potentialName]; } elseif ($lang === 'en') { $ln = ['code' => $lang, 'name' => 'English (US)']; } $languages[] = $ln; } $commonLangCodes = ['en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it', 'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko']; usort($languages, function (array $a, array $b) use ($commonLangCodes): int { $aC = array_search($a['code'], $commonLangCodes, true); $bC = array_search($b['code'], $commonLangCodes, true); if ($aC === false && $bC !== false) { // If a is common, but b is not, list a before b return 1; } if ($aC !== false && $bC === false) { // If a is common, but b is not, list a before b return -1; } if ($aC !== false && $bC !== false) { // If a is common, but b is not, list a before b return $aC - $bC; } if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) { // If a doesn't have a name, but b does, list b before a return 1; } if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) { // If a does have a name, but b doesn't, list a before b return -1; } // Otherwise compare the names return strcmp($a['name'], $b['name']); }); return $languages; } /** * @return string */ protected function getDeviceFromUserAgent() { if ($this->request->isUserAgent([IRequest::USER_AGENT_CLIENT_ANDROID])) { return self::DEVICE_ANDROID; } if ($this->request->isUserAgent([IRequest::USER_AGENT_CLIENT_IOS])) { return self::DEVICE_IOS; } if ($this->request->isUserAgent([IRequest::USER_AGENT_CLIENT_DESKTOP])) { return self::DEVICE_DESKTOP; } return self::DEVICE_BROWSER; } }