Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
81efcdf
Cancel request APIs
mauretto78 Apr 8, 2026
afc4ded
Requests cancellation
mauretto78 Apr 9, 2026
3e86ed5
wip
mauretto78 Apr 13, 2026
bc890e9
Merge branch 'develop' into cancel-request
mauretto78 Apr 14, 2026
e3e4047
refactoring
mauretto78 Apr 14, 2026
606f9b0
Merge branch 'develop' into cancel-request
mauretto78 Apr 20, 2026
b84ab74
Fix checks in setTranslation
mauretto78 Apr 20, 2026
00f002d
enable route
mauretto78 Apr 20, 2026
2105f92
Fixed Database::rollback method
mauretto78 Apr 20, 2026
de804bc
swagger docs
mauretto78 Apr 20, 2026
6c2f634
Ui integration
piedicianni Apr 21, 2026
5040d59
Fix
piedicianni Apr 21, 2026
c803e1d
Removed log
piedicianni Apr 21, 2026
d44c8e3
Merge branch 'develop' into cancel-request
mauretto78 Apr 27, 2026
2dfe516
Merge branch 'cancel-request' of github.com:matecat/MateCat into canc…
mauretto78 Apr 27, 2026
fcd77fe
Apply suggestion from @Copilot
mauretto78 Apr 27, 2026
789a457
Apply suggestion from @Copilot
mauretto78 Apr 27, 2026
4b39a72
Merge branch 'cancel-request' of github.com:matecat/MateCat into canc…
mauretto78 Apr 27, 2026
6d8fdd7
fixed phpstan
mauretto78 Apr 27, 2026
48d9dd6
phpstan fix
mauretto78 Apr 27, 2026
aafd8e1
Update lib/Controller/API/V3/CancelRequestController.php
mauretto78 Apr 28, 2026
7d668c1
Changing http verbs
mauretto78 Apr 28, 2026
1f26aec
Merge branch 'cancel-request' of github.com:matecat/MateCat into canc…
mauretto78 Apr 28, 2026
b94b9b2
coplit review
mauretto78 Apr 28, 2026
dfe81ba
fix
mauretto78 Apr 28, 2026
2c1245c
fixing phpstan errors
mauretto78 Apr 28, 2026
8e54b2b
Merge branch 'develop' into cancel-request
mauretto78 Apr 28, 2026
21f6db9
Merge remote-tracking branch 'origin/develop' into cancel-request
Ostico Apr 28, 2026
173adc0
✅ test(segment): add unit tests for segment disable/enable feature
riccio82 Apr 28, 2026
26b9f84
test: add unit tests for CancelRequestController, SegmentDisabledTrai…
mauretto78 Apr 28, 2026
e355db0
Merge branch 'cancel-request' of github.com:matecat/MateCat into canc…
mauretto78 Apr 28, 2026
45f9314
fix: correct Swagger docs for disable/enable segment endpoints - fix …
Copilot Apr 28, 2026
1a7cb68
fix: implement enable logic and use response->json in enableRequest
Copilot Apr 28, 2026
78d5ec0
fix: use response->json() in enableRequest and re-enable segment on e…
mauretto78 Apr 28, 2026
d981683
Merge branch 'cancel-request' of github.com:matecat/MateCat into canc…
mauretto78 Apr 28, 2026
45f62ef
fix: return boolean from isSegmentDisabled on cache miss path
mauretto78 Apr 28, 2026
6994a9b
Update lib/Controller/API/V3/CancelRequestController.php
mauretto78 Apr 28, 2026
a6e3077
Update lib/Controller/API/V3/CancelRequestController.php
mauretto78 Apr 28, 2026
fee5843
Merge branch 'develop' into cancel-request
Ostico Apr 28, 2026
fec5384
Update lib/Controller/Traits/SegmentDisabledTrait.php
mauretto78 Apr 28, 2026
4c983f0
fix: destroyCache in cancelRequest, mock findSegmentTranslation in te…
Copilot Apr 28, 2026
aca6c56
fix: add early return on 429 rate limit in cancelRequest
mauretto78 Apr 28, 2026
8839b2e
fix: make tests pass without DB connection
mauretto78 Apr 28, 2026
2aa6c0b
Merge branch 'cancel-request' of github.com:matecat/MateCat into canc…
mauretto78 Apr 28, 2026
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
25 changes: 24 additions & 1 deletion lib/Controller/API/App/SetTranslationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Controller\API\Commons\Exceptions\AuthenticationError;
use Controller\API\Commons\Validators\LoginValidator;
use Controller\Traits\APISourcePageGuesserTrait;
use Controller\Traits\SegmentDisabledTrait;
use Exception;
use InvalidArgumentException;
use Matecat\ICU\MessagePatternComparator;
Expand Down Expand Up @@ -57,7 +58,7 @@

class SetTranslationController extends AbstractStatefulKleinController
{

use SegmentDisabledTrait;
use APISourcePageGuesserTrait;
use ICUSourceSegmentChecker;

Expand Down Expand Up @@ -137,6 +138,7 @@ public function translate(): void

try {
$this->data = $this->validateTheRequest();
$this->checkIfSegmentIsNotDisabled();
$this->setSubFilteringBehavior();
$this->checkSegmentSplitData();
$this->initVersionHandler();
Expand Down Expand Up @@ -408,6 +410,7 @@ public function translate(): void
$this->response->json($result);
} catch (Exception $exception) {
$db->rollback();
$this->logger->error($exception->getMessage());
throw $exception;
}
}
Expand Down Expand Up @@ -554,6 +557,26 @@ private function validateTheRequest(): array
return $data;
}

/**
* checkIfIsNotDisabled
*
* Determines whether the segment associated with a specific job and segment ID
* is disabled by checking cached information. If the segment is found to be disabled,
* an exception is thrown.
*
* @return void
* @throws Exception If the segment is disabled.
*/
private function checkIfSegmentIsNotDisabled(): void
{
$id_job = $this->data['id_job'];
$id_segment = $this->data['id_segment'];

if ($this->isSegmentDisabled((int)$id_job, (int)$id_segment)) {
throw new RuntimeException("Segment #".$id_segment." is disabled", -5);
}
}

/**
* @return bool
*/
Expand Down
191 changes: 191 additions & 0 deletions lib/Controller/API/V3/CancelRequestController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php
/**
* Created by PhpStorm.
* User: vincenzoruffa
* Date: 13/09/2018
* Time: 13:03
*/

namespace Controller\API\V3;

use Controller\Abstracts\KleinController;
use Controller\API\Commons\Validators\LoginValidator;
use Controller\Traits\ChunkNotFoundHandlerTrait;
use Controller\Traits\RateLimiterTrait;
use Controller\Traits\SegmentDisabledTrait;
use Exception;
use Klein\Response;
use Model\DataAccess\DaoCacheTrait;
use Model\Exceptions\NotFoundException;
use Model\Segments\SegmentMetadataDao;
use Model\Translations\SegmentTranslationDao;
use Model\Translations\SegmentTranslationStruct;
use Utils\Constants\TranslationStatus;
use Utils\Tools\Utils;

class CancelRequestController extends KleinController
{
use DaoCacheTrait;
use RateLimiterTrait;
use SegmentDisabledTrait;
use ChunkNotFoundHandlerTrait;

protected function afterConstruct(): void
{
$this->appendValidator(new LoginValidator($this));
}

/**
* @throws Exception
*/
public function enableRequest(): void
{
$rawIdJob = $this->request->param('id_job');
$password = $this->request->param('password');
$rawIdSegment = $this->request->param('id_segment');
$id_job = filter_var($rawIdJob, FILTER_VALIDATE_INT);
$id_segment = filter_var($rawIdSegment, FILTER_VALIDATE_INT);
Comment thread
mauretto78 marked this conversation as resolved.
$route = '/api/v3/jobs/'.$id_job.'/'.$password.'/segment/enable/'.$id_segment;

$this->performChecks($id_job, $password, $id_segment, $route);

if ($this->response->code() === 429) {
return;
}

if($this->isSegmentDisabled($id_job, $id_segment)){
$this->destroySegmentDisabledCache($id_job, $id_segment);
}

$this->response->json([
'id_segment' => $id_segment,
]);
}
Comment thread
mauretto78 marked this conversation as resolved.

/**
* @throws Exception
*/
public function cancelRequest(): void
{
$rawIdJob = $this->request->param('id_job');
$password = $this->request->param('password');
$rawIdSegment = $this->request->param('id_segment');
$id_job = filter_var($rawIdJob, FILTER_VALIDATE_INT);
$id_segment = filter_var($rawIdSegment, FILTER_VALIDATE_INT);
$route = '/api/v3/jobs/'.$id_job.'/'.$password.'/segment/disable/'.$id_segment;

$this->performChecks($id_job, $password, $id_segment, $route);

if ($this->response->code() === 429) {
return;
}

// If the cache is empty, it means that the segment is not already disabled, so we can proceed with disabling it and
// setting the cache to avoid multiple disable requests for the same segment in a short time frame
if (!$this->isSegmentDisabled($id_job, $id_segment)) {
SegmentMetadataDao::destroyGetAllCache($id_segment);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancelRequest() updates the DB by inserting translation_disabled, but it only clears the getAll() cache. If SegmentMetadataDao::get($id_segment, 'translation_disabled') was cached as empty before disabling, it can stay stale (TTL=1 week) and later make isSegmentDisabled() incorrectly return false after Redis eviction. Clear the per-key cache too (e.g. destroy the get() cache entry) when disabling.

Suggested change
SegmentMetadataDao::destroyGetAllCache($id_segment);
SegmentMetadataDao::destroyGetAllCache($id_segment);
SegmentMetadataDao::destroyGetCache($id_segment, 'translation_disabled');

Copilot uses AI. Check for mistakes.
SegmentMetadataDao::setTranslationDisabled($id_segment);
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When disabling a segment, you update DB via setTranslationDisabled() but only destroy the getAll() cache. If SegmentMetadataDao::get($id_segment, 'translation_disabled') was previously cached as empty (TTL=1 week), isSegmentDisabled()'s DB fallback can keep returning "enabled" after its 1h Redis TTL expires. Invalidate the key-specific DAO cache too (e.g. SegmentMetadataDao::destroyCache($id_segment, 'translation_disabled')) when writing the flag.

Suggested change
SegmentMetadataDao::setTranslationDisabled($id_segment);
SegmentMetadataDao::setTranslationDisabled($id_segment);
SegmentMetadataDao::destroyCache($id_segment, 'translation_disabled');

Copilot uses AI. Check for mistakes.
$this->saveSegmentDisabledInCache($id_job, $id_segment);
}

$this->response->json([
'id_segment' => $id_segment,
]);
}

/**
* Performs several validation checks and rate limit handling for disabling a segment in a job.
*
* @param int $id_job The unique identifier of the job.
* @param string $password The password associated with the job for authentication.
* @param int $id_segment The unique identifier of the segment to be validated.
* @param string $route The API route being accessed.
*
* @throws NotFoundException If the job or segment is not found.
* @throws Exception If the user is not the owner or part of the team, or if the segment status is not "new".
*/
private function performChecks(int $id_job, string $password, int $id_segment, string $route): void
{
$userEmail = $this->user->email ?? "BLANK_EMAIL";
$userIp = Utils::getRealIpAddr() ?? "127.0.0.1";

// 1. check rate limit
$checkRateLimitEmail = $this->checkRateLimitResponse($this->response, $userEmail, $route, 5);
$checkRateLimitIp = $this->checkRateLimitResponse($this->response, $userIp, $route, 5);

if ($checkRateLimitIp instanceof Response) {
$this->response = $checkRateLimitIp;

return;
}

if ($checkRateLimitEmail instanceof Response) {
$this->response = $checkRateLimitEmail;

return;
}

// 2. check job id and password
$job = $this->getJob($id_job, $password);

if (null === $job) {
$this->incrementRateLimitCounter($userEmail, $route);
$this->incrementRateLimitCounter($userIp, $route);

throw new NotFoundException('Job not found.');
}

// 3. check segment translation
$segmentTranslation = $this->findSegmentTranslation($id_segment, $id_job);

if (empty($segmentTranslation)) {
$this->incrementRateLimitCounter($userEmail, $route);
$this->incrementRateLimitCounter($userIp, $route);

throw new NotFoundException('Segment not found');
}

// 4. check is user is the owner of the segment
$team = $job->getProject()->getTeam();

if(empty($team)){
$this->incrementRateLimitCounter($userEmail, $route);
$this->incrementRateLimitCounter($userIp, $route);

throw new NotFoundException('Team not found');
}

if(!empty($this->getUser()->uid) && $team->created_by != $this->getUser()->uid){

// check if user is part of the team
if (!$team->hasUser($this->getUser()->uid)){
$this->incrementRateLimitCounter($userEmail, $route);
$this->incrementRateLimitCounter($userIp, $route);

throw new Exception('User is not part of the team');
}
}

// 5. check segment status
if ($segmentTranslation->status !== TranslationStatus::STATUS_NEW) {
$this->incrementRateLimitCounter($userEmail, $route);
$this->incrementRateLimitCounter($userIp, $route);

throw new Exception('Segment is not in "new" status and cannot be disabled');
}

$this->incrementRateLimitCounter($userEmail, $route);
$this->incrementRateLimitCounter($userIp, $route);
}

/**
* @param int $id_segment
* @param int $id_job
*
* @return ?SegmentTranslationStruct
*/
protected function findSegmentTranslation(int $id_segment, int $id_job): ?SegmentTranslationStruct
{
return SegmentTranslationDao::findBySegmentAndJob($id_segment, $id_job);
}
}
5 changes: 5 additions & 0 deletions lib/Controller/API/V3/SegmentAnalysisController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Controller\Abstracts\KleinController;
use Controller\API\Commons\Validators\LoginValidator;
use Controller\Traits\SegmentDisabledTrait;
use Exception;
use Matecat\SubFiltering\MateCatFilter;
use Model\Analysis\Constants\ConstantsInterface;
Expand All @@ -29,6 +30,7 @@

class SegmentAnalysisController extends KleinController
{
use SegmentDisabledTrait;

const int MAX_PER_PAGE = 200;

Expand Down Expand Up @@ -346,6 +348,8 @@ private function formatSegment(
$metadataDao->getProjectStaticSubfilteringCustomHandlers($jobStruct->id_project)
);

$disabled = $this->isSegmentDisabled($segmentForAnalysis->id_job, $segmentForAnalysis->id);

return [
'id_segment' => (int)$segmentForAnalysis->id,
'id_chunk' => (int)$segmentForAnalysis->id_job,
Expand All @@ -366,6 +370,7 @@ private function formatSegment(
'notes' => (!empty($notesAggregate[$segmentForAnalysis->id]) ? $notesAggregate[$segmentForAnalysis->id] : []),
'status' => $this->getStatusObject($segmentForAnalysis),
'last_edit' => ($segmentForAnalysis->last_edit !== null) ? date(DATE_ATOM, strtotime($segmentForAnalysis->last_edit)) : null,
'disabled' => $disabled,
Comment on lines 351 to +373
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatSegment() calls isSegmentDisabled() per segment. On a cold cache this triggers one DB query per segment (up to 200/page) because isSegmentDisabled() falls back to SegmentMetadataDao::get(). Consider fetching all translation_disabled metadata in bulk (e.g. via SegmentMetadataDao::getBySegmentIds($segmentIds, 'translation_disabled') in getIssuesNotesAndIdRequests()) and deriving the disabled flag from that, avoiding an N+1 pattern.

Copilot uses AI. Check for mistakes.
];
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/Traits/ChunkNotFoundHandlerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ trait ChunkNotFoundHandlerTrait
* @return ?JobStruct
* @throws ReflectionException
*/
protected function getJob($id_job, $password): ?JobStruct
protected function getJob(int $id_job, string $password): ?JobStruct
{
$job = JobDao::getByIdAndPassword($id_job, $password);

Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/Traits/RateLimiterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,6 @@ private function getTtl(): int
$date = new DateTime();
$ttl = 60 - $date->format("s");

return 60 + $ttl;
return 60 + (int)$ttl;
}
}
Loading
Loading