Completed
Push — 4-cactus ( 870ab2...2fe737 )
by Alberto
15s queued 11s
created

AppController::prepareInclude()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 6
nop 1
dl 0
loc 30
rs 9.0777
c 0
b 0
f 0
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2016 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace BEdita\API\Controller;
14
15
use BadMethodCallException;
16
use BEdita\API\Datasource\JsonApiPaginator;
17
use BEdita\Core\State\CurrentApplication;
18
use Cake\Controller\Controller;
19
use Cake\Core\Configure;
20
use Cake\Datasource\Exception\RecordNotFoundException;
21
use Cake\Event\Event;
22
use Cake\Network\Exception\BadRequestException;
23
use Cake\Network\Exception\ForbiddenException;
24
use Cake\Network\Exception\NotAcceptableException;
25
use Cake\Network\Exception\NotFoundException;
26
use Cake\Routing\Router;
27
28
/**
29
 * Base class for all API Controller endpoints.
30
 *
31
 * @since 4.0.0
32
 *
33
 * @property \BEdita\API\Controller\Component\JsonApiComponent $JsonApi
34
 */
35
class AppController extends Controller
36
{
37
38
    /**
39
     * {@inheritDoc}
40
     */
41
    public $paginate = [
42
        'maxLimit' => 100,
43
        'order' => [
44
            'id' => 'asc',
45
        ],
46
    ];
47
48
    /**
49
     * {@inheritDoc}
50
     */
51
    public function initialize()
52
    {
53
        parent::initialize();
54
55
        $this->response = $this->response->withHeader('X-BEdita-Version', Configure::read('BEdita.version'));
0 ignored issues
show
Bug introduced by
The method withHeader() does not exist on null. ( Ignorable by Annotation )

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

55
        /** @scrutinizer ignore-call */ 
56
        $this->response = $this->response->withHeader('X-BEdita-Version', Configure::read('BEdita.version'));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
56
57
        $this->getApplication();
58
59
        $this->loadComponent('Paginator', (array)Configure::read('Pagination'));
60
        $this->loadComponent('RequestHandler');
61
        if ($this->request->is(['json', 'jsonapi'])) {
0 ignored issues
show
Bug introduced by
The method is() does not exist on null. ( Ignorable by Annotation )

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

61
        if ($this->request->/** @scrutinizer ignore-call */ is(['json', 'jsonapi'])) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
62
            $this->loadComponent('BEdita/API.JsonApi', [
63
                'contentType' => $this->request->is('json') ? 'json' : null,
64
                'checkMediaType' => $this->request->is('jsonapi'),
65
            ]);
66
            $this->Paginator->setPaginator((new JsonApiPaginator())->setConfig($this->Paginator->getConfig()));
67
68
            $this->RequestHandler->setConfig('inputTypeMap.json', [[$this->JsonApi, 'parseInput']], false);
69
            $this->RequestHandler->setConfig('viewClassMap.json', 'BEdita/API.JsonApi');
70
        }
71
72
        $this->loadComponent('Auth', [
73
            'authenticate' => ['BEdita/API.Jwt', 'BEdita/API.Anonymous'],
74
            'authorize' => [
75
                'BEdita/API.Endpoint' => [
76
                    'blockAnonymousUsers' => Configure::read('Security.blockAnonymousUsers'),
77
                ],
78
            ],
79
            'loginAction' => ['_name' => 'api:login'],
80
            'loginRedirect' => ['_name' => 'api:login'],
81
            'unauthorizedRedirect' => false,
82
            'storage' => 'Memory',
83
        ]);
84
85
        if (empty(Router::fullBaseUrl())) {
86
            Router::fullBaseUrl(
87
                rtrim(
88
                    sprintf('%s://%s/%s', $this->request->scheme(), $this->request->host(), $this->request->getAttribute('base')),
89
                    '/'
90
                )
91
            );
92
        }
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     */
98
    public function beforeFilter(Event $event)
99
    {
100
        if (!$this->request->is(['json', 'jsonapi'])) {
101
            throw new NotAcceptableException(
102
                __d('bedita', 'Bad request content type "{0}"', $this->request->getHeaderLine('Accept'))
103
            );
104
        }
105
106
        return null;
107
    }
108
109
    /**
110
     * Get application from request.
111
     * This is done primarily with an API_KEY header like 'X-Api-Key',
112
     * alternatively `api_key` query string is used (not recommended)
113
     *
114
     * @return void
115
     * @throws \Cake\Network\Exception\ForbiddenException Throws an exception if API key is missing or invalid.
116
     */
117
    protected function getApplication()
118
    {
119
        if (CurrentApplication::getApplication() === null) {
120
            $apiKey = $this->request->getHeaderLine('X-Api-Key');
121
            if (empty($apiKey)) {
122
                $apiKey = (string)$this->request->getQuery('api_key');
123
            }
124
            if (empty($apiKey) && empty(Configure::read('Security.blockAnonymousApps'))) {
125
                return;
126
            }
127
128
            try {
129
                CurrentApplication::setFromApiKey($apiKey);
130
            } catch (BadMethodCallException $e) {
131
                throw new ForbiddenException(__d('bedita', 'Missing API key'));
132
            } catch (RecordNotFoundException $e) {
133
                throw new ForbiddenException(__d('bedita', 'Invalid API key'));
134
            }
135
        }
136
    }
137
138
    /**
139
     * Prepare a list of associations to be contained from `?include` query parameter.
140
     *
141
     * @param string|array|null $include Association(s) to be included.
142
     * @return array
143
     * @throws \Cake\Network\Exception\BadRequestException Throws an exception if a
144
     */
145
    protected function prepareInclude($include)
146
    {
147
        if ($include === null) {
148
            return [];
149
        }
150
        if (!is_string($include)) {
151
            throw new BadRequestException(
152
                __d('bedita', 'Invalid "{0}" query parameter ({1})', 'include', __d('bedita', 'Must be a comma-separated string'))
153
            );
154
        }
155
156
        $contain = [];
157
        $include = array_filter(array_map('trim', explode(',', $include)));
158
        foreach ($include as $relationship) {
159
            if (strpos($relationship, '.') !== false) {
160
                throw new BadRequestException(__d('bedita', 'Inclusion of nested resources is not yet supported'));
161
            }
162
163
            try {
164
                $association = $this->findAssociation($relationship);
165
            } catch (NotFoundException $e) {
166
                throw new BadRequestException(
167
                    __d('bedita', 'Invalid "{0}" query parameter ({1})', 'include', __d('bedita', 'Relationship "{0}" does not exist', $relationship))
168
                );
169
            }
170
171
            $contain[] = $association->getName();
172
        }
173
174
        return $contain;
175
    }
176
177
    /**
178
     * Find the association corresponding to the relationship name.
179
     * Subclasses need to override this method.
180
     *
181
     * @param string $relationship Relationship name.
182
     * @return \Cake\ORM\Association|void
183
     * @throws \Cake\Network\Exception\NotFoundException Throws an exception if no suitable association could be found.
184
     * @codeCoverageIgnore
185
     */
186
    protected function findAssociation($relationship)
187
    {
188
        throw new NotFoundException(__d('bedita', 'Relationship "{0}" does not exist', $relationship));
189
    }
190
}
191