diff options
Diffstat (limited to 'lib')
25 files changed, 319 insertions, 65 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index fda8798fe43..acded1ed539 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1219,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', @@ -1904,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 69e3c41284b..bb20a68eae3 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1260,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', @@ -1945,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/ja.js b/lib/l10n/ja.js index 002812cb4a3..77759ed722f 100644 --- a/lib/l10n/ja.js +++ b/lib/l10n/ja.js @@ -324,18 +324,36 @@ OC.L10N.register( "Storage is temporarily not available" : "ストレージは一時的に利用できません", "Storage connection timeout. %s" : "ストレージへの接続がタイムアウト。 %s", "To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule." : "このチェックを実行させるには、Webサーバーが自分自身に接続できることを確認しなければならない。そのため、少なくとも一つの `trusted_domains` または `overwrite.cli.url` を解決して接続できなければなりません。この失敗は、サーバ側のDNSの不一致やアウトバウンドファイアウォールルールの結果かもしれません。", + "Analyze images" : "画像を分析する", + "Ask a question about the given images." : "与えられた画像について質問してください。", "Images" : "画像", + "Images to ask a question about" : "質問したい画像", "Question" : "質問", + "What to ask about the images." : "画像について質問すべきこと。", "Generated response" : "生成された応答", + "The answer to the question" : "質問への回答", + "Audio chat" : "音声チャット", + "Voice chat with the assistant" : "アシスタントとの音声チャット", "System prompt" : "システムプロンプト", "Define rules and assumptions that the assistant should follow during the conversation." : "会話中にアシスタントが従うべきルールと前提条件を定義します。", + "Chat voice message" : "チャット音声メッセージ", + "Describe a task that you want the assistant to do or ask a question." : "アシスタントに実行してほしいタスクを説明するか質問を投げかけてください。", "Chat history" : "チャット履歴", + "The history of chat messages before the current message, starting with a message by the user." : "現在のメッセージ以前に送信されたチャットメッセージの履歴で、ユーザーが送信したメッセージから始まるもの。", + "Input transcript" : "文字起こしを入力", + "Transcription of the audio input" : "音声入力の文字起こし", + "Response voice message" : "応答音声メッセージ", + "The generated voice response as part of the conversation" : "会話の一部として生成された音声応答", + "Output transcript" : "文字起こしを出力", + "Transcription of the audio output" : "音声出力の文字起こし", "Transcribe audio" : "音声の書き起こし", "Transcribe the things said in an audio" : "音声で言ったことを書き起こす", "Audio input" : "音声入力", "The audio to transcribe" : "文字起こしする音声", "Transcription" : "書き起こし", "The transcribed text" : "書き起こされたテキスト", + "Chat by voice with an agent" : "エージェントと音声でチャットする", + "Describe a task that you want the agent to do or ask a question." : "エージェントに実行してほしいタスクを説明するか質問を投げかけてください。エージェントと音声でチャットできます。", "Confirmation" : "確認", "Whether to confirm previously requested actions: 0 for denial and 1 for confirmation." : "以前に要求されたアクションを承認するかどうか: 0なら拒否、1なら承認。", "Conversation token" : "会話トークン", diff --git a/lib/l10n/ja.json b/lib/l10n/ja.json index 1fdcad48c1c..31f59295d19 100644 --- a/lib/l10n/ja.json +++ b/lib/l10n/ja.json @@ -322,18 +322,36 @@ "Storage is temporarily not available" : "ストレージは一時的に利用できません", "Storage connection timeout. %s" : "ストレージへの接続がタイムアウト。 %s", "To allow this check to run you have to make sure that your Web server can connect to itself. Therefore it must be able to resolve and connect to at least one of its `trusted_domains` or the `overwrite.cli.url`. This failure may be the result of a server-side DNS mismatch or outbound firewall rule." : "このチェックを実行させるには、Webサーバーが自分自身に接続できることを確認しなければならない。そのため、少なくとも一つの `trusted_domains` または `overwrite.cli.url` を解決して接続できなければなりません。この失敗は、サーバ側のDNSの不一致やアウトバウンドファイアウォールルールの結果かもしれません。", + "Analyze images" : "画像を分析する", + "Ask a question about the given images." : "与えられた画像について質問してください。", "Images" : "画像", + "Images to ask a question about" : "質問したい画像", "Question" : "質問", + "What to ask about the images." : "画像について質問すべきこと。", "Generated response" : "生成された応答", + "The answer to the question" : "質問への回答", + "Audio chat" : "音声チャット", + "Voice chat with the assistant" : "アシスタントとの音声チャット", "System prompt" : "システムプロンプト", "Define rules and assumptions that the assistant should follow during the conversation." : "会話中にアシスタントが従うべきルールと前提条件を定義します。", + "Chat voice message" : "チャット音声メッセージ", + "Describe a task that you want the assistant to do or ask a question." : "アシスタントに実行してほしいタスクを説明するか質問を投げかけてください。", "Chat history" : "チャット履歴", + "The history of chat messages before the current message, starting with a message by the user." : "現在のメッセージ以前に送信されたチャットメッセージの履歴で、ユーザーが送信したメッセージから始まるもの。", + "Input transcript" : "文字起こしを入力", + "Transcription of the audio input" : "音声入力の文字起こし", + "Response voice message" : "応答音声メッセージ", + "The generated voice response as part of the conversation" : "会話の一部として生成された音声応答", + "Output transcript" : "文字起こしを出力", + "Transcription of the audio output" : "音声出力の文字起こし", "Transcribe audio" : "音声の書き起こし", "Transcribe the things said in an audio" : "音声で言ったことを書き起こす", "Audio input" : "音声入力", "The audio to transcribe" : "文字起こしする音声", "Transcription" : "書き起こし", "The transcribed text" : "書き起こされたテキスト", + "Chat by voice with an agent" : "エージェントと音声でチャットする", + "Describe a task that you want the agent to do or ask a question." : "エージェントに実行してほしいタスクを説明するか質問を投げかけてください。エージェントと音声でチャットできます。", "Confirmation" : "確認", "Whether to confirm previously requested actions: 0 for denial and 1 for confirmation." : "以前に要求されたアクションを承認するかどうか: 0なら拒否、1なら承認。", "Conversation token" : "会話トークン", 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 838710e63d8..7bf32852209 100644 --- a/lib/private/AppFramework/App.php +++ b/lib/private/AppFramework/App.php @@ -138,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'); 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/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/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/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/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, ]; |