3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2015 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
30 * A WhitelistRule is used to determine if an API call is authorized.
34 * new WhitelistRule(array(
35 * 'entity' => 'Contact',
36 * 'actions' => array('get','getsingle'),
37 * 'required' => array('contact_type' => 'Organization'),
38 * 'fields' => array('id', 'display_name', 'sort_name', 'created_date'),
42 * This rule would allow API requests that attempt to get contacts of type "Organization",
43 * but only a handful of fields ('id', 'display_name', 'sort_name', 'created_date')
44 * can be filtered or returned.
47 * @package Civi\API\Subscriber
51 static $IGNORE_FIELDS = array(
67 * Create a batch of rules from an array.
72 public static function createAll($rules) {
74 foreach ($rules as $rule) {
75 $whitelist[] = new WhitelistRule($rule);
86 * Entity name or '*' (all entities)
93 * List of actions which match, or '*' (all actions)
100 * List of key=>value pairs that *must* appear in $params.
102 * If there are no required fields, use an empty array.
109 * List of fields which may be optionally inputted or returned, or '*" (all fields)
115 public function __construct($ruleSpec) {
116 $this->version
= $ruleSpec['version'];
118 if ($ruleSpec['entity'] === '*') {
122 $this->entity
= Request
::normalizeEntityName($ruleSpec['entity'], $ruleSpec['version']);
125 if ($ruleSpec['actions'] === '*') {
126 $this->actions
= '*';
129 $this->actions
= array();
130 foreach ((array) $ruleSpec['actions'] as $action) {
131 $this->actions
[] = Request
::normalizeActionName($action, $ruleSpec['version']);
135 $this->required
= $ruleSpec['required'];
136 $this->fields
= $ruleSpec['fields'];
142 public function isValid() {
143 if (empty($this->version
)) {
146 if (empty($this->entity
)) {
149 if (!is_array($this->actions
) && $this->actions
!== '*') {
152 if (!is_array($this->fields
) && $this->fields
!== '*') {
155 if (!is_array($this->required
)) {
162 * @param array $apiRequest
163 * Parsed API request.
164 * @return string|TRUE
165 * If match, return TRUE. Otherwise, return a string with an error code.
167 public function matches($apiRequest) {
168 if (!$this->isValid()) {
172 if ($this->version
!= $apiRequest['version']) {
175 if ($this->entity
!== '*' && $this->entity
!== $apiRequest['entity']) {
178 if ($this->actions
!== '*' && !in_array($apiRequest['action'], $this->actions
)) {
182 // These params *must* be included for the API request to proceed.
183 foreach ($this->required
as $param => $value) {
184 if (!isset($apiRequest['params'][$param])) {
185 return 'required-missing-' . $param;
187 if ($value !== '*' && $apiRequest['params'][$param] != $value) {
188 return 'required-wrong-' . $param;
192 // These params *may* be included at the caller's discretion
193 if ($this->fields
!== '*') {
194 $activatedFields = array_keys($apiRequest['params']);
195 $activatedFields = preg_grep('/^api\./', $activatedFields, PREG_GREP_INVERT
);
196 if ($apiRequest['action'] == 'get') {
197 // Kind'a silly we need to (re(re))parse here for each rule; would be more
198 // performant if pre-parsed by Request::create().
199 $options = _civicrm_api3_get_options_from_params($apiRequest['params'], TRUE, $apiRequest['entity'], 'get');
200 $return = \CRM_Utils_Array
::value('return', $options, array());
201 $activatedFields = array_merge($activatedFields, array_keys($return));
204 $unknowns = array_diff(
206 array_keys($this->required
),
211 if (!empty($unknowns)) {
212 return 'unknown-' . implode(',', $unknowns);
220 * Ensure that the return values comply with the whitelist's
223 * Most API's follow a convention where the result includes
224 * a 'values' array (which in turn is a list of records). Unfortunately,
225 * some don't. If the API result doesn't meet our expectation,
226 * then we probably don't know what's going on, so we abort the
229 * This will probably break some of the layered-sugar APIs (like
230 * getsingle, getvalue). Just use the meat-and-potatoes API instead.
231 * Or craft a suitably targeted patch.
233 * @param array $apiRequest
235 * @param array $apiResult
238 * Modified API result.
239 * @throws \API_Exception
241 public function filter($apiRequest, $apiResult) {
242 if ($this->fields
=== '*') {
245 if (isset($apiResult['values']) && empty($apiResult['values'])) {
246 // No data; filtering doesn't matter.
249 if (is_array($apiResult['values'])) {
250 $firstRow = \CRM_Utils_Array
::first($apiResult['values']);
251 if (is_array($firstRow)) {
252 $fields = $this->filterFields(array_keys($firstRow));
253 $apiResult['values'] = \CRM_Utils_Array
::filterColumns($apiResult['values'], $fields);
257 throw new \
API_Exception(sprintf('Filtering failed for %s.%s. Unrecognized result format.', $apiRequest['entity'], $apiRequest['action']));
261 * Determine which elements in $keys are acceptable under
262 * the whitelist policy.
265 * List of possible keys.
267 * List of acceptable keys.
269 protected function filterFields($keys) {
271 foreach ($keys as $key) {
272 if (in_array($key, $this->fields
)) {
275 elseif (preg_match('/^api\./', $key)) {