Completed
Push — master ( 34d5f0...715aef )
by Chauncey
07:28
created

AdminAction::getLastCaptchaValidation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Charcoal\Admin;
4
5
use RuntimeException;
6
7
// From PSR-7
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
11
// From Pimple
12
use Pimple\Container;
13
14
// From 'charcoal-factory'
15
use Charcoal\Factory\FactoryInterface;
16
17
// From 'charcoal-user'
18
use Charcoal\User\AuthAwareInterface;
19
use Charcoal\User\AuthAwareTrait;
20
21
// From 'charcoal-translator'
22
use Charcoal\Translator\Translation;
23
use Charcoal\Translator\TranslatorAwareTrait;
24
25
// From 'charcoal-app'
26
use Charcoal\App\Action\AbstractAction;
27
28
// From 'charcoal-admin'
29
use Charcoal\Admin\Ui\FeedbackContainerTrait;
30
use Charcoal\Admin\Support\AdminTrait;
31
use Charcoal\Admin\Support\BaseUrlTrait;
32
use Charcoal\Admin\Support\SecurityTrait;
33
34
/**
35
 * The base class for the `admin` Actions.
36
 */
37
abstract class AdminAction extends AbstractAction implements
38
    AuthAwareInterface
39
{
40
    use AdminTrait;
41
    use AuthAwareTrait;
42
    use BaseUrlTrait;
43
    use FeedbackContainerTrait;
44
    use SecurityTrait;
45
    use TranslatorAwareTrait;
46
47
    const GOOGLE_RECAPTCHA_SERVER_URL = 'https://www.google.com/recaptcha/api/siteverify';
48
49
    /**
50
     * The name of the project.
51
     *
52
     * @var \Charcoal\Translator\Translation|string|null
53
     */
54
    private $siteName;
55
56
    /**
57
     * Store the user response token from the last validation by Google reCAPTCHA API.
58
     *
59
     * @var string|null
60
     */
61
    private $recaptchaLastToken;
62
63
    /**
64
     * Store the result from the last validation by Google reCAPTCHA API.
65
     *
66
     * @var array|null
67
     */
68
    private $recaptchaLastResult;
69
70
    /**
71
     * Store the model factory.
72
     *
73
     * @var FactoryInterface $modelFactory
74
     */
75
    private $modelFactory;
76
77
    /**
78
     * Action's init method is called automatically from `charcoal-app`'s Action Route.
79
     *
80
     * For admin actions, initializations is:
81
     *
82
     * - to start a session, if necessary
83
     * - to authenticate
84
     * - to initialize the action data with the PSR Request object
85
     *
86
     * @param RequestInterface $request The request to initialize.
87
     * @return boolean
88
     * @see \Charcoal\App\Route\TemplateRoute::__invoke()
89
     */
90
    public function init(RequestInterface $request)
91
    {
92
        if (!session_id()) {
93
            session_cache_limiter(false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $cache_limiter of session_cache_limiter(). ( Ignorable by Annotation )

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

93
            session_cache_limiter(/** @scrutinizer ignore-type */ false);
Loading history...
94
            session_start();
95
        }
96
97
        $this->setDataFromRequest($request);
98
        $this->authRedirect($request);
99
100
        return parent::init($request);
101
    }
102
103
    /**
104
     * Determine if the current user is authenticated, if not redirect them to the login page.
105
     *
106
     * @todo   Move auth-check and redirection to a middleware or dedicated admin route.
107
     * @param  RequestInterface $request The request to initialize.
108
     * @return void
109
     */
110
    protected function authRedirect(RequestInterface $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

110
    protected function authRedirect(/** @scrutinizer ignore-unused */ RequestInterface $request)

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

Loading history...
111
    {
112
        // Test if authentication is required.
113
        if ($this->authRequired() === false) {
0 ignored issues
show
introduced by
The condition $this->authRequired() === false is always false.
Loading history...
114
            return;
115
        }
116
117
        // Test if user is authorized to access this controller
118
        if ($this->isAuthorized() === true) {
119
            return;
120
        }
121
122
        header('HTTP/1.0 403 Forbidden');
123
        exit;
124
    }
125
126
    /**
127
     * Sets the action data from a PSR Request object.
128
     *
129
     * @param  RequestInterface $request A PSR-7 compatible Request instance.
130
     * @return self
131
     */
132
    protected function setDataFromRequest(RequestInterface $request)
133
    {
134
        $keys = $this->validDataFromRequest();
135
        if (!empty($keys)) {
136
            $this->setData($request->getParams($keys));
0 ignored issues
show
Bug introduced by
The method getParams() does not exist on Psr\Http\Message\RequestInterface. It seems like you code against a sub-type of Psr\Http\Message\RequestInterface such as Slim\Http\Request. ( Ignorable by Annotation )

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

136
            $this->setData($request->/** @scrutinizer ignore-call */ getParams($keys));
Loading history...
137
        }
138
139
        return $this;
140
    }
141
142
    /**
143
     * Retrieve the list of parameters to extract from the HTTP request.
144
     *
145
     * @return string[]
146
     */
147
    protected function validDataFromRequest()
148
    {
149
        return [
150
            // HTTP Handling
151
            'next_url',
152
        ];
153
    }
154
155
    /**
156
     * Retrieve the name of the project.
157
     *
158
     * @return Translation|string|null
159
     */
160
    public function siteName()
161
    {
162
        return $this->siteName;
163
    }
164
165
    /**
166
     * Default response stub.
167
     *
168
     * @return array
169
     */
170
    public function results()
171
    {
172
        $results = [
173
            'success'   => $this->success(),
174
            'next_url'  => $this->redirectUrl(),
175
            'feedbacks' => $this->feedbacks()
176
        ];
177
        return $results;
178
    }
179
180
    /**
181
     * Set common dependencies used in all admin actions.
182
     *
183
     * @param  Container $container DI Container.
184
     * @return void
185
     */
186
    protected function setDependencies(Container $container)
187
    {
188
        parent::setDependencies($container);
189
190
        // Satisfies TranslatorAwareTrait dependencies
191
        $this->setTranslator($container['translator']);
192
193
        // Satisfies AuthAwareInterface + SecurityTrait dependencies
194
        $this->setAuthenticator($container['admin/authenticator']);
195
        $this->setAuthorizer($container['admin/authorizer']);
196
197
        // Satisfies AdminTrait dependencies
198
        $this->setDebug($container['config']);
199
        $this->setAppConfig($container['config']);
200
        $this->setAdminConfig($container['admin/config']);
201
202
        // Satisfies BaseUrlTrait dependencies
203
        $this->setBaseUrl($container['base-url']);
204
        $this->setAdminUrl($container['admin/base-url']);
205
206
207
        // Satisfies AdminAction dependencies
208
        $this->setSiteName($container['config']['project_name']);
209
        $this->setModelFactory($container['model/factory']);
210
    }
211
212
    /**
213
     * @param FactoryInterface $factory The factory used to create models.
214
     * @return void
215
     */
216
    protected function setModelFactory(FactoryInterface $factory)
217
    {
218
        $this->modelFactory = $factory;
219
    }
220
221
    /**
222
     * @return FactoryInterface The model factory.
223
     */
224
    protected function modelFactory()
225
    {
226
        return $this->modelFactory;
227
    }
228
229
    /**
230
     * Set the name of the project.
231
     *
232
     * @param  string $name Name of the project.
233
     * @return AdminAction Chainable
234
     */
235
    protected function setSiteName($name)
236
    {
237
        $this->siteName = $this->translator()->translation($name);
238
239
        return $this;
240
    }
241
242
    /**
243
     * Determine if a CAPTCHA test is available.
244
     *
245
     * For example, the "Login", "Lost Password", and "Reset Password" templates
246
     * can render the CAPTCHA test.
247
     *
248
     * @see    AdminTemplate::recaptchaEnabled() Duplicate
249
     * @return boolean
250
     */
251
    public function recaptchaEnabled()
252
    {
253
        $recaptcha = $this->apiConfig('google.recaptcha');
254
255
        return (!empty($recaptcha['public_key'])  || !empty($recaptcha['key'])) &&
256
               (!empty($recaptcha['private_key']) || !empty($recaptcha['secret']));
257
    }
258
259
    /**
260
     * Retrieve the Google reCAPTCHA secret key.
261
     *
262
     * @throws RuntimeException If Google reCAPTCHA is required but not configured.
263
     * @return string|null
264
     */
265
    public function recaptchaSecretKey()
266
    {
267
        $recaptcha = $this->apiConfig('google.recaptcha');
268
269
        if (!empty($recaptcha['private_key'])) {
270
            return $recaptcha['private_key'];
271
        } elseif (!empty($recaptcha['secret'])) {
272
            return $recaptcha['secret'];
273
        }
274
275
        return null;
276
    }
277
278
    /**
279
     * Retrieve the result from the last validation by Google reCAPTCHA API.
280
     *
281
     * @return array|null
282
     */
283
    protected function getLastCaptchaValidation()
284
    {
285
        return $this->recaptchaLastResult;
286
    }
287
288
    /**
289
     * Validate a Google reCAPTCHA user response.
290
     *
291
     * @todo   {@link https://github.com/mcaskill/charcoal-recaptcha Implement CAPTCHA validation as a service}.
292
     * @link   https://developers.google.com/recaptcha/docs/verify
293
     * @param  string $token A user response token provided by reCAPTCHA.
294
     * @throws RuntimeException If Google reCAPTCHA is not configured.
295
     * @return boolean Returns TRUE if the user response is valid, FALSE if it is invalid.
296
     */
297
    protected function validateCaptcha($token)
298
    {
299
        if (empty($token)) {
300
            throw new RuntimeException('Google reCAPTCHA response parameter is invalid or malformed.');
301
        }
302
303
        $secret = $this->recaptchaSecretKey();
304
        if (empty($secret)) {
305
            throw new RuntimeException('Google reCAPTCHA [apis.google.recaptcha.private_key] is not configured.');
306
        }
307
308
        $data = [
309
            'secret'   => $secret,
310
            'response' => $token,
311
        ];
312
313
        if (isset($_SERVER['REMOTE_ADDR'])) {
314
            $data['remoteip'] = $_SERVER['REMOTE_ADDR'];
315
        }
316
317
        $query = http_build_query($data);
318
        $url   = static::GOOGLE_RECAPTCHA_SERVER_URL.'?'.$query;
319
320
        $this->logger->debug(sprintf('Verifying reCAPTCHA user response: %s', $url));
321
322
        /**
323
         * @todo Use Guzzle
324
         */
325
        $result = file_get_contents($url);
326
        $result = (array)json_decode($result, true);
327
328
        $this->recaptchaLastToken  = $token;
329
        $this->recaptchaLastResult = $result;
330
331
        return isset($result['success']) && (bool)$result['success'];
332
    }
333
334
    /**
335
     * Validate a Google reCAPTCHA user response from a PSR Request object.
336
     *
337
     * @param  RequestInterface       $request  A PSR-7 compatible Request instance.
338
     * @param  ResponseInterface|null $response A PSR-7 compatible Response instance.
339
     *     If $response is provided and challenge fails, then it is replaced
340
     *     with a new Response object that represents a client error.
341
     * @return boolean Returns TRUE if the user response is valid, FALSE if it is invalid.
342
     */
343
    protected function validateCaptchaFromRequest(RequestInterface $request, ResponseInterface &$response = null)
344
    {
345
        $token = $request->getParam('g-recaptcha-response', false);
0 ignored issues
show
Bug introduced by
The method getParam() does not exist on Psr\Http\Message\RequestInterface. It seems like you code against a sub-type of Psr\Http\Message\RequestInterface such as Slim\Http\Request. ( Ignorable by Annotation )

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

345
        /** @scrutinizer ignore-call */ 
346
        $token = $request->getParam('g-recaptcha-response', false);
Loading history...
346
        if (empty($token)) {
347
            if ($response !== null) {
348
                $this->addFeedback('error', $this->translator()->translate('Missing CAPTCHA response.'));
349
                $this->setSuccess(false);
350
351
                $response = $response->withStatus(400);
352
            }
353
354
            return false;
355
        }
356
357
        $result = $this->validateCaptcha($token);
358
        if ($result === false && $response !== null) {
359
            $this->addFeedback('error', $this->translator()->translate('Invalid or malformed CAPTCHA response.'));
360
            $this->setSuccess(false);
361
362
            $response = $response->withStatus(400);
363
        }
364
365
        return $result;
366
    }
367
}
368