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

View file

@ -0,0 +1,76 @@
<?php
namespace Civi\ActionSchedule\Event;
use Civi\ActionSchedule\MappingInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Class MailingQueryEvent
* @package Civi\ActionSchedule\Event
*
* This event allows listeners to modify the query which generates mailing data.
* If you want to fetch extra mail-merge data as part of an initial query, then
* modify the mailing-query to add extra JOINs/SELECTs.
*
* The basic mailing query looks a bit like this (depending on configuration):
*
* @code
* SELECT reminder.id AS reminderID, reminder.contact_id as contactID, ...
* FROM `civicrm_action_log` reminder
* ... JOIN `target_entity` e ON e.id = reminder.entity_id ...
* WHERE reminder.action_schedule_id = #casActionScheduleId
* @endcode
*
* Listeners may modify the query. For example, suppose we want to load
* additional fields from the related 'foo' entity:
*
* @code
* $event->query->join('foo', '!casMailingJoinType civicrm_foo foo ON foo.myentity_id = e.id')
* ->select('foo.bar_value AS bar');
* @endcode
*
* There are several parameters pre-set for use in queries:
* - 'casActionScheduleId'
* - 'casEntityJoinExpr' - eg 'e.id = reminder.entity_id'
* - 'casMailingJoinType' - eg 'LEFT JOIN' or 'INNER JOIN' (depending on configuration)
* - 'casMappingId'
* - 'casMappingEntity'
*
* (Note: When adding more JOINs, it seems typical to use !casMailingJoinType, although
* some hard-code a LEFT JOIN. Don't have an explanation for why.)
*/
class MailingQueryEvent extends Event {
/**
* The schedule record which produced this mailing.
*
* @var \CRM_Core_DAO_ActionSchedule
*/
public $actionSchedule;
/**
* The mapping record which produced this mailing.
*
* @var MappingInterface
*/
public $mapping;
/**
* The alterable query. For details, see the class description.
* @var \CRM_Utils_SQL_Select
* @see MailingQueryEvent
*/
public $query;
/**
* @param \CRM_Core_DAO_ActionSchedule $actionSchedule
* @param MappingInterface $mapping
* @param \CRM_Utils_SQL_Select $query
*/
public function __construct($actionSchedule, $mapping, $query) {
$this->actionSchedule = $actionSchedule;
$this->mapping = $mapping;
$this->query = $query;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Civi\ActionSchedule\Event;
use Civi\ActionSchedule\MappingInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Class ActionScheduleEvent
* @package Civi\ActionSchedule\Event
*
* Register any available mappings.
*/
class MappingRegisterEvent extends Event {
/**
* @var array
* Array(scalar $id => Mapping $mapping).
*/
protected $mappings = array();
/**
* Register a new mapping.
*
* @param MappingInterface $mapping
* The new mapping.
* @return MappingRegisterEvent
*/
public function register(MappingInterface $mapping) {
$this->mappings[$mapping->getId()] = $mapping;
return $this;
}
/**
* @return array
* Array(scalar $id => MappingInterface $mapping).
*/
public function getMappings() {
ksort($this->mappings);
return $this->mappings;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Civi\ActionSchedule;
class Events {
/**
* Register any available mappings.
*
* @see EntityListEvent
*/
const MAPPINGS = 'civi.actionSchedule.getMappings';
/**
* Prepare the pre-mailing query. This query loads details about
* the contact/entity so that they're available for mail-merge.
*/
const MAILING_QUERY = 'civi.actionSchedule.prepareMailingQuery';
}

View file

@ -0,0 +1,344 @@
<?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\ActionSchedule;
/**
* Class Mapping
* @package Civi\ActionSchedule
*
* This is the initial implementation of MappingInterface; it was
* constructed by cutting out swaths from CRM_Core_BAO_ActionSchedule.
* New implementers should consider implementing MappingInterface on
* their own.
*
* Background: The original designers of ActionMappings intended that
* one could create and configure new mappings through the database.
* To, e.g., define the filtering options for CiviEvent, you
* would insert a record in "civicrm_action_mapping" with fields like
* "entity" (a table name, eg "civicrm_event"), "entity_value" (an
* option-group name, eg "event_types").
*
* Unfortunately, the metadata in "civicrm_action_mapping" proved
* inadequate and was not updated to cope. Instead, a number
* of work-arounds for specific entities were hard-coded into
* the core action-scheduling code. Ultimately, to add a new
* mapping type, one needed to run around and patch a dozen
* places.
*
* The new MappingInterface makes no pretense of database-driven
* configuration. The dozen places have been consolidated and
* replaced with functions in MappingInterface.
*
* This "Mapping" implementation is a refactoring of the old
* hard-coded bits. Internally, it uses the concepts from
* "civicrm_action_mapping". The resulting code is more
* convoluted than a clean implementation of MappingInterface, but
* it strictly matches the old behavior (based on logging/comparing
* the queries produced through ActionScheduleTest).
*/
abstract class Mapping implements MappingInterface {
private static $fields = array(
'id',
'entity',
'entity_label',
'entity_value',
'entity_value_label',
'entity_status',
'entity_status_label',
'entity_date_start',
'entity_date_end',
);
/**
* Create mapping.
*
* @param array $params
*
* @return static
*/
public static function create($params) {
return new static($params);
}
/**
* Class constructor.
*
* @param array $params
*/
public function __construct($params) {
foreach (self::$fields as $field) {
if (isset($params[$field])) {
$this->{$field} = $params[$field];
}
}
}
protected $id;
/**
* The basic entity to query (table name).
*
* @var string
* Ex: 'civicrm_activity', 'civicrm_event'.
*/
protected $entity;
/**
* The basic entity to query (label).
*
* @var
* Ex: 'Activity', 'Event'
*/
private $entity_label;
/**
* Level 1 filter -- the field/option-list to filter on.
*
* @var string
* Ex: 'activity_type', 'civicrm_event', 'event_template'.
*/
private $entity_value;
/**
* Level 1 filter -- The field label.
*
* @var string
* Ex: 'Activity Type', 'Event Name', 'Event Template'.
*/
private $entity_value_label;
/**
* Level 2 filter -- the field/option-list to filter on.
* @var string
* Ex: 'activity_status, 'civicrm_participant_status_type', 'auto_renew_options'.
*/
private $entity_status;
/**
* Level 2 filter -- the field label.
* @var string
* Ex: 'Activity Status', 'Participant Status', 'Auto Rewnewal Options'.
*/
private $entity_status_label;
/**
* Date filter -- the field name.
* @var string|NULL
* Ex: 'event_start_date'
*/
private $entity_date_start;
/**
* Date filter -- the field name.
* @var string|NULL
* Ex: 'event_end_date'.
*/
private $entity_date_end;
/**
* @return mixed
*/
public function getId() {
return $this->id;
}
/**
* @return string
*/
public function getEntity() {
return $this->entity;
}
/**
* Get a printable label for this mapping type.
*
* @return string
*/
public function getLabel() {
return $this->entity_label;
}
/**
* Get a printable label to use a header on the 'value' filter.
*
* @return string
*/
public function getValueHeader() {
return $this->entity_value_label;
}
/**
* Get a printable label to use a header on the 'status' filter.
*
* @return string
*/
public function getStatusHeader() {
return $this->entity_status_label;
}
/**
* Get a list of value options.
*
* @return array
* Array(string $value => string $label).
* Ex: array(123 => 'Phone Call', 456 => 'Meeting').
*/
public function getValueLabels() {
return self::getValueLabelMap($this->entity_value);
}
/**
* Get a list of status options.
*
* @param string|int $value
* The list of status options may be contingent upon the selected filter value.
* This is the selected filter value.
* @return array
* Array(string $value => string $label).
* Ex: Array(123 => 'Completed', 456 => 'Scheduled').
*/
public function getStatusLabels($value) {
if ($this->entity_status === 'auto_renew_options') {
if ($value && \CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType', $value, 'auto_renew')) {
return \CRM_Core_OptionGroup::values('auto_renew_options');
}
else {
return array();
}
}
return self::getValueLabelMap($this->entity_status);
}
/**
* Get a list of available date fields.
*
* @return array
* Array(string $fieldName => string $fieldLabel).
*/
public function getDateFields() {
$dateFieldLabels = array();
if (!empty($this->entity_date_start)) {
$dateFieldLabels[$this->entity_date_start] = ucwords(str_replace('_', ' ', $this->entity_date_start));
}
if (!empty($this->entity_date_end)) {
$dateFieldLabels[$this->entity_date_end] = ucwords(str_replace('_', ' ', $this->entity_date_end));
}
return $dateFieldLabels;
}
/**
* Get a list of recipient types.
*
* Note: A single schedule may filter on *zero* or *one* recipient types.
* When an admin chooses a value, it's stored in $schedule->recipient.
*
* @return array
* array(string $value => string $label).
* Ex: array('assignee' => 'Activity Assignee').
*/
public function getRecipientTypes() {
return array();
}
/**
* Get a list of recipients which match the given type.
*
* Note: A single schedule may filter on *multiple* recipients.
* When an admin chooses value(s), it's stored in $schedule->recipient_listing.
*
* @param string $recipientType
* Ex: 'participant_role'.
* @return array
* Array(mixed $name => string $label).
* Ex: array(1 => 'Attendee', 2 => 'Volunteer').
* @see getRecipientTypes
*/
public function getRecipientListing($recipientType) {
return array();
}
protected static function getValueLabelMap($name) {
static $valueLabelMap = NULL;
if ($valueLabelMap === NULL) {
// CRM-20510: Include CiviCampaign activity types along with CiviCase IF component is enabled
$valueLabelMap['activity_type'] = \CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'label', TRUE);
asort($valueLabelMap['activity_type']);
$valueLabelMap['activity_status'] = \CRM_Core_PseudoConstant::activityStatus();
$valueLabelMap['event_type'] = \CRM_Event_PseudoConstant::eventType();
$valueLabelMap['civicrm_event'] = \CRM_Event_PseudoConstant::event(NULL, FALSE, "( is_template IS NULL OR is_template != 1 )");
$valueLabelMap['civicrm_participant_status_type'] = \CRM_Event_PseudoConstant::participantStatus(NULL, NULL, 'label');
$valueLabelMap['event_template'] = \CRM_Event_PseudoConstant::eventTemplates();
$valueLabelMap['auto_renew_options'] = \CRM_Core_OptionGroup::values('auto_renew_options');
$valueLabelMap['contact_date_reminder_options'] = \CRM_Core_OptionGroup::values('contact_date_reminder_options');
$valueLabelMap['civicrm_membership_type'] = \CRM_Member_PseudoConstant::membershipType();
$allCustomFields = \CRM_Core_BAO_CustomField::getFields('');
$dateFields = array(
'birth_date' => ts('Birth Date'),
'created_date' => ts('Created Date'),
'modified_date' => ts('Modified Date'),
);
foreach ($allCustomFields as $fieldID => $field) {
if ($field['data_type'] == 'Date') {
$dateFields["custom_$fieldID"] = $field['label'];
}
}
$valueLabelMap['civicrm_contact'] = $dateFields;
}
return $valueLabelMap[$name];
}
/**
* Determine whether a schedule based on this mapping is sufficiently
* complete.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @return array
* Array (string $code => string $message).
* List of error messages.
*/
public function validateSchedule($schedule) {
return array();
}
/**
* Generate a query to locate contacts who match the given
* schedule.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @param string $phase
* See, e.g., RecipientBuilder::PHASE_RELATION_FIRST.
* @param array $defaultParams
* @return \CRM_Utils_SQL_Select
*/
public abstract function createQuery($schedule, $phase, $defaultParams);
}

View file

@ -0,0 +1,148 @@
<?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\ActionSchedule;
/**
* Interface MappingInterface
* @package Civi\ActionSchedule
*/
interface MappingInterface {
/**
* @return mixed
*/
public function getId();
/**
* @return string
*/
public function getEntity();
/**
* Get a printable label for this mapping type.
*
* @return string
*/
public function getLabel();
/**
* Get a printable label to use as the header on the 'value' filter.
*
* @return string
*/
public function getValueHeader();
/**
* Get a printable label to use as the header on the 'status' filter.
*
* @return string
*/
public function getStatusHeader();
/**
* Get a list of value options.
*
* @return array
* Array(string $value => string $label).
* Ex: array(123 => 'Phone Call', 456 => 'Meeting').
*/
public function getValueLabels();
/**
* Get a list of status options.
*
* @param string|int $value
* The list of status options may be contingent upon the selected filter value.
* This is the selected filter value.
* @return array
* Array(string $value => string $label).
* Ex: Array(123 => 'Completed', 456 => 'Scheduled').
*/
public function getStatusLabels($value);
/**
* Get a list of available date fields.
*
* @return array
* Array(string $fieldName => string $fieldLabel).
*/
public function getDateFields();
/**
* Get a list of recipient types.
*
* Note: A single schedule may filter on *zero* or *one* recipient types.
* When an admin chooses a value, it's stored in $schedule->recipient.
*
* @return array
* array(string $value => string $label).
* Ex: array('assignee' => 'Activity Assignee').
*/
public function getRecipientTypes();
/**
* Get a list of recipients which match the given type.
*
* Note: A single schedule may filter on *multiple* recipients.
* When an admin chooses value(s), it's stored in $schedule->recipient_listing.
*
* @param string $recipientType
* Ex: 'participant_role'.
* @return array
* Array(mixed $name => string $label).
* Ex: array(1 => 'Attendee', 2 => 'Volunteer').
* @see getRecipientTypes
*/
public function getRecipientListing($recipientType);
/**
* Determine whether a schedule based on this mapping is sufficiently
* complete.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @return array
* Array (string $code => string $message).
* List of error messages.
*/
public function validateSchedule($schedule);
/**
* Generate a query to locate contacts who match the given
* schedule.
*
* @param \CRM_Core_DAO_ActionSchedule $schedule
* @param string $phase
* See, e.g., RecipientBuilder::PHASE_RELATION_FIRST.
* @param array $defaultParams
* Default parameters that should be included with query.
* @return \CRM_Utils_SQL_Select
* @see RecipientBuilder
*/
public function createQuery($schedule, $phase, $defaultParams);
}

View file

@ -0,0 +1,668 @@
<?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\ActionSchedule;
/**
* Class RecipientBuilder
* @package Civi\ActionSchedule
*
* The RecipientBuilder prepares a list of recipients based on an action-schedule.
*
* This is a four-step process, with different steps depending on:
*
* (a) How the recipient is identified. Sometimes recipients are identified based
* on their relations (e.g. selecting the assignees of an activity or the
* participants of an event), and sometimes they are manually added using
* a flat contact list (e.g. with a contact ID or group ID).
* (b) Whether this is the first reminder or a follow-up/repeated reminder.
*
* The permutations of these (a)+(b) produce four phases -- RELATION_FIRST,
* RELATION_REPEAT, ADDITION_FIRST, ADDITION_REPEAT.
*
* Each phase requires running a complex query. As a general rule,
* MappingInterface::createQuery() produces a base query, and the RecipientBuilder
* appends extra bits (JOINs/WHEREs/GROUP BYs) depending on which step is running.
*
* For example, suppose we want to send reminders to anyone who registers for
* a "Conference" or "Exhibition" event with the 'pay later' option, and we want
* to fire the reminders X days after the registration date. The
* MappingInterface::createQuery() could return a query like:
*
* @code
* CRM_Utils_SQL_Select::from('civicrm_participant e')
* ->join('event', 'INNER JOIN civicrm_event event ON e.event_id = event.id')
* ->where('e.is_pay_later = 1')
* ->where('event.event_type_id IN (#myEventTypes)')
* ->param('myEventTypes', array(2, 5))
* ->param('casDateField', 'e.register_date')
* ->param($defaultParams)
* ...etc...
* @endcode
*
* In the RELATION_FIRST phase, RecipientBuilder adds a LEFT-JOIN+WHERE to find
* participants who have *not* yet received any reminder, and filters those
* participants based on whether X days have passed since "e.register_date".
*
* Notice that the query may define several SQL elements directly (eg
* via `from()`, `where()`, `join()`, `groupBy()`). Additionally, it
* must define some parameters (eg `casDateField`). These parameters will be
* read by RecipientBuilder and used in other parts of the query.
*
* At time of writing, these parameters are required:
* - casAddlCheckFrom: string, SQL FROM expression
* - casContactIdField: string, SQL column expression
* - casDateField: string, SQL column expression
* - casEntityIdField: string, SQL column expression
*
* Some parameters are optional:
* - casContactTableAlias: string, SQL table alias
* - casAnniversaryMode: bool
* - casUseReferenceDate: bool
*
* Additionally, some parameters are automatically predefined:
* - casNow
* - casMappingEntity: string, SQL table name
* - casMappingId: int
* - casActionScheduleId: int
*
* Note: Any parameters defined by the core Civi\ActionSchedule subsystem
* use the prefix `cas`. If you define new parameters (like `myEventTypes`
* above), then use a different name (to avoid conflicts).
*/
class RecipientBuilder {
private $now;
/**
* Generate action_log's for new, first-time alerts to related contacts.
*
* @see buildRelFirstPass
*/
const PHASE_RELATION_FIRST = 'rel-first';
/**
* Generate action_log's for new, first-time alerts to additional contacts.
*
* @see buildAddlFirstPass
*/
const PHASE_ADDITION_FIRST = 'addl-first';
/**
* Generate action_log's for repeated, follow-up alerts to related contacts.
*
* @see buildRelRepeatPass
*/
const PHASE_RELATION_REPEAT = 'rel-repeat';
/**
* Generate action_log's for repeated, follow-up alerts to additional contacts.
*
* @see buildAddlRepeatPass
*/
const PHASE_ADDITION_REPEAT = 'addl-repeat';
/**
* @var \CRM_Core_DAO_ActionSchedule
*/
private $actionSchedule;
/**
* @var MappingInterface
*/
private $mapping;
/**
* @param $now
* @param \CRM_Core_DAO_ActionSchedule $actionSchedule
* @param MappingInterface $mapping
*/
public function __construct($now, $actionSchedule, $mapping) {
$this->now = $now;
$this->actionSchedule = $actionSchedule;
$this->mapping = $mapping;
}
/**
* Fill the civicrm_action_log with any new/missing TODOs.
*
* @throws \CRM_Core_Exception
*/
public function build() {
$this->buildRelFirstPass();
if ($this->prepareAddlFilter('c.id')) {
$this->buildAddlFirstPass();
}
if ($this->actionSchedule->is_repeat) {
$this->buildRelRepeatPass();
}
if ($this->actionSchedule->is_repeat && $this->prepareAddlFilter('c.id')) {
$this->buildAddlRepeatPass();
}
}
/**
* Generate action_log's for new, first-time alerts to related contacts.
*
* @throws \Exception
*/
protected function buildRelFirstPass() {
$query = $this->prepareQuery(self::PHASE_RELATION_FIRST);
$startDateClauses = $this->prepareStartDateClauses();
// In some cases reference_date got outdated due to many reason e.g. In Membership renewal end_date got extended
// which means reference date mismatches with the end_date where end_date may be used as the start_action_date
// criteria for some schedule reminder so in order to send new reminder we INSERT new reminder with new reference_date
// value via UNION operation
$referenceReminderIDs = array();
$referenceDate = NULL;
if (!empty($query['casUseReferenceDate'])) {
// First retrieve all the action log's ids which are outdated or in other words reference_date now don't match with entity date.
// And the retrieve the updated entity date which will later used below to update all other outdated action log records
$sql = $query->copy()
->select('reminder.id as id')
->select($query['casDateField'] . ' as reference_date')
->merge($this->joinReminder('INNER JOIN', 'rel', $query))
->where("reminder.id IS NOT NULL AND reminder.reference_date IS NOT NULL AND reminder.reference_date <> !casDateField")
->where($startDateClauses)
->orderBy("reminder.id desc")
->strict()
->toSQL();
$dao = \CRM_Core_DAO::executeQuery($sql);
while ($dao->fetch()) {
$referenceReminderIDs[] = $dao->id;
$referenceDate = $dao->reference_date;
}
}
if (empty($referenceReminderIDs)) {
$firstQuery = $query->copy()
->merge($this->selectIntoActionLog(self::PHASE_RELATION_FIRST, $query))
->merge($this->joinReminder('LEFT JOIN', 'rel', $query))
->where("reminder.id IS NULL")
->where($startDateClauses)
->strict()
->toSQL();
\CRM_Core_DAO::executeQuery($firstQuery);
}
else {
// INSERT new log to send reminder as desired entity date got updated
$referenceQuery = $query->copy()
->merge($this->selectIntoActionLog(self::PHASE_RELATION_FIRST, $query))
->merge($this->joinReminder('LEFT JOIN', 'rel', $query))
->where("reminder.id = !reminderID")
->where($startDateClauses)
->param('reminderID', $referenceReminderIDs[0])
->strict()
->toSQL();
\CRM_Core_DAO::executeQuery($referenceQuery);
// Update all the previous outdated reference date valued, action_log rows to the latest changed entity date
$updateQuery = "UPDATE civicrm_action_log SET reference_date = '" . $referenceDate . "' WHERE id IN (" . implode(', ', $referenceReminderIDs) . ")";
\CRM_Core_DAO::executeQuery($updateQuery);
}
}
/**
* Generate action_log's for new, first-time alerts to additional contacts.
*
* @throws \Exception
*/
protected function buildAddlFirstPass() {
$query = $this->prepareQuery(self::PHASE_ADDITION_FIRST);
$insertAdditionalSql = \CRM_Utils_SQL_Select::from("civicrm_contact c")
->merge($query, array('params'))
->merge($this->selectIntoActionLog(self::PHASE_ADDITION_FIRST, $query))
->merge($this->joinReminder('LEFT JOIN', 'addl', $query))
->where('reminder.id IS NULL')
->where("c.is_deleted = 0 AND c.is_deceased = 0")
->merge($this->prepareAddlFilter('c.id'))
->where("c.id NOT IN (
SELECT rem.contact_id
FROM civicrm_action_log rem INNER JOIN {$this->mapping->getEntity()} e ON rem.entity_id = e.id
WHERE rem.action_schedule_id = {$this->actionSchedule->id}
AND rem.entity_table = '{$this->mapping->getEntity()}'
)")
// Where does e.id come from here? ^^^
->groupBy("c.id")
->strict()
->toSQL();
\CRM_Core_DAO::executeQuery($insertAdditionalSql);
}
/**
* Generate action_log's for repeated, follow-up alerts to related contacts.
*
* @throws \CRM_Core_Exception
* @throws \Exception
*/
protected function buildRelRepeatPass() {
$query = $this->prepareQuery(self::PHASE_RELATION_REPEAT);
$startDateClauses = $this->prepareStartDateClauses();
// CRM-15376 - do not send our reminders if original criteria no longer applies
// the first part of the startDateClause array is the earliest the reminder can be sent. If the
// event (e.g membership_end_date) has changed then the reminder may no longer apply
// @todo - this only handles events that get moved later. Potentially they might get moved earlier
$repeatInsert = $query
->merge($this->joinReminder('INNER JOIN', 'rel', $query))
->merge($this->selectActionLogFields(self::PHASE_RELATION_REPEAT, $query))
->select("MAX(reminder.action_date_time) as latest_log_time")
->merge($this->prepareRepetitionEndFilter($query['casDateField']))
->where($this->actionSchedule->start_action_date ? $startDateClauses[0] : array())
->groupBy("reminder.contact_id, reminder.entity_id, reminder.entity_table")
// @todo replace use of timestampdiff with a direct comparison as TIMESTAMPDIFF cannot use an index.
->having("TIMESTAMPDIFF(HOUR, latest_log_time, CAST(!casNow AS datetime)) >= TIMESTAMPDIFF(HOUR, latest_log_time, DATE_ADD(latest_log_time, INTERVAL !casRepetitionInterval))")
->param(array(
'casRepetitionInterval' => $this->parseRepetitionInterval(),
))
->strict()
->toSQL();
// For unknown reasons, we manually insert each row. Why not change
// selectActionLogFields() to selectIntoActionLog() above?
$arrValues = \CRM_Core_DAO::executeQuery($repeatInsert)->fetchAll();
if ($arrValues) {
\CRM_Core_DAO::executeQuery(
\CRM_Utils_SQL_Insert::into('civicrm_action_log')
->columns(array('contact_id', 'entity_id', 'entity_table', 'action_schedule_id'))
->rows($arrValues)
->toSQL()
);
}
}
/**
* Generate action_log's for repeated, follow-up alerts to additional contacts.
*
* @throws \CRM_Core_Exception
* @throws \Exception
*/
protected function buildAddlRepeatPass() {
$query = $this->prepareQuery(self::PHASE_ADDITION_REPEAT);
$addlCheck = \CRM_Utils_SQL_Select::from($query['casAddlCheckFrom'])
->select('*')
->merge($query, array('params', 'wheres'))// why only where? why not the joins?
->merge($this->prepareRepetitionEndFilter($query['casDateField']))
->limit(1)
->strict()
->toSQL();
$daoCheck = \CRM_Core_DAO::executeQuery($addlCheck);
if ($daoCheck->fetch()) {
$repeatInsertAddl = \CRM_Utils_SQL_Select::from('civicrm_contact c')
->merge($this->selectActionLogFields(self::PHASE_ADDITION_REPEAT, $query))
->merge($this->joinReminder('INNER JOIN', 'addl', $query))
->select("MAX(reminder.action_date_time) as latest_log_time")
->merge($this->prepareAddlFilter('c.id'), array('params'))
->where("c.is_deleted = 0 AND c.is_deceased = 0")
->groupBy("reminder.contact_id")
// @todo replace use of timestampdiff with a direct comparison as TIMESTAMPDIFF cannot use an index.
->having("TIMESTAMPDIFF(HOUR, latest_log_time, CAST(!casNow AS datetime)) >= TIMESTAMPDIFF(HOUR, latest_log_time, DATE_ADD(latest_log_time, INTERVAL !casRepetitionInterval))")
->param(array(
'casRepetitionInterval' => $this->parseRepetitionInterval(),
))
->strict()
->toSQL();
// For unknown reasons, we manually insert each row. Why not change
// selectActionLogFields() to selectIntoActionLog() above?
$addValues = \CRM_Core_DAO::executeQuery($repeatInsertAddl)->fetchAll();
if ($addValues) {
\CRM_Core_DAO::executeQuery(
\CRM_Utils_SQL_Insert::into('civicrm_action_log')
->columns(array('contact_id', 'entity_id', 'entity_table', 'action_schedule_id'))
->rows($addValues)
->toSQL()
);
}
}
}
/**
* @param string $phase
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function prepareQuery($phase) {
$defaultParams = array(
'casActionScheduleId' => $this->actionSchedule->id,
'casMappingId' => $this->mapping->getId(),
'casMappingEntity' => $this->mapping->getEntity(),
'casNow' => $this->now,
);
/** @var \CRM_Utils_SQL_Select $query */
$query = $this->mapping->createQuery($this->actionSchedule, $phase, $defaultParams);
if ($this->actionSchedule->limit_to /*1*/) {
$query->merge($this->prepareContactFilter($query['casContactIdField']));
}
if (empty($query['casContactTableAlias'])) {
$query['casContactTableAlias'] = 'c';
$query->join('c', "INNER JOIN civicrm_contact c ON c.id = !casContactIdField AND c.is_deleted = 0 AND c.is_deceased = 0 ");
}
$multilingual = \CRM_Core_I18n::isMultilingual();
if ($multilingual && !empty($this->actionSchedule->filter_contact_language)) {
$query->where($this->prepareLanguageFilter($query['casContactTableAlias']));
}
return $query;
}
/**
* Parse repetition interval.
*
* @return int|string
*/
protected function parseRepetitionInterval() {
$actionSchedule = $this->actionSchedule;
if ($actionSchedule->repetition_frequency_unit == 'day') {
$interval = "{$actionSchedule->repetition_frequency_interval} DAY";
}
elseif ($actionSchedule->repetition_frequency_unit == 'week') {
$interval = "{$actionSchedule->repetition_frequency_interval} WEEK";
}
elseif ($actionSchedule->repetition_frequency_unit == 'month') {
$interval = "{$actionSchedule->repetition_frequency_interval} MONTH";
}
elseif ($actionSchedule->repetition_frequency_unit == 'year') {
$interval = "{$actionSchedule->repetition_frequency_interval} YEAR";
}
else {
$interval = "{$actionSchedule->repetition_frequency_interval} HOUR";
}
return $interval;
}
/**
* Prepare filter options for limiting by contact ID or group ID.
*
* @param string $contactIdField
* @return \CRM_Utils_SQL_Select
*/
protected function prepareContactFilter($contactIdField) {
$actionSchedule = $this->actionSchedule;
if ($actionSchedule->group_id) {
if ($this->isSmartGroup($actionSchedule->group_id)) {
// Check that the group is in place in the cache and up to date
\CRM_Contact_BAO_GroupContactCache::check($actionSchedule->group_id);
return \CRM_Utils_SQL_Select::fragment()
->join('grp', "INNER JOIN civicrm_group_contact_cache grp ON {$contactIdField} = grp.contact_id")
->where(" grp.group_id IN ({$actionSchedule->group_id})");
}
else {
return \CRM_Utils_SQL_Select::fragment()
->join('grp', " INNER JOIN civicrm_group_contact grp ON {$contactIdField} = grp.contact_id AND grp.status = 'Added'")
->where(" grp.group_id IN ({$actionSchedule->group_id})");
}
}
elseif (!empty($actionSchedule->recipient_manual)) {
$rList = \CRM_Utils_Type::escape($actionSchedule->recipient_manual, 'String');
return \CRM_Utils_SQL_Select::fragment()
->where("{$contactIdField} IN ({$rList})");
}
return NULL;
}
/**
* Prepare language filter.
*
* @param string $contactTableAlias
* @return string
*/
protected function prepareLanguageFilter($contactTableAlias) {
$actionSchedule = $this->actionSchedule;
// get language filter for the schedule
$filter_contact_language = explode(\CRM_Core_DAO::VALUE_SEPARATOR, $actionSchedule->filter_contact_language);
$w = '';
if (($key = array_search(\CRM_Core_I18n::NONE, $filter_contact_language)) !== FALSE) {
$w .= "{$contactTableAlias}.preferred_language IS NULL OR {$contactTableAlias}.preferred_language = '' OR ";
unset($filter_contact_language[$key]);
}
if (count($filter_contact_language) > 0) {
$w .= "{$contactTableAlias}.preferred_language IN ('" . implode("','", $filter_contact_language) . "')";
}
$w = "($w)";
return $w;
}
/**
* @return array
*/
protected function prepareStartDateClauses() {
$actionSchedule = $this->actionSchedule;
$startDateClauses = array();
if ($actionSchedule->start_action_date) {
$op = ($actionSchedule->start_action_condition == 'before' ? '<=' : '>=');
$operator = ($actionSchedule->start_action_condition == 'before' ? 'DATE_SUB' : 'DATE_ADD');
$date = $operator . "(!casDateField, INTERVAL {$actionSchedule->start_action_offset} {$actionSchedule->start_action_unit})";
$startDateClauses[] = "'!casNow' >= {$date}";
// This is weird. Waddupwidat?
if ($this->mapping->getEntity() == 'civicrm_participant') {
$startDateClauses[] = $operator . "(!casNow, INTERVAL 1 DAY ) {$op} " . '!casDateField';
}
else {
$startDateClauses[] = "DATE_SUB(!casNow, INTERVAL 1 DAY ) <= {$date}";
}
}
elseif ($actionSchedule->absolute_date) {
$startDateClauses[] = "DATEDIFF(DATE('!casNow'),'{$actionSchedule->absolute_date}') = 0";
}
return $startDateClauses;
}
/**
* @param int $groupId
* @return bool
*/
protected function isSmartGroup($groupId) {
// Then decide which table to join onto the query
$group = \CRM_Contact_DAO_Group::getTableName();
// Get the group information
$sql = "
SELECT $group.id, $group.cache_date, $group.saved_search_id, $group.children
FROM $group
WHERE $group.id = {$groupId}
";
$groupDAO = \CRM_Core_DAO::executeQuery($sql);
if (
$groupDAO->fetch() &&
!empty($groupDAO->saved_search_id)
) {
return TRUE;
}
return FALSE;
}
/**
* @param string $dateField
* @return \CRM_Utils_SQL_Select
*/
protected function prepareRepetitionEndFilter($dateField) {
$repeatEventDateExpr = ($this->actionSchedule->end_action == 'before' ? 'DATE_SUB' : 'DATE_ADD')
. "({$dateField}, INTERVAL {$this->actionSchedule->end_frequency_interval} {$this->actionSchedule->end_frequency_unit})";
return \CRM_Utils_SQL_Select::fragment()
->where("@casNow <= !repetitionEndDate")
->param(array(
'!repetitionEndDate' => $repeatEventDateExpr,
));
}
/**
* @param string $contactIdField
* @return \CRM_Utils_SQL_Select|null
*/
protected function prepareAddlFilter($contactIdField) {
$contactAddlFilter = NULL;
if ($this->actionSchedule->limit_to !== NULL && !$this->actionSchedule->limit_to /*0*/) {
$contactAddlFilter = $this->prepareContactFilter($contactIdField);
}
return $contactAddlFilter;
}
/**
* Generate a query fragment like for populating
* action logs, e.g.
*
* "SELECT contact_id, entity_id, entity_table, action schedule_id"
*
* @param string $phase
* @param \CRM_Utils_SQL_Select $query
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function selectActionLogFields($phase, $query) {
switch ($phase) {
case self::PHASE_RELATION_FIRST:
case self::PHASE_RELATION_REPEAT:
$fragment = \CRM_Utils_SQL_Select::fragment();
// CRM-15376: We are not tracking the reference date for 'repeated' schedule reminders.
if (!empty($query['casUseReferenceDate'])) {
$fragment->select($query['casDateField']);
}
$fragment->select(
array(
"!casContactIdField as contact_id",
"!casEntityIdField as entity_id",
"@casMappingEntity as entity_table",
"#casActionScheduleId as action_schedule_id",
)
);
break;
case self::PHASE_ADDITION_FIRST:
case self::PHASE_ADDITION_REPEAT:
//CRM-19017: Load default params for fragment query object.
$params = array(
'casActionScheduleId' => $this->actionSchedule->id,
'casNow' => $this->now,
);
$fragment = \CRM_Utils_SQL_Select::fragment()->param($params);
$fragment->select(
array(
"c.id as contact_id",
"c.id as entity_id",
"'civicrm_contact' as entity_table",
"#casActionScheduleId as action_schedule_id",
)
);
break;
default:
throw new \CRM_Core_Exception("Unrecognized phase: $phase");
}
return $fragment;
}
/**
* Generate a query fragment like for populating
* action logs, e.g.
*
* "INSERT INTO civicrm_action_log (...) SELECT (...)"
*
* @param string $phase
* @param \CRM_Utils_SQL_Select $query
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function selectIntoActionLog($phase, $query) {
$actionLogColumns = array(
"contact_id",
"entity_id",
"entity_table",
"action_schedule_id",
);
if ($phase === self::PHASE_RELATION_FIRST || $phase === self::PHASE_RELATION_REPEAT) {
if (!empty($query['casUseReferenceDate'])) {
array_unshift($actionLogColumns, 'reference_date');
}
}
return $this->selectActionLogFields($phase, $query)
->insertInto('civicrm_action_log', $actionLogColumns);
}
/**
* Add a JOIN clause like "INNER JOIN civicrm_action_log reminder ON...".
*
* @param string $joinType
* Join type (eg INNER JOIN, LEFT JOIN).
* @param string $for
* Ex: 'rel', 'addl'.
* @param \CRM_Utils_SQL_Select $query
* @return \CRM_Utils_SQL_Select
* @throws \CRM_Core_Exception
*/
protected function joinReminder($joinType, $for, $query) {
switch ($for) {
case 'rel':
$contactIdField = $query['casContactIdField'];
$entityName = $this->mapping->getEntity();
$entityIdField = $query['casEntityIdField'];
break;
case 'addl':
$contactIdField = 'c.id';
$entityName = 'civicrm_contact';
$entityIdField = 'c.id';
break;
default:
throw new \CRM_Core_Exception("Unrecognized 'for': $for");
}
$joinClause = "civicrm_action_log reminder ON reminder.contact_id = {$contactIdField} AND
reminder.entity_id = {$entityIdField} AND
reminder.entity_table = '{$entityName}' AND
reminder.action_schedule_id = {$this->actionSchedule->id}";
// Why do we only include anniversary clause for 'rel' queries?
if ($for === 'rel' && !empty($query['casAnniversaryMode'])) {
// only consider reminders less than 11 months ago
$joinClause .= " AND reminder.action_date_time > DATE_SUB(!casNow, INTERVAL 11 MONTH)";
}
return \CRM_Utils_SQL_Select::fragment()->join("reminder", "$joinType $joinClause");
}
}

View file

@ -0,0 +1,236 @@
<?php
namespace Civi\Angular;
/**
* The AngularLoader loads any JS/CSS/JSON resources
* required for setting up AngularJS.
*
* The AngularLoader stops short of bootstrapping AngularJS. You may
* need to `<div ng-app="..."></div>` or `angular.bootstrap(...)`.
*
* @code
* $loader = new AngularLoader();
* $loader->setPageName('civicrm/case/a');
* $loader->setModules(array('crmApp'));
* $loader->load();
* @endCode
*
* @link https://docs.angularjs.org/guide/bootstrap
*/
class AngularLoader {
/**
* The weight to assign to any Angular JS module files.
*/
const DEFAULT_MODULE_WEIGHT = 200;
/**
* The resource manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \CRM_Core_Resources
*/
protected $res;
/**
* The Angular module manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \Civi\Angular\Manager
*/
protected $angular;
/**
* The region of the page into which JavaScript will be loaded.
*
* @var string
*/
protected $region;
/**
* @var string
* Ex: 'civicrm/a'.
*/
protected $pageName;
/**
* @var array
* A list of modules to load.
*/
protected $modules;
/**
* AngularLoader constructor.
*/
public function __construct() {
$this->res = \CRM_Core_Resources::singleton();
$this->angular = \Civi::service('angular');
$this->region = \CRM_Utils_Request::retrieve('snippet', 'String') ? 'ajax-snippet' : 'html-header';
$this->pageName = isset($_GET['q']) ? $_GET['q'] : NULL;
$this->modules = array();
}
/**
* Register resources required by Angular.
*/
public function load() {
$angular = $this->getAngular();
$res = $this->getRes();
$moduleNames = $this->findActiveModules();
if (!$this->isAllModules($moduleNames)) {
$assetParams = array('modules' => implode(',', $moduleNames));
}
else {
// The module list will be "all modules that the user can see".
$assetParams = array('nonce' => md5(implode(',', $moduleNames)));
}
$res->addSettingsFactory(function () use (&$moduleNames, $angular, $res, $assetParams) {
// TODO optimization; client-side caching
$result = array_merge($angular->getResources($moduleNames, 'settings', 'settings'), array(
'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(),
'angular' => array(
'modules' => $moduleNames,
'requires' => $angular->getResources($moduleNames, 'requires', 'requires'),
'cacheCode' => $res->getCacheCode(),
'bundleUrl' => \Civi::service('asset_builder')->getUrl('angular-modules.json', $assetParams),
),
));
return $result;
});
$res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, $this->getRegion(), FALSE);
$res->addScriptFile('civicrm', 'js/crm.angular.js', 101, $this->getRegion(), FALSE);
$headOffset = 0;
$config = \CRM_Core_Config::singleton();
if ($config->debug) {
foreach ($moduleNames as $moduleName) {
foreach ($this->angular->getResources($moduleName, 'css', 'cacheUrl') as $url) {
$res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
}
foreach ($this->angular->getResources($moduleName, 'js', 'cacheUrl') as $url) {
$res->addScriptUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
// addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
}
}
}
else {
// Note: addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
// $aggScriptUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=js&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
$aggScriptUrl = \Civi::service('asset_builder')->getUrl('angular-modules.js', $assetParams);
$res->addScriptUrl($aggScriptUrl, 120, $this->getRegion());
// FIXME: The following CSS aggregator doesn't currently handle path-adjustments - which can break icons.
//$aggStyleUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=css&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
//$aggStyleUrl = \Civi::service('asset_builder')->getUrl('angular-modules.css', $assetParams);
//$res->addStyleUrl($aggStyleUrl, 120, $this->getRegion());
foreach ($this->angular->getResources($moduleNames, 'css', 'cacheUrl') as $url) {
$res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
}
}
}
/**
* Get a list of all Angular modules which should be activated on this
* page.
*
* @return array
* List of module names.
* Ex: array('angularFileUpload', 'crmUi', 'crmUtil').
*/
public function findActiveModules() {
return $this->angular->resolveDependencies(array_merge(
$this->getModules(),
$this->angular->resolveDefaultModules($this->getPageName())
));
}
/**
* @param $moduleNames
* @return int
*/
private function isAllModules($moduleNames) {
$allModuleNames = array_keys($this->angular->getModules());
return count(array_diff($allModuleNames, $moduleNames)) === 0;
}
/**
* @return \CRM_Core_Resources
*/
public function getRes() {
return $this->res;
}
/**
* @param \CRM_Core_Resources $res
*/
public function setRes($res) {
$this->res = $res;
}
/**
* @return \Civi\Angular\Manager
*/
public function getAngular() {
return $this->angular;
}
/**
* @param \Civi\Angular\Manager $angular
*/
public function setAngular($angular) {
$this->angular = $angular;
}
/**
* @return string
*/
public function getRegion() {
return $this->region;
}
/**
* @param string $region
*/
public function setRegion($region) {
$this->region = $region;
}
/**
* @return string
* Ex: 'civicrm/a'.
*/
public function getPageName() {
return $this->pageName;
}
/**
* @param string $pageName
* Ex: 'civicrm/a'.
*/
public function setPageName($pageName) {
$this->pageName = $pageName;
}
/**
* @return array
*/
public function getModules() {
return $this->modules;
}
/**
* @param array $modules
*/
public function setModules($modules) {
$this->modules = $modules;
}
}

View file

@ -0,0 +1,191 @@
<?php
namespace Civi\Angular;
class ChangeSet implements ChangeSetInterface {
/**
* Update a listing of resources.
*
* @param array $changeSets
* Array(ChangeSet).
* @param string $resourceType
* Ex: 'requires', 'settings'
* @param array $resources
* The list of resources.
* @return mixed
*/
public static function applyResourceFilters($changeSets, $resourceType, $resources) {
if ($resourceType === 'partials') {
return self::applyHtmlFilters($changeSets, $resources);
}
foreach ($changeSets as $changeSet) {
/** @var ChangeSet $changeSet */
foreach ($changeSet->resFilters as $filter) {
if ($filter['resourceType'] === $resourceType) {
$resources = call_user_func($filter['callback'], $resources);
}
}
}
return $resources;
}
/**
* Update a set of HTML snippets.
*
* @param array $changeSets
* Array(ChangeSet).
* @param array $strings
* Array(string $path => string $html).
* @return array
* Updated list of $strings.
* @throws \CRM_Core_Exception
*/
private static function applyHtmlFilters($changeSets, $strings) {
$coder = new Coder();
foreach ($strings as $path => $html) {
/** @var \phpQueryObject $doc */
$doc = NULL;
// Most docs don't need phpQueryObject. Initialize phpQuery on first match.
foreach ($changeSets as $changeSet) {
/** @var ChangeSet $changeSet */
foreach ($changeSet->htmlFilters as $filter) {
if (preg_match($filter['regex'], $path)) {
if ($doc === NULL) {
$doc = \phpQuery::newDocument($html, 'text/html');
if (\CRM_Core_Config::singleton()->debug && !$coder->checkConsistentHtml($html)) {
throw new \CRM_Core_Exception("Cannot process $path: inconsistent markup. Use check-angular.php to investigate.");
}
}
call_user_func($filter['callback'], $doc, $path);
}
}
}
if ($doc !== NULL) {
$strings[$path] = $coder->encode($doc);
}
}
return $strings;
}
/**
* @var string
*/
protected $name;
/**
* @var array
* Each item is an array with keys:
* - resourceType: string
* - callback: function
*/
protected $resFilters = array();
/**
* @var array
* Each item is an array with keys:
* - regex: string
* - callback: function
*/
protected $htmlFilters = array();
/**
* @param string $name
* Symbolic name for this changeset.
* @return \Civi\Angular\ChangeSetInterface
*/
public static function create($name) {
$changeSet = new ChangeSet();
$changeSet->name = $name;
return $changeSet;
}
/**
* Declare that $module requires additional dependencies.
*
* @param string $module
* @param string|array $dependencies
* @return ChangeSet
*/
public function requires($module, $dependencies) {
$dependencies = (array) $dependencies;
return $this->alterResource('requires',
function ($values) use ($module, $dependencies) {
if (!isset($values[$module])) {
$values[$module] = array();
}
$values[$module] = array_unique(array_merge($values[$module], $dependencies));
return $values;
});
}
/**
* Declare a change to a resource.
*
* @param string $resourceType
* @param callable $callback
* @return ChangeSet
*/
public function alterResource($resourceType, $callback) {
$this->resFilters[] = array(
'resourceType' => $resourceType,
'callback' => $callback,
);
return $this;
}
/**
* Declare a change to HTML.
*
* @param string $file
* A file name, wildcard, or regex.
* Ex: '~/crmHello/intro.html' (filename)
* Ex: '~/crmHello/*.html' (wildcard)
* Ex: ';(Edit|List)Ctrl\.html$;' (regex)
* @param callable $callback
* Function which accepts up to two parameters:
* - phpQueryObject $doc
* - string $path
* @return ChangeSet
*/
public function alterHtml($file, $callback) {
$this->htmlFilters[] = array(
'regex' => ($file{0} === ';') ? $file : $this->createRegex($file),
'callback' => $callback,
);
return $this;
}
/**
* Convert a string with a wildcard (*) to a regex.
*
* @param string $filterExpr
* Ex: "/foo/*.bar"
* @return string
* Ex: ";^/foo/[^/]*\.bar$;"
*/
protected function createRegex($filterExpr) {
$regex = preg_quote($filterExpr, ';');
$regex = str_replace('\\*', '[^/]*', $regex);
$regex = ";^$regex$;";
return $regex;
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @param string $name
*/
public function setName($name) {
$this->name = $name;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Civi\Angular;
interface ChangeSetInterface {
/**
* Get the symbolic name of the changeset.
*
* @return string
*/
public function getName();
/**
* Declare that $module requires additional dependencies.
*
* @param string $module
* @param string|array $dependencies
* @return ChangeSet
*/
public function requires($module, $dependencies);
/**
* Declare a change to HTML.
*
* @param string $file
* A file name, wildcard, or regex.
* Ex: '~/crmHello/intro.html' (filename)
* Ex: '~/crmHello/*.html' (wildcard)
* Ex: ';(Edit|List)Ctrl\.html$;' (regex)
* @param callable $callback
* Function which accepts up to two parameters:
* - phpQueryObject $doc
* - string $path
* @return ChangeSet
*/
public function alterHtml($file, $callback);
}

View file

@ -0,0 +1,82 @@
<?php
namespace Civi\Angular;
class Coder {
/**
*
* Determine whether an HTML snippet remains consistent (through an
* decode/encode loop).
*
* Note: Variations in whitespace are permitted.
*
* @param string $html
* @return bool
*/
public function checkConsistentHtml($html) {
try {
$recodedHtml = $this->recode($html);
}
catch (\Exception $e) {
return FALSE;
}
$htmlSig = preg_replace('/[ \t\r\n\/]+/', '', $this->cleanup($html));
$docSig = preg_replace('/[ \t\r\n\/]+/', '', $recodedHtml);
if ($htmlSig !== $docSig || empty($html) != empty($htmlSig)) {
return FALSE;
}
return TRUE;
}
/**
* Parse an HTML snippet and re-encode is as HTML.
*
* This is useful for detecting cases where the parser or encoder
* have quirks/bugs.
*
* @param string $html
* @return string
*/
public function recode($html) {
$doc = \phpQuery::newDocument("$html", 'text/html');
return $this->encode($doc);
}
/**
* Encode a phpQueryObject as HTML.
*
* @param \phpQueryObject $doc
* @return string
* HTML
*/
public function encode($doc) {
$doc->document->formatOutput = TRUE;
return $this->cleanup($doc->markupOuter());
}
protected function cleanup($html) {
$html = preg_replace_callback("/([\\-a-zA-Z0-9]+)=(')([^']*)(')/", array($this, 'cleanupAttribute'), $html);
$html = preg_replace_callback('/([\-a-zA-Z0-9]+)=(")([^"]*)(")/', array($this, 'cleanupAttribute'), $html);
return $html;
}
protected function cleanupAttribute($matches) {
list ($full, $attr, $lquote, $value, $rquote) = $matches;
switch ($attr) {
case 'href':
if (strpos($value, '%7B%7B') !== FALSE && strpos($value, '%7D%7D') !== FALSE) {
$value = urldecode($value);
}
break;
default:
$value = html_entity_decode($value);
break;
}
return "$attr=$lquote$value$rquote";
}
}

View file

@ -0,0 +1,413 @@
<?php
namespace Civi\Angular;
/**
* Manage Angular resources.
*
* @package Civi\Angular
*/
class Manager {
/**
* @var \CRM_Core_Resources
*/
protected $res = NULL;
/**
* @var array|NULL
* Each item has some combination of these keys:
* - ext: string
* The Civi extension which defines the Angular module.
* - js: array(string $relativeFilePath)
* List of JS files (relative to the extension).
* - css: array(string $relativeFilePath)
* List of CSS files (relative to the extension).
* - partials: array(string $relativeFilePath)
* A list of partial-HTML folders (relative to the extension).
* This will be mapped to "~/moduleName" by crmResource.
* - settings: array(string $key => mixed $value)
* List of settings to preload.
*/
protected $modules = NULL;
/**
* @var \CRM_Utils_Cache_Interface
*/
protected $cache;
/**
* @var array
* Array(string $name => ChangeSet $change).
*/
protected $changeSets = NULL;
/**
* @param \CRM_Core_Resources $res
* The resource manager.
*/
public function __construct($res, \CRM_Utils_Cache_Interface $cache = NULL) {
$this->res = $res;
$this->cache = $cache ? $cache : new \CRM_Utils_Cache_Arraycache(array());
}
/**
* Get a list of AngularJS modules which should be autoloaded.
*
* @return array
* Each item has some combination of these keys:
* - ext: string
* The Civi extension which defines the Angular module.
* - js: array(string $relativeFilePath)
* List of JS files (relative to the extension).
* - css: array(string $relativeFilePath)
* List of CSS files (relative to the extension).
* - partials: array(string $relativeFilePath)
* A list of partial-HTML folders (relative to the extension).
* This will be mapped to "~/moduleName" by crmResource.
* - settings: array(string $key => mixed $value)
* List of settings to preload.
*/
public function getModules() {
if ($this->modules === NULL) {
$config = \CRM_Core_Config::singleton();
global $civicrm_root;
// Note: It would be nice to just glob("$civicrm_root/ang/*.ang.php"), but at time
// of writing CiviMail and CiviCase have special conditionals.
$angularModules = array();
$angularModules['angularFileUpload'] = include "$civicrm_root/ang/angularFileUpload.ang.php";
$angularModules['crmApp'] = include "$civicrm_root/ang/crmApp.ang.php";
$angularModules['crmAttachment'] = include "$civicrm_root/ang/crmAttachment.ang.php";
$angularModules['crmAutosave'] = include "$civicrm_root/ang/crmAutosave.ang.php";
$angularModules['crmCxn'] = include "$civicrm_root/ang/crmCxn.ang.php";
// $angularModules['crmExample'] = include "$civicrm_root/ang/crmExample.ang.php";
$angularModules['crmResource'] = include "$civicrm_root/ang/crmResource.ang.php";
$angularModules['crmUi'] = include "$civicrm_root/ang/crmUi.ang.php";
$angularModules['crmUtil'] = include "$civicrm_root/ang/crmUtil.ang.php";
$angularModules['dialogService'] = include "$civicrm_root/ang/dialogService.ang.php";
$angularModules['ngRoute'] = include "$civicrm_root/ang/ngRoute.ang.php";
$angularModules['ngSanitize'] = include "$civicrm_root/ang/ngSanitize.ang.php";
$angularModules['ui.utils'] = include "$civicrm_root/ang/ui.utils.ang.php";
$angularModules['ui.bootstrap'] = include "$civicrm_root/ang/ui.bootstrap.ang.php";
$angularModules['ui.sortable'] = include "$civicrm_root/ang/ui.sortable.ang.php";
$angularModules['unsavedChanges'] = include "$civicrm_root/ang/unsavedChanges.ang.php";
$angularModules['statuspage'] = include "$civicrm_root/ang/crmStatusPage.ang.php";
foreach (\CRM_Core_Component::getEnabledComponents() as $component) {
$angularModules = array_merge($angularModules, $component->getAngularModules());
}
\CRM_Utils_Hook::angularModules($angularModules);
foreach (array_keys($angularModules) as $module) {
if (!isset($angularModules[$module]['basePages'])) {
$angularModules[$module]['basePages'] = array('civicrm/a');
}
}
$this->modules = $this->resolvePatterns($angularModules);
}
return $this->modules;
}
/**
* Get the descriptor for an Angular module.
*
* @param string $name
* Module name.
* @return array
* Details about the module:
* - ext: string, the name of the Civi extension which defines the module
* - js: array(string $relativeFilePath).
* - css: array(string $relativeFilePath).
* - partials: array(string $relativeFilePath).
* @throws \Exception
*/
public function getModule($name) {
$modules = $this->getModules();
if (!isset($modules[$name])) {
throw new \Exception("Unrecognized Angular module");
}
return $modules[$name];
}
/**
* Resolve a full list of Angular dependencies.
*
* @param array $names
* List of Angular modules.
* Ex: array('crmMailing').
* @return array
* List of Angular modules, include all dependencies.
* Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute').
*/
public function resolveDependencies($names) {
$allModules = $this->getModules();
$visited = array();
$result = $names;
while (($missingModules = array_diff($result, array_keys($visited))) && !empty($missingModules)) {
foreach ($missingModules as $module) {
$visited[$module] = 1;
if (!isset($allModules[$module])) {
\Civi::log()->warning('Unrecognized Angular module {name}. Please ensure that all Angular modules are declared.', array(
'name' => $module,
'civi.tag' => 'deprecated',
));
}
elseif (isset($allModules[$module]['requires'])) {
$result = array_unique(array_merge($result, $allModules[$module]['requires']));
}
}
}
sort($result);
return $result;
}
/**
* Get a list of Angular modules that should be loaded on the given
* base-page.
*
* @param string $basePage
* The name of the base-page for which we want a list of moudles.
* @return array
* List of Angular modules.
* Ex: array('crmMailing', 'crmUi', 'crmUtil', 'ngRoute').
*/
public function resolveDefaultModules($basePage) {
$modules = $this->getModules();
$result = array();
foreach ($modules as $moduleName => $module) {
if (in_array($basePage, $module['basePages']) || in_array('*', $module['basePages'])) {
$result[] = $moduleName;
}
}
return $result;
}
/**
* Convert any globs in an Angular module to file names.
*
* @param array $modules
* List of Angular modules.
* @return array
* Updated list of Angular modules
*/
protected function resolvePatterns($modules) {
$newModules = array();
foreach ($modules as $moduleKey => $module) {
foreach (array('js', 'css', 'partials') as $fileset) {
if (!isset($module[$fileset])) {
continue;
}
$module[$fileset] = $this->res->glob($module['ext'], $module[$fileset]);
}
$newModules[$moduleKey] = $module;
}
return $newModules;
}
/**
* Get the partial HTML documents for a module (unfiltered).
*
* @param string $name
* Angular module name.
* @return array
* Array(string $extFilePath => string $html)
* @throws \Exception
* Invalid partials configuration.
*/
public function getRawPartials($name) {
$module = $this->getModule($name);
$result = array();
if (isset($module['partials'])) {
foreach ($module['partials'] as $partialDir) {
$partialDir = $this->res->getPath($module['ext']) . '/' . $partialDir;
$files = \CRM_Utils_File::findFiles($partialDir, '*.html', TRUE);
foreach ($files as $file) {
$filename = '~/' . $name . '/' . $file;
$result[$filename] = file_get_contents($partialDir . '/' . $file);
}
}
return $result;
}
return $result;
}
/**
* Get the partial HTML documents for a module.
*
* @param string $name
* Angular module name.
* @return array
* Array(string $extFilePath => string $html)
* @throws \Exception
* Invalid partials configuration.
*/
public function getPartials($name) {
$cacheKey = "angular-partials::$name";
$cacheValue = $this->cache->get($cacheKey);
if ($cacheValue === NULL) {
$cacheValue = ChangeSet::applyResourceFilters($this->getChangeSets(), 'partials', $this->getRawPartials($name));
$this->cache->set($cacheKey, $cacheValue);
}
return $cacheValue;
}
/**
* Get list of translated strings for a module.
*
* @param string $name
* Angular module name.
* @return array
* Translated strings: array(string $orig => string $translated).
*/
public function getTranslatedStrings($name) {
$module = $this->getModule($name);
$result = array();
$strings = $this->getStrings($name);
foreach ($strings as $string) {
// TODO: should we pass translation domain based on $module[ext] or $module[tsDomain]?
// It doesn't look like client side really supports the domain right now...
$translated = ts($string, array(
'domain' => array($module['ext'], NULL),
));
if ($translated != $string) {
$result[$string] = $translated;
}
}
return $result;
}
/**
* Get list of translatable strings for a module.
*
* @param string $name
* Angular module name.
* @return array
* Translatable strings.
*/
public function getStrings($name) {
$module = $this->getModule($name);
$result = array();
if (isset($module['js'])) {
foreach ($module['js'] as $file) {
$strings = $this->res->getStrings()->get(
$module['ext'],
$this->res->getPath($module['ext'], $file),
'text/javascript'
);
$result = array_unique(array_merge($result, $strings));
}
}
$partials = $this->getPartials($name);
foreach ($partials as $partial) {
$result = array_unique(array_merge($result, \CRM_Utils_JS::parseStrings($partial)));
}
return $result;
}
/**
* Get resources for one or more modules.
*
* @param string|array $moduleNames
* List of module names.
* @param string $resType
* Type of resource ('js', 'css', 'settings').
* @param string $refType
* Type of reference to the resource ('cacheUrl', 'rawUrl', 'path', 'settings').
* @return array
* List of URLs or paths.
* @throws \CRM_Core_Exception
*/
public function getResources($moduleNames, $resType, $refType) {
$result = array();
$moduleNames = (array) $moduleNames;
foreach ($moduleNames as $moduleName) {
$module = $this->getModule($moduleName);
if (isset($module[$resType])) {
foreach ($module[$resType] as $file) {
$refTypeSuffix = '';
if (is_string($file) && preg_match(';^(assetBuilder|ext)://;', $file)) {
$refTypeSuffix = '-' . parse_url($file, PHP_URL_SCHEME);
}
switch ($refType . $refTypeSuffix) {
case 'path':
$result[] = $this->res->getPath($module['ext'], $file);
break;
case 'rawUrl':
$result[] = $this->res->getUrl($module['ext'], $file);
break;
case 'cacheUrl':
$result[] = $this->res->getUrl($module['ext'], $file, TRUE);
break;
case 'path-assetBuilder':
$assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH);
$assetParams = array();
parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams);
$result[] = \Civi::service('asset_builder')->getPath($assetName, $assetParams);
break;
case 'rawUrl-assetBuilder':
case 'cacheUrl-assetBuilder':
$assetName = parse_url($file, PHP_URL_HOST) . parse_url($file, PHP_URL_PATH);
$assetParams = array();
parse_str('' . parse_url($file, PHP_URL_QUERY), $assetParams);
$result[] = \Civi::service('asset_builder')->getUrl($assetName, $assetParams);
break;
case 'path-ext':
$result[] = $this->res->getPath(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'));
break;
case 'rawUrl-ext':
$result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'));
break;
case 'cacheUrl-ext':
$result[] = $this->res->getUrl(parse_url($file, PHP_URL_HOST), ltrim(parse_url($file, PHP_URL_PATH), '/'), TRUE);
break;
case 'settings':
case 'requires':
if (!empty($module[$resType])) {
$result[$moduleName] = $module[$resType];
}
break;
default:
throw new \CRM_Core_Exception("Unrecognized resource format");
}
}
}
}
return ChangeSet::applyResourceFilters($this->getChangeSets(), $resType, $result);
}
/**
* @return array
* Array(string $name => ChangeSet $changeSet).
*/
public function getChangeSets() {
if ($this->changeSets === NULL) {
$this->changeSets = array();
\CRM_Utils_Hook::alterAngular($this);
}
return $this->changeSets;
}
/**
* @param ChangeSet $changeSet
* @return \Civi\Angular\Manager
*/
public function add($changeSet) {
$this->changeSets[$changeSet->getName()] = $changeSet;
return $this;
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Civi\Angular\Page;
/**
* This page is simply a container; any Angular modules defined by CiviCRM (or by CiviCRM extensions)
* will be activated on this page.
*
* @link https://issues.civicrm.org/jira/browse/CRM-14479
*/
class Main extends \CRM_Core_Page {
/**
* The weight to assign to any Angular JS module files.
*/
const DEFAULT_MODULE_WEIGHT = 200;
/**
* The resource manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \CRM_Core_Resources
* @deprecated
*/
public $res;
/**
* The Angular module manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \Civi\Angular\Manager
* @deprecated
*/
public $angular;
/**
* The region of the page into which JavaScript will be loaded.
*
* @var String
* @deprecated
*/
public $region;
/**
* @param string $title
* Title of the page.
* @param int $mode
* Mode of the page.
* @param \CRM_Core_Resources|null $res
* Resource manager.
*/
public function __construct($title = NULL, $mode = NULL, $res = NULL) {
parent::__construct($title, $mode);
$this->res = \CRM_Core_Resources::singleton();
$this->angular = \Civi::service('angular');
$this->region = \CRM_Utils_Request::retrieve('snippet', 'String') ? 'ajax-snippet' : 'html-header';
}
/**
* This function takes care of all the things common to all
* pages. This typically involves assigning the appropriate
* smarty variable :)
*
* @return string
* The content generated by running this page
*/
public function run() {
$this->registerResources();
return parent::run();
}
/**
* Register resources required by Angular.
*/
public function registerResources() {
$loader = new \Civi\Angular\AngularLoader();
$loader->setPageName('civicrm/a');
$loader->setModules(array('crmApp'));
$loader->load();
// If trying to load an Angular page via AJAX, the route must be passed as a
// URL parameter, since the server doesn't receive information about
// URL fragments (i.e, what comes after the #).
\CRM_Core_Resources::singleton()->addSetting(array(
'crmApp' => array(
'defaultRoute' => NULL,
),
'angularRoute' => \CRM_Utils_Request::retrieve('route', 'String'),
));
}
/**
* @inheritdoc
*/
public function getTemplateFileName() {
return 'Civi/Angular/Page/Main.tpl';
}
}

View file

@ -0,0 +1,178 @@
<?php
namespace Civi\Angular\Page;
/**
* This page aggregates data from Angular modules.
*
* Example: Aggregate metadata about all modules in JSON format.
* civicrm/ajax/angular-modules?format=json
*
* Example: Aggregate metadata for crmUi and crmUtil modules.
* civicrm/ajax/angular-modules?format=json&modules=crmUi,crmUtil
*
* Example: Aggregate *.js files for all modules.
* civicrm/ajax/angular-modules?format=js
*
* Example: Aggregate *.css files for all modules.
* civicrm/ajax/angular-modules?format=css
*/
class Modules extends \CRM_Core_Page {
/**
* Generate asset content (when accessed via older, custom
* "civicrm/ajax/anulgar-modules" route).
*
* @deprecated
*/
public function run() {
/**
* @var \Civi\Angular\Manager $angular
*/
$angular = \Civi::service('angular');
$moduleNames = $this->parseModuleNames(\CRM_Utils_Request::retrieve('modules', 'String'), $angular);
switch (\CRM_Utils_Request::retrieve('format', 'String')) {
case 'json':
case '':
$this->send(
'application/javascript',
json_encode($this->getMetadata($moduleNames, $angular))
);
break;
case 'js':
$this->send(
'application/javascript',
$this->digestJs($angular->getResources($moduleNames, 'js', 'path'))
);
break;
case 'css':
$this->send(
'text/css',
\CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n")
);
break;
default:
\CRM_Core_Error::fatal("Unrecognized format");
}
\CRM_Utils_System::civiExit();
}
/**
* Generate asset content (when accessed via AssetBuilder).
*
* @param \Civi\Core\Event\GenericHookEvent $event
* @see CRM_Utils_hook::buildAsset()
* @see \Civi\Core\AssetBuilder
*/
public static function buildAngularModules($event) {
$page = new Modules();
$angular = \Civi::service('angular');
switch ($event->asset) {
case 'angular-modules.json':
$moduleNames = $page->parseModuleNames(\CRM_Utils_Array::value('modules', $event->params), $angular);
$event->mimeType = 'application/json';
$event->content = json_encode($page->getMetadata($moduleNames, $angular));
break;
case 'angular-modules.js':
$moduleNames = $page->parseModuleNames(\CRM_Utils_Array::value('modules', $event->params), $angular);
$event->mimeType = 'application/javascript';
$event->content = $page->digestJs($angular->getResources($moduleNames, 'js', 'path'));
break;
case 'angular-modules.css':
$moduleNames = $page->parseModuleNames(\CRM_Utils_Array::value('modules', $event->params), $angular);
$event->mimeType = 'text/css';
$event->content = \CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n");
default:
// Not our problem.
}
}
/**
* @param array $files
* File paths.
* @return string
*/
public function digestJs($files) {
$scripts = array();
foreach ($files as $file) {
$scripts[] = file_get_contents($file);
}
$scripts = \CRM_Utils_JS::dedupeClosures(
$scripts,
array('angular', '$', '_'),
array('angular', 'CRM.$', 'CRM._')
);
// This impl of stripComments currently adds 10-20ms and cuts ~7%
return \CRM_Utils_JS::stripComments(implode("\n", $scripts));
}
/**
* @param string $modulesExpr
* Comma-separated list of module names.
* @param \Civi\Angular\Manager $angular
* @return array
* Any well-formed module names. All if moduleExpr is blank.
*/
public function parseModuleNames($modulesExpr, $angular) {
if ($modulesExpr) {
$moduleNames = preg_grep(
'/^[a-zA-Z0-9\-_\.]+$/',
explode(',', $modulesExpr)
);
return $moduleNames;
}
else {
$moduleNames = array_keys($angular->getModules());
return $moduleNames;
}
}
/**
* @param array $moduleNames
* List of module names.
* @param \Civi\Angular\Manager $angular
* @return array
*/
public function getMetadata($moduleNames, $angular) {
$modules = $angular->getModules();
$result = array();
foreach ($moduleNames as $moduleName) {
if (isset($modules[$moduleName])) {
$result[$moduleName] = array();
$result[$moduleName]['domain'] = $modules[$moduleName]['ext'];
$result[$moduleName]['js'] = $angular->getResources($moduleName, 'js', 'rawUrl');
$result[$moduleName]['css'] = $angular->getResources($moduleName, 'css', 'rawUrl');
$result[$moduleName]['partials'] = $angular->getPartials($moduleName);
$result[$moduleName]['strings'] = $angular->getTranslatedStrings($moduleName);
}
}
return $result;
}
/**
* Send a response.
*
* @param string $type
* Content type.
* @param string $data
* Content.
*/
public function send($type, $data) {
// Encourage browsers to cache for a long time - 1 year
$ttl = 60 * 60 * 24 * 364;
\CRM_Utils_System::setHttpHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $ttl));
\CRM_Utils_System::setHttpHeader("Content-Type", $type);
\CRM_Utils_System::setHttpHeader("Cache-Control", "max-age=$ttl, public");
echo $data;
}
}

View file

@ -0,0 +1,228 @@
<?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\CCase;
/**
* Class Analyzer
*
* @package Civi\CCase
*/
class Analyzer {
/**
* @var int
*/
private $caseId;
/**
* @var array per APIv3
*/
private $case;
/**
* @var string
*/
private $caseType;
/**
* @var array per APIv3
*/
private $activities;
/**
* @var \SimpleXMLElement
*/
private $xml;
/**
* @var array<string,array>
*/
private $indices;
/**
* @param $caseId
*/
public function __construct($caseId) {
$this->caseId = $caseId;
$this->flush();
}
/**
* Determine if case includes an activity of given type/status
*
* @param string $type
* Eg "Phone Call", "Interview Prospect", "Background Check".
* @param string $status
* Eg "Scheduled", "Completed".
* @return bool
*/
public function hasActivity($type, $status = NULL) {
$idx = $this->getActivityIndex(array('activity_type_id', 'status_id'));
$activityTypeGroup = civicrm_api3('option_group', 'get', array('name' => 'activity_type'));
$activityType = array(
'name' => $type,
'option_group_id' => $activityTypeGroup['id'],
);
$activityTypeID = civicrm_api3('option_value', 'get', $activityType);
$activityTypeID = $activityTypeID['values'][$activityTypeID['id']]['value'];
if ($status) {
$activityStatusGroup = civicrm_api3('option_group', 'get', array('name' => 'activity_status'));
$activityStatus = array(
'name' => $status,
'option_group_id' => $activityStatusGroup['id'],
);
$activityStatusID = civicrm_api3('option_value', 'get', $activityStatus);
$activityStatusID = $activityStatusID['values'][$activityStatusID['id']]['value'];
}
if ($status === NULL) {
return !empty($idx[$activityTypeID]);
}
else {
return !empty($idx[$activityTypeID][$activityStatusID]);
}
}
/**
* Get a list of all activities in the case.
*
* @return array
* list of activity records (api/v3 format)
*/
public function getActivities() {
if ($this->activities === NULL) {
// TODO find batch-oriented API for getting all activities in a case
$case = $this->getCase();
$activities = array();
if (isset($case['activities'])) {
foreach ($case['activities'] as $actId) {
$result = civicrm_api3('Activity', 'get', array(
'id' => $actId,
'is_current_revision' => 1,
));
$activities = array_merge($activities, $result['values']);
}
}
$this->activities = $activities;
}
return $this->activities;
}
/**
* Get a single activity record by type.
*
* @param string $type
* @throws \Civi\CCase\Exception\MultipleActivityException
* @return array|NULL, activity record (api/v3)
*/
public function getSingleActivity($type) {
$idx = $this->getActivityIndex(array('activity_type_id', 'id'));
$actTypes = array_flip(\CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'name'));
$typeId = $actTypes[$type];
$count = isset($idx[$typeId]) ? count($idx[$typeId]) : 0;
if ($count === 0) {
return NULL;
}
elseif ($count === 1) {
foreach ($idx[$typeId] as $item) {
return $item;
}
}
else {
throw new \Civi\CCase\Exception\MultipleActivityException("Wrong quantity of [$type] records. Expected 1 but found " . $count);
}
}
/**
* @return int
*/
public function getCaseId() {
return $this->caseId;
}
/**
* @return array, Case record (api/v3 format)
*/
public function getCase() {
if ($this->case === NULL) {
$this->case = civicrm_api3('case', 'getsingle', array('id' => $this->caseId));
}
return $this->case;
}
/**
* @return string
* @throws \CRM_Core_Exception
*/
public function getCaseType() {
if ($this->caseType === NULL) {
$case = $this->getCase();
$caseTypes = \CRM_Case_XMLRepository::singleton()->getAllCaseTypes();
if (!isset($caseTypes[$case['case_type_id']])) {
throw new \CRM_Core_Exception("Case does not have a recognized case-type!");
}
$this->caseType = $caseTypes[$case['case_type_id']];
}
return $this->caseType;
}
/**
* Get a list of all activities in the case (indexed by some property/properties)
*
* @param array $keys
* List of properties by which to index activities.
* @return array
* list of activity records (api/v3 format), indexed by $keys
*/
public function getActivityIndex($keys) {
$key = implode(";", $keys);
if (!isset($this->indices[$key])) {
$this->indices[$key] = \CRM_Utils_Array::index($keys, $this->getActivities());
}
return $this->indices[$key];
}
/**
* @return \SimpleXMLElement|NULL
*/
public function getXml() {
if ($this->xml === NULL) {
$this->xml = \CRM_Case_XMLRepository::singleton()->retrieve($this->getCaseType());
}
return $this->xml;
}
/**
* Flush any cached information.
*/
public function flush() {
$this->case = NULL;
$this->caseType = NULL;
$this->activities = NULL;
$this->indices = array();
}
}

View file

@ -0,0 +1,42 @@
<?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\CCase;
/**
* Interface CaseChangeListener
*
* @package Civi\CCase
*/
interface CaseChangeListener {
/**
* @param \Civi\CCase\Event\CaseChangeEvent $event
*
* @return mixed
*/
public function onCaseChange(\Civi\CCase\Event\CaseChangeEvent $event);
}

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\CCase\Event;
use Civi\Core\Event\GenericHookEvent;
/**
* Class CaseChangeEvent
* @package Civi\API\Event
*/
class CaseChangeEvent extends GenericHookEvent {
/**
* @var \Civi\CCase\Analyzer
*/
public $analyzer;
/**
* @param $analyzer
*/
public function __construct($analyzer) {
$this->analyzer = $analyzer;
}
/**
* @inheritDoc
*/
public function getHookValues() {
return array($this->analyzer);
}
}

View file

@ -0,0 +1,114 @@
<?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\CCase;
/**
* Class Events
*
* @package Civi\CCase
*/
class Events {
/**
* @var array (int $caseId => bool $active) list of cases for which we are actively firing case-change event
*
* We do not want to fire case-change events recursively.
*/
static $isActive = array();
/**
* Following a change to an activity or case, fire the case-change event.
*
* @param \Civi\Core\Event\PostEvent $event
* @throws \CRM_Core_Exception
*/
public static function fireCaseChange(\Civi\Core\Event\PostEvent $event) {
$caseId = NULL;
switch ($event->entity) {
case 'Activity':
if (!empty($event->object->case_id)) {
$caseId = $event->object->case_id;
}
break;
case 'Case':
// by the time we get the post-delete event, the record is gone, so
// there's nothing to analyze
if ($event->action != 'delete') {
$caseId = $event->id;
}
break;
default:
throw new \CRM_Core_Exception("CRM_Case_Listener does not support entity {$event->entity}");
}
if ($caseId) {
if (!isset(self::$isActive[$caseId])) {
$tx = new \CRM_Core_Transaction();
\CRM_Core_Transaction::addCallback(
\CRM_Core_Transaction::PHASE_POST_COMMIT,
array(__CLASS__, 'fireCaseChangeForRealz'),
array($caseId),
"Civi_CCase_Events::fire::{$caseId}"
);
}
}
}
/**
* Fire case change hook
*
* @param int|array $caseIds
*/
public static function fireCaseChangeForRealz($caseIds) {
foreach ((array) $caseIds as $caseId) {
if (!isset(self::$isActive[$caseId])) {
$tx = new \CRM_Core_Transaction();
self::$isActive[$caseId] = 1;
$analyzer = new \Civi\CCase\Analyzer($caseId);
\CRM_Utils_Hook::caseChange($analyzer);
unset(self::$isActive[$caseId]);
unset($tx);
}
}
}
/**
* Find any extra listeners declared in XML and pass the event along to them.
*
* @param \Civi\CCase\Event\CaseChangeEvent $event
*/
public static function delegateToXmlListeners(\Civi\CCase\Event\CaseChangeEvent $event) {
$p = new \CRM_Case_XMLProcessor_Process();
$listeners = $p->getListeners($event->analyzer->getCaseType());
foreach ($listeners as $listener) {
/** @var $listener \Civi\CCase\CaseChangeListener */
$listener->onCaseChange($event);
}
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace Civi\CCase\Exception;
/**
* Class MultipleActivityException
*
* @package Civi\CCase\Exception
*/
class MultipleActivityException extends \CRM_Core_Exception {
}

View file

@ -0,0 +1,123 @@
<?php
namespace Civi\CCase;
/**
* The sequence-listener looks for CiviCase XML tags with "<sequence>". If
* a change is made to any record in case-type which uses "<sequence>", then
* it attempts to add the next step in the sequence.
*/
class SequenceListener implements CaseChangeListener {
/**
* @var SequenceListener
*/
private static $singleton;
/**
* @param bool $reset
* Whether to forcibly rebuild the entire container.
* @return SequenceListener
*/
public static function singleton($reset = FALSE) {
if ($reset || self::$singleton === NULL) {
self::$singleton = new SequenceListener();
}
return self::$singleton;
}
/**
* @param \Civi\CCase\Event\CaseChangeEvent $event
*/
public static function onCaseChange_static(\Civi\CCase\Event\CaseChangeEvent $event) {
self::singleton()->onCaseChange($event);
}
/**
* @param \Civi\CCase\Event\CaseChangeEvent $event
*
* @throws \CiviCRM_API3_Exception
* @return void
*/
public function onCaseChange(\Civi\CCase\Event\CaseChangeEvent $event) {
/** @var \Civi\CCase\Analyzer $analyzer */
$analyzer = $event->analyzer;
$activitySetXML = $this->getSequenceXml($analyzer->getXml());
if (!$activitySetXML) {
return;
}
$actTypes = array_flip(\CRM_Core_PseudoConstant::activityType(TRUE, TRUE, FALSE, 'name'));
$actStatuses = array_flip(\CRM_Core_PseudoConstant::activityStatus('name'));
$actIndex = $analyzer->getActivityIndex(array('activity_type_id', 'status_id'));
foreach ($activitySetXML->ActivityTypes->ActivityType as $actTypeXML) {
$actTypeId = $actTypes[(string) $actTypeXML->name];
if (empty($actIndex[$actTypeId])) {
// Haven't tried this step yet!
$this->createActivity($analyzer, $actTypeXML);
return;
}
elseif (empty($actIndex[$actTypeId][$actStatuses['Completed']])) {
// Haven't gotten past this step yet!
return;
}
}
//CRM-17452 - Close the case only if all the activities are complete
$activities = $analyzer->getActivities();
foreach ($activities as $activity) {
if ($activity['status_id'] != $actStatuses['Completed']) {
return;
}
}
// OK, the all activities have completed
civicrm_api3('Case', 'create', array(
'id' => $analyzer->getCaseId(),
'status_id' => 'Closed',
));
$analyzer->flush();
}
/**
* Find the ActivitySet which defines the pipeline.
*
* @param \SimpleXMLElement $xml
* @return \SimpleXMLElement|NULL
*/
public function getSequenceXml($xml) {
if ($xml->ActivitySets && $xml->ActivitySets->ActivitySet) {
foreach ($xml->ActivitySets->ActivitySet as $activitySetXML) {
$seq = (string) $activitySetXML->sequence;
if ($seq && strtolower($seq) == 'true') {
if ($activitySetXML->ActivityTypes && $activitySetXML->ActivityTypes->ActivityType) {
return $activitySetXML;
}
else {
return NULL;
}
}
}
}
return NULL;
}
/**
* @param Analyzer $analyzer
* The case being analyzed -- to which we want to add an activity.
* @param \SimpleXMLElement $actXML the <ActivityType> tag which describes the new activity
*/
public function createActivity(Analyzer $analyzer, \SimpleXMLElement $actXML) {
$params = array(
'activity_type_id' => (string) $actXML->name,
'status_id' => 'Scheduled',
'activity_date_time' => \CRM_Utils_Time::getTime('YmdHis'),
'case_id' => $analyzer->getCaseId(),
);
$r = civicrm_api3('Activity', 'create', $params);
$analyzer->flush();
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Civi\CiUtil;
/**
* Class Arrays
*
* @package Civi\CiUtil
*/
class Arrays {
/**
* @param $arr
* @param $col
*
* @return array
*/
public static function collect($arr, $col) {
$r = array();
foreach ($arr as $k => $item) {
$r[$k] = $item[$col];
}
return $r;
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Civi\CiUtil;
/**
* Parse phpunit result files
*/
class CSVParser {
/**
* @param string $csvContent
* Content; each row in the row csv should start with two cells:.
* - cell 0: the test name
* - cell 1: the test status
* @return array
* (string $testName => string $status)
*/
public static function parseResults($csvContent) {
$fh = fopen('php://memory', 'r+');
fwrite($fh, $csvContent);
rewind($fh);
$results = array();
while (($r = fgetcsv($fh)) !== FALSE) {
$name = str_replace('.', '::', trim($r[0]));
$status = trim($r[1]);
$results[$name] = $status;
}
return $results;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Civi\CiUtil\Command;
/**
* Class AntagonistCommand
*
* @package Civi\CiUtil\Command
*/
class AntagonistCommand {
/**
* @param $argv
*/
public static function main($argv) {
if (count($argv) != 3) {
print "usage: {$argv[0]} <TargetTest::testFunc> </path/to/suite>\n";
exit(1);
}
list ($program, $target, $suite) = $argv;
$candidateTests = \Civi\CiUtil\PHPUnitScanner::findTestsByPath(array($suite));
// $candidateTests = array(
// array('class' => 'CRM_Core_RegionTest', 'method' => 'testBlank'),
// array('class' => 'CRM_Core_RegionTest', 'method' => 'testDefault'),
// array('class' => 'CRM_Core_RegionTest', 'method' => 'testOverride'),
// array('class' => 'CRM_Core_RegionTest', 'method' => 'testAllTypes'),
// );
$antagonist = self::findAntagonist($target, $candidateTests);
if ($antagonist) {
print_r(array('found an antagonist' => $antagonist));
}
else {
print_r(array('found no antagonists'));
}
}
/**
* @param string $target
* E.g. "MyTest::testFoo".
* @param array $candidateTests
* List of strings (e.g. "MyTest::testFoo").
* @return array|null
* array contains keys:
* - antagonist: array
* - file: string
* - class: string
* - method: string
* - expectedResults: array
* - actualResults: array
*/
public static function findAntagonist($target, $candidateTests) {
//$phpUnit = new \Civi\CiUtil\EnvTestRunner('./scripts/phpunit', 'EnvTests');
$phpUnit = new \Civi\CiUtil\EnvTestRunner('phpunit', 'tests/phpunit/EnvTests.php');
$expectedResults = $phpUnit->run(array($target));
print_r(array('$expectedResults' => $expectedResults));
foreach ($candidateTests as $candidateTest) {
$candidateTestName = $candidateTest['class'] . '::' . $candidateTest['method'];
if ($candidateTestName == $target) {
continue;
}
$actualResults = $phpUnit->run(array(
$candidateTestName,
$target,
));
print_r(array('$actualResults' => $actualResults));
foreach ($expectedResults as $testName => $expectedResult) {
if ($actualResults[$testName] != $expectedResult) {
return array(
'antagonist' => $candidateTest,
'expectedResults' => $expectedResults,
'actualResults' => $actualResults,
);
}
}
}
return NULL;
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Civi\CiUtil\Command;
/**
* Class CompareCommand
*
* @package Civi\CiUtil\Command
*/
class CompareCommand {
/**
* @param $argv
*/
public static function main($argv) {
if (empty($argv[1])) {
echo "summary: Compares the output of different test runs\n";
echo "usage: phpunit-compare [--out=txt|csv] [--phpunit-json|--jenkins-xml] <file1> <file2>...\n";
exit(1);
}
$parser = array('\Civi\CiUtil\PHPUnitParser', 'parseJsonResults');
$printerType = 'txt';
$suites = array(); // array('file' => string, 'results' => array)
for ($i = 1; $i < count($argv); $i++) {
switch ($argv[$i]) {
case '--phpunit-json':
$parser = array('\Civi\CiUtil\PHPUnitParser', 'parseJsonResults');
break;
case '--jenkins-xml':
$parser = array('\Civi\CiUtil\JenkinsParser', 'parseXmlResults');
break;
case '--csv':
$parser = array('\Civi\CiUtil\CSVParser', 'parseResults');
break;
case '--out=txt':
$printerType = 'txt';
break;
case '--out=csv':
$printerType = 'csv';
break;
default:
$suites[] = array(
'file' => $argv[$i],
'results' => call_user_func($parser, file_get_contents($argv[$i])),
);
}
}
$tests = array(); // array(string $name)
foreach ($suites as $suite) {
$tests = array_unique(array_merge(
$tests,
array_keys($suite['results'])
));
}
sort($tests);
if ($printerType == 'csv') {
$printer = new \Civi\CiUtil\CsvPrinter('php://stdout', \Civi\CiUtil\Arrays::collect($suites, 'file'));
}
else {
$printer = new \Civi\CiUtil\ComparisonPrinter(\Civi\CiUtil\Arrays::collect($suites, 'file'));
}
foreach ($tests as $test) {
$values = array();
foreach ($suites as $suite) {
$values[] = isset($suite['results'][$test]) ? $suite['results'][$test] : 'MISSING';
}
if (count(array_unique($values)) > 1) {
$printer->printRow($test, $values);
}
}
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Civi\CiUtil\Command;
/**
* Class LsCommand
*
* @package Civi\CiUtil\Command
*/
class LsCommand {
/**
* @param $argv
*/
public static function main($argv) {
$paths = $argv;
array_shift($paths);
foreach (\Civi\CiUtil\PHPUnitScanner::findTestsByPath($paths) as $test) {
printf("%s %s %s\n", $test['file'], $test['class'], $test['method']);
}
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Civi\CiUtil;
/**
* Class ComparisonPrinter
*
* @package Civi\CiUtil
*/
class ComparisonPrinter {
var $headers;
var $hasHeader = FALSE;
/**
* @param $headers
*/
public function __construct($headers) {
$this->headers = $headers;
}
public function printHeader() {
if ($this->hasHeader) {
return;
}
## LEGEND
print "LEGEND\n";
$i = 1;
foreach ($this->headers as $header) {
printf("% 2d: %s\n", $i, $header);
$i++;
}
print "\n";
## HEADER
printf("%-90s ", 'TEST NAME');
$i = 1;
foreach ($this->headers as $header) {
printf("%-10d ", $i);
$i++;
}
print "\n";
$this->hasHeader = TRUE;
}
/**
* @param $test
* @param $values
*/
public function printRow($test, $values) {
$this->printHeader();
printf("%-90s ", $test);
foreach ($values as $value) {
printf("%-10s ", $value);
}
print "\n";
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Civi\CiUtil;
/**
* Class CsvPrinter
*
* @package Civi\CiUtil
*/
class CsvPrinter {
var $file;
var $headers;
var $hasHeader = FALSE;
/**
* @param $file
* @param $headers
*/
public function __construct($file, $headers) {
$this->file = fopen($file, "w");
$this->headers = $headers;
}
public function printHeader() {
if ($this->hasHeader) {
return;
}
$headers = array_values($this->headers);
array_unshift($headers, 'TEST NAME');
fputcsv($this->file, $headers);
$this->hasHeader = TRUE;
}
/**
* @param $test
* @param $values
*/
public function printRow($test, $values) {
$this->printHeader();
$row = $values;
array_unshift($row, $test);
fputcsv($this->file, $row);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Civi\CiUtil;
/**
* Parse phpunit result files
*/
class EnvTestRunner {
protected $phpunit;
protected $envTestSuite;
/**
* @param string $phpunit
* @param string $envTestSuite
*/
public function __construct($phpunit = "phpunit", $envTestSuite = 'EnvTests') {
$this->phpunit = $phpunit;
$this->envTestSuite = $envTestSuite;
}
/**
* @param array $tests
* @return array
* (string $testName => string $status)
*/
public function run($tests) {
$envTests = implode(' ', $tests);
$jsonFile = tempnam(sys_get_temp_dir(), 'phpunit-json-');
unlink($jsonFile);
$command = "env PHPUNIT_TESTS=\"$envTests\" {$this->phpunit} --log-json $jsonFile {$this->envTestSuite}";
echo "Running [$command]\n";
system($command);
$results = PHPUnitParser::parseJsonResults(file_get_contents($jsonFile));
unlink($jsonFile);
return $results;
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Civi\CiUtil;
/**
* Parse Jenkins result files
*/
class JenkinsParser {
/**
* @param string $content
* Xml data.
* @return array
* (string $testName => string $status)
*/
public static function parseXmlResults($content) {
$xml = simplexml_load_string($content);
$results = array();
foreach ($xml->suites as $suites) {
foreach ($suites->suite as $suite) {
foreach ($suite->cases as $cases) {
foreach ($cases->case as $case) {
$name = "{$case->className}::{$case->testName}";
if ($case->failedSince == 0) {
$results[$name] = 'pass';
}
else {
$results[$name] = 'fail';
}
}
}
}
}
return $results;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Civi\CiUtil;
/**
* Parse phpunit result files
*/
class PHPUnitParser {
/**
* @param string $content
* Phpunit streaming JSON.
* @return array
* ["$class::$func" => $status]
*/
protected static function parseJsonStream($content) {
$content = '['
. strtr($content, array("}{" => "},{"))
. ']';
return json_decode($content, TRUE);
}
/**
* @param string $content
* Json stream.
* @return array
* (string $testName => string $status)
*/
public static function parseJsonResults($content) {
$records = self::parseJsonStream($content);
$results = array();
foreach ($records as $r) {
if ($r['event'] == 'test') {
$results[$r['test']] = $r['status'];
}
}
return $results;
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Civi\CiUtil;
use Symfony\Component\Finder\Finder;
/**
* Search for PHPUnit test cases
*/
class PHPUnitScanner {
/**
* @param $path
* @return array <string> class names
*/
public static function _findTestClasses($path) {
// print_r(array(
// 'loading' => $path,
// get_included_files()
// ));
$origClasses = get_declared_classes();
require_once $path;
$newClasses = get_declared_classes();
return preg_grep('/Test$/', array_diff(
$newClasses,
$origClasses
));
}
/**
* @param $paths
* @return array (string $file => string $class)
* @throws \Exception
*/
public static function findTestClasses($paths) {
$testClasses = array();
$finder = new Finder();
foreach ($paths as $path) {
if (is_dir($path)) {
foreach ($finder->files()->in($paths)->name('*Test.php') as $file) {
$testClass = self::_findTestClasses((string) $file);
if (count($testClass) == 1) {
$testClasses[(string) $file] = array_shift($testClass);
}
elseif (count($testClass) > 1) {
throw new \Exception("Too many classes in $file");
}
else {
throw new \Exception("Too few classes in $file");
}
}
}
elseif (is_file($path)) {
$testClass = self::_findTestClasses($path);
if (count($testClass) == 1) {
$testClasses[$path] = array_shift($testClass);
}
elseif (count($testClass) > 1) {
throw new \Exception("Too many classes in $path");
}
else {
throw new \Exception("Too few classes in $path");
}
}
}
return $testClasses;
}
/**
* @param array $paths
*
* @return array
* each element is an array with keys:
* - file: string
* - class: string
* - method: string
*/
public static function findTestsByPath($paths) {
$r = array();
$testClasses = self::findTestClasses($paths);
foreach ($testClasses as $testFile => $testClass) {
$clazz = new \ReflectionClass($testClass);
foreach ($clazz->getMethods() as $method) {
if (preg_match('/^test/', $method->name)) {
$r[] = array(
'file' => $testFile,
'class' => $testClass,
'method' => $method->name,
);
}
}
}
return $r;
}
}

View file

@ -0,0 +1,383 @@
<?php
namespace Civi\Core;
use Civi\Core\Exception\UnknownAssetException;
/**
* Class AssetBuilder
* @package Civi\Core
*
* The AssetBuilder is used to manage semi-dynamic assets.
* In normal production use, these assets are built on first
* reference and then stored in a public-facing cache folder.
* (In debug mode, these assets are constructed during every request.)
*
* There are generally two aspects to usage -- creating a URL
* for the asset, and defining the content of the asset.
*
* For example, suppose we wanted to define a static file
* named "api-fields.json" which lists all the fields of
* all the API entities.
*
* @code
* // Build a URL to `api-fields.json`.
* $url = \Civi::service('asset_builder')->getUrl('api-fields.json');
*
* // Define the content of `api-fields.json`.
* function hook_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
* if ($asset !== 'api-fields.json') return;
*
* $entities = civicrm_api3('Entity', 'get', array());
* $fields = array();
* foreach ($entities['values'] as $entity) {
* $fields[$entity] = civicrm_api3($entity, 'getfields');
* }
*
* $mimeType = 'application/json';
* $content = json_encode($fields);
* }
* @endCode
*
* Assets can be parameterized. Each combination of ($asset,$params)
* will be cached separately. For example, we might want a copy of
* 'api-fields.json' which only includes a handful of chosen entities.
* Simply pass the chosen entities into `getUrl()`, then update
* the definition to use `$params['entities']`, as in:
*
* @code
* // Build a URL to `api-fields.json`.
* $url = \Civi::service('asset_builder')->getUrl('api-fields.json', array(
* 'entities' => array('Contact', 'Phone', 'Email', 'Address'),
* ));
*
* // Define the content of `api-fields.json`.
* function hook_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
* if ($asset !== 'api-fields.json') return;
*
* $fields = array();
* foreach ($params['entities'] as $entity) {
* $fields[$entity] = civicrm_api3($entity, 'getfields');
* }
*
* $mimeType = 'application/json';
* $content = json_encode($fields);
* }
* @endCode
*
* Note: These assets are designed to hold non-sensitive data, such as
* aggregated JS or common metadata. There probably are ways to
* secure it (e.g. alternative digest() calculations), but the
* current implementation is KISS.
*/
class AssetBuilder {
/**
* @return array
* Array(string $value => string $label).
*/
public static function getCacheModes() {
return array(
'0' => ts('Disable'),
'1' => ts('Enable'),
'auto' => ts('Auto'),
);
}
protected $cacheEnabled;
/**
* AssetBuilder constructor.
* @param $cacheEnabled
*/
public function __construct($cacheEnabled = NULL) {
if ($cacheEnabled === NULL) {
$cacheEnabled = \Civi::settings()->get('assetCache');
if ($cacheEnabled === 'auto') {
$cacheEnabled = !\CRM_Core_Config::singleton()->debug;
}
$cacheEnabled = (bool) $cacheEnabled;
}
$this->cacheEnabled = $cacheEnabled;
}
/**
* Determine if $name is a well-formed asset name.
*
* @param string $name
* @return bool
*/
public function isValidName($name) {
return preg_match(';^[a-zA-Z0-9\.\-_/]+$;', $name)
&& strpos($name, '..') === FALSE
&& strpos($name, '.') !== FALSE;
}
/**
* @param string $name
* Ex: 'angular.json'.
* @param array $params
* @return string
* URL.
* Ex: 'http://example.org/files/civicrm/dyn/angular.abcd1234abcd1234.json'.
*/
public function getUrl($name, $params = array()) {
if (!$this->isValidName($name)) {
throw new \RuntimeException("Invalid dynamic asset name");
}
if ($this->isCacheEnabled()) {
$fileName = $this->build($name, $params);
return $this->getCacheUrl($fileName);
}
else {
return \CRM_Utils_System::url('civicrm/asset/builder', array(
'an' => $name,
'ap' => $this->encode($params),
'ad' => $this->digest($name, $params),
), TRUE, NULL, FALSE);
}
}
/**
* @param string $name
* Ex: 'angular.json'.
* @param array $params
* @return string
* URL.
* Ex: '/var/www/files/civicrm/dyn/angular.abcd1234abcd1234.json'.
*/
public function getPath($name, $params = array()) {
if (!$this->isValidName($name)) {
throw new \RuntimeException("Invalid dynamic asset name");
}
$fileName = $this->build($name, $params);
return $this->getCachePath($fileName);
}
/**
* Build the cached copy of an $asset.
*
* @param string $name
* Ex: 'angular.json'.
* @param array $params
* @param bool $force
* Build the asset anew, even if it already exists.
* @return string
* File name (relative to cache folder).
* Ex: 'angular.abcd1234abcd1234.json'.
* @throws UnknownAssetException
*/
public function build($name, $params, $force = FALSE) {
if (!$this->isValidName($name)) {
throw new UnknownAssetException("Asset name is malformed");
}
$nameParts = explode('.', $name);
array_splice($nameParts, -1, 0, array($this->digest($name, $params)));
$fileName = implode('.', $nameParts);
if ($force || !file_exists($this->getCachePath($fileName))) {
// No file locking, but concurrent writers should produce
// the same data, so we'll just plow ahead.
if (!file_exists($this->getCachePath())) {
mkdir($this->getCachePath());
}
$rendered = $this->render($name, $params);
file_put_contents($this->getCachePath($fileName), $rendered['content']);
return $fileName;
}
return $fileName;
}
/**
* Generate the content for a dynamic asset.
*
* @param string $name
* @param array $params
* @return array
* Array with keys:
* - statusCode: int, ex: 200.
* - mimeType: string, ex: 'text/html'.
* - content: string, ex: '<body>Hello world</body>'.
* @throws \CRM_Core_Exception
*/
public function render($name, $params = array()) {
if (!$this->isValidName($name)) {
throw new UnknownAssetException("Asset name is malformed");
}
\CRM_Utils_Hook::buildAsset($name, $params, $mimeType, $content);
if ($mimeType === NULL && $content === NULL) {
throw new UnknownAssetException("Unrecognized asset name: $name");
}
// Beg your pardon, sir. Please may I have an HTTP response class instead?
return array(
'statusCode' => 200,
'mimeType' => $mimeType,
'content' => $content,
);
}
/**
* Clear out any cache files.
*/
public function clear() {
\CRM_Utils_File::cleanDir($this->getCachePath());
}
/**
* Determine the local path of a cache file.
*
* @param string|NULL $fileName
* Ex: 'angular.abcd1234abcd1234.json'.
* @return string
* URL.
* Ex: '/var/www/files/civicrm/dyn/angular.abcd1234abcd1234.json'.
*/
protected function getCachePath($fileName = NULL) {
// imageUploadDir has the correct functional properties but a wonky name.
$suffix = ($fileName === NULL) ? '' : (DIRECTORY_SEPARATOR . $fileName);
return
\CRM_Utils_File::addTrailingSlash(\CRM_Core_Config::singleton()->imageUploadDir)
. 'dyn' . $suffix;
}
/**
* Determine the URL of a cache file.
*
* @param string|NULL $fileName
* Ex: 'angular.abcd1234abcd1234.json'.
* @return string
* URL.
* Ex: 'http://example.org/files/civicrm/dyn/angular.abcd1234abcd1234.json'.
*/
protected function getCacheUrl($fileName = NULL) {
// imageUploadURL has the correct functional properties but a wonky name.
$suffix = ($fileName === NULL) ? '' : ('/' . $fileName);
return
\CRM_Utils_File::addTrailingSlash(\CRM_Core_Config::singleton()->imageUploadURL, '/')
. 'dyn' . $suffix;
}
/**
* Create a unique identifier for the $params.
*
* This identifier is designed to avoid accidental cache collisions.
*
* @param string $name
* @param array $params
* @return string
*/
protected function digest($name, $params) {
// WISHLIST: For secure digest, generate+persist privatekey & call hash_hmac.
ksort($params);
$digest = md5(
$name .
\CRM_Core_Resources::singleton()->getCacheCode() .
\CRM_Core_Config_Runtime::getId() .
json_encode($params)
);
return $digest;
}
/**
* Encode $params in a format that's optimized for shorter URLs.
*
* @param array $params
* @return string
*/
protected function encode($params) {
if (empty($params)) {
return '';
}
$str = json_encode($params);
if (function_exists('gzdeflate')) {
$str = gzdeflate($str);
}
return base64_encode($str);
}
/**
* @param string $str
* @return array
*/
protected function decode($str) {
if ($str === NULL || $str === FALSE || $str === '') {
return array();
}
$str = base64_decode($str);
if (function_exists('gzdeflate')) {
$str = gzinflate($str);
}
return json_decode($str, TRUE);
}
/**
* @return bool
*/
public function isCacheEnabled() {
return $this->cacheEnabled;
}
/**
* @param bool|null $cacheEnabled
* @return AssetBuilder
*/
public function setCacheEnabled($cacheEnabled) {
$this->cacheEnabled = $cacheEnabled;
return $this;
}
/**
* (INTERNAL ONLY)
*
* Execute a page-request for `civicrm/asset/builder`.
*/
public static function pageRun() {
// Beg your pardon, sir. Please may I have an HTTP response class instead?
$asset = self::pageRender($_GET);
if (function_exists('http_response_code')) {
// PHP 5.4+
http_response_code($asset['statusCode']);
}
else {
header('X-PHP-Response-Code: ' . $asset['statusCode'], TRUE, $asset['statusCode']);
}
header('Content-Type: ' . $asset['mimeType']);
echo $asset['content'];
\CRM_Utils_System::civiExit();
}
/**
* (INTERNAL ONLY)
*
* Execute a page-request for `civicrm/asset/builder`.
*
* @param array $get
* The _GET values.
* @return array
* Array with keys:
* - statusCode: int, ex 200.
* - mimeType: string, ex 'text/html'.
* - content: string, ex '<body>Hello world</body>'.
*/
public static function pageRender($get) {
// Beg your pardon, sir. Please may I have an HTTP response class instead?
try {
$assets = \Civi::service('asset_builder');
return $assets->render($get['an'], $assets->decode($get['ap']));
}
catch (UnknownAssetException $e) {
return array(
'statusCode' => 404,
'mimeType' => 'text/plain',
'content' => $e->getMessage(),
);
}
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace Civi\Core;
use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Symfony\Component\EventDispatcher\Event;
/**
* Class CiviEventDispatcher
* @package Civi\Core
*
* The CiviEventDispatcher is a Symfony dispatcher. Additionally, if an event
* follows the naming convention of "hook_*", then it will also be dispatched
* through CRM_Utils_Hook::invoke().
*
* @see \CRM_Utils_Hook
*/
class CiviEventDispatcher extends ContainerAwareEventDispatcher {
const DEFAULT_HOOK_PRIORITY = -100;
/**
* Track the list of hook-events for which we have autoregistered
* the hook adapter.
*
* @var array
* Array(string $eventName => trueish).
*/
private $autoListeners = array();
/**
* Determine whether $eventName should delegate to the CMS hook system.
*
* @param string $eventName
* Ex: 'civi.token.eval', 'hook_civicrm_post`.
* @return bool
*/
protected function isHookEvent($eventName) {
return (substr($eventName, 0, 5) === 'hook_') && (strpos($eventName, '::') === FALSE);
}
/**
* @inheritDoc
*/
public function dispatch($eventName, Event $event = NULL) {
$this->bindPatterns($eventName);
return parent::dispatch($eventName, $event);
}
/**
* @inheritDoc
*/
public function getListeners($eventName = NULL) {
$this->bindPatterns($eventName);
return parent::getListeners($eventName);
}
/**
* @inheritDoc
*/
public function hasListeners($eventName = NULL) {
// All hook_* events have default listeners, so hasListeners(NULL) is a truism.
return ($eventName === NULL || $this->isHookEvent($eventName))
? TRUE : parent::hasListeners($eventName);
}
/**
* Invoke hooks using an event object.
*
* @param \Civi\Core\Event\GenericHookEvent $event
* @param string $eventName
* Ex: 'hook_civicrm_dashboard'.
*/
public static function delegateToUF($event, $eventName) {
$hookName = substr($eventName, 5);
$hooks = \CRM_Utils_Hook::singleton();
$params = $event->getHookValues();
$count = count($params);
switch ($count) {
case 0:
$fResult = $hooks->invokeViaUF($count, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
break;
case 1:
$fResult = $hooks->invokeViaUF($count, $params[0], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
break;
case 2:
$fResult = $hooks->invokeViaUF($count, $params[0], $params[1], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
break;
case 3:
$fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
break;
case 4:
$fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], \CRM_Utils_Hook::$_nullObject, \CRM_Utils_Hook::$_nullObject, $hookName);
break;
case 5:
$fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], \CRM_Utils_Hook::$_nullObject, $hookName);
break;
case 6:
$fResult = $hooks->invokeViaUF($count, $params[0], $params[1], $params[2], $params[3], $params[4], $params[5], $hookName);
break;
default:
throw new \RuntimeException("hook_{$hookName} cannot support more than 6 parameters");
}
$event->addReturnValues($fResult);
}
/**
* @param string $eventName
* Ex: 'civi.api.resolve' or 'hook_civicrm_dashboard'.
*/
protected function bindPatterns($eventName) {
if ($eventName !== NULL && !isset($this->autoListeners[$eventName])) {
$this->autoListeners[$eventName] = 1;
if ($this->isHookEvent($eventName)) {
// WISHLIST: For native extensions (and possibly D6/D7/D8/BD), enumerate
// the listeners and list them one-by-one. This would make it easier to
// inspect via "cv debug:event-dispatcher".
$this->addListener($eventName, array(
'\Civi\Core\CiviEventDispatcher',
'delegateToUF',
), self::DEFAULT_HOOK_PRIORITY);
}
}
}
}

View file

@ -0,0 +1,227 @@
<?php
namespace Civi\Core;
/**
* Class CiviEventInspector
*
* The event inspector is a development tool which provides metadata about events.
* It can be used for code-generators and documentation-generators.
*
* @code
* $i = new CiviEventInspector();
* print_r(CRM_Utils_Array::collect('name', $i->getAll()));
* @endCode
*
* An event definition includes these fields:
* - type: string, required. Ex: 'hook' or 'object'
* - name: string, required. Ex: 'hook_civicrm_post' or 'civi.dao.postInsert'
* - class: string, required. Ex: 'Civi\Core\Event\GenericHookEvent'.
* - signature: string, required FOR HOOKS. Ex: '$first, &$second'.
* - fields: array, required FOR HOOKS. List of hook parameters.
* - stub: ReflectionMethod, optional. An example function with docblocks/inputs.
*
* Note: The inspector is only designed for use in developer workflows, such
* as code-generation and inspection. It should be not called by regular
* runtime logic.
*/
class CiviEventInspector {
/**
* Register the default hooks defined by 'CRM_Utils_Hook'.
*
* @param \Civi\Core\Event\GenericHookEvent $e
* @see \CRM_Utils_Hook::eventDefs()
*/
public static function findBuiltInEvents(\Civi\Core\Event\GenericHookEvent $e) {
$skipList = array('singleton');
$e->inspector->addStaticStubs('CRM_Utils_Hook', 'hook_civicrm_',
function ($eventDef, $method) use ($skipList) {
return in_array($method->name, $skipList) ? NULL : $eventDef;
});
}
/**
* @var array
* Array(string $name => array $eventDef).
*
* Ex: $eventDefs['hook_civicrm_foo']['description_html'] = 'Hello world';
*/
protected $eventDefs;
/**
* Perform a scan to identify/describe all events.
*
* @param bool $force
* @return CiviEventInspector
*/
public function build($force = FALSE) {
if ($force || $this->eventDefs === NULL) {
$this->eventDefs = array();
\CRM_Utils_Hook::eventDefs($this);
ksort($this->eventDefs);
}
return $this;
}
/**
* Get a list of all events.
*
* @return array
* Array(string $name => array $eventDef).
* Ex: $result['hook_civicrm_foo']['description_html'] = 'Hello world';
*/
public function getAll() {
$this->build();
return $this->eventDefs;
}
/**
* Find any events that match a pattern.
*
* @param string $regex
* @return array
* Array(string $name => array $eventDef).
* Ex: $result['hook_civicrm_foo']['description_html'] = 'Hello world';
*/
public function find($regex) {
$this->build();
return array_filter($this->eventDefs, function ($e) use ($regex) {
return preg_match($regex, $e['name']);
});
}
/**
* Get the definition of one event.
*
* @param string $name
* Ex: 'hook_civicrm_alterSettingsMetaData'.
* @return array
* Ex: $result['description_html'] = 'Hello world';
*/
public function get($name) {
$this->build();
return $this->eventDefs[$name];
}
/**
* @param $eventDef
* @return bool
* TRUE if valid.
*/
public function validate($eventDef) {
if (!is_array($eventDef) || empty($eventDef['name']) || !isset($eventDef['type'])) {
return FALSE;
}
if (!in_array($eventDef['type'], array('hook', 'object'))) {
return FALSE;
}
if ($eventDef['type'] === 'hook') {
if (!isset($eventDef['signature']) || !is_array($eventDef['fields'])) {
return FALSE;
}
}
return TRUE;
}
/**
* Add a new event definition.
*
* @param array $eventDef
* @return CiviEventInspector
*/
public function add($eventDef) {
$name = isset($eventDef['name']) ? $eventDef['name'] : NULL;
if (!isset($eventDef['type'])) {
$eventDef['type'] = preg_match('/^hook_/', $eventDef['name']) ? 'hook' : 'object';
}
if ($eventDef['type'] === 'hook' && empty($eventDef['signature'])) {
$eventDef['signature'] = implode(', ', array_map(
function ($field) {
$sigil = $field['ref'] ? '&$' : '$';
return $sigil . $field['name'];
},
$eventDef['fields']
));
}
if (TRUE !== $this->validate($eventDef)) {
throw new \CRM_Core_Exception("Failed to register event ($name). Invalid definition.");
}
$this->eventDefs[$name] = $eventDef;
return $this;
}
/**
* Scan a Symfony event class for metadata, and add it.
*
* @param string $event
* Ex: 'civi.api.authorize'.
* @param string $className
* Ex: 'Civi\API\Event\AuthorizeEvent'.
* @return CiviEventInspector
*/
public function addEventClass($event, $className) {
$this->add(array(
'name' => $event,
'class' => $className,
));
return $this;
}
/**
* Scan a class for hook stubs, and add all of them.
*
* @param string $className
* The name of a class which contains static stub functions.
* Ex: 'CRM_Utils_Hook'.
* @param string $prefix
* A prefix to apply to all hook names.
* Ex: 'hook_civicrm_'.
* @param null|callable $filter
* An optional function to filter/rewrite the metadata for each hook.
* @return CiviEventInspector
*/
public function addStaticStubs($className, $prefix, $filter = NULL) {
$class = new \ReflectionClass($className);
foreach ($class->getMethods(\ReflectionMethod::IS_STATIC) as $method) {
if (!isset($method->name)) {
continue;
}
$eventDef = array(
'name' => $prefix . $method->name,
'description_html' => $method->getDocComment() ? \CRM_Admin_Page_APIExplorer::formatDocBlock($method->getDocComment()) : '',
'fields' => array(),
'class' => 'Civi\Core\Event\GenericHookEvent',
'stub' => $method,
);
foreach ($method->getParameters() as $parameter) {
$eventDef['fields'][$parameter->getName()] = array(
'name' => $parameter->getName(),
'ref' => (bool) $parameter->isPassedByReference(),
// WISHLIST: 'type' => 'mixed',
);
}
if ($filter !== NULL) {
$eventDef = $filter($eventDef, $method);
if ($eventDef === NULL) {
continue;
}
}
$this->add($eventDef);
}
return $this;
}
}

View file

@ -0,0 +1,460 @@
<?php
namespace Civi\Core;
use Civi\API\Provider\ActionObjectProvider;
use Civi\Core\Event\SystemInstallEvent;
use Civi\Core\Lock\LockManager;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\FileCacheReader;
use Doctrine\Common\Cache\FilesystemCache;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Tools\Setup;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
// TODO use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
/**
* Class Container
* @package Civi\Core
*/
class Container {
const SELF = 'civi_container_factory';
/**
* @param bool $reset
* Whether to forcibly rebuild the entire container.
* @return \Symfony\Component\DependencyInjection\TaggedContainerInterface
*/
public static function singleton($reset = FALSE) {
if ($reset || !isset(\Civi::$statics[__CLASS__]['container'])) {
self::boot(TRUE);
}
return \Civi::$statics[__CLASS__]['container'];
}
/**
* Find a cached container definition or construct a new one.
*
* There are many weird contexts in which Civi initializes (eg different
* variations of multitenancy and different permutations of CMS/CRM bootstrap),
* and hook_container may fire a bit differently in each context. To mitigate
* risk of leaks between environments, we compute a unique envID
* (md5(DB_NAME, HTTP_HOST, SCRIPT_FILENAME, etc)) and use separate caches for
* each (eg "templates_c/CachedCiviContainer.$ENVID.php").
*
* Constants:
* - CIVICRM_CONTAINER_CACHE -- 'always' [default], 'never', 'auto'
* - CIVICRM_DSN
* - CIVICRM_DOMAIN_ID
* - CIVICRM_TEMPLATE_COMPILEDIR
*
* @return ContainerInterface
*/
public function loadContainer() {
// Note: The container's raison d'etre is to manage construction of other
// services. Consequently, we assume a minimal service available -- the classloader
// has been setup, and civicrm.settings.php is loaded, but nothing else works.
$cacheMode = defined('CIVICRM_CONTAINER_CACHE') ? CIVICRM_CONTAINER_CACHE : 'always';
// In pre-installation environments, don't bother with caching.
if (!defined('CIVICRM_TEMPLATE_COMPILEDIR') || !defined('CIVICRM_DSN') || $cacheMode === 'never' || \CRM_Utils_System::isInUpgradeMode()) {
$containerBuilder = $this->createContainer();
$containerBuilder->compile();
return $containerBuilder;
}
$envId = \CRM_Core_Config_Runtime::getId();
$file = CIVICRM_TEMPLATE_COMPILEDIR . "/CachedCiviContainer.{$envId}.php";
$containerConfigCache = new ConfigCache($file, $cacheMode === 'auto');
if (!$containerConfigCache->isFresh()) {
$containerBuilder = $this->createContainer();
$containerBuilder->compile();
$dumper = new PhpDumper($containerBuilder);
$containerConfigCache->write(
$dumper->dump(array('class' => 'CachedCiviContainer')),
$containerBuilder->getResources()
);
}
require_once $file;
$c = new \CachedCiviContainer();
return $c;
}
/**
* Construct a new container.
*
* @var ContainerBuilder
* @return \Symfony\Component\DependencyInjection\ContainerBuilder
*/
public function createContainer() {
$civicrm_base_path = dirname(dirname(__DIR__));
$container = new ContainerBuilder();
$container->addCompilerPass(new RegisterListenersPass('dispatcher'));
$container->addObjectResource($this);
$container->setParameter('civicrm_base_path', $civicrm_base_path);
//$container->set(self::SELF, $this);
$container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
$container->setDefinition(self::SELF, new Definition(
'Civi\Core\Container',
array()
));
// TODO Move configuration to an external file; define caching structure
// if (empty($configDirectories)) {
// throw new \Exception(__CLASS__ . ': Missing required properties (civicrmRoot, configDirectories)');
// }
// $locator = new FileLocator($configDirectories);
// $loaderResolver = new LoaderResolver(array(
// new YamlFileLoader($container, $locator)
// ));
// $delegatingLoader = new DelegatingLoader($loaderResolver);
// foreach (array('services.yml') as $file) {
// $yamlUserFiles = $locator->locate($file, NULL, FALSE);
// foreach ($yamlUserFiles as $file) {
// $delegatingLoader->load($file);
// }
// }
$container->setDefinition('angular', new Definition(
'Civi\Angular\Manager',
array()
))
->setFactory(array(new Reference(self::SELF), 'createAngularManager'));
$container->setDefinition('dispatcher', new Definition(
'Civi\Core\CiviEventDispatcher',
array(new Reference('service_container'))
))
->setFactory(array(new Reference(self::SELF), 'createEventDispatcher'));
$container->setDefinition('magic_function_provider', new Definition(
'Civi\API\Provider\MagicFunctionProvider',
array()
));
$container->setDefinition('civi_api_kernel', new Definition(
'Civi\API\Kernel',
array(new Reference('dispatcher'), new Reference('magic_function_provider'))
))
->setFactory(array(new Reference(self::SELF), 'createApiKernel'));
$container->setDefinition('cxn_reg_client', new Definition(
'Civi\Cxn\Rpc\RegistrationClient',
array()
))
->setFactory('CRM_Cxn_BAO_Cxn::createRegistrationClient');
$container->setDefinition('psr_log', new Definition('CRM_Core_Error_Log', array()));
foreach (array('js_strings', 'community_messages') as $cacheName) {
$container->setDefinition("cache.{$cacheName}", new Definition(
'CRM_Utils_Cache_Interface',
array(
array(
'name' => $cacheName,
'type' => array('*memory*', 'SqlGroup', 'ArrayCache'),
),
)
))->setFactory('CRM_Utils_Cache::create');
}
$container->setDefinition('sql_triggers', new Definition(
'Civi\Core\SqlTriggers',
array()
));
$container->setDefinition('asset_builder', new Definition(
'Civi\Core\AssetBuilder',
array()
));
$container->setDefinition('pear_mail', new Definition('Mail'))
->setFactory('CRM_Utils_Mail::createMailer');
if (empty(\Civi::$statics[__CLASS__]['boot'])) {
throw new \RuntimeException("Cannot initialize container. Boot services are undefined.");
}
foreach (\Civi::$statics[__CLASS__]['boot'] as $bootService => $def) {
$container->setDefinition($bootService, new Definition())->setSynthetic(TRUE);
}
// Expose legacy singletons as services in the container.
$singletons = array(
'resources' => 'CRM_Core_Resources',
'httpClient' => 'CRM_Utils_HttpClient',
'cache.default' => 'CRM_Utils_Cache',
'i18n' => 'CRM_Core_I18n',
// Maybe? 'config' => 'CRM_Core_Config',
// Maybe? 'smarty' => 'CRM_Core_Smarty',
);
foreach ($singletons as $name => $class) {
$container->setDefinition($name, new Definition(
$class
))
->setFactory(array($class, 'singleton'));
}
$container->setDefinition('civi.mailing.triggers', new Definition(
'Civi\Core\SqlTrigger\TimestampTriggers',
array('civicrm_mailing', 'Mailing')
))->addTag('kernel.event_listener', array('event' => 'hook_civicrm_triggerInfo', 'method' => 'onTriggerInfo'));
$container->setDefinition('civi.activity.triggers', new Definition(
'Civi\Core\SqlTrigger\TimestampTriggers',
array('civicrm_activity', 'Activity')
))->addTag('kernel.event_listener', array('event' => 'hook_civicrm_triggerInfo', 'method' => 'onTriggerInfo'));
$container->setDefinition('civi.case.triggers', new Definition(
'Civi\Core\SqlTrigger\TimestampTriggers',
array('civicrm_case', 'Case')
))->addTag('kernel.event_listener', array('event' => 'hook_civicrm_triggerInfo', 'method' => 'onTriggerInfo'));
$container->setDefinition('civi.case.staticTriggers', new Definition(
'Civi\Core\SqlTrigger\StaticTriggers',
array(
array(
array(
'upgrade_check' => array('table' => 'civicrm_case', 'column' => 'modified_date'),
'table' => 'civicrm_case_activity',
'when' => 'AFTER',
'event' => array('INSERT'),
'sql' => "\nUPDATE civicrm_case SET modified_date = CURRENT_TIMESTAMP WHERE id = NEW.case_id;\n",
),
array(
'upgrade_check' => array('table' => 'civicrm_case', 'column' => 'modified_date'),
'table' => 'civicrm_activity',
'when' => 'BEFORE',
'event' => array('UPDATE', 'DELETE'),
'sql' => "\nUPDATE civicrm_case SET modified_date = CURRENT_TIMESTAMP WHERE id IN (SELECT ca.case_id FROM civicrm_case_activity ca WHERE ca.activity_id = OLD.id);\n",
),
),
)
))
->addTag('kernel.event_listener', array('event' => 'hook_civicrm_triggerInfo', 'method' => 'onTriggerInfo'));
$container->setDefinition('civi_token_compat', new Definition(
'Civi\Token\TokenCompatSubscriber',
array()
))->addTag('kernel.event_subscriber');
$container->setDefinition("crm_mailing_action_tokens", new Definition(
"CRM_Mailing_ActionTokens",
array()
))->addTag('kernel.event_subscriber');
foreach (array('Activity', 'Contribute', 'Event', 'Mailing', 'Member') as $comp) {
$container->setDefinition("crm_" . strtolower($comp) . "_tokens", new Definition(
"CRM_{$comp}_Tokens",
array()
))->addTag('kernel.event_subscriber');
}
if (\CRM_Utils_Constant::value('CIVICRM_FLEXMAILER_HACK_SERVICES')) {
\Civi\Core\Resolver::singleton()->call(CIVICRM_FLEXMAILER_HACK_SERVICES, array($container));
}
\CRM_Utils_Hook::container($container);
return $container;
}
/**
* @return \Civi\Angular\Manager
*/
public function createAngularManager() {
return new \Civi\Angular\Manager(\CRM_Core_Resources::singleton());
}
/**
* @param ContainerInterface $container
* @return \Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher
*/
public function createEventDispatcher($container) {
$dispatcher = new CiviEventDispatcher($container);
$dispatcher->addListener(SystemInstallEvent::EVENT_NAME, array('\Civi\Core\InstallationCanary', 'check'));
$dispatcher->addListener(SystemInstallEvent::EVENT_NAME, array('\Civi\Core\DatabaseInitializer', 'initialize'));
$dispatcher->addListener(SystemInstallEvent::EVENT_NAME, array('\Civi\Core\LocalizationInitializer', 'initialize'));
$dispatcher->addListener('hook_civicrm_pre', array('\Civi\Core\Event\PreEvent', 'dispatchSubevent'), 100);
$dispatcher->addListener('hook_civicrm_post', array('\Civi\Core\Event\PostEvent', 'dispatchSubevent'), 100);
$dispatcher->addListener('hook_civicrm_post::Activity', array('\Civi\CCase\Events', 'fireCaseChange'));
$dispatcher->addListener('hook_civicrm_post::Case', array('\Civi\CCase\Events', 'fireCaseChange'));
$dispatcher->addListener('hook_civicrm_caseChange', array('\Civi\CCase\Events', 'delegateToXmlListeners'));
$dispatcher->addListener('hook_civicrm_caseChange', array('\Civi\CCase\SequenceListener', 'onCaseChange_static'));
$dispatcher->addListener('hook_civicrm_eventDefs', array('\Civi\Core\CiviEventInspector', 'findBuiltInEvents'));
// TODO We need a better code-convention for metadata about non-hook events.
$dispatcher->addListener('hook_civicrm_eventDefs', array('\Civi\API\Events', 'hookEventDefs'));
$dispatcher->addListener('hook_civicrm_eventDefs', array('\Civi\Core\Event\SystemInstallEvent', 'hookEventDefs'));
$dispatcher->addListener('hook_civicrm_buildAsset', array('\Civi\Angular\Page\Modules', 'buildAngularModules'));
$dispatcher->addListener('hook_civicrm_buildAsset', array('\CRM_Utils_VisualBundle', 'buildAssetJs'));
$dispatcher->addListener('hook_civicrm_buildAsset', array('\CRM_Utils_VisualBundle', 'buildAssetCss'));
$dispatcher->addListener('civi.dao.postInsert', array('\CRM_Core_BAO_RecurringEntity', 'triggerInsert'));
$dispatcher->addListener('civi.dao.postUpdate', array('\CRM_Core_BAO_RecurringEntity', 'triggerUpdate'));
$dispatcher->addListener('civi.dao.postDelete', array('\CRM_Core_BAO_RecurringEntity', 'triggerDelete'));
$dispatcher->addListener('hook_civicrm_unhandled_exception', array(
'CRM_Core_LegacyErrorHandler',
'handleException',
), -200);
$dispatcher->addListener(\Civi\ActionSchedule\Events::MAPPINGS, array('CRM_Activity_ActionMapping', 'onRegisterActionMappings'));
$dispatcher->addListener(\Civi\ActionSchedule\Events::MAPPINGS, array('CRM_Contact_ActionMapping', 'onRegisterActionMappings'));
$dispatcher->addListener(\Civi\ActionSchedule\Events::MAPPINGS, array('CRM_Contribute_ActionMapping_ByPage', 'onRegisterActionMappings'));
$dispatcher->addListener(\Civi\ActionSchedule\Events::MAPPINGS, array('CRM_Contribute_ActionMapping_ByType', 'onRegisterActionMappings'));
$dispatcher->addListener(\Civi\ActionSchedule\Events::MAPPINGS, array('CRM_Event_ActionMapping', 'onRegisterActionMappings'));
$dispatcher->addListener(\Civi\ActionSchedule\Events::MAPPINGS, array('CRM_Member_ActionMapping', 'onRegisterActionMappings'));
if (\CRM_Utils_Constant::value('CIVICRM_FLEXMAILER_HACK_LISTENERS')) {
\Civi\Core\Resolver::singleton()->call(CIVICRM_FLEXMAILER_HACK_LISTENERS, array($dispatcher));
}
return $dispatcher;
}
/**
* @return LockManager
*/
public static function createLockManager() {
// Ideally, downstream implementers could override any definitions in
// the container. For now, we'll make-do with some define()s.
$lm = new LockManager();
$lm
->register('/^cache\./', defined('CIVICRM_CACHE_LOCK') ? CIVICRM_CACHE_LOCK : array('CRM_Core_Lock', 'createScopedLock'))
->register('/^data\./', defined('CIVICRM_DATA_LOCK') ? CIVICRM_DATA_LOCK : array('CRM_Core_Lock', 'createScopedLock'))
->register('/^worker\.mailing\.send\./', defined('CIVICRM_WORK_LOCK') ? CIVICRM_WORK_LOCK : array('CRM_Core_Lock', 'createCivimailLock'))
->register('/^worker\./', defined('CIVICRM_WORK_LOCK') ? CIVICRM_WORK_LOCK : array('CRM_Core_Lock', 'createScopedLock'));
// Registrations may use complex resolver expressions, but (as a micro-optimization)
// the default factory is specified as an array.
return $lm;
}
/**
* @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher
* @param $magicFunctionProvider
*
* @return \Civi\API\Kernel
*/
public function createApiKernel($dispatcher, $magicFunctionProvider) {
$dispatcher->addSubscriber(new \Civi\API\Subscriber\ChainSubscriber());
$dispatcher->addSubscriber(new \Civi\API\Subscriber\TransactionSubscriber());
$dispatcher->addSubscriber(new \Civi\API\Subscriber\I18nSubscriber());
$dispatcher->addSubscriber($magicFunctionProvider);
$dispatcher->addSubscriber(new \Civi\API\Subscriber\PermissionCheck());
$dispatcher->addSubscriber(new \Civi\API\Subscriber\APIv3SchemaAdapter());
$dispatcher->addSubscriber(new \Civi\API\Subscriber\WrapperAdapter(array(
\CRM_Utils_API_HTMLInputCoder::singleton(),
\CRM_Utils_API_NullOutputCoder::singleton(),
\CRM_Utils_API_ReloadOption::singleton(),
\CRM_Utils_API_MatchOption::singleton(),
)));
$dispatcher->addSubscriber(new \Civi\API\Subscriber\XDebugSubscriber());
$kernel = new \Civi\API\Kernel($dispatcher);
$reflectionProvider = new \Civi\API\Provider\ReflectionProvider($kernel);
$dispatcher->addSubscriber($reflectionProvider);
$dispatcher->addSubscriber(new \Civi\API\Subscriber\DynamicFKAuthorization(
$kernel,
'Attachment',
array('create', 'get', 'delete'),
// Given a file ID, determine the entity+table it's attached to.
'SELECT if(cf.id,1,0) as is_valid, cef.entity_table, cef.entity_id
FROM civicrm_file cf
LEFT JOIN civicrm_entity_file cef ON cf.id = cef.file_id
WHERE cf.id = %1',
// Get a list of custom fields (field_name,table_name,extends)
'SELECT concat("custom_",fld.id) as field_name,
grp.table_name as table_name,
grp.extends as extends
FROM civicrm_custom_field fld
INNER JOIN civicrm_custom_group grp ON fld.custom_group_id = grp.id
WHERE fld.data_type = "File"
',
array('civicrm_activity', 'civicrm_mailing', 'civicrm_contact', 'civicrm_grant')
));
$kernel->setApiProviders(array(
$reflectionProvider,
$magicFunctionProvider,
));
return $kernel;
}
/**
* Get a list of boot services.
*
* These are services which must be setup *before* the container can operate.
*
* @param bool $loadFromDB
* @throws \CRM_Core_Exception
*/
public static function boot($loadFromDB) {
// Array(string $serviceId => object $serviceInstance).
$bootServices = array();
\Civi::$statics[__CLASS__]['boot'] = &$bootServices;
$bootServices['runtime'] = $runtime = new \CRM_Core_Config_Runtime();
$runtime->initialize($loadFromDB);
$bootServices['paths'] = new \Civi\Core\Paths();
$class = $runtime->userFrameworkClass;
$bootServices['userSystem'] = $userSystem = new $class();
$userSystem->initialize();
$userPermissionClass = 'CRM_Core_Permission_' . $runtime->userFramework;
$bootServices['userPermissionClass'] = new $userPermissionClass();
$bootServices['cache.settings'] = \CRM_Utils_Cache::create(array(
'name' => 'settings',
'type' => array('*memory*', 'SqlGroup', 'ArrayCache'),
));
$bootServices['settings_manager'] = new \Civi\Core\SettingsManager($bootServices['cache.settings']);
$bootServices['lockManager'] = self::createLockManager();
if ($loadFromDB && $runtime->dsn) {
\CRM_Core_DAO::init($runtime->dsn);
\CRM_Utils_Hook::singleton(TRUE);
\CRM_Extension_System::singleton(TRUE);
\CRM_Extension_System::singleton(TRUE)->getClassLoader()->register();
$runtime->includeCustomPath();
$c = new self();
$container = $c->loadContainer();
foreach ($bootServices as $name => $obj) {
$container->set($name, $obj);
}
\Civi::$statics[__CLASS__]['container'] = $container;
}
}
public static function getBootService($name) {
return \Civi::$statics[__CLASS__]['boot'][$name];
}
/**
* Determine whether the container services are available.
*
* @return bool
*/
public static function isContainerBooted() {
return isset(\Civi::$statics[__CLASS__]['container']);
}
}

View file

@ -0,0 +1,54 @@
<?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\Core\DAO\Event;
/**
* Class PostUpdate
* @package Civi\Core\DAO\Event
*/
class PostDelete extends \Symfony\Component\EventDispatcher\Event {
/**
* @var DAO Object
*/
public $object;
/**
* @var DAO delete result
*/
public $result;
/**
* @param $object
* @param $result
*/
public function __construct($object, $result) {
$this->object = $object;
$this->result = $result;
}
}

View file

@ -0,0 +1,47 @@
<?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\Core\DAO\Event;
/**
* Class PostUpdate
* @package Civi\Core\DAO\Event
*/
class PostUpdate extends \Symfony\Component\EventDispatcher\Event {
/**
* @var DAO Object
*/
public $object;
/**
* @param $object
*/
public function __construct($object) {
$this->object = $object;
}
}

View file

@ -0,0 +1,47 @@
<?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\Core\DAO\Event;
/**
* Class PreDelete
* @package Civi\Core\DAO\Event
*/
class PreDelete extends \Symfony\Component\EventDispatcher\Event {
/**
* @var DAO Object
*/
public $object;
/**
* @param $object
*/
public function __construct($object) {
$this->object = $object;
}
}

View file

@ -0,0 +1,53 @@
<?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\Core;
use Civi\Core\Event\SystemInstallEvent;
/**
* Class DatabaseInitializer
* @package Civi\Core
*/
class DatabaseInitializer {
/**
* Flush system to build the menu and MySQL triggers
*
* @param \Civi\Core\Event\SystemInstallEvent $event
* @throws \CRM_Core_Exception
*/
public static function initialize(SystemInstallEvent $event) {
$api_params = array(
'version' => 3,
'triggers' => 1,
'session' => 1,
);
civicrm_api('System', 'flush', $api_params);
}
}

View file

@ -0,0 +1,255 @@
<?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\Core\Event;
/**
* Class GenericHookEvent
* @package Civi\API\Event
*
* The GenericHookEvent is used to expose all traditional hooks to the
* Symfony EventDispatcher.
*
* The traditional notation for a hook is based on a function signature:
*
* function hook_civicrm_foo($bar, &$whiz, &$bang);
*
* The notation for Symfony Events is based on a class with properties
* and methods. This requires some kind of mapping. `GenericHookEvent`
* maps each parameter to a field (using magic methods):
*
* @code
* // Creating an event object.
* $event = GenericHookEvent::create(array(
* 'bar' => 'abc',
* 'whiz' => &$whiz,
* 'bang' => &$bang,
* );
*
* // Accessing event properties.
* echo $event->bar;
* $event->whiz['array_field'] = 123;
* $event->bang->objProperty = 'abcd';
*
* // Dispatching an event.
* Civi::service('dispatcher')->dispatch('hook_civicrm_foo', $event);
* @endCode
*
* Design Discussion:
*
* 1. Implementing new event classes for every hook would produce a
* large amount of boilerplate. Symfony Events have an interesting solution to
* that problem: use `GenericEvent` instead of custom event classes.
* `GenericHookEvent` is conceptually similar to `GenericEvent`, but it adds
* support for (a) altering properties and (b) mapping properties to hook notation
* (an ordered parameter list).
*
* 2. A handful of hooks define a return-value. The return-value is treated
* as an array, and all the returned values are merged into one big array.
* You can add and retrieve return-values using these methods:
*
* @code
* $event->addReturnValues(array(...));
* foreach ($event->getReturnValues() as $retVal) { ... }
* @endCode
*/
class GenericHookEvent extends \Symfony\Component\EventDispatcher\Event {
/**
* @var array
* Ex: array(0 => &$contactID, 1 => &$contentPlacement).
*/
protected $hookValues;
/**
* @var array
* Ex: array(0 => 'contactID', 1 => 'contentPlacement').
*/
protected $hookFields;
/**
* @var array
* Ex: array('contactID' => 0, 'contentPlacement' => 1).
*/
protected $hookFieldsFlip;
/**
* Some legacy hooks expect listener-functions to return a value.
* OOP listeners may set the $returnValue.
*
* This field is not recommended for use in new hooks. The return-value
* convention is not portable across different implementations of the hook
* system. Instead, it's more portable to provide an alterable, named field.
*
* @var mixed
* @deprecated
*/
private $returnValues = array();
/**
* List of field names that are prohibited due to conflicts
* in the class-hierarchy.
*
* @var array
*/
private static $BLACKLIST = array(
'name',
'dispatcher',
'propagationStopped',
'hookBlacklist',
'hookValues',
'hookFields',
'hookFieldsFlip',
);
/**
* Create a GenericHookEvent using key-value pairs.
*
* @param array $params
* Ex: array('contactID' => &$contactID, 'contentPlacement' => &$contentPlacement).
* @return \Civi\Core\Event\GenericHookEvent
*/
public static function create($params) {
$e = new static();
$e->hookValues = array_values($params);
$e->hookFields = array_keys($params);
$e->hookFieldsFlip = array_flip($e->hookFields);
self::assertValidHookFields($e->hookFields);
return $e;
}
/**
* Create a GenericHookEvent using ordered parameters.
*
* @param array $hookFields
* Ex: array(0 => 'contactID', 1 => 'contentPlacement').
* @param array $hookValues
* Ex: array(0 => &$contactID, 1 => &$contentPlacement).
* @return \Civi\Core\Event\GenericHookEvent
*/
public static function createOrdered($hookFields, $hookValues) {
$e = new static();
if (count($hookValues) > count($hookFields)) {
$hookValues = array_slice($hookValues, 0, count($hookFields));
}
$e->hookValues = $hookValues;
$e->hookFields = $hookFields;
$e->hookFieldsFlip = array_flip($e->hookFields);
self::assertValidHookFields($e->hookFields);
return $e;
}
/**
* @param array $fields
* List of field names.
*/
private static function assertValidHookFields($fields) {
$bad = array_intersect($fields, self::$BLACKLIST);
if ($bad) {
throw new \RuntimeException("Hook relies on conflicted field names: "
. implode(', ', $bad));
}
}
/**
* @return array
* Ex: array(0 => &$contactID, 1 => &$contentPlacement).
*/
public function getHookValues() {
return $this->hookValues;
}
/**
* @return mixed
* @deprecated
*/
public function getReturnValues() {
return empty($this->returnValues) ? TRUE : $this->returnValues;
}
/**
* @param mixed $fResult
* @return GenericHookEvent
* @deprecated
*/
public function addReturnValues($fResult) {
if (!empty($fResult) && is_array($fResult)) {
$this->returnValues = array_merge($this->returnValues, $fResult);
}
return $this;
}
/**
* @inheritDoc
*/
public function &__get($name) {
if (isset($this->hookFieldsFlip[$name])) {
return $this->hookValues[$this->hookFieldsFlip[$name]];
}
}
/**
* @inheritDoc
*/
public function __set($name, $value) {
if (isset($this->hookFieldsFlip[$name])) {
$this->hookValues[$this->hookFieldsFlip[$name]] = $value;
}
}
/**
* @inheritDoc
*/
public function __isset($name) {
return isset($this->hookFieldsFlip[$name])
&& isset($this->hookValues[$this->hookFieldsFlip[$name]]);
}
/**
* @inheritDoc
*/
public function __unset($name) {
if (isset($this->hookFieldsFlip[$name])) {
// Unset while preserving order.
$this->hookValues[$this->hookFieldsFlip[$name]] = NULL;
}
}
/**
* Determine whether the hook supports the given field.
*
* The field may or may not be empty. Use isset() or empty() to
* check that.
*
* @param string $name
* @return bool
*/
public function hasField($name) {
return isset($this->hookFieldsFlip[$name]);
}
}

View file

@ -0,0 +1,87 @@
<?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\Core\Event;
/**
* Class AuthorizeEvent
* @package Civi\API\Event
*/
class PostEvent extends GenericHookEvent {
/**
* This adapter automatically emits a narrower event.
*
* For example, `hook_civicrm_pre(Contact, ...)` will also dispatch `hook_civicrm_pre::Contact`.
*
* @param \Civi\Core\Event\PostEvent $event
*/
public static function dispatchSubevent(PostEvent $event) {
\Civi::service('dispatcher')->dispatch("hook_civicrm_post::" . $event->entity, $event);
}
/**
* @var string 'create'|'edit'|'delete' etc
*/
public $action;
/**
* @var string
*/
public $entity;
/**
* @var int|NULL
*/
public $id;
/**
* @var Object
*/
public $object;
/**
* @param $action
* @param $entity
* @param $id
* @param $object
*/
public function __construct($action, $entity, $id, &$object) {
$this->action = $action;
$this->entity = $entity;
$this->id = $id;
$this->object = &$object;
}
/**
* @inheritDoc
*/
public function getHookValues() {
return array($this->action, $this->entity, $this->id, &$this->object);
}
}

View file

@ -0,0 +1,87 @@
<?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\Core\Event;
/**
* Class AuthorizeEvent
* @package Civi\API\Event
*/
class PreEvent extends GenericHookEvent {
/**
* This adapter automatically emits a narrower event.
*
* For example, `hook_civicrm_pre(Contact, ...)` will also dispatch `hook_civicrm_pre::Contact`.
*
* @param \Civi\Core\Event\PreEvent $event
*/
public static function dispatchSubevent(PreEvent $event) {
\Civi::service('dispatcher')->dispatch("hook_civicrm_pre::" . $event->entity, $event);
}
/**
* @var string 'create'|'edit'|'delete' etc
*/
public $action;
/**
* @var string
*/
public $entity;
/**
* @var int|NULL
*/
public $id;
/**
* @var array
*/
public $params;
/**
* @param $action
* @param $entity
* @param $id
* @param $params
*/
public function __construct($action, $entity, $id, &$params) {
$this->action = $action;
$this->entity = $entity;
$this->id = $id;
$this->params = &$params;
}
/**
* @inheritDoc
*/
public function getHookValues() {
return array($this->action, $this->entity, $this->id, &$this->params);
}
}

View file

@ -0,0 +1,49 @@
<?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\Core\Event;
/**
* Class SystemInstallEvent
* @package Civi\API\Event
*/
class SystemInstallEvent extends \Symfony\Component\EventDispatcher\Event {
/**
* The SystemInstallEvent fires once after installation - during the first page-view.
*/
const EVENT_NAME = 'civi.core.install';
/**
* @param \Civi\Core\Event\GenericHookEvent $e
* @see \CRM_Utils_Hook::eventDefs
*/
public static function hookEventDefs($e) {
$e->inspector->addEventClass(self::EVENT_NAME, __CLASS__);
}
}

View file

@ -0,0 +1,62 @@
<?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\Core\Event;
/**
* Class UnhandledExceptionEvent
* @package Civi\API\Event
*/
class UnhandledExceptionEvent extends GenericHookEvent {
/**
* @var \Exception
*/
public $exception;
/**
* @var mixed reserved for future use
*/
public $request;
/**
* @param $e
* @param $request
*/
public function __construct($e, $request) {
$this->request = $request;
$this->exception = $e;
}
/**
* @inheritDoc
*/
public function getHookValues() {
return array($this->exception, $this->request);
}
}

View file

@ -0,0 +1,6 @@
<?php
namespace Civi\Core\Exception;
class UnknownAssetException extends \CRM_Core_Exception {
}

View file

@ -0,0 +1,59 @@
<?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\Core;
use Civi\Core\Event\SystemInstallEvent;
/**
* Class InstallationCanary
* @package Civi\Core
*/
class InstallationCanary {
/**
* Check whether the install has run before.
*
* Circa v4.7.betaX, we introduced a new mechanism for tracking installation
* and firing a post-install event. However, it's fairly difficult to test the
* edge-cases directly, so this canary should fire if there are any problems
* in the design/implementation of the installation-tracker.
*
* This should not exist. It should be removed in a future version.
*
* @param \Civi\Core\Event\SystemInstallEvent $event
* @throws \CRM_Core_Exception
*/
public static function check(SystemInstallEvent $event) {
if (\CRM_Core_DAO::checkTableExists('civicrm_install_canary')) {
throw new \CRM_Core_Exception("Found installation canary. This suggests that something went wrong with tracking installation process. Please post to forum or JIRA.");
}
\Civi::log()->info('Creating canary table');
\CRM_Core_DAO::executeQuery('CREATE TABLE civicrm_install_canary (id int(10) unsigned NOT NULL) ENGINE=InnoDB');
}
}

View file

@ -0,0 +1,104 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 4.7 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2015 |
+--------------------------------------------------------------------+
| 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\Core;
use Civi;
use Civi\Core\Event\SystemInstallEvent;
/**
* Class LocalizationInitializer
* @package Civi\Core
*/
class LocalizationInitializer {
/**
* Load the locale settings based on the installation language
*
* @param \Civi\Core\Event\SystemInstallEvent $event
* @throws \CRM_Core_Exception
*/
public static function initialize(SystemInstallEvent $event) {
// get the current installation language
global $tsLocale;
$seedLanguage = $tsLocale;
if (!$seedLanguage) {
return;
}
// get the corresponding settings file if any
$localeDir = \CRM_Core_I18n::getResourceDir();
$fileName = $localeDir . $seedLanguage . DIRECTORY_SEPARATOR . 'settings.default.json';
// initalization
$settingsParams = array();
if (file_exists($fileName)) {
// load the file and parse it
$json = file_get_contents($fileName);
$settings = json_decode($json, TRUE);
if (!empty($settings)) {
// get all valid settings
$results = civicrm_api3('Setting', 'getfields', array());
$validSettings = array_keys($results['values']);
// add valid settings to params to send to api
foreach ($settings as $setting => $value) {
if (in_array($setting, $validSettings)) {
$settingsParams[$setting] = $value;
}
}
// ensure we don't mess with multilingual
unset($settingsParams['languageLimit']);
// support for enabled languages (option group)
if (isset($settings['languagesOption']) && count($settings['languagesOption']) > 0) {
\CRM_Core_BAO_OptionGroup::setActiveValues('languages', $settings['languagesOption']);
}
// set default currency in currencies_enabled (option group)
if (isset($settings['defaultCurrency'])) {
\CRM_Admin_Form_Setting_Localization::updateEnabledCurrencies(array($settings['defaultCurrency']), $settings['defaultCurrency']);
}
}
}
// in any case, enforce the seedLanguage as the default language
$settingsParams['lcMessages'] = $seedLanguage;
// apply the config
civicrm_api3('Setting', 'create', $settingsParams);
}
}

View file

@ -0,0 +1,64 @@
<?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\Core\Lock;
/**
* Lock interface.
*/
interface LockInterface {
/**
* Acquire lock.
*
* @param int|NULL $timeout
* The number of seconds to wait to get the lock.
* For a default value, use NULL.
* @return bool
*/
public function acquire($timeout = NULL);
/**
* @return bool|null|string
* Trueish/falsish.
*/
public function release();
/**
* @return bool|null|string
* Trueish/falsish.
* @deprecated
* Not supported by some locking strategies. If you need to poll, better
* to use acquire(0).
*/
public function isFree();
/**
* @return bool
*/
public function isAcquired();
}

View file

@ -0,0 +1,121 @@
<?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\Core\Lock;
use Civi\Core\Resolver;
/**
* Class LockManager
* @package Civi\Core\Lock
*
* The lock-manager allows one to define the lock policy -- i.e. given a
* specific lock, how does one acquire the lock?
*/
class LockManager {
private $rules = array();
/**
* @param string $name
* Symbolic name for the lock. Names generally look like
* "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
*
* Categories: worker|data|cache|...
* Component: core|mailing|member|contribute|...
* @return LockInterface
* @throws \CRM_Core_Exception
*/
public function create($name) {
$factory = $this->getFactory($name);
if ($factory) {
/** @var LockInterface $lock */
$lock = call_user_func_array($factory, array($name));
return $lock;
}
else {
throw new \CRM_Core_Exception("Lock \"$name\" does not match any rules. Use register() to add more rules.");
}
}
/**
* Create and attempt to acquire a lock.
*
* Note: Be sure to check $lock->isAcquired() to determine whether
* acquisition was successful.
*
* @param string $name
* Symbolic name for the lock. Names generally look like
* "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
*
* Categories: worker|data|cache|...
* Component: core|mailing|member|contribute|...
* @param int|NULL $timeout
* The number of seconds to wait to get the lock.
* For a default value, use NULL.
* @return LockInterface
* @throws \CRM_Core_Exception
*/
public function acquire($name, $timeout = NULL) {
$lock = $this->create($name);
$lock->acquire($timeout);
return $lock;
}
/**
* @param string $name
* Symbolic name for the lock.
* @return callable|NULL
*/
public function getFactory($name) {
foreach ($this->rules as $rule) {
if (preg_match($rule['pattern'], $name)) {
return Resolver::singleton()->get($rule['factory']);
}
}
return NULL;
}
/**
* Register the lock-factory to use for specific lock-names.
*
* @param string $pattern
* A regex to match against the lock name.
* @param string|array $factory
* A callback. The callback should accept a $name parameter.
* Callbacks will be located using the resolver.
* @return LockManager
* @see Resolver
*/
public function register($pattern, $factory) {
$this->rules[] = array(
'pattern' => $pattern,
'factory' => $factory,
);
return $this;
}
}

View file

@ -0,0 +1,92 @@
<?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\Core\Lock;
/**
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
*/
class NullLock implements LockInterface {
private $hasLock = FALSE;
/**
* Create lock.
*
* @param string $name
*
* @return static
*/
public static function create($name) {
return new static();
}
/**
* Acquire lock.
*
* @param int|NULL $timeout
* The number of seconds to wait to get the lock.
* For a default value, use NULL.
*
* @return bool
*/
public function acquire($timeout = NULL) {
$this->hasLock = TRUE;
return TRUE;
}
/**
* Release lock.
*
* @return bool|null|string
* Trueish/falsish.
*/
public function release() {
$this->hasLock = FALSE;
return TRUE;
}
/**
* @return bool|null|string
* Trueish/falsish.
* @deprecated
* Not supported by some locking strategies. If you need to poll, better
* to use acquire(0).
*/
public function isFree() {
return !$this->hasLock;
}
/**
* @return bool
*/
public function isAcquired() {
return $this->hasLock;
}
}

View file

@ -0,0 +1,211 @@
<?php
namespace Civi\Core;
/**
* Class Paths
* @package Civi\Core
*
* This paths class translates path-expressions into local file paths and
* URLs. Path-expressions may take a few forms:
*
* - Paths and URLs may use a variable prefix. For example, '[civicrm.files]/upload'
* - Paths and URLS may be absolute.
* - Paths may be relative (base dir: [civicrm.files]).
* - URLs may be relative (base dir: [cms.root]).
*/
class Paths {
const DEFAULT_URL = 'cms.root';
const DEFAULT_PATH = 'civicrm.files';
/**
* @var array
* Array(string $name => array(url => $, path => $)).
*/
private $variables = array();
private $variableFactory = array();
/**
* Class constructor.
*/
public function __construct() {
$paths = $this;
$this
->register('civicrm.root', function () {
return \CRM_Core_Config::singleton()->userSystem->getCiviSourceStorage();
})
->register('civicrm.packages', function () {
return array(
'path' => \Civi::paths()->getPath('[civicrm.root]/packages/'),
'url' => \Civi::paths()->getUrl('[civicrm.root]/packages/'),
);
})
->register('civicrm.vendor', function () {
return array(
'path' => \Civi::paths()->getPath('[civicrm.root]/vendor/'),
'url' => \Civi::paths()->getUrl('[civicrm.root]/vendor/'),
);
})
->register('civicrm.bower', function () {
return array(
'path' => \Civi::paths()->getPath('[civicrm.root]/bower_components/'),
'url' => \Civi::paths()->getUrl('[civicrm.root]/bower_components/'),
);
})
->register('civicrm.files', function () {
return \CRM_Core_Config::singleton()->userSystem->getDefaultFileStorage();
})
->register('wp.frontend.base', function () {
return array('url' => rtrim(CIVICRM_UF_BASEURL, '/') . '/');
})
->register('wp.frontend', function () use ($paths) {
$config = \CRM_Core_Config::singleton();
$suffix = defined('CIVICRM_UF_WP_BASEPAGE') ? CIVICRM_UF_WP_BASEPAGE : $config->wpBasePage;
return array(
'url' => $paths->getVariable('wp.frontend.base', 'url') . $suffix,
);
})
->register('wp.backend.base', function () {
return array('url' => rtrim(CIVICRM_UF_BASEURL, '/') . '/wp-admin/');
})
->register('wp.backend', function () use ($paths) {
return array(
'url' => $paths->getVariable('wp.backend.base', 'url') . 'admin.php',
);
})
->register('cms', function () {
return array(
'path' => \CRM_Core_Config::singleton()->userSystem->cmsRootPath(),
'url' => \CRM_Utils_System::baseCMSURL(),
);
})
->register('cms.root', function () {
return array(
'path' => \CRM_Core_Config::singleton()->userSystem->cmsRootPath(),
// Misleading: this *removes* the language part of the URL, producing a pristine base URL.
'url' => \CRM_Utils_System::languageNegotiationURL(\CRM_Utils_System::baseCMSURL(), FALSE, TRUE),
);
});
}
/**
* Register a new URL/file path mapping.
*
* @param string $name
* The name of the variable.
* @param callable $factory
* Function which returns an array with keys:
* - path: string.
* - url: string.
* @return Paths
*/
public function register($name, $factory) {
$this->variableFactory[$name] = $factory;
return $this;
}
/**
* @param string $name
* Ex: 'civicrm.root'.
* @param string $attr
* Ex: 'url', 'path'.
* @return mixed
*/
public function getVariable($name, $attr) {
if (!isset($this->variables[$name])) {
$this->variables[$name] = call_user_func($this->variableFactory[$name]);
if (isset($GLOBALS['civicrm_paths'][$name])) {
$this->variables[$name] = array_merge($this->variables[$name], $GLOBALS['civicrm_paths'][$name]);
}
}
if (!isset($this->variables[$name][$attr])) {
throw new \RuntimeException("Cannot resolve path using \"$name.$attr\"");
}
return $this->variables[$name][$attr];
}
/**
* Does the variable exist.
*
* @param string $name
*
* @return bool
*/
public function hasVariable($name) {
return isset($this->variableFactory[$name]);
}
/**
* Determine the absolute path to a file, given that the file is most likely
* in a given particular variable.
*
* @param string $value
* The file path.
* Use "." to reference to default file root.
* Values may begin with a variable, e.g. "[civicrm.files]/upload".
* @return mixed|string
*/
public function getPath($value) {
$defaultContainer = self::DEFAULT_PATH;
if ($value && $value{0} == '[' && preg_match(';^\[([a-zA-Z0-9\._]+)\]/(.*);', $value, $matches)) {
$defaultContainer = $matches[1];
$value = $matches[2];
}
if (empty($value)) {
return FALSE;
}
if ($value === '.') {
$value = '';
}
return \CRM_Utils_File::absoluteDirectory($value, $this->getVariable($defaultContainer, 'path'));
}
/**
* Determine the URL to a file.
*
* @param string $value
* The file path. The path may begin with a variable, e.g. "[civicrm.files]/upload".
* @param string $preferFormat
* The preferred format ('absolute', 'relative').
* The result data may not meet the preference -- if the setting
* refers to an external domain, then the result will be
* absolute (regardless of preference).
* @param bool|NULL $ssl
* NULL to autodetect. TRUE to force to SSL.
* @return mixed|string
*/
public function getUrl($value, $preferFormat = 'relative', $ssl = NULL) {
$defaultContainer = self::DEFAULT_URL;
if ($value && $value{0} == '[' && preg_match(';^\[([a-zA-Z0-9\._]+)\](/(.*))$;', $value, $matches)) {
$defaultContainer = $matches[1];
$value = empty($matches[3]) ? '.' : $matches[3];
}
if (empty($value)) {
return FALSE;
}
if ($value === '.') {
$value = '';
}
if (substr($value, 0, 4) == 'http') {
return $value;
}
$value = $this->getVariable($defaultContainer, 'url') . $value;
if ($preferFormat === 'relative') {
$parsed = parse_url($value);
if (isset($_SERVER['HTTP_HOST']) && isset($parsed['host']) && $_SERVER['HTTP_HOST'] == $parsed['host']) {
$value = $parsed['path'];
}
}
if ($ssl || ($ssl === NULL && \CRM_Utils_System::isSSL())) {
$value = str_replace('http://', 'https://', $value);
}
return $value;
}
}

View file

@ -0,0 +1,287 @@
<?php
namespace Civi\Core;
/**
* The resolver takes a string expression and returns an object or callable.
*
* The following patterns will resolve to objects:
* - 'obj://objectName' - An object from Civi\Core\Container
* - 'ClassName' - An instance of ClassName (with default constructor).
* If you need more control over construction, then register with the
* container.
*
* The following patterns will resolve to callables:
* - 'function_name' - A function(callable).
* - 'ClassName::methodName" - A static method of a class.
* - 'call://objectName/method' - A method on an object from Civi\Core\Container.
* - 'api3://EntityName/action' - A method call on an API.
* (Performance note: Requires full setup/teardown of API subsystem.)
* - 'api3://EntityName/action?first=@1&second=@2' - Call an API method, mapping the
* first & second args to named parameters.
* (Performance note: Requires parsing/interpolating arguments).
* - 'global://Variable/Key2/Key3?getter' - A dummy which looks up a global variable.
* - 'global://Variable/Key2/Key3?setter' - A dummy which updates a global variable.
* - '0' or '1' - A dummy which returns the constant '0' or '1'.
*
* Note: To differentiate classes and functions, there is a hard requirement that
* class names begin with an uppercase letter.
*
* Note: If you are working in a context which requires a callable, it is legitimate to use
* an object notation ("obj://objectName" or "ClassName") if the object supports __invoke().
*
* @package Civi\Core
*/
class Resolver {
protected static $_singleton;
/**
* Singleton function.
*
* @return Resolver
*/
public static function singleton() {
if (self::$_singleton === NULL) {
self::$_singleton = new Resolver();
}
return self::$_singleton;
}
/**
* Convert a callback expression to a valid PHP callback.
*
* @param string|array $id
* A callback expression; any of the following.
*
* @return array|callable
* A PHP callback. Do not serialize (b/c it may include an object).
* @throws \RuntimeException
*/
public function get($id) {
if (!is_string($id)) {
// An array or object does not need to be further resolved.
return $id;
}
if (strpos($id, '::') !== FALSE) {
// Callback: Static method.
return explode('::', $id);
}
elseif (strpos($id, '://') !== FALSE) {
$url = parse_url($id);
switch ($url['scheme']) {
case 'obj':
// Object: Lookup in container.
return \Civi::service($url['host']);
case 'call':
// Callback: Object/method in container.
$obj = \Civi::service($url['host']);
return array($obj, ltrim($url['path'], '/'));
case 'api3':
// Callback: API.
return new ResolverApi($url);
case 'global':
// Lookup in a global variable.
return new ResolverGlobalCallback($url['query'], $url['host'] . (isset($url['path']) ? rtrim($url['path'], '/') : ''));
default:
throw new \RuntimeException("Unsupported callback scheme: " . $url['scheme']);
}
}
elseif (in_array($id, array('0', '1'))) {
// Callback: Constant value.
return new ResolverConstantCallback((int) $id);
}
elseif ($id{0} >= 'A' && $id{0} <= 'Z') {
// Object: New/default instance.
return new $id();
}
else {
// Callback: Function.
return $id;
}
}
/**
* Invoke a callback expression.
*
* @param string|callable $id
* @param array $args
* Ordered parameters. To call-by-reference, set an array-parameter by reference.
*
* @return mixed
*/
public function call($id, $args) {
$cb = $this->get($id);
return $cb ? call_user_func_array($cb, $args) : NULL;
}
}
/**
* Private helper which produces a dummy callback.
*
* @package Civi\Core
*/
class ResolverConstantCallback {
/**
* @var mixed
*/
private $value;
/**
* Class constructor.
*
* @param mixed $value
* The value to be returned by the dummy callback.
*/
public function __construct($value) {
$this->value = $value;
}
/**
* Invoke function.
*
* @return mixed
*/
public function __invoke() {
return $this->value;
}
}
/**
* Private helper which treats an API as a callable function.
*
* @package Civi\Core
*/
class ResolverApi {
/**
* @var array
* - string scheme
* - string host
* - string path
* - string query (optional)
*/
private $url;
/**
* Class constructor.
*
* @param array $url
* Parsed URL (e.g. "api3://EntityName/action?foo=bar").
*
* @see parse_url
*/
public function __construct($url) {
$this->url = $url;
}
/**
* Fire an API call.
*/
public function __invoke() {
$apiParams = array();
if (isset($this->url['query'])) {
parse_str($this->url['query'], $apiParams);
}
if (count($apiParams)) {
$args = func_get_args();
if (count($args)) {
$this->interpolate($apiParams, $this->createPlaceholders('@', $args));
}
}
$result = civicrm_api3($this->url['host'], ltrim($this->url['path'], '/'), $apiParams);
return isset($result['values']) ? $result['values'] : NULL;
}
/**
* Create placeholders.
*
* @param string $prefix
* @param array $args
* Positional arguments.
*
* @return array
* Named placeholders based on the positional arguments
* (e.g. "@1" => "firstValue").
*/
protected function createPlaceholders($prefix, $args) {
$result = array();
foreach ($args as $offset => $arg) {
$result[$prefix . (1 + $offset)] = $arg;
}
return $result;
}
/**
* Recursively interpolate values.
*
* @code
* $params = array('foo' => '@1');
* $this->interpolate($params, array('@1'=> $object))
* assert $data['foo'] == $object;
* @endcode
*
* @param array $array
* Array which may or many not contain a mix of tokens.
* @param array $replacements
* A list of tokens to substitute.
*/
protected function interpolate(&$array, $replacements) {
foreach (array_keys($array) as $key) {
if (is_array($array[$key])) {
$this->interpolate($array[$key], $replacements);
continue;
}
foreach ($replacements as $oldVal => $newVal) {
if ($array[$key] === $oldVal) {
$array[$key] = $newVal;
}
}
}
}
}
class ResolverGlobalCallback {
private $mode, $path;
/**
* Class constructor.
*
* @param string $mode
* 'getter' or 'setter'.
* @param string $path
*/
public function __construct($mode, $path) {
$this->mode = $mode;
$this->path = $path;
}
/**
* Invoke function.
*
* @param mixed $arg1
*
* @return mixed
*/
public function __invoke($arg1 = NULL) {
if ($this->mode === 'getter') {
return \CRM_Utils_Array::pathGet($GLOBALS, explode('/', $this->path));
}
elseif ($this->mode === 'setter') {
\CRM_Utils_Array::pathSet($GLOBALS, explode('/', $this->path), $arg1);
return NULL;
}
else {
throw new \RuntimeException("Resolver failed: global:// must specify getter or setter mode.");
}
}
}

View file

@ -0,0 +1,403 @@
<?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\Core;
/**
* Class SettingsBag
* @package Civi\Core
*
* Read and write settings for a given domain (or contact).
*
* If the target entity does not already have a value for the setting, then
* the defaults will be used. If mandatory values are provided, they will
* override any defaults or custom settings.
*
* It's expected that the SettingsBag will have O(50-250) settings -- and that
* we'll load the full bag on many page requests. Consequently, we don't
* want the full metadata (help text and version history and HTML widgets)
* for all 250 settings, but we do need the default values.
*
* This class is not usually instantiated directly. Instead, use SettingsManager
* or Civi::settings().
*
* @see \Civi::settings()
* @see SettingsManagerTest
*/
class SettingsBag {
protected $domainId;
protected $contactId;
/**
* @var array
* Array(string $settingName => mixed $value).
*/
protected $defaults;
/**
* @var array
* Array(string $settingName => mixed $value).
*/
protected $mandatory;
/**
* The result of combining default values, mandatory
* values, and user values.
*
* @var array|NULL
* Array(string $settingName => mixed $value).
*/
protected $combined;
/**
* @var array
*/
protected $values;
/**
* @param int $domainId
* The domain for which we want settings.
* @param int|NULL $contactId
* The contact for which we want settings. Use NULL for domain settings.
*/
public function __construct($domainId, $contactId) {
$this->domainId = $domainId;
$this->contactId = $contactId;
$this->values = array();
$this->combined = NULL;
}
/**
* Set/replace the default values.
*
* @param array $defaults
* Array(string $settingName => mixed $value).
* @return SettingsBag
*/
public function loadDefaults($defaults) {
$this->defaults = $defaults;
$this->combined = NULL;
return $this;
}
/**
* Set/replace the mandatory values.
*
* @param array $mandatory
* Array(string $settingName => mixed $value).
* @return SettingsBag
*/
public function loadMandatory($mandatory) {
$this->mandatory = $mandatory;
$this->combined = NULL;
return $this;
}
/**
* Load all explicit settings that apply to this domain or contact.
*
* @return SettingsBag
*/
public function loadValues() {
// Note: Don't use DAO child classes. They require fields() which require
// translations -- which are keyed off settings!
$this->values = array();
$this->combined = NULL;
// Ordinarily, we just load values from `civicrm_setting`. But upgrades require care.
// In v4.0 and earlier, all values were stored in `civicrm_domain.config_backend`.
// In v4.1-v4.6, values were split between `civicrm_domain` and `civicrm_setting`.
// In v4.7+, all values are stored in `civicrm_setting`.
// Whenever a value is available in civicrm_setting, it will take precedence.
$isUpgradeMode = \CRM_Core_Config::isUpgradeMode();
if ($isUpgradeMode && empty($this->contactId) && \CRM_Core_DAO::checkFieldExists('civicrm_domain', 'config_backend', FALSE)) {
$config_backend = \CRM_Core_DAO::singleValueQuery('SELECT config_backend FROM civicrm_domain WHERE id = %1',
array(1 => array($this->domainId, 'Positive')));
$oldSettings = \CRM_Upgrade_Incremental_php_FourSeven::convertBackendToSettings($this->domainId, $config_backend);
\CRM_Utils_Array::extend($this->values, $oldSettings);
}
// Normal case. Aside: Short-circuit prevents unnecessary query.
if (!$isUpgradeMode || \CRM_Core_DAO::checkTableExists('civicrm_setting')) {
$dao = \CRM_Core_DAO::executeQuery($this->createQuery()->toSQL());
while ($dao->fetch()) {
$this->values[$dao->name] = ($dao->value !== NULL) ? unserialize($dao->value) : NULL;
}
}
return $this;
}
/**
* Add a batch of settings. Save them.
*
* @param array $settings
* Array(string $settingName => mixed $settingValue).
* @return SettingsBag
*/
public function add(array $settings) {
foreach ($settings as $key => $value) {
$this->set($key, $value);
}
return $this;
}
/**
* Get a list of all effective settings.
*
* @return array
* Array(string $settingName => mixed $settingValue).
*/
public function all() {
if ($this->combined === NULL) {
$this->combined = $this->combine(
array($this->defaults, $this->values, $this->mandatory)
);
}
return $this->combined;
}
/**
* Determine the effective value.
*
* @param string $key
* @return mixed
*/
public function get($key) {
$all = $this->all();
return isset($all[$key]) ? $all[$key] : NULL;
}
/**
* Determine the default value of a setting.
*
* @param string $key
* The simple name of the setting.
* @return mixed|NULL
*/
public function getDefault($key) {
return isset($this->defaults[$key]) ? $this->defaults[$key] : NULL;
}
/**
* Determine the explicitly designated value, regardless of
* any default or mandatory values.
*
* @param string $key
* The simple name of the setting.
* @return mixed|NULL
*/
public function getExplicit($key) {
return (isset($this->values[$key]) ? $this->values[$key] : NULL);
}
/**
* Determine the mandatory value of a setting.
*
* @param string $key
* The simple name of the setting.
* @return mixed|NULL
*/
public function getMandatory($key) {
return isset($this->mandatory[$key]) ? $this->mandatory[$key] : NULL;
}
/**
* Determine if the entity has explicitly designated a value.
*
* Note that get() may still return other values based on
* mandatory values or defaults.
*
* @param string $key
* The simple name of the setting.
* @return bool
*/
public function hasExplict($key) {
// NULL means no designated value.
return isset($this->values[$key]);
}
/**
* Removes any explicit settings. This restores the default.
*
* @param string $key
* The simple name of the setting.
* @return SettingsBag
*/
public function revert($key) {
// It might be better to DELETE (to avoid long-term leaks),
// but setting NULL is simpler for now.
return $this->set($key, NULL);
}
/**
* Add a single setting. Save it.
*
* @param string $key
* The simple name of the setting.
* @param mixed $value
* The new, explicit value of the setting.
* @return SettingsBag
*/
public function set($key, $value) {
$this->setDb($key, $value);
$this->values[$key] = $value;
$this->combined = NULL;
return $this;
}
/**
* @return \CRM_Utils_SQL_Select
*/
protected function createQuery() {
$select = \CRM_Utils_SQL_Select::from('civicrm_setting')
->select('id, name, value, domain_id, contact_id, is_domain, component_id, created_date, created_id')
->where('domain_id = #id', array(
'id' => $this->domainId,
));
if ($this->contactId === NULL) {
$select->where('is_domain = 1');
}
else {
$select->where('contact_id = #id', array(
'id' => $this->contactId,
));
$select->where('is_domain = 0');
}
return $select;
}
/**
* Combine a series of arrays, excluding any
* null values. Later values override earlier
* values.
*
* @param array $arrays
* List of arrays to combine.
* @return array
*/
protected function combine($arrays) {
$combined = array();
foreach ($arrays as $array) {
foreach ($array as $k => $v) {
if ($v !== NULL) {
$combined[$k] = $v;
}
}
}
return $combined;
}
/**
* Update the DB record for this setting.
*
* @param string $name
* The simple name of the setting.
* @param mixed $value
* The new value of the setting.
*/
protected function setDb($name, $value) {
if (\CRM_Core_BAO_Setting::isUpgradeFromPreFourOneAlpha1()) {
// civicrm_setting table is not going to be present.
return;
}
$fields = array();
$fieldsToSet = \CRM_Core_BAO_Setting::validateSettingsInput(array($name => $value), $fields);
//We haven't traditionally validated inputs to setItem, so this breaks things.
//foreach ($fieldsToSet as $settingField => &$settingValue) {
// self::validateSetting($settingValue, $fields['values'][$settingField]);
//}
$metadata = $fields['values'][$name];
$dao = new \CRM_Core_DAO_Setting();
$dao->name = $name;
$dao->domain_id = $this->domainId;
if ($this->contactId) {
$dao->contact_id = $this->contactId;
$dao->is_domain = 0;
}
else {
$dao->is_domain = 1;
}
$dao->find(TRUE);
// Call 'on_change' listeners. It would be nice to only fire when there's
// a genuine change in the data. However, PHP developers have mixed
// expectations about whether 0, '0', '', NULL, and FALSE represent the same
// value, so there's no universal way to determine if a change is genuine.
if (isset($metadata['on_change'])) {
foreach ($metadata['on_change'] as $callback) {
call_user_func(
\Civi\Core\Resolver::singleton()->get($callback),
unserialize($dao->value),
$value,
$metadata,
$this->domainId
);
}
}
if (!is_array($value) && \CRM_Utils_System::isNull($value)) {
$dao->value = 'null';
}
else {
$dao->value = serialize($value);
}
if (!isset(\Civi::$statics[__CLASS__]['upgradeMode'])) {
\Civi::$statics[__CLASS__]['upgradeMode'] = \CRM_Core_Config::isUpgradeMode();
}
if (\Civi::$statics[__CLASS__]['upgradeMode'] && \CRM_Core_DAO::checkFieldExists('civicrm_setting', 'group_name')) {
$dao->group_name = 'placeholder';
}
$dao->created_date = \CRM_Utils_Time::getTime('YmdHis');
$session = \CRM_Core_Session::singleton();
if (\CRM_Contact_BAO_Contact_Utils::isContactId($session->get('userID'))) {
$dao->created_id = $session->get('userID');
}
if ($dao->id) {
$dao->save();
}
else {
// Cannot use $dao->save(); in upgrade mode (eg WP + Civi 4.4=>4.7), the DAO will refuse
// to save the field `group_name`, which is required in older schema.
\CRM_Core_DAO::executeQuery(\CRM_Utils_SQL_Insert::dao($dao)->toSQL());
}
$dao->free();
}
}

View file

@ -0,0 +1,354 @@
<?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\Core;
/**
* Class SettingsManager
* @package Civi\Core
*
* The SettingsManager is responsible for tracking settings across various
* domains and users.
*
* Generally, for any given setting, there are three levels where values
* can be declared:
*
* - Mandatory values (which come from a global $civicrm_setting).
* - Explicit values (which are chosen by the user and stored in the DB).
* - Default values (which come from the settings metadata).
*
* Note: During the early stages of bootstrap, default values are not be available.
* Loading the defaults requires loading metadata from various sources. However,
* near the end of bootstrap, one calls SettingsManager::useDefaults() to fetch
* and merge the defaults.
*
* Note: In a typical usage, there will only be one active domain and one
* active contact (each having its own bag) within a given request. However,
* in some edge-cases, you may need to work with multiple domains/contacts
* at the same time.
*
* Note: The global $civicrm_setting is meant to provide sysadmins with a way
* to override settings in `civicrm.settings.php`, but it has traditionally been
* possible for extensions to manipulate $civicrm_setting in a hook. If you do
* this, please call `useMandatory()` to tell SettingsManager to re-scan
* $civicrm_setting.
*
* @see SettingsManagerTest
*/
class SettingsManager {
/**
* @var \CRM_Utils_Cache_Interface
*/
protected $cache;
/**
* @var
* Array (int $id => SettingsBag $bag).
*/
protected $bagsByDomain = array(), $bagsByContact = array();
/**
* @var array|NULL
* Array(string $entity => array(string $settingName => mixed $value)).
* Ex: $mandatory['domain']['uploadDir'].
* NULL means "autoload from $civicrm_setting".
*/
protected $mandatory = NULL;
/**
* Whether to use defaults.
*
* @var bool
*/
protected $useDefaults = FALSE;
/**
* @param \CRM_Utils_Cache_Interface $cache
* A semi-durable location to store metadata.
*/
public function __construct($cache) {
$this->cache = $cache;
}
/**
* Ensure that all defaults values are included with
* all current and future bags.
*
* @return SettingsManager
*/
public function useDefaults() {
if (!$this->useDefaults) {
$this->useDefaults = TRUE;
if (!empty($this->bagsByDomain)) {
foreach ($this->bagsByDomain as $bag) {
/** @var SettingsBag $bag */
$bag->loadDefaults($this->getDefaults('domain'));
}
}
if (!empty($this->bagsByContact)) {
foreach ($this->bagsByContact as $bag) {
/** @var SettingsBag $bag */
$bag->loadDefaults($this->getDefaults('contact'));
}
}
}
return $this;
}
/**
* Ensure that mandatory values are included with
* all current and future bags.
*
* If you call useMandatory multiple times, it will
* re-scan the global $civicrm_setting.
*
* @return SettingsManager
*/
public function useMandatory() {
$this->mandatory = NULL;
foreach ($this->bagsByDomain as $bag) {
/** @var SettingsBag $bag */
$bag->loadMandatory($this->getMandatory('domain'));
}
foreach ($this->bagsByContact as $bag) {
/** @var SettingsBag $bag */
$bag->loadMandatory($this->getMandatory('contact'));
}
return $this;
}
/**
* @param int|NULL $domainId
* @return SettingsBag
*/
public function getBagByDomain($domainId) {
if ($domainId === NULL) {
$domainId = \CRM_Core_Config::domainID();
}
if (!isset($this->bagsByDomain[$domainId])) {
$this->bagsByDomain[$domainId] = new SettingsBag($domainId, NULL);
if (\CRM_Core_Config::singleton()->dsn) {
$this->bagsByDomain[$domainId]->loadValues();
}
$this->bagsByDomain[$domainId]
->loadMandatory($this->getMandatory('domain'))
->loadDefaults($this->getDefaults('domain'));
}
return $this->bagsByDomain[$domainId];
}
/**
* @param int|NULL $domainId
* @param int|NULL $contactId
* @return SettingsBag
*/
public function getBagByContact($domainId, $contactId) {
if ($domainId === NULL) {
$domainId = \CRM_Core_Config::domainID();
}
$key = "$domainId:$contactId";
if (!isset($this->bagsByContact[$key])) {
$this->bagsByContact[$key] = new SettingsBag($domainId, $contactId);
if (\CRM_Core_Config::singleton()->dsn) {
$this->bagsByContact[$key]->loadValues();
}
$this->bagsByContact[$key]
->loadDefaults($this->getDefaults('contact'))
->loadMandatory($this->getMandatory('contact'));
}
return $this->bagsByContact[$key];
}
/**
* Determine the default settings.
*
* @param string $entity
* Ex: 'domain' or 'contact'.
* @return array
* Array(string $settingName => mixed $value).
*/
protected function getDefaults($entity) {
if (!$this->useDefaults) {
return self::getSystemDefaults($entity);
}
$cacheKey = 'defaults:' . $entity;
$defaults = $this->cache->get($cacheKey);
if (!is_array($defaults)) {
$specs = SettingsMetadata::getMetadata(array(
'is_contact' => ($entity === 'contact' ? 1 : 0),
));
$defaults = array();
foreach ($specs as $key => $spec) {
$defaults[$key] = \CRM_Utils_Array::value('default', $spec);
}
\CRM_Utils_Array::extend($defaults, self::getSystemDefaults($entity));
$this->cache->set($cacheKey, $defaults);
}
return $defaults;
}
/**
* Get a list of mandatory/overriden settings.
*
* @param string $entity
* Ex: 'domain' or 'contact'.
* @return array
* Array(string $settingName => mixed $value).
*/
protected function getMandatory($entity) {
if ($this->mandatory === NULL) {
$this->mandatory = self::parseMandatorySettings(\CRM_Utils_Array::value('civicrm_setting', $GLOBALS));
}
return $this->mandatory[$entity];
}
/**
* Parse mandatory settings.
*
* In previous versions, settings were broken down into verbose+dynamic group names, e.g.
*
* $civicrm_settings['Foo Bar Preferences']['foo'] = 'bar';
*
* We now simplify to two simple groups, 'domain' and 'contact'.
*
* $civicrm_settings['domain']['foo'] = 'bar';
*
* However, the old groups are grand-fathered in as aliases.
*
* @param array $civicrm_setting
* Ex: $civicrm_setting['Group Name']['field'] = 'value'.
* Group names are an historical quirk; ignore them.
* @return array
*/
public static function parseMandatorySettings($civicrm_setting) {
$result = array(
'domain' => array(),
'contact' => array(),
);
$rewriteGroups = array(
//\CRM_Core_BAO_Setting::ADDRESS_STANDARDIZATION_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::CAMPAIGN_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::CONTRIBUTE_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::DEVELOPER_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::DIRECTORY_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::EVENT_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::LOCALIZATION_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::MAP_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::MEMBER_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::MULTISITE_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::PERSONAL_PREFERENCES_NAME => 'contact',
'Personal Preferences' => 'contact',
//\CRM_Core_BAO_Setting::SEARCH_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME => 'domain',
//\CRM_Core_BAO_Setting::URL_PREFERENCES_NAME => 'domain',
'domain' => 'domain',
'contact' => 'contact',
);
if (is_array($civicrm_setting)) {
foreach ($civicrm_setting as $oldGroup => $values) {
$newGroup = isset($rewriteGroups[$oldGroup]) ? $rewriteGroups[$oldGroup] : 'domain';
$result[$newGroup] = array_merge($result[$newGroup], $values);
}
}
return $result;
}
/**
* Flush all in-memory and persistent caches related to settings.
*
* @return SettingsManager
*/
public function flush() {
$this->mandatory = NULL;
$this->cache->flush();
\Civi::cache('settings')->flush(); // SettingsMetadata; not guaranteed to use same cache.
foreach ($this->bagsByDomain as $bag) {
/** @var SettingsBag $bag */
$bag->loadValues();
$bag->loadDefaults($this->getDefaults('domain'));
$bag->loadMandatory($this->getMandatory('domain'));
}
foreach ($this->bagsByContact as $bag) {
/** @var SettingsBag $bag */
$bag->loadValues();
$bag->loadDefaults($this->getDefaults('contact'));
$bag->loadMandatory($this->getMandatory('contact'));
}
return $this;
}
/**
* Get a list of critical system defaults.
*
* The setting system can be modified by extensions, which means that it's not fully available
* during bootstrap -- in particular, defaults cannot be loaded. For a very small number of settings,
* we must define defaults before the system bootstraps.
*
* @param string $entity
*
* @return array
*/
private static function getSystemDefaults($entity) {
$defaults = array();
switch ($entity) {
case 'domain':
$defaults = array(
'installed' => FALSE,
'enable_components' => array('CiviEvent', 'CiviContribute', 'CiviMember', 'CiviMail', 'CiviReport', 'CiviPledge'),
'customFileUploadDir' => '[civicrm.files]/custom/',
'imageUploadDir' => '[civicrm.files]/persist/contribute/',
'uploadDir' => '[civicrm.files]/upload/',
'imageUploadURL' => '[civicrm.files]/persist/contribute/',
'extensionsDir' => '[civicrm.files]/ext/',
'extensionsURL' => '[civicrm.files]/ext/',
'resourceBase' => '[civicrm.root]/',
'userFrameworkResourceURL' => '[civicrm.root]/',
);
break;
}
return $defaults;
}
}

View file

@ -0,0 +1,165 @@
<?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\Core;
/**
* Class SettingsMetadata
* @package Civi\Core
*/
class SettingsMetadata {
const ALL = 'all';
/**
* WARNING: This interface may change.
*
* This provides information about the setting - similar to the fields concept for DAO information.
* As the setting is serialized code creating validation setting input needs to know the data type
* This also helps move information out of the form layer into the data layer where people can interact with
* it via the API or other mechanisms. In order to keep this consistent it is important the form layer
* also leverages it.
*
* Note that this function should never be called when using the runtime getvalue function. Caching works
* around the expectation it will be called during setting administration
*
* Function is intended for configuration rather than runtime access to settings
*
* The following params will filter the result. If none are passed all settings will be returns
*
* @param array $filters
* @param int $domainID
*
* @return array
* the following information as appropriate for each setting
* - name
* - type
* - default
* - add (CiviCRM version added)
* - is_domain
* - is_contact
* - description
* - help_text
*/
public static function getMetadata($filters = array(), $domainID = NULL) {
if ($domainID === NULL) {
$domainID = \CRM_Core_Config::domainID();
}
$cache = \Civi::cache('settings');
$cacheString = 'settingsMetadata_' . $domainID . '_';
// the caching into 'All' seems to be a duplicate of caching to
// settingsMetadata__ - I think the reason was to cache all settings as defined & then those altered by a hook
$settingsMetadata = $cache->get($cacheString);
$cached = is_array($settingsMetadata);
if (!$cached) {
$settingsMetadata = $cache->get(self::ALL);
if (empty($settingsMetadata)) {
global $civicrm_root;
$metaDataFolders = array($civicrm_root . '/settings');
\CRM_Utils_Hook::alterSettingsFolders($metaDataFolders);
$settingsMetadata = self::loadSettingsMetaDataFolders($metaDataFolders);
$cache->set(self::ALL, $settingsMetadata);
}
}
\CRM_Utils_Hook::alterSettingsMetaData($settingsMetadata, $domainID, NULL);
if (!$cached) {
$cache->set($cacheString, $settingsMetadata);
}
self::_filterSettingsSpecification($filters, $settingsMetadata);
return $settingsMetadata;
}
/**
* Load the settings files defined in a series of folders.
* @param array $metaDataFolders
* List of folder paths.
* @return array
*/
protected static function loadSettingsMetaDataFolders($metaDataFolders) {
$settingsMetadata = array();
$loadedFolders = array();
foreach ($metaDataFolders as $metaDataFolder) {
$realFolder = realpath($metaDataFolder);
if (is_dir($realFolder) && !isset($loadedFolders[$realFolder])) {
$loadedFolders[$realFolder] = TRUE;
$settingsMetadata = $settingsMetadata + self::loadSettingsMetadata($metaDataFolder);
}
}
return $settingsMetadata;
}
/**
* Load up settings metadata from files.
*
* @param array $metaDataFolder
*
* @return array
*/
protected static function loadSettingsMetadata($metaDataFolder) {
$settingMetaData = array();
$settingsFiles = \CRM_Utils_File::findFiles($metaDataFolder, '*.setting.php');
foreach ($settingsFiles as $file) {
$settings = include $file;
$settingMetaData = array_merge($settingMetaData, $settings);
}
return $settingMetaData;
}
/**
* Filter the settings metadata according to filters passed in. This is a convenience filter
* and allows selective reverting / filling of settings
*
* @param array $filters
* Filters to match against data.
* @param array $settingSpec
* Metadata to filter.
*/
protected static function _filterSettingsSpecification($filters, &$settingSpec) {
if (empty($filters)) {
return;
}
elseif (array_keys($filters) == array('name')) {
$settingSpec = array($filters['name'] => \CRM_Utils_Array::value($filters['name'], $settingSpec, ''));
return;
}
else {
foreach ($settingSpec as $field => $fieldValues) {
if (array_intersect_assoc($fieldValues, $filters) != $filters) {
unset($settingSpec[$field]);
}
}
return;
}
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Civi\Core;
/**
* Class SettingsStack
*
* The settings stack allows you to temporarily change (then restore) settings. It's intended
* primarily for use in testing.
*
* Like the global `$civicrm_setting` variable, it works best with typical inert settings that
* do not trigger extra activation logic. A handful of settings (such as `enable_components`
* and ~5 others) should be avoided, but most settings should work.
*
* @package Civi\Core
*/
class SettingsStack {
/**
* @var array
* Ex: $stack[0] == ['settingName', 'oldSettingValue'];
*/
protected $stack = array();
/**
* Temporarily apply a setting.
*
* @param $settingValue
* @param $setting
*/
public function push($setting, $settingValue) {
if (isset($GLOBALS['civicrm_setting']['domain'][$setting])) {
$this->stack[] = array($setting, $GLOBALS['civicrm_setting']['domain'][$setting]);
}
else {
$this->stack[] = array($setting, NULL);
}
$GLOBALS['civicrm_setting']['domain'][$setting] = $settingValue;
\Civi::service('settings_manager')->useMandatory();
}
/**
* Restore original settings.
*/
public function popAll() {
while ($frame = array_pop($this->stack)) {
list($setting, $value) = $frame;
if ($value === NULL) {
unset($GLOBALS['civicrm_setting']['domain'][$setting]);
}
else {
$GLOBALS['civicrm_setting']['domain'][$setting] = $value;
}
}
\Civi::service('settings_manager')->useMandatory();
}
}

View file

@ -0,0 +1,127 @@
<?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\Core\SqlTrigger;
/**
* Build a set of simple, literal SQL triggers.
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
*/
class StaticTriggers {
/**
* @var array
* A list of triggers, in the same format as hook_civicrm_triggerInfo.
* Additionally, you may specify `upgrade_check` to ensure that the trigger
* is *not* installed during early upgrade steps (before key dependencies are met).
*
* Ex: $triggers[0]['upgrade_check'] = array('table' => 'civicrm_case', 'column'=> 'modified_date');
*
* @see \CRM_Utils_Hook::triggerInfo
*/
private $triggers;
/**
* StaticTriggers constructor.
* @param $triggers
*/
public function __construct($triggers) {
$this->triggers = $triggers;
}
/**
* Add our list of triggers to the global list.
*
* @param \Civi\Core\Event\GenericHookEvent $e
* @see \CRM_Utils_Hook::triggerInfo
*/
public function onTriggerInfo($e) {
$this->alterTriggerInfo($e->info, $e->tableName);
}
/**
* Add our list of triggers to the global list.
*
* @see \CRM_Utils_Hook::triggerInfo
* @see \CRM_Core_DAO::triggerRebuild
*
* @param array $info
* See hook_civicrm_triggerInfo.
* @param string|NULL $tableFilter
* See hook_civicrm_triggerInfo.
*/
public function alterTriggerInfo(&$info, $tableFilter = NULL) {
foreach ($this->getTriggers() as $trigger) {
if ($tableFilter !== NULL) {
// Because sadism.
if (in_array($tableFilter, (array) $trigger['table'])) {
$trigger['table'] = $tableFilter;
}
}
if (\CRM_Core_Config::isUpgradeMode() && isset($trigger['upgrade_check'])) {
$uc = $trigger['upgrade_check'];
if (!\CRM_Core_DAO::checkFieldExists($uc['table'], $uc['column'])
) {
continue;
}
}
unset($trigger['upgrade_check']);
$info[] = $trigger;
}
}
/**
* @return mixed
*/
public function getTriggers() {
return $this->triggers;
}
/**
* @param mixed $triggers
* @return StaticTriggers
*/
public function setTriggers($triggers) {
$this->triggers = $triggers;
return $this;
}
/**
* @param $trigger
* @return StaticTriggers
*/
public function addTrigger($trigger) {
$this->triggers[] = $trigger;
return $this;
}
}

View file

@ -0,0 +1,334 @@
<?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\Core\SqlTrigger;
use Civi\Core\Event\GenericHookEvent;
/**
* Build a set of SQL triggers for tracking timestamps on an entity.
*
* This class is a generalization of CRM-10554 with the aim of enabling CRM-20958.
*
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2017
*/
class TimestampTriggers {
/**
* @var string
* SQL table name.
* Ex: 'civicrm_contact', 'civicrm_activity'.
*/
private $tableName;
/**
* @var string
* An entity name (from civicrm_custom_group.extends).
* Ex: 'Contact', 'Activity'.
*/
private $customDataEntity;
/**
* @var string
* SQL column name.
* Ex: 'created_date'.
*/
private $createdDate;
/**
* @var string
* SQL column name.
* Ex: 'modified_date'.
*/
private $modifiedDate;
/**
* @var array
* Ex: $relations[0] == array('table' => 'civicrm_bar', 'column' => 'foo_id');
*/
private $relations;
/**
* @param string $tableName
* SQL table name.
* Ex: 'civicrm_contact', 'civicrm_activity'.
* @param string $customDataEntity
* An entity name (from civicrm_custom_group.extends).
* Ex: 'Contact', 'Activity'.
* @return TimestampTriggers
*/
public static function create($tableName, $customDataEntity) {
return new static($tableName, $customDataEntity);
}
/**
* TimestampTriggers constructor.
*
* @param string $tableName
* SQL table name.
* Ex: 'civicrm_contact', 'civicrm_activity'.
* @param string $customDataEntity
* An entity name (from civicrm_custom_group.extends).
* Ex: 'Contact', 'Activity'.
* @param string $createdDate
* SQL column name.
* Ex: 'created_date'.
* @param string $modifiedDate
* SQL column name.
* Ex: 'modified_date'.
* @param array $relations
* Ex: $relations[0] == array('table' => 'civicrm_bar', 'column' => 'foo_id');
*/
public function __construct(
$tableName,
$customDataEntity,
$createdDate = 'created_date',
$modifiedDate = 'modified_date',
$relations = array()
) {
$this->tableName = $tableName;
$this->customDataEntity = $customDataEntity;
$this->createdDate = $createdDate;
$this->modifiedDate = $modifiedDate;
$this->relations = $relations;
}
/**
* Add our list of triggers to the global list.
*
* @param \Civi\Core\Event\GenericHookEvent $e
* @see \CRM_Utils_Hook::triggerInfo
*/
public function onTriggerInfo($e) {
$this->alterTriggerInfo($e->info, $e->tableName);
}
/**
* Add our list of triggers to the global list.
*
* @see \CRM_Utils_Hook::triggerInfo
* @see \CRM_Core_DAO::triggerRebuild
*
* @param array $info
* See hook_civicrm_triggerInfo.
* @param string|NULL $tableFilter
* See hook_civicrm_triggerInfo.
*/
public function alterTriggerInfo(&$info, $tableFilter = NULL) {
// If we haven't upgraded yet, then the created_date/modified_date may not exist.
// In the past, this was a version-based check, but checkFieldExists()
// seems more robust.
if (\CRM_Core_Config::isUpgradeMode()) {
if (!\CRM_Core_DAO::checkFieldExists($this->getTableName(),
$this->getCreatedDate())
) {
return;
}
}
if ($tableFilter == NULL || $tableFilter == $this->getTableName()) {
$info[] = array(
'table' => array($this->getTableName()),
'when' => 'BEFORE',
'event' => array('INSERT'),
'sql' => "\nSET NEW.{$this->getCreatedDate()} = CURRENT_TIMESTAMP;\n",
);
}
// Update timestamp when modifying closely related tables
$relIdx = \CRM_Utils_Array::index(
array('column', 'table'),
$this->getAllRelations()
);
foreach ($relIdx as $column => $someRelations) {
$this->generateTimestampTriggers($info, $tableFilter,
array_keys($someRelations), $column);
}
}
/**
* Generate triggers to update the timestamp.
*
* The corresponding civicrm_FOO row is updated on insert/update/delete
* to a table that extends civicrm_FOO.
* Don't regenerate triggers for all such tables if only asked for one table.
*
* @param array $info
* Reference to the array where generated trigger information is being stored
* @param string|null $tableFilter
* Name of the table for which triggers are being generated, or NULL if all tables
* @param array $relatedTableNames
* Array of all core or all custom table names extending civicrm_FOO
* @param string $contactRefColumn
* 'contact_id' if processing core tables, 'entity_id' if processing custom tables
*
* @link https://issues.civicrm.org/jira/browse/CRM-15602
* @see triggerInfo
*/
public function generateTimestampTriggers(
&$info,
$tableFilter,
$relatedTableNames,
$contactRefColumn
) {
// Safety
$contactRefColumn = \CRM_Core_DAO::escapeString($contactRefColumn);
// If specific related table requested, just process that one.
// (Reply: This feels fishy.)
if (in_array($tableFilter, $relatedTableNames)) {
$relatedTableNames = array($tableFilter);
}
// If no specific table requested (include all related tables),
// or a specific related table requested (as matched above)
if (empty($tableFilter) || isset($relatedTableNames[$tableFilter])) {
$info[] = array(
'table' => $relatedTableNames,
'when' => 'AFTER',
'event' => array('INSERT', 'UPDATE'),
'sql' => "\nUPDATE {$this->getTableName()} SET {$this->getModifiedDate()} = CURRENT_TIMESTAMP WHERE id = NEW.$contactRefColumn;\n",
);
$info[] = array(
'table' => $relatedTableNames,
'when' => 'AFTER',
'event' => array('DELETE'),
'sql' => "\nUPDATE {$this->getTableName()} SET {$this->getModifiedDate()} = CURRENT_TIMESTAMP WHERE id = OLD.$contactRefColumn;\n",
);
}
}
/**
* @return string
*/
public function getTableName() {
return $this->tableName;
}
/**
* @param string $tableName
* @return TimestampTriggers
*/
public function setTableName($tableName) {
$this->tableName = $tableName;
return $this;
}
/**
* @return string
*/
public function getCustomDataEntity() {
return $this->customDataEntity;
}
/**
* @param string $customDataEntity
* @return TimestampTriggers
*/
public function setCustomDataEntity($customDataEntity) {
$this->customDataEntity = $customDataEntity;
return $this;
}
/**
* @return string
*/
public function getCreatedDate() {
return $this->createdDate;
}
/**
* @param string $createdDate
* @return TimestampTriggers
*/
public function setCreatedDate($createdDate) {
$this->createdDate = $createdDate;
return $this;
}
/**
* @return string
*/
public function getModifiedDate() {
return $this->modifiedDate;
}
/**
* @param string $modifiedDate
* @return TimestampTriggers
*/
public function setModifiedDate($modifiedDate) {
$this->modifiedDate = $modifiedDate;
return $this;
}
/**
* @return array
* Each item is an array('table' => string, 'column' => string)
*/
public function getRelations() {
return $this->relations;
}
/**
* @param array $relations
* @return TimestampTriggers
*/
public function setRelations($relations) {
$this->relations = $relations;
return $this;
}
/**
* Get a list of all tracked relations.
*
* This is basically the curated list (`$this->relations`) plus any custom data.
*
* @return array
* Each item is an array('table' => string, 'column' => string)
*/
public function getAllRelations() {
$relations = $this->getRelations();
if ($this->getCustomDataEntity()) {
$customGroupDAO = \CRM_Core_BAO_CustomGroup::getAllCustomGroupsByBaseEntity($this->getCustomDataEntity());
$customGroupDAO->is_multiple = 0;
$customGroupDAO->find();
while ($customGroupDAO->fetch()) {
$relations[] = array(
'table' => $customGroupDAO->table_name,
'column' => 'entity_id',
);
}
}
return $relations;
}
}

View file

@ -0,0 +1,241 @@
<?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\Core;
/**
* Class SqlTriggers
* @package Civi\Core
*
* This class manages creation and destruction of SQL triggers.
*/
class SqlTriggers {
/**
* The name of the output file.
*
* @var string|NULL
*/
private $file = NULL;
/**
* Build a list of triggers via hook and add them to (err, reconcile them
* with) the database.
*
* @param string $tableName
* the specific table requiring a rebuild; or NULL to rebuild all tables.
* @param bool $force
*
* @see CRM-9716
*/
public function rebuild($tableName = NULL, $force = FALSE) {
$info = array();
$logging = new \CRM_Logging_Schema();
$logging->triggerInfo($info, $tableName, $force);
\CRM_Core_I18n_Schema::triggerInfo($info, $tableName);
\CRM_Contact_BAO_Contact::triggerInfo($info, $tableName);
\CRM_Utils_Hook::triggerInfo($info, $tableName);
// drop all existing triggers on all tables
$logging->dropTriggers($tableName);
// now create the set of new triggers
$this->createTriggers($info, $tableName);
}
/**
* @param array $info
* per hook_civicrm_triggerInfo.
* @param string $onlyTableName
* the specific table requiring a rebuild; or NULL to rebuild all tables.
*/
public function createTriggers(&$info, $onlyTableName = NULL) {
// Validate info array, should probably raise errors?
if (is_array($info) == FALSE) {
return;
}
$triggers = array();
// now enumerate the tables and the events and collect the same set in a different format
foreach ($info as $value) {
// clean the incoming data, skip malformed entries
// TODO: malformed entries should raise errors or get logged.
if (isset($value['table']) == FALSE ||
isset($value['event']) == FALSE ||
isset($value['when']) == FALSE ||
isset($value['sql']) == FALSE
) {
continue;
}
if (is_string($value['table']) == TRUE) {
$tables = array($value['table']);
}
else {
$tables = $value['table'];
}
if (is_string($value['event']) == TRUE) {
$events = array(strtolower($value['event']));
}
else {
$events = array_map('strtolower', $value['event']);
}
$whenName = strtolower($value['when']);
foreach ($tables as $tableName) {
if (!isset($triggers[$tableName])) {
$triggers[$tableName] = array();
}
foreach ($events as $eventName) {
$template_params = array('{tableName}', '{eventName}');
$template_values = array($tableName, $eventName);
$sql = str_replace($template_params,
$template_values,
$value['sql']
);
$variables = str_replace($template_params,
$template_values,
\CRM_Utils_Array::value('variables', $value)
);
if (!isset($triggers[$tableName][$eventName])) {
$triggers[$tableName][$eventName] = array();
}
if (!isset($triggers[$tableName][$eventName][$whenName])) {
// We're leaving out cursors, conditions, and handlers for now
// they are kind of dangerous in this context anyway
// better off putting them in stored procedures
$triggers[$tableName][$eventName][$whenName] = array(
'variables' => array(),
'sql' => array(),
);
}
if ($variables) {
$triggers[$tableName][$eventName][$whenName]['variables'][] = $variables;
}
$triggers[$tableName][$eventName][$whenName]['sql'][] = $sql;
}
}
}
// now spit out the sql
foreach ($triggers as $tableName => $tables) {
if ($onlyTableName != NULL && $onlyTableName != $tableName) {
continue;
}
foreach ($tables as $eventName => $events) {
foreach ($events as $whenName => $parts) {
$varString = implode("\n", $parts['variables']);
$sqlString = implode("\n", $parts['sql']);
$validName = \CRM_Core_DAO::shortenSQLName($tableName, 48, TRUE);
$triggerName = "{$validName}_{$whenName}_{$eventName}";
$triggerSQL = "CREATE TRIGGER $triggerName $whenName $eventName ON $tableName FOR EACH ROW BEGIN $varString $sqlString END";
$this->enqueueQuery("DROP TRIGGER IF EXISTS $triggerName");
$this->enqueueQuery($triggerSQL);
}
}
}
}
/**
* Wrapper function to drop triggers.
*
* @param string $tableName
* the specific table requiring a rebuild; or NULL to rebuild all tables.
*/
public function dropTriggers($tableName = NULL) {
$info = array();
$logging = new \CRM_Logging_Schema();
$logging->triggerInfo($info, $tableName);
// drop all existing triggers on all tables
$logging->dropTriggers($tableName);
}
/**
* Enqueue a query which alters triggers.
*
* As this requires a high permission level we funnel the queries through here to
* facilitate them being taken 'offline'.
*
* @param string $triggerSQL
* The sql to run to create or drop the triggers.
* @param array $params
* Optional parameters to interpolate into the string.
*/
public function enqueueQuery($triggerSQL, $params = array()) {
if (\Civi::settings()->get('logging_no_trigger_permission')) {
if (!file_exists($this->getFile())) {
// Ugh. Need to let user know somehow. This is the first change.
\CRM_Core_Session::setStatus(ts('The mysql commands you need to run are stored in %1', array(
1 => $this->getFile(),
)),
'',
'alert',
array('expires' => 0)
);
}
$buf = "\n";
$buf .= "DELIMITER //\n";
$buf .= \CRM_Core_DAO::composeQuery($triggerSQL, $params) . " //\n";
$buf .= "DELIMITER ;\n";
file_put_contents($this->getFile(), $buf, FILE_APPEND);
}
else {
\CRM_Core_DAO::executeQuery($triggerSQL, $params, TRUE, NULL, FALSE, FALSE);
}
}
/**
* @return NULL|string
*/
public function getFile() {
if ($this->file === NULL) {
$prefix = 'trigger' . \CRM_Utils_Request::id();
$config = \CRM_Core_Config::singleton();
$this->file = "{$config->configAndLogDir}CiviCRM." . $prefix . md5($config->dsn) . '.sql';
}
return $this->file;
}
}

View file

@ -0,0 +1,213 @@
<?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\Core\Transaction;
/**
* A "frame" is a layer in a series of nested transactions. Generally,
* the outermost frame is a normal SQL transaction (BEGIN/ROLLBACK/COMMIT)
* and any nested frames are SQL savepoints (SAVEPOINT foo/ROLLBACK TO SAVEPOINT).
*
* @package Civi
* @copyright CiviCRM LLC (c) 2004-2017
*/
class Frame {
const F_NEW = 0, F_ACTIVE = 1, F_DONE = 2, F_FORCED = 3;
/**
* @var \CRM_Core_DAO
*/
private $dao;
/**
* @var string|null e.g. "BEGIN" or "SAVEPOINT foo"
*/
private $beginStmt;
/**
* @var string|null e.g. "COMMIT"
*/
private $commitStmt;
/**
* @var string|null e.g. "ROLLBACK" or "ROLLBACK TO SAVEPOINT foo"
*/
private $rollbackStmt;
/**
* @var int
*/
private $refCount = 0;
private $callbacks;
private $doCommit = TRUE;
/**
* @var int
*/
private $state = self::F_NEW;
/**
* @param \CRM_Core_DAO $dao
* @param string|null $beginStmt e.g. "BEGIN" or "SAVEPOINT foo"
* @param string|null $commitStmt e.g. "COMMIT"
* @param string|null $rollbackStmt e.g. "ROLLBACK" or "ROLLBACK TO SAVEPOINT foo"
*/
public function __construct($dao, $beginStmt, $commitStmt, $rollbackStmt) {
$this->dao = $dao;
$this->beginStmt = $beginStmt;
$this->commitStmt = $commitStmt;
$this->rollbackStmt = $rollbackStmt;
$this->callbacks = array(
\CRM_Core_Transaction::PHASE_PRE_COMMIT => array(),
\CRM_Core_Transaction::PHASE_POST_COMMIT => array(),
\CRM_Core_Transaction::PHASE_PRE_ROLLBACK => array(),
\CRM_Core_Transaction::PHASE_POST_ROLLBACK => array(),
);
}
public function inc() {
$this->refCount++;
}
public function dec() {
$this->refCount--;
}
/**
* @return bool
*/
public function isEmpty() {
return ($this->refCount == 0);
}
/**
* @return bool
*/
public function isRollbackOnly() {
return !$this->doCommit;
}
public function setRollbackOnly() {
$this->doCommit = FALSE;
}
/**
* Begin frame processing.
*
* @throws \CRM_Core_Exception
*/
public function begin() {
if ($this->state !== self::F_NEW) {
throw new \CRM_Core_Exception('State is not F_NEW');
};
$this->state = self::F_ACTIVE;
if ($this->beginStmt) {
$this->dao->query($this->beginStmt);
}
}
/**
* Finish frame processing.
*
* @param int $newState
*
* @throws \CRM_Core_Exception
*/
public function finish($newState = self::F_DONE) {
if ($this->state == self::F_FORCED) {
return;
}
if ($this->state !== self::F_ACTIVE) {
throw new \CRM_Core_Exception('State is not F_ACTIVE');
};
$this->state = $newState;
if ($this->doCommit) {
$this->invokeCallbacks(\CRM_Core_Transaction::PHASE_PRE_COMMIT);
if ($this->commitStmt) {
$this->dao->query($this->commitStmt);
}
$this->invokeCallbacks(\CRM_Core_Transaction::PHASE_POST_COMMIT);
}
else {
$this->invokeCallbacks(\CRM_Core_Transaction::PHASE_PRE_ROLLBACK);
if ($this->rollbackStmt) {
$this->dao->query($this->rollbackStmt);
}
$this->invokeCallbacks(\CRM_Core_Transaction::PHASE_POST_ROLLBACK);
}
}
public function forceRollback() {
$this->setRollbackOnly();
$this->finish(self::F_FORCED);
}
/**
* Add a transaction callback.
*
* Pre-condition: isActive()
*
* @param int $phase
* A constant; one of: self::PHASE_{PRE,POST}_{COMMIT,ROLLBACK}.
* @param mixed $callback
* A PHP callback.
* @param array|NULL $params Optional values to pass to callback.
* See php manual call_user_func_array for details.
* @param null $id
*/
public function addCallback($phase, $callback, $params = NULL, $id = NULL) {
if ($id) {
$this->callbacks[$phase][$id] = array(
'callback' => $callback,
'parameters' => (is_array($params) ? $params : array($params)),
);
}
else {
$this->callbacks[$phase][] = array(
'callback' => $callback,
'parameters' => (is_array($params) ? $params : array($params)),
);
}
}
/**
* @param int $phase
*/
public function invokeCallbacks($phase) {
if (is_array($this->callbacks[$phase])) {
foreach ($this->callbacks[$phase] as $cb) {
call_user_func_array($cb['callback'], $cb['parameters']);
}
}
}
}

View file

@ -0,0 +1,177 @@
<?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\Core\Transaction;
/**
*
* @package Civi
* @copyright CiviCRM LLC (c) 2004-2017
*/
class Manager {
private static $singleton = NULL;
/**
* @var \CRM_Core_DAO
*/
private $dao;
/**
* @var array<Frame> stack of SQL transactions/savepoints
*/
private $frames = array();
/**
* @var int
*/
private $savePointCount = 0;
/**
* @param bool $fresh
* @return Manager
*/
public static function singleton($fresh = FALSE) {
if (NULL === self::$singleton || $fresh) {
self::$singleton = new Manager(new \CRM_Core_DAO());
}
return self::$singleton;
}
/**
* @param \CRM_Core_DAO $dao
* Handle for the DB connection that will execute transaction statements.
* (all we really care about is the query() function)
*/
public function __construct($dao) {
$this->dao = $dao;
}
/**
* Increment the transaction count / add a new transaction level
*
* @param bool $nest
* Determines what to do if there's currently an active transaction:.
* - If true, then make a new nested transaction ("SAVEPOINT")
* - If false, then attach to the existing transaction
*/
public function inc($nest = FALSE) {
if (!isset($this->frames[0])) {
$frame = $this->createBaseFrame();
array_unshift($this->frames, $frame);
$frame->inc();
$frame->begin();
}
elseif ($nest) {
$frame = $this->createSavePoint();
array_unshift($this->frames, $frame);
$frame->inc();
$frame->begin();
}
else {
$this->frames[0]->inc();
}
}
/**
* Decrement the transaction count / close out a transaction level
*
* @throws \CRM_Core_Exception
*/
public function dec() {
if (!isset($this->frames[0]) || $this->frames[0]->isEmpty()) {
throw new \CRM_Core_Exception('Transaction integrity error: Expected to find active frame');
}
$this->frames[0]->dec();
if ($this->frames[0]->isEmpty()) {
// Callbacks may cause additional work (such as new transactions),
// and it would be confusing if the old frame was still active.
// De-register it before calling finish().
$oldFrame = array_shift($this->frames);
$oldFrame->finish();
}
}
/**
* Force an immediate rollback, regardless of how many
* transaction or frame objects exist.
*
* This is only appropriate when it is _certain_ that the
* callstack will not wind-down normally -- e.g. before
* a call to exit().
*/
public function forceRollback() {
// we take the long-way-round (rolling back each frame) so that the
// internal state of each frame is consistent with its outcome
$oldFrames = $this->frames;
$this->frames = array();
foreach ($oldFrames as $oldFrame) {
$oldFrame->forceRollback();
}
}
/**
* Get the (innermost) SQL transaction.
*
* @return \Civi\Core\Transaction\Frame
*/
public function getFrame() {
return isset($this->frames[0]) ? $this->frames[0] : NULL;
}
/**
* Get the (outermost) SQL transaction (i.e. the one
* demarcated by BEGIN/COMMIT/ROLLBACK)
*
* @return \Civi\Core\Transaction\Frame
*/
public function getBaseFrame() {
if (empty($this->frames)) {
return NULL;
}
return $this->frames[count($this->frames) - 1];
}
/**
* @return \Civi\Core\Transaction\Frame
*/
protected function createBaseFrame() {
return new Frame($this->dao, 'BEGIN', 'COMMIT', 'ROLLBACK');
}
/**
* @return \Civi\Core\Transaction\Frame
*/
protected function createSavePoint() {
$spId = $this->savePointCount++;
return new Frame($this->dao, "SAVEPOINT civi_{$spId}", NULL, "ROLLBACK TO SAVEPOINT civi_{$spId}");
}
}

View file

@ -0,0 +1,566 @@
<?php
namespace Civi\Install;
/**
* Class Requirements
* @package Civi\Install
*/
class Requirements {
/**
* Requirement severity -- Requirement successfully met.
*/
const REQUIREMENT_OK = 0;
/**
* Requirement severity -- Warning condition; proceed but flag warning.
*/
const REQUIREMENT_WARNING = 1;
/**
* Requirement severity -- Error condition; abort installation.
*/
const REQUIREMENT_ERROR = 2;
protected $system_checks = array(
'checkMemory',
'checkServerVariables',
'checkMysqlConnectExists',
'checkJsonEncodeExists',
);
protected $database_checks = array(
'checkMysqlConnection',
'checkMysqlVersion',
'checkMysqlInnodb',
'checkMysqlTempTables',
'checkMySQLAutoIncrementIncrementOne',
'checkMysqlTrigger',
'checkMysqlThreadStack',
'checkMysqlLockTables',
);
/**
* Run all requirements tests.
*
* @param array $config
* An array with two keys:
* - file_paths
* - db_config
*
* @return array
* An array of check summaries. Each array contains the keys 'title', 'severity', and 'details'.
*/
public function checkAll(array $config) {
return array_merge($this->checkSystem($config['file_paths']), $this->checkDatabase($config['db_config']));
}
/**
* Check system requirements are met, such as sufficient memory,
* necessary file paths are writable and required php extensions
* are available.
*
* @param array $file_paths
* An array of file paths that will be checked to confirm they
* are writable.
*
* @return array
*/
public function checkSystem(array $file_paths) {
$errors = array();
$errors[] = $this->checkFilepathIsWritable($file_paths);
foreach ($this->system_checks as $check) {
$errors[] = $this->$check();
}
return $errors;
}
/**
* Check database connection, database version and other
* database requirements are met.
*
* @param array $db_config
* An array with keys:
* - host (with optional port specified eg. localhost:12345)
* - database (name of database to select)
* - username
* - password
*
* @return array
*/
public function checkDatabase(array $db_config) {
$errors = array();
foreach ($this->database_checks as $check) {
$errors[] = $this->$check($db_config);
}
return $errors;
}
/**
* Generates a mysql connection
*
* @param $db_confic array
* @return object mysqli connection
*/
protected function connect($db_config) {
$host = NULL;
if (!empty($db_config['host'])) {
$host = $db_config['host'];
}
elseif (!empty($db_config['server'])) {
$host = $db_config['server'];
}
$conn = @mysqli_connect($host, $db_config['username'], $db_config['password'], $db_config['database'], !empty($db_config['port']) ? $db_config['port'] : NULL);
return $conn;
}
/**
* Check configured php Memory.
* @return array
*/
public function checkMemory() {
$min = 1024 * 1024 * 32;
$recommended = 1024 * 1024 * 64;
$mem = $this->getPHPMemory();
$mem_string = ini_get('memory_limit');
$results = array(
'title' => 'CiviCRM memory check',
'severity' => $this::REQUIREMENT_OK,
'details' => "You have $mem_string allocated (minimum 32Mb, recommended 64Mb)",
);
if ($mem < $min && $mem > 0) {
$results['severity'] = $this::REQUIREMENT_ERROR;
}
elseif ($mem < $recommended && $mem != 0) {
$results['severity'] = $this::REQUIREMENT_WARNING;
}
elseif ($mem == 0) {
$results['details'] = "Cannot determine PHP memory allocation. Install only if you're sure you've allocated at least 32 MB.";
$results['severity'] = $this::REQUIREMENT_WARNING;
}
return $results;
}
/**
* Get Configured PHP memory.
* @return float
*/
protected function getPHPMemory() {
$memString = ini_get("memory_limit");
switch (strtolower(substr($memString, -1))) {
case "k":
return round(substr($memString, 0, -1) * 1024);
case "m":
return round(substr($memString, 0, -1) * 1024 * 1024);
case "g":
return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
default:
return round($memString);
}
}
/**
* @return array
*/
public function checkServerVariables() {
$results = array(
'title' => 'CiviCRM PHP server variables',
'severity' => $this::REQUIREMENT_OK,
'details' => 'The required $_SERVER variables are set',
);
$required_variables = array('SCRIPT_NAME', 'HTTP_HOST', 'SCRIPT_FILENAME');
$missing = array();
foreach ($required_variables as $required_variable) {
if (empty($_SERVER[$required_variable])) {
$missing[] = '$_SERVER[' . $required_variable . ']';
}
}
if ($missing) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'The following PHP variables are not set: ' . implode(', ', $missing);
}
return $results;
}
/**
* @return array
*/
public function checkJsonEncodeExists() {
$results = array(
'title' => 'CiviCRM JSON encoding support',
'severity' => $this::REQUIREMENT_OK,
'details' => 'Function json_encode() found',
);
if (!function_exists('json_encode')) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Function json_encode() does not exist';
}
return $results;
}
/**
* @return array
*/
public function checkMysqlConnectExists() {
$results = array(
'title' => 'CiviCRM MySQL check',
'severity' => $this::REQUIREMENT_OK,
'details' => 'Function mysqli_connect() found',
);
if (!function_exists('mysqli_connect')) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Function mysqli_connect() does not exist';
}
return $results;
}
/**
* @param array $db_config
*
* @return array
*/
public function checkMysqlConnection(array $db_config) {
$results = array(
'title' => 'CiviCRM MySQL connection',
'severity' => $this::REQUIREMENT_OK,
'details' => "Connected",
);
$conn = $this->connect($db_config);
if (!$conn) {
$results['details'] = mysqli_connect_error();
$results['severity'] = $this::REQUIREMENT_ERROR;
return $results;
}
if (!@mysqli_select_db($conn, $db_config['database'])) {
$results['details'] = mysqli_error($conn);
$results['severity'] = $this::REQUIREMENT_ERROR;
return $results;
}
return $results;
}
/**
* @param array $db_config
*
* @return array
*/
public function checkMysqlVersion(array $db_config) {
$min = '5.1';
$results = array(
'title' => 'CiviCRM MySQL Version',
'severity' => $this::REQUIREMENT_OK,
);
$conn = $this->connect($db_config);
if (!$conn || !($info = mysqli_get_server_info($conn))) {
$results['severity'] = $this::REQUIREMENT_WARNING;
$results['details'] = "Cannot determine the version of MySQL installed. Please ensure at least version {$min} is installed.";
return $results;
}
if (version_compare($info, $min) == -1) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = "MySQL version is {$info}; minimum required is {$min}";
return $results;
}
$results['details'] = "MySQL version is {$info}";
return $results;
}
/**
* @param array $db_config
*
* @return array
*/
public function checkMysqlInnodb(array $db_config) {
$results = array(
'title' => 'CiviCRM InnoDB support',
'severity' => $this::REQUIREMENT_ERROR,
'details' => 'Could not determine if MySQL has InnoDB support. Assuming none.',
);
$conn = $this->connect($db_config);
if (!$conn) {
return $results;
}
$innodb_support = FALSE;
$result = mysqli_query($conn, "SHOW ENGINES");
while ($values = mysqli_fetch_array($result)) {
if ($values['Engine'] == 'InnoDB') {
if (strtolower($values['Support']) == 'yes' || strtolower($values['Support']) == 'default') {
$innodb_support = TRUE;
break;
}
}
}
if ($innodb_support) {
$results['severity'] = $this::REQUIREMENT_OK;
$results['details'] = 'MySQL supports InnoDB';
}
return $results;
}
/**
* @param array $db_config
*
* @return array
*/
public function checkMysqlTempTables(array $db_config) {
$results = array(
'title' => 'CiviCRM MySQL Temp Tables',
'severity' => $this::REQUIREMENT_OK,
'details' => 'MySQL server supports temporary tables',
);
$conn = $this->connect($db_config);
if (!$conn) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = "Could not connect to database";
return $results;
}
if (!@mysqli_select_db($conn, $db_config['database'])) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = "Could not select the database";
return $results;
}
$r = mysqli_query($conn, 'CREATE TEMPORARY TABLE civicrm_install_temp_table_test (test text)');
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = "Database does not support creation of temporary tables";
return $results;
}
mysqli_query($conn, 'DROP TEMPORARY TABLE civicrm_install_temp_table_test');
return $results;
}
/**
* @param $db_config
*
* @return array
*/
public function checkMysqlTrigger($db_config) {
$results = array(
'title' => 'CiviCRM MySQL Trigger',
'severity' => $this::REQUIREMENT_OK,
'details' => 'Database supports MySQL triggers',
);
$conn = $this->connect($db_config);
if (!$conn) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not connect to database';
return $results;
}
if (!@mysqli_select_db($conn, $db_config['database'])) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = "Could not select the database";
return $results;
}
$r = mysqli_query($conn, 'CREATE TABLE civicrm_install_temp_table_test (test text)');
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not create a table to run test';
return $results;
}
$r = mysqli_query($conn, 'CREATE TRIGGER civicrm_install_temp_table_test_trigger BEFORE INSERT ON civicrm_install_temp_table_test FOR EACH ROW BEGIN END');
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Database does not support creation of triggers';
}
else {
mysqli_query($conn, 'DROP TRIGGER civicrm_install_temp_table_test_trigger');
}
mysqli_query($conn, 'DROP TABLE civicrm_install_temp_table_test');
return $results;
}
/**
* @param array $db_config
*
* @return array
*/
public function checkMySQLAutoIncrementIncrementOne(array $db_config) {
$results = array(
'title' => 'CiviCRM MySQL AutoIncrementIncrement',
'severity' => $this::REQUIREMENT_OK,
'details' => 'MySQL server auto_increment_increment is 1',
);
$conn = $this->connect($db_config);
if (!$conn) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not connect to database';
return $results;
}
$r = mysqli_query($conn, "SHOW variables like 'auto_increment_increment'");
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not query database server variables';
return $results;
}
$values = mysqli_fetch_row($r);
if ($values[1] != 1) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'MySQL server auto_increment_increment is not 1';
}
return $results;
}
/**
* @param $db_config
*
* @return array
*/
public function checkMysqlThreadStack($db_config) {
$min_thread_stack = 192;
$results = array(
'title' => 'CiviCRM Mysql thread stack',
'severity' => $this::REQUIREMENT_OK,
'details' => 'MySQL thread_stack is OK',
);
$conn = $this->connect($db_config);
if (!$conn) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not connect to database';
return $results;
}
if (!@mysqli_select_db($conn, $db_config['database'])) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not select the database';
return $results;
}
$r = mysqli_query($conn, "SHOW VARIABLES LIKE 'thread_stack'"); // bytes => kb
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not query thread_stack value';
}
else {
$values = mysqli_fetch_row($r);
if ($values[1] < (1024 * $min_thread_stack)) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'MySQL thread_stack is ' . ($values[1] / 1024) . "kb (minimum required is {$min_thread_stack} kb";
}
}
return $results;
}
/**
* @param $db_config
*
* @return array
*/
public function checkMysqlLockTables($db_config) {
$results = array(
'title' => 'CiviCRM MySQL Lock Tables',
'severity' => $this::REQUIREMENT_OK,
'details' => 'Can successfully lock and unlock tables',
);
$conn = $this->connect($db_config);
if (!$conn) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not connect to database';
return $results;
}
if (!@mysqli_select_db($conn, $db_config['database'])) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not select the database';
mysqli_close($conn);
return $results;
}
$r = mysqli_query($conn, 'CREATE TEMPORARY TABLE civicrm_install_temp_table_test (test text)');
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not create a table';
mysqli_close($conn);
return $results;
}
$r = mysqli_query($conn, 'LOCK TABLES civicrm_install_temp_table_test WRITE');
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not obtain a write lock';
mysqli_close($conn);
return $results;
}
$r = mysqli_query($conn, 'UNLOCK TABLES');
if (!$r) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = 'Could not release table lock';
}
mysqli_close($conn);
return $results;
}
/**
* @param $file_paths
*
* @return array
*/
public function checkFilepathIsWritable($file_paths) {
$results = array(
'title' => 'CiviCRM directories are writable',
'severity' => $this::REQUIREMENT_OK,
'details' => 'All required directories are writable: ' . implode(', ', $file_paths),
);
$unwritable_dirs = array();
foreach ($file_paths as $path) {
if (!is_writable($path)) {
$unwritable_dirs[] = $path;
}
}
if ($unwritable_dirs) {
$results['severity'] = $this::REQUIREMENT_ERROR;
$results['details'] = "The following directories need to be made writable by the webserver: " . implode(', ', $unwritable_dirs);
}
return $results;
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace Civi\Payment\Exception;
/**
* Class PaymentProcessorException
*/
class PaymentProcessorException extends \CRM_Core_Exception {
}

View file

@ -0,0 +1,135 @@
<?php
namespace Civi\Payment;
/**
* Class System
* @package Civi\Payment
*/
class System {
/**
* @var System
*/
private static $singleton;
/**
* @var array cache
*/
private $cache = array();
/**
* @return \Civi\Payment\System
*/
public static function singleton() {
if (!self::$singleton) {
self::$singleton = new self();
}
return self::$singleton;
}
/**
* Starting from the processor as an array retrieve the processor as an object.
*
* If there is no valid configuration it will not be retrieved.
*
* @param array $processor
* @param bool $force
* Override the config check. This is required in uninstall as no valid instances exist
* but will deliberately not work with any valid processors.
*
* @return CRM_Core_Payment|NULL
*
* @throws \CRM_Core_Exception
*/
public function getByProcessor($processor, $force = FALSE) {
$id = $force ? 0 : $processor['id'];
if (!isset($this->cache[$id]) || $force) {
$ext = \CRM_Extension_System::singleton()->getMapper();
if ($ext->isExtensionKey($processor['class_name'])) {
$paymentClass = $ext->keyToClass($processor['class_name'], 'payment');
require_once $ext->classToPath($paymentClass);
}
else {
$paymentClass = 'CRM_Core_' . $processor['class_name'];
if (empty($paymentClass)) {
throw new \CRM_Core_Exception('no class provided');
}
require_once str_replace('_', DIRECTORY_SEPARATOR, $paymentClass) . '.php';
}
$processorObject = new $paymentClass(!empty($processor['is_test']) ? 'test' : 'live', $processor);
if (!$force && $processorObject->checkConfig()) {
$processorObject = NULL;
}
else {
$processorObject->setPaymentProcessor($processor);
}
$this->cache[$id] = $processorObject;
}
return $this->cache[$id];
}
/**
* Get payment processor by it's ID.
*
* @param int $id
*
* @return \CRM_Core_Payment|NULL
* @throws \CiviCRM_API3_Exception
*/
public function getById($id) {
if ($id == 0) {
return new \CRM_Core_Payment_Manual();
}
$processor = civicrm_api3('payment_processor', 'getsingle', array('id' => $id, 'is_test' => NULL));
return self::getByProcessor($processor);
}
/**
* @param string $name
* @param bool $is_test
*
* @return \CRM_Core_Payment|NULL
* @throws \CiviCRM_API3_Exception
*/
public function getByName($name, $is_test) {
$processor = civicrm_api3('payment_processor', 'getsingle', array('name' => $name, 'is_test' => $is_test));
return self::getByProcessor($processor);
}
/**
* Flush processors from static cache.
*
* This is particularly used for tests.
*/
public function flushProcessors() {
$this->cache = array();
\CRM_Financial_BAO_PaymentProcessor::getAllPaymentProcessors('all', TRUE);
\CRM_Financial_BAO_PaymentProcessor::getAllPaymentProcessors('live', TRUE);
\CRM_Financial_BAO_PaymentProcessor::getAllPaymentProcessors('test', TRUE);
}
/**
* Sometimes we want to instantiate a processor object when no valid instance exists (eg. when uninstalling a
* processor).
*
* This function does not load instance specific details for the processor.
*
* @param string $className
*
* @return \Civi\Payment\CRM_Core_Payment|NULL
* @throws \CiviCRM_API3_Exception
*/
public function getByClass($className) {
return $this->getByProcessor(array(
'class_name' => $className,
'id' => 0,
'is_test' => 0,
),
TRUE);
}
}

View file

@ -0,0 +1,171 @@
<?php
namespace Civi;
use PDO;
use PDOException;
/**
* Class Test
*
* A facade for managing the test environment.
*/
class Test {
/**
* @var array
*/
private static $singletons = array();
/**
* Get the data source used for testing.
*
* @param string|NULL $part
* One of NULL, 'hostspec', 'port', 'username', 'password', 'database'.
* @return string|array|NULL
* If $part is omitted, return full DSN array.
* If $part is a string, return that part of the DSN.
*/
public static function dsn($part = NULL) {
if (!isset(self::$singletons['dsn'])) {
require_once "DB.php";
self::$singletons['dsn'] = \DB::parseDSN(CIVICRM_DSN);
}
if ($part === NULL) {
return self::$singletons['dsn'];
}
if (isset(self::$singletons['dsn'][$part])) {
return self::$singletons['dsn'][$part];
}
return NULL;
}
/**
* Get a connection to the test database.
*
* @return PDO
*/
public static function pdo() {
if (!isset(self::$singletons['pdo'])) {
$dsninfo = self::dsn();
$host = $dsninfo['hostspec'];
$port = @$dsninfo['port'];
try {
self::$singletons['pdo'] = new PDO("mysql:host={$host}" . ($port ? ";port=$port" : ""),
$dsninfo['username'], $dsninfo['password'],
array(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE)
);
}
catch (PDOException $e) {
echo "Can't connect to MySQL server:" . PHP_EOL . $e->getMessage() . PHP_EOL;
exit(1);
}
}
return self::$singletons['pdo'];
}
/**
* Create a builder for the headless environment.
*
* @return \Civi\Test\CiviEnvBuilder
*
* @code
* \Civi\Test::headless()->apply();
* \Civi\Test::headless()->sqlFile('ex.sql')->apply();
* @endCode
*/
public static function headless() {
$civiRoot = dirname(__DIR__);
$builder = new \Civi\Test\CiviEnvBuilder('CiviEnvBuilder');
$builder
->callback(function ($ctx) {
if (CIVICRM_UF !== 'UnitTests') {
throw new \RuntimeException("\\Civi\\Test::headless() requires CIVICRM_UF=UnitTests");
}
$dbName = \Civi\Test::dsn('database');
echo "Installing {$dbName} schema\n";
\Civi\Test::schema()->dropAll();
}, 'headless-drop')
->sqlFile($civiRoot . "/sql/civicrm.mysql")
->sql("DELETE FROM civicrm_extension")
->callback(function ($ctx) {
\Civi\Test::data()->populate();
}, 'populate');
return $builder;
}
/**
* Create a builder for end-to-end testing on the live environment.
*
* @return \Civi\Test\CiviEnvBuilder
*
* @code
* \Civi\Test::e2e()->apply();
* \Civi\Test::e2e()->install('foo.bar')->apply();
* @endCode
*/
public static function e2e() {
$builder = new \Civi\Test\CiviEnvBuilder('CiviEnvBuilder');
$builder
->callback(function ($ctx) {
if (CIVICRM_UF === 'UnitTests') {
throw new \RuntimeException("\\Civi\\Test::e2e() requires a real CMS. Found CIVICRM_UF=UnitTests.");
}
}, 'e2e-check');
return $builder;
}
/**
* @return \Civi\Test\Schema
*/
public static function schema() {
if (!isset(self::$singletons['schema'])) {
self::$singletons['schema'] = new \Civi\Test\Schema();
}
return self::$singletons['schema'];
}
/**
* @return \Civi\Test\Data
*/
public static function data() {
if (!isset(self::$singletons['data'])) {
self::$singletons['data'] = new \Civi\Test\Data('CiviTesterData');
}
return self::$singletons['data'];
}
/**
* Prepare and execute a batch of SQL statements.
*
* @param string $query
* @return bool
*/
public static function execute($query) {
$pdo = \Civi\Test::pdo();
$string = preg_replace("/^#[^\n]*$/m", "\n", $query);
$string = preg_replace("/^(--[^-]).*/m", "\n", $string);
$queries = preg_split('/;\s*$/m', $string);
foreach ($queries as $query) {
$query = trim($query);
if (!empty($query)) {
$result = $pdo->query($query);
if ($pdo->errorCode() == 0) {
continue;
}
else {
var_dump($result);
var_dump($pdo->errorInfo());
// die( "Cannot execute $query: " . $pdo->errorInfo() );
}
}
}
return TRUE;
}
}

View file

@ -0,0 +1,208 @@
<?php
namespace Civi\Test;
use Civi\Test\CiviEnvBuilder\CallbackStep;
use Civi\Test\CiviEnvBuilder\ExtensionsStep;
use Civi\Test\CiviEnvBuilder\SqlFileStep;
use Civi\Test\CiviEnvBuilder\SqlStep;
use Civi\Test\CiviEnvBuilder\StepInterface;
use RuntimeException;
/**
* Class CiviEnvBuilder
*
* Provides a fluent interface for tracking a set of steps.
* By computing and storing a signature for the list steps, we can
* determine whether to (a) do nothing with the list or (b)
* reapply all the steps.
*/
class CiviEnvBuilder {
protected $name;
private $steps = array();
/**
* @var string|NULL
* A digest of the values in $steps.
*/
private $targetSignature = NULL;
public function __construct($name) {
$this->name = $name;
}
public function addStep(StepInterface $step) {
$this->targetSignature = NULL;
$this->steps[] = $step;
return $this;
}
public function callback($callback, $signature = NULL) {
return $this->addStep(new CallbackStep($callback, $signature));
}
public function sql($sql) {
return $this->addStep(new SqlStep($sql));
}
public function sqlFile($file) {
return $this->addStep(new SqlFileStep($file));
}
/**
* Require that an extension be installed.
*
* @param string|array $names
* One or more extension names. You may use a wildcard '*'.
* @return CiviEnvBuilder
*/
public function install($names) {
return $this->addStep(new ExtensionsStep('install', $names));
}
/**
* Require an extension be installed (identified by its directory).
*
* @param string $dir
* The current test directory. We'll search for info.xml to
* see what this extension is.
* @return CiviEnvBuilder
* @throws \CRM_Extension_Exception_ParseException
*/
public function installMe($dir) {
return $this->addStep(new ExtensionsStep('install', $this->whoAmI($dir)));
}
/**
* Require an extension be uninstalled.
*
* @param string|array $names
* One or more extension names. You may use a wildcard '*'.
* @return CiviEnvBuilder
*/
public function uninstall($names) {
return $this->addStep(new ExtensionsStep('uninstall', $names));
}
/**
* Require an extension be uninstalled (identified by its directory).
*
* @param string $dir
* The current test directory. We'll search for info.xml to
* see what this extension is.
* @return CiviEnvBuilder
* @throws \CRM_Extension_Exception_ParseException
*/
public function uninstallMe($dir) {
return $this->addStep(new ExtensionsStep('uninstall', $this->whoAmI($dir)));
}
protected function assertValid() {
foreach ($this->steps as $step) {
if (!$step->isValid()) {
throw new RuntimeException("Found invalid step: " . var_dump($step, 1));
}
}
}
/**
* @return string
*/
protected function getTargetSignature() {
if ($this->targetSignature === NULL) {
$buf = '';
foreach ($this->steps as $step) {
$buf .= $step->getSig();
}
$this->targetSignature = md5($buf);
}
return $this->targetSignature;
}
/**
* @return string
*/
protected function getSavedSignature() {
$liveSchemaRev = NULL;
$pdo = \Civi\Test::pdo();
$pdoStmt = $pdo->query(sprintf(
"SELECT rev FROM %s.civitest_revs WHERE name = %s",
\Civi\Test::dsn('database'),
$pdo->quote($this->name)
));
foreach ($pdoStmt as $row) {
$liveSchemaRev = $row['rev'];
}
return $liveSchemaRev;
}
/**
* @param $newSignature
*/
protected function setSavedSignature($newSignature) {
$pdo = \Civi\Test::pdo();
$query = sprintf(
'INSERT INTO %s.civitest_revs (name,rev) VALUES (%s,%s) '
. 'ON DUPLICATE KEY UPDATE rev = %s;',
\Civi\Test::dsn('database'),
$pdo->quote($this->name),
$pdo->quote($newSignature),
$pdo->quote($newSignature)
);
if (\Civi\Test::execute($query) === FALSE) {
throw new RuntimeException("Failed to flag schema version: $query");
}
}
/**
* Determine if there's been a change in the preferred configuration.
* If the preferred-configuration matches the last test, keep it. Otherwise,
* destroy and recreate.
*
* @param bool $force
* Forcibly execute the build, even if the configuration hasn't changed.
* This will slow-down the tests, but it may be appropriate for some very sloppy
* tests.
* @return CiviEnvBuilder
*/
public function apply($force = FALSE) {
$dbName = \Civi\Test::dsn('database');
$query = "USE {$dbName};"
. "CREATE TABLE IF NOT EXISTS civitest_revs (name VARCHAR(64) PRIMARY KEY, rev VARCHAR(64));";
if (\Civi\Test::execute($query) === FALSE) {
throw new \RuntimeException("Failed to flag schema version: $query");
}
$this->assertValid();
if (!$force && $this->getSavedSignature() === $this->getTargetSignature()) {
return $this;
}
foreach ($this->steps as $step) {
$step->run($this);
}
$this->setSavedSignature($this->getTargetSignature());
return $this;
}
/**
* @param $dir
* @return null
* @throws \CRM_Extension_Exception_ParseException
*/
protected function whoAmI($dir) {
while ($dir && dirname($dir) !== $dir && !file_exists("$dir/info.xml")) {
$dir = dirname($dir);
}
if (file_exists("$dir/info.xml")) {
$info = \CRM_Extension_Info::loadFromFile("$dir/info.xml");
$name = $info->key;
return $name;
}
return $name;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Civi\Test\CiviEnvBuilder;
class CallbackStep implements StepInterface {
private $callback;
private $sig;
/**
* CallbackStep constructor.
* @param $callback
* @param $sig
*/
public function __construct($callback, $sig = NULL) {
$this->callback = $callback;
$this->sig = $sig === NULL ? md5(var_export($callback, 1)) : $sig;
}
public function getSig() {
return $this->sig;
}
public function isValid() {
return is_callable($this->callback);
}
public function run($ctx) {
call_user_func($this->callback, $ctx);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Civi\Test\CiviEnvBuilder;
class ExtensionsStep implements StepInterface {
private $action;
private $names;
/**
* ExtensionStep constructor.
* @param string $action
* Ex: 'install', 'uninstall'.
* @param string|array $names
*/
public function __construct($action, $names) {
$this->action = $action;
$this->names = (array) $names;
}
public function getSig() {
return 'ext:' . implode(',', $this->names);
}
public function isValid() {
if (!in_array($this->action, array('install', 'uninstall'))) {
return FALSE;
}
foreach ($this->names as $name) {
if (!is_string($name)) {
return FALSE;
}
}
return TRUE;
}
public function run($ctx) {
$allKeys = \CRM_Extension_System::singleton()->getFullContainer()->getKeys();
$names = \CRM_Utils_String::filterByWildcards($this->names, $allKeys, TRUE);
$manager = \CRM_Extension_System::singleton()->getManager();
switch ($this->action) {
case 'install':
$manager->install($names);
break;
case 'uninstall':
$manager->disable($names);
$manager->uninstall($names);
break;
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Civi\Test\CiviEnvBuilder;
class SqlFileStep implements StepInterface {
private $file;
/**
* SqlFileStep constructor.
* @param string $file
*/
public function __construct($file) {
$this->file = $file;
}
public function getSig() {
return implode(' ', array(
$this->file,
filemtime($this->file),
filectime($this->file),
));
}
public function isValid() {
return is_file($this->file) && is_readable($this->file);
}
public function run($ctx) {
/** @var $ctx \CiviEnvBuilder */
if (\Civi\Test::execute(@file_get_contents($this->file)) === FALSE) {
throw new \RuntimeException("Cannot load {$this->file}. Aborting.");
}
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Civi\Test\CiviEnvBuilder;
class SqlStep implements StepInterface {
private $sql;
/**
* SqlFileStep constructor.
* @param string $sql
*/
public function __construct($sql) {
$this->sql = $sql;
}
public function getSig() {
return md5($this->sql);
}
public function isValid() {
return TRUE;
}
public function run($ctx) {
/** @var $ctx \CiviEnvBuilder */
if (\Civi\Test::execute($this->sql) === FALSE) {
throw new \RuntimeException("Cannot execute: {$this->sql}");
}
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace Civi\Test\CiviEnvBuilder;
interface StepInterface {
public function getSig();
public function isValid();
public function run($ctx);
}

View file

@ -0,0 +1,290 @@
<?php
namespace Civi\Test;
/**
* Class CiviTestListener
* @package Civi\Test
*
* CiviTestListener participates in test-execution, looking for test-classes
* which have certain tags. If the tags are found, the listener will perform
* additional setup/teardown logic.
*
* @see EndToEndInterface
* @see HeadlessInterface
* @see HookInterface
*/
class CiviTestListener extends \PHPUnit_Framework_BaseTestListener {
/**
* @var \CRM_Core_TemporaryErrorScope
*/
private $errorScope;
/**
* @var array
* Ex: $cache['Some_Test_Class']['civicrm_foobar'] = 'hook_civicrm_foobar';
* Array(string $testClass => Array(string $hookName => string $methodName)).
*/
private $cache = array();
/**
* @var \CRM_Core_Transaction|NULL
*/
private $tx;
public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) {
$byInterface = $this->indexTestsByInterface($suite->tests());
$this->validateGroups($byInterface);
$this->autoboot($byInterface);
}
public function endTestSuite(\PHPUnit_Framework_TestSuite $suite) {
$this->cache = array();
}
public function startTest(\PHPUnit_Framework_Test $test) {
if ($this->isCiviTest($test)) {
error_reporting(E_ALL);
$this->errorScope = \CRM_Core_TemporaryErrorScope::useException();
}
if ($test instanceof HeadlessInterface) {
$this->bootHeadless($test);
}
if ($test instanceof HookInterface) {
// Note: bootHeadless() indirectly resets any hooks, which means that hook_civicrm_config
// is unsubscribable. However, after bootHeadless(), we're free to subscribe to hooks again.
$this->registerHooks($test);
}
if ($test instanceof TransactionalInterface) {
$this->tx = new \CRM_Core_Transaction(TRUE);
$this->tx->rollback();
}
else {
$this->tx = NULL;
}
}
public function endTest(\PHPUnit_Framework_Test $test, $time) {
if ($test instanceof TransactionalInterface) {
$this->tx->rollback()->commit();
$this->tx = NULL;
}
if ($test instanceof HookInterface) {
\CRM_Utils_Hook::singleton()->reset();
}
if ($this->isCiviTest($test)) {
error_reporting(E_ALL & ~E_NOTICE);
$this->errorScope = NULL;
}
}
/**
* @param HeadlessInterface|\PHPUnit_Framework_Test $test
*/
protected function bootHeadless($test) {
if (CIVICRM_UF !== 'UnitTests') {
throw new \RuntimeException('HeadlessInterface requires CIVICRM_UF=UnitTests');
}
// Hrm, this seems wrong. Shouldn't we be resetting the entire session?
$session = \CRM_Core_Session::singleton();
$session->set('userID', NULL);
$test->setUpHeadless();
\CRM_Utils_System::flushCache();
\Civi::reset();
\CRM_Core_Session::singleton()->set('userID', NULL);
$config = \CRM_Core_Config::singleton(TRUE, TRUE); // ugh, performance
if (property_exists($config->userPermissionClass, 'permissions')) {
$config->userPermissionClass->permissions = NULL;
}
}
/**
* @param \Civi\Test\HookInterface $test
* @return array
* Array(string $hookName => string $methodName)).
*/
protected function findTestHooks(HookInterface $test) {
$class = get_class($test);
if (!isset($this->cache[$class])) {
$funcs = array();
foreach (get_class_methods($class) as $func) {
if (preg_match('/^hook_/', $func)) {
$funcs[substr($func, 5)] = $func;
}
}
$this->cache[$class] = $funcs;
}
return $this->cache[$class];
}
/**
* @param \PHPUnit_Framework_Test $test
* @return bool
*/
protected function isCiviTest(\PHPUnit_Framework_Test $test) {
return $test instanceof HookInterface || $test instanceof HeadlessInterface;
}
/**
* Find any hook functions in $test and register them.
*
* @param \Civi\Test\HookInterface $test
*/
protected function registerHooks(HookInterface $test) {
if (CIVICRM_UF !== 'UnitTests') {
// This is not ideal -- it's just a side-effect of how hooks and E2E tests work.
// We can temporarily subscribe to hooks in-process, but for other processes, it gets messy.
throw new \RuntimeException('CiviHookTestInterface requires CIVICRM_UF=UnitTests');
}
\CRM_Utils_Hook::singleton()->reset();
/** @var \CRM_Utils_Hook_UnitTests $hooks */
$hooks = \CRM_Utils_Hook::singleton();
foreach ($this->findTestHooks($test) as $hook => $func) {
$hooks->setHook($hook, array($test, $func));
}
}
/**
* The first time we come across HeadlessInterface or EndToEndInterface, we'll
* try to autoboot.
*
* Once the system is booted, there's nothing we can do -- we're stuck with that
* environment. (Thank you, prolific define()s!) If there's a conflict between a
* test-class and the active boot-level, then we'll have to bail.
*
* @param array $byInterface
* List of test classes, keyed by major interface (HeadlessInterface vs EndToEndInterface).
*/
protected function autoboot($byInterface) {
if (defined('CIVICRM_UF')) {
// OK, nothing we can do. System has booted already.
}
elseif (!empty($byInterface['HeadlessInterface'])) {
putenv('CIVICRM_UF=UnitTests');
eval($this->cv('php:boot --level=full', 'phpcode'));
}
elseif (!empty($byInterface['EndToEndInterface'])) {
putenv('CIVICRM_UF=');
eval($this->cv('php:boot --level=full', 'phpcode'));
}
$blurb = "Tip: Run the headless tests and end-to-end tests separately, e.g.\n"
. " $ phpunit4 --group headless\n"
. " $ phpunit4 --group e2e \n";
if (!empty($byInterface['HeadlessInterface']) && CIVICRM_UF !== 'UnitTests') {
$testNames = implode(', ', array_keys($byInterface['HeadlessInterface']));
throw new \RuntimeException("Suite includes headless tests ($testNames) which require CIVICRM_UF=UnitTests.\n\n$blurb");
}
if (!empty($byInterface['EndToEndInterface']) && CIVICRM_UF === 'UnitTests') {
$testNames = implode(', ', array_keys($byInterface['EndToEndInterface']));
throw new \RuntimeException("Suite includes end-to-end tests ($testNames) which do not support CIVICRM_UF=UnitTests.\n\n$blurb");
}
}
/**
* Call the "cv" command.
*
* This duplicates the standalone `cv()` wrapper that is recommended in bootstrap.php.
* This duplication is necessary because `cv()` is optional, and downstream implementers
* may alter, rename, or omit the wrapper, and (by virtue of its role in bootstrap) there
* it is impossible to define it centrally.
*
* @param string $cmd
* The rest of the command to send.
* @param string $decode
* Ex: 'json' or 'phpcode'.
* @return string
* Response output (if the command executed normally).
* @throws \RuntimeException
* If the command terminates abnormally.
*/
protected function cv($cmd, $decode = 'json') {
$cmd = 'cv ' . $cmd;
$descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
$oldOutput = getenv('CV_OUTPUT');
putenv("CV_OUTPUT=json");
$process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
putenv("CV_OUTPUT=$oldOutput");
fclose($pipes[0]);
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
if (proc_close($process) !== 0) {
throw new \RuntimeException("Command failed ($cmd):\n$result");
}
switch ($decode) {
case 'raw':
return $result;
case 'phpcode':
// If the last output is /*PHPCODE*/, then we managed to complete execution.
if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") {
throw new \RuntimeException("Command failed ($cmd):\n$result");
}
return $result;
case 'json':
return json_decode($result, 1);
default:
throw new \RuntimeException("Bad decoder format ($decode)");
}
}
/**
* @param $tests
* @return array
*/
protected function indexTestsByInterface($tests) {
$byInterface = array('HeadlessInterface' => array(), 'EndToEndInterface' => array());
foreach ($tests as $test) {
/** @var \PHPUnit_Framework_Test $test */
if ($test instanceof HeadlessInterface) {
$byInterface['HeadlessInterface'][get_class($test)] = 1;
}
if ($test instanceof EndToEndInterface) {
$byInterface['EndToEndInterface'][get_class($test)] = 1;
}
}
return $byInterface;
}
/**
* Ensure that any tests have sensible groups, e.g.
*
* `HeadlessInterface` ==> `group headless`
* `EndToEndInterface` ==> `group e2e`
*
* @param array $byInterface
*/
protected function validateGroups($byInterface) {
foreach ($byInterface['HeadlessInterface'] as $className => $nonce) {
$clazz = new \ReflectionClass($className);
$docComment = str_replace("\r\n", "\n", $clazz->getDocComment());
if (strpos($docComment, "@group headless\n") === FALSE) {
echo "WARNING: Class $className implements HeadlessInterface. It should declare \"@group headless\".\n";
}
if (strpos($docComment, "@group e2e\n") !== FALSE) {
echo "WARNING: Class $className implements HeadlessInterface. It should not declare \"@group e2e\".\n";
}
}
foreach ($byInterface['EndToEndInterface'] as $className => $nonce) {
$clazz = new \ReflectionClass($className);
$docComment = str_replace("\r\n", "\n", $clazz->getDocComment());
if (strpos($docComment, "@group e2e\n") === FALSE) {
echo "WARNING: Class $className implements EndToEndInterface. It should declare \"@group e2e\".\n";
}
if (strpos($docComment, "@group headless\n") !== FALSE) {
echo "WARNING: Class $className implements EndToEndInterface. It should not declare \"@group headless\".\n";
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more