Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
38 / 40
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DefaultAuthorizationForm
95.00% covered (success)
95.00%
38 / 40
50.00% covered (danger)
50.00%
2 / 4
13
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 showForm
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
7
 transformAuthorizationCode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2
3namespace Taproot\IndieAuth\Callback;
4
5use BadMethodCallException;
6use BarnabyWalters\Mf2 as M;
7use Exception;
8use Psr\Http\Message\ServerRequestInterface;
9use GuzzleHttp\Psr7\Response;
10use Psr\Http\Message\ResponseInterface;
11use Psr\Log\LoggerAwareInterface;
12use Psr\Log\LoggerInterface;
13use Psr\Log\NullLogger;
14
15use function Taproot\IndieAuth\renderTemplate;
16
17/**
18 * Default Authorization Form
19 * 
20 * This implementation of {@see AuthorizationFormInterface} is used by {@see \Taproot\IndieAuth\Server} if the user doesn’t 
21 * provide one of their own. It presents the user with a simple consent screen, showing any
22 * available details about the client app, and allowing the user to grant any requested scopes.
23 * 
24 * When the form is submitted, any granted scopes are then added to the authorization code data.
25 * 
26 * You can customise the authorization template used by passing either path to your own template or 
27 * a custom callable to the constructor. Refer to the default template `/templates/default_authorization_page.html.php`
28 * as a starting point.
29 * 
30 * If you want to add additional form controls (e.g. configurable token lifetimes), as well as
31 * making a new template, you’ll need to make a subclass which overrides {@see DefaultAuthorizationForm::transformAuthorizationCode()}
32 * to additionally handle your new form data.
33 * 
34 * For any more involved customisation, it may make sense to create your own implementation of {@see AuthorizationFormInterface}.
35 */
36class DefaultAuthorizationForm implements AuthorizationFormInterface, LoggerAwareInterface {
37    /** @var string $csrfKey */
38    public $csrfKey;
39
40    /** @var callable $formTemplateCallable */
41    private $formTemplateCallable;
42
43    /** @var LoggerInterface $logger */
44    private $logger;
45
46    /**
47     * Constructor
48     * 
49     * @param string|callable|null $formTemplate The path to a custom template, or a template callable with the signature `function (array $context): string`. Uses the default if null.
50     * @param string|null $csrfKey The key used to retrieve a CSRF token from the request attributes, and as its form data name. Uses the default defined in Server if null. Only change this if you’re using a custom CSRF middleware.
51     * @param LoggerInterface|null $logger A logger.
52     */
53    public function __construct($formTemplate=null, ?string $csrfKey=null, ?LoggerInterface $logger=null) {
54        $this->csrfKey = $csrfKey ?? \Taproot\IndieAuth\Server::DEFAULT_CSRF_KEY;
55        $this->logger = $logger ?? new NullLogger;
56
57        $formTemplate = $formTemplate ?? __DIR__ . '/../../templates/default_authorization_page.html.php';
58        if (is_string($formTemplate)) {
59            if (!file_exists($formTemplate)) {
60                $this->logger->warning("\$formTemplate string passed to DefaultAuthorizationForm was not a valid path.", ['formTemplate' => $formTemplate]);
61            }
62            $formTemplate = function (array $context) use ($formTemplate): string {
63                return renderTemplate($formTemplate, $context);
64            };
65        }
66
67        if (!is_callable($formTemplate)) {
68            throw new BadMethodCallException("\$formTemplate must be a string (path), callable, or null.");
69        }
70
71        $this->formTemplateCallable = $formTemplate;
72    }
73
74    public function showForm(ServerRequestInterface $request, array $authenticationResult, string $formAction, $clientHAppOrException): ResponseInterface {
75        // Show an authorization page. List all requested scopes, as this default
76        // function has no way of knowing which scopes are supported by the consumer.
77        $scopes = [];
78        foreach(explode(' ', $request->getQueryParams()['scope'] ?? '') as $s) {
79            $scopes[$s] = null; // Ideally there would be a description of the scope here, we don’t have one though.
80        }
81
82        $exception = null;
83        $appData = null;
84        if ($clientHAppOrException instanceof Exception) {
85            $exception = $clientHAppOrException;
86        } elseif (M\isMicroformat($clientHAppOrException)) {
87            $appData = [
88                'name' => M\getPlaintext($clientHAppOrException, 'name'),
89                'url' => M\getPlaintext($clientHAppOrException, 'url'),
90                'photo' => M\getPlaintext($clientHAppOrException, 'photo'),
91                'author' => null
92            ];
93
94            if (M\hasProp($clientHAppOrException, 'author')) {
95                $rawAuthor = $clientHAppOrException['properties']['author'][0];
96                if (is_string($rawAuthor)) {
97                    $appData['author'] = $rawAuthor;
98                } elseif (M\isMicroformat($rawAuthor)) {
99                    $appData['author'] = [
100                        'name' => M\getPlaintext($rawAuthor, 'name'),
101                        'url' => M\getPlaintext($rawAuthor, 'url'),
102                        'photo' => M\getPlaintext($rawAuthor, 'photo')
103                    ];
104                }
105            }
106        }
107
108        return new Response(200, ['content-type' => 'text/html'], call_user_func($this->formTemplateCallable, [
109            'scopes' => $scopes,
110            'user' => $authenticationResult,
111            'formAction' => $formAction,
112            'request' => $request,
113            'clientHApp' => $appData,
114            'exception' => $exception,
115            'clientId' => $request->getQueryParams()['client_id'],
116            'clientRedirectUri' => $request->getQueryParams()['redirect_uri'],
117            'csrfFormElement' => '<input type="hidden" name="' . htmlentities($this->csrfKey) . '" value="' . htmlentities($request->getAttribute($this->csrfKey)) . '" />'
118        ]));
119    }
120
121    public function transformAuthorizationCode(ServerRequestInterface $request, array $code): array {
122        // Add any granted scopes from the form to the code.
123        $grantedScopes = $request->getParsedBody()['taproot_indieauth_server_scope'] ?? [];
124
125        // This default implementation naievely accepts any scopes it receives from the form.
126        // You may wish to perform some sort of validation.
127        $code['scope'] = join(' ', $grantedScopes);
128
129        // You may wish to additionally make any other necessary changes to the the code based on
130        // the form submission, e.g. if the user set a custom token lifetime, or wanted extra data
131        // stored on the token to affect how it behaves.
132        return $code;
133    }
134
135    public function setLogger(LoggerInterface $logger): void {
136        $this->logger = $logger;
137    }
138}