1 | <?php |
||||
2 | |||||
3 | namespace SilverStripe\RestfulServer; |
||||
4 | |||||
5 | use SilverStripe\CMS\Model\SiteTree; |
||||
0 ignored issues
–
show
|
|||||
6 | use SilverStripe\Control\Controller; |
||||
7 | use SilverStripe\Control\Director; |
||||
8 | use SilverStripe\Control\HTTPRequest; |
||||
9 | use SilverStripe\Core\Config\Config; |
||||
10 | use SilverStripe\Core\Injector\Injector; |
||||
11 | use SilverStripe\ORM\ArrayList; |
||||
12 | use SilverStripe\ORM\DataList; |
||||
13 | use SilverStripe\ORM\DataObject; |
||||
14 | use SilverStripe\ORM\SS_List; |
||||
15 | use SilverStripe\ORM\ValidationException; |
||||
16 | use SilverStripe\ORM\ValidationResult; |
||||
17 | use SilverStripe\Security\Member; |
||||
18 | use SilverStripe\Security\Security; |
||||
19 | |||||
20 | /** |
||||
21 | * Generic RESTful server, which handles webservice access to arbitrary DataObjects. |
||||
22 | * Relies on serialization/deserialization into different formats provided |
||||
23 | * by the DataFormatter APIs in core. |
||||
24 | * |
||||
25 | * @todo Implement PUT/POST/DELETE for relations |
||||
26 | * @todo Access-Control for relations (you might be allowed to view Members and Groups, |
||||
27 | * but not their relation with each other) |
||||
28 | * @todo Make SearchContext specification customizeable for each class |
||||
29 | * @todo Allow for range-searches (e.g. on Created column) |
||||
30 | * @todo Filter relation listings by $api_access and canView() permissions |
||||
31 | * @todo Exclude relations when "fields" are specified through URL (they should be explicitly |
||||
32 | * requested in this case) |
||||
33 | * @todo Custom filters per DataObject subclass, e.g. to disallow showing unpublished pages in |
||||
34 | * SiteTree/Versioned/Hierarchy |
||||
35 | * @todo URL parameter namespacing for search-fields, limit, fields, add_fields |
||||
36 | * (might all be valid dataobject properties) |
||||
37 | * e.g. you wouldn't be able to search for a "limit" property on your subclass as |
||||
38 | * its overlayed with the search logic |
||||
39 | * @todo i18n integration (e.g. Page/1.xml?lang=de_DE) |
||||
40 | * @todo Access to extendable methods/relations like SiteTree/1/Versions or SiteTree/1/Version/22 |
||||
41 | * @todo Respect $api_access array notation in search contexts |
||||
42 | */ |
||||
43 | class RestfulServer extends Controller |
||||
44 | { |
||||
45 | /** |
||||
46 | * @config |
||||
47 | * @var array |
||||
48 | */ |
||||
49 | private static $url_handlers = array( |
||||
50 | '$ClassName!/$ID/$Relation' => 'handleAction', |
||||
51 | '' => 'notFound' |
||||
52 | ); |
||||
53 | |||||
54 | /** |
||||
55 | * @config |
||||
56 | * @var string root of the api route, MUST have a trailing slash |
||||
57 | */ |
||||
58 | private static $api_base = "api/v1/"; |
||||
59 | |||||
60 | /** |
||||
61 | * @config |
||||
62 | * @var string Class name for an authenticator to use on API access |
||||
63 | */ |
||||
64 | private static $authenticator = BasicRestfulAuthenticator::class; |
||||
65 | |||||
66 | /** |
||||
67 | * If no extension is given in the request, resolve to this extension |
||||
68 | * (and subsequently the {@link self::$default_mimetype}. |
||||
69 | * |
||||
70 | * @config |
||||
71 | * @var string |
||||
72 | */ |
||||
73 | private static $default_extension = "xml"; |
||||
74 | |||||
75 | /** |
||||
76 | * Custom endpoints that map to a specific class. |
||||
77 | * This is done to make the API have fixed endpoints, |
||||
78 | * instead of using fully namespaced classnames, as the module does by default |
||||
79 | * The fully namespaced classnames can also still be used though |
||||
80 | * Example: |
||||
81 | * ['mydataobject' => MyDataObject::class] |
||||
82 | * |
||||
83 | * @config array |
||||
84 | */ |
||||
85 | private static $endpoint_aliases = []; |
||||
86 | |||||
87 | /** |
||||
88 | * Whether or not to send an additional "Location" header for POST requests |
||||
89 | * to satisfy HTTP 1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html |
||||
90 | * |
||||
91 | * Note: With this enabled (the default), no POST request for resource creation |
||||
92 | * will return an HTTP 201. Because of the addition of the "Location" header, |
||||
93 | * all responses become a straight HTTP 200. |
||||
94 | * |
||||
95 | * @config |
||||
96 | * @var boolean |
||||
97 | */ |
||||
98 | private static $location_header_on_create = true; |
||||
99 | |||||
100 | /** |
||||
101 | * If no extension is given, resolve the request to this mimetype. |
||||
102 | * |
||||
103 | * @var string |
||||
104 | */ |
||||
105 | protected static $default_mimetype = "text/xml"; |
||||
106 | |||||
107 | /** |
||||
108 | * @uses authenticate() |
||||
109 | * @var Member |
||||
110 | */ |
||||
111 | protected $member; |
||||
112 | |||||
113 | private static $allowed_actions = array( |
||||
114 | 'index', |
||||
115 | 'notFound' |
||||
116 | ); |
||||
117 | |||||
118 | public function init() |
||||
119 | { |
||||
120 | /* This sets up SiteTree the same as when viewing a page through the frontend. Versioned defaults |
||||
121 | * to Stage, and then when viewing the front-end Versioned::choose_site_stage changes it to Live. |
||||
122 | * TODO: In 3.2 we should make the default Live, then change to Stage in the admin area (with a nicer API) |
||||
123 | */ |
||||
124 | if (class_exists(SiteTree::class)) { |
||||
125 | singleton(SiteTree::class)->extend('modelascontrollerInit', $this); |
||||
126 | } |
||||
127 | parent::init(); |
||||
128 | } |
||||
129 | |||||
130 | /** |
||||
131 | * Backslashes in fully qualified class names (e.g. NameSpaced\ClassName) |
||||
132 | * kills both requests (i.e. URIs) and XML (invalid character in a tag name) |
||||
133 | * So we'll replace them with a hyphen (-), as it's also unambiguious |
||||
134 | * in both cases (invalid in a php class name, and safe in an xml tag name) |
||||
135 | * |
||||
136 | * @param string $classname |
||||
137 | * @return string 'escaped' class name |
||||
138 | */ |
||||
139 | protected function sanitiseClassName($className) |
||||
140 | { |
||||
141 | return str_replace('\\', '-', $className); |
||||
142 | } |
||||
143 | |||||
144 | /** |
||||
145 | * Convert hyphen escaped class names back into fully qualified |
||||
146 | * PHP safe variant. |
||||
147 | * |
||||
148 | * @param string $classname |
||||
149 | * @return string syntactically valid classname |
||||
150 | */ |
||||
151 | protected function unsanitiseClassName($className) |
||||
152 | { |
||||
153 | return str_replace('-', '\\', $className); |
||||
154 | } |
||||
155 | |||||
156 | /** |
||||
157 | * Parse many many relation class (works with through array syntax) |
||||
158 | * |
||||
159 | * @param string|array $class |
||||
160 | * @return string|array |
||||
161 | */ |
||||
162 | public static function parseRelationClass($class) |
||||
163 | { |
||||
164 | // detect many many through syntax |
||||
165 | if (is_array($class) |
||||
166 | && array_key_exists('through', $class) |
||||
167 | && array_key_exists('to', $class) |
||||
168 | ) { |
||||
169 | $toRelation = $class['to']; |
||||
170 | |||||
171 | $hasOne = Config::inst()->get($class['through'], 'has_one'); |
||||
172 | if (empty($hasOne) || !is_array($hasOne) || !array_key_exists($toRelation, $hasOne)) { |
||||
173 | return $class; |
||||
174 | } |
||||
175 | |||||
176 | return $hasOne[$toRelation]; |
||||
177 | } |
||||
178 | |||||
179 | return $class; |
||||
180 | } |
||||
181 | |||||
182 | /** |
||||
183 | * This handler acts as the switchboard for the controller. |
||||
184 | * Since no $Action url-param is set, all requests are sent here. |
||||
185 | */ |
||||
186 | public function index(HTTPRequest $request) |
||||
187 | { |
||||
188 | $className = $this->resolveClassName($request); |
||||
189 | $id = $request->param('ID') ?: null; |
||||
190 | $relation = $request->param('Relation') ?: null; |
||||
191 | |||||
192 | // Check input formats |
||||
193 | if (!class_exists($className)) { |
||||
194 | return $this->notFound(); |
||||
195 | } |
||||
196 | if ($id && !is_numeric($id)) { |
||||
197 | return $this->notFound(); |
||||
198 | } |
||||
199 | if ($relation |
||||
200 | && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation) |
||||
201 | ) { |
||||
202 | return $this->notFound(); |
||||
203 | } |
||||
204 | |||||
205 | // if api access is disabled, don't proceed |
||||
206 | $apiAccess = Config::inst()->get($className, 'api_access'); |
||||
207 | if (!$apiAccess) { |
||||
208 | return $this->permissionFailure(); |
||||
209 | } |
||||
210 | |||||
211 | // authenticate through HTTP BasicAuth |
||||
212 | $this->member = $this->authenticate(); |
||||
0 ignored issues
–
show
It seems like
$this->authenticate() can also be of type false . However, the property $member is declared as type SilverStripe\Security\Member . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
![]() |
|||||
213 | |||||
214 | try { |
||||
215 | // handle different HTTP verbs |
||||
216 | if ($this->request->isGET() || $this->request->isHEAD()) { |
||||
217 | return $this->getHandler($className, $id, $relation); |
||||
0 ignored issues
–
show
It seems like
$id can also be of type string ; however, parameter $id of SilverStripe\RestfulServ...fulServer::getHandler() does only seem to accept integer , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
218 | } |
||||
219 | |||||
220 | if ($this->request->isPOST()) { |
||||
221 | return $this->postHandler($className, $id, $relation); |
||||
222 | } |
||||
223 | |||||
224 | if ($this->request->isPUT()) { |
||||
225 | return $this->putHandler($className, $id, $relation); |
||||
0 ignored issues
–
show
The call to
SilverStripe\RestfulServ...fulServer::putHandler() has too many arguments starting with $relation .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||
226 | } |
||||
227 | |||||
228 | if ($this->request->isDELETE()) { |
||||
229 | return $this->deleteHandler($className, $id, $relation); |
||||
0 ignored issues
–
show
The call to
SilverStripe\RestfulServ...Server::deleteHandler() has too many arguments starting with $relation .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||
230 | } |
||||
231 | } catch (\Exception $e) { |
||||
232 | return $this->exceptionThrown($this->getRequestDataFormatter($className), $e); |
||||
233 | } |
||||
234 | |||||
235 | // if no HTTP verb matches, return error |
||||
236 | return $this->methodNotAllowed(); |
||||
237 | } |
||||
238 | |||||
239 | /** |
||||
240 | * Handler for object read. |
||||
241 | * |
||||
242 | * The data object will be returned in the following format: |
||||
243 | * |
||||
244 | * <ClassName> |
||||
245 | * <FieldName>Value</FieldName> |
||||
246 | * ... |
||||
247 | * <HasOneRelName id="ForeignID" href="LinkToForeignRecordInAPI" /> |
||||
248 | * ... |
||||
249 | * <HasManyRelName> |
||||
250 | * <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" /> |
||||
251 | * <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" /> |
||||
252 | * </HasManyRelName> |
||||
253 | * ... |
||||
254 | * <ManyManyRelName> |
||||
255 | * <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" /> |
||||
256 | * <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" /> |
||||
257 | * </ManyManyRelName> |
||||
258 | * </ClassName> |
||||
259 | * |
||||
260 | * Access is controlled by two variables: |
||||
261 | * |
||||
262 | * - static $api_access must be set. This enables the API on a class by class basis |
||||
263 | * - $obj->canView() must return true. This lets you implement record-level security |
||||
264 | * |
||||
265 | * @todo Access checking |
||||
266 | * |
||||
267 | * @param string $className |
||||
268 | * @param int $id |
||||
269 | * @param string $relation |
||||
270 | * @return string The serialized representation of the requested object(s) - usually XML or JSON. |
||||
271 | */ |
||||
272 | protected function getHandler($className, $id, $relationName) |
||||
273 | { |
||||
274 | $sort = ['ID' => 'ASC']; |
||||
275 | |||||
276 | if ($sortQuery = $this->request->getVar('sort')) { |
||||
277 | /** @var DataObject $singleton */ |
||||
278 | $singleton = singleton($className); |
||||
279 | // Only apply a sort filter if it is a valid field on the DataObject |
||||
280 | if ($singleton && $singleton->hasDatabaseField($sortQuery)) { |
||||
281 | $sort = [ |
||||
282 | $sortQuery => $this->request->getVar('dir') === 'DESC' ? 'DESC' : 'ASC', |
||||
283 | ]; |
||||
284 | } |
||||
285 | } |
||||
286 | |||||
287 | $limit = [ |
||||
288 | 'start' => (int) $this->request->getVar('start'), |
||||
289 | 'limit' => (int) $this->request->getVar('limit'), |
||||
290 | ]; |
||||
291 | |||||
292 | $params = $this->request->getVars(); |
||||
293 | |||||
294 | $responseFormatter = $this->getResponseDataFormatter($className); |
||||
295 | if (!$responseFormatter) { |
||||
0 ignored issues
–
show
|
|||||
296 | return $this->unsupportedMediaType(); |
||||
297 | } |
||||
298 | |||||
299 | // $obj can be either a DataObject or a SS_List, |
||||
300 | // depending on the request |
||||
301 | if ($id) { |
||||
302 | // Format: /api/v1/<MyClass>/<ID> |
||||
303 | $obj = $this->getObjectQuery($className, $id, $params)->First(); |
||||
304 | if (!$obj) { |
||||
0 ignored issues
–
show
|
|||||
305 | return $this->notFound(); |
||||
306 | } |
||||
307 | if (!$obj->canView($this->getMember())) { |
||||
308 | return $this->permissionFailure(); |
||||
309 | } |
||||
310 | |||||
311 | // Format: /api/v1/<MyClass>/<ID>/<Relation> |
||||
312 | if ($relationName) { |
||||
313 | $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName); |
||||
314 | if (!$obj) { |
||||
315 | return $this->notFound(); |
||||
316 | } |
||||
317 | |||||
318 | // TODO Avoid creating data formatter again for relation class (see above) |
||||
319 | $responseFormatter = $this->getResponseDataFormatter($obj->dataClass()); |
||||
320 | } |
||||
321 | } else { |
||||
322 | // Format: /api/v1/<MyClass> |
||||
323 | $obj = $this->getObjectsQuery($className, $params, $sort, $limit); |
||||
324 | } |
||||
325 | |||||
326 | $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); |
||||
327 | |||||
328 | $rawFields = $this->request->getVar('fields'); |
||||
329 | $realFields = $responseFormatter->getRealFields($className, explode(',', $rawFields)); |
||||
330 | $fields = $rawFields ? $realFields : null; |
||||
331 | |||||
332 | if ($obj instanceof SS_List) { |
||||
333 | $objs = ArrayList::create($obj->toArray()); |
||||
334 | foreach ($objs as $obj) { |
||||
335 | if (!$obj->canView($this->getMember())) { |
||||
336 | $objs->remove($obj); |
||||
337 | } |
||||
338 | } |
||||
339 | $responseFormatter->setTotalSize($objs->count()); |
||||
340 | $this->extend('updateRestfulGetHandler', $objs, $responseFormatter); |
||||
341 | |||||
342 | return $responseFormatter->convertDataObjectSet($objs, $fields); |
||||
343 | } |
||||
344 | |||||
345 | if (!$obj) { |
||||
346 | $responseFormatter->setTotalSize(0); |
||||
347 | return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields); |
||||
0 ignored issues
–
show
The call to
SilverStripe\RestfulServ...:convertDataObjectSet() has too many arguments starting with $fields .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||
348 | } |
||||
349 | |||||
350 | $this->extend('updateRestfulGetHandler', $obj, $responseFormatter); |
||||
351 | |||||
352 | return $responseFormatter->convertDataObject($obj, $fields); |
||||
353 | } |
||||
354 | |||||
355 | /** |
||||
356 | * Uses the default {@link SearchContext} specified through |
||||
357 | * {@link DataObject::getDefaultSearchContext()} to augument |
||||
358 | * an existing query object (mostly a component query from {@link DataObject}) |
||||
359 | * with search clauses. |
||||
360 | * |
||||
361 | * @todo Allow specifying of different searchcontext getters on model-by-model basis |
||||
362 | * |
||||
363 | * @param string $className |
||||
364 | * @param array $params |
||||
365 | * @return SS_List |
||||
366 | */ |
||||
367 | protected function getSearchQuery( |
||||
368 | $className, |
||||
369 | $params = null, |
||||
370 | $sort = null, |
||||
371 | $limit = null, |
||||
372 | $existingQuery = null |
||||
373 | ) { |
||||
374 | if (singleton($className)->hasMethod('getRestfulSearchContext')) { |
||||
375 | $searchContext = singleton($className)->{'getRestfulSearchContext'}(); |
||||
376 | } else { |
||||
377 | $searchContext = singleton($className)->getDefaultSearchContext(); |
||||
378 | } |
||||
379 | return $searchContext->getQuery($params, $sort, $limit, $existingQuery); |
||||
380 | } |
||||
381 | |||||
382 | /** |
||||
383 | * Returns a dataformatter instance based on the request |
||||
384 | * extension or mimetype. Falls back to {@link self::$default_extension}. |
||||
385 | * |
||||
386 | * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers |
||||
387 | * @param string Classname of a DataObject |
||||
0 ignored issues
–
show
The type
SilverStripe\RestfulServer\Classname was not found. Maybe you did not declare it correctly or list all dependencies?
The issue could also be caused by a filter entry in the build configuration.
If the path has been excluded in your configuration, e.g. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||
388 | * @return DataFormatter |
||||
389 | */ |
||||
390 | protected function getDataFormatter($includeAcceptHeader = false, $className = null) |
||||
391 | { |
||||
392 | $extension = $this->request->getExtension(); |
||||
393 | $contentTypeWithEncoding = $this->request->getHeader('Content-Type'); |
||||
394 | preg_match('/([^;]*)/', $contentTypeWithEncoding, $contentTypeMatches); |
||||
395 | $contentType = $contentTypeMatches[0]; |
||||
396 | $accept = $this->request->getHeader('Accept'); |
||||
397 | $mimetypes = $this->request->getAcceptMimetypes(); |
||||
398 | if (!$className) { |
||||
399 | $className = $this->resolveClassName($this->request); |
||||
400 | } |
||||
401 | |||||
402 | // get formatter |
||||
403 | if (!empty($extension)) { |
||||
404 | $formatter = DataFormatter::for_extension($extension); |
||||
405 | } elseif ($includeAcceptHeader && !empty($accept) && strpos($accept, '*/*') === false) { |
||||
406 | $formatter = DataFormatter::for_mimetypes($mimetypes); |
||||
407 | if (!$formatter) { |
||||
0 ignored issues
–
show
|
|||||
408 | $formatter = DataFormatter::for_extension($this->config()->default_extension); |
||||
409 | } |
||||
410 | } elseif (!empty($contentType)) { |
||||
411 | $formatter = DataFormatter::for_mimetype($contentType); |
||||
412 | } else { |
||||
413 | $formatter = DataFormatter::for_extension($this->config()->default_extension); |
||||
414 | } |
||||
415 | |||||
416 | if (!$formatter) { |
||||
0 ignored issues
–
show
|
|||||
417 | return false; |
||||
418 | } |
||||
419 | |||||
420 | // set custom fields |
||||
421 | if ($customAddFields = $this->request->getVar('add_fields')) { |
||||
422 | $customAddFields = $formatter->getRealFields($className, explode(',', $customAddFields)); |
||||
423 | $formatter->setCustomAddFields($customAddFields); |
||||
424 | } |
||||
425 | if ($customFields = $this->request->getVar('fields')) { |
||||
426 | $customFields = $formatter->getRealFields($className, explode(',', $customFields)); |
||||
427 | $formatter->setCustomFields($customFields); |
||||
428 | } |
||||
429 | $formatter->setCustomRelations($this->getAllowedRelations($className)); |
||||
430 | |||||
431 | $apiAccess = Config::inst()->get($className, 'api_access'); |
||||
432 | if (is_array($apiAccess)) { |
||||
433 | $formatter->setCustomAddFields( |
||||
434 | array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view']) |
||||
435 | ); |
||||
436 | if ($formatter->getCustomFields()) { |
||||
437 | $formatter->setCustomFields( |
||||
438 | array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view']) |
||||
439 | ); |
||||
440 | } else { |
||||
441 | $formatter->setCustomFields((array)$apiAccess['view']); |
||||
442 | } |
||||
443 | if ($formatter->getCustomRelations()) { |
||||
444 | $formatter->setCustomRelations( |
||||
445 | array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view']) |
||||
446 | ); |
||||
447 | } else { |
||||
448 | $formatter->setCustomRelations((array)$apiAccess['view']); |
||||
449 | } |
||||
450 | } |
||||
451 | |||||
452 | // set relation depth |
||||
453 | $relationDepth = $this->request->getVar('relationdepth'); |
||||
454 | if (is_numeric($relationDepth)) { |
||||
455 | $formatter->relationDepth = (int)$relationDepth; |
||||
456 | } |
||||
457 | |||||
458 | return $formatter; |
||||
459 | } |
||||
460 | |||||
461 | /** |
||||
462 | * @param string Classname of a DataObject |
||||
463 | * @return DataFormatter |
||||
464 | */ |
||||
465 | protected function getRequestDataFormatter($className = null) |
||||
466 | { |
||||
467 | return $this->getDataFormatter(false, $className); |
||||
468 | } |
||||
469 | |||||
470 | /** |
||||
471 | * @param string Classname of a DataObject |
||||
472 | * @return DataFormatter |
||||
473 | */ |
||||
474 | protected function getResponseDataFormatter($className = null) |
||||
475 | { |
||||
476 | return $this->getDataFormatter(true, $className); |
||||
477 | } |
||||
478 | |||||
479 | /** |
||||
480 | * Handler for object delete |
||||
481 | */ |
||||
482 | protected function deleteHandler($className, $id) |
||||
483 | { |
||||
484 | $obj = DataObject::get_by_id($className, $id); |
||||
485 | if (!$obj) { |
||||
0 ignored issues
–
show
|
|||||
486 | return $this->notFound(); |
||||
487 | } |
||||
488 | if (!$obj->canDelete($this->getMember())) { |
||||
489 | return $this->permissionFailure(); |
||||
490 | } |
||||
491 | |||||
492 | $obj->delete(); |
||||
493 | |||||
494 | $this->getResponse()->setStatusCode(204); // No Content |
||||
495 | return true; |
||||
496 | } |
||||
497 | |||||
498 | /** |
||||
499 | * Handler for object write |
||||
500 | */ |
||||
501 | protected function putHandler($className, $id) |
||||
502 | { |
||||
503 | $obj = DataObject::get_by_id($className, $id); |
||||
504 | if (!$obj) { |
||||
0 ignored issues
–
show
|
|||||
505 | return $this->notFound(); |
||||
506 | } |
||||
507 | |||||
508 | if (!$obj->canEdit($this->getMember())) { |
||||
509 | return $this->permissionFailure(); |
||||
510 | } |
||||
511 | |||||
512 | $reqFormatter = $this->getRequestDataFormatter($className); |
||||
513 | if (!$reqFormatter) { |
||||
0 ignored issues
–
show
|
|||||
514 | return $this->unsupportedMediaType(); |
||||
515 | } |
||||
516 | |||||
517 | $responseFormatter = $this->getResponseDataFormatter($className); |
||||
518 | if (!$responseFormatter) { |
||||
0 ignored issues
–
show
|
|||||
519 | return $this->unsupportedMediaType(); |
||||
520 | } |
||||
521 | |||||
522 | try { |
||||
523 | /** @var DataObject|string */ |
||||
524 | $obj = $this->updateDataObject($obj, $reqFormatter); |
||||
525 | } catch (ValidationException $e) { |
||||
526 | return $this->validationFailure($responseFormatter, $e->getResult()); |
||||
527 | } |
||||
528 | |||||
529 | if (is_string($obj)) { |
||||
530 | return $obj; |
||||
531 | } |
||||
532 | |||||
533 | $this->getResponse()->setStatusCode(202); // Accepted |
||||
534 | $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); |
||||
535 | |||||
536 | // Append the default extension for the output format to the Location header |
||||
537 | // or else we'll use the default (XML) |
||||
538 | $types = $responseFormatter->supportedExtensions(); |
||||
539 | $type = ''; |
||||
540 | if (count($types)) { |
||||
541 | $type = ".{$types[0]}"; |
||||
542 | } |
||||
543 | |||||
544 | $urlSafeClassName = $this->sanitiseClassName(get_class($obj)); |
||||
545 | $apiBase = $this->config()->api_base; |
||||
546 | $objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type); |
||||
547 | $this->getResponse()->addHeader('Location', $objHref); |
||||
548 | |||||
549 | return $responseFormatter->convertDataObject($obj); |
||||
550 | } |
||||
551 | |||||
552 | /** |
||||
553 | * Handler for object append / method call. |
||||
554 | * |
||||
555 | * @todo Posting to an existing URL (without a relation) |
||||
556 | * current resolves in creatig a new element, |
||||
557 | * rather than a "Conflict" message. |
||||
558 | */ |
||||
559 | protected function postHandler($className, $id, $relation) |
||||
560 | { |
||||
561 | if ($id) { |
||||
562 | if (!$relation) { |
||||
563 | $this->response->setStatusCode(409); |
||||
564 | return 'Conflict'; |
||||
565 | } |
||||
566 | |||||
567 | $obj = DataObject::get_by_id($className, $id); |
||||
568 | if (!$obj) { |
||||
0 ignored issues
–
show
|
|||||
569 | return $this->notFound(); |
||||
570 | } |
||||
571 | |||||
572 | $reqFormatter = $this->getRequestDataFormatter($className); |
||||
573 | if (!$reqFormatter) { |
||||
0 ignored issues
–
show
|
|||||
574 | return $this->unsupportedMediaType(); |
||||
575 | } |
||||
576 | |||||
577 | $relation = $reqFormatter->getRealFieldName($className, $relation); |
||||
578 | |||||
579 | if (!$obj->hasMethod($relation)) { |
||||
580 | return $this->notFound(); |
||||
581 | } |
||||
582 | |||||
583 | if (!Config::inst()->get($className, 'allowed_actions') || |
||||
584 | !in_array($relation, Config::inst()->get($className, 'allowed_actions'))) { |
||||
585 | return $this->permissionFailure(); |
||||
586 | } |
||||
587 | |||||
588 | $obj->$relation(); |
||||
589 | |||||
590 | $this->getResponse()->setStatusCode(204); // No Content |
||||
591 | return true; |
||||
592 | } |
||||
593 | |||||
594 | if (!singleton($className)->canCreate($this->getMember())) { |
||||
595 | return $this->permissionFailure(); |
||||
596 | } |
||||
597 | |||||
598 | $obj = Injector::inst()->create($className); |
||||
599 | |||||
600 | $reqFormatter = $this->getRequestDataFormatter($className); |
||||
601 | if (!$reqFormatter) { |
||||
0 ignored issues
–
show
|
|||||
602 | return $this->unsupportedMediaType(); |
||||
603 | } |
||||
604 | |||||
605 | $responseFormatter = $this->getResponseDataFormatter($className); |
||||
606 | |||||
607 | try { |
||||
608 | /** @var DataObject|string $obj */ |
||||
609 | $obj = $this->updateDataObject($obj, $reqFormatter); |
||||
0 ignored issues
–
show
It seems like
$obj can also be of type string ; however, parameter $obj of SilverStripe\RestfulServ...ver::updateDataObject() does only seem to accept SilverStripe\ORM\DataObject , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
610 | } catch (ValidationException $e) { |
||||
611 | return $this->validationFailure($responseFormatter, $e->getResult()); |
||||
612 | } |
||||
613 | |||||
614 | if (is_string($obj)) { |
||||
615 | return $obj; |
||||
616 | } |
||||
617 | |||||
618 | $this->getResponse()->setStatusCode(201); // Created |
||||
619 | $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); |
||||
620 | |||||
621 | // Append the default extension for the output format to the Location header |
||||
622 | // or else we'll use the default (XML) |
||||
623 | $types = $responseFormatter->supportedExtensions(); |
||||
624 | $type = ''; |
||||
625 | if (count($types)) { |
||||
626 | $type = ".{$types[0]}"; |
||||
627 | } |
||||
628 | |||||
629 | // Deviate slightly from the spec: Helps datamodel API access restrict |
||||
630 | // to consulting just canCreate(), not canView() as a result of the additional |
||||
631 | // "Location" header. |
||||
632 | if ($this->config()->get('location_header_on_create')) { |
||||
633 | $urlSafeClassName = $this->sanitiseClassName(get_class($obj)); |
||||
634 | $apiBase = $this->config()->api_base; |
||||
635 | $objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type); |
||||
636 | $this->getResponse()->addHeader('Location', $objHref); |
||||
637 | } |
||||
638 | |||||
639 | return $responseFormatter->convertDataObject($obj); |
||||
640 | } |
||||
641 | |||||
642 | /** |
||||
643 | * Converts either the given HTTP Body into an array |
||||
644 | * (based on the DataFormatter instance), or returns |
||||
645 | * the POST variables. |
||||
646 | * Automatically filters out certain critical fields |
||||
647 | * that shouldn't be set by the client (e.g. ID). |
||||
648 | * |
||||
649 | * @param DataObject $obj |
||||
650 | * @param DataFormatter $formatter |
||||
651 | * @return DataObject|string The passed object, or "No Content" if incomplete input data is provided |
||||
652 | */ |
||||
653 | protected function updateDataObject($obj, $formatter) |
||||
654 | { |
||||
655 | // if neither an http body nor POST data is present, return error |
||||
656 | $body = $this->request->getBody(); |
||||
657 | if (!$body && !$this->request->postVars()) { |
||||
658 | $this->getResponse()->setStatusCode(204); // No Content |
||||
659 | return 'No Content'; |
||||
660 | } |
||||
661 | |||||
662 | if (!empty($body)) { |
||||
663 | $rawdata = $formatter->convertStringToArray($body); |
||||
0 ignored issues
–
show
Are you sure the assignment to
$rawdata is correct as $formatter->convertStringToArray($body) targeting SilverStripe\RestfulServ...:convertStringToArray() seems to always return null.
This check looks for function or method calls that always return null and whose return value is assigned to a variable. class A
{
function getObject()
{
return null;
}
}
$a = new A();
$object = $a->getObject();
The method The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes. ![]() |
|||||
664 | } else { |
||||
665 | // assume application/x-www-form-urlencoded which is automatically parsed by PHP |
||||
666 | $rawdata = $this->request->postVars(); |
||||
667 | } |
||||
668 | |||||
669 | $className = $obj->ClassName; |
||||
670 | // update any aliased field names |
||||
671 | $data = []; |
||||
672 | foreach ($rawdata as $key => $value) { |
||||
673 | $newkey = $formatter->getRealFieldName($className, $key); |
||||
674 | $data[$newkey] = $value; |
||||
675 | } |
||||
676 | |||||
677 | // @todo Disallow editing of certain keys in database |
||||
678 | $data = array_diff_key($data, ['ID', 'Created']); |
||||
679 | |||||
680 | $apiAccess = singleton($className)->config()->api_access; |
||||
681 | if (is_array($apiAccess) && isset($apiAccess['edit'])) { |
||||
682 | $data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit'])); |
||||
0 ignored issues
–
show
It seems like
array_combine($apiAccess...'], $apiAccess['edit']) can also be of type false ; however, parameter $array2 of array_intersect_key() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
683 | } |
||||
684 | |||||
685 | $obj->update($data); |
||||
686 | $obj->write(); |
||||
687 | |||||
688 | return $obj; |
||||
689 | } |
||||
690 | |||||
691 | /** |
||||
692 | * Gets a single DataObject by ID, |
||||
693 | * through a request like /api/v1/<MyClass>/<MyID> |
||||
694 | * |
||||
695 | * @param string $className |
||||
696 | * @param int $id |
||||
697 | * @param array $params |
||||
698 | * @return DataList |
||||
699 | */ |
||||
700 | protected function getObjectQuery($className, $id, $params) |
||||
701 | { |
||||
702 | return DataList::create($className)->byIDs([$id]); |
||||
703 | } |
||||
704 | |||||
705 | /** |
||||
706 | * @param DataObject $obj |
||||
707 | * @param array $params |
||||
708 | * @param int|array $sort |
||||
709 | * @param int|array $limit |
||||
710 | * @return SQLQuery |
||||
0 ignored issues
–
show
The type
SilverStripe\RestfulServer\SQLQuery was not found. Maybe you did not declare it correctly or list all dependencies?
The issue could also be caused by a filter entry in the build configuration.
If the path has been excluded in your configuration, e.g. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||
711 | */ |
||||
712 | protected function getObjectsQuery($className, $params, $sort, $limit) |
||||
713 | { |
||||
714 | return $this->getSearchQuery($className, $params, $sort, $limit); |
||||
0 ignored issues
–
show
|
|||||
715 | } |
||||
716 | |||||
717 | |||||
718 | /** |
||||
719 | * @param DataObject $obj |
||||
720 | * @param array $params |
||||
721 | * @param int|array $sort |
||||
722 | * @param int|array $limit |
||||
723 | * @param string $relationName |
||||
724 | * @return SQLQuery|boolean |
||||
725 | */ |
||||
726 | protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) |
||||
727 | { |
||||
728 | // The relation method will return a DataList, that getSearchQuery subsequently manipulates |
||||
729 | if ($obj->hasMethod($relationName)) { |
||||
730 | // $this->HasOneName() will return a dataobject or null, neither |
||||
731 | // of which helps us get the classname in a consistent fashion. |
||||
732 | // So we must use a way that is reliable. |
||||
733 | if ($relationClass = DataObject::getSchema()->hasOneComponent(get_class($obj), $relationName)) { |
||||
734 | $joinField = $relationName . 'ID'; |
||||
735 | // Again `byID` will return the wrong type for our purposes. So use `byIDs` |
||||
736 | $list = DataList::create($relationClass)->byIDs([$obj->$joinField]); |
||||
737 | } else { |
||||
738 | $list = $obj->$relationName(); |
||||
739 | } |
||||
740 | |||||
741 | $apiAccess = Config::inst()->get($list->dataClass(), 'api_access'); |
||||
742 | |||||
743 | |||||
744 | if (!$apiAccess) { |
||||
745 | return false; |
||||
746 | } |
||||
747 | |||||
748 | return $this->getSearchQuery($list->dataClass(), $params, $sort, $limit, $list); |
||||
0 ignored issues
–
show
|
|||||
749 | } |
||||
750 | } |
||||
751 | |||||
752 | /** |
||||
753 | * @return string |
||||
754 | */ |
||||
755 | protected function permissionFailure() |
||||
756 | { |
||||
757 | // return a 401 |
||||
758 | $this->getResponse()->setStatusCode(401); |
||||
759 | $this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"'); |
||||
760 | $this->getResponse()->addHeader('Content-Type', 'text/plain'); |
||||
761 | |||||
762 | $response = "You don't have access to this item through the API."; |
||||
763 | $this->extend(__FUNCTION__, $response); |
||||
764 | |||||
765 | return $response; |
||||
766 | } |
||||
767 | |||||
768 | /** |
||||
769 | * @return string |
||||
770 | */ |
||||
771 | protected function notFound() |
||||
772 | { |
||||
773 | // return a 404 |
||||
774 | $this->getResponse()->setStatusCode(404); |
||||
775 | $this->getResponse()->addHeader('Content-Type', 'text/plain'); |
||||
776 | |||||
777 | $response = "That object wasn't found"; |
||||
778 | $this->extend(__FUNCTION__, $response); |
||||
779 | |||||
780 | return $response; |
||||
781 | } |
||||
782 | |||||
783 | /** |
||||
784 | * @return string |
||||
785 | */ |
||||
786 | protected function methodNotAllowed() |
||||
787 | { |
||||
788 | $this->getResponse()->setStatusCode(405); |
||||
789 | $this->getResponse()->addHeader('Content-Type', 'text/plain'); |
||||
790 | |||||
791 | $response = "Method Not Allowed"; |
||||
792 | $this->extend(__FUNCTION__, $response); |
||||
793 | |||||
794 | return $response; |
||||
795 | } |
||||
796 | |||||
797 | /** |
||||
798 | * @return string |
||||
799 | */ |
||||
800 | protected function unsupportedMediaType() |
||||
801 | { |
||||
802 | $this->response->setStatusCode(415); // Unsupported Media Type |
||||
803 | $this->getResponse()->addHeader('Content-Type', 'text/plain'); |
||||
804 | |||||
805 | $response = "Unsupported Media Type"; |
||||
806 | $this->extend(__FUNCTION__, $response); |
||||
807 | |||||
808 | return $response; |
||||
809 | } |
||||
810 | |||||
811 | /** |
||||
812 | * @param ValidationResult $result |
||||
813 | * @return mixed |
||||
814 | */ |
||||
815 | protected function validationFailure(DataFormatter $responseFormatter, ValidationResult $result) |
||||
816 | { |
||||
817 | $this->getResponse()->setStatusCode(400); |
||||
818 | $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); |
||||
819 | |||||
820 | $response = [ |
||||
821 | 'type' => ValidationException::class, |
||||
822 | 'messages' => $result->getMessages(), |
||||
823 | ]; |
||||
824 | |||||
825 | $this->extend(__FUNCTION__, $response, $result); |
||||
826 | |||||
827 | return $responseFormatter->convertArray($response); |
||||
828 | } |
||||
829 | |||||
830 | /** |
||||
831 | * @param DataFormatter $responseFormatter |
||||
832 | * @param \Exception $e |
||||
833 | * @return string |
||||
834 | */ |
||||
835 | protected function exceptionThrown(DataFormatter $responseFormatter, \Exception $e) |
||||
836 | { |
||||
837 | $this->getResponse()->setStatusCode(500); |
||||
838 | $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); |
||||
839 | |||||
840 | $response = [ |
||||
841 | 'type' => get_class($e), |
||||
842 | 'message' => $e->getMessage(), |
||||
843 | ]; |
||||
844 | |||||
845 | $this->extend(__FUNCTION__, $response, $e); |
||||
846 | |||||
847 | return $responseFormatter->convertArray($response); |
||||
848 | } |
||||
849 | |||||
850 | /** |
||||
851 | * A function to authenticate a user |
||||
852 | * |
||||
853 | * @return Member|false the logged in member |
||||
854 | */ |
||||
855 | protected function authenticate() |
||||
856 | { |
||||
857 | $authClass = $this->config()->authenticator; |
||||
858 | $member = $authClass::authenticate(); |
||||
859 | Security::setCurrentUser($member); |
||||
860 | return $member; |
||||
861 | } |
||||
862 | |||||
863 | /** |
||||
864 | * Return only relations which have $api_access enabled. |
||||
865 | * @todo Respect field level permissions once they are available in core |
||||
866 | * |
||||
867 | * @param string $class |
||||
868 | * @param Member $member |
||||
869 | * @return array |
||||
870 | */ |
||||
871 | protected function getAllowedRelations($class, $member = null) |
||||
872 | { |
||||
873 | $allowedRelations = []; |
||||
874 | $obj = singleton($class); |
||||
875 | $relations = (array)$obj->hasOne() + (array)$obj->hasMany() + (array)$obj->manyMany(); |
||||
876 | if ($relations) { |
||||
877 | foreach ($relations as $relName => $relClass) { |
||||
878 | $relClass = static::parseRelationClass($relClass); |
||||
879 | |||||
880 | //remove dot notation from relation names |
||||
881 | $parts = explode('.', $relClass); |
||||
0 ignored issues
–
show
It seems like
$relClass can also be of type array ; however, parameter $string of explode() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
882 | $relClass = array_shift($parts); |
||||
883 | if (Config::inst()->get($relClass, 'api_access')) { |
||||
884 | $allowedRelations[] = $relName; |
||||
885 | } |
||||
886 | } |
||||
887 | } |
||||
888 | return $allowedRelations; |
||||
889 | } |
||||
890 | |||||
891 | /** |
||||
892 | * Get the current Member, if available |
||||
893 | * |
||||
894 | * @return Member|null |
||||
895 | */ |
||||
896 | protected function getMember() |
||||
897 | { |
||||
898 | return Security::getCurrentUser(); |
||||
899 | } |
||||
900 | |||||
901 | /** |
||||
902 | * Checks if given param ClassName maps to an object in endpoint_aliases, |
||||
903 | * else simply return the unsanitised version of ClassName |
||||
904 | * |
||||
905 | * @param HTTPRequest $request |
||||
906 | * @return string |
||||
907 | */ |
||||
908 | protected function resolveClassName(HTTPRequest $request) |
||||
909 | { |
||||
910 | $className = $request->param('ClassName'); |
||||
911 | $aliases = self::config()->get('endpoint_aliases'); |
||||
912 | |||||
913 | return empty($aliases[$className]) ? $this->unsanitiseClassName($className) : $aliases[$className]; |
||||
914 | } |
||||
915 | } |
||||
916 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths