Completed
Push — master ( e5a757...47ec18 )
by Robbie
14s
created

RestfulServer::getDataFormatter()   D

Complexity

Conditions 15
Paths 410

Size

Total Lines 67
Code Lines 43

Duplication

Lines 14
Ratio 20.9 %

Importance

Changes 0
Metric Value
dl 14
loc 67
rs 4.2775
c 0
b 0
f 0
cc 15
eloc 43
nc 410
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
139
            return $this->notFound();
140
        }
141
        if ($relation
0 ignored issues
show
Bug Best Practice introduced by
The expression $relation of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
142
            && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)
143
            ) {
144
            return $this->notFound();
145
        }
146
147
        // if api access is disabled, don't proceed
148
        $apiAccess = singleton($className)->stat('api_access');
149
        if (!$apiAccess) {
150
            return $this->permissionFailure();
151
        }
152
153
        // authenticate through HTTP BasicAuth
154
        $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...
155
156
        // handle different HTTP verbs
157
        if ($this->request->isGET() || $this->request->isHEAD()) {
158
            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

158
            return $this->getHandler($className, /** @scrutinizer ignore-type */ $id, $relation);
Loading history...
159
        }
160
161
        if ($this->request->isPOST()) {
162
            return $this->postHandler($className, $id, $relation);
163
        }
164
165
        if ($this->request->isPUT()) {
166
            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

166
            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...
167
        }
168
169
        if ($this->request->isDELETE()) {
170
            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

170
            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...
171
        }
172
173
        // if no HTTP verb matches, return error
174
        return $this->methodNotAllowed();
175
    }
176
177
    /**
178
     * Handler for object read.
179
     *
180
     * The data object will be returned in the following format:
181
     *
182
     * <ClassName>
183
     *   <FieldName>Value</FieldName>
184
     *   ...
185
     *   <HasOneRelName id="ForeignID" href="LinkToForeignRecordInAPI" />
186
     *   ...
187
     *   <HasManyRelName>
188
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
189
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
190
     *   </HasManyRelName>
191
     *   ...
192
     *   <ManyManyRelName>
193
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
194
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
195
     *   </ManyManyRelName>
196
     * </ClassName>
197
     *
198
     * Access is controlled by two variables:
199
     *
200
     *   - static $api_access must be set. This enables the API on a class by class basis
201
     *   - $obj->canView() must return true. This lets you implement record-level security
202
     *
203
     * @todo Access checking
204
     *
205
     * @param String $className
206
     * @param Int $id
207
     * @param String $relation
208
     * @return String The serialized representation of the requested object(s) - usually XML or JSON.
209
     */
210
    protected function getHandler($className, $id, $relationName)
211
    {
212
        $sort = '';
213
214
        if ($this->request->getVar('sort')) {
215
            $dir = $this->request->getVar('dir');
216
            $sort = array($this->request->getVar('sort') => ($dir ? $dir : 'ASC'));
217
        }
218
219
        $limit = array(
220
            'start' => $this->request->getVar('start'),
221
            'limit' => $this->request->getVar('limit')
222
        );
223
224
        $params = $this->request->getVars();
225
226
        $responseFormatter = $this->getResponseDataFormatter($className);
227
        if (!$responseFormatter) {
228
            return $this->unsupportedMediaType();
229
        }
230
231
        // $obj can be either a DataObject or a SS_List,
232
        // depending on the request
233
        if ($id) {
234
            // Format: /api/v1/<MyClass>/<ID>
235
            $obj = $this->getObjectQuery($className, $id, $params)->First();
236
            if (!$obj) {
237
                return $this->notFound();
238
            }
239
            if (!$obj->canView($this->getMember())) {
240
                return $this->permissionFailure();
241
            }
242
243
            // Format: /api/v1/<MyClass>/<ID>/<Relation>
244
            if ($relationName) {
245
                $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
246
                if (!$obj) {
247
                    return $this->notFound();
248
                }
249
250
                // TODO Avoid creating data formatter again for relation class (see above)
251
                $responseFormatter = $this->getResponseDataFormatter($obj->dataClass());
252
            }
253
        } else {
254
            // Format: /api/v1/<MyClass>
255
            $obj = $this->getObjectsQuery($className, $params, $sort, $limit);
256
        }
257
258
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
259
260
        $rawFields = $this->request->getVar('fields');
261
        $fields = $rawFields ? explode(',', $rawFields) : null;
262
263
        if ($obj instanceof SS_List) {
264
            $objs = ArrayList::create($obj->toArray());
265
            foreach ($objs as $obj) {
266
                if (!$obj->canView($this->getMember())) {
267
                    $objs->remove($obj);
268
                }
269
            }
270
            $responseFormatter->setTotalSize($objs->count());
271
            return $responseFormatter->convertDataObjectSet($objs, $fields);
272
        }
273
274
        if (!$obj) {
275
            $responseFormatter->setTotalSize(0);
276
            return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields);
277
        }
278
279
        return $responseFormatter->convertDataObject($obj, $fields);
280
    }
281
282
    /**
283
     * Uses the default {@link SearchContext} specified through
284
     * {@link DataObject::getDefaultSearchContext()} to augument
285
     * an existing query object (mostly a component query from {@link DataObject})
286
     * with search clauses.
287
     *
288
     * @todo Allow specifying of different searchcontext getters on model-by-model basis
289
     *
290
     * @param string $className
291
     * @param array $params
292
     * @return SS_List
293
     */
294
    protected function getSearchQuery(
295
        $className,
296
        $params = null,
297
        $sort = null,
298
        $limit = null,
299
        $existingQuery = null
300
    ) {
301
        if (singleton($className)->hasMethod('getRestfulSearchContext')) {
302
            $searchContext = singleton($className)->{'getRestfulSearchContext'}();
303
        } else {
304
            $searchContext = singleton($className)->getDefaultSearchContext();
305
        }
306
        return $searchContext->getQuery($params, $sort, $limit, $existingQuery);
307
    }
308
309
    /**
310
     * Returns a dataformatter instance based on the request
311
     * extension or mimetype. Falls back to {@link self::$default_extension}.
312
     *
313
     * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers
314
     * @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...
315
     * @return DataFormatter
316
     */
317
    protected function getDataFormatter($includeAcceptHeader = false, $className = null)
318
    {
319
        $extension = $this->request->getExtension();
320
        $contentTypeWithEncoding = $this->request->getHeader('Content-Type');
321
        preg_match('/([^;]*)/', $contentTypeWithEncoding, $contentTypeMatches);
322
        $contentType = $contentTypeMatches[0];
323
        $accept = $this->request->getHeader('Accept');
324
        $mimetypes = $this->request->getAcceptMimetypes();
325
        if (!$className) {
326
            $className = $this->unsanitiseClassName($this->request->param('ClassName'));
327
        }
328
329
        // get formatter
330
        if (!empty($extension)) {
331
            $formatter = DataFormatter::for_extension($extension);
332
        } elseif ($includeAcceptHeader && !empty($accept) && $accept != '*/*') {
333
            $formatter = DataFormatter::for_mimetypes($mimetypes);
334
            if (!$formatter) {
335
                $formatter = DataFormatter::for_extension(self::$default_extension);
336
            }
337
        } elseif (!empty($contentType)) {
338
            $formatter = DataFormatter::for_mimetype($contentType);
339
        } else {
340
            $formatter = DataFormatter::for_extension(self::$default_extension);
341
        }
342
343
        if (!$formatter) {
344
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type SilverStripe\RestfulServer\DataFormatter.
Loading history...
345
        }
346
347
        // set custom fields
348
        if ($customAddFields = $this->request->getVar('add_fields')) {
349
            $formatter->setCustomAddFields(explode(',', $customAddFields));
350
        }
351
        if ($customFields = $this->request->getVar('fields')) {
352
            $formatter->setCustomFields(explode(',', $customFields));
353
        }
354
        $formatter->setCustomRelations($this->getAllowedRelations($className));
355
356
        $apiAccess = singleton($className)->stat('api_access');
357
        if (is_array($apiAccess)) {
358
            $formatter->setCustomAddFields(
359
                array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view'])
360
            );
361 View Code Duplication
            if ($formatter->getCustomFields()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
362
                $formatter->setCustomFields(
363
                    array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view'])
364
                );
365
            } else {
366
                $formatter->setCustomFields((array)$apiAccess['view']);
367
            }
368 View Code Duplication
            if ($formatter->getCustomRelations()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
369
                $formatter->setCustomRelations(
370
                    array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view'])
371
                );
372
            } else {
373
                $formatter->setCustomRelations((array)$apiAccess['view']);
374
            }
375
        }
376
377
        // set relation depth
378
        $relationDepth = $this->request->getVar('relationdepth');
379
        if (is_numeric($relationDepth)) {
380
            $formatter->relationDepth = (int)$relationDepth;
381
        }
382
383
        return $formatter;
384
    }
385
386
    /**
387
     * @param String Classname of a DataObject
388
     * @return DataFormatter
389
     */
390
    protected function getRequestDataFormatter($className = null)
391
    {
392
        return $this->getDataFormatter(false, $className);
393
    }
394
395
    /**
396
     * @param String Classname of a DataObject
397
     * @return DataFormatter
398
     */
399
    protected function getResponseDataFormatter($className = null)
400
    {
401
        return $this->getDataFormatter(true, $className);
402
    }
403
404
    /**
405
     * Handler for object delete
406
     */
407
    protected function deleteHandler($className, $id)
408
    {
409
        $obj = DataObject::get_by_id($className, $id);
410
        if (!$obj) {
411
            return $this->notFound();
412
        }
413
        if (!$obj->canDelete($this->getMember())) {
414
            return $this->permissionFailure();
415
        }
416
417
        $obj->delete();
418
419
        $this->getResponse()->setStatusCode(204); // No Content
420
        return true;
421
    }
422
423
    /**
424
     * Handler for object write
425
     */
426
    protected function putHandler($className, $id)
427
    {
428
        $obj = DataObject::get_by_id($className, $id);
429
        if (!$obj) {
430
            return $this->notFound();
431
        }
432
433
        if (!$obj->canEdit($this->getMember())) {
434
            return $this->permissionFailure();
435
        }
436
437
        $reqFormatter = $this->getRequestDataFormatter($className);
438
        if (!$reqFormatter) {
439
            return $this->unsupportedMediaType();
440
        }
441
442
        $responseFormatter = $this->getResponseDataFormatter($className);
443
        if (!$responseFormatter) {
444
            return $this->unsupportedMediaType();
445
        }
446
447
        /** @var DataObject|string */
448
        $obj = $this->updateDataObject($obj, $reqFormatter);
449
        if (is_string($obj)) {
450
            return $obj;
451
        }
452
453
        $this->getResponse()->setStatusCode(200); // Success
454
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
455
456
        // Append the default extension for the output format to the Location header
457
        // or else we'll use the default (XML)
458
        $types = $responseFormatter->supportedExtensions();
459
        $type = '';
460
        if (count($types)) {
461
            $type = ".{$types[0]}";
462
        }
463
464
        $urlSafeClassName = $this->sanitiseClassName(get_class($obj));
465
        $apiBase = $this->config()->api_base;
466
        $objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type);
467
        $this->getResponse()->addHeader('Location', $objHref);
468
469
        return $responseFormatter->convertDataObject($obj);
470
    }
471
472
    /**
473
     * Handler for object append / method call.
474
     *
475
     * @todo Posting to an existing URL (without a relation)
476
     * current resolves in creatig a new element,
477
     * rather than a "Conflict" message.
478
     */
479
    protected function postHandler($className, $id, $relation)
480
    {
481
        if ($id) {
482
            if (!$relation) {
483
                $this->response->setStatusCode(409);
484
                return 'Conflict';
485
            }
486
487
            $obj = DataObject::get_by_id($className, $id);
488
            if (!$obj) {
489
                return $this->notFound();
490
            }
491
492
            if (!$obj->hasMethod($relation)) {
493
                return $this->notFound();
494
            }
495
496
            if (!$obj->stat('allowed_actions') || !in_array($relation, $obj->stat('allowed_actions'))) {
497
                return $this->permissionFailure();
498
            }
499
500
            $obj->$relation();
501
502
            $this->getResponse()->setStatusCode(204); // No Content
503
            return true;
504
        }
505
506
        if (!singleton($className)->canCreate($this->getMember())) {
507
            return $this->permissionFailure();
508
        }
509
        $obj = new $className();
510
511
        $reqFormatter = $this->getRequestDataFormatter($className);
512
        if (!$reqFormatter) {
513
            return $this->unsupportedMediaType();
514
        }
515
516
        $responseFormatter = $this->getResponseDataFormatter($className);
517
518
        /** @var DataObject|string $obj */
519
        $obj = $this->updateDataObject($obj, $reqFormatter);
520
        if (is_string($obj)) {
521
            return $obj;
522
        }
523
524
        $this->getResponse()->setStatusCode(201); // Created
525
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
526
527
        // Append the default extension for the output format to the Location header
528
        // or else we'll use the default (XML)
529
        $types = $responseFormatter->supportedExtensions();
530
        $type = '';
531
        if (count($types)) {
532
            $type = ".{$types[0]}";
533
        }
534
535
        $urlSafeClassName = $this->sanitiseClassName(get_class($obj));
536
        $apiBase = $this->config()->api_base;
537
        $objHref = Director::absoluteURL($apiBase . "$urlSafeClassName/$obj->ID" . $type);
538
        $this->getResponse()->addHeader('Location', $objHref);
539
540
        return $responseFormatter->convertDataObject($obj);
541
    }
542
543
    /**
544
     * Converts either the given HTTP Body into an array
545
     * (based on the DataFormatter instance), or returns
546
     * the POST variables.
547
     * Automatically filters out certain critical fields
548
     * that shouldn't be set by the client (e.g. ID).
549
     *
550
     * @param DataObject $obj
551
     * @param DataFormatter $formatter
552
     * @return DataObject|string The passed object, or "No Content" if incomplete input data is provided
553
     */
554
    protected function updateDataObject($obj, $formatter)
555
    {
556
        // if neither an http body nor POST data is present, return error
557
        $body = $this->request->getBody();
558
        if (!$body && !$this->request->postVars()) {
559
            $this->getResponse()->setStatusCode(204); // No Content
560
            return 'No Content';
561
        }
562
563
        if (!empty($body)) {
564
            $data = $formatter->convertStringToArray($body);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $data 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...
565
        } else {
566
            // assume application/x-www-form-urlencoded which is automatically parsed by PHP
567
            $data = $this->request->postVars();
568
        }
569
570
        // @todo Disallow editing of certain keys in database
571
        $data = array_diff_key($data, array('ID', 'Created'));
572
573
        $className = $this->unsanitiseClassName($this->request->param('ClassName'));
574
        $apiAccess = singleton($className)->config()->api_access;
575
        if (is_array($apiAccess) && isset($apiAccess['edit'])) {
576
            $data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit']));
577
        }
578
579
        $obj->update($data);
580
        $obj->write();
581
582
        return $obj;
583
    }
584
585
    /**
586
     * Gets a single DataObject by ID,
587
     * through a request like /api/v1/<MyClass>/<MyID>
588
     *
589
     * @param string $className
590
     * @param int $id
591
     * @param array $params
592
     * @return DataList
593
     */
594
    protected function getObjectQuery($className, $id, $params)
595
    {
596
        return DataList::create($className)->byIDs(array($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

596
        return DataList::create(/** @scrutinizer ignore-type */ $className)->byIDs(array($id));
Loading history...
597
    }
598
599
    /**
600
     * @param DataObject $obj
601
     * @param array $params
602
     * @param int|array $sort
603
     * @param int|array $limit
604
     * @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...
605
     */
606
    protected function getObjectsQuery($className, $params, $sort, $limit)
607
    {
608
        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...
609
    }
610
611
612
    /**
613
     * @param DataObject $obj
614
     * @param array $params
615
     * @param int|array $sort
616
     * @param int|array $limit
617
     * @param string $relationName
618
     * @return SQLQuery|boolean
619
     */
620
    protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName)
621
    {
622
        // The relation method will return a DataList, that getSearchQuery subsequently manipulates
623
        if ($obj->hasMethod($relationName)) {
624
            // $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...
625
            // of which helps us get the classname in a consistent fashion.
626
            // So we must use a way that is reliable.
627
            if ($relationClass = DataObject::getSchema()->hasOneComponent(get_class($obj), $relationName)) {
628
                $joinField = $relationName . 'ID';
629
                // Again `byID` will return the wrong type for our purposes. So use `byIDs`
630
                $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

630
                $list = DataList::create(/** @scrutinizer ignore-type */ $relationClass)->byIDs([$obj->$joinField]);
Loading history...
631
            } else {
632
                $list = $obj->$relationName();
633
            }
634
635
            $apiAccess = singleton($list->dataClass())->stat('api_access');
636
            if (!$apiAccess) {
637
                return false;
638
            }
639
640
            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...
641
        }
642
    }
643
644
    protected function permissionFailure()
645
    {
646
        // return a 401
647
        $this->getResponse()->setStatusCode(401);
648
        $this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"');
649
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
650
        return "You don't have access to this item through the API.";
651
    }
652
653
    protected function notFound()
654
    {
655
        // return a 404
656
        $this->getResponse()->setStatusCode(404);
657
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
658
        return "That object wasn't found";
659
    }
660
661
    protected function methodNotAllowed()
662
    {
663
        $this->getResponse()->setStatusCode(405);
664
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
665
        return "Method Not Allowed";
666
    }
667
668
    protected function unsupportedMediaType()
669
    {
670
        $this->response->setStatusCode(415); // Unsupported Media Type
671
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
672
        return "Unsupported Media Type";
673
    }
674
675
    /**
676
     * A function to authenticate a user
677
     *
678
     * @return Member|false the logged in member
679
     */
680
    protected function authenticate()
681
    {
682
        $authClass = $this->config()->authenticator;
683
        $member = $authClass::authenticate();
684
        Security::setCurrentUser($member);
685
        return $member;
686
    }
687
688
    /**
689
     * Return only relations which have $api_access enabled.
690
     * @todo Respect field level permissions once they are available in core
691
     *
692
     * @param string $class
693
     * @param Member $member
694
     * @return array
695
     */
696
    protected function getAllowedRelations($class, $member = null)
697
    {
698
        $allowedRelations = array();
699
        $obj = singleton($class);
700
        $relations = (array)$obj->hasOne() + (array)$obj->hasMany() + (array)$obj->manyMany();
701
        if ($relations) {
702
            foreach ($relations as $relName => $relClass) {
703
                //remove dot notation from relation names
704
                $parts = explode('.', $relClass);
705
                $relClass = array_shift($parts);
706
                if (singleton($relClass)->stat('api_access')) {
707
                    $allowedRelations[] = $relName;
708
                }
709
            }
710
        }
711
        return $allowedRelations;
712
    }
713
714
    /**
715
     * Get the current Member, if available
716
     *
717
     * @return Member|null
718
     */
719
    protected function getMember()
720
    {
721
        return Security::getCurrentUser();
722
    }
723
}
724