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