diff options
Diffstat (limited to 'lib')
34 files changed, 455 insertions, 134 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 5da1bc4ba30..acded1ed539 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -696,6 +696,7 @@ return array( 'OCP\\Notification\\IManager' => $baseDir . '/lib/public/Notification/IManager.php', 'OCP\\Notification\\INotification' => $baseDir . '/lib/public/Notification/INotification.php', 'OCP\\Notification\\INotifier' => $baseDir . '/lib/public/Notification/INotifier.php', + 'OCP\\Notification\\IPreloadableNotifier' => $baseDir . '/lib/public/Notification/IPreloadableNotifier.php', 'OCP\\Notification\\IncompleteNotificationException' => $baseDir . '/lib/public/Notification/IncompleteNotificationException.php', 'OCP\\Notification\\IncompleteParsedNotificationException' => $baseDir . '/lib/public/Notification/IncompleteParsedNotificationException.php', 'OCP\\Notification\\InvalidValueException' => $baseDir . '/lib/public/Notification/InvalidValueException.php', @@ -1218,6 +1219,7 @@ return array( 'OC\\Comments\\ManagerFactory' => $baseDir . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => $baseDir . '/lib/private/Config.php', 'OC\\Config\\ConfigManager' => $baseDir . '/lib/private/Config/ConfigManager.php', + 'OC\\Config\\PresetManager' => $baseDir . '/lib/private/Config/PresetManager.php', 'OC\\Config\\UserConfig' => $baseDir . '/lib/private/Config/UserConfig.php', 'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php', @@ -1903,6 +1905,7 @@ return array( 'OC\\Preview\\WatcherConnector' => $baseDir . '/lib/private/Preview/WatcherConnector.php', 'OC\\Preview\\WebP' => $baseDir . '/lib/private/Preview/WebP.php', 'OC\\Preview\\XBitmap' => $baseDir . '/lib/private/Preview/XBitmap.php', + 'OC\\Profile\\Actions\\BlueskyAction' => $baseDir . '/lib/private/Profile/Actions/BlueskyAction.php', 'OC\\Profile\\Actions\\EmailAction' => $baseDir . '/lib/private/Profile/Actions/EmailAction.php', 'OC\\Profile\\Actions\\FediverseAction' => $baseDir . '/lib/private/Profile/Actions/FediverseAction.php', 'OC\\Profile\\Actions\\PhoneAction' => $baseDir . '/lib/private/Profile/Actions/PhoneAction.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 87ef0dddb48..bb20a68eae3 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -737,6 +737,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Notification\\IManager' => __DIR__ . '/../../..' . '/lib/public/Notification/IManager.php', 'OCP\\Notification\\INotification' => __DIR__ . '/../../..' . '/lib/public/Notification/INotification.php', 'OCP\\Notification\\INotifier' => __DIR__ . '/../../..' . '/lib/public/Notification/INotifier.php', + 'OCP\\Notification\\IPreloadableNotifier' => __DIR__ . '/../../..' . '/lib/public/Notification/IPreloadableNotifier.php', 'OCP\\Notification\\IncompleteNotificationException' => __DIR__ . '/../../..' . '/lib/public/Notification/IncompleteNotificationException.php', 'OCP\\Notification\\IncompleteParsedNotificationException' => __DIR__ . '/../../..' . '/lib/public/Notification/IncompleteParsedNotificationException.php', 'OCP\\Notification\\InvalidValueException' => __DIR__ . '/../../..' . '/lib/public/Notification/InvalidValueException.php', @@ -1259,6 +1260,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Comments\\ManagerFactory' => __DIR__ . '/../../..' . '/lib/private/Comments/ManagerFactory.php', 'OC\\Config' => __DIR__ . '/../../..' . '/lib/private/Config.php', 'OC\\Config\\ConfigManager' => __DIR__ . '/../../..' . '/lib/private/Config/ConfigManager.php', + 'OC\\Config\\PresetManager' => __DIR__ . '/../../..' . '/lib/private/Config/PresetManager.php', 'OC\\Config\\UserConfig' => __DIR__ . '/../../..' . '/lib/private/Config/UserConfig.php', 'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php', @@ -1944,6 +1946,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Preview\\WatcherConnector' => __DIR__ . '/../../..' . '/lib/private/Preview/WatcherConnector.php', 'OC\\Preview\\WebP' => __DIR__ . '/../../..' . '/lib/private/Preview/WebP.php', 'OC\\Preview\\XBitmap' => __DIR__ . '/../../..' . '/lib/private/Preview/XBitmap.php', + 'OC\\Profile\\Actions\\BlueskyAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/BlueskyAction.php', 'OC\\Profile\\Actions\\EmailAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/EmailAction.php', 'OC\\Profile\\Actions\\FediverseAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/FediverseAction.php', 'OC\\Profile\\Actions\\PhoneAction' => __DIR__ . '/../../..' . '/lib/private/Profile/Actions/PhoneAction.php', diff --git a/lib/l10n/es.js b/lib/l10n/es.js index 9c7ea8f5f41..a01ed7e43b8 100644 --- a/lib/l10n/es.js +++ b/lib/l10n/es.js @@ -275,6 +275,7 @@ OC.L10N.register( "A valid Login must be provided" : "Se debe proporcionar un usuario válido", "Login contains whitespace at the beginning or at the end" : "El usuario contiene espacios en blanco al inicio o al final", "Login must not consist of dots only" : "El usuario no debe consistir sólo de puntos", + "Username is too long" : "El nombre de usuario es demasiado largo", "Login is invalid because files already exist for this user" : "El nombre de inicio de sesión es inválido porque ya existen archivos para este usuario", "Account disabled" : "Cuenta deshabilitada", "Login canceled by app" : "Login cancelado por la app", @@ -328,13 +329,17 @@ OC.L10N.register( "Images" : "Imágenes", "Images to ask a question about" : "Imágenes sobre las cuales se formulará una pregunta", "Question" : "Pregunta", + "What to ask about the images." : "Que preguntar sobre las imágenes.", "Generated response" : "Respuesta generada", "The answer to the question" : "La respuesta a la pregunta", + "Audio chat" : "Chat de audio", "Voice chat with the assistant" : "Chat de voz con el asistente", "System prompt" : "Prompt del sistema", "Define rules and assumptions that the assistant should follow during the conversation." : "Definir las reglas y supuestos que el asistente debe seguir durante la conversación.", "Chat voice message" : "Mensaje de voz del chat", + "Describe a task that you want the assistant to do or ask a question." : "Describa una tarea que desea que el asistente realice o haga una pregunta.", "Chat history" : "Historial de la conversación", + "The history of chat messages before the current message, starting with a message by the user." : "El historial de mensajes de chat anterior al mensaje actual, comenzando con un mensaje de parte del usuario.", "Input transcript" : "Transcripción de entrada", "Transcription of the audio input" : "Transcripción de la entrada de audio", "Response voice message" : "Mensaje de voz de respuesta", @@ -347,6 +352,8 @@ OC.L10N.register( "The audio to transcribe" : "El audio a transcribir", "Transcription" : "Transcripción", "The transcribed text" : "El texto transcrito", + "Chat by voice with an agent" : "Chatear a través de voz con un agente", + "Describe a task that you want the agent to do or ask a question." : "Describa una tarea que desea que el asistente realice o haga una pregunta.", "Confirmation" : "Confirmación", "Whether to confirm previously requested actions: 0 for denial and 1 for confirmation." : "Si se deben confirmar acciones solicitadas anteriormente: 0 para denegar y 1 para confirmar.", "Conversation token" : "Token de conversación", diff --git a/lib/l10n/es.json b/lib/l10n/es.json index 29ee23631f9..fc43c0e1d91 100644 --- a/lib/l10n/es.json +++ b/lib/l10n/es.json @@ -273,6 +273,7 @@ "A valid Login must be provided" : "Se debe proporcionar un usuario válido", "Login contains whitespace at the beginning or at the end" : "El usuario contiene espacios en blanco al inicio o al final", "Login must not consist of dots only" : "El usuario no debe consistir sólo de puntos", + "Username is too long" : "El nombre de usuario es demasiado largo", "Login is invalid because files already exist for this user" : "El nombre de inicio de sesión es inválido porque ya existen archivos para este usuario", "Account disabled" : "Cuenta deshabilitada", "Login canceled by app" : "Login cancelado por la app", @@ -326,13 +327,17 @@ "Images" : "Imágenes", "Images to ask a question about" : "Imágenes sobre las cuales se formulará una pregunta", "Question" : "Pregunta", + "What to ask about the images." : "Que preguntar sobre las imágenes.", "Generated response" : "Respuesta generada", "The answer to the question" : "La respuesta a la pregunta", + "Audio chat" : "Chat de audio", "Voice chat with the assistant" : "Chat de voz con el asistente", "System prompt" : "Prompt del sistema", "Define rules and assumptions that the assistant should follow during the conversation." : "Definir las reglas y supuestos que el asistente debe seguir durante la conversación.", "Chat voice message" : "Mensaje de voz del chat", + "Describe a task that you want the assistant to do or ask a question." : "Describa una tarea que desea que el asistente realice o haga una pregunta.", "Chat history" : "Historial de la conversación", + "The history of chat messages before the current message, starting with a message by the user." : "El historial de mensajes de chat anterior al mensaje actual, comenzando con un mensaje de parte del usuario.", "Input transcript" : "Transcripción de entrada", "Transcription of the audio input" : "Transcripción de la entrada de audio", "Response voice message" : "Mensaje de voz de respuesta", @@ -345,6 +350,8 @@ "The audio to transcribe" : "El audio a transcribir", "Transcription" : "Transcripción", "The transcribed text" : "El texto transcrito", + "Chat by voice with an agent" : "Chatear a través de voz con un agente", + "Describe a task that you want the agent to do or ask a question." : "Describa una tarea que desea que el asistente realice o haga una pregunta.", "Confirmation" : "Confirmación", "Whether to confirm previously requested actions: 0 for denial and 1 for confirmation." : "Si se deben confirmar acciones solicitadas anteriormente: 0 para denegar y 1 para confirmar.", "Conversation token" : "Token de conversación", diff --git a/lib/l10n/sw.js b/lib/l10n/sw.js index 0eca41da17e..84ddd8abf9b 100644 --- a/lib/l10n/sw.js +++ b/lib/l10n/sw.js @@ -59,7 +59,9 @@ OC.L10N.register( "Transcribe audio" : "Transcribe audio", "Audio input" : "Audio input", "Confirmation" : "Uthibitisho", + "Generate image" : "Generate image", "Prompt" : "Prompt", + "Describe the image you want to generate" : "Describe the image you want to generate", "Describe a task that you want the assistant to do or ask a question" : "Describe a task that you want the assistant to do or ask a question", "The history of chat messages before the current message, starting with a message by the user" : "The history of chat messages before the current message, starting with a message by the user", "Text" : "Maandishi", diff --git a/lib/l10n/sw.json b/lib/l10n/sw.json index 5759a8f07f4..6adf1f1d8ee 100644 --- a/lib/l10n/sw.json +++ b/lib/l10n/sw.json @@ -57,7 +57,9 @@ "Transcribe audio" : "Transcribe audio", "Audio input" : "Audio input", "Confirmation" : "Uthibitisho", + "Generate image" : "Generate image", "Prompt" : "Prompt", + "Describe the image you want to generate" : "Describe the image you want to generate", "Describe a task that you want the assistant to do or ask a question" : "Describe a task that you want the assistant to do or ask a question", "The history of chat messages before the current message, starting with a message by the user" : "The history of chat messages before the current message, starting with a message by the user", "Text" : "Maandishi", diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 9c7c35d4a6b..d00b1d2e9a3 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -78,6 +78,7 @@ class AccountManager implements IAccountManager { self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED, self::PROPERTY_ROLE => self::SCOPE_LOCAL, self::PROPERTY_TWITTER => self::SCOPE_LOCAL, + self::PROPERTY_BLUESKY => self::SCOPE_LOCAL, self::PROPERTY_WEBSITE => self::SCOPE_LOCAL, ]; @@ -564,6 +565,13 @@ class AccountManager implements IAccountManager { ], [ + 'name' => self::PROPERTY_BLUESKY, + 'value' => '', + 'scope' => $scopes[self::PROPERTY_BLUESKY], + 'verified' => self::NOT_VERIFIED, + ], + + [ 'name' => self::PROPERTY_FEDIVERSE, 'value' => '', 'scope' => $scopes[self::PROPERTY_FEDIVERSE], @@ -713,6 +721,47 @@ class AccountManager implements IAccountManager { } } + private function validateBlueSkyHandle(string $text): bool { + if ($text === '') { + return true; + } + + $lowerText = strtolower($text); + + if ($lowerText === 'bsky.social') { + // "bsky.social" itself is not a valid handle + return false; + } + + if (str_ends_with($lowerText, '.bsky.social')) { + $parts = explode('.', $lowerText); + + // Must be exactly: username.bsky.social → 3 parts + if (count($parts) !== 3 || $parts[1] !== 'bsky' || $parts[2] !== 'social') { + return false; + } + + $username = $parts[0]; + + // Must be 3–18 chars, alphanumeric/hyphen, no start/end hyphen + return preg_match('/^[a-z0-9][a-z0-9-]{2,17}$/', $username) === 1; + } + + // Allow custom domains (Bluesky handle via personal domain) + return filter_var($text, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; + } + + + private function sanitizePropertyBluesky(IAccountProperty $property): void { + if ($property->getName() === self::PROPERTY_BLUESKY) { + if (!$this->validateBlueSkyHandle($property->getValue())) { + throw new InvalidArgumentException(self::PROPERTY_BLUESKY); + } + + $property->setValue($property->getValue()); + } + } + /** * @throws InvalidArgumentException If the property value is not a valid fediverse handle (username@instance where instance is a valid domain) */ @@ -805,6 +854,15 @@ class AccountManager implements IAccountManager { } try { + $property = $account->getProperty(self::PROPERTY_BLUESKY); + if ($property->getValue() !== '') { + $this->sanitizePropertyBluesky($property); + } + } catch (PropertyDoesNotExistException $e) { + // valid case, nothing to do + } + + try { $property = $account->getProperty(self::PROPERTY_FEDIVERSE); if ($property->getValue() !== '') { $this->sanitizePropertyFediverse($property); diff --git a/lib/private/AppConfig.php b/lib/private/AppConfig.php index 2280ac1a79f..cef612536d6 100644 --- a/lib/private/AppConfig.php +++ b/lib/private/AppConfig.php @@ -13,9 +13,9 @@ use InvalidArgumentException; use JsonException; use OC\AppFramework\Bootstrap\Coordinator; use OC\Config\ConfigManager; +use OC\Config\PresetManager; use OCP\Config\Lexicon\Entry; use OCP\Config\Lexicon\ILexicon; -use OCP\Config\Lexicon\Preset; use OCP\Config\Lexicon\Strictness; use OCP\Config\ValueType; use OCP\DB\Exception as DBException; @@ -27,7 +27,6 @@ use OCP\IAppConfig; use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; -use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -66,13 +65,14 @@ class AppConfig implements IAppConfig { /** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ private array $configLexiconDetails = []; private bool $ignoreLexiconAliases = false; - private ?Preset $configLexiconPreset = null; /** @var ?array<string, string> */ private ?array $appVersionsCache = null; public function __construct( protected IDBConnection $connection, protected IConfig $config, + private readonly ConfigManager $configManager, + private readonly PresetManager $presetManager, protected LoggerInterface $logger, protected ICrypto $crypto, ) { @@ -520,8 +520,7 @@ class AppConfig implements IAppConfig { // interested to check options in case a modification of the value is needed // ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN if ($origKey !== $key && $type === self::VALUE_BOOL) { - $configManager = Server::get(ConfigManager::class); - $value = ($configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0'; + $value = ($this->configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0'; } return $value; @@ -1108,7 +1107,7 @@ class AppConfig implements IAppConfig { $this->assertParams($app, $key); try { $details = $this->getDetails($app, $key); - } catch (AppConfigUnknownKeyException $e) { + } catch (AppConfigUnknownKeyException) { $details = [ 'app' => $app, 'key' => $key @@ -1129,13 +1128,13 @@ class AppConfig implements IAppConfig { 'valueType' => $lexiconEntry->getValueType(), 'valueTypeName' => $lexiconEntry->getValueType()->name, 'sensitive' => $lexiconEntry->isFlagged(self::FLAG_SENSITIVE), - 'default' => $lexiconEntry->getDefault($this->getLexiconPreset()), + 'default' => $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()), 'definition' => $lexiconEntry->getDefinition(), 'note' => $lexiconEntry->getNote(), ]); } - return array_filter($details); + return array_filter($details, static fn ($v): bool => ($v !== null)); } /** @@ -1228,7 +1227,6 @@ class AppConfig implements IAppConfig { public function clearCache(bool $reload = false): void { $this->lazyLoaded = $this->fastLoaded = false; $this->lazyCache = $this->fastCache = $this->valueTypes = $this->configLexiconDetails = []; - $this->configLexiconPreset = null; if (!$reload) { return; @@ -1714,7 +1712,7 @@ class AppConfig implements IAppConfig { $lazy = $lexiconEntry->isLazy(); // only look for default if needed, default from Lexicon got priority if ($default !== null) { - $default = $lexiconEntry->getDefault($this->getLexiconPreset()) ?? $default; + $default = $lexiconEntry->getDefault($this->presetManager->getLexiconPreset()) ?? $default; } if ($lexiconEntry->isFlagged(self::FLAG_SENSITIVE)) { @@ -1802,14 +1800,6 @@ class AppConfig implements IAppConfig { $this->ignoreLexiconAliases = $ignore; } - private function getLexiconPreset(): Preset { - if ($this->configLexiconPreset === null) { - $this->configLexiconPreset = Preset::tryFrom($this->config->getSystemValueInt(ConfigManager::PRESET_CONFIGKEY, 0)) ?? Preset::NONE; - } - - return $this->configLexiconPreset; - } - /** * Returns the installed versions of all apps * diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php index 77135986d5f..7bf32852209 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -71,7 +71,6 @@ class App { return null; } - /** * Shortcut for calling a controller method and printing the result * @@ -82,7 +81,12 @@ class App { * @param array $urlParams list of URL parameters (optional) * @throws HintException */ - public static function main(string $controllerName, string $methodName, DIContainer $container, ?array $urlParams = null) { + public static function main( + string $controllerName, + string $methodName, + DIContainer $container, + ?array $urlParams = null, + ): void { /** @var IProfiler $profiler */ $profiler = $container->get(IProfiler::class); $eventLogger = $container->get(IEventLogger::class); @@ -134,8 +138,7 @@ class App { $eventLogger->start('app:controller:dispatcher', 'Initialize dispatcher and pre-middleware'); // initialize the dispatcher and run all the middleware before the controller - /** @var Dispatcher $dispatcher */ - $dispatcher = $container['Dispatcher']; + $dispatcher = $container->get(Dispatcher::class); $eventLogger->end('app:controller:dispatcher'); @@ -211,25 +214,4 @@ class App { } } } - - /** - * Shortcut for calling a controller method and printing the result. - * Similar to App:main except that no headers will be sent. - * - * @param string $controllerName the name of the controller under which it is - * stored in the DI container - * @param string $methodName the method that you want to call - * @param array $urlParams an array with variables extracted from the routes - * @param DIContainer $container an instance of a pimple container. - */ - public static function part(string $controllerName, string $methodName, array $urlParams, - DIContainer $container) { - $container['urlParams'] = $urlParams; - $controller = $container[$controllerName]; - - $dispatcher = $container['Dispatcher']; - - [, , $output] = $dispatcher->dispatch($controller, $methodName); - return $output; - } } diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 5ccc1b7d348..0bce8ac193b 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -63,7 +63,7 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; class DIContainer extends SimpleContainer implements IAppContainer { - private string $appName; + protected string $appName; private array $middleWares = []; private ServerContainer $server; @@ -152,7 +152,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { $this->registerDeprecatedAlias('Dispatcher', Dispatcher::class); $this->registerService(Dispatcher::class, function (ContainerInterface $c) { return new Dispatcher( - $c->get('Protocol'), + $c->get(Http::class), $c->get(MiddlewareDispatcher::class), $c->get(IControllerMethodReflector::class), $c->get(IRequest::class), diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index ed26e75ec89..0db3bfc1c77 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -196,7 +196,9 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer { $this->registerService($alias, function (ContainerInterface $container) use ($target, $alias): mixed { try { $logger = $container->get(LoggerInterface::class); - $logger->debug('The requested alias "' . $alias . '" is deprecated. Please request "' . $target . '" directly. This alias will be removed in a future Nextcloud version.', ['app' => 'serverDI']); + $logger->debug('The requested alias "' . $alias . '" is deprecated. Please request "' . $target . '" directly. This alias will be removed in a future Nextcloud version.', [ + 'app' => $this->appName ?? 'serverDI', + ]); } catch (ContainerExceptionInterface $e) { // Could not get logger. Continue } diff --git a/lib/private/Avatar/Avatar.php b/lib/private/Avatar/Avatar.php index 7aa2d220b88..dc65c9d5743 100644 --- a/lib/private/Avatar/Avatar.php +++ b/lib/private/Avatar/Avatar.php @@ -10,17 +10,17 @@ declare(strict_types=1); namespace OC\Avatar; use Imagick; +use OC\User\User; use OCP\Color; use OCP\Files\NotFoundException; use OCP\IAvatar; +use OCP\IConfig; use Psr\Log\LoggerInterface; /** * This class gets and sets users avatars. */ abstract class Avatar implements IAvatar { - protected LoggerInterface $logger; - /** * https://github.com/sebdesign/cap-height -- for 500px height * Automated check: https://codepen.io/skjnldsv/pen/PydLBK/ @@ -35,8 +35,10 @@ abstract class Avatar implements IAvatar { <text x="50%" y="350" style="font-weight:normal;font-size:280px;font-family:\'Noto Sans\';text-anchor:middle;fill:#{fgFill}">{letter}</text> </svg>'; - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; + public function __construct( + protected IConfig $config, + protected LoggerInterface $logger, + ) { } /** @@ -84,8 +86,7 @@ abstract class Avatar implements IAvatar { * @return string * */ - protected function getAvatarVector(int $size, bool $darkTheme): string { - $userDisplayName = $this->getDisplayName(); + protected function getAvatarVector(string $userDisplayName, int $size, bool $darkTheme): string { $fgRGB = $this->avatarBackgroundColor($userDisplayName); $bgRGB = $fgRGB->alphaBlending(0.1, $darkTheme ? new Color(0, 0, 0) : new Color(255, 255, 255)); $fill = sprintf('%02x%02x%02x', $bgRGB->red(), $bgRGB->green(), $bgRGB->blue()); @@ -96,9 +97,30 @@ abstract class Avatar implements IAvatar { } /** + * Select the rendering font based on the user's display name and language + */ + private function getFont(string $userDisplayName): string { + if (preg_match('/\p{Han}/u', $userDisplayName) === 1) { + switch ($this->getAvatarLanguage()) { + case 'zh_TW': + return __DIR__ . '/../../../core/fonts/NotoSansTC-Regular.ttf'; + case 'zh_HK': + return __DIR__ . '/../../../core/fonts/NotoSansHK-Regular.ttf'; + case 'ja': + return __DIR__ . '/../../../core/fonts/NotoSansJP-Regular.ttf'; + case 'ko': + return __DIR__ . '/../../../core/fonts/NotoSansKR-Regular.ttf'; + default: + return __DIR__ . '/../../../core/fonts/NotoSansSC-Regular.ttf'; + } + } + return __DIR__ . '/../../../core/fonts/NotoSans-Regular.ttf'; + } + + /** * Generate png avatar from svg with Imagick */ - protected function generateAvatarFromSvg(int $size, bool $darkTheme): ?string { + protected function generateAvatarFromSvg(string $userDisplayName, int $size, bool $darkTheme): ?string { if (!extension_loaded('imagick')) { return null; } @@ -107,9 +129,10 @@ abstract class Avatar implements IAvatar { if (in_array('RSVG', $formats, true)) { return null; } + $text = $this->getAvatarText(); try { - $font = __DIR__ . '/../../../core/fonts/NotoSans-Regular.ttf'; - $svg = $this->getAvatarVector($size, $darkTheme); + $font = $this->getFont($text); + $svg = $this->getAvatarVector($userDisplayName, $size, $darkTheme); $avatar = new Imagick(); $avatar->setFont($font); $avatar->readImageBlob($svg); @@ -151,7 +174,7 @@ abstract class Avatar implements IAvatar { } imagefilledrectangle($im, 0, 0, $size, $size, $background); - $font = __DIR__ . '/../../../core/fonts/NotoSans-Regular.ttf'; + $font = $this->getFont($text); $fontSize = $size * 0.4; [$x, $y] = $this->imageTTFCenter( @@ -258,4 +281,12 @@ abstract class Avatar implements IAvatar { return $finalPalette[$this->hashToInt($hash, $steps * 3)]; } + + /** + * Get the language to be used for avatar generation. + * This is used to determine the font to use for the avatar text (e.g. CJK characters). + */ + protected function getAvatarLanguage(): string { + return $this->config->getSystemValueString('default_language', 'en'); + } } diff --git a/lib/private/Avatar/AvatarManager.php b/lib/private/Avatar/AvatarManager.php index 60a3d358bf4..c68467085f0 100644 --- a/lib/private/Avatar/AvatarManager.php +++ b/lib/private/Avatar/AvatarManager.php @@ -92,10 +92,10 @@ class AvatarManager implements IAvatarManager { return new UserAvatar($folder, $this->l, $user, $this->logger, $this->config); default: // use a placeholder avatar which caches the generated images - return new PlaceholderAvatar($folder, $user, $this->logger); + return new PlaceholderAvatar($folder, $user, $this->config, $this->logger); } - return new PlaceholderAvatar($folder, $user, $this->logger); + return new PlaceholderAvatar($folder, $user, $this->config, $this->logger); } /** @@ -129,6 +129,6 @@ class AvatarManager implements IAvatarManager { * @param string $name The guest name, e.g. "Albert". */ public function getGuestAvatar(string $name): IAvatar { - return new GuestAvatar($name, $this->logger); + return new GuestAvatar($name, $this->config, $this->logger); } } diff --git a/lib/private/Avatar/GuestAvatar.php b/lib/private/Avatar/GuestAvatar.php index 7ae633f1260..c0c7de0c078 100644 --- a/lib/private/Avatar/GuestAvatar.php +++ b/lib/private/Avatar/GuestAvatar.php @@ -10,6 +10,7 @@ namespace OC\Avatar; use OCP\Files\SimpleFS\InMemoryFile; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\IConfig; use Psr\Log\LoggerInterface; /** @@ -23,9 +24,10 @@ class GuestAvatar extends Avatar { */ public function __construct( private string $userDisplayName, + IConfig $config, LoggerInterface $logger, ) { - parent::__construct($logger); + parent::__construct($config, $logger); } /** diff --git a/lib/private/Avatar/PlaceholderAvatar.php b/lib/private/Avatar/PlaceholderAvatar.php index 07c54f62713..f5f49fb7cb2 100644 --- a/lib/private/Avatar/PlaceholderAvatar.php +++ b/lib/private/Avatar/PlaceholderAvatar.php @@ -14,6 +14,7 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\IConfig; use OCP\IImage; use Psr\Log\LoggerInterface; @@ -27,9 +28,10 @@ class PlaceholderAvatar extends Avatar { public function __construct( private ISimpleFolder $folder, private User $user, + IConfig $config, LoggerInterface $logger, ) { - parent::__construct($logger); + parent::__construct($config, $logger); } /** @@ -87,8 +89,9 @@ class PlaceholderAvatar extends Avatar { throw new NotFoundException; } - if (!$data = $this->generateAvatarFromSvg($size, $darkTheme)) { - $data = $this->generateAvatar($this->getDisplayName(), $size, $darkTheme); + $userDisplayName = $this->getDisplayName(); + if (!$data = $this->generateAvatarFromSvg($userDisplayName, $size, $darkTheme)) { + $data = $this->generateAvatar($userDisplayName, $size, $darkTheme); } try { diff --git a/lib/private/Avatar/UserAvatar.php b/lib/private/Avatar/UserAvatar.php index bef0a20e7b8..aca2aa574bc 100644 --- a/lib/private/Avatar/UserAvatar.php +++ b/lib/private/Avatar/UserAvatar.php @@ -26,11 +26,11 @@ class UserAvatar extends Avatar { public function __construct( private ISimpleFolder $folder, private IL10N $l, - private User $user, + protected User $user, LoggerInterface $logger, - private IConfig $config, + IConfig $config, ) { - parent::__construct($logger); + parent::__construct($config, $logger); } /** @@ -201,8 +201,9 @@ class UserAvatar extends Avatar { try { $ext = $this->getExtension($generated, $darkTheme); } catch (NotFoundException $e) { - if (!$data = $this->generateAvatarFromSvg(1024, $darkTheme)) { - $data = $this->generateAvatar($this->getDisplayName(), 1024, $darkTheme); + $userDisplayName = $this->getDisplayName(); + if (!$data = $this->generateAvatarFromSvg($userDisplayName, 1024, $darkTheme)) { + $data = $this->generateAvatar($userDisplayName, 1024, $darkTheme); } $avatar = $this->folder->newFile($darkTheme ? 'avatar-dark.png' : 'avatar.png'); $avatar->putContent($data); @@ -234,8 +235,9 @@ class UserAvatar extends Avatar { throw new NotFoundException; } if ($generated) { - if (!$data = $this->generateAvatarFromSvg($size, $darkTheme)) { - $data = $this->generateAvatar($this->getDisplayName(), $size, $darkTheme); + $userDisplayName = $this->getDisplayName(); + if (!$data = $this->generateAvatarFromSvg($userDisplayName, $size, $darkTheme)) { + $data = $this->generateAvatar($userDisplayName, $size, $darkTheme); } } else { $avatar = new \OCP\Image(); @@ -293,4 +295,9 @@ class UserAvatar extends Avatar { public function isCustomAvatar(): bool { return $this->config->getUserValue($this->user->getUID(), 'avatar', 'generated', 'false') !== 'true'; } + + #[\Override] + protected function getAvatarLanguage(): string { + return $this->config->getUserValue($this->user->getUID(), 'core', 'lang', parent::getAvatarLanguage()); + } } diff --git a/lib/private/Config/ConfigManager.php b/lib/private/Config/ConfigManager.php index ed516abdcbf..28397402249 100644 --- a/lib/private/Config/ConfigManager.php +++ b/lib/private/Config/ConfigManager.php @@ -14,10 +14,8 @@ use OCP\App\IAppManager; use OCP\Config\Exceptions\TypeConflictException; use OCP\Config\IUserConfig; use OCP\Config\Lexicon\Entry; -use OCP\Config\Lexicon\Preset; use OCP\Config\ValueType; use OCP\IAppConfig; -use OCP\IConfig; use OCP\Server; use Psr\Log\LoggerInterface; @@ -27,20 +25,23 @@ use Psr\Log\LoggerInterface; * @since 32.0.0 */ class ConfigManager { - /** @since 32.0.0 */ - public const PRESET_CONFIGKEY = 'config_preset'; - /** @var AppConfig|null $appConfig */ private ?IAppConfig $appConfig = null; /** @var UserConfig|null $userConfig */ private ?IUserConfig $userConfig = null; public function __construct( - private readonly IConfig $config, private readonly LoggerInterface $logger, ) { } + public function clearConfigCaches(): void { + $this->loadConfigServices(); + $this->appConfig->clearCache(); + $this->userConfig->clearCacheAll(); + } + + /** * Use the rename values from the list of ConfigLexiconEntry defined in each app ConfigLexicon * to migrate config value to a new config key. @@ -82,17 +83,6 @@ class ConfigManager { } /** - * store in config.php the new preset - * refresh cached preset - */ - public function setLexiconPreset(Preset $preset): void { - $this->config->setSystemValue(self::PRESET_CONFIGKEY, $preset->value); - $this->loadConfigServices(); - $this->appConfig->clearCache(); - $this->userConfig->clearCacheAll(); - } - - /** * config services cannot be load at __construct() or install will fail */ private function loadConfigServices(): void { diff --git a/lib/private/Config/PresetManager.php b/lib/private/Config/PresetManager.php new file mode 100644 index 00000000000..d9c029d15c2 --- /dev/null +++ b/lib/private/Config/PresetManager.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Config; + +use OCP\Config\Lexicon\Preset; +use OCP\IConfig; + +/** + * tools to maintains configurations + */ +class PresetManager { + private const PRESET_CONFIGKEY = 'config_preset'; + + private ?Preset $configLexiconPreset = null; + + public function __construct( + private readonly IConfig $config, + private readonly ConfigManager $configManager, + ) { + } + + /** + * store in config.php the new preset + * refresh cached preset + */ + public function setLexiconPreset(Preset $preset): void { + $this->config->setSystemValue(self::PRESET_CONFIGKEY, $preset->value); + $this->configLexiconPreset = $preset; + $this->configManager->clearConfigCaches(); + } + + /** + * returns currently selected Preset + */ + public function getLexiconPreset(): Preset { + if ($this->configLexiconPreset === null) { + $this->configLexiconPreset = Preset::tryFrom($this->config->getSystemValueInt(self::PRESET_CONFIGKEY, 0)) ?? Preset::NONE; + } + + return $this->configLexiconPreset; + } +} diff --git a/lib/private/Config/UserConfig.php b/lib/private/Config/UserConfig.php index 04ba0e29db0..4ddad3ec2f2 100644 --- a/lib/private/Config/UserConfig.php +++ b/lib/private/Config/UserConfig.php @@ -18,7 +18,6 @@ use OCP\Config\Exceptions\UnknownKeyException; use OCP\Config\IUserConfig; use OCP\Config\Lexicon\Entry; use OCP\Config\Lexicon\ILexicon; -use OCP\Config\Lexicon\Preset; use OCP\Config\Lexicon\Strictness; use OCP\Config\ValueType; use OCP\DB\Exception as DBException; @@ -27,7 +26,6 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IConfig; use OCP\IDBConnection; use OCP\Security\ICrypto; -use OCP\Server; use Psr\Log\LoggerInterface; /** @@ -68,11 +66,12 @@ class UserConfig implements IUserConfig { /** @var array<string, array{entries: array<string, Entry>, aliases: array<string, string>, strictness: Strictness}> ['app_id' => ['strictness' => ConfigLexiconStrictness, 'entries' => ['config_key' => ConfigLexiconEntry[]]] */ private array $configLexiconDetails = []; private bool $ignoreLexiconAliases = false; - private ?Preset $configLexiconPreset = null; public function __construct( protected IDBConnection $connection, protected IConfig $config, + private readonly ConfigManager $configManager, + private readonly PresetManager $presetManager, protected LoggerInterface $logger, protected ICrypto $crypto, ) { @@ -772,8 +771,7 @@ class UserConfig implements IUserConfig { // interested to check options in case a modification of the value is needed // ie inverting value from previous key when using lexicon option RENAME_INVERT_BOOLEAN if ($origKey !== $key && $type === ValueType::BOOL) { - $configManager = Server::get(ConfigManager::class); - $value = ($configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0'; + $value = ($this->configManager->convertToBool($value, $this->getLexiconEntry($app, $key))) ? '1' : '0'; } return $value; @@ -1636,7 +1634,6 @@ class UserConfig implements IUserConfig { public function clearCacheAll(): void { $this->lazyLoaded = $this->fastLoaded = []; $this->lazyCache = $this->fastCache = $this->valueDetails = $this->configLexiconDetails = []; - $this->configLexiconPreset = null; } /** @@ -1937,7 +1934,7 @@ class UserConfig implements IUserConfig { // only look for default if needed, default from Lexicon got priority if not overwritten by admin if ($default !== null) { - $default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault($this->getLexiconPreset()) ?? $default; + $default = $this->getSystemDefault($app, $configValue) ?? $configValue->getDefault($this->presetManager->getLexiconPreset()) ?? $default; } // returning false will make get() returning $default and set() not changing value in database @@ -2039,12 +2036,4 @@ class UserConfig implements IUserConfig { public function ignoreLexiconAliases(bool $ignore): void { $this->ignoreLexiconAliases = $ignore; } - - private function getLexiconPreset(): Preset { - if ($this->configLexiconPreset === null) { - $this->configLexiconPreset = Preset::tryFrom($this->config->getSystemValueInt(ConfigManager::PRESET_CONFIGKEY, 0)) ?? Preset::NONE; - } - - return $this->configLexiconPreset; - } } diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 88bdc377e2b..f86cbc341a4 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -16,6 +16,7 @@ use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\ConnectionLost; +use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; @@ -915,11 +916,13 @@ class Connection extends PrimaryReadReplicaConnection { } /** - * @return IDBConnection::PLATFORM_MYSQL|IDBConnection::PLATFORM_ORACLE|IDBConnection::PLATFORM_POSTGRES|IDBConnection::PLATFORM_SQLITE + * @return IDBConnection::PLATFORM_MYSQL|IDBConnection::PLATFORM_ORACLE|IDBConnection::PLATFORM_POSTGRES|IDBConnection::PLATFORM_SQLITE|IDBConnection::PLATFORM_MARIADB */ - public function getDatabaseProvider(): string { + public function getDatabaseProvider(bool $strict = false): string { $platform = $this->getDatabasePlatform(); - if ($platform instanceof MySQLPlatform) { + if ($strict && $platform instanceof MariaDBPlatform) { + return IDBConnection::PLATFORM_MARIADB; + } elseif ($platform instanceof MySQLPlatform) { return IDBConnection::PLATFORM_MYSQL; } elseif ($platform instanceof OraclePlatform) { return IDBConnection::PLATFORM_ORACLE; diff --git a/lib/private/DB/ConnectionAdapter.php b/lib/private/DB/ConnectionAdapter.php index 78ca780f218..d9ccb3c54f2 100644 --- a/lib/private/DB/ConnectionAdapter.php +++ b/lib/private/DB/ConnectionAdapter.php @@ -237,10 +237,10 @@ class ConnectionAdapter implements IDBConnection { } /** - * @return self::PLATFORM_MYSQL|self::PLATFORM_ORACLE|self::PLATFORM_POSTGRES|self::PLATFORM_SQLITE + * @return self::PLATFORM_MYSQL|self::PLATFORM_ORACLE|self::PLATFORM_POSTGRES|self::PLATFORM_SQLITE|self::PLATFORM_MARIADB */ - public function getDatabaseProvider(): string { - return $this->inner->getDatabaseProvider(); + public function getDatabaseProvider(bool $strict = false): string { + return $this->inner->getDatabaseProvider($strict); } /** diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php index 1d1ccd29bf7..1d44c049793 100644 --- a/lib/private/DB/QueryBuilder/QueryBuilder.php +++ b/lib/private/DB/QueryBuilder/QueryBuilder.php @@ -96,6 +96,7 @@ class QueryBuilder implements IQueryBuilder { return match($this->connection->getDatabaseProvider()) { IDBConnection::PLATFORM_ORACLE => new OCIExpressionBuilder($this->connection, $this, $this->logger), IDBConnection::PLATFORM_POSTGRES => new PgSqlExpressionBuilder($this->connection, $this, $this->logger), + IDBConnection::PLATFORM_MARIADB, IDBConnection::PLATFORM_MYSQL => new MySqlExpressionBuilder($this->connection, $this, $this->logger), IDBConnection::PLATFORM_SQLITE => new SqliteExpressionBuilder($this->connection, $this, $this->logger), }; @@ -121,6 +122,7 @@ class QueryBuilder implements IQueryBuilder { return match($this->connection->getDatabaseProvider()) { IDBConnection::PLATFORM_ORACLE => new OCIFunctionBuilder($this->connection, $this, $this->helper), IDBConnection::PLATFORM_POSTGRES => new PgSqlFunctionBuilder($this->connection, $this, $this->helper), + IDBConnection::PLATFORM_MARIADB, IDBConnection::PLATFORM_MYSQL => new FunctionBuilder($this->connection, $this, $this->helper), IDBConnection::PLATFORM_SQLITE => new SqliteFunctionBuilder($this->connection, $this, $this->helper), }; diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php index 1d19f00b0a1..a327109cc12 100644 --- a/lib/private/Mail/EMailTemplate.php +++ b/lib/private/Mail/EMailTemplate.php @@ -190,32 +190,46 @@ EOF; <tr style="padding:0;text-align:left;vertical-align:top"> <th style="Margin:0;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;text-align:left"> <center data-parsed="" style="min-width:490px;width:100%%"> - <table class="button btn default primary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;margin:0 0 30px 0;margin-right:15px;border-radius:8px;max-width:300px;padding:0;text-align:center;vertical-align:top;width:auto;background:%1\$s;background-color:%1\$s;color:#fefefe;"> - <tr style="padding:0;text-align:left;vertical-align:top"> - <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> - <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%"> + <!--[if (gte mso 9)|(IE)]> + <table> + <tr> + <td> + <![endif]--> + <table class="button btn default primary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;margin:0 0 30px 0;margin-right:15px;border-radius:8px;max-width:300px;padding:0;text-align:center;vertical-align:top;width:auto;background:%1\$s;background-color:%1\$s;color:#fefefe;"> <tr style="padding:0;text-align:left;vertical-align:top"> - <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid %2\$s;border-collapse:collapse!important;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> - <a href="%3\$s" style="Margin:0;border:0 solid %4\$s;color:%5\$s;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:normal;margin:0;padding:8px;text-align:left;outline:1px solid %6\$s;text-decoration:none">%7\$s</a> + <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> + <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%"> + <tr style="padding:0;text-align:left;vertical-align:top"> + <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid %2\$s;border-collapse:collapse!important;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> + <a href="%3\$s" style="Margin:0;border:0 solid %4\$s;color:%5\$s;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:normal;margin:0;padding:8px;text-align:left;outline:1px solid %6\$s;text-decoration:none">%7\$s</a> + </td> + </tr> + </table> </td> </tr> </table> + <!--[if (gte mso 9)|(IE)]> </td> - </tr> - </table> - <table class="button btn default secondary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;background-color: #ccc;margin:0 0 30px 0;max-height:40px;max-width:300px;padding:1px;border-radius:8px;text-align:center;vertical-align:top;width:auto"> - <tr style="padding:0;text-align:left;vertical-align:top"> - <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> - <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%"> + <td> + <![endif]--> + <table class="button btn default secondary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;background-color: #ccc;margin:0 0 30px 0;max-height:40px;max-width:300px;padding:1px;border-radius:8px;text-align:center;vertical-align:top;width:auto"> <tr style="padding:0;text-align:left;vertical-align:top"> - <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid #777;border-collapse:collapse!important;color:#fefefe;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> - <a href="%8\$s" style="Margin:0;background-color:#fff;border:0 solid #777;color:#6C6C6C!important;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:normal;margin:0;border-radius: 7px;padding:8px;text-align:left;text-decoration:none">%9\$s</a> + <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> + <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%"> + <tr style="padding:0;text-align:left;vertical-align:top"> + <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid #777;border-collapse:collapse!important;color:#fefefe;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word"> + <a href="%8\$s" style="Margin:0;background-color:#fff;border:0 solid #777;color:#6C6C6C!important;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:normal;margin:0;border-radius: 7px;padding:8px;text-align:left;text-decoration:none">%9\$s</a> + </td> + </tr> + </table> </td> </tr> </table> + <!--[if (gte mso 9)|(IE)]> </td> </tr> </table> + <![endif]--> </center> </th> <th class="expander" style="Margin:0;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0!important;text-align:left;visibility:hidden;width:0"></th> diff --git a/lib/private/Notification/Manager.php b/lib/private/Notification/Manager.php index 8c457db8beb..0cbda651a8b 100644 --- a/lib/private/Notification/Manager.php +++ b/lib/private/Notification/Manager.php @@ -21,6 +21,7 @@ use OCP\Notification\IncompleteNotificationException; use OCP\Notification\IncompleteParsedNotificationException; use OCP\Notification\INotification; use OCP\Notification\INotifier; +use OCP\Notification\IPreloadableNotifier; use OCP\Notification\UnknownNotificationException; use OCP\RichObjectStrings\IRichTextFormatter; use OCP\RichObjectStrings\IValidator; @@ -390,6 +391,17 @@ class Manager implements IManager { return $notification; } + public function preloadDataForParsing(array $notifications, string $languageCode): void { + $notifiers = $this->getNotifiers(); + foreach ($notifiers as $notifier) { + if (!($notifier instanceof IPreloadableNotifier)) { + continue; + } + + $notifier->preloadDataForParsing($notifications, $languageCode); + } + } + /** * @param INotification $notification */ diff --git a/lib/private/Profile/Actions/BlueskyAction.php b/lib/private/Profile/Actions/BlueskyAction.php new file mode 100644 index 00000000000..d05682aac1a --- /dev/null +++ b/lib/private/Profile/Actions/BlueskyAction.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Profile\Actions; + +use OCP\Accounts\IAccountManager; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\L10N\IFactory; +use OCP\Profile\ILinkAction; + +class BlueskyAction implements ILinkAction { + private string $value = ''; + + public function __construct( + private IAccountManager $accountManager, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, + ) { + } + + public function preload(IUser $targetUser): void { + $account = $this->accountManager->getAccount($targetUser); + $this->value = $account->getProperty(IAccountManager::PROPERTY_BLUESKY)->getValue(); + } + + public function getAppId(): string { + return 'core'; + } + + public function getId(): string { + return IAccountManager::PROPERTY_BLUESKY; + } + + public function getDisplayId(): string { + return $this->l10nFactory->get('lib')->t('Bluesky'); + } + + public function getTitle(): string { + $displayUsername = $this->value; + return $this->l10nFactory->get('lib')->t('View %s on Bluesky', [$displayUsername]); + } + + public function getPriority(): int { + return 60; + } + + public function getIcon(): string { + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'actions/bluesky.svg')); + } + + public function getTarget(): ?string { + if (empty($this->value)) { + return null; + } + $username = $this->value; + return 'https://bsky.app/profile/' . $username; + } +} diff --git a/lib/private/Profile/ProfileManager.php b/lib/private/Profile/ProfileManager.php index 1ade208fbcf..c38412f6bd0 100644 --- a/lib/private/Profile/ProfileManager.php +++ b/lib/private/Profile/ProfileManager.php @@ -10,10 +10,12 @@ declare(strict_types=1); namespace OC\Profile; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Config\PresetManager; use OC\Core\Db\ProfileConfig; use OC\Core\Db\ProfileConfigMapper; use OC\Core\ResponseDefinitions; use OC\KnownUser\KnownUserService; +use OC\Profile\Actions\BlueskyAction; use OC\Profile\Actions\EmailAction; use OC\Profile\Actions\FediverseAction; use OC\Profile\Actions\PhoneAction; @@ -24,6 +26,7 @@ use OCP\Accounts\PropertyDoesNotExistException; use OCP\App\IAppManager; use OCP\AppFramework\Db\DoesNotExistException; use OCP\Cache\CappedMemoryCache; +use OCP\Config\Lexicon\Preset; use OCP\IConfig; use OCP\IUser; use OCP\L10N\IFactory; @@ -56,6 +59,7 @@ class ProfileManager implements IProfileManager { PhoneAction::class, WebsiteAction::class, TwitterAction::class, + BlueskyAction::class, FediverseAction::class, ]; @@ -83,6 +87,7 @@ class ProfileManager implements IProfileManager { private IFactory $l10nFactory, private LoggerInterface $logger, private Coordinator $coordinator, + private readonly PresetManager $presetManager, ) { $this->configCache = new CappedMemoryCache(); } @@ -313,6 +318,7 @@ class ProfileManager implements IProfileManager { // Construct the default config for account properties $propertiesConfig = []; foreach (self::DEFAULT_PROPERTY_VISIBILITY as $property => $visibility) { + $this->applyDefaultProfilePreset($property, $visibility); $propertiesConfig[$property] = ['visibility' => $visibility]; } @@ -320,6 +326,31 @@ class ProfileManager implements IProfileManager { } /** + * modify property visibility, based on current Preset + * + * @psalm-suppress UnhandledMatchCondition if conditions are not met, we do not change $visibility + */ + private function applyDefaultProfilePreset(string $property, string &$visibility): void { + try { + $overwrite = match ($this->presetManager->getLexiconPreset()) { + Preset::SHARED, Preset::SCHOOL, Preset::UNIVERSITY => match ($property) { + IAccountManager::PROPERTY_ADDRESS, IAccountManager::PROPERTY_EMAIL, IAccountManager::PROPERTY_PHONE => self::VISIBILITY_HIDE, + }, + Preset::PRIVATE, Preset::FAMILY, Preset::CLUB => match ($property) { + IAccountManager::PROPERTY_EMAIL => self::VISIBILITY_SHOW, + }, + Preset::SMALL, Preset::MEDIUM, Preset::LARGE => match ($property) { + IAccountManager::PROPERTY_EMAIL, IAccountManager::PROPERTY_PHONE => self::VISIBILITY_SHOW, + }, + }; + } catch (\UnhandledMatchError) { + return; + } + + $visibility = $overwrite; + } + + /** * Return the profile config of the target user, * if a config does not already exist a default config is created and returned */ diff --git a/lib/private/Setup/MySQL.php b/lib/private/Setup/MySQL.php index 1e2dda4c609..c4794a86743 100644 --- a/lib/private/Setup/MySQL.php +++ b/lib/private/Setup/MySQL.php @@ -8,6 +8,7 @@ namespace OC\Setup; use Doctrine\DBAL\Platforms\MySQL80Platform; +use Doctrine\DBAL\Platforms\MySQL84Platform; use OC\DB\ConnectionAdapter; use OC\DB\MySqlTools; use OCP\IDBConnection; @@ -92,22 +93,29 @@ class MySQL extends AbstractDatabase { * @throws \OC\DatabaseSetupException */ private function createDBUser($connection): void { + $name = $this->dbUser; + $password = $this->dbPassword; + try { - $name = $this->dbUser; - $password = $this->dbPassword; // we need to create 2 accounts, one for global use and one for local user. if we don't specify the local one, // the anonymous user would take precedence when there is one. - if ($connection->getDatabasePlatform() instanceof Mysql80Platform) { + if ($connection->getDatabasePlatform() instanceof MySQL84Platform) { + $query = "CREATE USER ?@'localhost' IDENTIFIED WITH caching_sha2_password BY ?"; + $connection->executeStatement($query, [$name,$password]); + $query = "CREATE USER ?@'%' IDENTIFIED WITH caching_sha2_password BY ?"; + $connection->executeStatement($query, [$name,$password]); + } elseif ($connection->getDatabasePlatform() instanceof Mysql80Platform) { + // TODO: Remove this elseif section as soon as MySQL 8.0 is out-of-support (after April 2026) $query = "CREATE USER ?@'localhost' IDENTIFIED WITH mysql_native_password BY ?"; - $connection->executeUpdate($query, [$name,$password]); + $connection->executeStatement($query, [$name,$password]); $query = "CREATE USER ?@'%' IDENTIFIED WITH mysql_native_password BY ?"; - $connection->executeUpdate($query, [$name,$password]); + $connection->executeStatement($query, [$name,$password]); } else { $query = "CREATE USER ?@'localhost' IDENTIFIED BY ?"; - $connection->executeUpdate($query, [$name,$password]); + $connection->executeStatement($query, [$name,$password]); $query = "CREATE USER ?@'%' IDENTIFIED BY ?"; - $connection->executeUpdate($query, [$name,$password]); + $connection->executeStatement($query, [$name,$password]); } } catch (\Exception $ex) { $this->logger->error('Database user creation failed.', [ @@ -158,6 +166,11 @@ class MySQL extends AbstractDatabase { //use the admin login data for the new database user $this->dbUser = $adminUser; $this->createDBUser($connection); + // if sharding is used we need to manually call this for every shard as those also need the user setup! + /** @var ConnectionAdapter $connection */ + foreach ($connection->getInner()->getShardConnections() as $shard) { + $this->createDBUser($shard); + } break; } else { diff --git a/lib/private/SystemConfig.php b/lib/private/SystemConfig.php index 57777b06ed6..7e8946f4d05 100644 --- a/lib/private/SystemConfig.php +++ b/lib/private/SystemConfig.php @@ -15,8 +15,9 @@ use OCP\IConfig; * fixes cyclic DI: AllConfig needs AppConfig needs Database needs AllConfig */ class SystemConfig { - /** @var array */ - protected $sensitiveValues = [ + protected array $sensitiveValues; + + protected const DEFAULT_SENSITIVE_VALUES = [ 'instanceid' => true, 'datadirectory' => true, 'dbname' => true, @@ -114,6 +115,7 @@ class SystemConfig { public function __construct( private Config $config, ) { + $this->sensitiveValues = array_merge(self::DEFAULT_SENSITIVE_VALUES, $this->config->getValue('config_extra_sensitive_values', [])); } /** diff --git a/lib/public/Accounts/IAccountManager.php b/lib/public/Accounts/IAccountManager.php index 92fc0002674..ae5535ef13b 100644 --- a/lib/public/Accounts/IAccountManager.php +++ b/lib/public/Accounts/IAccountManager.php @@ -97,10 +97,16 @@ interface IAccountManager { /** * @since 15.0.0 + * @deprecated 32.0.0 */ public const PROPERTY_TWITTER = 'twitter'; /** + * @since 32.0.0 + */ + public const PROPERTY_BLUESKY = 'bluesky'; + + /** * @since 26.0.0 */ public const PROPERTY_FEDIVERSE = 'fediverse'; @@ -160,6 +166,7 @@ interface IAccountManager { self::PROPERTY_PRONOUNS, self::PROPERTY_ROLE, self::PROPERTY_TWITTER, + self::PROPERTY_BLUESKY, self::PROPERTY_WEBSITE, ]; diff --git a/lib/public/IDBConnection.php b/lib/public/IDBConnection.php index e0fe603ec57..ea9b71d8958 100644 --- a/lib/public/IDBConnection.php +++ b/lib/public/IDBConnection.php @@ -45,6 +45,11 @@ interface IDBConnection { public const PLATFORM_SQLITE = 'sqlite'; /** + * @since 32.0.0 + */ + public const PLATFORM_MARIADB = 'mariadb'; + + /** * Gets the QueryBuilder for the connection. * * @return \OCP\DB\QueryBuilder\IQueryBuilder @@ -357,11 +362,15 @@ interface IDBConnection { /** * Returns the database provider name + * * @link https://github.com/nextcloud/server/issues/30877 + * + * @param bool $strict differentiate between database flavors, e.g. MySQL vs MariaDB + * @return self::PLATFORM_MYSQL|self::PLATFORM_ORACLE|self::PLATFORM_POSTGRES|self::PLATFORM_SQLITE|self::PLATFORM_MARIADB + * @since 32.0.0 Optional parameter $strict was added * @since 28.0.0 - * @return self::PLATFORM_MYSQL|self::PLATFORM_ORACLE|self::PLATFORM_POSTGRES|self::PLATFORM_SQLITE */ - public function getDatabaseProvider(): string; + public function getDatabaseProvider(bool $strict = false): string; /** * Get the shard definition by name, if configured diff --git a/lib/public/Notification/IManager.php b/lib/public/Notification/IManager.php index 23664af17cd..207a89344b0 100644 --- a/lib/public/Notification/IManager.php +++ b/lib/public/Notification/IManager.php @@ -11,7 +11,7 @@ namespace OCP\Notification; use OCP\AppFramework\Attribute\Consumable; #[Consumable(since: '9.0.0')] -interface IManager extends IApp, INotifier { +interface IManager extends IApp, IPreloadableNotifier { /** * @param string $appClass The service must implement IApp, otherwise a * \InvalidArgumentException is thrown later diff --git a/lib/public/Notification/INotifier.php b/lib/public/Notification/INotifier.php index bdc7207216f..b6851e3dfb3 100644 --- a/lib/public/Notification/INotifier.php +++ b/lib/public/Notification/INotifier.php @@ -10,6 +10,11 @@ namespace OCP\Notification; use OCP\AppFramework\Attribute\Implementable; +/** + * Please consider implementing {@see IPreloadableNotifier} to improve performance. It allows to + * preload and cache data for many notifications at once instead of loading the data for each + * prepared notification separately. + */ #[Implementable(since: '9.0.0')] interface INotifier { /** diff --git a/lib/public/Notification/IPreloadableNotifier.php b/lib/public/Notification/IPreloadableNotifier.php new file mode 100644 index 00000000000..2bdcd84d254 --- /dev/null +++ b/lib/public/Notification/IPreloadableNotifier.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\Notification; + +use OCP\AppFramework\Attribute\Implementable; + +/** + * Allow notifier implementations to preload and cache data for many notifications at once to + * improve performance by, for example, bundling SQL queries. + */ +#[Implementable(since: '32.0.0')] +interface IPreloadableNotifier extends INotifier { + /** + * This method provides a way for notifier implementations to preload and cache data for many + * notifications. The data is meant to be consumed later in the {@see INotifier::prepare()} + * method to improve performance. + * + * @since 32.0.0 + * + * @param INotification[] $notifications The notifications which are about to be prepared in the next step. + * @param string $languageCode The code of the language that should be used to prepare the notification. + */ + public function preloadDataForParsing(array $notifications, string $languageCode): void; +} diff --git a/lib/public/Profile/IProfileManager.php b/lib/public/Profile/IProfileManager.php index f4e90e39d12..aec06fb4c86 100644 --- a/lib/public/Profile/IProfileManager.php +++ b/lib/public/Profile/IProfileManager.php @@ -55,6 +55,7 @@ interface IProfileManager { IAccountManager::PROPERTY_EMAIL => self::VISIBILITY_SHOW_USERS_ONLY, IAccountManager::PROPERTY_PHONE => self::VISIBILITY_SHOW_USERS_ONLY, IAccountManager::PROPERTY_TWITTER => self::VISIBILITY_SHOW, + IAccountManager::PROPERTY_BLUESKY => self::VISIBILITY_SHOW, IAccountManager::PROPERTY_WEBSITE => self::VISIBILITY_SHOW, IAccountManager::PROPERTY_PRONOUNS => self::VISIBILITY_SHOW, ]; |