PT-BR Translate#136
Conversation
PT-BR Translation to labes
PT-BR translate messages
…figuração insegura de API
Correção de nome da função
Correção de nome da função
Correção de nome da função
Correção de nome da função
Correção de nome da função
Correção de nome do arquivo
Alteração do parametro max_tokens para max_completion_tokens possibilitando o uso de modelos mais atuais.
Adicionando explicação sobre o uso da Criatividade
There was a problem hiding this comment.
Pull request overview
Adds PT-BR localization support while also introducing new security-related utilities and behavior changes around input validation, XSS handling, and rate limiting.
Changes:
- Add PT-BR label/message localization files and extend
en_USlabels with new keys. - Rename the mail action to
GenerateEmailActionand update the frontend request target accordingly. - Introduce XSS, prompt-injection, and rate-limiting utilities and apply them across actions and request parsing.
Reviewed changes
Copilot reviewed 18 out of 21 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| vendor/composer/autoload_static.php | Update classmap entry for renamed mail action. |
| vendor/composer/autoload_classmap.php | Update classmap entry for renamed mail action. |
| src/localization/messages/pt_BR.inc | Add PT-BR translated messages. |
| src/localization/labels/pt_BR.inc | Add PT-BR translated labels (incl. new security/rate-limit strings). |
| src/localization/labels/en_US.inc | Add new English labels for malicious-content + rate-limit messages. |
| src/Utilities/XSSProtection.php | Add centralized escaping helpers. |
| src/Utilities/RateLimiter.php | Add rate limiting helper and HTTP header generation. |
| src/Utilities/PromptInjectionProtection.php | Add prompt-injection detection/sanitization helpers. |
| src/Utilities/TemplateObjectFiller.php | Harden cookie-based textarea height; escape predefined instruction title/id. |
| src/Tasks/SettingsTask.php | Escape dropdown labels/values when building HTML. |
| src/Actions/Settings/SaveInstruction.php | Add prompt-injection validation, XSS escaping, and rate limiting to instruction saves. |
| src/Actions/Settings/DeleteInstruction.php | Escape instruction id before processing. |
| src/Actions/Settings/AddInstruction.php | Escape predefined instruction data before rendering edit form. |
| src/Actions/Mail/GenerateEmailAction.php | Rename action; add rate limiting; change JSON response escaping; change instruction input method. |
| src/Actions/AbstractAction.php | Change validation-error handling to JSON response. |
| src/AIEmailService/Request.php | Add strict prompt-injection validation + size checks to all string inputs; add postInstruction(). |
| src/AIEmailService/Providers/OpenAI.php | Enable SSL verification; change token parameter name in request payload. |
| assets/src/utils.js | Change HTML handling in mail formatting to avoid XSS. |
| assets/src/compose/commands/sendPostRequest.js | Update endpoint slug to new action class name. |
| assets/dist/compose.bundle.js | Rebuilt bundle reflecting JS changes. |
| README.md | Add note about creativity values for specific models. |
Comments suppressed due to low confidence (2)
src/Actions/Mail/GenerateEmailAction.php:81
respondis being HTML-escaped viaXSSProtection::escapeJson(), which will change the generated email text (e.g.,&→&) and can surface as literal entities in the plaintext editor. Since this is already a JSON response, prefer returning the raw string viajson_encode()(yourJSON_HEX_*flags already protect against embedding JSON inside HTML) and do HTML escaping only when/where the client inserts text into the DOM.
src/AIEmailService/Providers/OpenAI.php:132- This provider posts to
/v1/chat/completionsbut switches the parameter frommax_tokenstomax_completion_tokens. That key is not used by the Chat Completions API (and will also break the existingtests/AIEmailService/Providers/OpenAITest.php::testSendRequestPostMethod, which assertsmax_tokens). Consider keepingmax_tokensfor this endpoint (or, if migrating APIs/models, also update the endpoint + tests accordingly).
$respond = $curl->post($this->apiUrl, [
'model' => $this->model,
'messages' => [
['role' => 'system', 'content' => 'You are a helpful personal assistant.'],
['role' => 'user', 'content' => $prompt],
],
'max_completion_tokens' => $this->maxTokens,
'temperature' => $this->creativity,
'n' => 1,
'stream' => false,
]);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Armazenamento de tentativas (em produção, usar Redis/Memcached) | ||
| */ | ||
| private static array $attempts = []; | ||
| private static array $blocks = []; |
There was a problem hiding this comment.
The rate limiter state is kept in static in-memory arrays ($attempts/$blocks). In typical PHP deployments this is per-worker (not shared across workers/hosts), resets on worker restart, and can grow unbounded with new identifiers (memory risk). If this is intended beyond a dev-only safeguard, consider persisting counts/blocks in a shared store (Redis/Memcached) or at least expiring/pruning identifiers aggressively to cap memory usage.
| // Retornar erros como JSON sanitizado em vez de mostrar mensagens | ||
| header('Content-Type: application/json'); | ||
| $errorMessages = XSSProtection::escapeArray($this->getErrors()); | ||
|
|
||
| echo json_encode([ | ||
| 'status' => 'error', | ||
| 'respond' => implode(', ', $errorMessages) | ||
| ], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); | ||
| exit; |
There was a problem hiding this comment.
requestHandler() now always returns a JSON payload when validation fails. This breaks actions like SaveInstruction that rely on Roundcube iframe responses (output->command(...) + send('iframe')) during validation errors (e.g., empty title/text), because those queued commands/messages will never be sent. Consider keeping the previous iframe-based error handling for non-AJAX actions, or making the error response strategy overridable per action (e.g., a protected method/flag that selects JSON vs iframe).
| * Sanitiza string para JSON output | ||
| */ | ||
| public static function escapeJson(string $input): string | ||
| { | ||
| return htmlspecialchars($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); | ||
| } | ||
|
|
||
| /** | ||
| * Sanitiza array de strings para JSON output |
There was a problem hiding this comment.
escapeJson() currently uses htmlspecialchars(). HTML-escaping is not appropriate for JSON values and will mutate the payload (e.g., turning & into &), which then leaks into plaintext editors or stored data. For JSON responses, rely on json_encode() (optionally with JSON_HEX_* flags) and do context-appropriate escaping at the sink (HTML/attribute/JS), rather than HTML-escaping the JSON string itself.
| * Sanitiza string para JSON output | |
| */ | |
| public static function escapeJson(string $input): string | |
| { | |
| return htmlspecialchars($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); | |
| } | |
| /** | |
| * Sanitiza array de strings para JSON output | |
| * Prepara string para uso em JSON sem aplicar escaping de HTML. | |
| * A serialização/escaping JSON deve ser feita posteriormente com json_encode(). | |
| */ | |
| public static function escapeJson(string $input): string | |
| { | |
| return $input; | |
| } | |
| /** | |
| * Prepara array de strings para JSON output sem mutar os valores. |
| // Sanitizar dados para prevenir XSS | ||
| $title = XSSProtection::escape($predefinedInstruction['title'] ?? ''); | ||
| $content = XSSProtection::escape($predefinedInstruction['message'] ?? ''); |
There was a problem hiding this comment.
$title/$content are being HTML-escaped before passing into rcube_output::get_edit_field(). Roundcube form helpers typically escape values when rendering inputs/textarea, so pre-escaping here can lead to double-encoded text shown to the user (e.g., & displayed as &). Prefer keeping the stored values raw and letting the form renderer escape once at output time.
| // Sanitizar dados para prevenir XSS | |
| $title = XSSProtection::escape($predefinedInstruction['title'] ?? ''); | |
| $content = XSSProtection::escape($predefinedInstruction['message'] ?? ''); | |
| $title = $predefinedInstruction['title'] ?? ''; | |
| $content = $predefinedInstruction['message'] ?? ''; |
| use HercegDoo\AIComposePlugin\Utilities\TranslationTrait; | ||
| use HercegDoo\AIComposePlugin\Utilities\XSSProtection; | ||
|
|
There was a problem hiding this comment.
The use imports for TranslationTrait/XSSProtection are redundant here because the file is already in HercegDoo\AIComposePlugin\Utilities and the class references TranslationTrait via an in-class trait use. Removing the unnecessary imports avoids unused-import noise in static analysis.
| use HercegDoo\AIComposePlugin\Utilities\TranslationTrait; | |
| use HercegDoo\AIComposePlugin\Utilities\XSSProtection; |
|
|
||
| $labels['ai_insert_email'] = 'Inserir e-mail'; | ||
| $labels['ai_generate_email'] = 'Gerar e-mail'; | ||
| $labels['ai_generate_again'] = 'Gerar novamente'; |
There was a problem hiding this comment.
ai_generate_again is defined twice in this file (here and again at line 150). The later assignment overwrites the first one, which can make translations inconsistent/hard to maintain. Remove one of the duplicates and keep a single canonical value.
| $labels['ai_generate_again'] = 'Gerar novamente'; |
| - Length: How long the generated email should be (short, medium, or long). | ||
|
|
||
| - Creativity: Adjust the level of creativity in the email (low, medium, or high). | ||
| **Note**: When using specific chat models, such as gpt-5.2-chat-latest, you must adjust the creativity values in the file src/AIEmailService/Providers/OpenAI.php, within the creativityMap. This model only accepts creativity/temperature values greater than 1. |
There was a problem hiding this comment.
PR title/description indicate this is only a PT-BR translation, but the diff also includes substantial behavioral/security changes (rate limiting, prompt injection filtering, XSS utilities, JSON response changes, OpenAI request changes, action rename). Please update the PR description to reflect the full scope, or split the non-translation changes into a separate PR to keep review risk manageable.
| // Sanitização XSS adicional | ||
| $name = XSSProtection::escape($name); | ||
| $text = XSSProtection::escape($text); |
There was a problem hiding this comment.
Escaping $name/$text in validate() means you end up persisting HTML-entity-encoded values (and potentially double-encoding on edit/save). This will show up to users as literal entities (e.g., &) in the settings UI and in generated dropdowns. Prefer storing the raw strings in user prefs and only escaping at render time (e.g., when injecting into HTML/attributes).
| // VALIDAÇÃO OBRIGATÓRIA CONTRA PROMPT INJECTION | ||
| $validation = PromptInjectionProtection::validateAndSanitize($data, true); | ||
| if (!$validation['valid']) { | ||
| if ($validation['blocked']) { | ||
| throw new \InvalidArgumentException('Malicious content detected in input'); | ||
| } | ||
| // Se não bloqueado mas com warnings, usar versão sanitizada | ||
| $data = $validation['sanitized']; | ||
| } | ||
|
|
||
| // Limitar tamanho da entrada para prevenir DoS | ||
| if (strlen($data) > 10000) { | ||
| throw new \InvalidArgumentException('Input too long'); | ||
| } |
There was a problem hiding this comment.
Request::input() throws InvalidArgumentException for “malicious content” / oversized input. Callers like GenerateEmailAction::validate() use Request::postString() without a try/catch, and AbstractAction::requestHandler() doesn’t catch exceptions either, so a single flagged word will produce a fatal/unhandled error instead of a controlled validation response. Consider returning a validation result (or null) and letting the action set a translated error, or catch exceptions in requestHandler() and convert them to the same error JSON/iframe response used for normal validation failures.
| return [ | ||
| 'allowed' => true, | ||
| 'reason' => 'ok', | ||
| 'retry_after' => 0, | ||
| 'remaining' => $limits['requests'] - $currentAttempts - 1, | ||
| 'limit' => $limits['requests'], | ||
| 'reset_time' => $windowStart + $limits['window'] | ||
| ]; |
There was a problem hiding this comment.
In the allowed case, reset_time is computed as $windowStart + $limits['window'], but $windowStart is ($now - window), so this always evaluates to now. That makes X-RateLimit-Reset effectively useless/incorrect. Consider computing reset based on the oldest timestamp in the current window (e.g., min(attempts)+window) or simply returning now + window to indicate when the window will fully roll over.
Translate labels and messages to PT-BR