diff --git a/CRM/Core/BAO/EntityTag.php b/CRM/Core/BAO/EntityTag.php index daa8568faae9658277f4b511e68fd54b23ace127..65d5587b7dbc3f28433c56f16f96f2733c3ae939 100644 --- a/CRM/Core/BAO/EntityTag.php +++ b/CRM/Core/BAO/EntityTag.php @@ -96,7 +96,7 @@ class CRM_Core_BAO_EntityTag extends CRM_Core_DAO_EntityTag { * Delete the tag for a contact. * * @param array $params - * + * @deprecated * WARNING: Nonstandard params searches by tag_id rather than id! */ public static function del(&$params) { diff --git a/Civi/Api4/Action/Contact/Delete.php b/Civi/Api4/Action/Contact/Delete.php new file mode 100644 index 0000000000000000000000000000000000000000..5d2267441ac8d23bd68c563748e49879cfdda2e9 --- /dev/null +++ b/Civi/Api4/Action/Contact/Delete.php @@ -0,0 +1,37 @@ +<?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 | + +--------------------------------------------------------------------+ + */ + +namespace Civi\Api4\Action\Contact; + +/** + * Deletes a contact, by default moving to trash. Set `useTrash = FALSE` for permanent deletion. + * @inheritDoc + */ +class Delete extends \Civi\Api4\Generic\DAODeleteAction { + use \Civi\Api4\Generic\Traits\SoftDelete; + + /** + * @param $items + * @return array + * @throws \API_Exception + */ + protected function deleteObjects($items) { + foreach ($items as $item) { + if (!\CRM_Contact_BAO_Contact::deleteContact($item['id'], FALSE, !$this->useTrash, $this->checkPermissions)) { + throw new \API_Exception("Could not delete {$this->getEntityName()} id {$item['id']}"); + } + $ids[] = ['id' => $item['id']]; + } + return $ids; + } + +} diff --git a/Civi/Api4/Contact.php b/Civi/Api4/Contact.php index 1fe634a0859cf541ca8a028707f07f57956c0b95..34f6463f5530f8c5f28962f67c3330352255d88f 100644 --- a/Civi/Api4/Contact.php +++ b/Civi/Api4/Contact.php @@ -26,6 +26,15 @@ namespace Civi\Api4; */ class Contact extends Generic\DAOEntity { + /** + * @param bool $checkPermissions + * @return Action\Contact\Delete + */ + public static function delete($checkPermissions = TRUE) { + return (new Action\Contact\Delete(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + /** * @param bool $checkPermissions * @return Action\Contact\GetChecksum diff --git a/Civi/Api4/Generic/DAODeleteAction.php b/Civi/Api4/Generic/DAODeleteAction.php index b2b9584e921db92494b24ae08eba4e2340de2c38..84d818de9b1c51d34b46a547a2eb583a9e5aa0c5 100644 --- a/Civi/Api4/Generic/DAODeleteAction.php +++ b/Civi/Api4/Generic/DAODeleteAction.php @@ -14,6 +14,7 @@ namespace Civi\Api4\Generic; use Civi\API\Exception\UnauthorizedException; use Civi\Api4\Utils\CoreUtil; +use Civi\Api4\Utils\ReflectionUtils; /** * Delete one or more $ENTITIES. @@ -56,7 +57,8 @@ class DAODeleteAction extends AbstractBatchAction { $ids = []; $baoName = $this->getBaoName(); - if ($this->getEntityName() !== 'EntityTag' && method_exists($baoName, 'del')) { + // Use BAO::del() method if it is not deprecated + if (method_exists($baoName, 'del') && !ReflectionUtils::isMethodDeprecated($baoName, 'del')) { foreach ($items as $item) { $args = [$item['id']]; $bao = call_user_func_array([$baoName, 'del'], $args); diff --git a/Civi/Api4/Generic/Traits/SoftDelete.php b/Civi/Api4/Generic/Traits/SoftDelete.php new file mode 100644 index 0000000000000000000000000000000000000000..108495d7fc182db925c92a9292d6b6b07fbf2c45 --- /dev/null +++ b/Civi/Api4/Generic/Traits/SoftDelete.php @@ -0,0 +1,27 @@ +<?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 | + +--------------------------------------------------------------------+ + */ + +namespace Civi\Api4\Generic\Traits; + +/** + * This trait is used by entities with a "move to trash" option. + * @method $this setUseTrash(bool $useTrash) Pass FALSE to force delete and bypass trash + * @method bool getUseTrash() + */ +trait SoftDelete { + + /** + * Should $ENTITY be moved to the trash instead of permanently deleted? + * @var bool + */ + protected $useTrash = TRUE; + +} diff --git a/Civi/Api4/Utils/ReflectionUtils.php b/Civi/Api4/Utils/ReflectionUtils.php index a5d59803f13a46770cd8ee83b4a3e7beb6abf293..0263718e3aa44b82a5bd78cbcc3e373536910bfb 100644 --- a/Civi/Api4/Utils/ReflectionUtils.php +++ b/Civi/Api4/Utils/ReflectionUtils.php @@ -179,6 +179,20 @@ class ReflectionUtils { } } + /** + * Check if a class method is deprecated + * + * @param string $className + * @param string $methodName + * @return bool + * @throws \ReflectionException + */ + public static function isMethodDeprecated(string $className, string $methodName): bool { + $reflection = new \ReflectionClass($className); + $docBlock = $reflection->getMethod($methodName)->getDocComment(); + return strpos($docBlock, "@deprecated") !== FALSE; + } + /** * Find any methods in this class which match the given prefix. * diff --git a/Civi/Test/Api3TestTrait.php b/Civi/Test/Api3TestTrait.php index 65bd2ee2162e5c167d6bc1a56ef70513a13d937c..dafe7e70a63ad275eb59c093ab2f181335f5d8dd 100644 --- a/Civi/Test/Api3TestTrait.php +++ b/Civi/Test/Api3TestTrait.php @@ -510,6 +510,10 @@ trait Api3TestTrait { } } + if (isset($actionInfo[0]['params']['useTrash'])) { + $v4Params['useTrash'] = empty($v3Params['skip_undelete']); + } + // Build where clause for 'getcount', 'getsingle', 'getvalue', 'get' & 'replace' if ($v4Action == 'get' || $v3Action == 'replace') { foreach ($v3Params as $key => $val) { diff --git a/tests/phpunit/api/v3/ContactTest.php b/tests/phpunit/api/v3/ContactTest.php index b161260412fe4871080c11ba26a4258cbecdc412..af6e0df138917e8d9b46ffe5601b846e396a5e0c 100644 --- a/tests/phpunit/api/v3/ContactTest.php +++ b/tests/phpunit/api/v3/ContactTest.php @@ -3154,14 +3154,17 @@ class api_v3_ContactTest extends CiviUnitTestCase { /** * Test that delete with skip undelete respects permissions. - * TODO: Api4 + * + * @param int $version * * @throws \CRM_Core_Exception - * @throws \CiviCRM_API3_Exception + * @dataProvider versionThreeAndFour */ - public function testContactDeletePermissions(): void { + public function testContactDeletePermissions(int $version): void { + $this->_apiversion = $version; $contactID = $this->individualCreate(); - $tag = $this->callAPISuccess('Tag', 'create', ['name' => 'to be deleted']); + $this->quickCleanup(['civicrm_entity_tag', 'civicrm_tag']); + $tag = $this->callAPISuccess('Tag', 'create', ['name' => uniqid('to be deleted')]); $this->callAPISuccess('EntityTag', 'create', ['entity_id' => $contactID, 'tag_id' => $tag['id']]); CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM']; $this->callAPIFailure('Contact', 'delete', [ @@ -3169,12 +3172,14 @@ class api_v3_ContactTest extends CiviUnitTestCase { 'check_permissions' => 1, 'skip_undelete' => 1, ]); + $this->callAPISuccessGetCount('EntityTag', ['entity_id' => $contactID], 1); $this->callAPISuccess('Contact', 'delete', [ 'id' => $contactID, 'check_permissions' => 0, 'skip_undelete' => 1, ]); $this->callAPISuccessGetCount('EntityTag', ['entity_id' => $contactID], 0); + $this->quickCleanup(['civicrm_entity_tag', 'civicrm_tag']); } /** diff --git a/tests/phpunit/api/v4/Entity/ConformanceTest.php b/tests/phpunit/api/v4/Entity/ConformanceTest.php index 85a069a180cf20e2106d9f4dfd47f05c03df2b89..0b22857262ed3124d131171f4ab0edfa2da0c7ab 100644 --- a/tests/phpunit/api/v4/Entity/ConformanceTest.php +++ b/tests/phpunit/api/v4/Entity/ConformanceTest.php @@ -424,10 +424,15 @@ class ConformanceTest extends UnitTestCase implements HookInterface { $this->assertEquals(0, $this->checkAccessCounts["{$entity}::delete"]); $isReadOnly = $this->isReadOnly($entityClass); - $deleteResult = $entityClass::delete() + $deleteAction = $entityClass::delete() ->setCheckPermissions(!$isReadOnly) - ->addWhere('id', '=', $id) - ->execute(); + ->addWhere('id', '=', $id); + + if (property_exists($deleteAction, 'useTrash')) { + $deleteAction->setUseTrash(FALSE); + } + + $deleteResult = $deleteAction->execute(); // should get back an array of deleted id $this->assertEquals([['id' => $id]], (array) $deleteResult); diff --git a/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php b/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php index 6c0700f2953131fd204b69a4fb297a243f555c0a..5b2ebeeba6b6186a5dbf0b41dd1484be733b0afe 100644 --- a/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php +++ b/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php @@ -31,4 +31,21 @@ namespace api\v4\Mock; */ class MockV4ReflectionGrandchild extends MockV4ReflectionChild { + /** + * Function marked deprecated + * @see \api\v4\Utils\ReflectionUtilsTest::testIsMethodDeprecated + * @deprecated + */ + public static function deprecatedFn() { + + } + + /** + * Function not marked deprecated + * @see \api\v4\Utils\ReflectionUtilsTest::testIsMethodDeprecated + */ + public static function nonDeprecatedFn() { + + } + } diff --git a/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php b/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php index 868ae79d640c5d15d5cfa9453c8bce18c6982fa4..bd6d218a6d784c074ee52b24649b2a4d4e796961 100644 --- a/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php +++ b/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php @@ -109,4 +109,10 @@ This is the base class.'; $this->assertEquals($expected, ReflectionUtils::parseDocBlock($input)); } + public function testIsMethodDeprecated() { + $mockClass = 'api\v4\Mock\MockV4ReflectionGrandchild'; + $this->assertTrue(ReflectionUtils::isMethodDeprecated($mockClass, 'deprecatedFn')); + $this->assertFalse(ReflectionUtils::isMethodDeprecated($mockClass, 'nonDeprecatedFn')); + } + }