First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
185
sites/all/modules/civicrm/Civi/API/Api3SelectQuery.php
Normal file
185
sites/all/modules/civicrm/Civi/API/Api3SelectQuery.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
55
sites/all/modules/civicrm/Civi/API/Event/AuthorizeEvent.php
Normal file
55
sites/all/modules/civicrm/Civi/API/Event/AuthorizeEvent.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
91
sites/all/modules/civicrm/Civi/API/Event/Event.php
Normal file
91
sites/all/modules/civicrm/Civi/API/Event/Event.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
63
sites/all/modules/civicrm/Civi/API/Event/ExceptionEvent.php
Normal file
63
sites/all/modules/civicrm/Civi/API/Event/ExceptionEvent.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
45
sites/all/modules/civicrm/Civi/API/Event/PrepareEvent.php
Normal file
45
sites/all/modules/civicrm/Civi/API/Event/PrepareEvent.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
61
sites/all/modules/civicrm/Civi/API/Event/ResolveEvent.php
Normal file
61
sites/all/modules/civicrm/Civi/API/Event/ResolveEvent.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
70
sites/all/modules/civicrm/Civi/API/Event/RespondEvent.php
Normal file
70
sites/all/modules/civicrm/Civi/API/Event/RespondEvent.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
120
sites/all/modules/civicrm/Civi/API/Events.php
Normal file
120
sites/all/modules/civicrm/Civi/API/Events.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
251
sites/all/modules/civicrm/Civi/API/ExternalBatch.php
Normal file
251
sites/all/modules/civicrm/Civi/API/ExternalBatch.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
491
sites/all/modules/civicrm/Civi/API/Kernel.php
Normal file
491
sites/all/modules/civicrm/Civi/API/Kernel.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
168
sites/all/modules/civicrm/Civi/API/Provider/AdhocProvider.php
Normal file
168
sites/all/modules/civicrm/Civi/API/Provider/AdhocProvider.php
Normal 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'])]);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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['*'];
|
||||
}
|
||||
|
||||
}
|
156
sites/all/modules/civicrm/Civi/API/Provider/StaticProvider.php
Normal file
156
sites/all/modules/civicrm/Civi/API/Provider/StaticProvider.php
Normal 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());
|
||||
}
|
||||
|
||||
}
|
118
sites/all/modules/civicrm/Civi/API/Request.php
Normal file
118
sites/all/modules/civicrm/Civi/API/Request.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
512
sites/all/modules/civicrm/Civi/API/SelectQuery.php
Normal file
512
sites/all/modules/civicrm/Civi/API/SelectQuery.php
Normal 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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
108
sites/all/modules/civicrm/Civi/API/Subscriber/I18nSubscriber.php
Normal file
108
sites/all/modules/civicrm/Civi/API/Subscriber/I18nSubscriber.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 we’re 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 might’ve 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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']]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
107
sites/all/modules/civicrm/Civi/API/Subscriber/WrapperAdapter.php
Normal file
107
sites/all/modules/civicrm/Civi/API/Subscriber/WrapperAdapter.php
Normal 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'];
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
282
sites/all/modules/civicrm/Civi/API/WhitelistRule.php
Normal file
282
sites/all/modules/civicrm/Civi/API/WhitelistRule.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
19
sites/all/modules/civicrm/Civi/ActionSchedule/Events.php
Normal file
19
sites/all/modules/civicrm/Civi/ActionSchedule/Events.php
Normal 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';
|
||||
|
||||
}
|
344
sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php
Normal file
344
sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php
Normal 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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
236
sites/all/modules/civicrm/Civi/Angular/AngularLoader.php
Normal file
236
sites/all/modules/civicrm/Civi/Angular/AngularLoader.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
191
sites/all/modules/civicrm/Civi/Angular/ChangeSet.php
Normal file
191
sites/all/modules/civicrm/Civi/Angular/ChangeSet.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
82
sites/all/modules/civicrm/Civi/Angular/Coder.php
Normal file
82
sites/all/modules/civicrm/Civi/Angular/Coder.php
Normal 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";
|
||||
}
|
||||
|
||||
}
|
413
sites/all/modules/civicrm/Civi/Angular/Manager.php
Normal file
413
sites/all/modules/civicrm/Civi/Angular/Manager.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
100
sites/all/modules/civicrm/Civi/Angular/Page/Main.php
Normal file
100
sites/all/modules/civicrm/Civi/Angular/Page/Main.php
Normal 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';
|
||||
}
|
||||
|
||||
}
|
178
sites/all/modules/civicrm/Civi/Angular/Page/Modules.php
Normal file
178
sites/all/modules/civicrm/Civi/Angular/Page/Modules.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
228
sites/all/modules/civicrm/Civi/CCase/Analyzer.php
Normal file
228
sites/all/modules/civicrm/Civi/CCase/Analyzer.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
42
sites/all/modules/civicrm/Civi/CCase/CaseChangeListener.php
Normal file
42
sites/all/modules/civicrm/Civi/CCase/CaseChangeListener.php
Normal 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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
114
sites/all/modules/civicrm/Civi/CCase/Events.php
Normal file
114
sites/all/modules/civicrm/Civi/CCase/Events.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
namespace Civi\CCase\Exception;
|
||||
|
||||
/**
|
||||
* Class MultipleActivityException
|
||||
*
|
||||
* @package Civi\CCase\Exception
|
||||
*/
|
||||
class MultipleActivityException extends \CRM_Core_Exception {
|
||||
|
||||
}
|
123
sites/all/modules/civicrm/Civi/CCase/SequenceListener.php
Normal file
123
sites/all/modules/civicrm/Civi/CCase/SequenceListener.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
24
sites/all/modules/civicrm/Civi/CiUtil/Arrays.php
Normal file
24
sites/all/modules/civicrm/Civi/CiUtil/Arrays.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
32
sites/all/modules/civicrm/Civi/CiUtil/CSVParser.php
Normal file
32
sites/all/modules/civicrm/Civi/CiUtil/CSVParser.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
21
sites/all/modules/civicrm/Civi/CiUtil/Command/LsCommand.php
Normal file
21
sites/all/modules/civicrm/Civi/CiUtil/Command/LsCommand.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
59
sites/all/modules/civicrm/Civi/CiUtil/ComparisonPrinter.php
Normal file
59
sites/all/modules/civicrm/Civi/CiUtil/ComparisonPrinter.php
Normal 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";
|
||||
}
|
||||
|
||||
}
|
46
sites/all/modules/civicrm/Civi/CiUtil/CsvPrinter.php
Normal file
46
sites/all/modules/civicrm/Civi/CiUtil/CsvPrinter.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
37
sites/all/modules/civicrm/Civi/CiUtil/EnvTestRunner.php
Normal file
37
sites/all/modules/civicrm/Civi/CiUtil/EnvTestRunner.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
35
sites/all/modules/civicrm/Civi/CiUtil/JenkinsParser.php
Normal file
35
sites/all/modules/civicrm/Civi/CiUtil/JenkinsParser.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
38
sites/all/modules/civicrm/Civi/CiUtil/PHPUnitParser.php
Normal file
38
sites/all/modules/civicrm/Civi/CiUtil/PHPUnitParser.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
97
sites/all/modules/civicrm/Civi/CiUtil/PHPUnitScanner.php
Normal file
97
sites/all/modules/civicrm/Civi/CiUtil/PHPUnitScanner.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
383
sites/all/modules/civicrm/Civi/Core/AssetBuilder.php
Normal file
383
sites/all/modules/civicrm/Civi/Core/AssetBuilder.php
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
135
sites/all/modules/civicrm/Civi/Core/CiviEventDispatcher.php
Normal file
135
sites/all/modules/civicrm/Civi/Core/CiviEventDispatcher.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
227
sites/all/modules/civicrm/Civi/Core/CiviEventInspector.php
Normal file
227
sites/all/modules/civicrm/Civi/Core/CiviEventInspector.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
460
sites/all/modules/civicrm/Civi/Core/Container.php
Normal file
460
sites/all/modules/civicrm/Civi/Core/Container.php
Normal 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']);
|
||||
}
|
||||
|
||||
}
|
54
sites/all/modules/civicrm/Civi/Core/DAO/Event/PostDelete.php
Normal file
54
sites/all/modules/civicrm/Civi/Core/DAO/Event/PostDelete.php
Normal 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;
|
||||
}
|
||||
}
|
47
sites/all/modules/civicrm/Civi/Core/DAO/Event/PostUpdate.php
Normal file
47
sites/all/modules/civicrm/Civi/Core/DAO/Event/PostUpdate.php
Normal 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;
|
||||
}
|
||||
}
|
47
sites/all/modules/civicrm/Civi/Core/DAO/Event/PreDelete.php
Normal file
47
sites/all/modules/civicrm/Civi/Core/DAO/Event/PreDelete.php
Normal 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;
|
||||
}
|
||||
}
|
53
sites/all/modules/civicrm/Civi/Core/DatabaseInitializer.php
Normal file
53
sites/all/modules/civicrm/Civi/Core/DatabaseInitializer.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
255
sites/all/modules/civicrm/Civi/Core/Event/GenericHookEvent.php
Normal file
255
sites/all/modules/civicrm/Civi/Core/Event/GenericHookEvent.php
Normal 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]);
|
||||
}
|
||||
|
||||
}
|
87
sites/all/modules/civicrm/Civi/Core/Event/PostEvent.php
Normal file
87
sites/all/modules/civicrm/Civi/Core/Event/PostEvent.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
87
sites/all/modules/civicrm/Civi/Core/Event/PreEvent.php
Normal file
87
sites/all/modules/civicrm/Civi/Core/Event/PreEvent.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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__);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Civi\Core\Exception;
|
||||
|
||||
class UnknownAssetException extends \CRM_Core_Exception {
|
||||
|
||||
}
|
59
sites/all/modules/civicrm/Civi/Core/InstallationCanary.php
Normal file
59
sites/all/modules/civicrm/Civi/Core/InstallationCanary.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
104
sites/all/modules/civicrm/Civi/Core/LocalizationInitializer.php
Normal file
104
sites/all/modules/civicrm/Civi/Core/LocalizationInitializer.php
Normal 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
64
sites/all/modules/civicrm/Civi/Core/Lock/LockInterface.php
Normal file
64
sites/all/modules/civicrm/Civi/Core/Lock/LockInterface.php
Normal 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();
|
||||
|
||||
}
|
121
sites/all/modules/civicrm/Civi/Core/Lock/LockManager.php
Normal file
121
sites/all/modules/civicrm/Civi/Core/Lock/LockManager.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
92
sites/all/modules/civicrm/Civi/Core/Lock/NullLock.php
Normal file
92
sites/all/modules/civicrm/Civi/Core/Lock/NullLock.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
211
sites/all/modules/civicrm/Civi/Core/Paths.php
Normal file
211
sites/all/modules/civicrm/Civi/Core/Paths.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
287
sites/all/modules/civicrm/Civi/Core/Resolver.php
Normal file
287
sites/all/modules/civicrm/Civi/Core/Resolver.php
Normal 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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
403
sites/all/modules/civicrm/Civi/Core/SettingsBag.php
Normal file
403
sites/all/modules/civicrm/Civi/Core/SettingsBag.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
354
sites/all/modules/civicrm/Civi/Core/SettingsManager.php
Normal file
354
sites/all/modules/civicrm/Civi/Core/SettingsManager.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
165
sites/all/modules/civicrm/Civi/Core/SettingsMetadata.php
Normal file
165
sites/all/modules/civicrm/Civi/Core/SettingsMetadata.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
57
sites/all/modules/civicrm/Civi/Core/SettingsStack.php
Normal file
57
sites/all/modules/civicrm/Civi/Core/SettingsStack.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
241
sites/all/modules/civicrm/Civi/Core/SqlTriggers.php
Normal file
241
sites/all/modules/civicrm/Civi/Core/SqlTriggers.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
213
sites/all/modules/civicrm/Civi/Core/Transaction/Frame.php
Normal file
213
sites/all/modules/civicrm/Civi/Core/Transaction/Frame.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
177
sites/all/modules/civicrm/Civi/Core/Transaction/Manager.php
Normal file
177
sites/all/modules/civicrm/Civi/Core/Transaction/Manager.php
Normal 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}");
|
||||
}
|
||||
|
||||
}
|
566
sites/all/modules/civicrm/Civi/Install/Requirements.php
Normal file
566
sites/all/modules/civicrm/Civi/Install/Requirements.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
namespace Civi\Payment\Exception;
|
||||
|
||||
/**
|
||||
* Class PaymentProcessorException
|
||||
*/
|
||||
class PaymentProcessorException extends \CRM_Core_Exception {
|
||||
|
||||
}
|
135
sites/all/modules/civicrm/Civi/Payment/System.php
Normal file
135
sites/all/modules/civicrm/Civi/Payment/System.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
171
sites/all/modules/civicrm/Civi/Test.php
Normal file
171
sites/all/modules/civicrm/Civi/Test.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
208
sites/all/modules/civicrm/Civi/Test/CiviEnvBuilder.php
Normal file
208
sites/all/modules/civicrm/Civi/Test/CiviEnvBuilder.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
namespace Civi\Test\CiviEnvBuilder;
|
||||
|
||||
interface StepInterface {
|
||||
public function getSig();
|
||||
|
||||
public function isValid();
|
||||
|
||||
public function run($ctx);
|
||||
|
||||
}
|
290
sites/all/modules/civicrm/Civi/Test/CiviTestListener.php
Normal file
290
sites/all/modules/civicrm/Civi/Test/CiviTestListener.php
Normal 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
Loading…
Add table
Add a link
Reference in a new issue