diff --git a/CRM/FormProcessor/BAO/FormProcessorAction.php b/CRM/FormProcessor/BAO/FormProcessorAction.php index d7600d9194bea3b824a47418d455391f62e31807..8c192bdcfe42e463a2eafc72ca381d981bc088ed 100644 --- a/CRM/FormProcessor/BAO/FormProcessorAction.php +++ b/CRM/FormProcessor/BAO/FormProcessorAction.php @@ -153,10 +153,9 @@ class CRM_FormProcessor_BAO_FormProcessorAction extends CRM_FormProcessor_DAO_Fo $action = new CRM_FormProcessor_BAO_FormProcessorAction(); $action->form_processor_id = $formProcessorId; $action->find(FALSE); - while ($input->fetch()) { + while ($action->fetch()) { CRM_Utils_Hook::pre('delete', 'FormProcessorAction', $action->id, CRM_Core_DAO::$_nullArray); - $action->id = $id; $action->delete(); CRM_Utils_Hook::post('delete', 'FormProcessorAction', $action->id, CRM_Core_DAO::$_nullArray); diff --git a/CRM/FormProcessor/BAO/FormProcessorInput.php b/CRM/FormProcessor/BAO/FormProcessorInput.php index 1191b622e8524ecf86905c9dd34f5882002e0a7b..67702ff8825167c4aa5752e81adb59ad345b4539 100644 --- a/CRM/FormProcessor/BAO/FormProcessorInput.php +++ b/CRM/FormProcessor/BAO/FormProcessorInput.php @@ -141,7 +141,6 @@ class CRM_FormProcessor_BAO_FormProcessorInput extends CRM_FormProcessor_DAO_For while ($input->fetch()) { CRM_Utils_Hook::pre('delete', 'FormProcessorInput', $input->id, CRM_Core_DAO::$_nullArray); - $input->id = $id; $input->delete(); CRM_Utils_Hook::post('delete', 'FormProcessorInput', $input->id, CRM_Core_DAO::$_nullArray); diff --git a/api/v3/FormProcessorAction/Get.php b/api/v3/FormProcessorAction/Get.php index e64b46aae2d32a131f3d7ab80f70bdb91ad5778c..8814605855dc2efc06946259547456aca48518a2 100644 --- a/api/v3/FormProcessorAction/Get.php +++ b/api/v3/FormProcessorAction/Get.php @@ -9,7 +9,10 @@ * @throws API_Exception */ function civicrm_api3_form_processor_action_get($params) { - $returnValues = CRM_FormProcessor_BAO_FormProcessorInput::getValues($params); + $returnValues = CRM_FormProcessor_BAO_FormProcessorAction::getValues($params); + foreach($returnValues as $index => $action) { + $returnValues[$index]['type'] = $action['type']->toArray(); + } return civicrm_api3_create_success($returnValues, $params, 'FormProcessorInput', 'Get'); } diff --git a/api/v3/FormProcessorInput/Get.php b/api/v3/FormProcessorInput/Get.php index 9a2cae6e59179895c3dd2905d5b6646335470867..93ca91bfe1c9f86fc77a674e343bd8e494aeb001 100644 --- a/api/v3/FormProcessorInput/Get.php +++ b/api/v3/FormProcessorInput/Get.php @@ -10,6 +10,9 @@ */ function civicrm_api3_form_processor_input_get($params) { $returnValues = CRM_FormProcessor_BAO_FormProcessorInput::getValues($params); + foreach($returnValues as $index => $input) { + $returnValues[$index]['type'] = $input['type']->toArray(); + } return civicrm_api3_create_success($returnValues, $params, 'FormProcessorInput', 'Get'); } diff --git a/form_processor.php b/form_processor.php index 896cba571327948974af26f974ec0d9ab42270e7..39572e0a6127245be22fcd1613d6980f2706e80c 100644 --- a/form_processor.php +++ b/form_processor.php @@ -24,6 +24,28 @@ function form_processor_civicrm_container(ContainerBuilder $container) { $apiKernelDefinition->addMethodCall('registerApiProvider', array($apiProviderDefinition)); } +/** + * Implements hook_civicrm_alterAPIPermissions() + * + * All Form Processor api's are under the Administer CiviCRM permission. + * Except for the FormProcessorExecutor api. + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterAPIPermissions/ + */ +function form_processor_civicrm_alterAPIPermissions($entity, $action, &$params, &$permissions) { + $permissions['form_processor']['get'] = array('administer CiviCRM'); + $permissions['form_processor']['create'] = array('administer CiviCRM'); + $permissions['form_processor']['delete'] = array('administer CiviCRM'); + + $permissions['form_processor_input']['get'] = array('administer CiviCRM'); + $permissions['form_processor_input']['create'] = array('administer CiviCRM'); + $permissions['form_processor_input']['delete'] = array('administer CiviCRM'); + + $permissions['form_processor_delete']['get'] = array('administer CiviCRM'); + $permissions['form_processor_delete']['create'] = array('administer CiviCRM'); + $permissions['form_processor_delete']['delete'] = array('administer CiviCRM'); +} + /** * Check whether the action provider extension is installed and enabled. */ diff --git a/tests/phpunit/UnitTestActionProvider.php b/tests/phpunit/UnitTestActionProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..cd4dd9ed442117f5199c3550432fa02bbeedccb4 --- /dev/null +++ b/tests/phpunit/UnitTestActionProvider.php @@ -0,0 +1,518 @@ +<?php + +/** + * This is a helper class for the unit test. + * This class emulates an action provider class as we cannot be sure + * whether the action provider extension is installed on the test environment. + * However we want to test the form processor and it is integrated with an action_provider service + * so we have to emulate that service. + * + * This class is a stub and implements only the methods getActions and getActionByName. + * It does contain one stub action which is an add to group action. + * + * We have almost recreated the action-provider extension in this file. And that only for + * the puprose of unit testing ;-) + */ +class UnitTestActionProvider { + + private $availableActions; + + public function __construct() { + $this->availableActions = array( + 'AddToGroup' => new UnitTestActionProvider_StubAction_AddToGroup(), + ); + } + + /** + * Returns all available actions + */ + public function getActions() { + return $this->availableActions; + } + + /** + * Returns an action by its name. + * + * @return \Civi\ActionProvider\Action\AbstractAction|null when action is not found. + */ + public function getActionByName($name) { + if (isset($this->availableActions[$name])) { + return $this->availableActions[$name]; + } + return null; + } + + /** + * Returns a new ParameterBag + * + * This function exists so we can encapsulate the creation of a ParameterBag to the provider. + * + * @return ParameterBagInterface + */ + public function createParameterBag() { + return new UnitTestActionProviderParameterBag(); + } +} + +/** + * This is a stub class which emulates the action provider container. + */ +class UnitTestActionProviderContainer { + + /** + * @var Provider + */ + protected $defaultProvider; + + public function __construct() { + $this->defaultProvider = new UnitTestActionProvider(); + } + + /** + * return Provider + */ + public function getDefaultProvider() { + return $this->defaultProvider; + } + + /** + * Returns the provider object the name of the context. + * + * @param string $context + * @return Provider + */ + public function getProviderByContext($context) { + return $this->defaultProvider; + } + + /** + * Returns whether the container has already a particulair context. + * + * @param string $context + * @return bool + */ + public function hasContext($context) { + return true; + } + +} + +/** + * This is the stub class for the action. + */ +class UnitTestActionProvider_StubAction_AddToGroup { + + private $configuration; + + private $defaultConfiguration; + + public function __construct() { + $this->defaultConfiguration = new UnitTestActionProvider_ParameterBag(); + } + + /** + * Returns the specification of the configuration options for the actual action. + * + * @return SpecificationBag + */ + public function getConfigurationSpecification() { + return new UnitTestActionProvider_SpecificationBag(array( + new UnitTestActionProvider_Specification('group_id', 'Integer', 'Select group', true, null, 'Group', null, FALSE), + )); + } + + /** + * Returns the specification of the parameters of the actual action. + * + * @return SpecificationBag + */ + public function getParameterSpecification() { + return new UnitTestActionProvider_SpecificationBag(array( + new UnitTestActionProvider_Specification('contact_id', 'Integer', 'Contact ID', true) + )); + } + + /** + * Returns the specification of the output parameters of this action. + * + * This function could be overriden by child classes. + * + * @return SpecificationBag + */ + public function getOutputSpecification() { + return new UnitTestActionProvider_SpecificationBag(array( + new UnitTestActionProvider_Specification('contact_id', 'Integer', 'Contact ID', true) + )); + } + + public function setConfiguration($configuration) { + $this->configuration = $configuration; + } + + public function getConfiguration() { + return $this->configuration; + } + + /** + * @return ParameterBag + */ + public function getDefaultConfiguration() { + return $this->defaultConfiguration; + } + + public function getTitle() { + return 'Stub Action'; + } + + public function execute($parameters) { + civicrm_api3('GroupContact', 'create', array( + 'contact_id' => $parameters->getParameter('contact_id'), + 'group_id' => $this->configuration->getParameter('group_id'), + )); + } + + /** + * Converts the object to an array. + * + * @return array + */ + public function toArray() { + $return['parameter_spec'] = $this->getParameterSpecification()->toArray(); + $return['configuration_spec'] = $this->getConfigurationSpecification()->toArray(); + $return['output_spec'] = $this->getOutputSpecification()->toArray(); + $return['default_configuration'] = $this->getDefaultConfiguration()->toArray(); + $return['name'] = 'AddToGroup'; + $return['title'] = $this->getTitle(); + return $return; + } + +} + +class UnitTestActionProvider_ParameterBag implements \IteratorAggregate { + + protected $parameters = array(); + + /** + * Get the parameter. + */ + public function getParameter($name) { + if (isset($this->parameters[$name])) { + return $this->parameters[$name]; + } + return null; + } + /** + * Tests whether the parameter with the name exists. + */ + public function doesParameterExists($name) { + if (isset($this->parameters[$name])) { + return true; + } + return false; + } + + /** + * Sets parameter. + */ + public function setParameter($name, $value) { + $this->parameters[$name] = $value; + } + + public function getIterator() { + return new \ArrayIterator($this->parameters); + } + + /** + * Converts the object to an array. + * + * @return array + */ + public function toArray() { + return $this->parameters; + } + +} + +class UnitTestActionProvider_SpecificationBag implements \IteratorAggregate { + + protected $parameterSpecifications = array(); + + public function __construct($specifcations = array()) { + foreach($specifcations as $spec) { + $this->parameterSpecifications[$spec->getName()] = $spec; + } + } + + /** + * @param string $name + * The name of the parameter. + * @return Specification|null + */ + public function getSpecificationByName($name) { + foreach($this->parameterSpecifications as $key => $spec) { + if ($spec->getName() == $name) { + return $this->parameterSpecifications[$key]; + } + } + return null; + } + + public function getIterator() { + return new \ArrayIterator($this->parameterSpecifications); + } + + /** + * Converts the object to an array. + * + * @return array + */ + public function toArray() { + $return = array(); + foreach($this->parameterSpecifications as $spec) { + $return[] = $spec->toArray(); + } + return $return; + } + +} + + +class UnitTestActionProvider_Specification { + + /** + * @var mixed + */ + protected $defaultValue; + /** + * @var string + */ + protected $name; + /** + * @var string + */ + protected $title; + /** + * @var string + */ + protected $description; + /** + * @var bool + */ + protected $required = FALSE; + /** + * @var array + */ + protected $options = array(); + /** + * @var bool + */ + protected $multiple = FALSE; + /** + * @var string + */ + protected $dataType; + /** + * @var string + */ + protected $fkEntity; + + /** + * @param $name + * @param $dataType + */ + public function __construct($name, $dataType = 'String', $title='', $required = false, $defaultValue = null, $fkEntity = null, $options = array(), $multiple = false) { + $this->setName($name); + $this->setDataType($dataType); + $this->setTitle($title); + $this->setRequired($required); + $this->setDefaultValue($defaultValue); + $this->setFkEntity($fkEntity); + $this->setOptions($options); + $this->setMultiple($multiple); + } + + /** + * @return mixed + */ + public function getDefaultValue() { + return $this->defaultValue; + } + + /** + * @param mixed $defaultValue + * + * @return $this + */ + public function setDefaultValue($defaultValue) { + $this->defaultValue = $defaultValue; + return $this; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getTitle() { + return $this->title; + } + + /** + * @param string $title + * + * @return $this + */ + public function setTitle($title) { + $this->title = $title; + return $this; + } + + /** + * @return string + */ + public function getDescription() { + return $this->description; + } + + /** + * @param string $description + * + * @return $this + */ + public function setDescription($description) { + $this->description = $description; + return $this; + } + + /** + * @return bool + */ + public function isRequired() { + return $this->required; + } + + /** + * @param bool $required + * + * @return $this + */ + public function setRequired($required) { + $this->required = $required; + return $this; + } + + /** + * @return string + */ + public function getDataType() { + return $this->dataType; + } + + /** + * @param $dataType + * + * @return $this + * @throws \Exception + */ + public function setDataType($dataType) { + if (!in_array($dataType, $this->getValidDataTypes())) { + throw new \Exception(sprintf('Invalid data type "%s', $dataType)); + } + $this->dataType = $dataType; + return $this; + } + + /** + * Add valid types that are not not part of \CRM_Utils_Type::dataTypes + * + * @return array + */ + private function getValidDataTypes() { + $extraTypes = array('Boolean', 'Text', 'Float'); + $extraTypes = array_combine($extraTypes, $extraTypes); + return array_merge(\CRM_Utils_Type::dataTypes(), $extraTypes); + } + + /** + * @return array + */ + public function getOptions() { + return $this->options; + } + + /** + * @param array $options + * + * @return $this + */ + public function setOptions($options) { + $this->options = $options; + return $this; + } + + /** + * @param $option + */ + public function addOption($option) { + $this->options[] = $option; + } + + /** + * @return bool + */ + public function isMultiple() { + return $this->multiple; + } + + /** + * @param bool $multiple + * + * @return $this + */ + public function setMultiple($multiple) { + $this->multiple = $multiple; + return $this; + } + + /** + * @return string + */ + public function getFkEntity() { + return $this->fkEntity; + } + + /** + * @param string $fkEntity + * + * @return $this + */ + public function setFkEntity($fkEntity) { + $this->fkEntity = $fkEntity; + return $this; + } + + public function toArray() { + $ret = array(); + foreach (get_object_vars($this) as $key => $val) { + $key = strtolower(preg_replace('/(?=[A-Z])/', '_$0', $key)); + $ret[$key] = $val; + } + return $ret; + } + +} diff --git a/tests/phpunit/api/v3/ApiPermissionTest.php b/tests/phpunit/api/v3/ApiPermissionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..85a42404f14b8683a9552255ff7a179c82f4a017 --- /dev/null +++ b/tests/phpunit/api/v3/ApiPermissionTest.php @@ -0,0 +1,199 @@ +<?php + +require_once(__DIR__.'./../../UnitTestActionProvider.php'); + +use CRM_FormProcessor_ExtensionUtil as E; +use Civi\Test\HeadlessInterface; +use Civi\Test\HookInterface; +use Civi\Test\TransactionalInterface; + +/** + * This test tests whether the api is not accessible for people who dont have the right + * to access the api. All FormProcessor api's are only accessible with the permission 'administer CiviCRM'. + * + * As this test does api calls it somehow also show when an api breaks. + * + * Tips: + * - With HookInterface, you may implement CiviCRM hooks directly in the test class. + * Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar). + * - With TransactionalInterface, any data changes made by setUp() or test****() functions will + * rollback automatically -- as long as you don't manipulate schema or truncate tables. + * If this test needs to manipulate schema or truncate tables, then either: + * a. Do all that using setupHeadless() and Civi\Test. + * b. Disable TransactionalInterface, and handle all setup/teardown yourself. + * + * @group headless + */ +class Api_v3_ApiPermissionTest extends \PHPUnit_Framework_TestCase implements HeadlessInterface, TransactionalInterface { + + protected $formProcessorId; + + protected $formProcessorInputId; + + protected $formProcessorActionId; + + public function setUpHeadless() { + // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile(). + // See: https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md + return \Civi\Test::headless() + ->installMe(__DIR__) + ->apply(); + } + + public function setUp() { + parent::setUp(); + + $config = \CRM_Core_Config::singleton(); + $config->userPermissionClass = new \CRM_Core_Permission_UnitTests(); + $config->userPermissionClass->permissions = array(); + + $formProcessor = new \CRM_FormProcessor_BAO_FormProcessor(); + $formProcessor->name = 'name'; + $formProcessor->title = 'Title'; + $formProcessor->save(); + $this->formProcessorId = $formProcessor->id; + + $formProcessorInput = new \CRM_FormProcessor_BAO_FormProcessorInput(); + $formProcessorInput->name = 'contact_id'; + $formProcessorInput->type = 'Integer'; + $formProcessorInput->form_processor_id = $this->formProcessorId; + $formProcessorInput->save(); + $this->formProcessorInputId = $formProcessorInput->id; + + $formProcessorAction = new \CRM_FormProcessor_BAO_FormProcessorAction(); + $formProcessorAction->title = 'contact_id'; + $formProcessorAction->type = 'AddToGroup'; + $formProcessorAction->form_processor_id = $this->formProcessorId; + $formProcessorAction->weight = 0; + $formProcessorAction->save(); + $this->formProcessorActionId = $formProcessorAction->id; + } + + public function tearDown() { + parent::tearDown(); + } + + /** + * First check whether the permission fails when the api is accessed by + * a user with access CiviCRM permission. After that test whether the api + * call succeeds with an administer CiviCRM permission. + * + * @param string + * @param string + * @param array + * @param array + * @param array + * @dataProvider apiCalls + */ + public function testApiPermissions($entity, $action, $params, $id_params, $expected) { + $civi_container = \Civi::container(); + $civi_container->set('action_provider', new \UnitTestActionProviderContainer()); + + foreach($id_params as $key => $prop) { + $params[$key] = $this->$prop; + } + $params['version'] = 3; + $params['check_permissions'] = 1; + \CRM_Core_Config::singleton()->userPermissionClass->permissions = array('access CiviCRM'); + $result = civicrm_api($entity, $action, $params); + $this->assertArrayHasKey('error_code', $result); + $this->assertEquals('unauthorized', $result['error_code']); + + \CRM_Core_Config::singleton()->userPermissionClass->permissions = array('administer CiviCRM'); + $result = civicrm_api($entity, $action, $params); + if (isset($result['is_error']) && !empty($result['is_error'])) { + var_dump($result); + } + foreach($expected as $key => $expected_value) { + $this->assertArrayHasKey($key, $result); + $this->assertEquals($expected_value, $result[$key]); + } + + } + + public function apiCalls() { + return array( + 'FormProcessor.Get' => array( + 'entity' => 'FormProcessor', + 'action' => 'get', + 'params' => array(), + 'id_params' => array('id' => 'formProcessorId'), + 'expected' => array('count' => 1), + ), + 'FormProcessor.Create' => array( + 'entity' => 'FormProcessor', + 'action' => 'create', + 'params' => array( + 'title' => 'Test', + 'name' => 'test', + ), + 'id_params' => array('id' => 'formProcessorId'), + 'expected' => array('count' => 1), + ), + 'FormProcessor.Delete' => array( + 'entity' => 'FormProcessor', + 'action' => 'delete', + 'params' => array( + ), + 'id_params' => array('id' => 'formProcessorId'), + 'expected' => array('count' => 0, 'is_error' => 0), + ), + + // Test for the input api + 'FormProcessorInput.Get' => array( + 'entity' => 'FormProcessorInput', + 'action' => 'get', + 'params' => array(), + 'id_params' => array('id' => 'formProcessorInputId'), + 'expected' => array('count' => 1), + ), + 'FormProcessorInput.Create' => array( + 'entity' => 'FormProcessorInput', + 'action' => 'create', + 'params' => array( + 'name' => 'test', + 'configuration' => array(), + ), + 'id_params' => array('id' => 'formProcessorInputId'), + 'expected' => array('count' => 1), + ), + 'FormProcessorInput.Delete' => array( + 'entity' => 'FormProcessorInput', + 'action' => 'delete', + 'params' => array( + ), + 'id_params' => array('id' => 'formProcessorInputId'), + 'expected' => array('count' => 0, 'is_error' => 0), + ), + + // Test for the action api. + 'FormProcessorAction.Get' => array( + 'entity' => 'FormProcessorAction', + 'action' => 'get', + 'params' => array(), + 'id_params' => array('id' => 'formProcessorActionId'), + 'expected' => array('count' => 1), + ), + 'FormProcessorAction.Create' => array( + 'entity' => 'FormProcessorAction', + 'action' => 'create', + 'params' => array( + 'name' => 'test', + 'configuration' => array(), + 'mapping' => array(), + ), + 'id_params' => array('id' => 'formProcessorActionId'), + 'expected' => array('count' => 1), + ), + 'FormProcessorAction.Delete' => array( + 'entity' => 'FormProcessorAction', + 'action' => 'delete', + 'params' => array( + ), + 'id_params' => array('id' => 'formProcessorActionId'), + 'expected' => array('count' => 0, 'is_error' => 0), + ), + ); + } + +} \ No newline at end of file