Completed
Push — master ( ba7b5b...e5a757 )
by Robbie
01:33
created

RestfulServer::getMember()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * Generic RESTful server, which handles webservice access to arbitrary DataObjects.
4
 * Relies on serialization/deserialization into different formats provided
5
 * by the DataFormatter APIs in core.
6
 *
7
 * @todo Finish RestfulServer_Item and RestfulServer_List implementation and re-enable $url_handlers
8
 * @todo Implement PUT/POST/DELETE for relations
9
 * @todo Access-Control for relations (you might be allowed to view Members and Groups,
10
 *       but not their relation with each other)
11
 * @todo Make SearchContext specification customizeable for each class
12
 * @todo Allow for range-searches (e.g. on Created column)
13
 * @todo Filter relation listings by $api_access and canView() permissions
14
 * @todo Exclude relations when "fields" are specified through URL (they should be explicitly
15
 *       requested in this case)
16
 * @todo Custom filters per DataObject subclass, e.g. to disallow showing unpublished pages in
17
 * SiteTree/Versioned/Hierarchy
18
 * @todo URL parameter namespacing for search-fields, limit, fields, add_fields
19
 *       (might all be valid dataobject properties)
20
 *       e.g. you wouldn't be able to search for a "limit" property on your subclass as
21
 *       its overlayed with the search logic
22
 * @todo i18n integration (e.g. Page/1.xml?lang=de_DE)
23
 * @todo Access to extendable methods/relations like SiteTree/1/Versions or SiteTree/1/Version/22
24
 * @todo Respect $api_access array notation in search contexts
25
 *
26
 * @package framework
27
 * @subpackage api
28
 */
29
class RestfulServer extends Controller
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
30
{
31
    public static $url_handlers = array(
32
        '$ClassName/$ID/$Relation' => 'handleAction'
33
        #'$ClassName/#ID' => 'handleItem',
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% 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...
34
        #'$ClassName' => 'handleList',
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% 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...
35
    );
36
37
    protected static $api_base = "api/v1/";
38
39
    protected static $authenticator = 'BasicRestfulAuthenticator';
40
41
    /**
42
     * If no extension is given in the request, resolve to this extension
43
     * (and subsequently the {@link self::$default_mimetype}.
44
     *
45
     * @var string
46
     */
47
    public static $default_extension = "xml";
48
49
    /**
50
     * If no extension is given, resolve the request to this mimetype.
51
     *
52
     * @var string
53
     */
54
    protected static $default_mimetype = "text/xml";
55
56
    /**
57
     * @uses authenticate()
58
     * @var Member
59
     */
60
    protected $member;
61
62
    public static $allowed_actions = array(
63
        'index'
64
    );
65
66
    /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
67
    function handleItem($request) {
68
        return new RestfulServer_Item(DataObject::get_by_id($request->param("ClassName"), $request->param("ID")));
69
    }
70
71
    function handleList($request) {
72
        return new RestfulServer_List(DataObject::get($request->param("ClassName"),""));
73
    }
74
    */
75
76
    public function init()
77
    {
78
        /* This sets up SiteTree the same as when viewing a page through the frontend. Versioned defaults
79
         * to Stage, and then when viewing the front-end Versioned::choose_site_stage changes it to Live.
80
         * TODO: In 3.2 we should make the default Live, then change to Stage in the admin area (with a nicer API)
81
         */
82
        if (class_exists('SiteTree')) {
83
            singleton('SiteTree')->extend('modelascontrollerInit', $this);
84
        }
85
        parent::init();
86
    }
87
88
    /**
89
     * This handler acts as the switchboard for the controller.
90
     * Since no $Action url-param is set, all requests are sent here.
91
     */
92
    public function index()
93
    {
94
        if (!isset($this->urlParams['ClassName'])) {
95
            return $this->notFound();
96
        }
97
        $className = $this->urlParams['ClassName'];
98
        $id = (isset($this->urlParams['ID'])) ? $this->urlParams['ID'] : null;
99
        $relation = (isset($this->urlParams['Relation'])) ? $this->urlParams['Relation'] : null;
100
101
        // Check input formats
102
        if (!class_exists($className)) {
103
            return $this->notFound();
104
        }
105
        if ($id && !is_numeric($id)) {
106
            return $this->notFound();
107
        }
108
        if (
109
            $relation
110
            && !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $relation)
111
            ) {
112
            return $this->notFound();
113
        }
114
115
        // if api access is disabled, don't proceed
116
        $apiAccess = singleton($className)->stat('api_access');
117
        if (!$apiAccess) {
118
            return $this->permissionFailure();
119
        }
120
121
        // authenticate through HTTP BasicAuth
122
        $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 object<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...
123
124
        // handle different HTTP verbs
125
        if ($this->request->isGET() || $this->request->isHEAD()) {
126
            return $this->getHandler($className, $id, $relation);
127
        }
128
129
        if ($this->request->isPOST()) {
130
            return $this->postHandler($className, $id, $relation);
131
        }
132
133
        if ($this->request->isPUT()) {
134
            return $this->putHandler($className, $id, $relation);
0 ignored issues
show
Unused Code introduced by
The call to RestfulServer::putHandler() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
135
        }
136
137
        if ($this->request->isDELETE()) {
138
            return $this->deleteHandler($className, $id, $relation);
0 ignored issues
show
Unused Code introduced by
The call to RestfulServer::deleteHandler() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
139
        }
140
141
        // if no HTTP verb matches, return error
142
        return $this->methodNotAllowed();
143
    }
144
145
    /**
146
     * Handler for object read.
147
     *
148
     * The data object will be returned in the following format:
149
     *
150
     * <ClassName>
151
     *   <FieldName>Value</FieldName>
152
     *   ...
153
     *   <HasOneRelName id="ForeignID" href="LinkToForeignRecordInAPI" />
154
     *   ...
155
     *   <HasManyRelName>
156
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
157
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
158
     *   </HasManyRelName>
159
     *   ...
160
     *   <ManyManyRelName>
161
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
162
     *     <ForeignClass id="ForeignID" href="LinkToForeignRecordInAPI" />
163
     *   </ManyManyRelName>
164
     * </ClassName>
165
     *
166
     * Access is controlled by two variables:
167
     *
168
     *   - static $api_access must be set. This enables the API on a class by class basis
169
     *   - $obj->canView() must return true. This lets you implement record-level security
170
     *
171
     * @todo Access checking
172
     *
173
     * @param String $className
174
     * @param Int $id
175
     * @param String $relation
0 ignored issues
show
Documentation introduced by
There is no parameter named $relation. Did you maybe mean $relationName?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
176
     * @return String The serialized representation of the requested object(s) - usually XML or JSON.
177
     */
178
    protected function getHandler($className, $id, $relationName)
179
    {
180
        $sort = '';
181
182
        if ($this->request->getVar('sort')) {
183
            $dir = $this->request->getVar('dir');
184
            $sort = array($this->request->getVar('sort') => ($dir ? $dir : 'ASC'));
185
        }
186
187
        $limit = array(
188
            'start' => $this->request->getVar('start'),
189
            'limit' => $this->request->getVar('limit')
190
        );
191
192
        $params = $this->request->getVars();
193
194
        $responseFormatter = $this->getResponseDataFormatter($className);
195
        if (!$responseFormatter) {
196
            return $this->unsupportedMediaType();
197
        }
198
199
        // $obj can be either a DataObject or a SS_List,
200
        // depending on the request
201
        if ($id) {
202
            // Format: /api/v1/<MyClass>/<ID>
203
            $obj = $this->getObjectQuery($className, $id, $params)->First();
204
            if (!$obj) {
205
                return $this->notFound();
206
            }
207
            if (!$obj->canView($this->getMember())) {
208
                return $this->permissionFailure();
209
            }
210
211
            // Format: /api/v1/<MyClass>/<ID>/<Relation>
212
            if ($relationName) {
213
                $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName);
0 ignored issues
show
Bug introduced by
It seems like $sort defined by '' on line 180 can also be of type string; however, RestfulServer::getObjectRelationQuery() does only seem to accept integer|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
214
                if (!$obj) {
215
                    return $this->notFound();
216
                }
217
218
                // TODO Avoid creating data formatter again for relation class (see above)
219
                $responseFormatter = $this->getResponseDataFormatter($obj->dataClass());
220
            }
221
        } else {
222
            // Format: /api/v1/<MyClass>
223
            $obj = $this->getObjectsQuery($className, $params, $sort, $limit);
0 ignored issues
show
Bug introduced by
It seems like $sort defined by '' on line 180 can also be of type string; however, RestfulServer::getObjectsQuery() does only seem to accept integer|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
224
        }
225
226
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
227
228
        $rawFields = $this->request->getVar('fields');
229
        $fields = $rawFields ? explode(',', $rawFields) : null;
230
231
        if ($obj instanceof SS_List) {
232
            $objs = ArrayList::create($obj->toArray());
233
            foreach ($objs as $obj) {
234
                if (!$obj->canView($this->getMember())) {
235
                    $objs->remove($obj);
236
                }
237
            }
238
            $responseFormatter->setTotalSize($objs->count());
239
            return $responseFormatter->convertDataObjectSet($objs, $fields);
0 ignored issues
show
Unused Code introduced by
The call to DataFormatter::convertDataObjectSet() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
240
        }
241
242
        if (!$obj) {
243
            $responseFormatter->setTotalSize(0);
244
            return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields);
0 ignored issues
show
Unused Code introduced by
The call to DataFormatter::convertDataObjectSet() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
245
        }
246
247
        return $responseFormatter->convertDataObject($obj, $fields);
0 ignored issues
show
Unused Code introduced by
The call to DataFormatter::convertDataObject() has too many arguments starting with $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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
248
    }
249
250
    /**
251
     * Uses the default {@link SearchContext} specified through
252
     * {@link DataObject::getDefaultSearchContext()} to augument
253
     * an existing query object (mostly a component query from {@link DataObject})
254
     * with search clauses.
255
     *
256
     * @todo Allow specifying of different searchcontext getters on model-by-model basis
257
     *
258
     * @param string $className
259
     * @param array $params
260
     * @return SS_List
261
     */
262
    protected function getSearchQuery($className, $params = null, $sort = null,
263
        $limit = null, $existingQuery = null
264
    ) {
265
        if (singleton($className)->hasMethod('getRestfulSearchContext')) {
266
            $searchContext = singleton($className)->{'getRestfulSearchContext'}();
267
        } else {
268
            $searchContext = singleton($className)->getDefaultSearchContext();
269
        }
270
        return $searchContext->getQuery($params, $sort, $limit, $existingQuery);
271
    }
272
273
    /**
274
     * Returns a dataformatter instance based on the request
275
     * extension or mimetype. Falls back to {@link self::$default_extension}.
276
     *
277
     * @param boolean $includeAcceptHeader Determines wether to inspect and prioritize any HTTP Accept headers
278
     * @param String Classname of a DataObject
279
     * @return DataFormatter
280
     */
281
    protected function getDataFormatter($includeAcceptHeader = false, $className = null)
282
    {
283
        $extension = $this->request->getExtension();
284
        $contentTypeWithEncoding = $this->request->getHeader('Content-Type');
285
        preg_match('/([^;]*)/', $contentTypeWithEncoding, $contentTypeMatches);
286
        $contentType = $contentTypeMatches[0];
287
        $accept = $this->request->getHeader('Accept');
288
        $mimetypes = $this->request->getAcceptMimetypes();
289
        if (!$className) {
290
            $className = $this->urlParams['ClassName'];
291
        }
292
293
        // get formatter
294
        if (!empty($extension)) {
295
            $formatter = DataFormatter::for_extension($extension);
296
        } elseif ($includeAcceptHeader && !empty($accept) && $accept != '*/*') {
297
            $formatter = DataFormatter::for_mimetypes($mimetypes);
298
            if (!$formatter) {
299
                $formatter = DataFormatter::for_extension(self::$default_extension);
300
            }
301
        } elseif (!empty($contentType)) {
302
            $formatter = DataFormatter::for_mimetype($contentType);
303
        } else {
304
            $formatter = DataFormatter::for_extension(self::$default_extension);
305
        }
306
307
        if (!$formatter) {
308
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by RestfulServer::getDataFormatter of type DataFormatter.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
309
        }
310
311
        // set custom fields
312
        if ($customAddFields = $this->request->getVar('add_fields')) {
313
            $formatter->setCustomAddFields(explode(',', $customAddFields));
314
        }
315
        if ($customFields = $this->request->getVar('fields')) {
316
            $formatter->setCustomFields(explode(',', $customFields));
317
        }
318
        $formatter->setCustomRelations($this->getAllowedRelations($className));
319
320
        $apiAccess = singleton($className)->stat('api_access');
321
        if (is_array($apiAccess)) {
322
            $formatter->setCustomAddFields(
323
                array_intersect((array)$formatter->getCustomAddFields(), (array)$apiAccess['view'])
324
            );
325 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...
326
                $formatter->setCustomFields(
327
                    array_intersect((array)$formatter->getCustomFields(), (array)$apiAccess['view'])
328
                );
329
            } else {
330
                $formatter->setCustomFields((array)$apiAccess['view']);
331
            }
332 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...
333
                $formatter->setCustomRelations(
334
                    array_intersect((array)$formatter->getCustomRelations(), (array)$apiAccess['view'])
335
                );
336
            } else {
337
                $formatter->setCustomRelations((array)$apiAccess['view']);
338
            }
339
        }
340
341
        // set relation depth
342
        $relationDepth = $this->request->getVar('relationdepth');
343
        if (is_numeric($relationDepth)) {
344
            $formatter->relationDepth = (int)$relationDepth;
345
        }
346
347
        return $formatter;
348
    }
349
350
    /**
351
     * @param String Classname of a DataObject
352
     * @return DataFormatter
353
     */
354
    protected function getRequestDataFormatter($className = null)
355
    {
356
        return $this->getDataFormatter(false, $className);
357
    }
358
359
    /**
360
     * @param String Classname of a DataObject
361
     * @return DataFormatter
362
     */
363
    protected function getResponseDataFormatter($className = null)
364
    {
365
        return $this->getDataFormatter(true, $className);
366
    }
367
368
    /**
369
     * Handler for object delete
370
     */
371
    protected function deleteHandler($className, $id)
372
    {
373
        $obj = DataObject::get_by_id($className, $id);
374
        if (!$obj) {
375
            return $this->notFound();
376
        }
377
        if (!$obj->canDelete($this->getMember())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getMember() targeting RestfulServer::getMember() can also be of type object<DataObject>; however, DataObject::canDelete() does only seem to accept object<Member>|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
378
            return $this->permissionFailure();
379
        }
380
381
        $obj->delete();
382
383
        $this->getResponse()->setStatusCode(204); // No Content
384
        return true;
385
    }
386
387
    /**
388
     * Handler for object write
389
     */
390
    protected function putHandler($className, $id)
391
    {
392
        $obj = DataObject::get_by_id($className, $id);
393
        if (!$obj) {
394
            return $this->notFound();
395
        }
396
        if (!$obj->canEdit($this->getMember())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getMember() targeting RestfulServer::getMember() can also be of type object<DataObject>; however, DataObject::canEdit() does only seem to accept object<Member>|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
397
            return $this->permissionFailure();
398
        }
399
400
        $reqFormatter = $this->getRequestDataFormatter($className);
401
        if (!$reqFormatter) {
402
            return $this->unsupportedMediaType();
403
        }
404
405
        $responseFormatter = $this->getResponseDataFormatter($className);
406
        if (!$responseFormatter) {
407
            return $this->unsupportedMediaType();
408
        }
409
410
        /** @var DataObject|string */
411
        $obj = $this->updateDataObject($obj, $reqFormatter);
412
        if (is_string($obj)) {
413
            return $obj;
414
        }
415
416
        $this->getResponse()->setStatusCode(200); // Success
417
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
418
419
        // Append the default extension for the output format to the Location header
420
        // or else we'll use the default (XML)
421
        $types = $responseFormatter->supportedExtensions();
422
        $type = '';
423
        if (count($types)) {
424
            $type = ".{$types[0]}";
425
        }
426
427
        $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
428
        $this->getResponse()->addHeader('Location', $objHref);
0 ignored issues
show
Security Bug introduced by
It seems like $objHref defined by \Director::absoluteURL(s...s}/{$obj->ID}" . $type) on line 427 can also be of type false; however, SS_HTTPResponse::addHeader() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
429
430
        return $responseFormatter->convertDataObject($obj);
431
    }
432
433
    /**
434
     * Handler for object append / method call.
435
     *
436
     * @todo Posting to an existing URL (without a relation)
437
     * current resolves in creatig a new element,
438
     * rather than a "Conflict" message.
439
     */
440
    protected function postHandler($className, $id, $relation)
441
    {
442
        if ($id) {
443
            if (!$relation) {
444
                $this->response->setStatusCode(409);
445
                return 'Conflict';
446
            }
447
448
            $obj = DataObject::get_by_id($className, $id);
449
            if (!$obj) {
450
                return $this->notFound();
451
            }
452
453
            if (!$obj->hasMethod($relation)) {
454
                return $this->notFound();
455
            }
456
457
            if (!$obj->stat('allowed_actions') || !in_array($relation, $obj->stat('allowed_actions'))) {
458
                return $this->permissionFailure();
459
            }
460
461
            $obj->$relation();
462
463
            $this->getResponse()->setStatusCode(204); // No Content
464
            return true;
465
        }
466
467
        if (!singleton($className)->canCreate($this->getMember())) {
468
            return $this->permissionFailure();
469
        }
470
        $obj = new $className();
471
472
        $reqFormatter = $this->getRequestDataFormatter($className);
473
        if (!$reqFormatter) {
474
            return $this->unsupportedMediaType();
475
        }
476
477
        $responseFormatter = $this->getResponseDataFormatter($className);
478
479
        /** @var DataObject|string $obj */
480
        $obj = $this->updateDataObject($obj, $reqFormatter);
0 ignored issues
show
Bug introduced by
It seems like $obj defined by $this->updateDataObject($obj, $reqFormatter) on line 480 can also be of type string; however, RestfulServer::updateDataObject() does only seem to accept object<DataObject>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
481
        if (is_string($obj)) {
482
            return $obj;
483
        }
484
485
        $this->getResponse()->setStatusCode(201); // Created
486
        $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType());
487
488
        // Append the default extension for the output format to the Location header
489
        // or else we'll use the default (XML)
490
        $types = $responseFormatter->supportedExtensions();
491
        $type = '';
492
        if (count($types)) {
493
            $type = ".{$types[0]}";
494
        }
495
496
        $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID" . $type);
497
        $this->getResponse()->addHeader('Location', $objHref);
0 ignored issues
show
Security Bug introduced by
It seems like $objHref defined by \Director::absoluteURL(s...s}/{$obj->ID}" . $type) on line 496 can also be of type false; however, SS_HTTPResponse::addHeader() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
498
499
        return $responseFormatter->convertDataObject($obj);
500
    }
501
502
    /**
503
     * Converts either the given HTTP Body into an array
504
     * (based on the DataFormatter instance), or returns
505
     * the POST variables.
506
     * Automatically filters out certain critical fields
507
     * that shouldn't be set by the client (e.g. ID).
508
     *
509
     * @param DataObject $obj
510
     * @param DataFormatter $formatter
511
     * @return DataObject|string The passed object, or "No Content" if incomplete input data is provided
512
     */
513
    protected function updateDataObject($obj, $formatter)
514
    {
515
        // if neither an http body nor POST data is present, return error
516
        $body = $this->request->getBody();
517
        if (!$body && !$this->request->postVars()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $body of type null|string is loosely compared to false; 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...
518
            $this->getResponse()->setStatusCode(204); // No Content
519
            return 'No Content';
520
        }
521
522
        if (!empty($body)) {
523
            $data = $formatter->convertStringToArray($body);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $data is correct as $formatter->convertStringToArray($body) (which targets DataFormatter::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...
524
        } else {
525
            // assume application/x-www-form-urlencoded which is automatically parsed by PHP
526
            $data = $this->request->postVars();
527
        }
528
529
        // @todo Disallow editing of certain keys in database
530
        $data = array_diff_key($data, array('ID', 'Created'));
531
532
        $apiAccess = singleton($this->urlParams['ClassName'])->stat('api_access');
533
        if (is_array($apiAccess) && isset($apiAccess['edit'])) {
534
            $data = array_intersect_key($data, array_combine($apiAccess['edit'], $apiAccess['edit']));
535
        }
536
537
        $obj->update($data);
538
        $obj->write();
539
540
        return $obj;
541
    }
542
543
    /**
544
     * Gets a single DataObject by ID,
545
     * through a request like /api/v1/<MyClass>/<MyID>
546
     *
547
     * @param string $className
548
     * @param int $id
549
     * @param array $params
550
     * @return DataList
551
     */
552
    protected function getObjectQuery($className, $id, $params)
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
553
    {
554
        return DataList::create($className)->byIDs(array($id));
555
    }
556
557
    /**
558
     * @param DataObject $obj
0 ignored issues
show
Bug introduced by
There is no parameter named $obj. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
559
     * @param array $params
560
     * @param int|array $sort
561
     * @param int|array $limit
562
     * @return SQLQuery
563
     */
564
    protected function getObjectsQuery($className, $params, $sort, $limit)
565
    {
566
        return $this->getSearchQuery($className, $params, $sort, $limit);
567
    }
568
569
570
    /**
571
     * @param DataObject $obj
572
     * @param array $params
573
     * @param int|array $sort
574
     * @param int|array $limit
575
     * @param string $relationName
576
     * @return SQLQuery|boolean
577
     */
578
    protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName)
579
    {
580
        // The relation method will return a DataList, that getSearchQuery subsequently manipulates
581
        if ($obj->hasMethod($relationName)) {
582
            if ($relationClass = $obj->has_one($relationName)) {
0 ignored issues
show
Deprecated Code introduced by
The method DataObject::has_one() has been deprecated with message: 4.0 Method has been replaced by hasOne() and hasOneComponent()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
583
                $joinField = $relationName . 'ID';
584
                $list = DataList::create($relationClass)->byIDs(array($obj->$joinField));
585
            } else {
586
                $list = $obj->$relationName();
587
            }
588
589
            $apiAccess = singleton($list->dataClass())->stat('api_access');
590
            if (!$apiAccess) {
591
                return false;
592
            }
593
594
            return $this->getSearchQuery($list->dataClass(), $params, $sort, $limit, $list);
595
        }
596
    }
597
598
    protected function permissionFailure()
599
    {
600
        // return a 401
601
        $this->getResponse()->setStatusCode(401);
602
        $this->getResponse()->addHeader('WWW-Authenticate', 'Basic realm="API Access"');
603
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
604
        return "You don't have access to this item through the API.";
605
    }
606
607
    protected function notFound()
608
    {
609
        // return a 404
610
        $this->getResponse()->setStatusCode(404);
611
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
612
        return "That object wasn't found";
613
    }
614
615
    protected function methodNotAllowed()
616
    {
617
        $this->getResponse()->setStatusCode(405);
618
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
619
        return "Method Not Allowed";
620
    }
621
622
    protected function unsupportedMediaType()
623
    {
624
        $this->response->setStatusCode(415); // Unsupported Media Type
625
        $this->getResponse()->addHeader('Content-Type', 'text/plain');
626
        return "Unsupported Media Type";
627
    }
628
629
    /**
630
     * A function to authenticate a user
631
     *
632
     * @return Member|false the logged in member
633
     */
634
    protected function authenticate()
635
    {
636
        $authClass = self::config()->authenticator;
637
        return $authClass::authenticate();
638
    }
639
640
    /**
641
     * Return only relations which have $api_access enabled.
642
     * @todo Respect field level permissions once they are available in core
643
     *
644
     * @param string $class
645
     * @param Member $member
646
     * @return array
647
     */
648
    protected function getAllowedRelations($class, $member = null)
0 ignored issues
show
Unused Code introduced by
The parameter $member is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
649
    {
650
        $allowedRelations = array();
651
        $obj = singleton($class);
652
        $relations = (array)$obj->has_one() + (array)$obj->has_many() + (array)$obj->many_many();
653
        if ($relations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $relations of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
654
            foreach ($relations as $relName => $relClass) {
655
                if (singleton($relClass)->stat('api_access')) {
656
                    $allowedRelations[] = $relName;
657
                }
658
            }
659
        }
660
        return $allowedRelations;
661
    }
662
663
    /**
664
     * Get the current Member, if available
665
     *
666
     * @return Member|null
667
     */
668
    protected function getMember()
669
    {
670
        return Member::currentUser();
671
    }
672
}
673
674
/**
675
 * Restful server handler for a SS_List
676
 *
677
 * @package framework
678
 * @subpackage api
679
 */
680
class RestfulServer_List
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
681
{
682
    public static $url_handlers = array(
683
        '#ID' => 'handleItem',
684
    );
685
686
    public function __construct($list)
687
    {
688
        $this->list = $list;
0 ignored issues
show
Bug introduced by
The property list does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
689
    }
690
691
    public function handleItem($request)
692
    {
693
        return new RestulServer_Item($this->list->getById($request->param('ID')));
694
    }
695
}
696
697
/**
698
 * Restful server handler for a single DataObject
699
 *
700
 * @package framework
701
 * @subpackage api
702
 */
703
class RestfulServer_Item
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
704
{
705
    public static $url_handlers = array(
706
        '$Relation' => 'handleRelation',
707
    );
708
709
    public function __construct($item)
710
    {
711
        $this->item = $item;
0 ignored issues
show
Bug introduced by
The property item does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
712
    }
713
714
    public function handleRelation($request)
715
    {
716
        $funcName = $request('Relation');
717
        $relation = $this->item->$funcName();
718
719
        if ($relation instanceof SS_List) {
720
            return new RestfulServer_List($relation);
721
        } else {
722
            return new RestfulServer_Item($relation);
723
        }
724
    }
725
}
726