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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/Controller/API/App/CreateProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ private function validateMtEngine(?int $mt_engine = 0): array
* @return array<string, mixed>
* @throws \DomainException
* @throws \TypeError
* @throws InvalidArgumentException
*/
private static function sanitizeTmKeyArr(array $elem): array
{
Expand Down
9 changes: 7 additions & 2 deletions lib/Controller/API/App/TMXFileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Model\TmKeyManagement\MemoryKeyStruct;
use TypeError;
use Utils\Registry\AppConfig;
use Utils\TmKeyManagement\TmKeyManager;
use Utils\TmKeyManagement\TmKeyStruct;
use Utils\TMS\TMSFile;
use Utils\TMS\TMSService;
Expand Down Expand Up @@ -69,8 +70,12 @@ public function import(): void
$userMemoryKey = $mkDao->read($searchMemoryKey);

if (!empty($userMemoryKey) && isset($userMemoryKey[0]->tm_key) && empty($userMemoryKey[0]->tm_key->name)) {
$userMemoryKey[0]->tm_key->name = $fileInfo->name;
$mkDao->atomicUpdate($userMemoryKey[0]);
try {
$userMemoryKey[0]->tm_key->name = TmKeyManager::validateName($fileInfo->name);
$mkDao->atomicUpdate($userMemoryKey[0]);
} catch (InvalidArgumentException) {
// the TMX filename is not a usable resource name: skip the optional rename
}
}
}
}
Expand Down
10 changes: 0 additions & 10 deletions lib/Controller/API/App/TmKeyManagementController.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,6 @@ private function sortKeysInTheRightOrder(array $keys, array $jobKeyList): array

}

if (!empty($sortedKeys)) {
$sortedKeys = array_map(function (ClientTmKeyStruct $jobKey) {
if ($jobKey->name !== null) {
$jobKey->name = html_entity_decode($jobKey->name);
}

return $jobKey;
}, $sortedKeys);
}

return $sortedKeys;
}

Expand Down
24 changes: 5 additions & 19 deletions lib/Controller/API/App/UserKeysController.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,32 +166,18 @@ private function validateTheRequest(): array
{
$key = filter_var($this->request->param('key'), FILTER_SANITIZE_SPECIAL_CHARS, ['flags' => FILTER_FLAG_STRIP_LOW]);
$emails = filter_var($this->request->param('emails'), FILTER_SANITIZE_SPECIAL_CHARS, ['flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH]);
$description = filter_var($this->request->param('description'), FILTER_SANITIZE_SPECIAL_CHARS, ['flags' => FILTER_FLAG_STRIP_LOW]);
$description = $this->request->param('description');
$remove_from = filter_var($this->request->param('remove_from'), FILTER_SANITIZE_FULL_SPECIAL_CHARS, ['flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH]);

// check for eventual errors on the input passed
if (empty($key)) {
throw new InvalidArgumentException("Key missing", -2);
}

// Prevent XSS attack
// ===========================
// POC. Try to add this string in the input:
// <details x=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:2 open ontoggle="prompt(document.cookie);">
// in this case, an error MUST be thrown
if ($this->request->param('description') and $this->request->param('description') !== $description) {
throw new InvalidArgumentException(
"<span>Resource names cannot contain the following characters:</span>"
. "<ul>"
. "<li>&lt; (less than)</li>"
. "<li>&gt; (greater than)</li>"
. "<li>&amp; (ampersand)</li>"
. "<li>&quot; (double quote)</li>"
. "<li>&#39; (single quote)</li>"
. "</ul>",
-3
);
}
// Names are stored raw: TmKeyManager::validateName() enforces semantics
// (string type, valid UTF-8, no invisible characters) and rejects bad
// input with code -3; HTML/XML escaping happens at each output sink.
$description = TmKeyManager::validateName($description);

return [
'key' => $key,
Expand Down
1 change: 1 addition & 0 deletions lib/Controller/API/V1/NewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ function ($item) {
* @return array<string, mixed>
* @throws \DomainException
* @throws \TypeError
* @throws InvalidArgumentException
*/
private static function sanitizeTmKeyArr(array $elem): array
{
Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/API/V3/DownloadQRController.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ private function createXMLFile(array $data, array $categoryIssues = []): false|s
$xml .= '<id_file>' . $datum[10] . '</id_file>';
$xml .= '<warning>' . $datum[11] . '</warning>';
$xml .= '<suggestion_match>' . $datum[12] . '</suggestion_match>';
$xml .= '<suggestion_source>' . $datum[13] . '</suggestion_source>';
$xml .= '<suggestion_source>' . htmlspecialchars($datum[13] ?? '', ENT_QUOTES | ENT_XML1, 'UTF-8') . '</suggestion_source>';
$xml .= '<suggestion>' . $datum[14] . '</suggestion>';
$xml .= '<edit_distance>' . $datum[15] . '</edit_distance>';
$xml .= '<locked>' . $datum[16] . '</locked>';
Expand Down
2 changes: 1 addition & 1 deletion lib/Utils/TMS/TMSService.php
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ private function buildTmOriginProp(array $row, ?int $uid): string

$suggestionsArray = json_decode($row['suggestions_array'], true);
$suggestionOrigin = Utils::changeMemorySuggestionSource($suggestionsArray[0], $row['tm_keys'], $this->database, $uid);
$tmOrigin = '<prop type="x-MateCAT-suggestion-origin">' . $suggestionOrigin . "</prop>";
$tmOrigin = '<prop type="x-MateCAT-suggestion-origin">' . htmlspecialchars($suggestionOrigin, ENT_QUOTES | ENT_XML1, 'UTF-8') . "</prop>";
if (preg_match("/[a-f0-9]{8,}/", $suggestionsArray[0]['memory_key'])) {
$tmOrigin .= "\n <prop type=\"x-MateCAT-suggestion-private-key\">" . $suggestionsArray[0]['memory_key'] . "</prop>";
}
Expand Down
50 changes: 45 additions & 5 deletions lib/Utils/TmKeyManagement/TmKeyManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use DomainException;
use Exception;
use InvalidArgumentException;
use Model\DataAccess\Database;
use Normalizer;
use Model\DataAccess\IDatabase;
use Model\TmKeyManagement\MemoryKeyDao;
use Model\TmKeyManagement\MemoryKeyStruct;
Expand Down Expand Up @@ -196,6 +198,47 @@ public static function isValidStructure(array $arr): TmKeyStruct|bool
return $myObj;
}

/**
* Validates and normalizes a user-provided resource name.
*
* Names are stored raw: no HTML escaping happens here, encoding for a
* specific destination (HTML, XML, email template) is the output layer's
* job. This method only enforces semantics: a name must be a UTF-8 string
* without invisible characters.
*
* @param mixed $name
*
* @return string|null
*
* @throws InvalidArgumentException
*/
public static function validateName(mixed $name): ?string
{
if (is_null($name)) {
return null;
}

if (!is_string($name)) {
throw new InvalidArgumentException("Resource name must be a string", -3);
}

if (!mb_check_encoding($name, 'UTF-8')) {
throw new InvalidArgumentException("Resource name is not valid UTF-8", -3);
}

// Cc covers NUL/ESC/newlines, Cf covers zero-width and bidi overrides
// used to spoof how a name reads. U+200D (ZWJ) is excluded so emoji
// sequences survive.
$name = preg_replace('/(?![\x{200D}])[\p{Cc}\p{Cf}]/u', '', $name) ?? '';
$name = Normalizer::normalize(trim($name), Normalizer::FORM_C);

if ($name === false) {
throw new InvalidArgumentException("Resource name is not valid UTF-8", -3);
}

return mb_substr($name, 0, 255);
}

/**
* This method sanitize fields received with struct
*
Expand All @@ -205,6 +248,7 @@ public static function isValidStructure(array $arr): TmKeyStruct|bool
*
* @throws \TypeError
* @throws DomainException
* @throws InvalidArgumentException
*/
public static function sanitize(TmKeyStruct $obj): TmKeyStruct
{
Expand All @@ -230,11 +274,7 @@ public static function sanitize(TmKeyStruct $obj): TmKeyStruct
$obj->uid_rev = $sanitized !== false ? (int)$sanitized : null;
}

if (!is_null($obj->name)) {
$obj->name = preg_replace('/[^.\-_\p{L}\p{N}\s{}]+/u', '', $obj->name);
$sanitized = filter_var($obj->name, FILTER_SANITIZE_SPECIAL_CHARS, ['flags' => FILTER_FLAG_STRIP_LOW]);
$obj->name = $sanitized !== false ? $sanitized : null;
}
$obj->name = self::validateName($obj->name);

if (!is_null($obj->key)) {
$sanitized = filter_var($obj->key, FILTER_SANITIZE_SPECIAL_CHARS, ['flags' => FILTER_FLAG_STRIP_LOW]);
Expand Down
6 changes: 3 additions & 3 deletions lib/View/Emails/ShareKey/message_content.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<p class="p_line_height_1_5">
Hello,
<br>
<?=$senderFullName?><? if( !empty( $senderEmail ) ): ?> (<?=$senderEmail?>)<? endif; ?> shared the following language resource with you.
<?=htmlspecialchars($senderFullName, ENT_QUOTES, 'UTF-8')?><? if( !empty( $senderEmail ) ): ?> (<?=htmlspecialchars($senderEmail, ENT_QUOTES, 'UTF-8')?>)<? endif; ?> shared the following language resource with you.
</p>
<p class="p_line_height_1_5">
<span class="align-center" style="font-weight: bold;">Description:</span> <span><?=$tm_key_name?></span>
<span class="align-center" style="font-weight: bold;">Description:</span> <span><?=htmlspecialchars($tm_key_name, ENT_QUOTES, 'UTF-8')?></span>
<br>
<span class="align-center" style="font-weight: bold;">Private key:</span> <span> <?=$tm_key_value?></span>
</p>
<p class="p_line_height_1_5">
If you have an account on Matecat registered to <?=$addressMail?>, you will find the language resource in the 'Translation Memory and Glossary' tab of the settings panel.
If you have an account on Matecat registered to <?=htmlspecialchars($addressMail, ENT_QUOTES, 'UTF-8')?>, you will find the language resource in the 'Translation Memory and Glossary' tab of the settings panel.
</p>
<p class="p_bottom_5">Otherwise:</p>
<ol>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"classnames": "^2.2.6",
"crypto-js": "^4.1.1",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.4.12",
"draft-js": "^0.11.4",
"events": "^3.3.0",
"file-saver": "^2.0.5",
Expand Down
4 changes: 3 additions & 1 deletion public/js/actions/CatToolActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ let CatToolActions = {
* tc (top center), br (bottom right), bl (bottom left), bc (bottom center)
* closeCallback (Function) A callback function that will be called when the notification is about to be removed.
* openCallback (Function) A callback function that will be called when the notification is successfully added.
* allowHtml: (Boolean, Default false) Set to true if the text contains HTML, like buttons
* allowHtml: (Boolean, Default false) Reserved for operator-authored broadcasts (SSE global messages).
* The HTML is sanitized before rendering. For anything interpolating user data,
* pass a ReactNode as text instead.
* autoDismiss: (Boolean, Default true) Set if notification is dismissible by the user.
*
*/
Expand Down
7 changes: 0 additions & 7 deletions public/js/actions/ManageActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@ let ManageActions = {
text: 'Something went wrong, the project has been assigned to another member or moved to another team.',
type: 'warning',
position: 'bl',
allowHtml: true,
autoDismiss: false,
}
CatToolActions.addNotification(notification)
Expand Down Expand Up @@ -234,7 +233,6 @@ let ManageActions = {
name,
type: 'success',
position: 'bl',
allowHtml: true,
timer: 3000,
}
CatToolActions.addNotification(notification)
Expand Down Expand Up @@ -283,7 +281,6 @@ let ManageActions = {
text: `The selected projects have been successfully assigned to ${user.first_name} ${user.last_name}.`,
type: 'warning',
position: 'bl',
allowHtml: true,
timer: 10000,
}
CatToolActions.addNotification(notification)
Expand All @@ -293,7 +290,6 @@ let ManageActions = {
text: 'Some projects failed',
type: 'error',
position: 'bl',
allowHtml: true,
timer: 10000,
}
CatToolActions.addNotification(errorNotification)
Expand Down Expand Up @@ -366,7 +362,6 @@ let ManageActions = {
' Team',
type: 'success',
position: 'bl',
allowHtml: true,
timer: 3000,
}
CatToolActions.addNotification(notification)
Expand Down Expand Up @@ -457,7 +452,6 @@ let ManageActions = {
text: `The selected projects have been successfully moved to the ${team.name} team.`,
type: 'warning',
position: 'bl',
allowHtml: true,
timer: 10000,
}
CatToolActions.addNotification(notification)
Expand All @@ -467,7 +461,6 @@ let ManageActions = {
text: 'Some projects failed',
type: 'error',
position: 'bl',
allowHtml: true,
timer: 10000,
}
CatToolActions.addNotification(errorNotification)
Expand Down
22 changes: 16 additions & 6 deletions public/js/actions/SegmentActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,13 +603,24 @@ const SegmentActions = {
!config.isReview &&
config.job_completion_current_phase == 'revise'
if (projectCompletionCheck) {
let message =
'All segments are in <b>read-only mode</b> because this job is under review.'
let message = (
<>
All segments are in <b>read-only mode</b> because this job is under
review.
</>
)

if (config.chunk_completion_undoable && config.last_completion_event_id) {
message =
message +
'<p class=\'warning-call-to\'><a href="javascript:void(0);" id="showTranslateWarningMessageUndoLink" >Re-Open Job</a></p>'
message = (
<>
{message}
<p className="warning-call-to">
<a href="#" id="showTranslateWarningMessageUndoLink">
Re-Open Job
</a>
</p>
</>
)
}

addNotification({
Expand All @@ -620,7 +631,6 @@ const SegmentActions = {
text: message,
title: 'Warning',
type: 'warning',
allowHtml: true,
})
}
if (TextUtils.justSelecting('readonly')) return
Expand Down
22 changes: 16 additions & 6 deletions public/js/components/header/cattol/MarkAsCompleteButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,24 @@ export const MarkAsCompleteButton = ({featureEnabled, isReview}) => {

const showTranslateWarningMessage = () => {
// if (!lastCompletionEventId) return
let message =
'All segments are in <b>read-only mode</b> because this job is under review.'
let message = (
<>
All segments are in <b>read-only mode</b> because this job is under
review.
</>
)

if (config.chunk_completion_undoable && config.last_completion_event_id) {
message =
message +
'<p class=\'warning-call-to\'><a href="javascript:void(0);" id="showTranslateWarningMessageUndoLink" >Re-Open Job</a></p>'
message = (
<>
{message}
<p className="warning-call-to">
<a href="#" id="showTranslateWarningMessageUndoLink">
Re-Open Job
</a>
</p>
</>
)
}

CatToolActions.addNotification({
Expand All @@ -78,7 +89,6 @@ export const MarkAsCompleteButton = ({featureEnabled, isReview}) => {
text: message,
title: 'Warning',
type: 'warning',
allowHtml: true,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ let SegmentFilterUtils = {
text: text,
title: title,
type: 'warning',
allowHtml: true,
})
})()
},
Expand Down
1 change: 0 additions & 1 deletion public/js/components/modals/ShareTmModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ class ShareTmModal extends React.Component {
type: 'success',
text: `The resource has been shared.`,
position: 'br',
allowHtml: true,
timer: 5000,
})
callback.call()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
* position: (String, Default "bl") Position of the notification. Available: tr (top right), tl (top left),
* tc (top center), br (bottom right), bl (bottom left), bc (bottom center)
* autoDismiss: (Boolean, Default true) Set if notification is dismissible by the user.
* allowHtml: (Boolean, Default false) Set to true if the text contains HTML, like buttons
* allowHtml: (Boolean, Default false) Reserved for operator-authored broadcasts (SSE global messages).
* The HTML is sanitized before rendering. For anything interpolating user data,
* pass a ReactNode as text instead.
* closeCallback (Function) A callback function that will be called when the notification is about to be removed.
* openCallback (Function) A callback function that will be called when the notification is successfully added.
* dismissable (Boolean, Default true) If show or not the button to close the notification
Expand Down
Loading
Loading