Application   A
last analyzed

Complexity

Total Complexity 18

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 80
c 0
b 0
f 0
dl 0
loc 214
rs 10
wmc 18

7 Methods

Rating   Name   Duplication   Size   Complexity  
A middleware() 0 49 1
A loadPluginsFromConfig() 0 8 4
A bootstrapCli() 0 6 1
A getAuthenticationService() 0 44 4
A csrfMiddleware() 0 14 2
A bootstrap() 0 21 3
A loadProjectConfig() 0 9 3
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2022 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 App;
14
15
use App\Event\TreeCacheEventHandler;
16
use App\Identifier\ApiIdentifier;
17
use App\Middleware\ConfigurationMiddleware;
18
use App\Middleware\ProjectMiddleware;
19
use App\Middleware\RecoveryMiddleware;
20
use App\Middleware\StatusMiddleware;
21
use Authentication\AuthenticationService;
22
use Authentication\AuthenticationServiceInterface;
23
use Authentication\AuthenticationServiceProviderInterface;
24
use Authentication\Identifier\IdentifierInterface;
25
use Authentication\Middleware\AuthenticationMiddleware;
26
use BEdita\I18n\Middleware\I18nMiddleware;
27
use BEdita\WebTools\Middleware\OAuth2Middleware;
28
use Cake\Core\Configure;
29
use Cake\Core\Configure\Engine\PhpConfig;
30
use Cake\Error\Middleware\ErrorHandlerMiddleware;
31
use Cake\Event\EventManager;
32
use Cake\Http\BaseApplication;
33
use Cake\Http\Middleware\BodyParserMiddleware;
34
use Cake\Http\Middleware\CsrfProtectionMiddleware;
35
use Cake\Http\MiddlewareQueue;
36
use Cake\Routing\Middleware\AssetMiddleware;
37
use Cake\Routing\Middleware\RoutingMiddleware;
38
use Psr\Http\Message\ServerRequestInterface;
39
40
/**
41
 * Application class.
42
 */
43
class Application extends BaseApplication implements AuthenticationServiceProviderInterface
44
{
45
    /**
46
     * Default plugin options
47
     *
48
     * @var array
49
     */
50
    protected const PLUGIN_DEFAULTS = [
51
        'debugOnly' => false,
52
        'autoload' => false,
53
        'bootstrap' => true,
54
        'routes' => true,
55
        'ignoreMissing' => true,
56
    ];
57
58
    /**
59
     * @inheritDoc
60
     */
61
    public function bootstrap(): void
62
    {
63
        parent::bootstrap();
64
65
        if (PHP_SAPI === 'cli') {
66
            $this->bootstrapCli();
67
        }
68
69
        /*
70
         * Only try to load DebugKit in development mode
71
         * Debug Kit should not be installed on a production system
72
         */
73
        if (Configure::read('debug')) {
74
            $this->addOptionalPlugin('DebugKit');
75
        }
76
77
        $this->addPlugin('BEdita/WebTools');
78
        $this->addPlugin('BEdita/I18n');
79
        $this->addPlugin('Authentication');
80
81
        EventManager::instance()->on(new TreeCacheEventHandler());
82
    }
83
84
    /**
85
     * Load CLI plugins
86
     *
87
     * @return void
88
     */
89
    protected function bootstrapCli(): void
90
    {
91
        $this->addOptionalPlugin('Bake');
92
        $this->addOptionalPlugin('IdeHelper');
93
        $this->addOptionalPlugin('Cake/Repl');
94
        $this->loadPluginsFromConfig();
95
    }
96
97
    /**
98
     * Load plugins from 'Plugins' configuration.
99
     * This method will be invoked by `ProjectMiddleware`.
100
     * Should not be invoked in `bootstrap`, to avoid duplicate plugin bootstratp calls.
101
     *
102
     * @return void
103
     */
104
    public function loadPluginsFromConfig(): void
105
    {
106
        $plugins = (array)Configure::read('Plugins');
107
        foreach ($plugins as $plugin => $options) {
108
            $options = array_merge(static::PLUGIN_DEFAULTS, $options);
109
            if (!$options['debugOnly'] || Configure::read('debug')) {
110
                $this->addPlugin($plugin, $options);
111
                $this->plugins->get($plugin)->bootstrap($this);
112
            }
113
        }
114
    }
115
116
    /**
117
     * @inheritDoc
118
     */
119
    public function middleware($middlewareQueue): MiddlewareQueue
120
    {
121
        $middlewareQueue
122
            // Catch any exceptions in the lower layers,
123
            // and make an error page/response
124
            ->add(new ErrorHandlerMiddleware(Configure::read('Error')))
125
126
            // Load current project configuration if `multiproject` instance
127
            // Manager plugins will also be loaded here via `loadPluginsFromConfig()`
128
            ->add(new ProjectMiddleware($this))
129
130
            // Provides a `GET /status` endpoint. This must be
131
            ->add(new StatusMiddleware())
132
133
            // Load configuration from API for the current project.
134
            ->add(new ConfigurationMiddleware())
135
136
            // Handle plugin/theme assets like CakePHP normally does.
137
            ->add(new AssetMiddleware([
138
                'cacheTime' => Configure::read('Asset.cacheTime'),
139
            ]))
140
141
            // Add I18n middleware.
142
            ->add(new I18nMiddleware([
143
                'cookie' => Configure::read('I18n.cookie'),
144
                'switchLangUrl' => Configure::read('I18n.switchLangUrl'),
145
            ]))
146
147
            // Add routing middleware.
148
            ->add(new RoutingMiddleware($this))
149
150
            // Csrf Middleware
151
            ->add($this->csrfMiddleware())
152
153
            // Authentication middleware.
154
            ->add(new AuthenticationMiddleware($this))
155
156
            // Authentication middleware.
157
            ->add(new OAuth2Middleware())
158
159
            // Recovery middleware
160
            ->add(new RecoveryMiddleware())
161
162
            // Parse various types of encoded request bodies so that they are
163
            // available as array through $request->getData()
164
            // https://book.cakephp.org/4/en/controllers/middleware.html#body-parser-middleware
165
            ->add(new BodyParserMiddleware());
166
167
        return $middlewareQueue;
168
    }
169
170
    /**
171
     * Get internal Csrf Middleware
172
     *
173
     * @return \Cake\Http\Middleware\CsrfProtectionMiddleware
174
     */
175
    protected function csrfMiddleware(): CsrfProtectionMiddleware
176
    {
177
        // Csrf Middleware
178
        $csrf = new CsrfProtectionMiddleware(['httponly' => true]);
179
        // Token check will be skipped when callback returns `true`.
180
        $csrf->skipCheckCallback(function ($request) {
181
            $actions = (array)Configure::read(sprintf('CsrfExceptions.%s', $request->getParam('controller')));
182
            // Skip token check for API URLs.
183
            if (in_array($request->getParam('action'), $actions)) {
184
                return true;
185
            }
186
        });
187
188
        return $csrf;
189
    }
190
191
    /**
192
     * Load project configuration if corresponding config file is found
193
     *
194
     * @param string|null $project The project name.
195
     * @param string $projectsPath The project configuration files base path.
196
     * @return void
197
     */
198
    public static function loadProjectConfig(?string $project, string $projectsPath): void
199
    {
200
        if (empty($project)) {
201
            return;
202
        }
203
204
        if (file_exists($projectsPath . $project . '.php')) {
205
            Configure::config('projects', new PhpConfig($projectsPath));
206
            Configure::load($project, 'projects');
207
        }
208
    }
209
210
    /**
211
     * @inheritDoc
212
     */
213
    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
214
    {
215
        $service = new AuthenticationService([
216
            'unauthenticatedRedirect' => '/login',
217
            'queryParam' => 'redirect',
218
        ]);
219
220
        $path = $request->getUri()->getPath();
221
        $query = $request->getQueryParams();
222
        if (strpos($path, '/ext/login') === 0) {
223
            $providers = (array)Configure::read('OAuth2Providers');
224
            $service->loadIdentifier('BEdita/WebTools.OAuth2', compact('providers'));
225
            $service->loadAuthenticator('BEdita/WebTools.OAuth2', compact('providers') + [
226
                'redirect' => ['_name' => 'login:oauth2'],
227
            ]);
228
229
            if (empty($query)) {
230
                return $service;
231
            }
232
        }
233
234
        $service->loadAuthenticator('Authentication.Session', [
235
            'sessionKey' => 'BEditaManagerAuth',
236
            'fields' => [
237
                IdentifierInterface::CREDENTIAL_TOKEN => 'token',
238
            ],
239
        ]);
240
241
        $service->loadIdentifier(ApiIdentifier::class, [
242
                'timezoneField' => 'timezone',
243
        ]);
244
245
        if ($path === '/login') {
246
            $service->loadAuthenticator('Authentication.Form', [
247
                'loginUrl' => '/login',
248
                'fields' => [
249
                    IdentifierInterface::CREDENTIAL_USERNAME => 'username',
250
                    IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
251
                    'timezone' => 'timezone_offset',
252
                ],
253
            ]);
254
        }
255
256
        return $service;
257
    }
258
}
259