commiting uncommited changes on live site
[weblabels.fsf.org.git] / crm.fsf.org / 20131203 / files / sites / all / modules-new / civicrm / Civi / API / WhitelistRule.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
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. |
13 | |
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. |
18 | |
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 +--------------------------------------------------------------------+
26 */
27 namespace Civi\API;
28
29 /**
30 * A WhitelistRule is used to determine if an API call is authorized.
31 * For example:
32 *
33 * @code
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'),
39 * ));
40 * @endcode
41 *
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.
45 *
46 * Class WhitelistRule
47 * @package Civi\API\Subscriber
48 */
49 class WhitelistRule {
50
51 static $IGNORE_FIELDS = array(
52 'check_permissions',
53 'debug',
54 'offset',
55 'option_offset',
56 'option_limit',
57 'option_sort',
58 'options',
59 'return',
60 'rowCount',
61 'sequential',
62 'sort',
63 'version',
64 );
65
66 /**
67 * Create a batch of rules from an array.
68 *
69 * @param array $rules
70 * @return array
71 */
72 public static function createAll($rules) {
73 $whitelist = array();
74 foreach ($rules as $rule) {
75 $whitelist[] = new WhitelistRule($rule);
76 }
77 return $whitelist;
78 }
79
80 /**
81 * @var int
82 */
83 public $version;
84
85 /**
86 * Entity name or '*' (all entities)
87 *
88 * @var string
89 */
90 public $entity;
91
92 /**
93 * List of actions which match, or '*' (all actions)
94 *
95 * @var string|array
96 */
97 public $actions;
98
99 /**
100 * List of key=>value pairs that *must* appear in $params.
101 *
102 * If there are no required fields, use an empty array.
103 *
104 * @var array
105 */
106 public $required;
107
108 /**
109 * List of fields which may be optionally inputted or returned, or '*" (all fields)
110 *
111 * @var array
112 */
113 public $fields;
114
115 public function __construct($ruleSpec) {
116 $this->version = $ruleSpec['version'];
117
118 if ($ruleSpec['entity'] === '*') {
119 $this->entity = '*';
120 }
121 else {
122 $this->entity = Request::normalizeEntityName($ruleSpec['entity'], $ruleSpec['version']);
123 }
124
125 if ($ruleSpec['actions'] === '*') {
126 $this->actions = '*';
127 }
128 else {
129 $this->actions = array();
130 foreach ((array) $ruleSpec['actions'] as $action) {
131 $this->actions[] = Request::normalizeActionName($action, $ruleSpec['version']);
132 }
133 }
134
135 $this->required = $ruleSpec['required'];
136 $this->fields = $ruleSpec['fields'];
137 }
138
139 /**
140 * @return bool
141 */
142 public function isValid() {
143 if (empty($this->version)) {
144 return FALSE;
145 }
146 if (empty($this->entity)) {
147 return FALSE;
148 }
149 if (!is_array($this->actions) && $this->actions !== '*') {
150 return FALSE;
151 }
152 if (!is_array($this->fields) && $this->fields !== '*') {
153 return FALSE;
154 }
155 if (!is_array($this->required)) {
156 return FALSE;
157 }
158 return TRUE;
159 }
160
161 /**
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.
166 */
167 public function matches($apiRequest) {
168 if (!$this->isValid()) {
169 return 'invalid';
170 }
171
172 if ($this->version != $apiRequest['version']) {
173 return 'version';
174 }
175 if ($this->entity !== '*' && $this->entity !== $apiRequest['entity']) {
176 return 'entity';
177 }
178 if ($this->actions !== '*' && !in_array($apiRequest['action'], $this->actions)) {
179 return 'action';
180 }
181
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;
186 }
187 if ($value !== '*' && $apiRequest['params'][$param] != $value) {
188 return 'required-wrong-' . $param;
189 }
190 }
191
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));
202 }
203
204 $unknowns = array_diff(
205 $activatedFields,
206 array_keys($this->required),
207 $this->fields,
208 self::$IGNORE_FIELDS
209 );
210
211 if (!empty($unknowns)) {
212 return 'unknown-' . implode(',', $unknowns);
213 }
214 }
215
216 return TRUE;
217 }
218
219 /**
220 * Ensure that the return values comply with the whitelist's
221 * "fields" policy.
222 *
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
227 * request.
228 *
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.
232 *
233 * @param array $apiRequest
234 * API request.
235 * @param array $apiResult
236 * API result.
237 * @return array
238 * Modified API result.
239 * @throws \API_Exception
240 */
241 public function filter($apiRequest, $apiResult) {
242 if ($this->fields === '*') {
243 return $apiResult;
244 }
245 if (isset($apiResult['values']) && empty($apiResult['values'])) {
246 // No data; filtering doesn't matter.
247 return $apiResult;
248 }
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);
254 return $apiResult;
255 }
256 }
257 throw new \API_Exception(sprintf('Filtering failed for %s.%s. Unrecognized result format.', $apiRequest['entity'], $apiRequest['action']));
258 }
259
260 /**
261 * Determine which elements in $keys are acceptable under
262 * the whitelist policy.
263 *
264 * @param array $keys
265 * List of possible keys.
266 * @return array
267 * List of acceptable keys.
268 */
269 protected function filterFields($keys) {
270 $r = array();
271 foreach ($keys as $key) {
272 if (in_array($key, $this->fields)) {
273 $r[] = $key;
274 }
275 elseif (preg_match('/^api\./', $key)) {
276 $r[] = $key;
277 }
278 }
279 return $r;
280 }
281
282 }