diff --git a/lib/Plugins/Features/AbstractRevisionFeature.php b/lib/Plugins/Features/AbstractRevisionFeature.php index 93a8be55e8..febb18993f 100644 --- a/lib/Plugins/Features/AbstractRevisionFeature.php +++ b/lib/Plugins/Features/AbstractRevisionFeature.php @@ -348,19 +348,6 @@ public function postJobMerged( $projectStructure ) { } } - /** - * Entry point for project data validation for this feature. - * - * @param $projectStructure - * - * @throws ConnectionException - * @throws \Exceptions\ValidationError - * @throws ReflectionException - */ - public function validateProjectCreation( $projectStructure ) { - self::loadAndValidateModelFromJsonFile( $projectStructure ); - } - /** * * project_completion_event_saved @@ -368,6 +355,8 @@ public function validateProjectCreation( $projectStructure ) { * @param Jobs_JobStruct $chunk * @param CompletionEventStruct $event * @param $completion_event_id + * + * @throws Exception */ public function project_completion_event_saved( Jobs_JobStruct $chunk, CompletionEventStruct $event, $completion_event_id ) { $model = new QualityReportModel( $chunk ); @@ -466,7 +455,7 @@ public function job_password_changed( Jobs_JobStruct $job, $old_password ) { private function setQaModelFromJsonFile( $projectStructure ) { /** @var RecursiveArrayObject $model_json */ - $model_json = $projectStructure[ 'features' ][ 'review_extended' ][ '__meta' ][ 'qa_model' ]; + $model_json = $projectStructure[ 'features' ][ 'quality_framework' ]; $model_record = ModelDao::createModelFromJsonDefinition( $model_json->toArray() ); @@ -479,95 +468,106 @@ private function setQaModelFromJsonFile( $projectStructure ) { } /** - * Validate the project is valid in the scope of ReviewExtended feature. - * A project is valid if we area able to find a qa_model.json file inside - * a __meta folder. The qa_model.json file must be valid too. + * Validate the project is valid in the scope of the ReviewExtended feature. + * A project is valid if we can find a qa_model.json file inside a `__meta` folder. + * The qa_model.json file must also be valid. * - * If validation fails, adds errors to the projectStructure. + * If validation fails, add errors to the projectStructure. * - * @param $projectStructure + * @param ArrayObject $projectStructure * @param string|null $jsonPath * * @throws ConnectionException * @throws ReflectionException + * @throws Exception */ - - public static function loadAndValidateModelFromJsonFile( &$projectStructure, ?string $jsonPath = null ) { - + public static function loadAndValidateQualityFramework( ArrayObject &$projectStructure, ?string $jsonPath = null ) { + if ( get_called_class() instanceof ReviewExtended || get_called_class() == ReviewExtended::class ) { return; } - // CASE 1 there is an injected QA template id from API or UI - /** RecursiveArrayObject */ - if ( !empty( $projectStructure[ 'qa_model_template' ] ) ) { - $decoded_model = $projectStructure[ 'qa_model_template' ]; - } // CASE 2 there a is an injected qa_model, from api by passing an explicit qa_model_id - elseif ( !empty( $projectStructure[ 'qa_model' ] ) ) { - $decoded_model = $projectStructure[ 'qa_model' ]; - } // CASE3 otherwise, load default or from zip file - else { - // detect if the project created was a zip file, in which case try to detect - // id_qa_model from json file. - // otherwise assign the default model - - $qa_model = false; - $fs = FilesStorageFactory::create(); - $zip_file = $fs->getTemporaryUploadedZipFile( $projectStructure[ 'uploadToken' ] ); - - Log::doJsonLog( $zip_file ); - - if ( $zip_file !== false ) { - $zip = new ZipArchive(); - $zip->open( $zip_file ); - $qa_model = $zip->getFromName( '__meta/qa_model.json' ); - - if ( AbstractFilesStorage::isOnS3() ) { - unlink( $zip_file ); - } - - } - - // File is not a zip OR model was not found in zip - Log::doJsonLog( "QA model is : " . var_export( $qa_model, true ) ); - - if ( empty( $qa_model ) ) { - if ( $jsonPath == null ) { - $qa_model = file_get_contents( INIT::$ROOT . '/inc/qa_model.json' ); - } else { - $qa_model = file_get_contents( $jsonPath ); - } - } + // Use Null Coalescing Operator to simplify checks for template or model + $decoded_model = $projectStructure[ 'qa_model_template' ] ?? $projectStructure[ 'qa_model' ]; - $decoded_model = new RecursiveArrayObject( json_decode( $qa_model, true ) ); - - // set the user id to allow having ownership in qa_models table - $decoded_model[ 'model' ][ 'uid' ] = $projectStructure[ 'uid' ]; + // Try to load from ZIP file if no model is injected + if ( empty( $decoded_model ) ) { + $decoded_model = self::extractQaModelFromZip( $projectStructure[ 'uploadToken' ] ); + } + // Still empty? + if ( empty( $decoded_model ) ) { + $decoded_model = self::loadModelFromPathOrDefault( $projectStructure, $jsonPath ); } + // If decoding the model failed, register the error if ( empty( $decoded_model ) ) { $projectStructure[ 'result' ][ 'errors' ][] = [ - 'code' => '-900', // TODO: decide how to assign such errors + 'code' => '-900', 'message' => 'QA model failed to decode' ]; } - /** - * Append the qa model to the project structure for later use. - */ + // Initialize features if not already set if ( !isset( $projectStructure[ 'features' ] ) ) { $projectStructure[ 'features' ] = []; } - $projectStructure[ 'features' ] = [ - 'review_extended' => [ - '__meta' => [ - 'qa_model' => $decoded_model - ] - ] - ]; + // Append the QA model to the project structure + $projectStructure[ 'features' ][ 'quality_framework' ] = $decoded_model; + + } + + /** + * Get a model from path or default + * + * @param ArrayObject $projectStructure + * @param string|null $jsonPath + * + * @return array|RecursiveArrayObject + */ + private static function loadModelFromPathOrDefault( ArrayObject $projectStructure, ?string $jsonPath ) { + + if ( empty( $qa_model ) ) { + // Use null coalescing to simplify fallback logic + $path = $jsonPath ?? INIT::$ROOT . '/inc/qa_model.json'; + $qa_model = file_get_contents( $path ); + } + + $decoded_model = new RecursiveArrayObject( json_decode( $qa_model, true ) ); + // Set the user ID to allow ownership in the QA models table + $decoded_model[ 'model' ][ 'uid' ] = $projectStructure[ 'uid' ]; + + return $decoded_model; + } + + /** + * Extract QA model from ZIP file + * + * @throws ReflectionException + * @throws ConnectionException + * @throws Exception + */ + private static function extractQaModelFromZip( $uploadToken ) { + $fs = FilesStorageFactory::create(); + $zip_file = $fs->getTemporaryUploadedZipFile( $uploadToken ); + + if ( $zip_file === false ) { + return null; + } + + $zip = new ZipArchive(); + $qa_model = null; + if ( $zip->open( $zip_file ) === true ) { + $qa_model = $zip->getFromName( '__meta/qa_model.json' ); + $zip->close(); + } + + if ( AbstractFilesStorage::isOnS3() ) { + unlink( $zip_file ); + } + return $qa_model; } /** diff --git a/lib/Utils/ProjectManager.php b/lib/Utils/ProjectManager.php index 3e2f365898..fd7ad1e8a0 100644 --- a/lib/Utils/ProjectManager.php +++ b/lib/Utils/ProjectManager.php @@ -577,6 +577,8 @@ public function createProject() { * Validations should populate the projectStructure with errors and codes. */ $featureSet = ( $this->features !== null ) ? $this->features : new FeatureSet(); + \Features\SecondPassReview::loadAndValidateQualityFramework( $this->projectStructure ); + $featureSet->run( 'loadCustomQualityFramework', $this->projectStructure ); $featureSet->run( 'validateProjectCreation', $this->projectStructure ); $this->filter = MateCatFilter::getInstance( $featureSet, $this->projectStructure[ 'source_language' ], $this->projectStructure[ 'target_language' ] ); @@ -585,6 +587,7 @@ public function createProject() { * @var ArrayObject $this ->projectStructure['result']['errors'] */ if ( $this->projectStructure[ 'result' ][ 'errors' ]->count() ) { + $this->_log( $this->projectStructure[ 'result' ][ 'errors' ] ); return false; } diff --git a/plugins/airbnb b/plugins/airbnb index a8fb830574..3a37c5f708 160000 --- a/plugins/airbnb +++ b/plugins/airbnb @@ -1 +1 @@ -Subproject commit a8fb8305741c6af856236259c9b30c8745baa8e6 +Subproject commit 3a37c5f708bb489ae2a41a2402055fababbcba44 diff --git a/plugins/patreon b/plugins/patreon index 140f792260..3bee98d74d 160000 --- a/plugins/patreon +++ b/plugins/patreon @@ -1 +1 @@ -Subproject commit 140f792260be2b0d5cb74d4e530eb3ad6daa34ac +Subproject commit 3bee98d74d7f540b7e7467b59def3189296f6658 diff --git a/plugins/skyscanner b/plugins/skyscanner index 594b61cb67..5a82f7b5d9 160000 --- a/plugins/skyscanner +++ b/plugins/skyscanner @@ -1 +1 @@ -Subproject commit 594b61cb6770196f5a1c6cc7ff935801d3af4d62 +Subproject commit 5a82f7b5d9f4cacdf28fa317b1c1e1b21d3d9de4 diff --git a/plugins/uber b/plugins/uber index a33e6078d5..67e31dbc63 160000 --- a/plugins/uber +++ b/plugins/uber @@ -1 +1 @@ -Subproject commit a33e6078d55407b2355066f4580c1ed2c5d7e0cc +Subproject commit 67e31dbc63f2989a9533eac03d6309c7e26769c7