* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
-class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation {
+class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation implements \Civi\Test\HookInterface {
+
+ use CRM_Core_DynamicFKAccessTrait;
/**
* Get a list of valid statuses for translated-strings.
return $f;
}
+ /**
+ * When manipulating strings via the `Translation` entity (APIv4), ensure that the references are well-formed.
+ *
+ * @param \Civi\Api4\Event\ValidateValuesEvent $e
+ */
+ public static function self_civi_api4_validate(\Civi\Api4\Event\ValidateValuesEvent $e) {
+ $statuses = self::getStatuses('validate');
+ $dataTypes = [CRM_Utils_Type::T_STRING, CRM_Utils_Type::T_TEXT, CRM_Utils_Type::T_LONGTEXT];
+ $htmlTypes = ['Text', 'TextArea', 'RichTextEditor'];
+
+ foreach ($e->records as $r => $record) {
+ if (array_key_exists('status_id', $record) && !isset($statuses[$record['status_id']])) {
+ $e->addError($r, 'status_id', 'invalid', ts('Invalid status'));
+ }
+
+ $entityIdFields = ['entity_table', 'entity_field', 'entity_id'];
+ $entityIdCount = (empty($record['entity_table']) ? 0 : 1)
+ + (empty($record['entity_field']) ? 0 : 1)
+ + (empty($record['entity_id']) ? 0 : 1);
+ if ($entityIdCount === 0) {
+ continue;
+ }
+ elseif ($entityIdCount < 3) {
+ $e->addError($r, $entityIdFields, 'full_entity', ts('Must specify all entity identification fields'));
+ }
+
+ $simpleName = '/^[a-zA-Z0-9_]+$/';
+ if (!preg_match($simpleName, $record['entity_table']) || !preg_match($simpleName, $record['entity_field']) || !is_numeric($record['entity_id'])) {
+ $e->addError($r, $entityIdFields, 'malformed_entity', ts('Entity reference is malformed'));
+ continue;
+ }
+
+ // Which fields support translation?
+ // - One could follow the same path as "Multilingual". Use
+ // $translatable = CRM_Core_I18n_SchemaStructure::columns();
+ // if (!isset($translatable[$record['entity_table']][$record['entity_field']])) {
+ // - Or, since we don't need schema-changes, we could be more generous and allow all freeform text fields...
+
+ $daoClass = CRM_Core_DAO_AllCoreTables::getClassForTable($record['entity_table']);
+ if (!$daoClass) {
+ $e->addError($r, 'entity_table', 'bad_table', ts('Entity reference specifies a non-existent or non-translatable table'));
+ continue;
+ }
+
+ $dao = new $daoClass();
+ $dao->id = $record['entity_id'];
+
+ $field = $dao->getFieldSpec($record['entity_field']);
+ if (!$field || !in_array($field['type'] ?? '', $dataTypes) || !in_array($field['html']['type'] ?? '', $htmlTypes)) {
+ $e->addError($r, 'entity_field', 'bad_field', ts('Entity reference specifies a non-existent or non-translatable field'));
+ }
+ if (!$dao->find()) {
+ $e->addError($r, 'entity_id', 'nonexistent_id', ts('Entity does not exist'));
+ }
+ }
+ }
+
}
--- /dev/null
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+
+namespace api\v4\Entity;
+
+use api\v4\UnitTestCase;
+
+/**
+ * @group headless
+ */
+class TranslationTest extends UnitTestCase {
+
+ protected $ids = [];
+
+ public function getCreateOKExamples() {
+ $es = [];
+
+ $es['asDraft'] = [
+ [
+ 'status_id:name' => 'draft',
+ 'entity_table' => 'civicrm_event',
+ 'entity_field' => 'description',
+ 'entity_id' => '*EVENT*',
+ 'language' => 'fr_CA',
+ 'string' => 'Hello world',
+ ],
+ ];
+
+ $es['defaultStatus'] = [
+ [
+ 'entity_table' => 'civicrm_event',
+ 'entity_field' => 'title',
+ 'entity_id' => '*EVENT*',
+ 'language' => 'fr_CA',
+ 'string' => 'Hello title',
+ ],
+ ];
+
+ return $es;
+ }
+
+ public function getCreateBadExamples() {
+ $es = [];
+
+ $es['badStatus'] = [
+ [
+ 'status_id:name' => 'jumping',
+ 'entity_table' => 'civicrm_event',
+ 'entity_field' => 'description',
+ 'entity_id' => '*EVENT*',
+ 'language' => 'fr_CA',
+ 'string' => 'Hello world',
+ ],
+ '/Invalid status/',
+ ];
+
+ $es['malformedField'] = [
+ [
+ 'entity_table' => 'civicrm_event',
+ 'entity_field' => 'ti!tle',
+ 'entity_id' => '*EVENT*',
+ 'language' => 'fr_CA',
+ 'string' => 'Hello title',
+ ],
+ '/Entity reference is malformed/',
+ ];
+
+ $es['badTable'] = [
+ [
+ 'entity_table' => 'typozcivicrm_event',
+ 'entity_field' => 'title',
+ 'entity_id' => '*EVENT*',
+ 'language' => 'fr_CA',
+ 'string' => 'Hello title',
+ ],
+ '/(non-existent or non-translatable table|Cannot resolve permissions for dynamic foreign key)/',
+ ];
+
+ $es['badFieldName'] = [
+ [
+ 'status_id:name' => 'active',
+ 'entity_table' => 'civicrm_event',
+ 'entity_field' => 'zoological_taxonomy',
+ 'entity_id' => '*EVENT*',
+ 'language' => 'fr_CA',
+ 'string' => 'Hello world',
+ ],
+ '/non-existent or non-translatable field/',
+ ];
+
+ $es['badFieldType'] = [
+ [
+ 'status_id:name' => 'active',
+ 'entity_table' => 'civicrm_event',
+ 'entity_field' => 'event_type_id',
+ 'entity_id' => '*EVENT*',
+ 'language' => 'fr_CA',
+ 'string' => '9',
+ ],
+ '/non-existent or non-translatable field/',
+ ];
+
+ $es['badEntityId'] = [
+ [
+ 'status_id:name' => 'active',
+ 'entity_table' => 'civicrm_event',
+ 'entity_field' => 'description',
+ 'entity_id' => 9999999,
+ 'language' => 'fr_CA',
+ 'string' => 'Hello world',
+ ],
+ '/Entity does not exist/',
+ ];
+
+ return $es;
+ }
+
+ public function getUpdateBadExamples() {
+ $createOk = $this->getCreateOKExamples()['asDraft'][0];
+ $bads = $this->getCreateBadExamples();
+
+ $es = [];
+ foreach ($bads as $id => $bad) {
+ array_unshift($bad, $createOk);
+ $es[$id] = $bad;
+ }
+ return $es;
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->ids = [];
+ }
+
+ /**
+ * @dataProvider getCreateOKExamples
+ * @param array $record
+ */
+ public function testCreateOK($record) {
+ $record = $this->fillRecord($record);
+ $createResults = \civicrm_api4('Translation', 'create', [
+ 'checkPermissions' => FALSE,
+ 'values' => $record,
+ ]);
+ $this->assertEquals(1, $createResults->count());
+ foreach ($createResults as $createResult) {
+ $getResult = \civicrm_api4('Translation', 'get', [
+ 'where' => [['id', '=', $createResult['id']]],
+ ]);
+ $this->assertEquals($record['string'], $getResult->single()['string']);
+ }
+ }
+
+ /**
+ * @dataProvider getCreateBadExamples
+ * @param array $record
+ * @param string $errorRegex
+ * Regular expression to compare against the error message.
+ */
+ public function testCreateBad($record, $errorRegex) {
+ $record = $this->fillRecord($record);
+ try {
+ \civicrm_api4('Translation', 'create', [
+ 'checkPermissions' => FALSE,
+ 'values' => $record,
+ ]);
+ $this->fail('Create should have failed');
+ }
+ catch (\API_Exception $e) {
+ $this->assertRegExp($errorRegex, $e->getMessage());
+ }
+ }
+
+ /**
+ * @dataProvider getUpdateBadExamples
+ * @param $createRecord
+ * @param $badUpdate
+ * @param $errorRegex
+ *
+ * @throws \API_Exception
+ * @throws \Civi\API\Exception\NotImplementedException
+ */
+ public function testUpdateBad($createRecord, $badUpdate, $errorRegex) {
+ $record = $this->fillRecord($createRecord);
+ $createResults = \civicrm_api4('Translation', 'create', [
+ 'checkPermissions' => FALSE,
+ 'values' => $record,
+ ]);
+ $this->assertEquals(1, $createResults->count());
+ foreach ($createResults as $createResult) {
+ $badUpdate = $this->fillRecord($badUpdate);
+ try {
+ \civicrm_api4('Translation', 'update', [
+ 'where' => [['id', '=', $createResult['id']]],
+ 'values' => $badUpdate,
+ ]);
+ $this->fail('Update should fail');
+ }
+ catch (\API_Exception $e) {
+ $this->assertRegExp($errorRegex, $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * Fill in mocked values for the would-be record..
+ *
+ * @param array $record
+ *
+ * @return array
+ */
+ protected function fillRecord($record) {
+ if ($record['entity_id'] === '*EVENT*') {
+ $eventId = $this->ids['*EVENT*'] ?? \CRM_Core_DAO::createTestObject('CRM_Event_BAO_Event')->id;
+ $record['entity_id'] = $this->ids['*EVENT*'] = $eventId;
+ }
+ return $record;
+ }
+
+}