First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,125 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* This class determines what fields are allowed for a request
* and validates that the fields are provided correctly.
*/
class APIv3SchemaAdapter implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::PREPARE => array(
array('onApiPrepare', Events::W_MIDDLE),
array('onApiPrepare_validate', Events::W_LATE),
),
);
}
/**
* @param \Civi\API\Event\PrepareEvent $event
* API preparation event.
*
* @throws \API_Exception
*/
public function onApiPrepare(\Civi\API\Event\PrepareEvent $event) {
$apiRequest = $event->getApiRequest();
if ($apiRequest['version'] > 3) {
return;
}
$apiRequest['fields'] = _civicrm_api3_api_getfields($apiRequest);
_civicrm_api3_swap_out_aliases($apiRequest, $apiRequest['fields']);
if (strtolower($apiRequest['action']) != 'getfields') {
if (empty($apiRequest['params']['id'])) {
$apiRequest['params'] = array_merge($this->getDefaults($apiRequest['fields']), $apiRequest['params']);
}
// Note: If 'id' is set then verify_mandatory will only check 'version'.
civicrm_api3_verify_mandatory($apiRequest['params'], NULL, $this->getRequired($apiRequest['fields']));
}
$event->setApiRequest($apiRequest);
}
/**
* @param \Civi\API\Event\Event $event
* API preparation event.
*
* @throws \Exception
*/
public function onApiPrepare_validate(\Civi\API\Event\Event $event) {
$apiRequest = $event->getApiRequest();
// Not sure why this is omitted for generic actions. It would make sense
// to omit 'getfields', but that's only one generic action.
if (isset($apiRequest['function']) && !$apiRequest['is_generic'] && isset($apiRequest['fields'])) {
_civicrm_api3_validate_fields($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $apiRequest['fields']);
$event->setApiRequest($apiRequest);
}
}
/**
* Return array of defaults for the given API (function is a wrapper on getfields).
* @param $fields
* @return array
*/
public function getDefaults($fields) {
$defaults = array();
foreach ($fields as $field => $values) {
if (isset($values['api.default'])) {
$defaults[$field] = $values['api.default'];
}
}
return $defaults;
}
/**
* Return array of required fields for the given API (function is a wrapper on getfields).
* @param $fields
* @return array
*/
public function getRequired($fields) {
$required = array('version');
foreach ($fields as $field => $values) {
if (!empty($values['api.required'])) {
$required[] = $field;
}
}
return $required;
}
}

View file

@ -0,0 +1,224 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* The ChainSubscriber looks for API parameters which specify a nested or
* chained API call. For example:
*
* @code
* $result = civicrm_api('Contact', 'create', array(
* 'version' => 3,
* 'first_name' => 'Amy',
* 'api.Email.create' => array(
* 'email' => 'amy@example.com',
* 'location_type_id' => 123,
* ),
* ));
* @endcode
*
* The ChainSubscriber looks for any parameters of the form "api.Email.create";
* if found, it issues the nested API call (and passes some extra context --
* eg Amy's contact_id).
*/
class ChainSubscriber implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::RESPOND => array('onApiRespond', Events::W_EARLY),
);
}
/**
* @param \Civi\API\Event\RespondEvent $event
* API response event.
*
* @throws \Exception
*/
public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
$apiRequest = $event->getApiRequest();
if ($apiRequest['version'] < 4) {
$result = $event->getResponse();
if (\CRM_Utils_Array::value('is_error', $result, 0) == 0) {
$this->callNestedApi($event->getApiKernel(), $apiRequest['params'], $result, $apiRequest['action'], $apiRequest['entity'], $apiRequest['version']);
$event->setResponse($result);
}
}
}
/**
* Call any nested api calls.
*
* TODO: We don't really need this to be a separate function.
* @param \Civi\API\Kernel $apiKernel
* @param $params
* @param $result
* @param $action
* @param $entity
* @param $version
* @throws \Exception
*/
protected function callNestedApi($apiKernel, &$params, &$result, $action, $entity, $version) {
$lowercase_entity = _civicrm_api_get_entity_name_from_camel($entity);
// We don't need to worry about nested api in the getfields/getoptions
// actions, so just return immediately.
if (in_array($action, array('getfields', 'getfield', 'getoptions'))) {
return;
}
if ($action == 'getsingle') {
// I don't understand the protocol here, but we don't want
// $result to be a recursive array
// $result['values'][0] = $result;
$oldResult = $result;
$result = array('values' => array(0 => $oldResult));
}
foreach ($params as $field => $newparams) {
if ((is_array($newparams) || $newparams === 1) && $field <> 'api.has_parent' && substr($field, 0, 3) == 'api') {
// 'api.participant.delete' => 1 is a valid options - handle 1
// instead of an array
if ($newparams === 1) {
$newparams = array('version' => $version);
}
// can be api_ or api.
$separator = $field[3];
if (!($separator == '.' || $separator == '_')) {
continue;
}
$subAPI = explode($separator, $field);
$subaction = empty($subAPI[2]) ? $action : $subAPI[2];
$subParams = array(
'debug' => \CRM_Utils_Array::value('debug', $params),
);
$subEntity = _civicrm_api_get_entity_name_from_camel($subAPI[1]);
// Hard coded list of entitys that have fields starting api_ and shouldn't be automatically
// deemed to be chained API calls
$skipList = array(
'SmsProvider' => array('type', 'url', 'params'),
'Job' => array('prefix', 'entity', 'action'),
'Contact' => array('key'),
);
if (isset($skipList[$entity]) && in_array($subEntity, $skipList[$entity])) {
continue;
}
foreach ($result['values'] as $idIndex => $parentAPIValues) {
if ($subEntity != 'contact') {
//contact spits the dummy at activity_id so what else won't it like?
//set entity_id & entity table based on the parent's id & entity.
//e.g for something like note if the parent call is contact
//'entity_table' will be set to 'contact' & 'id' to the contact id
//from the parent call. in this case 'contact_id' will also be
//set to the parent's id
if (!($subEntity == 'line_item' && $lowercase_entity == 'contribution' && $action != 'create')) {
$subParams["entity_id"] = $parentAPIValues['id'];
$subParams['entity_table'] = 'civicrm_' . $lowercase_entity;
}
$addEntityId = TRUE;
if ($subEntity == 'relationship' && $lowercase_entity == 'contact') {
// if a relationship call is chained to a contact call, we need
// to check whether contact_id_a or contact_id_b for the
// relationship is given. If so, don't add an extra subParam
// "contact_id" => parent_id.
// See CRM-16084.
foreach (array_keys($newparams) as $key) {
if (substr($key, 0, 11) == 'contact_id_') {
$addEntityId = FALSE;
break;
}
}
}
if ($addEntityId) {
$subParams[$lowercase_entity . "_id"] = $parentAPIValues['id'];
}
}
if ($entity != 'Contact' && \CRM_Utils_Array::value(strtolower($subEntity . "_id"), $parentAPIValues)) {
//e.g. if event_id is in the values returned & subentity is event
//then pass in event_id as 'id' don't do this for contact as it
//does some weird things like returning primary email &
//thus limiting the ability to chain email
//TODO - this might need the camel treatment
$subParams['id'] = $parentAPIValues[$subEntity . "_id"];
}
if (\CRM_Utils_Array::value('entity_table', $result['values'][$idIndex]) == $subEntity) {
$subParams['id'] = $result['values'][$idIndex]['entity_id'];
}
// if we are dealing with the same entity pass 'id' through
// (useful for get + delete for example)
if ($lowercase_entity == $subEntity) {
$subParams['id'] = $result['values'][$idIndex]['id'];
}
$subParams['version'] = $version;
if (!empty($params['check_permissions'])) {
$subParams['check_permissions'] = $params['check_permissions'];
}
$subParams['sequential'] = 1;
$subParams['api.has_parent'] = 1;
if (array_key_exists(0, $newparams)) {
$genericParams = $subParams;
// it is a numerically indexed array - ie. multiple creates
foreach ($newparams as $entityparams) {
$subParams = array_merge($genericParams, $entityparams);
_civicrm_api_replace_variables($subParams, $result['values'][$idIndex], $separator);
$result['values'][$idIndex][$field][] = $apiKernel->run($subEntity, $subaction, $subParams);
if ($result['is_error'] === 1) {
throw new \Exception($subEntity . ' ' . $subaction . 'call failed with' . $result['error_message']);
}
}
}
else {
$subParams = array_merge($subParams, $newparams);
_civicrm_api_replace_variables($subParams, $result['values'][$idIndex], $separator);
$result['values'][$idIndex][$field] = $apiKernel->run($subEntity, $subaction, $subParams);
if (!empty($result['is_error'])) {
throw new \Exception($subEntity . ' ' . $subaction . 'call failed with' . $result['error_message']);
}
}
}
}
}
if ($action == 'getsingle') {
$result = $result['values'][0];
}
}
}

View file

@ -0,0 +1,375 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Given an entity which dynamically attaches itself to another entity,
* determine if one has permission to the other entity.
*
* Example: Suppose one tries to manipulate a File which is attached to a
* Mailing. DynamicFKAuthorization will enforce permissions on the File by
* imitating the permissions of the Mailing.
*
* Note: This enforces a constraint: all matching API calls must define
* "id" (e.g. for the file) or "entity_table+entity_id" or
* "field_name+entity_id".
*
* Note: The permission guard does not exactly authorize the request, but it
* may veto authorization.
*/
class DynamicFKAuthorization implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::AUTHORIZE => array(
array('onApiAuthorize', Events::W_EARLY),
),
);
}
/**
* @var \Civi\API\Kernel
*
* Treat as private. Marked public due to PHP 5.3-compatibility issues.
*/
public $kernel;
/**
* @var string, the entity for which we want to manage permissions
*/
protected $entityName;
/**
* @var array <string> the actions for which we want to manage permissions
*/
protected $actions;
/**
* @var string, SQL. Given a file ID, determine the entity+table it's attached to.
*
* ex: "SELECT if(cf.id,1,0) as is_valid, cef.entity_table, cef.entity_id
* FROM civicrm_file cf
* INNER JOIN civicrm_entity_file cef ON cf.id = cef.file_id
* WHERE cf.id = %1"
*
* Note: %1 is a parameter
* Note: There are three parameters
* - is_valid: "1" if %1 identifies an actual record; otherwise "0"
* - entity_table: NULL or the name of a related table
* - entity_id: NULL or the ID of a row in the related table
*/
protected $lookupDelegateSql;
/**
* @var string, SQL. Get a list of (field_name, table_name, extends) tuples.
*
* For example, one tuple might be ("custom_123", "civicrm_value_mygroup_4",
* "Activity").
*/
protected $lookupCustomFieldSql;
/**
* @var array
*
* Each item is an array(field_name => $, table_name => $, extends => $)
*/
protected $lookupCustomFieldCache;
/**
* @var array list of related tables for which FKs are allowed
*/
protected $allowedDelegates;
/**
* @param \Civi\API\Kernel $kernel
* The API kernel.
* @param string $entityName
* The entity for which we want to manage permissions (e.g. "File" or
* "Note").
* @param array $actions
* The actions for which we want to manage permissions (e.g. "create",
* "get", "delete").
* @param string $lookupDelegateSql
* See docblock in DynamicFKAuthorization::$lookupDelegateSql.
* @param string $lookupCustomFieldSql
* See docblock in DynamicFKAuthorization::$lookupCustomFieldSql.
* @param array|NULL $allowedDelegates
* e.g. "civicrm_mailing","civicrm_activity"; NULL to allow any.
*/
public function __construct($kernel, $entityName, $actions, $lookupDelegateSql, $lookupCustomFieldSql, $allowedDelegates = NULL) {
$this->kernel = $kernel;
$this->entityName = \CRM_Utils_String::convertStringToCamel($entityName);
$this->actions = $actions;
$this->lookupDelegateSql = $lookupDelegateSql;
$this->lookupCustomFieldSql = $lookupCustomFieldSql;
$this->allowedDelegates = $allowedDelegates;
}
/**
* @param \Civi\API\Event\AuthorizeEvent $event
* API authorization event.
* @throws \API_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
$apiRequest = $event->getApiRequest();
if ($apiRequest['version'] == 3 && \CRM_Utils_String::convertStringToCamel($apiRequest['entity']) == $this->entityName && in_array(strtolower($apiRequest['action']), $this->actions)) {
if (isset($apiRequest['params']['field_name'])) {
$fldIdx = \CRM_Utils_Array::index(array('field_name'), $this->getCustomFields());
if (empty($fldIdx[$apiRequest['params']['field_name']])) {
throw new \Exception("Failed to map custom field to entity table");
}
$apiRequest['params']['entity_table'] = $fldIdx[$apiRequest['params']['field_name']]['entity_table'];
unset($apiRequest['params']['field_name']);
}
if (/*!$isTrusted */
empty($apiRequest['params']['id']) && empty($apiRequest['params']['entity_table'])
) {
throw new \API_Exception("Mandatory key(s) missing from params array: 'id' or 'entity_table'");
}
if (isset($apiRequest['params']['id'])) {
list($isValidId, $entityTable, $entityId) = $this->getDelegate($apiRequest['params']['id']);
if ($isValidId && $entityTable && $entityId) {
$this->authorizeDelegate($apiRequest['action'], $entityTable, $entityId, $apiRequest);
$this->preventReassignment($apiRequest['params']['id'], $entityTable, $entityId, $apiRequest);
return;
}
elseif ($isValidId) {
throw new \API_Exception("Failed to match record to related entity");
}
elseif (!$isValidId && strtolower($apiRequest['action']) == 'get') {
// The matches will be an empty set; doesn't make a difference if we
// reject or accept.
// To pass SyntaxConformanceTest, we won't veto "get" on empty-set.
return;
}
}
if (isset($apiRequest['params']['entity_table'])) {
$this->authorizeDelegate(
$apiRequest['action'],
$apiRequest['params']['entity_table'],
\CRM_Utils_Array::value('entity_id', $apiRequest['params'], NULL),
$apiRequest
);
return;
}
throw new \API_Exception("Failed to run permission check");
}
}
/**
* @param string $action
* The API action (e.g. "create").
* @param string $entityTable
* The target entity table (e.g. "civicrm_mailing").
* @param int|NULL $entityId
* The target entity ID.
* @param array $apiRequest
* The full API request.
* @throws \Exception
* @throws \API_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
public function authorizeDelegate($action, $entityTable, $entityId, $apiRequest) {
$entity = $this->getDelegatedEntityName($entityTable);
if (!$entity) {
throw new \API_Exception("Failed to run permission check: Unrecognized target entity table ($entityTable)");
}
if (!$entityId) {
throw new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity): Missing entity_id");
}
if ($this->isTrusted($apiRequest)) {
return;
}
/**
* @var \Exception $exception
*/
$exception = NULL;
$self = $this;
\CRM_Core_Transaction::create(TRUE)->run(function($tx) use ($entity, $action, $entityId, &$exception, $self) {
$tx->rollback(); // Just to be safe.
$params = array(
'version' => 3,
'check_permissions' => 1,
'id' => $entityId,
);
$result = $self->kernel->run($entity, $self->getDelegatedAction($action), $params);
if ($result['is_error'] || empty($result['values'])) {
$exception = new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity,$entityId)", array(
'cause' => $result,
));
}
});
if ($exception) {
throw $exception;
}
}
/**
* If the request attempts to change the entity_table/entity_id of an
* existing record, then generate an error.
*
* @param int $fileId
* The main record being changed.
* @param string $entityTable
* The saved FK.
* @param int $entityId
* The saved FK.
* @param array $apiRequest
* The full API request.
* @throws \API_Exception
*/
public function preventReassignment($fileId, $entityTable, $entityId, $apiRequest) {
if (strtolower($apiRequest['action']) == 'create' && $fileId && !$this->isTrusted($apiRequest)) {
// TODO: no change in field_name?
if (isset($apiRequest['params']['entity_table']) && $entityTable != $apiRequest['params']['entity_table']) {
throw new \API_Exception("Cannot modify entity_table");
}
if (isset($apiRequest['params']['entity_id']) && $entityId != $apiRequest['params']['entity_id']) {
throw new \API_Exception("Cannot modify entity_id");
}
}
}
/**
* @param string $entityTable
* The target entity table (e.g. "civicrm_mailing" or "civicrm_activity").
* @return string|NULL
* The target entity name (e.g. "Mailing" or "Activity").
*/
public function getDelegatedEntityName($entityTable) {
if ($this->allowedDelegates === NULL || in_array($entityTable, $this->allowedDelegates)) {
$className = \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable);
if ($className) {
$entityName = \CRM_Core_DAO_AllCoreTables::getBriefName($className);
if ($entityName) {
return $entityName;
}
}
}
return NULL;
}
/**
* @param string $action
* API action name -- e.g. "create" ("When running *create* on a file...").
* @return string
* e.g. "create" ("Check for *create* permission on the mailing to which
* it is attached.")
*/
public function getDelegatedAction($action) {
switch ($action) {
case 'get':
// reading attachments requires reading the other entity
return 'get';
case 'create':
case 'delete':
// creating/updating/deleting an attachment requires editing
// the other entity
return 'create';
default:
return $action;
}
}
/**
* @param int $id
* e.g. file ID.
* @return array
* (0 => bool $isValid, 1 => string $entityTable, 2 => int $entityId)
* @throws \Exception
*/
public function getDelegate($id) {
$query = \CRM_Core_DAO::executeQuery($this->lookupDelegateSql, array(
1 => array($id, 'Positive'),
));
if ($query->fetch()) {
if (!preg_match('/^civicrm_value_/', $query->entity_table)) {
// A normal attachment directly on its entity.
return array($query->is_valid, $query->entity_table, $query->entity_id);
}
// Ex: Translate custom-field table ("civicrm_value_foo_4") to
// entity table ("civicrm_activity").
$tblIdx = \CRM_Utils_Array::index(array('table_name'), $this->getCustomFields());
if (isset($tblIdx[$query->entity_table])) {
return array($query->is_valid, $tblIdx[$query->entity_table]['entity_table'], $query->entity_id);
}
throw new \Exception('Failed to lookup entity table for custom field.');
}
else {
return array(FALSE, NULL, NULL);
}
}
/**
* @param array $apiRequest
* The full API request.
* @return bool
*/
public function isTrusted($apiRequest) {
// isn't this redundant?
return empty($apiRequest['params']['check_permissions']) or $apiRequest['params']['check_permissions'] == FALSE;
}
/**
* @return array
* Each item has keys 'field_name', 'table_name', 'extends', 'entity_table'
*/
public function getCustomFields() {
$query = \CRM_Core_DAO::executeQuery($this->lookupCustomFieldSql);
$rows = array();
while ($query->fetch()) {
$rows[] = array(
'field_name' => $query->field_name,
'table_name' => $query->table_name,
'extends' => $query->extends,
'entity_table' => \CRM_Core_BAO_CustomGroup::getTableNameByEntityName($query->extends),
);
}
return $rows;
}
}

View file

@ -0,0 +1,108 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class I18nSubscriber
* @package Civi\API\Subscriber
*/
class I18nSubscriber implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::PREPARE => array('onApiPrepare', Events::W_MIDDLE),
);
}
/**
* @param \Civi\API\Event\Event $event
* API preparation event.
*
* @throws \API_Exception
*/
public function onApiPrepare(\Civi\API\Event\Event $event) {
$apiRequest = $event->getApiRequest();
// support multi-lingual requests
if ($language = \CRM_Utils_Array::value('option.language', $apiRequest['params'])) {
$this->setLocale($language);
}
}
/**
* Sets the tsLocale and dbLocale for multi-lingual sites.
* Some code duplication from CRM/Core/BAO/ConfigSetting.php retrieve()
* to avoid regressions from refactoring.
* @param $lcMessagesRequest
* @throws \API_Exception
*/
public function setLocale($lcMessagesRequest) {
// We must validate whether the locale is valid, otherwise setting a bad
// dbLocale could probably lead to sql-injection.
$domain = new \CRM_Core_DAO_Domain();
$domain->id = \CRM_Core_Config::domainID();
$domain->find(TRUE);
// are we in a multi-language setup?
$multiLang = $domain->locales ? TRUE : FALSE;
$lcMessages = NULL;
// on multi-lang sites based on request and civicrm_uf_match
if ($multiLang) {
$config = \CRM_Core_Config::singleton();
$languageLimit = array();
if (isset($config->languageLimit) and $config->languageLimit) {
$languageLimit = $config->languageLimit;
}
if (in_array($lcMessagesRequest, array_keys($languageLimit))) {
$lcMessages = $lcMessagesRequest;
}
else {
throw new \API_Exception(ts('Language not enabled: %1', array(1 => $lcMessagesRequest)));
}
}
global $dbLocale;
// set suffix for table names - use views if more than one language
if ($lcMessages) {
$dbLocale = $multiLang && $lcMessages ? "_{$lcMessages}" : '';
// FIXME: an ugly hack to fix CRM-4041
global $tsLocale;
$tsLocale = $lcMessages;
}
}
}

View file

@ -0,0 +1,136 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* For any API requests that correspond to a Doctrine entity
* ($apiRequest['doctrineClass']), check permissions specified in
* Civi\API\Annotation\Permission.
*/
class PermissionCheck implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::AUTHORIZE => array(
array('onApiAuthorize', Events::W_LATE),
),
);
}
/**
* @param \Civi\API\Event\AuthorizeEvent $event
* API authorization event.
*
* @throws \Civi\API\Exception\UnauthorizedException
*/
public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
$apiRequest = $event->getApiRequest();
if ($apiRequest['version'] < 4) {
// return early unless were told explicitly to do the permission check
if (empty($apiRequest['params']['check_permissions']) or $apiRequest['params']['check_permissions'] == FALSE) {
$event->authorize();
$event->stopPropagation();
return;
}
require_once 'CRM/Core/DAO/permissions.php';
$permissions = _civicrm_api3_permissions($apiRequest['entity'], $apiRequest['action'], $apiRequest['params']);
// $params mightve been reset by the alterAPIPermissions() hook
if (isset($apiRequest['params']['check_permissions']) and $apiRequest['params']['check_permissions'] == FALSE) {
$event->authorize();
$event->stopPropagation();
return;
}
if (!\CRM_Core_Permission::check($permissions) and !self::checkACLPermission($apiRequest)) {
if (is_array($permissions)) {
foreach ($permissions as &$permission) {
if (is_array($permission)) {
$permission = '( ' . implode(' or ', $permission) . ' )';
}
}
$permissions = implode(' and ', $permissions);
}
// FIXME: Generating the exception ourselves allows for detailed error
// but doesn't play well with multiple authz subscribers.
throw new \Civi\API\Exception\UnauthorizedException("API permission check failed for {$apiRequest['entity']}/{$apiRequest['action']} call; insufficient permission: require $permissions");
}
$event->authorize();
$event->stopPropagation();
}
elseif ($apiRequest['version'] == 4) {
if (!$apiRequest->getCheckPermissions()) {
$event->authorize();
$event->stopPropagation();
}
}
}
/**
* Check API for ACL permission.
*
* @param array $apiRequest
*
* @return bool
*/
public function checkACLPermission($apiRequest) {
switch ($apiRequest['entity']) {
case 'UFGroup':
case 'UFField':
$ufGroups = \CRM_Core_PseudoConstant::get('CRM_Core_DAO_UFField', 'uf_group_id');
$aclCreate = \CRM_ACL_API::group(\CRM_Core_Permission::CREATE, NULL, 'civicrm_uf_group', $ufGroups);
$aclEdit = \CRM_ACL_API::group(\CRM_Core_Permission::EDIT, NULL, 'civicrm_uf_group', $ufGroups);
$ufGroupId = $apiRequest['entity'] == 'UFGroup' ? $apiRequest['params']['id'] : $apiRequest['params']['uf_group_id'];
if (in_array($ufGroupId, $aclEdit) or $aclCreate) {
return TRUE;
}
break;
//CRM-16777: Disable schedule reminder with ACLs.
case 'ActionSchedule':
$events = \CRM_Event_BAO_Event::getEvents();
$aclEdit = \CRM_ACL_API::group(\CRM_Core_Permission::EDIT, NULL, 'civicrm_event', $events);
$param = array('id' => $apiRequest['params']['id']);
$eventId = \CRM_Core_BAO_ActionSchedule::retrieve($param, $value = array());
if (in_array($eventId->entity_value, $aclEdit)) {
return TRUE;
}
break;
}
return FALSE;
}
}

View file

@ -0,0 +1,176 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class TransactionSubscriber
*
* Implement transaction management for API calls. Two API options are accepted:
* - is_transactional: bool|'nest' - if true, then all work is done inside a
* transaction. By default, true for mutator actions (C-UD). 'nest' will
* force creation of a nested transaction; otherwise, the default is to
* re-use any existing transactions.
* - options.force_rollback: bool - if true, all work is done in a nested
* transaction which will be rolled back.
*
* @package Civi\API\Subscriber
*/
class TransactionSubscriber implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::PREPARE => array('onApiPrepare', Events::W_EARLY),
Events::RESPOND => array('onApiRespond', Events::W_MIDDLE),
Events::EXCEPTION => array('onApiException', Events::W_EARLY),
);
}
/**
* @var array (scalar $apiRequestId => CRM_Core_Transaction $tx)
*/
private $transactions = array();
/**
* @var array (scalar $apiRequestId => bool)
*
* A list of requests which should be forcibly rolled back to
* their save points.
*/
private $forceRollback = array();
/**
* Determine if an API request should be treated as transactional.
*
* @param \Civi\API\Provider\ProviderInterface $apiProvider
* The API provider responsible for this request.
* @param array $apiRequest
* The full API request.
* @return bool
*/
public function isTransactional($apiProvider, $apiRequest) {
if ($this->isForceRollback($apiProvider, $apiRequest)) {
return TRUE;
}
if (isset($apiRequest['params']['is_transactional'])) {
return \CRM_Utils_String::strtobool($apiRequest['params']['is_transactional']) || $apiRequest['params']['is_transactional'] == 'nest';
}
return strtolower($apiRequest['action']) == 'create' || strtolower($apiRequest['action']) == 'delete' || strtolower($apiRequest['action']) == 'submit';
}
/**
* Determine if caller wants us to *always* rollback.
*
* @param \Civi\API\Provider\ProviderInterface $apiProvider
* The API provider responsible for this request.
* @param array $apiRequest
* The full API request.
* @return bool
*/
public function isForceRollback($apiProvider, $apiRequest) {
// FIXME: When APIv3 uses better parsing, only one check will be needed.
if (isset($apiRequest['params']['options']['force_rollback'])) {
return \CRM_Utils_String::strtobool($apiRequest['params']['options']['force_rollback']);
}
if (isset($apiRequest['options']['force_rollback'])) {
return \CRM_Utils_String::strtobool($apiRequest['options']['force_rollback']);
}
return FALSE;
}
/**
* Determine if caller wants a nested transaction or a re-used transaction.
*
* @param \Civi\API\Provider\ProviderInterface $apiProvider
* The API provider responsible for this request.
* @param array $apiRequest
* The full API request.
* @return bool
* True if a new nested transaction is required; false if active tx may be used
*/
public function isNested($apiProvider, $apiRequest) {
if ($this->isForceRollback($apiProvider, $apiRequest)) {
return TRUE;
}
if (isset($apiRequest['params']['is_transactional']) && $apiRequest['params']['is_transactional'] === 'nest') {
return TRUE;
}
return FALSE;
}
/**
* Open a new transaction instance (if appropriate in the current policy)
*
* @param \Civi\API\Event\PrepareEvent $event
* API preparation event.
*/
public function onApiPrepare(\Civi\API\Event\PrepareEvent $event) {
$apiRequest = $event->getApiRequest();
if ($this->isTransactional($event->getApiProvider(), $apiRequest)) {
$this->transactions[$apiRequest['id']] = new \CRM_Core_Transaction($this->isNested($event->getApiProvider(), $apiRequest));
}
if ($this->isForceRollback($event->getApiProvider(), $apiRequest)) {
$this->transactions[$apiRequest['id']]->rollback();
}
}
/**
* Close any pending transactions.
*
* @param \Civi\API\Event\RespondEvent $event
* API response event.
*/
public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
$apiRequest = $event->getApiRequest();
if (isset($this->transactions[$apiRequest['id']])) {
if (civicrm_error($event->getResponse())) {
$this->transactions[$apiRequest['id']]->rollback();
}
unset($this->transactions[$apiRequest['id']]);
}
}
/**
* Rollback the pending transaction.
*
* @param \Civi\API\Event\ExceptionEvent $event
* API exception event.
*/
public function onApiException(\Civi\API\Event\ExceptionEvent $event) {
$apiRequest = $event->getApiRequest();
if (isset($this->transactions[$apiRequest['id']])) {
$this->transactions[$apiRequest['id']]->rollback();
unset($this->transactions[$apiRequest['id']]);
}
}
}

View file

@ -0,0 +1,122 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Civi\API\Event\AuthorizeEvent;
use Civi\API\Event\RespondEvent;
use Civi\API\WhitelistRule;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* The WhitelistSubscriber listens to API requests and matches them against
* a whitelist of allowed API calls. If an API call does NOT appear in the
* whitelist, then it generates an error.
*
* @package Civi
* @copyright CiviCRM LLC (c) 2004-2017
*/
class WhitelistSubscriber implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::AUTHORIZE => array('onApiAuthorize', Events::W_EARLY),
Events::RESPOND => array('onApiRespond', Events::W_MIDDLE),
);
}
/**
* Array(WhitelistRule).
*
* @var array
*/
protected $rules;
/**
* Array (scalar $reqId => WhitelistRule $rule).
*
* @var array
*/
protected $activeRules;
/**
* @param array $rules
* Array of WhitelistRule.
* @see WhitelistRule
* @throws \CRM_Core_Exception
*/
public function __construct($rules) {
$this->rules = array();
foreach ($rules as $rule) {
/** @var WhitelistRule $rule */
if ($rule->isValid()) {
$this->rules[] = $rule;
}
else {
throw new \CRM_Core_Exception("Invalid rule");
}
}
}
/**
* Determine which, if any, whitelist rules apply this request.
* Reject unauthorized requests.
*
* @param AuthorizeEvent $event
* @throws \CRM_Core_Exception
*/
public function onApiAuthorize(AuthorizeEvent $event) {
$apiRequest = $event->getApiRequest();
if (empty($apiRequest['params']['check_permissions']) || $apiRequest['params']['check_permissions'] !== 'whitelist') {
return;
}
foreach ($this->rules as $rule) {
if (TRUE === $rule->matches($apiRequest)) {
$this->activeRules[$apiRequest['id']] = $rule;
return;
}
}
throw new \CRM_Core_Exception('The request does not match any active API authorizations.');
}
/**
* Apply any filtering rules based on the chosen whitelist rule.
* @param RespondEvent $event
*/
public function onApiRespond(RespondEvent $event) {
$apiRequest = $event->getApiRequest();
$id = $apiRequest['id'];
if (isset($this->activeRules[$id])) {
$event->setResponse($this->activeRules[$id]->filter($apiRequest, $event->getResponse()));
unset($this->activeRules[$id]);
}
}
}

View file

@ -0,0 +1,107 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* This is a wrapper for the legacy "API Wrapper" interface which allows
* wrappers to run through the new kernel. It translates from dispatcher events
* ('civi.api.prepare', 'civi.api.respond') to wrapper calls ('fromApiInput', 'toApiOutput').
*/
class WrapperAdapter implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::PREPARE => array('onApiPrepare', Events::W_MIDDLE),
Events::RESPOND => array('onApiRespond', Events::W_EARLY * 2),
);
}
/**
* @var array(\API_Wrapper)
*/
protected $defaults;
/**
* @param array $defaults
* array(\API_Wrapper).
*/
public function __construct($defaults = array()) {
$this->defaults = $defaults;
}
/**
* @param \Civi\API\Event\PrepareEvent $event
* API preparation event.
*/
public function onApiPrepare(\Civi\API\Event\PrepareEvent $event) {
$apiRequest = $event->getApiRequest();
// For input filtering, process $apiWrappers in forward order
foreach ($this->getWrappers($apiRequest) as $apiWrapper) {
$apiRequest = $apiWrapper->fromApiInput($apiRequest);
}
$event->setApiRequest($apiRequest);
}
/**
* @param \Civi\API\Event\RespondEvent $event
* API response event.
*/
public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
$apiRequest = $event->getApiRequest();
$result = $event->getResponse();
// For output filtering, process $apiWrappers in reverse order
foreach (array_reverse($this->getWrappers($apiRequest)) as $apiWrapper) {
$result = $apiWrapper->toApiOutput($apiRequest, $result);
}
$event->setResponse($result);
}
/**
* @param array $apiRequest
* The full API request.
* @return array<\API_Wrapper>
*/
public function getWrappers($apiRequest) {
if (!isset($apiRequest['wrappers'])) {
$apiRequest['wrappers'] = $this->defaults;
\CRM_Utils_Hook::apiWrappers($apiRequest['wrappers'], $apiRequest);
}
return $apiRequest['wrappers'];
}
}

View file

@ -0,0 +1,66 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2017 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
namespace Civi\API\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class XDebugSubscriber
* @package Civi\API\Subscriber
*/
class XDebugSubscriber implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::RESPOND => array('onApiRespond', Events::W_LATE),
);
}
/**
* @param \Civi\API\Event\RespondEvent $event
* API response event.
*/
public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
$apiRequest = $event->getApiRequest();
$result = $event->getResponse();
if (function_exists('xdebug_time_index')
&& \CRM_Utils_Array::value('debug', $apiRequest['params'])
// result would not be an array for getvalue
&& is_array($result)
) {
$result['xdebug']['peakMemory'] = xdebug_peak_memory_usage();
$result['xdebug']['memory'] = xdebug_memory_usage();
$result['xdebug']['timeIndex'] = xdebug_time_index();
$event->setResponse($result);
}
}
}