Passed
Push — master ( eaef7a...5f7861 )
by Robbie
03:03 queued 01:21
created

RestfulServer::methodNotAllowed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\RestfulServer;
4
5
use SilverStripe\ORM\ArrayList;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\ORM\DataList;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\ORM\SS_List;
13
use SilverStripe\ORM\ValidationException;
14
use SilverStripe\ORM\ValidationResult;
15
use SilverStripe\Security\Member;
16
use SilverStripe\Security\Security;
17
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTree 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. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
19
/**
20
 * Generic RESTful server, which handles webservice access to arbitrary DataObjects.
21
 * Relies on serialization/deserialization into different formats provided
22
 * by the DataFormatter APIs in core.
23
 *
24
 * @todo Implement PUT/POST/DELETE for relations
25
 * @todo Access-Control for relations (you might be allowed to view Members and Groups,
26
 *       but not their relation with each other)
27
 * @todo Make SearchContext specification customizeable for each class
28
 * @todo Allow for range-searches (e.g. on Created column)
29
 * @todo Filter relation listings by $api_access and canView() permissions
30
 * @todo Exclude relations when "fields" are specified through URL (they should be explicitly
31
 *       requested in this case)
32
 * @todo Custom filters per DataObject subclass, e.g. to disallow showing unpublished pages in
33
 * SiteTree/Versioned/Hierarchy
34
 * @todo URL parameter namespacing for search-fields, limit, fields, add_fields
35
 *       (might all be valid dataobject properties)
36
 *       e.g. you wouldn't be able to search for a "limit" property on your subclass as
37
 *       its overlayed with the search logic
38
 * @todo i18n integration (e.g. Page/1.xml?lang=de_DE)
39
 * @todo Access to extendable methods/relations like SiteTree/1/Versions or SiteTree/1/Version/22
40
 * @todo Respect $api_access array notation in search contexts
41
 */
42
class RestfulServer extends Controller
43
{
44
    private static $url_handlers = array(
45
        '$ClassName!/$ID/$Relation' => 'handleAction',
46
        '' => 'notFound'
47
    );
48
49
    /**
50
     * @config
51
     * @var string root of the api route, MUST have a trailing slash
52
     */
53
    private static $api_base = "api/v1/";
54
55
    /**
56
     * @config
57
     * @var string Class name for an authenticator to use on API access
58
     */
59
    private static $authenticator = BasicRestfulAuthenticator::class;
60
61
    /**
62
     * If no extension is given in the request, resolve to this extension
63
     * (and subsequently the {@link self::$default_mimetype}.
64
     *
65
     * @var string
66
     */
67
    private static $default_extension = "xml";
68
69
    /**
70
     * If no extension is given, resolve the request to this mimetype.
71
     *
72
     * @var string
73
     */
74
    protected static $default_mimetype = "text/xml";
75
76
    /**
77
     * @uses authenticate()
78
     * @var Member
79
     */
80
    protected $member;
81
82
    private static $allowed_actions = array(
83
        'index',
84
        'notFound'
85
    );
86
87
    public function init()
88
    {
89
        /* This sets up SiteTree the same as when viewing a page through the frontend. Versioned defaults
90
         * to Stage, and then when viewing the front-end Versioned::choose_site_stage changes it to Live.
91
         * TODO: In 3.2 we should make the default Live, then change to Stage in the admin area (with a nicer API)
92
         */
93
        if (class_exists(SiteTree::class)) {
94
            singleton(SiteTree::class)->extend('modelascontrollerInit', $this);
95
        }
96
        parent::init();
97
    }
98
99
    /**
100
     * Backslashes in fully qualified class names (e.g. NameSpaced\ClassName)
101
     * kills both requests (i.e. URIs) and XML (invalid character in a tag name)
102
     * So we'll replace them with a hyphen (-), as it's also unambiguious
103
     * in both cases (invalid in a php class name, and safe in an xml tag name)
104
     *
105
     * @param string $classname
106
     * @return string 'escaped' class name
107
     */
108
    protected function sanitiseClassName($className)
109
    {
110
        return str_replace('\\', '-', $className);
111
    }
112
113
    /**
114
     * Convert hyphen escaped class names back into fully qualified
115
     * PHP safe variant.
116
     *
117
     * @param string $classname
118
     * @return string syntactically valid classname
119
     */
120
    protected function unsanitiseClassName($className)
121
    {
122
        return str_replace('-', '\\', $className);
123
    }
124
125
    /**
126
     * This handler acts as the switchboard for the controller.
127
     * Since no $Action url-param is set, all requests are sent here.
128
     */
129
    public function index(HTTPRequest $request)
130
    {
131
        $className = $this->unsanitiseClassName($request->param('ClassName'));
132
        $id = $request->param('ID') ?: null;
133
        $relation = $request->param('Relation') ?: null;
134
135
        // Check input formats
136
        if (!class_exists($className)) {
137
            return $this->notFound();
138
        }
139
        if ($id && !is_numeric($id)) {
140
            return $this->notFound();
141
        }
142
        if ($relation
143
            && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)
144
        ) {
145
            return $this->notFound();
146
        }
147
148
        // if api access is disabled, don't proceed
149
        $apiAccess = Config::inst()->get($className, 'api_access');
150
        if (!$apiAccess) {
151
            return $this->permissionFailure();
152
        }
153
154
        // authenticate through HTTP BasicAuth
155
        $this->member = $this->authenticate();
0 ignored issues
show
Documentation Bug introduced by
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 $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

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;
}
Loading history...
156
157
        try {
158
            // handle different HTTP verbs
159
            if ($this->request->isGET() || $this->request->isHEAD()) {
160
                return $this->getHandler($className, $id, $relation);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

160
                return $this->getHandler($className, /** @scrutinizer ignore-type */ $id, $relation);
Loading history...
161
            }
162
163
            if ($this->request->isPOST()) {
164
                return $this->postHandler($className, $id, $relation);
165
            }
166
167
            if ($this->request->isPUT()) {
168
                return $this->putHandler($className, $id, $relation);
0 ignored issues
show
Unused Code introduced by
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 ignore-call  annotation

168
                return $this->/** @scrutinizer ignore-call */ putHandler($className, $id, $relation);

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.

Loading history...
169
            }
170
171
            if ($this->request->isDELETE()) {
172
                return $this->deleteHandler($className, $id, $relation);
0 ignored issues
show
Unused Code introduced by
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 ignore-call  annotation

172
                return $this->/** @scrutinizer ignore-call */ deleteHandler($className, $id, $relation);

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.

Loading history...
173
            }
174
        } catch (\Exception $e) {
175
            return $this->exceptionThrown($this->getRequestDataFormatter($className), $e);
176
        }
177
178
        // if no HTTP verb matches, return error
179
        return $this->methodNotAllowed();
180
    }
181
182
    /**
183
     * Handler for object read.
184
     *
185
     * The data object will be returned in the following format:
186
     *
187
     * <ClassName>
188
     *   <FieldName>Value</FieldName>
189
     *   ...
190
     *   <HasOneRelName id="ForeignID" href="LinkToForeignRecordInAPI" />
191
     *   ...
192
     *   <HasManyRelName>
193
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
194
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
195
     *   </HasManyRelName>
196
     *   ...
197
     *   <ManyManyRelName>
198
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
199
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
200
     *   </ManyManyRelName>
201
     * </ClassName>
202
     *
203
     * Access is controlled by two variables:
204
     *
205
     *   - static $api_access must be set. This enables the API on a class by class basis
206
     *   - $obj->canView() must return true. This lets you implement record-level security
207
     *
208
     * @todo Access checking
209
     *
210
     * @param string $className
211
     * @param Int $id
212
     * @param string $relation
213
     * @return string The serialized representation of the requested object(s) - usually XML or JSON.
214
     */
215
    protected function getHandler($className, $id, $relationName)
216
    {
217
        $sort = '';
218
219
        if ($this->request->getVar('sort')) {
220
            $dir = $this->request->getVar('dir');
221
            $sort = array($this->request->getVar('sort') => ($dir ? $dir : 'ASC'));
222
        }
223
224
        $limit = array(
225
            'start' => $this->request->getVar('start'),
226
            'limit' => $this->request->getVar('limit')
227
        );
228
229
        $params = $this->request->getVars();
230
231
        $responseFormatter = $this->getResponseDataFormatter($className);
232
        if (!$responseFormatter) {
0 ignored issues
show
introduced by
$responseFormatter is of type SilverStripe\RestfulServer\DataFormatter, thus it always evaluated to true.
Loading history...
233
            return $this->unsupportedMediaType();
234
        }
235
236
        // $obj can be either a DataObject or a SS_List,
237
        // depending on the request
238
        if ($id) {
239
            // Format: /api/v1/<MyClass>/<ID>
240
            $obj = $this->getObjectQuery($className, $id, $params)->First();
241
            if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
242
                return $this->notFound();
243
            }
244
            if (!$obj->canView($this->getMember())) {
245
                return $this->permissionFailure();
246
            }
247
248
            // Format: /api/v1/<MyClass>/<ID>/<Relation>
249
            if ($relationName) {
250
                $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
0 ignored issues
show
Bug introduced by
It seems like $sort can also be of type string; however, parameter $sort of SilverStripe\RestfulServ...etObjectRelationQuery() does only seem to accept integer|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 ignore-type  annotation

250
                $obj = $this->getObjectRelationQuery($obj, $params, /** @scrutinizer ignore-type */ $sort, $limit, $relationName);
Loading history...
251
                if (!$obj) {
252
                    return $this->notFound();
253
                }
254
255
                // TODO Avoid creating data formatter again for relation class (see above)
256
                $responseFormatter = $this->getResponseDataFormatter($obj->dataClass());
257
            }
258
        } else {
259
            // Format: /api/v1/<MyClass>
260
            $obj = $this->getObjectsQuery($className, $params, $sort, $limit);
0 ignored issues
show
Bug introduced by
It seems like $sort can also be of type string; however, parameter $sort of SilverStripe\RestfulServ...rver::getObjectsQuery() does only seem to accept integer|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 ignore-type  annotation

260
            $obj = $this->getObjectsQuery($className, $params, /** @scrutinizer ignore-type */ $sort, $limit);
Loading history...
261
        }
262
263
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
264
265
        $rawFields = $this->request->getVar('fields');
266
        $realFields = $responseFormatter->getRealFields($className, explode(',', $rawFields));
267
        $fields = $rawFields ? $realFields : null;
268
269
        if ($obj instanceof SS_List) {
270
            $objs = ArrayList::create($obj->toArray());
271
            foreach ($objs as $obj) {
272
                if (!$obj->canView($this->getMember())) {
273
                    $objs->remove($obj);
274
                }
275
            }
276
            $responseFormatter->setTotalSize($objs->count());
277
            return $responseFormatter->convertDataObjectSet($objs, $fields);
0 ignored issues
show
Unused Code introduced by
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 ignore-call  annotation

277
            return $responseFormatter->/** @scrutinizer ignore-call */ convertDataObjectSet($objs, $fields);

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.

Loading history...
278
        }
279
280
        if (!$obj) {
281
            $responseFormatter->setTotalSize(0);
282
            return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields);
283
        }
284
285
        return $responseFormatter->convertDataObject($obj, $fields);
0 ignored issues
show
Bug introduced by
It seems like $obj can also be of type true; however, parameter $do of SilverStripe\RestfulServ...er::convertDataObject() does only seem to accept SilverStripe\ORM\DataObjectInterface, 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 ignore-type  annotation

285
        return $responseFormatter->convertDataObject(/** @scrutinizer ignore-type */ $obj, $fields);
Loading history...
Unused Code introduced by
The call to SilverStripe\RestfulServ...er::convertDataObject() 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 ignore-call  annotation

285
        return $responseFormatter->/** @scrutinizer ignore-call */ convertDataObject($obj, $fields);

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.

Loading history...
286
    }
287
288
    /**
289
     * Uses the default {@link SearchContext} specified through
290
     * {@link DataObject::getDefaultSearchContext()} to augument
291
     * an existing query object (mostly a component query from {@link DataObject})
292
     * with search clauses.
293
     *
294
     * @todo Allow specifying of different searchcontext getters on model-by-model basis
295
     *
296
     * @param string $className
297
     * @param array $params
298
     * @return SS_List
299
     */
300
    protected function getSearchQuery(
301
        $className,
302
        $params = null,
303
        $sort = null,
304
        $limit = null,
305
        $existingQuery = null
306
    ) {
307
        if (singleton($className)->hasMethod('getRestfulSearchContext')) {
308
            $searchContext = singleton($className)->{'getRestfulSearchContext'}();
309
        } else {
310
            $searchContext = singleton($className)->getDefaultSearchContext();
311
        }
312
        return $searchContext->getQuery($params, $sort, $limit, $existingQuery);
313
    }
314
315
    /**
316
     * Returns a dataformatter instance based on the request
317
     * extension or mimetype. Falls back to {@link self::$default_extension}.
318
     *
319
     * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers
320
     * @param string Classname of a DataObject
0 ignored issues
show
Bug introduced by
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. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
321
     * @return DataFormatter
322
     */
323
    protected function getDataFormatter($includeAcceptHeader = false, $className = null)
324
    {
325
        $extension = $this->request->getExtension();
326
        $contentTypeWithEncoding = $this->request->getHeader('Content-Type');
327
        preg_match('/([^;]*)/', $contentTypeWithEncoding, $contentTypeMatches);
328
        $contentType = $contentTypeMatches[0];
329
        $accept = $this->request->getHeader('Accept');
330
        $mimetypes = $this->request->getAcceptMimetypes();
331
        if (!$className) {
332
            $className = $this->unsanitiseClassName($this->request->param('ClassName'));
333
        }
334
335
        // get formatter
336
        if (!empty($extension)) {
337
            $formatter = DataFormatter::for_extension($extension);
338
        } elseif ($includeAcceptHeader && !empty($accept) && strpos($accept, '*/*') === false) {
339
            $formatter = DataFormatter::for_mimetypes($mimetypes);
340
            if (!$formatter) {
0 ignored issues
show
introduced by
$formatter is of type SilverStripe\RestfulServer\DataFormatter, thus it always evaluated to true.
Loading history...
341
                $formatter = DataFormatter::for_extension($this->config()->default_extension);
342
            }
343
        } elseif (!empty($contentType)) {
344
            $formatter = DataFormatter::for_mimetype($contentType);
345
        } else {
346
            $formatter = DataFormatter::for_extension($this->config()->default_extension);
347
        }
348
349
        if (!$formatter) {
0 ignored issues
show
introduced by
$formatter is of type SilverStripe\RestfulServer\DataFormatter, thus it always evaluated to true.
Loading history...
350
            return false;
351
        }
352
353
        // set custom fields
354
        if ($customAddFields = $this->request->getVar('add_fields')) {
355
            $customAddFields = $formatter->getRealFields($className, explode(',', $customAddFields));
356
            $formatter->setCustomAddFields($customAddFields);
357
        }
358
        if ($customFields = $this->request->getVar('fields')) {
359
            $customFields = $formatter->getRealFields($className, explode(',', $customFields));
360
            $formatter->setCustomFields($customFields);
361
        }
362
        $formatter->setCustomRelations($this->getAllowedRelations($className));
363
364
        $apiAccess = Config::inst()->get($className, 'api_access');
365
        if (is_array($apiAccess)) {
366
            $formatter->setCustomAddFields(
367
                array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view'])
368
            );
369
            if ($formatter->getCustomFields()) {
370
                $formatter->setCustomFields(
371
                    array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view'])
372
                );
373
            } else {
374
                $formatter->setCustomFields((array)$apiAccess['view']);
375
            }
376
            if ($formatter->getCustomRelations()) {
377
                $formatter->setCustomRelations(
378
                    array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view'])
379
                );
380
            } else {
381
                $formatter->setCustomRelations((array)$apiAccess['view']);
382
            }
383
        }
384
385
        // set relation depth
386
        $relationDepth = $this->request->getVar('relationdepth');
387
        if (is_numeric($relationDepth)) {
388
            $formatter->relationDepth = (int)$relationDepth;
389
        }
390
391
        return $formatter;
392
    }
393
394
    /**
395
     * @param string Classname of a DataObject
396
     * @return DataFormatter
397
     */
398
    protected function getRequestDataFormatter($className = null)
399
    {
400
        return $this->getDataFormatter(false, $className);
401
    }
402
403
    /**
404
     * @param string Classname of a DataObject
405
     * @return DataFormatter
406
     */
407
    protected function getResponseDataFormatter($className = null)
408
    {
409
        return $this->getDataFormatter(true, $className);
410
    }
411
412
    /**
413
     * Handler for object delete
414
     */
415
    protected function deleteHandler($className, $id)
416
    {
417
        $obj = DataObject::get_by_id($className, $id);
418
        if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
419
            return $this->notFound();
420
        }
421
        if (!$obj->canDelete($this->getMember())) {
422
            return $this->permissionFailure();
423
        }
424
425
        $obj->delete();
426
427
        $this->getResponse()->setStatusCode(204); // No Content
428
        return true;
429
    }
430
431
    /**
432
     * Handler for object write
433
     */
434
    protected function putHandler($className, $id)
435
    {
436
        $obj = DataObject::get_by_id($className, $id);
437
        if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
438
            return $this->notFound();
439
        }
440
441
        if (!$obj->canEdit($this->getMember())) {
442
            return $this->permissionFailure();
443
        }
444
445
        $reqFormatter = $this->getRequestDataFormatter($className);
446
        if (!$reqFormatter) {
0 ignored issues
show
introduced by
$reqFormatter is of type SilverStripe\RestfulServer\DataFormatter, thus it always evaluated to true.
Loading history...
447
            return $this->unsupportedMediaType();
448
        }
449
450
        $responseFormatter = $this->getResponseDataFormatter($className);
451
        if (!$responseFormatter) {
0 ignored issues
show
introduced by
$responseFormatter is of type SilverStripe\RestfulServer\DataFormatter, thus it always evaluated to true.
Loading history...
452
            return $this->unsupportedMediaType();
453
        }
454
455
        try {
456
            /** @var DataObject|string */
457
            $obj = $this->updateDataObject($obj, $reqFormatter);
458
        } catch (ValidationException $e) {
459
            return $this->validationFailure($responseFormatter, $e->getResult());
460
        }
461
462
        if (is_string($obj)) {
463
            return $obj;
464
        }
465
466
        $this->getResponse()->setStatusCode(200); // Success
467
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
468
469
        // Append the default extension for the output format to the Location header
470
        // or else we'll use the default (XML)
471
        $types = $responseFormatter->supportedExtensions();
472
        $type = '';
473
        if (count($types)) {
474
            $type = ".{$types[0]}";
475
        }
476
477
        $urlSafeClassName = $this->sanitiseClassName(get_class($obj));
478
        $apiBase = $this->config()->api_base;
479
        $objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type);
480
        $this->getResponse()->addHeader('Location', $objHref);
481
482
        return $responseFormatter->convertDataObject($obj);
483
    }
484
485
    /**
486
     * Handler for object append / method call.
487
     *
488
     * @todo Posting to an existing URL (without a relation)
489
     * current resolves in creatig a new element,
490
     * rather than a "Conflict" message.
491
     */
492
    protected function postHandler($className, $id, $relation)
493
    {
494
        if ($id) {
495
            if (!$relation) {
496
                $this->response->setStatusCode(409);
497
                return 'Conflict';
498
            }
499
500
            $obj = DataObject::get_by_id($className, $id);
501
            if (!$obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
502
                return $this->notFound();
503
            }
504
505
            $reqFormatter = $this->getRequestDataFormatter($className);
506
            if (!$reqFormatter) {
0 ignored issues
show
introduced by
$reqFormatter is of type SilverStripe\RestfulServer\DataFormatter, thus it always evaluated to true.
Loading history...
507
                return $this->unsupportedMediaType();
508
            }
509
510
            $relation = $reqFormatter->getRealFieldName($className, $relation);
511
512
            if (!$obj->hasMethod($relation)) {
513
                return $this->notFound();
514
            }
515
516
            if (!Config::inst()->get($className, 'allowed_actions') ||
517
                !in_array($relation, Config::inst()->get($className, 'allowed_actions'))) {
518
                return $this->permissionFailure();
519
            }
520
521
            $obj->$relation();
522
523
            $this->getResponse()->setStatusCode(204); // No Content
524
            return true;
525
        }
526
527
        if (!singleton($className)->canCreate($this->getMember())) {
528
            return $this->permissionFailure();
529
        }
530
        $obj = new $className();
531
532
        $reqFormatter = $this->getRequestDataFormatter($className);
533
        if (!$reqFormatter) {
0 ignored issues
show
introduced by
$reqFormatter is of type SilverStripe\RestfulServer\DataFormatter, thus it always evaluated to true.
Loading history...
534
            return $this->unsupportedMediaType();
535
        }
536
537
        $responseFormatter = $this->getResponseDataFormatter($className);
538
539
        try {
540
            /** @var DataObject|string $obj */
541
            $obj = $this->updateDataObject($obj, $reqFormatter);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

541
            $obj = $this->updateDataObject(/** @scrutinizer ignore-type */ $obj, $reqFormatter);
Loading history...
542
        } catch (ValidationException $e) {
543
            return $this->validationFailure($responseFormatter, $e->getResult());
544
        }
545
546
        if (is_string($obj)) {
547
            return $obj;
548
        }
549
550
        $this->getResponse()->setStatusCode(201); // Created
551
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
552
553
        // Append the default extension for the output format to the Location header
554
        // or else we'll use the default (XML)
555
        $types = $responseFormatter->supportedExtensions();
556
        $type = '';
557
        if (count($types)) {
558
            $type = ".{$types[0]}";
559
        }
560
561
        $urlSafeClassName = $this->sanitiseClassName(get_class($obj));
562
        $apiBase = $this->config()->api_base;
563
        $objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type);
564
        $this->getResponse()->addHeader('Location', $objHref);
565
566
        return $responseFormatter->convertDataObject($obj);
567
    }
568
569
    /**
570
     * Converts either the given HTTP Body into an array
571
     * (based on the DataFormatter instance), or returns
572
     * the POST variables.
573
     * Automatically filters out certain critical fields
574
     * that shouldn't be set by the client (e.g. ID).
575
     *
576
     * @param DataObject $obj
577
     * @param DataFormatter $formatter
578
     * @return DataObject|string The passed object, or "No Content" if incomplete input data is provided
579
     */
580
    protected function updateDataObject($obj, $formatter)
581
    {
582
        // if neither an http body nor POST data is present, return error
583
        $body = $this->request->getBody();
584
        if (!$body && !$this->request->postVars()) {
585
            $this->getResponse()->setStatusCode(204); // No Content
586
            return 'No Content';
587
        }
588
589
        if (!empty($body)) {
590
            $rawdata = $formatter->convertStringToArray($body);
0 ignored issues
show
Bug introduced by
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 getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
591
        } else {
592
            // assume application/x-www-form-urlencoded which is automatically parsed by PHP
593
            $rawdata = $this->request->postVars();
594
        }
595
596
        $className = $this->unsanitiseClassName($this->request->param('ClassName'));
597
        // update any aliased field names
598
        $data = [];
599
        foreach ($rawdata as $key => $value) {
600
            $newkey = $formatter->getRealFieldName($className, $key);
601
            $data[$newkey] = $value;
602
        }
603
604
        // @todo Disallow editing of certain keys in database
605
        $data = array_diff_key($data, ['ID', 'Created']);
606
607
        $apiAccess = singleton($className)->config()->api_access;
608
        if (is_array($apiAccess) && isset($apiAccess['edit'])) {
609
            $data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit']));
610
        }
611
612
        $obj->update($data);
613
        $obj->write();
614
615
        return $obj;
616
    }
617
618
    /**
619
     * Gets a single DataObject by ID,
620
     * through a request like /api/v1/<MyClass>/<MyID>
621
     *
622
     * @param string $className
623
     * @param int $id
624
     * @param array $params
625
     * @return DataList
626
     */
627
    protected function getObjectQuery($className, $id, $params)
628
    {
629
        return DataList::create($className)->byIDs([$id]);
0 ignored issues
show
Bug introduced by
$className of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

629
        return DataList::create(/** @scrutinizer ignore-type */ $className)->byIDs([$id]);
Loading history...
630
    }
631
632
    /**
633
     * @param DataObject $obj
634
     * @param array $params
635
     * @param int|array $sort
636
     * @param int|array $limit
637
     * @return SQLQuery
0 ignored issues
show
Bug introduced by
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. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
638
     */
639
    protected function getObjectsQuery($className, $params, $sort, $limit)
640
    {
641
        return $this->getSearchQuery($className, $params, $sort, $limit);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getSearchQ...$params, $sort, $limit) returns the type SilverStripe\ORM\SS_List which is incompatible with the documented return type SilverStripe\RestfulServer\SQLQuery.
Loading history...
642
    }
643
644
645
    /**
646
     * @param DataObject $obj
647
     * @param array $params
648
     * @param int|array $sort
649
     * @param int|array $limit
650
     * @param string $relationName
651
     * @return SQLQuery|boolean
652
     */
653
    protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName)
654
    {
655
        // The relation method will return a DataList, that getSearchQuery subsequently manipulates
656
        if ($obj->hasMethod($relationName)) {
657
            // $this->HasOneName() will return a dataobject or null, neither
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
658
            // of which helps us get the classname in a consistent fashion.
659
            // So we must use a way that is reliable.
660
            if ($relationClass = DataObject::getSchema()->hasOneComponent(get_class($obj), $relationName)) {
661
                $joinField = $relationName . 'ID';
662
                // Again `byID` will return the wrong type for our purposes. So use `byIDs`
663
                $list = DataList::create($relationClass)->byIDs([$obj->$joinField]);
0 ignored issues
show
Bug introduced by
$relationClass of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

663
                $list = DataList::create(/** @scrutinizer ignore-type */ $relationClass)->byIDs([$obj->$joinField]);
Loading history...
664
            } else {
665
                $list = $obj->$relationName();
666
            }
667
668
            $apiAccess = Config::inst()->get($list->dataClass(), 'api_access');
669
670
671
            if (!$apiAccess) {
672
                return false;
673
            }
674
675
            return $this->getSearchQuery($list->dataClass(), $params, $sort, $limit, $list);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getSearchQ..., $sort, $limit, $list) returns the type SilverStripe\ORM\SS_List which is incompatible with the documented return type boolean|SilverStripe\RestfulServer\SQLQuery.
Loading history...
676
        }
677
    }
678
679
    /**
680
     * @return string
681
     */
682
    protected function permissionFailure()
683
    {
684
        // return a 401
685
        $this->getResponse()->setStatusCode(401);
686
        $this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"');
687
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
688
689
        $reponse = "You don't have access to this item through the API.";
690
        $this->extend(__FUNCTION__, $reponse);
691
692
        return $reponse;
693
    }
694
695
    /**
696
     * @return string
697
     */
698
    protected function notFound()
699
    {
700
        // return a 404
701
        $this->getResponse()->setStatusCode(404);
702
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
703
704
        $reponse = "That object wasn't found";
705
        $this->extend(__FUNCTION__, $reponse);
706
707
        return $reponse;
708
    }
709
710
    /**
711
     * @return string
712
     */
713
    protected function methodNotAllowed()
714
    {
715
        $this->getResponse()->setStatusCode(405);
716
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
717
718
        $reponse = "Method Not Allowed";
719
        $this->extend(__FUNCTION__, $reponse);
720
721
        return $reponse;
722
    }
723
724
    /**
725
     * @return string
726
     */
727
    protected function unsupportedMediaType()
728
    {
729
        $this->response->setStatusCode(415); // Unsupported Media Type
730
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
731
732
        $reponse = "Unsupported Media Type";
733
        $this->extend(__FUNCTION__, $reponse);
734
735
        return $reponse;
736
    }
737
738
    /**
739
     * @param ValidationResult $result
740
     * @return mixed
741
     */
742
    protected function validationFailure(DataFormatter $responseFormatter, ValidationResult $result)
743
    {
744
        $this->getResponse()->setStatusCode(400);
745
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
746
747
        $response = [
748
            'type' => ValidationException::class,
749
            'messages' => $result->getMessages(),
750
        ];
751
752
        $this->extend(__FUNCTION__, $response, $result);
753
754
        return $responseFormatter->convertArray($response);
755
    }
756
757
    /**
758
     * @param DataFormatter $responseFormatter
759
     * @param \Exception $e
760
     * @return string
761
     */
762
    protected function exceptionThrown(DataFormatter $responseFormatter, \Exception $e)
763
    {
764
        $this->getResponse()->setStatusCode(500);
765
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
766
767
        $response = [
768
            'type' => get_class($e),
769
            'message' => $e->getMessage(),
770
        ];
771
772
        $this->extend(__FUNCTION__, $response, $e);
773
774
        return $responseFormatter->convertArray($response);
775
    }
776
777
    /**
778
     * A function to authenticate a user
779
     *
780
     * @return Member|false the logged in member
781
     */
782
    protected function authenticate()
783
    {
784
        $authClass = $this->config()->authenticator;
785
        $member = $authClass::authenticate();
786
        Security::setCurrentUser($member);
787
        return $member;
788
    }
789
790
    /**
791
     * Return only relations which have $api_access enabled.
792
     * @todo Respect field level permissions once they are available in core
793
     *
794
     * @param string $class
795
     * @param Member $member
796
     * @return array
797
     */
798
    protected function getAllowedRelations($class, $member = null)
799
    {
800
        $allowedRelations = [];
801
        $obj = singleton($class);
802
        $relations = (array)$obj->hasOne() + (array)$obj->hasMany() + (array)$obj->manyMany();
803
        if ($relations) {
804
            foreach ($relations as $relName => $relClass) {
805
                //remove dot notation from relation names
806
                $parts = explode('.', $relClass);
807
                $relClass = array_shift($parts);
808
                if (Config::inst()->get($relClass, 'api_access')) {
809
                    $allowedRelations[] = $relName;
810
                }
811
            }
812
        }
813
        return $allowedRelations;
814
    }
815
816
    /**
817
     * Get the current Member, if available
818
     *
819
     * @return Member|null
820
     */
821
    protected function getMember()
822
    {
823
        return Security::getCurrentUser();
824
    }
825
}
826