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,185 @@
<?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;
/**
*/
class Api3SelectQuery extends SelectQuery {
protected $apiVersion = 3;
/**
* @inheritDoc
*/
protected function buildWhereClause() {
$filters = array();
foreach ($this->where as $key => $value) {
$table_name = NULL;
$column_name = NULL;
if (substr($key, 0, 7) == 'filter.') {
// Legacy support for old filter syntax per the test contract.
// (Convert the style to the later one & then deal with them).
$filterArray = explode('.', $key);
$value = array($filterArray[1] => $value);
$key = 'filters';
}
// Legacy support for 'filter's construct.
if ($key == 'filters') {
foreach ($value as $filterKey => $filterValue) {
if (substr($filterKey, -4, 4) == 'high') {
$key = substr($filterKey, 0, -5);
$value = array('<=' => $filterValue);
}
if (substr($filterKey, -3, 3) == 'low') {
$key = substr($filterKey, 0, -4);
$value = array('>=' => $filterValue);
}
if ($filterKey == 'is_current' || $filterKey == 'isCurrent') {
// Is current is almost worth creating as a 'sql filter' in the DAO function since several entities have the concept.
$todayStart = date('Ymd000000', strtotime('now'));
$todayEnd = date('Ymd235959', strtotime('now'));
$a = self::MAIN_TABLE_ALIAS;
$this->query->where("($a.start_date <= '$todayStart' OR $a.start_date IS NULL)
AND ($a.end_date >= '$todayEnd' OR $a.end_date IS NULL)
AND a.is_active = 1");
}
}
}
// Ignore the "options" param if it is referring to api options and not a field in this entity
if (
$key === 'options' && is_array($value)
&& !in_array(\CRM_Utils_Array::first(array_keys($value)), \CRM_Core_DAO::acceptedSQLOperators())
) {
continue;
}
$field = $this->getField($key);
if ($field) {
$key = $field['name'];
}
if (in_array($key, $this->entityFieldNames)) {
$table_name = self::MAIN_TABLE_ALIAS;
$column_name = $key;
}
elseif (($cf_id = \CRM_Core_BAO_CustomField::getKeyID($key)) != FALSE) {
// If we check a custom field on 'IS NULL', it should also work when there is no
// record in the custom value table, see CRM-20740.
$side = empty($value['IS NULL']) ? 'INNER' : 'LEFT OUTER';
list($table_name, $column_name) = $this->addCustomField($this->apiFieldSpec['custom_' . $cf_id], $side);
}
elseif (strpos($key, '.')) {
$fkInfo = $this->addFkField($key, 'INNER');
if ($fkInfo) {
list($table_name, $column_name) = $fkInfo;
$this->validateNestedInput($key, $value);
}
}
// I don't know why I had to specifically exclude 0 as a key - wouldn't the others have caught it?
// We normally silently ignore null values passed in - if people want IS_NULL they can use acceptedSqlOperator syntax.
if ((!$table_name) || empty($key) || is_null($value)) {
// No valid filter field. This might be a chained call or something.
// Just ignore this for the $where_clause.
continue;
}
$operator = is_array($value) ? \CRM_Utils_Array::first(array_keys($value)) : NULL;
if (!in_array($operator, \CRM_Core_DAO::acceptedSQLOperators(), TRUE)) {
$value = array('=' => $value);
}
$filters[$key] = \CRM_Core_DAO::createSQLFilter("{$table_name}.{$column_name}", $value);
}
// Support OR groups
if (!empty($this->where['options']['or'])) {
$orGroups = $this->where['options']['or'];
if (is_string($orGroups)) {
$orGroups = array_map('trim', explode(',', $orGroups));
}
if (!is_array(\CRM_Utils_Array::first($orGroups))) {
$orGroups = array($orGroups);
}
foreach ($orGroups as $orGroup) {
$orClause = array();
foreach ($orGroup as $key) {
if (!isset($filters[$key])) {
throw new \CiviCRM_API3_Exception("'$key' specified in OR group but not added to params");
}
$orClause[] = $filters[$key];
unset($filters[$key]);
}
$this->query->where(implode(' OR ', $orClause));
}
}
// Add the remaining params using AND
foreach ($filters as $filter) {
$this->query->where($filter);
}
}
/**
* @inheritDoc
*/
protected function getFields() {
require_once 'api/v3/Generic.php';
// Call this function directly instead of using the api wrapper to force unique field names off
$apiSpec = \civicrm_api3_generic_getfields(array(
'entity' => $this->entity,
'version' => 3,
'params' => array('action' => 'get'),
), FALSE);
return $apiSpec['values'];
}
/**
* Fetch a field from the getFields list
*
* Searches by name, uniqueName, and api.aliases
*
* @param string $fieldName
* Field name.
* @return NULL|mixed
*/
protected function getField($fieldName) {
if (!$fieldName) {
return NULL;
}
if (isset($this->apiFieldSpec[$fieldName])) {
return $this->apiFieldSpec[$fieldName];
}
foreach ($this->apiFieldSpec as $field) {
if (
$fieldName == \CRM_Utils_Array::value('uniqueName', $field) ||
array_search($fieldName, \CRM_Utils_Array::value('api.aliases', $field, array())) !== FALSE
) {
return $field;
}
}
return NULL;
}
}

View file

@ -0,0 +1,55 @@
<?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\Event;
/**
* Class AuthorizeEvent
* @package Civi\API\Event
*/
class AuthorizeEvent extends Event {
/**
* @var bool
*/
private $authorized = FALSE;
/**
* Mark the request as authorized.
*/
public function authorize() {
$this->authorized = TRUE;
}
/**
* @return bool
* TRUE if the request has been authorized.
*/
public function isAuthorized() {
return $this->authorized;
}
}

View file

@ -0,0 +1,91 @@
<?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\Event;
/**
* Class Event
* @package Civi\API\Event
*/
class Event extends \Symfony\Component\EventDispatcher\Event {
/**
* @var \Civi\API\Kernel
*/
protected $apiKernel;
/**
* @var \Civi\API\Provider\ProviderInterface
* The API provider responsible for executing the request.
*/
protected $apiProvider;
/**
* @var array
* The full description of the API request.
*
* @see \Civi\API\Request::create
*/
protected $apiRequest;
/**
* @param \Civi\API\Provider\ProviderInterface $apiProvider
* The API responsible for executing the request.
* @param array $apiRequest
* The full description of the API request.
* @param \Civi\API\Kernel $apiKernel
*/
public function __construct($apiProvider, $apiRequest, $apiKernel) {
$this->apiKernel = $apiKernel;
$this->apiProvider = $apiProvider;
$this->apiRequest = $apiRequest;
}
/**
* Get api kernel.
*
* @return \Civi\API\Kernel
*/
public function getApiKernel() {
return $this->apiKernel;
}
/**
* @return \Civi\API\Provider\ProviderInterface
*/
public function getApiProvider() {
return $this->apiProvider;
}
/**
* @return array
*/
public function getApiRequest() {
return $this->apiRequest;
}
}

View file

@ -0,0 +1,63 @@
<?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\Event;
/**
* Class ExceptionEvent
* @package Civi\API\Event
*/
class ExceptionEvent extends Event {
/**
* @var \Exception
*/
private $exception;
/**
* @param \Exception $exception
* The exception which arose while processing the API request.
* @param \Civi\API\Provider\ProviderInterface $apiProvider
* The API provider responsible for executing the request.
* @param array $apiRequest
* The full description of the API request.
* @param \Civi\API\Kernel $apiKernel
* The kernel which fired the event.
*/
public function __construct($exception, $apiProvider, $apiRequest, $apiKernel) {
$this->exception = $exception;
parent::__construct($apiProvider, $apiRequest, $apiKernel);
}
/**
* @return \Exception
*/
public function getException() {
return $this->exception;
}
}

View file

@ -0,0 +1,45 @@
<?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\Event;
/**
* Class PrepareEvent
* @package Civi\API\Event
*/
class PrepareEvent extends Event {
/**
* @param array $apiRequest
* The full description of the API request.
* @return PrepareEvent
*/
public function setApiRequest($apiRequest) {
$this->apiRequest = $apiRequest;
return $this;
}
}

View file

@ -0,0 +1,61 @@
<?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\Event;
/**
* Class ResolveEvent
* @package Civi\API\Event
*/
class ResolveEvent extends Event {
/**
* @param array $apiRequest
* The full description of the API request.
* @param \Civi\API\Kernel $apiKernel
* The kernel which fired the event.
*/
public function __construct($apiRequest, $apiKernel) {
parent::__construct(NULL, $apiRequest, $apiKernel);
}
/**
* @param \Civi\API\Provider\ProviderInterface $apiProvider
* The API provider responsible for executing the request.
*/
public function setApiProvider($apiProvider) {
$this->apiProvider = $apiProvider;
}
/**
* @param array $apiRequest
* The full description of the API request.
*/
public function setApiRequest($apiRequest) {
$this->apiRequest = $apiRequest;
}
}

View file

@ -0,0 +1,70 @@
<?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\Event;
/**
* Class RespondEvent
* @package Civi\API\Event
*/
class RespondEvent extends Event {
/**
* @var mixed
*/
private $response;
/**
* @param \Civi\API\Provider\ProviderInterface $apiProvider
* The API provider responsible for executing the request.
* @param array $apiRequest
* The full description of the API request.
* @param mixed $response
* The response to return to the client.
* @param \Civi\API\Kernel $apiKernel
* The kernel which fired the event.
*/
public function __construct($apiProvider, $apiRequest, $response, $apiKernel) {
$this->response = $response;
parent::__construct($apiProvider, $apiRequest, $apiKernel);
}
/**
* @return mixed
*/
public function getResponse() {
return $this->response;
}
/**
* @param mixed $response
* The response to return to the client.
*/
public function setResponse($response) {
$this->response = $response;
}
}

View file

@ -0,0 +1,120 @@
<?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;
/**
* The API kernel dispatches a series of events while processing each API request.
* For a successful API request, the sequence is RESOLVE => AUTHORIZE => PREPARE => RESPOND.
* If an exception arises in any stage, then the sequence is aborted and the EXCEPTION
* event is dispatched.
*
* Event subscribers which are concerned about the order of execution should assign
* a weight to their subscription (such as W_EARLY, W_MIDDLE, or W_LATE).
* W_LATE).
*/
class Events {
/**
* Determine whether the API request is allowed for the current user.
* For successful execution, at least one listener must invoke
* $event->authorize().
*
* @see AuthorizeEvent
*/
const AUTHORIZE = 'civi.api.authorize';
/**
* Determine which API provider executes the given request. For successful
* execution, at least one listener must invoke
* $event->setProvider($provider).
*
* @see ResolveEvent
*/
const RESOLVE = 'civi.api.resolve';
/**
* Apply pre-execution logic
*
* @see PrepareEvent
*/
const PREPARE = 'civi.api.prepare';
/**
* Apply post-execution logic
*
* @see RespondEvent
*/
const RESPOND = 'civi.api.respond';
/**
* Handle any exceptions.
*
* @see ExceptionEvent
*/
const EXCEPTION = 'civi.api.exception';
/**
* Weight - Early
*/
const W_EARLY = 100;
/**
* Weight - Middle
*/
const W_MIDDLE = 0;
/**
* Weight - Late
*/
const W_LATE = -100;
/**
* @return array<string>
*/
public static function allEvents() {
return array(
self::AUTHORIZE,
self::EXCEPTION,
self::PREPARE,
self::RESOLVE,
self::RESPOND,
);
}
/**
* @param \Civi\Core\Event\GenericHookEvent $e
* @see \CRM_Utils_Hook::eventDefs
*/
public static function hookEventDefs($e) {
$e->inspector->addEventClass(self::AUTHORIZE, 'Civi\API\Event\AuthorizeEvent');
$e->inspector->addEventClass(self::EXCEPTION, 'Civi\API\Event\ExceptionEvent');
$e->inspector->addEventClass(self::PREPARE, 'Civi\API\Event\PrepareEvent');
$e->inspector->addEventClass(self::RESOLVE, 'Civi\API\Event\ResolveEvent');
$e->inspector->addEventClass(self::RESPOND, 'Civi\API\Event\RespondEvent');
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Civi\API\Exception;
require_once 'api/Exception.php';
/**
* Class NotImplementedException
* @package Civi\API\Exception
*/
class NotImplementedException extends \API_Exception {
/**
* @param string $message
* The human friendly error message.
* @param array $extraParams
* Extra params to return. eg an extra array of ids. It is not mandatory,
* but can help the computer using the api. Keep in mind the api consumer
* isn't to be trusted. eg. the database password is NOT a good extra data.
* @param \Exception|NULL $previous
* A previous exception which caused this new exception.
*/
public function __construct($message, $extraParams = array(), \Exception $previous = NULL) {
parent::__construct($message, \API_Exception::NOT_IMPLEMENTED, $extraParams, $previous);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Civi\API\Exception;
require_once 'api/Exception.php';
/**
* Class UnauthorizedException
* @package Civi\API\Exception
*/
class UnauthorizedException extends \API_Exception {
/**
* @param string $message
* The human friendly error message.
* @param array $extraParams
* Extra params to return. eg an extra array of ids. It is not mandatory,
* but can help the computer using the api. Keep in mind the api consumer
* isn't to be trusted. eg. the database password is NOT a good extra data.
* @param \Exception|NULL $previous
* A previous exception which caused this new exception.
*/
public function __construct($message, $extraParams = array(), \Exception $previous = NULL) {
parent::__construct($message, \API_Exception::UNAUTHORIZED, $extraParams, $previous);
}
}

View file

@ -0,0 +1,251 @@
<?php
namespace Civi\API;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
/**
* Class ExternalBatch
* @package Civi\API
*
* Perform a series of external, asynchronous, concurrent API call.
*/
class ExternalBatch {
/**
* The time to wait when polling for process status (microseconds).
*/
const POLL_INTERVAL = 10000;
/**
* @var array
* Array(int $idx => array $apiCall).
*/
protected $apiCalls;
protected $defaultParams;
protected $root;
protected $settingsPath;
protected $env;
/**
* @var array
* Array(int $idx => Process $process).
*/
protected $processes;
/**
* @var array
* Array(int $idx => array $apiResult).
*/
protected $apiResults;
/**
* @param array $defaultParams
* Default values to merge into any API calls.
*/
public function __construct($defaultParams = array()) {
global $civicrm_root;
$this->root = $civicrm_root;
$this->settingsPath = defined('CIVICRM_SETTINGS_PATH') ? CIVICRM_SETTINGS_PATH : NULL;
$this->defaultParams = $defaultParams;
$this->env = $_ENV;
if (empty($_ENV['PATH'])) {
// FIXME: If we upgrade to newer Symfony\Process and use the newer
// inheritEnv feature, then this becomes unnecessary.
throw new \CRM_Core_Exception('ExternalBatch cannot detect environment: $_ENV is missing. (Tip: Set variables_order=EGPCS in php.ini.)');
}
}
/**
* @param string $entity
* @param string $action
* @param array $params
* @return ExternalBatch
*/
public function addCall($entity, $action, $params = array()) {
$params = array_merge($this->defaultParams, $params);
$this->apiCalls[] = array(
'entity' => $entity,
'action' => $action,
'params' => $params,
);
return $this;
}
/**
* @param array $env
* List of environment variables to add.
* @return static
*/
public function addEnv($env) {
$this->env = array_merge($this->env, $env);
return $this;
}
/**
* Run all the API calls concurrently.
*
* @return static
* @throws \CRM_Core_Exception
*/
public function start() {
foreach ($this->apiCalls as $idx => $apiCall) {
$process = $this->createProcess($apiCall);
$process->start();
$this->processes[$idx] = $process;
}
return $this;
}
/**
* @return int
* The number of running processes.
*/
public function getRunningCount() {
$count = 0;
foreach ($this->processes as $process) {
if ($process->isRunning()) {
$count++;
}
}
return $count;
}
public function wait() {
while (!empty($this->processes)) {
usleep(self::POLL_INTERVAL);
foreach (array_keys($this->processes) as $idx) {
/** @var Process $process */
$process = $this->processes[$idx];
if (!$process->isRunning()) {
$parsed = json_decode($process->getOutput(), TRUE);
if ($process->getExitCode() || $parsed === NULL) {
$this->apiResults[] = array(
'is_error' => 1,
'error_message' => 'External API returned malformed response.',
'trace' => array(
'code' => $process->getExitCode(),
'stdout' => $process->getOutput(),
'stderr' => $process->getErrorOutput(),
),
);
}
else {
$this->apiResults[] = $parsed;
}
unset($this->processes[$idx]);
}
}
}
return $this;
}
/**
* @return array
*/
public function getResults() {
return $this->apiResults;
}
/**
* @param int $idx
* @return array
*/
public function getResult($idx = 0) {
return $this->apiResults[$idx];
}
/**
* Determine if the local environment supports running API calls
* externally.
*
* @return bool
*/
public function isSupported() {
// If you try in Windows, feel free to change this...
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' || !function_exists('proc_open')) {
return FALSE;
}
if (!file_exists($this->root . '/bin/cli.php') || !file_exists($this->settingsPath)) {
return FALSE;
}
return TRUE;
}
/**
* @param array $apiCall
* Array with keys: entity, action, params.
* @return Process
* @throws \CRM_Core_Exception
*/
public function createProcess($apiCall) {
$parts = array();
if (defined('CIVICRM_TEST') && CIVICRM_TEST) {
// When testing, civicrm.settings.php may rely on $_CV, which is only
// populated/propagated if we execute through `cv`.
$parts[] = 'cv api';
$parts[] = escapeshellarg($apiCall['entity'] . '.' . $apiCall['action']);
$parts[] = "--out=json-strict";
foreach ($apiCall['params'] as $key => $value) {
$parts[] = escapeshellarg("$key=$value");
}
}
else {
// But in production, we may not have `cv` installed.
$executableFinder = new PhpExecutableFinder();
$php = $executableFinder->find();
if (!$php) {
throw new \CRM_Core_Exception("Failed to locate PHP interpreter.");
}
$parts[] = $php;
$parts[] = escapeshellarg($this->root . '/bin/cli.php');
$parts[] = escapeshellarg("-e=" . $apiCall['entity']);
$parts[] = escapeshellarg("-a=" . $apiCall['action']);
$parts[] = "--json";
$parts[] = escapeshellarg("-u=dummyuser");
foreach ($apiCall['params'] as $key => $value) {
$parts[] = escapeshellarg("--$key=$value");
}
}
$command = implode(" ", $parts);
$env = array_merge($this->env, array(
'CIVICRM_SETTINGS' => $this->settingsPath,
));
return new Process($command, $this->root, $env);
}
/**
* @return string
*/
public function getRoot() {
return $this->root;
}
/**
* @param string $root
*/
public function setRoot($root) {
$this->root = $root;
}
/**
* @return string
*/
public function getSettingsPath() {
return $this->settingsPath;
}
/**
* @param string $settingsPath
*/
public function setSettingsPath($settingsPath) {
$this->settingsPath = $settingsPath;
}
}

View file

@ -0,0 +1,491 @@
<?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;
use Civi\API\Event\AuthorizeEvent;
use Civi\API\Event\PrepareEvent;
use Civi\API\Event\ExceptionEvent;
use Civi\API\Event\ResolveEvent;
use Civi\API\Event\RespondEvent;
use Civi\API\Provider\ProviderInterface;
/**
* @package Civi
* @copyright CiviCRM LLC (c) 2004-2017
*/
class Kernel {
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcher
*/
protected $dispatcher;
/**
* @var array<ProviderInterface>
*/
protected $apiProviders;
/**
* @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
* The event dispatcher which receives kernel events.
* @param array $apiProviders
* Array of ProviderInterface.
*/
public function __construct($dispatcher, $apiProviders = array()) {
$this->apiProviders = $apiProviders;
$this->dispatcher = $dispatcher;
}
/**
* @deprecated
* @param string $entity
* Type of entities to deal with.
* @param string $action
* Create, get, delete or some special action name.
* @param array $params
* Array to be passed to API function.
* @param mixed $extra
* Unused/deprecated.
* @return array|int
* @see runSafe
*/
public function run($entity, $action, $params, $extra = NULL) {
return $this->runSafe($entity, $action, $params, $extra);
}
/**
* Parse and execute an API request. Any errors will be converted to
* normal format.
*
* @param string $entity
* Type of entities to deal with.
* @param string $action
* Create, get, delete or some special action name.
* @param array $params
* Array to be passed to API function.
* @param mixed $extra
* Unused/deprecated.
*
* @return array|int
* @throws \API_Exception
*/
public function runSafe($entity, $action, $params, $extra = NULL) {
$apiRequest = Request::create($entity, $action, $params, $extra);
try {
$apiResponse = $this->runRequest($apiRequest);
return $this->formatResult($apiRequest, $apiResponse);
}
catch (\Exception $e) {
$this->dispatcher->dispatch(Events::EXCEPTION, new ExceptionEvent($e, NULL, $apiRequest, $this));
if ($e instanceof \PEAR_Exception) {
$err = $this->formatPearException($e, $apiRequest);
}
elseif ($e instanceof \API_Exception) {
$err = $this->formatApiException($e, $apiRequest);
}
else {
$err = $this->formatException($e, $apiRequest);
}
return $this->formatResult($apiRequest, $err);
}
}
/**
* Determine if a hypothetical API call would be authorized.
*
* @param string $entity
* Type of entities to deal with.
* @param string $action
* Create, get, delete or some special action name.
* @param array $params
* Array to be passed to function.
* @param mixed $extra
* Unused/deprecated.
*
* @return bool
* TRUE if authorization would succeed.
* @throws \Exception
*/
public function runAuthorize($entity, $action, $params, $extra = NULL) {
$apiProvider = NULL;
$apiRequest = Request::create($entity, $action, $params, $extra);
try {
$this->boot($apiRequest);
list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
$this->authorize($apiProvider, $apiRequest);
return TRUE;
}
catch (\Civi\API\Exception\UnauthorizedException $e) {
return FALSE;
}
}
/**
* Execute an API request.
*
* The request must be in canonical format. Exceptions will be propagated out.
*
* @param array $apiRequest
* @return array
* @throws \API_Exception
* @throws \Civi\API\Exception\NotImplementedException
* @throws \Civi\API\Exception\UnauthorizedException
*/
public function runRequest($apiRequest) {
$this->boot($apiRequest);
$errorScope = \CRM_Core_TemporaryErrorScope::useException();
list($apiProvider, $apiRequest) = $this->resolve($apiRequest);
$this->authorize($apiProvider, $apiRequest);
$apiRequest = $this->prepare($apiProvider, $apiRequest);
$result = $apiProvider->invoke($apiRequest);
return $this->respond($apiProvider, $apiRequest, $result);
}
/**
* Bootstrap - Load basic dependencies and sanity-check inputs.
*
* @param \Civi\API\V4\Action|array $apiRequest
* @throws \API_Exception
*/
public function boot($apiRequest) {
require_once 'api/Exception.php';
if (!is_array($apiRequest['params'])) {
throw new \API_Exception('Input variable `params` is not an array', 2000);
}
switch ($apiRequest['version']) {
case 2:
case 3:
require_once 'api/v3/utils.php';
_civicrm_api3_initialize();
break;
case 4:
// nothing to do
break;
default:
throw new \API_Exception('Unknown api version', 2000);
}
}
/**
* @param array $apiRequest
* @throws \API_Exception
*/
protected function validate($apiRequest) {
}
/**
* Determine which, if any, service will execute the API request.
*
* @param array $apiRequest
* The full description of the API request.
* @throws Exception\NotImplementedException
* @return array
* A tuple with the provider-object and a revised apiRequest.
* Array(0 => ProviderInterface, 1 => array $apiRequest).
*/
public function resolve($apiRequest) {
/** @var ResolveEvent $resolveEvent */
$resolveEvent = $this->dispatcher->dispatch(Events::RESOLVE, new ResolveEvent($apiRequest, $this));
$apiRequest = $resolveEvent->getApiRequest();
if (!$resolveEvent->getApiProvider()) {
throw new \Civi\API\Exception\NotImplementedException("API (" . $apiRequest['entity'] . ", " . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
}
return array($resolveEvent->getApiProvider(), $apiRequest);
}
/**
* Determine if the API request is allowed (under current policy)
*
* @param ProviderInterface $apiProvider
* The API provider responsible for executing the request.
* @param array $apiRequest
* The full description of the API request.
* @throws Exception\UnauthorizedException
*/
public function authorize($apiProvider, $apiRequest) {
/** @var AuthorizeEvent $event */
$event = $this->dispatcher->dispatch(Events::AUTHORIZE, new AuthorizeEvent($apiProvider, $apiRequest, $this));
if (!$event->isAuthorized()) {
throw new \Civi\API\Exception\UnauthorizedException("Authorization failed");
}
}
/**
* Allow third-party code to manipulate the API request before execution.
*
* @param ProviderInterface $apiProvider
* The API provider responsible for executing the request.
* @param array $apiRequest
* The full description of the API request.
* @return array
* The revised API request.
*/
public function prepare($apiProvider, $apiRequest) {
/** @var PrepareEvent $event */
$event = $this->dispatcher->dispatch(Events::PREPARE, new PrepareEvent($apiProvider, $apiRequest, $this));
return $event->getApiRequest();
}
/**
* Allow third-party code to manipulate the API response after execution.
*
* @param ProviderInterface $apiProvider
* The API provider responsible for executing the request.
* @param array $apiRequest
* The full description of the API request.
* @param array $result
* The response to return to the client.
* @return mixed
* The revised $result.
*/
public function respond($apiProvider, $apiRequest, $result) {
/** @var RespondEvent $event */
$event = $this->dispatcher->dispatch(Events::RESPOND, new RespondEvent($apiProvider, $apiRequest, $result, $this));
return $event->getResponse();
}
/**
* @param int $version
* API version.
* @return array
* Array<string>.
*/
public function getEntityNames($version) {
// Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
$entityNames = array();
foreach ($this->getApiProviders() as $provider) {
/** @var ProviderInterface $provider */
$entityNames = array_merge($entityNames, $provider->getEntityNames($version));
}
$entityNames = array_unique($entityNames);
sort($entityNames);
return $entityNames;
}
/**
* @param int $version
* API version.
* @param string $entity
* API entity.
* @return array
* Array<string>
*/
public function getActionNames($version, $entity) {
// Question: Would it better to eliminate $this->apiProviders and just use $this->dispatcher?
$actionNames = array();
foreach ($this->getApiProviders() as $provider) {
/** @var ProviderInterface $provider */
$actionNames = array_merge($actionNames, $provider->getActionNames($version, $entity));
}
$actionNames = array_unique($actionNames);
sort($actionNames);
return $actionNames;
}
/**
* @param \Exception $e
* An unhandled exception.
* @param array $apiRequest
* The full description of the API request.
* @return array
* API response.
*/
public function formatException($e, $apiRequest) {
$data = array();
if (!empty($apiRequest['params']['debug'])) {
$data['trace'] = $e->getTraceAsString();
}
return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
}
/**
* @param \API_Exception $e
* An unhandled exception.
* @param array $apiRequest
* The full description of the API request.
* @return array
* (API response)
*/
public function formatApiException($e, $apiRequest) {
$data = $e->getExtraParams();
$data['entity'] = \CRM_Utils_Array::value('entity', $apiRequest);
$data['action'] = \CRM_Utils_Array::value('action', $apiRequest);
if (\CRM_Utils_Array::value('debug', \CRM_Utils_Array::value('params', $apiRequest))
&& empty($data['trace']) // prevent recursion
) {
$data['trace'] = $e->getTraceAsString();
}
return $this->createError($e->getMessage(), $data, $apiRequest, $e->getCode());
}
/**
* @param \PEAR_Exception $e
* An unhandled exception.
* @param array $apiRequest
* The full description of the API request.
* @return array
* API response.
*/
public function formatPearException($e, $apiRequest) {
$data = array();
$error = $e->getCause();
if ($error instanceof \DB_Error) {
$data["error_code"] = \DB::errorMessage($error->getCode());
$data["sql"] = $error->getDebugInfo();
}
if (!empty($apiRequest['params']['debug'])) {
if (method_exists($e, 'getUserInfo')) {
$data['debug_info'] = $error->getUserInfo();
}
if (method_exists($e, 'getExtraData')) {
$data['debug_info'] = $data + $error->getExtraData();
}
$data['trace'] = $e->getTraceAsString();
}
else {
$data['tip'] = "add debug=1 to your API call to have more info about the error";
}
return $this->createError($e->getMessage(), $data, $apiRequest);
}
/**
* @param string $msg
* Descriptive error message.
* @param array $data
* Error data.
* @param array $apiRequest
* The full description of the API request.
* @param mixed $code
* Doesn't appear to be used.
*
* @throws \API_Exception
* @return array
* Array<type>.
*/
public function createError($msg, $data, $apiRequest, $code = NULL) {
// FIXME what to do with $code?
if ($msg == 'DB Error: constraint violation' || substr($msg, 0, 9) == 'DB Error:' || $msg == 'DB Error: already exists') {
try {
$fields = _civicrm_api3_api_getfields($apiRequest);
_civicrm_api3_validate_foreign_keys($apiRequest['entity'], $apiRequest['action'], $apiRequest['params'], $fields);
}
catch (\Exception $e) {
$msg = $e->getMessage();
}
}
$data = \civicrm_api3_create_error($msg, $data);
if (isset($apiRequest['params']) && is_array($apiRequest['params']) && !empty($apiRequest['params']['api.has_parent'])) {
$errorCode = empty($data['error_code']) ? 'chained_api_failed' : $data['error_code'];
throw new \API_Exception('Error in call to ' . $apiRequest['entity'] . '_' . $apiRequest['action'] . ' : ' . $msg, $errorCode, $data);
}
return $data;
}
/**
* @param array $apiRequest
* The full description of the API request.
* @param array $result
* The response to return to the client.
* @return mixed
*/
public function formatResult($apiRequest, $result) {
if (isset($apiRequest, $apiRequest['params'])) {
if (isset($apiRequest['params']['format.is_success']) && $apiRequest['params']['format.is_success'] == 1) {
return (empty($result['is_error'])) ? 1 : 0;
}
if (!empty($apiRequest['params']['format.only_id']) && isset($result['id'])) {
// FIXME dispatch
return $result['id'];
}
}
return $result;
}
/**
* @return array<ProviderInterface>
*/
public function getApiProviders() {
return $this->apiProviders;
}
/**
* @param array $apiProviders
* Array<ProviderInterface>.
* @return Kernel
*/
public function setApiProviders($apiProviders) {
$this->apiProviders = $apiProviders;
return $this;
}
/**
* @param ProviderInterface $apiProvider
* The API provider responsible for executing the request.
* @return Kernel
*/
public function registerApiProvider($apiProvider) {
$this->apiProviders[] = $apiProvider;
if ($apiProvider instanceof \Symfony\Component\EventDispatcher\EventSubscriberInterface) {
$this->getDispatcher()->addSubscriber($apiProvider);
}
return $this;
}
/**
* @return \Symfony\Component\EventDispatcher\EventDispatcher
*/
public function getDispatcher() {
return $this->dispatcher;
}
/**
* @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
* The event dispatcher which receives kernel events.
* @return Kernel
*/
public function setDispatcher($dispatcher) {
$this->dispatcher = $dispatcher;
return $this;
}
}

View file

@ -0,0 +1,168 @@
<?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\Provider;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An adhoc provider is useful for creating mock API implementations.
*/
class AdhocProvider implements EventSubscriberInterface, ProviderInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
// Using a high priority allows adhoc implementations
// to override standard implementations -- which is
// handy for testing/mocking.
return array(
Events::RESOLVE => array(
array('onApiResolve', Events::W_EARLY),
),
Events::AUTHORIZE => array(
array('onApiAuthorize', Events::W_EARLY),
),
);
}
/**
* @var array (string $name => array('perm' => string, 'callback' => callable))
*/
protected $actions = array();
/**
* @var string
*/
protected $entity;
/**
* @var int
*/
protected $version;
/**
* @param int $version
* API version.
* @param string $entity
* API entity.
*/
public function __construct($version, $entity) {
$this->entity = $entity;
$this->version = $version;
}
/**
* Register a new API.
*
* @param string $name
* Action name.
* @param string $perm
* Permissions required for invoking the action.
* @param mixed $callback
* The function which executes the API.
* @return AdhocProvider
*/
public function addAction($name, $perm, $callback) {
$this->actions[strtolower($name)] = array(
'perm' => $perm,
'callback' => $callback,
);
return $this;
}
/**
* @param \Civi\API\Event\ResolveEvent $event
* API resolution event.
*/
public function onApiResolve(\Civi\API\Event\ResolveEvent $event) {
$apiRequest = $event->getApiRequest();
if ($this->matchesRequest($apiRequest)) {
$event->setApiRequest($apiRequest);
$event->setApiProvider($this);
$event->stopPropagation();
}
}
/**
* @param \Civi\API\Event\AuthorizeEvent $event
* API authorization event.
*/
public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
$apiRequest = $event->getApiRequest();
if ($this->matchesRequest($apiRequest) && \CRM_Core_Permission::check($this->actions[strtolower($apiRequest['action'])]['perm'])) {
$event->authorize();
$event->stopPropagation();
}
}
/**
* @inheritDoc
* @param array $apiRequest
* @return array|mixed
*/
public function invoke($apiRequest) {
return call_user_func($this->actions[strtolower($apiRequest['action'])]['callback'], $apiRequest);
}
/**
* @inheritDoc
* @param int $version
* @return array
*/
public function getEntityNames($version) {
return array($this->entity);
}
/**
* @inheritDoc
* @param int $version
* @param string $entity
* @return array
*/
public function getActionNames($version, $entity) {
if ($version == $this->version && $entity == $this->entity) {
return array_keys($this->actions);
}
else {
return array();
}
}
/**
* @param array $apiRequest
* The full description of the API request.
*
* @return bool
*/
public function matchesRequest($apiRequest) {
return $apiRequest['entity'] == $this->entity && $apiRequest['version'] == $this->version && isset($this->actions[strtolower($apiRequest['action'])]);
}
}

View file

@ -0,0 +1,304 @@
<?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\Provider;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* This class manages the loading of API's using strict file+function naming
* conventions.
*/
class MagicFunctionProvider implements EventSubscriberInterface, ProviderInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::RESOLVE => array(
array('onApiResolve', Events::W_MIDDLE),
),
);
}
/**
* @var array (string $cachekey => array('function' => string, 'is_generic' => bool))
*/
private $cache;
/**
*/
public function __construct() {
$this->cache = array();
}
/**
* @param \Civi\API\Event\ResolveEvent $event
* API resolution event.
*/
public function onApiResolve(\Civi\API\Event\ResolveEvent $event) {
$apiRequest = $event->getApiRequest();
$resolved = $this->resolve($apiRequest);
if ($resolved['function']) {
$apiRequest += $resolved;
$event->setApiRequest($apiRequest);
$event->setApiProvider($this);
$event->stopPropagation();
}
}
/**
* @inheritDoc
* @param array $apiRequest
* @return array
*/
public function invoke($apiRequest) {
$function = $apiRequest['function'];
if ($apiRequest['function'] && $apiRequest['is_generic']) {
// Unlike normal API implementations, generic implementations require explicit
// knowledge of the entity and action (as well as $params). Bundle up these bits
// into a convenient data structure.
$result = $function($apiRequest);
}
elseif ($apiRequest['function'] && !$apiRequest['is_generic']) {
$result = $function($apiRequest['params']);
}
return $result;
}
/**
* @inheritDoc
* @param int $version
* @return array
*/
public function getEntityNames($version) {
$entities = array();
$include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path()));
#$include_dirs = array(dirname(__FILE__). '/../../');
foreach ($include_dirs as $include_dir) {
$api_dir = implode(DIRECTORY_SEPARATOR,
array($include_dir, 'api', 'v' . $version));
if (!is_dir($api_dir)) {
continue;
}
$iterator = new \DirectoryIterator($api_dir);
foreach ($iterator as $fileinfo) {
$file = $fileinfo->getFilename();
// Check for entities with a master file ("api/v3/MyEntity.php")
$parts = explode(".", $file);
if (end($parts) == "php" && $file != "utils.php" && !preg_match('/Tests?.php$/', $file)) {
// without the ".php"
$entities[] = substr($file, 0, -4);
}
// Check for entities with standalone action files (eg "api/v3/MyEntity/MyAction.php").
$action_dir = $api_dir . DIRECTORY_SEPARATOR . $file;
if (preg_match('/^[A-Z][A-Za-z0-9]*$/', $file) && is_dir($action_dir)) {
if (count(glob("$action_dir/[A-Z]*.php")) > 0) {
$entities[] = $file;
}
}
}
}
$entities = array_diff($entities, array('Generic'));
$entities = array_unique($entities);
sort($entities);
return $entities;
}
/**
* @inheritDoc
* @param int $version
* @param string $entity
* @return array
*/
public function getActionNames($version, $entity) {
$entity = _civicrm_api_get_camel_name($entity);
$entities = $this->getEntityNames($version);
if (!in_array($entity, $entities)) {
return array();
}
$this->loadEntity($entity, $version);
$functions = get_defined_functions();
$actions = array();
$prefix = 'civicrm_api' . $version . '_' . _civicrm_api_get_entity_name_from_camel($entity) . '_';
$prefixGeneric = 'civicrm_api' . $version . '_generic_';
foreach ($functions['user'] as $fct) {
if (strpos($fct, $prefix) === 0) {
$actions[] = substr($fct, strlen($prefix));
}
elseif (strpos($fct, $prefixGeneric) === 0) {
$actions[] = substr($fct, strlen($prefixGeneric));
}
}
return $actions;
}
/**
* Look up the implementation for a given API request.
*
* @param array $apiRequest
* Array with keys:
* - entity: string, required.
* - action: string, required.
* - params: array.
* - version: scalar, required.
*
* @return array
* Array with keys:
* - function: callback (mixed)
* - is_generic: boolean
*/
protected function resolve($apiRequest) {
$cachekey = strtolower($apiRequest['entity']) . ':' . strtolower($apiRequest['action']) . ':' . $apiRequest['version'];
if (isset($this->cache[$cachekey])) {
return $this->cache[$cachekey];
}
$camelName = _civicrm_api_get_camel_name($apiRequest['entity'], $apiRequest['version']);
$actionCamelName = _civicrm_api_get_camel_name($apiRequest['action']);
// Determine if there is an entity-specific implementation of the action
$stdFunction = $this->getFunctionName($apiRequest['entity'], $apiRequest['action'], $apiRequest['version']);
if (function_exists($stdFunction)) {
// someone already loaded the appropriate file
// FIXME: This has the affect of masking bugs in load order; this is
// included to provide bug-compatibility.
$this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE);
return $this->cache[$cachekey];
}
$stdFiles = array(
// By convention, the $camelName.php is more likely to contain the
// function, so test it first
'api/v' . $apiRequest['version'] . '/' . $camelName . '.php',
'api/v' . $apiRequest['version'] . '/' . $camelName . '/' . $actionCamelName . '.php',
);
foreach ($stdFiles as $stdFile) {
if (\CRM_Utils_File::isIncludable($stdFile)) {
require_once $stdFile;
if (function_exists($stdFunction)) {
$this->cache[$cachekey] = array('function' => $stdFunction, 'is_generic' => FALSE);
return $this->cache[$cachekey];
}
}
}
// Determine if there is a generic implementation of the action
require_once 'api/v3/Generic.php';
# $genericFunction = 'civicrm_api3_generic_' . $apiRequest['action'];
$genericFunction = $this->getFunctionName('generic', $apiRequest['action'], $apiRequest['version']);
$genericFiles = array(
// By convention, the Generic.php is more likely to contain the
// function, so test it first
'api/v' . $apiRequest['version'] . '/Generic.php',
'api/v' . $apiRequest['version'] . '/Generic/' . $actionCamelName . '.php',
);
foreach ($genericFiles as $genericFile) {
if (\CRM_Utils_File::isIncludable($genericFile)) {
require_once $genericFile;
if (function_exists($genericFunction)) {
$this->cache[$cachekey] = array('function' => $genericFunction, 'is_generic' => TRUE);
return $this->cache[$cachekey];
}
}
}
$this->cache[$cachekey] = array('function' => FALSE, 'is_generic' => FALSE);
return $this->cache[$cachekey];
}
/**
* Determine the function name for a given API request.
*
* @param string $entity
* API entity name.
* @param string $action
* API action name.
* @param int $version
* API version.
*
* @return string
*/
protected function getFunctionName($entity, $action, $version) {
$entity = _civicrm_api_get_entity_name_from_camel($entity);
return 'civicrm_api' . $version . '_' . $entity . '_' . $action;
}
/**
* Load/require all files related to an entity.
*
* This should not normally be called because it's does a file-system scan; it's
* only appropriate when introspection is really required (eg for "getActions").
*
* @param string $entity
* API entity name.
* @param int $version
* API version.
*/
protected function loadEntity($entity, $version) {
$camelName = _civicrm_api_get_camel_name($entity, $version);
// Check for master entity file; to match _civicrm_api_resolve(), only load the first one
$stdFile = 'api/v' . $version . '/' . $camelName . '.php';
if (\CRM_Utils_File::isIncludable($stdFile)) {
require_once $stdFile;
}
// Check for standalone action files; to match _civicrm_api_resolve(), only load the first one
$loaded_files = array(); // array($relativeFilePath => TRUE)
$include_dirs = array_unique(explode(PATH_SEPARATOR, get_include_path()));
foreach ($include_dirs as $include_dir) {
foreach (array($camelName, 'Generic') as $name) {
$action_dir = implode(DIRECTORY_SEPARATOR,
array($include_dir, 'api', "v${version}", $name));
if (!is_dir($action_dir)) {
continue;
}
$iterator = new \DirectoryIterator($action_dir);
foreach ($iterator as $fileinfo) {
$file = $fileinfo->getFilename();
if (array_key_exists($file, $loaded_files)) {
continue; // action provided by an earlier item on include_path
}
$parts = explode(".", $file);
if (end($parts) == "php" && !preg_match('/Tests?\.php$/', $file)) {
require_once $action_dir . DIRECTORY_SEPARATOR . $file;
$loaded_files[$file] = TRUE;
}
}
}
}
}
}

View file

@ -0,0 +1,63 @@
<?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\Provider;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An API "provider" provides a means to execute API requests.
*/
interface ProviderInterface {
/**
* @param array $apiRequest
* The full description of the API request.
* @return array
* structured response data (per civicrm_api3_create_success)
* @see civicrm_api3_create_success
* @throws \API_Exception
*/
public function invoke($apiRequest);
/**
* @param int $version
* API version.
* @return array<string>
*/
public function getEntityNames($version);
/**
* @param int $version
* API version.
* @param string $entity
* API entity.
* @return array<string>
*/
public function getActionNames($version, $entity);
}

View file

@ -0,0 +1,150 @@
<?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\Provider;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* This class defines operations for inspecting the API's metadata.
*/
class ReflectionProvider implements EventSubscriberInterface, ProviderInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::RESOLVE => array(
// TODO decide if we really want to override others
array('onApiResolve', Events::W_EARLY),
),
Events::AUTHORIZE => array(
// TODO decide if we really want to override others
array('onApiAuthorize', Events::W_EARLY),
),
);
}
/**
* @var \Civi\API\Kernel
*/
private $apiKernel;
/**
* @var array (string $entityName => array(string $actionName))
*/
private $actions;
/**
* @param \Civi\API\Kernel $apiKernel
* The API kernel.
*/
public function __construct($apiKernel) {
$this->apiKernel = $apiKernel;
$this->actions = array(
'Entity' => array('get', 'getactions'),
'*' => array('getactions'), // 'getfields'
);
}
/**
* @param \Civi\API\Event\ResolveEvent $event
* API resolution event.
*/
public function onApiResolve(\Civi\API\Event\ResolveEvent $event) {
$apiRequest = $event->getApiRequest();
$actions = $this->getActionNames($apiRequest['version'], $apiRequest['entity']);
if (in_array($apiRequest['action'], $actions)) {
$apiRequest['is_metadata'] = TRUE;
$event->setApiRequest($apiRequest);
$event->setApiProvider($this);
$event->stopPropagation();
// TODO decide if we really want to override others
}
}
/**
* @param \Civi\API\Event\AuthorizeEvent $event
* API authorization event.
*/
public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
$apiRequest = $event->getApiRequest();
if (isset($apiRequest['is_metadata'])) {
// if (\CRM_Core_Permission::check('access AJAX API')
// || \CRM_Core_Permission::check('access CiviCRM')) {
$event->authorize();
$event->stopPropagation();
// }
}
}
/**
* @inheritDoc
* @param array $apiRequest
* @return array
* @throws \API_Exception
*/
public function invoke($apiRequest) {
if (strtolower($apiRequest['entity']) == 'entity' && $apiRequest['action'] == 'get') {
return civicrm_api3_create_success($this->apiKernel->getEntityNames($apiRequest['version']), $apiRequest['params'], 'entity', 'get');
}
switch ($apiRequest['action']) {
case 'getactions':
return civicrm_api3_create_success($this->apiKernel->getActionNames($apiRequest['version'], $apiRequest['entity']), $apiRequest['params'], $apiRequest['entity'], $apiRequest['action']);
//case 'getfields':
// return $this->doGetFields($apiRequest);
default:
}
// We shouldn't get here because onApiResolve() checks $this->actions
throw new \API_Exception("Unsupported action (" . $apiRequest['entity'] . '.' . $apiRequest['action'] . ']');
}
/**
* @inheritDoc
* @param int $version
* @return array
*/
public function getEntityNames($version) {
return array('Entity');
}
/**
* @inheritDoc
* @param int $version
* @param string $entity
* @return array
*/
public function getActionNames($version, $entity) {
$entity = _civicrm_api_get_camel_name($entity, $version);
return isset($this->actions[$entity]) ? $this->actions[$entity] : $this->actions['*'];
}
}

View file

@ -0,0 +1,156 @@
<?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\Provider;
use Civi\API\Events;
/**
* A static provider is useful for creating mock API implementations which
* manages records in-memory.
*
* TODO Add a static provider to SyntaxConformanceTest to ensure that it's
* representative.
*/
class StaticProvider extends AdhocProvider {
protected $records;
protected $fields;
/**
* @return array
*/
public static function getSubscribedEvents() {
return array(
Events::RESOLVE => array(
array('onApiResolve', Events::W_MIDDLE),
),
Events::AUTHORIZE => array(
array('onApiAuthorize', Events::W_MIDDLE),
),
);
}
/**
* @param int $version
* API version.
* @param string $entity
* API entity.
* @param array $fields
* List of fields in this fake entity.
* @param array $perms
* Array(string $action => string $perm).
* @param array $records
* List of mock records to be read/updated by API calls.
*/
public function __construct($version, $entity, $fields, $perms = array(), $records = array()) {
parent::__construct($version, $entity);
$perms = array_merge(array(
'create' => \CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
'get' => \CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
'delete' => \CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
), $perms);
$this->records = \CRM_Utils_Array::index(array('id'), $records);
$this->fields = $fields;
$this->addAction('create', $perms['create'], array($this, 'doCreate'));
$this->addAction('get', $perms['get'], array($this, 'doGet'));
$this->addAction('delete', $perms['delete'], array($this, 'doDelete'));
}
/**
* @return array
*/
public function getRecords() {
return $this->records;
}
/**
* @param array $records
* List of mock records to be read/updated by API calls.
*/
public function setRecords($records) {
$this->records = $records;
}
/**
* @param array $apiRequest
* The full description of the API request.
* @return array
* Formatted API result
* @throws \API_Exception
*/
public function doCreate($apiRequest) {
if (isset($apiRequest['params']['id'])) {
$id = $apiRequest['params']['id'];
}
else {
$id = max(array_keys($this->records)) + 1;
$this->records[$id] = array();
}
if (!isset($this->records[$id])) {
throw new \API_Exception("Invalid ID: $id");
}
foreach ($this->fields as $field) {
if (isset($apiRequest['params'][$field])) {
$this->records[$id][$field] = $apiRequest['params'][$field];
}
}
return civicrm_api3_create_success($this->records[$id]);
}
/**
* @param array $apiRequest
* The full description of the API request.
* @return array
* Formatted API result
* @throws \API_Exception
*/
public function doGet($apiRequest) {
return _civicrm_api3_basic_array_get($apiRequest['entity'], $apiRequest['params'], $this->records, 'id', $this->fields);
}
/**
* @param array $apiRequest
* The full description of the API request.
* @return array
* Formatted API result
* @throws \API_Exception
*/
public function doDelete($apiRequest) {
$id = @$apiRequest['params']['id'];
if ($id && isset($this->records[$id])) {
unset($this->records[$id]);
}
return civicrm_api3_create_success(array());
}
}

View file

@ -0,0 +1,118 @@
<?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;
/**
* Class Request
* @package Civi\API
*/
class Request {
private static $nextId = 1;
/**
* Create a formatted/normalized request object.
*
* @param string $entity
* API entity name.
* @param string $action
* API action name.
* @param array $params
* API parameters.
* @param mixed $extra
* Who knows? ...
*
* @throws \API_Exception
* @return array
* the request descriptor; keys:
* - version: int
* - entity: string
* - action: string
* - params: array (string $key => mixed $value) [deprecated in v4]
* - extra: unspecified
* - fields: NULL|array (string $key => array $fieldSpec)
* - options: \CRM_Utils_OptionBag derived from params [v4-only]
* - data: \CRM_Utils_OptionBag derived from params [v4-only]
* - chains: unspecified derived from params [v4-only]
*/
public static function create($entity, $action, $params, $extra = NULL) {
$version = \CRM_Utils_Array::value('version', $params);
switch ($version) {
default:
$apiRequest = array();
$apiRequest['id'] = self::$nextId++;
$apiRequest['version'] = (int) $version;
$apiRequest['params'] = $params;
$apiRequest['extra'] = $extra;
$apiRequest['fields'] = NULL;
$apiRequest['entity'] = self::normalizeEntityName($entity, $apiRequest['version']);
$apiRequest['action'] = self::normalizeActionName($action, $apiRequest['version']);
return $apiRequest;
case 4:
$callable = array("Civi\\Api4\\Entity\\$entity", $action);
if (!is_callable($callable)) {
throw new Exception\NotImplementedException("API ($entity, $action) does not exist (join the API team and implement it!)");
}
$apiCall = call_user_func($callable);
$apiRequest['id'] = self::$nextId++;
unset($params['version']);
foreach ($params as $name => $param) {
$setter = 'set' . ucfirst($name);
$apiCall->$setter($param);
}
return $apiCall;
}
}
/**
* Normalize entity to be CamelCase.
*
* APIv1-v3 munges entity/action names, and accepts any mixture of case and underscores.
*
* @param string $entity
* @param int $version
* @return string
*/
public static function normalizeEntityName($entity, $version) {
return \CRM_Utils_String::convertStringToCamel(\CRM_Utils_String::munge($entity));
}
/**
* Normalize api action name to be lowercase.
*
* APIv1-v3 munges entity/action names, and accepts any mixture of case and underscores.
*
* @param $action
* @param $version
* @return string
*/
public static function normalizeActionName($action, $version) {
return strtolower(\CRM_Utils_String::munge($action));
}
}

View file

@ -0,0 +1,512 @@
<?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;
use Civi\API\Exception\UnauthorizedException;
/**
* Query builder for civicrm_api_basic_get.
*
* Fetches an entity based on specified params for the "where" clause,
* return properties for the "select" clause,
* as well as limit and order.
*
* Automatically joins on custom fields to return or filter by them.
*
* Supports an additional sql fragment which the calling api can provide.
*
* @package Civi\API
*/
abstract class SelectQuery {
const
MAX_JOINS = 4,
MAIN_TABLE_ALIAS = 'a';
/**
* @var string
*/
protected $entity;
public $select = array();
public $where = array();
public $orderBy = array();
public $limit;
public $offset;
/**
* @var array
*/
protected $selectFields = array();
/**
* @var bool
*/
public $isFillUniqueFields = FALSE;
/**
* @var \CRM_Utils_SQL_Select
*/
protected $query;
/**
* @var array
*/
protected $joins = array();
/**
* @var array
*/
protected $apiFieldSpec;
/**
* @var array
*/
protected $entityFieldNames;
/**
* @var array
*/
protected $aclFields = array();
/**
* @var string|bool
*/
protected $checkPermissions;
protected $apiVersion;
/**
* @param string $entity
* @param bool $checkPermissions
*/
public function __construct($entity, $checkPermissions) {
$this->entity = $entity;
require_once 'api/v3/utils.php';
$baoName = _civicrm_api3_get_BAO($entity);
$bao = new $baoName();
$this->entityFieldNames = _civicrm_api3_field_names(_civicrm_api3_build_fields_array($bao));
$this->apiFieldSpec = $this->getFields();
$this->query = \CRM_Utils_SQL_Select::from($bao->tableName() . ' ' . self::MAIN_TABLE_ALIAS);
$bao->free();
// Add ACLs first to avoid redundant subclauses
$this->checkPermissions = $checkPermissions;
$this->query->where($this->getAclClause(self::MAIN_TABLE_ALIAS, $baoName));
}
/**
* Build & execute the query and return results array
*
* @return array|int
* @throws \API_Exception
* @throws \CRM_Core_Exception
* @throws \Exception
*/
public function run() {
$this->buildSelectFields();
$this->buildWhereClause();
if (in_array('count_rows', $this->select)) {
$this->query->select("count(*) as c");
}
else {
foreach ($this->selectFields as $column => $alias) {
$this->query->select("$column as `$alias`");
}
// Order by
$this->buildOrderBy();
}
// Limit
if (!empty($this->limit) || !empty($this->offset)) {
$this->query->limit($this->limit, $this->offset);
}
$result_entities = array();
$result_dao = \CRM_Core_DAO::executeQuery($this->query->toSQL());
while ($result_dao->fetch()) {
if (in_array('count_rows', $this->select)) {
$result_dao->free();
return (int) $result_dao->c;
}
$result_entities[$result_dao->id] = array();
foreach ($this->selectFields as $column => $alias) {
$returnName = $alias;
$alias = str_replace('.', '_', $alias);
if (property_exists($result_dao, $alias) && $result_dao->$alias != NULL) {
$result_entities[$result_dao->id][$returnName] = $result_dao->$alias;
}
// Backward compatibility on fields names.
if ($this->isFillUniqueFields && !empty($this->apiFieldSpec[$alias]['uniqueName'])) {
$result_entities[$result_dao->id][$this->apiFieldSpec[$alias]['uniqueName']] = $result_dao->$alias;
}
foreach ($this->apiFieldSpec as $returnName => $spec) {
if (empty($result_entities[$result_dao->id][$returnName]) && !empty($result_entities[$result_dao->id][$spec['name']])) {
$result_entities[$result_dao->id][$returnName] = $result_entities[$result_dao->id][$spec['name']];
}
}
};
}
$result_dao->free();
return $result_entities;
}
/**
* @param \CRM_Utils_SQL_Select $sqlFragment
* @return SelectQuery
*/
public function merge($sqlFragment) {
$this->query->merge($sqlFragment);
return $this;
}
/**
* Joins onto an fk field
*
* Adds one or more joins to the query to make this field available for use in a clause.
*
* Enforces permissions at the api level and by appending the acl clause for that entity to the join.
*
* @param $fkFieldName
* @param $side
*
* @return array|null
* Returns the table and field name for adding this field to a SELECT or WHERE clause
* @throws \API_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
protected function addFkField($fkFieldName, $side) {
$stack = explode('.', $fkFieldName);
if (count($stack) < 2) {
return NULL;
}
$prev = self::MAIN_TABLE_ALIAS;
foreach ($stack as $depth => $fieldName) {
// Setup variables then skip the first level
if (!$depth) {
$fk = $fieldName;
// We only join on core fields
// @TODO: Custom contact ref fields could be supported too
if (!in_array($fk, $this->entityFieldNames)) {
return NULL;
}
$fkField = &$this->apiFieldSpec[$fk];
continue;
}
// More than 4 joins deep seems excessive - DOS attack?
if ($depth > self::MAX_JOINS) {
throw new UnauthorizedException("Maximum number of joins exceeded in parameter $fkFieldName");
}
$subStack = array_slice($stack, 0, $depth);
$this->getJoinInfo($fkField, $subStack);
if (!isset($fkField['FKApiName']) || !isset($fkField['FKClassName'])) {
// Join doesn't exist - might be another param with a dot in it for some reason, we'll just ignore it.
return NULL;
}
// Ensure we have permission to access the other api
if (!$this->checkPermissionToJoin($fkField['FKApiName'], $subStack)) {
throw new UnauthorizedException("Authorization failed to join onto {$fkField['FKApiName']} api in parameter $fkFieldName");
}
if (!isset($fkField['FKApiSpec'])) {
$fkField['FKApiSpec'] = \_civicrm_api_get_fields($fkField['FKApiName']);
}
$fieldInfo = \CRM_Utils_Array::value($fieldName, $fkField['FKApiSpec']);
$keyColumn = \CRM_Utils_Array::value('FKKeyColumn', $fkField, 'id');
if (!$fieldInfo || !isset($fkField['FKApiSpec'][$keyColumn])) {
// Join doesn't exist - might be another param with a dot in it for some reason, we'll just ignore it.
return NULL;
}
$fkTable = \CRM_Core_DAO_AllCoreTables::getTableForClass($fkField['FKClassName']);
$tableAlias = implode('_to_', $subStack) . "_to_$fkTable";
// Add acl condition
$joinCondition = array_merge(
array("$prev.$fk = $tableAlias.$keyColumn"),
$this->getAclClause($tableAlias, \_civicrm_api3_get_BAO($fkField['FKApiName']), $subStack)
);
if (!empty($fkField['FKCondition'])) {
$joinCondition[] = str_replace($fkTable, $tableAlias, $fkField['FKCondition']);
}
$this->join($side, $fkTable, $tableAlias, $joinCondition);
if (strpos($fieldName, 'custom_') === 0) {
list($tableAlias, $fieldName) = $this->addCustomField($fieldInfo, $side, $tableAlias);
}
// Get ready to recurse to the next level
$fk = $fieldName;
$fkField = &$fkField['FKApiSpec'][$fieldName];
$prev = $tableAlias;
}
return array($tableAlias, $fieldName);
}
/**
* Get join info for dynamically-joined fields (e.g. "entity_id", "option_group")
*
* @param $fkField
* @param $stack
*/
protected function getJoinInfo(&$fkField, $stack) {
if ($fkField['name'] == 'entity_id') {
$entityTableParam = substr(implode('.', $stack), 0, -2) . 'table';
$entityTable = \CRM_Utils_Array::value($entityTableParam, $this->where);
if ($entityTable && is_string($entityTable) && \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable)) {
$fkField['FKClassName'] = \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable);
$fkField['FKApiName'] = \CRM_Core_DAO_AllCoreTables::getBriefName($fkField['FKClassName']);
}
}
if (!empty($fkField['pseudoconstant']['optionGroupName'])) {
$fkField['FKClassName'] = 'CRM_Core_DAO_OptionValue';
$fkField['FKApiName'] = 'OptionValue';
$fkField['FKKeyColumn'] = 'value';
$fkField['FKCondition'] = "civicrm_option_value.option_group_id = (SELECT id FROM civicrm_option_group WHERE name = '{$fkField['pseudoconstant']['optionGroupName']}')";
}
}
/**
* Joins onto a custom field
*
* Adds a join to the query to make this field available for use in a clause.
*
* @param array $customField
* @param string $side
* @param string $baseTable
* @return array
* Returns the table and field name for adding this field to a SELECT or WHERE clause
*/
protected function addCustomField($customField, $side, $baseTable = self::MAIN_TABLE_ALIAS) {
$tableName = $customField["table_name"];
$columnName = $customField["column_name"];
$tableAlias = "{$baseTable}_to_$tableName";
$this->join($side, $tableName, $tableAlias, array("`$tableAlias`.entity_id = `$baseTable`.id"));
return array($tableAlias, $columnName);
}
/**
* Fetch a field from the getFields list
*
* @param string $fieldName
* @return array|null
*/
abstract protected function getField($fieldName);
/**
* Perform input validation on params that use the join syntax
*
* Arguably this should be done at the api wrapper level, but doing it here provides a bit more consistency
* in that api permissions to perform the join are checked first.
*
* @param $fieldName
* @param $value
* @throws \Exception
*/
protected function validateNestedInput($fieldName, &$value) {
$stack = explode('.', $fieldName);
$spec = $this->apiFieldSpec;
$fieldName = array_pop($stack);
foreach ($stack as $depth => $name) {
$entity = $spec[$name]['FKApiName'];
$spec = $spec[$name]['FKApiSpec'];
}
$params = array($fieldName => $value);
\_civicrm_api3_validate_fields($entity, 'get', $params, $spec);
$value = $params[$fieldName];
}
/**
* Check permission to join onto another api entity
*
* @param string $entity
* @param array $fieldStack
* The stack of fields leading up to this join
* @return bool
*/
protected function checkPermissionToJoin($entity, $fieldStack) {
if (!$this->checkPermissions) {
return TRUE;
}
// Build an array of params that relate to the joined entity
$params = array(
'version' => 3,
'return' => array(),
'check_permissions' => $this->checkPermissions,
);
$prefix = implode('.', $fieldStack) . '.';
$len = strlen($prefix);
foreach ($this->select as $key => $ret) {
if (strpos($key, $prefix) === 0) {
$params['return'][substr($key, $len)] = $ret;
}
}
foreach ($this->where as $key => $param) {
if (strpos($key, $prefix) === 0) {
$params[substr($key, $len)] = $param;
}
}
return \Civi::service('civi_api_kernel')->runAuthorize($entity, 'get', $params);
}
/**
* Get acl clause for an entity
*
* @param string $tableAlias
* @param string $baoName
* @param array $stack
* @return array
*/
protected function getAclClause($tableAlias, $baoName, $stack = array()) {
if (!$this->checkPermissions) {
return array();
}
// Prevent (most) redundant acl sub clauses if they have already been applied to the main entity.
// FIXME: Currently this only works 1 level deep, but tracking through multiple joins would increase complexity
// and just doing it for the first join takes care of most acl clause deduping.
if (count($stack) === 1 && in_array($stack[0], $this->aclFields)) {
return array();
}
$clauses = $baoName::getSelectWhereClause($tableAlias);
if (!$stack) {
// Track field clauses added to the main entity
$this->aclFields = array_keys($clauses);
}
return array_filter($clauses);
}
/**
* Orders the query by one or more fields
*
* @throws \API_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*/
protected function buildOrderBy() {
$sortParams = is_string($this->orderBy) ? explode(',', $this->orderBy) : (array) $this->orderBy;
foreach ($sortParams as $index => $item) {
$item = trim($item);
if ($item == '(1)') {
continue;
}
$words = preg_split("/[\s]+/", $item);
if ($words) {
// Direction defaults to ASC unless DESC is specified
$direction = strtoupper(\CRM_Utils_Array::value(1, $words, '')) == 'DESC' ? ' DESC' : '';
$field = $this->getField($words[0]);
if ($field) {
$this->query->orderBy(self::MAIN_TABLE_ALIAS . '.' . $field['name'] . $direction, NULL, $index);
}
elseif (strpos($words[0], '.')) {
$join = $this->addFkField($words[0], 'LEFT');
if ($join) {
$this->query->orderBy("`{$join[0]}`.`{$join[1]}`$direction", NULL, $index);
}
}
else {
throw new \API_Exception("Unknown field specified for sort. Cannot order by '$item'");
}
}
}
}
/**
* @param string $side
* @param string $tableName
* @param string $tableAlias
* @param array $conditions
*/
public function join($side, $tableName, $tableAlias, $conditions) {
// INNER JOINs take precedence over LEFT JOINs
if ($side != 'LEFT' || !isset($this->joins[$tableAlias])) {
$this->joins[$tableAlias] = $side;
$this->query->join($tableAlias, "$side JOIN `$tableName` `$tableAlias` ON " . implode(' AND ', $conditions));
}
}
/**
* Populate where clauses
*
* @throws \Civi\API\Exception\UnauthorizedException
* @throws \Exception
*/
abstract protected function buildWhereClause();
/**
* Populate $this->selectFields
*
* @throws \Civi\API\Exception\UnauthorizedException
*/
protected function buildSelectFields() {
$return_all_fields = (empty($this->select) || !is_array($this->select));
$return = $return_all_fields ? $this->entityFieldNames : $this->select;
if ($return_all_fields || in_array('custom', $this->select)) {
foreach (array_keys($this->apiFieldSpec) as $fieldName) {
if (strpos($fieldName, 'custom_') === 0) {
$return[] = $fieldName;
}
}
}
// Always select the ID if the table has one.
if (array_key_exists('id', $this->apiFieldSpec)) {
$this->selectFields[self::MAIN_TABLE_ALIAS . ".id"] = "id";
}
// core return fields
foreach ($return as $fieldName) {
$field = $this->getField($fieldName);
if ($field && in_array($field['name'], $this->entityFieldNames)) {
$this->selectFields[self::MAIN_TABLE_ALIAS . ".{$field['name']}"] = $field['name'];
}
elseif (strpos($fieldName, '.')) {
$fkField = $this->addFkField($fieldName, 'LEFT');
if ($fkField) {
$this->selectFields[implode('.', $fkField)] = $fieldName;
}
}
elseif ($field && strpos($fieldName, 'custom_') === 0) {
list($table_name, $column_name) = $this->addCustomField($field, 'LEFT');
if ($field['data_type'] != 'ContactReference') {
// 'ordinary' custom field. We will select the value as custom_XX.
$this->selectFields["$table_name.$column_name"] = $fieldName;
}
else {
// contact reference custom field. The ID will be stored in custom_XX_id.
// custom_XX will contain the sort name of the contact.
$this->query->join("c_$fieldName", "LEFT JOIN civicrm_contact c_$fieldName ON c_$fieldName.id = `$table_name`.`$column_name`");
$this->selectFields["$table_name.$column_name"] = $fieldName . "_id";
// We will call the contact table for the join c_XX.
$this->selectFields["c_$fieldName.sort_name"] = $fieldName;
}
}
}
}
/**
* Load entity fields
* @return array
*/
abstract protected function getFields();
}

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);
}
}
}

View file

@ -0,0 +1,282 @@
<?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;
/**
* A WhitelistRule is used to determine if an API call is authorized.
* For example:
*
* @code
* new WhitelistRule(array(
* 'entity' => 'Contact',
* 'actions' => array('get','getsingle'),
* 'required' => array('contact_type' => 'Organization'),
* 'fields' => array('id', 'display_name', 'sort_name', 'created_date'),
* ));
* @endcode
*
* This rule would allow API requests that attempt to get contacts of type "Organization",
* but only a handful of fields ('id', 'display_name', 'sort_name', 'created_date')
* can be filtered or returned.
*
* Class WhitelistRule
* @package Civi\API\Subscriber
*/
class WhitelistRule {
static $IGNORE_FIELDS = array(
'check_permissions',
'debug',
'offset',
'option_offset',
'option_limit',
'option_sort',
'options',
'return',
'rowCount',
'sequential',
'sort',
'version',
);
/**
* Create a batch of rules from an array.
*
* @param array $rules
* @return array
*/
public static function createAll($rules) {
$whitelist = array();
foreach ($rules as $rule) {
$whitelist[] = new WhitelistRule($rule);
}
return $whitelist;
}
/**
* @var int
*/
public $version;
/**
* Entity name or '*' (all entities)
*
* @var string
*/
public $entity;
/**
* List of actions which match, or '*' (all actions)
*
* @var string|array
*/
public $actions;
/**
* List of key=>value pairs that *must* appear in $params.
*
* If there are no required fields, use an empty array.
*
* @var array
*/
public $required;
/**
* List of fields which may be optionally inputted or returned, or '*" (all fields)
*
* @var array
*/
public $fields;
public function __construct($ruleSpec) {
$this->version = $ruleSpec['version'];
if ($ruleSpec['entity'] === '*') {
$this->entity = '*';
}
else {
$this->entity = Request::normalizeEntityName($ruleSpec['entity'], $ruleSpec['version']);
}
if ($ruleSpec['actions'] === '*') {
$this->actions = '*';
}
else {
$this->actions = array();
foreach ((array) $ruleSpec['actions'] as $action) {
$this->actions[] = Request::normalizeActionName($action, $ruleSpec['version']);
}
}
$this->required = $ruleSpec['required'];
$this->fields = $ruleSpec['fields'];
}
/**
* @return bool
*/
public function isValid() {
if (empty($this->version)) {
return FALSE;
}
if (empty($this->entity)) {
return FALSE;
}
if (!is_array($this->actions) && $this->actions !== '*') {
return FALSE;
}
if (!is_array($this->fields) && $this->fields !== '*') {
return FALSE;
}
if (!is_array($this->required)) {
return FALSE;
}
return TRUE;
}
/**
* @param array $apiRequest
* Parsed API request.
* @return string|TRUE
* If match, return TRUE. Otherwise, return a string with an error code.
*/
public function matches($apiRequest) {
if (!$this->isValid()) {
return 'invalid';
}
if ($this->version != $apiRequest['version']) {
return 'version';
}
if ($this->entity !== '*' && $this->entity !== $apiRequest['entity']) {
return 'entity';
}
if ($this->actions !== '*' && !in_array($apiRequest['action'], $this->actions)) {
return 'action';
}
// These params *must* be included for the API request to proceed.
foreach ($this->required as $param => $value) {
if (!isset($apiRequest['params'][$param])) {
return 'required-missing-' . $param;
}
if ($value !== '*' && $apiRequest['params'][$param] != $value) {
return 'required-wrong-' . $param;
}
}
// These params *may* be included at the caller's discretion
if ($this->fields !== '*') {
$activatedFields = array_keys($apiRequest['params']);
$activatedFields = preg_grep('/^api\./', $activatedFields, PREG_GREP_INVERT);
if ($apiRequest['action'] == 'get') {
// Kind'a silly we need to (re(re))parse here for each rule; would be more
// performant if pre-parsed by Request::create().
$options = _civicrm_api3_get_options_from_params($apiRequest['params'], TRUE, $apiRequest['entity'], 'get');
$return = \CRM_Utils_Array::value('return', $options, array());
$activatedFields = array_merge($activatedFields, array_keys($return));
}
$unknowns = array_diff(
$activatedFields,
array_keys($this->required),
$this->fields,
self::$IGNORE_FIELDS
);
if (!empty($unknowns)) {
return 'unknown-' . implode(',', $unknowns);
}
}
return TRUE;
}
/**
* Ensure that the return values comply with the whitelist's
* "fields" policy.
*
* Most API's follow a convention where the result includes
* a 'values' array (which in turn is a list of records). Unfortunately,
* some don't. If the API result doesn't meet our expectation,
* then we probably don't know what's going on, so we abort the
* request.
*
* This will probably break some of the layered-sugar APIs (like
* getsingle, getvalue). Just use the meat-and-potatoes API instead.
* Or craft a suitably targeted patch.
*
* @param array $apiRequest
* API request.
* @param array $apiResult
* API result.
* @return array
* Modified API result.
* @throws \API_Exception
*/
public function filter($apiRequest, $apiResult) {
if ($this->fields === '*') {
return $apiResult;
}
if (isset($apiResult['values']) && empty($apiResult['values'])) {
// No data; filtering doesn't matter.
return $apiResult;
}
if (is_array($apiResult['values'])) {
$firstRow = \CRM_Utils_Array::first($apiResult['values']);
if (is_array($firstRow)) {
$fields = $this->filterFields(array_keys($firstRow));
$apiResult['values'] = \CRM_Utils_Array::filterColumns($apiResult['values'], $fields);
return $apiResult;
}
}
throw new \API_Exception(sprintf('Filtering failed for %s.%s. Unrecognized result format.', $apiRequest['entity'], $apiRequest['action']));
}
/**
* Determine which elements in $keys are acceptable under
* the whitelist policy.
*
* @param array $keys
* List of possible keys.
* @return array
* List of acceptable keys.
*/
protected function filterFields($keys) {
$r = array();
foreach ($keys as $key) {
if (in_array($key, $this->fields)) {
$r[] = $key;
}
elseif (preg_match('/^api\./', $key)) {
$r[] = $key;
}
}
return $r;
}
}