├── docs
├── coverage
│ ├── phpunit_css
│ │ ├── custom.css
│ │ ├── octicons.css
│ │ └── style.css
│ ├── phpunit_icons
│ │ ├── file-directory.svg
│ │ └── file-code.svg
│ ├── phpunit_js
│ │ └── file.js
│ └── Storage
│ │ ├── Sqlite3Storage.php.html
│ │ └── index.html
├── graphs
│ └── classes.html
├── reports
│ ├── errors.html
│ └── deprecated.html
├── files
│ ├── src-storage-sqlite3storage.html
│ ├── src-server.html
│ ├── src-indieauthexception.html
│ ├── src-middleware-noopmiddleware.html
│ ├── src-middleware-closurerequesthandler.html
│ ├── src-middleware-responserequesthandler.html
│ ├── src-storage-tokenstorageinterface.html
│ ├── src-storage-filesystemjsonstorage.html
│ ├── src-callback-defaultauthorizationform.html
│ ├── src-callback-authorizationforminterface.html
│ └── src-middleware-doublesubmitcookiecsrfmiddleware.html
├── packages
│ └── default.html
├── namespaces
│ ├── default.html
│ └── taproot.html
├── index.html
├── css
│ └── template.css
└── js
│ └── search.js
├── .gitignore
├── phpunit.xml
├── src
├── Storage
│ └── Sqlite3Storage.php
├── Middleware
│ ├── ResponseRequestHandler.php
│ ├── NoOpMiddleware.php
│ ├── ClosureRequestHandler.php
│ └── DoubleSubmitCookieCsrfMiddleware.php
├── IndieAuthException.php
└── Callback
│ ├── AuthorizationFormInterface.php
│ ├── DefaultAuthorizationForm.php
│ └── SingleUserPasswordAuthenticationCallback.php
├── tests
├── templates
│ ├── code_exception_response.txt.php
│ └── authorization_form_json_response.json.php
├── DefaultAuthorizationFormTest.php
├── FilesystemJsonStorageTest.php
├── SingleUserPasswordAuthenticationCallbackTest.php
└── FunctionTest.php
├── infection.json.dist
├── psalm.xml
├── run_coverage.sh
├── phpdoc.dist.xml
├── composer.json
├── .github
└── workflows
│ └── php.yml
├── templates
├── single_user_password_authentication_form.html.php
├── default_exception_response.html.php
└── index.php
└── bin
└── migrate.php
/docs/coverage/phpunit_css/custom.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | vendor
3 | composer.lock
4 | tests/tmp/*
5 | phpDocumentor.phar
6 | phpdoc.xml
7 | .phpdoc
8 | .phpunit.result.cache
--------------------------------------------------------------------------------
/docs/coverage/phpunit_css/octicons.css:
--------------------------------------------------------------------------------
1 | .octicon {
2 | display: inline-block;
3 | vertical-align: text-top;
4 | fill: currentColor;
5 | }
6 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | tests
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Storage/Sqlite3Storage.php:
--------------------------------------------------------------------------------
1 | = $exception->getCode() ?>
--------------------------------------------------------------------------------
/docs/coverage/phpunit_icons/file-directory.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ],
6 | "excludes": [
7 | "test"
8 | ]
9 | },
10 | "mutators": {
11 | "@default": true
12 | },
13 | "logs": {
14 | "text": "tests/tmp/infection.log"
15 | }
16 | }
--------------------------------------------------------------------------------
/docs/coverage/phpunit_icons/file-code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/run_coverage.sh:
--------------------------------------------------------------------------------
1 | rm -rf docs/coverage/
2 | XDEBUG_MODE=coverage ./vendor/bin/phpunit tests --coverage-filter src --coverage-html docs/coverage
3 | mv docs/coverage/_css docs/coverage/phpunit_css
4 | mv docs/coverage/_icons docs/coverage/phpunit_icons
5 | mv docs/coverage/_js docs/coverage/phpunit_js
6 | grep -rl _css docs/coverage | xargs sed -i "" -e 's/_css/phpunit_css/g'
7 | grep -rl _icons docs/coverage | xargs sed -i "" -e 's/_icons/phpunit_icons/g'
8 | grep -rl _js docs/coverage | xargs sed -i "" -e 's/_js/phpunit_js/g'
--------------------------------------------------------------------------------
/phpdoc.dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | docs
10 |
11 |
12 |
13 |
14 | src
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Middleware/ResponseRequestHandler.php:
--------------------------------------------------------------------------------
1 | response = $response;
15 | }
16 |
17 | public function handle(ServerRequestInterface $request): ResponseInterface {
18 | return $this->response;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Middleware/NoOpMiddleware.php:
--------------------------------------------------------------------------------
1 | handle($request);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Middleware/ClosureRequestHandler.php:
--------------------------------------------------------------------------------
1 | callable = $callable;
17 | $this->args = array_slice(func_get_args(), 1);
18 | }
19 |
20 | public function handle(ServerRequestInterface $request): ResponseInterface {
21 | return call_user_func_array($this->callable, array_merge([$request], $this->args));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/templates/authorization_form_json_response.json.php:
--------------------------------------------------------------------------------
1 | $formAction,
15 | 'clientHApp' => $clientHApp,
16 | 'exception' => is_null($exception) ? null : get_class($exception),
17 | 'user' => $user,
18 | 'scopes' => $scopes,
19 | 'clientId' => $clientId,
20 | 'clientRedirectUri' => $clientRedirectUri,
21 | 'csrfFormElement' => $csrfFormElement
22 | ]);
23 | ?>
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "taproot/indieauth",
3 | "description": "PHP PSR-7-compliant IndieAuth Server and Client implementation.",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Barnaby Walters",
9 | "email": "barnaby@waterpigs.co.uk"
10 | }
11 | ],
12 | "autoload": {
13 | "files": ["src/functions.php"],
14 | "psr-4": {
15 | "Taproot\\IndieAuth\\": "src"
16 | }
17 | },
18 | "autoload-dev": {
19 | "psr-4": {
20 | "Taproot\\IndieAuth\\Test\\": "tests"
21 | }
22 | },
23 | "require": {
24 | "php": ">= 7.3.0",
25 | "psr/http-message": "^1.0",
26 | "psr/log": "^1.1||^2.0||^3.0",
27 | "psr/http-server-middleware": "^1.0",
28 | "indieauth/client": "^1.1",
29 | "dflydev/fig-cookies": "^3.0",
30 | "mf2/mf2": "^0.4.6||^0.5",
31 | "barnabywalters/mf-cleaner": "^0.2",
32 | "guzzlehttp/psr7": "^1.8||^2.0"
33 | },
34 | "require-dev": {
35 | "guzzlehttp/guzzle": "^7.3",
36 | "phpunit/phpunit": "^9.5",
37 | "vimeo/psalm": "^4.7"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 |
13 | strategy:
14 | matrix:
15 | php: ['7.3', '7.4', '8.0', '8.1']
16 |
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: shivammathur/setup-php@2.11.0
21 | with:
22 | php-version: ${{ matrix.php }}
23 | extensions: 'xdebug'
24 |
25 | - uses: actions/checkout@v2
26 |
27 | - name: Validate composer.json and composer.lock
28 | run: composer validate --strict
29 |
30 | - name: Cache Composer packages
31 | id: composer-cache
32 | uses: actions/cache@v2
33 | with:
34 | path: vendor
35 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
36 | restore-keys: |
37 | ${{ runner.os }}-php-
38 |
39 | - name: Install dependencies
40 | run: composer install --prefer-dist --no-progress
41 |
42 | - name: Run Test Suite
43 | run: XDEBUG_MODE=coverage ./vendor/bin/phpunit tests --coverage-filter src --coverage-text
44 |
45 | - name: Run Static Analysis
46 | run: ./vendor/bin/psalm
47 |
--------------------------------------------------------------------------------
/tests/DefaultAuthorizationFormTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(1, $logCount);
22 | $this->assertEquals(LogLevel::WARNING, $logLevel);
23 | }
24 | }
25 |
26 | class MockLogger extends AbstractLogger {
27 | private $callback;
28 |
29 | public function __construct($callback) {
30 | $this->callback = $callback;
31 | }
32 |
33 | // Ignoring the signature mismatch error on this line for the moment to retain compat with PHP7.
34 | public function log($level, $message, array $context=[]): void {
35 | call_user_func($this->callback, $level, $message, $context);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/docs/coverage/phpunit_js/file.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | var $window = $(window)
3 | , $top_link = $('#toplink')
4 | , $body = $('body, html')
5 | , offset = $('#code').offset().top
6 | , hidePopover = function ($target) {
7 | $target.data('popover-hover', false);
8 |
9 | setTimeout(function () {
10 | if (!$target.data('popover-hover')) {
11 | $target.popover('hide');
12 | }
13 | }, 300);
14 | };
15 |
16 | $top_link.hide().click(function(event) {
17 | event.preventDefault();
18 | $body.animate({scrollTop:0}, 800);
19 | });
20 |
21 | $window.scroll(function() {
22 | if($window.scrollTop() > offset) {
23 | $top_link.fadeIn();
24 | } else {
25 | $top_link.fadeOut();
26 | }
27 | }).scroll();
28 |
29 | $('.popin')
30 | .popover({trigger: 'manual'})
31 | .on({
32 | 'mouseenter.popover': function () {
33 | var $target = $(this);
34 | var $container = $target.children().first();
35 |
36 | $target.data('popover-hover', true);
37 |
38 | // popover already displayed
39 | if ($target.next('.popover').length) {
40 | return;
41 | }
42 |
43 | // show the popover
44 | $container.popover('show');
45 |
46 | // register mouse events on the popover
47 | $target.next('.popover:not(.popover-initialized)')
48 | .on({
49 | 'mouseenter': function () {
50 | $target.data('popover-hover', true);
51 | },
52 | 'mouseleave': function () {
53 | hidePopover($container);
54 | }
55 | })
56 | .addClass('popover-initialized');
57 | },
58 | 'mouseleave.popover': function () {
59 | hidePopover($(this).children().first());
60 | }
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/templates/single_user_password_authentication_form.html.php:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | IndieAuth • Log In
11 |
12 |
49 |
50 |
51 |
61 |
62 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/templates/default_exception_response.html.php:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 | IndieAuth • Error!
10 |
11 |
44 |
45 |
46 |
47 | Error: = htmlentities($exception->getMessage()) ?>
48 |
49 | = htmlentities($exception->getExplanation()) ?>
50 |
51 |
53 | trustQueryParams() and !empty($request->getQueryParams()['client_id'])): ?>
54 | Return to app (= htmlentities($request->getQueryParams()['client_id']) ?> ?>)
55 |
56 |
57 |
58 |
59 |
60 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/bin/migrate.php:
--------------------------------------------------------------------------------
1 | function(string $path): array {
13 | $logs = [];
14 |
15 | foreach (new DirectoryIterator($path) as $fileInfo) {
16 | if ($fileInfo->isFile() && $fileInfo->getExtension() == 'json') {
17 | $tokenPath = $fileInfo->getPathname();
18 | $token = json_decode(file_get_contents($tokenPath), true);
19 |
20 | if (array_key_exists('exp', $token) or array_key_exists('code_exp', $token)) {
21 | // This token is new or has already been migrated.
22 | continue;
23 | }
24 |
25 | // The exact migration depends on whether the code was exchanged for a token already.
26 | if ($token['exchanged_at'] ?? false) {
27 | $token['exp'] = $token['valid_until'];
28 | $token['iat'] = $token['exchanged_at'];
29 | } else {
30 | $token['code_exp'] = $token['valid_until'];
31 | }
32 |
33 | file_put_contents($tokenPath, json_encode($token));
34 | }
35 | }
36 |
37 | return $logs;
38 | }
39 | ];
40 |
41 | function showHelp() {
42 | global $migrations; // ew, global variables :(
43 |
44 | echo << v0.2.0). The utility
50 | will be expanded when more migrations become necessary, and the command line interface
51 | will change, so it’s not recommended to automate running this tool.
52 |
53 | EOD;
54 | }
55 |
56 | // If this file is being executed directly…
57 | if (!empty($argv) and trim($argv[0]) !== '' and strpos($argv[0], 'migrate.php') !== false) {
58 | // Script currently only accepts a single JSON path argument. Expand when required.
59 | if ($argc != 2) {
60 | showHelp();
61 | die;
62 | }
63 |
64 | $jsonPath = rtrim($argv[1], '/');
65 |
66 | $logs = $migrations['json_v0.1.0_v0.2.0']($jsonPath);
67 |
68 | foreach ($logs as $logline) {
69 | echo "$logline\n";
70 | }
71 | }
72 |
73 | // For testing, and automating migrations (in the unlikely event anyone wants to do that).
74 | return $migrations;
75 |
--------------------------------------------------------------------------------
/docs/coverage/phpunit_css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 10px;
3 | }
4 |
5 | .popover {
6 | max-width: none;
7 | }
8 |
9 | .octicon {
10 | margin-right:.25em;
11 | }
12 |
13 | .table-bordered>thead>tr>td {
14 | border-bottom-width: 1px;
15 | }
16 |
17 | .table tbody>tr>td, .table thead>tr>td {
18 | padding-top: 3px;
19 | padding-bottom: 3px;
20 | }
21 |
22 | .table-condensed tbody>tr>td {
23 | padding-top: 0;
24 | padding-bottom: 0;
25 | }
26 |
27 | .table .progress {
28 | margin-bottom: inherit;
29 | }
30 |
31 | .table-borderless th, .table-borderless td {
32 | border: 0 !important;
33 | }
34 |
35 | .table tbody tr.covered-by-large-tests, li.covered-by-large-tests, tr.success, td.success, li.success, span.success {
36 | background-color: #dff0d8;
37 | }
38 |
39 | .table tbody tr.covered-by-medium-tests, li.covered-by-medium-tests {
40 | background-color: #c3e3b5;
41 | }
42 |
43 | .table tbody tr.covered-by-small-tests, li.covered-by-small-tests {
44 | background-color: #99cb84;
45 | }
46 |
47 | .table tbody tr.danger, .table tbody td.danger, li.danger, span.danger {
48 | background-color: #f2dede;
49 | }
50 |
51 | .table tbody tr.warning, .table tbody td.warning, li.warning, span.warning {
52 | background-color: #fcf8e3;
53 | }
54 |
55 | .table tbody td.info {
56 | background-color: #d9edf7;
57 | }
58 |
59 | td.big {
60 | width: 117px;
61 | }
62 |
63 | td.small {
64 | }
65 |
66 | td.codeLine {
67 | font-family: "Source Code Pro", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
68 | white-space: pre-wrap;
69 | }
70 |
71 | td span.comment {
72 | color: #888a85;
73 | }
74 |
75 | td span.default {
76 | color: #2e3436;
77 | }
78 |
79 | td span.html {
80 | color: #888a85;
81 | }
82 |
83 | td span.keyword {
84 | color: #2e3436;
85 | font-weight: bold;
86 | }
87 |
88 | pre span.string {
89 | color: #2e3436;
90 | }
91 |
92 | span.success, span.warning, span.danger {
93 | margin-right: 2px;
94 | padding-left: 10px;
95 | padding-right: 10px;
96 | text-align: center;
97 | }
98 |
99 | #toplink {
100 | position: fixed;
101 | left: 5px;
102 | bottom: 5px;
103 | outline: 0;
104 | }
105 |
106 | svg text {
107 | font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif;
108 | font-size: 11px;
109 | color: #666;
110 | fill: #666;
111 | }
112 |
113 | .scrollbox {
114 | height:245px;
115 | overflow-x:hidden;
116 | overflow-y:scroll;
117 | }
118 |
119 | table + .structure-heading {
120 | border-top: 1px solid lightgrey;
121 | padding-top: 0.5em;
122 | }
123 |
--------------------------------------------------------------------------------
/tests/FilesystemJsonStorageTest.php:
--------------------------------------------------------------------------------
1 | isFile()) {
17 | unlink($fileInfo->getPathname());
18 | }
19 | }
20 | }
21 |
22 | protected function tearDown(): void {
23 | // Clean tmp dir.
24 | foreach (new DirectoryIterator(TMP_DIR) as $fileInfo) {
25 | if ($fileInfo->isFile()) {
26 | unlink($fileInfo->getPathname());
27 | }
28 | }
29 | }
30 |
31 | public function testCrud() {
32 | $s = new FilesystemJsonStorage(TMP_DIR, SECRET);
33 |
34 | $t1data = ['example' => 'data'];
35 | $t1path = $s->getPath('t1');
36 |
37 | $this->assertTrue($s->put('t1', $t1data), "Saving t1 data failed");
38 |
39 | $this->assertFileExists($t1path, "t1 was not stored to $t1path");
40 |
41 | $this->assertEquals($t1data, $s->get('t1'), "The result of getting t1 did not match the saved data.");
42 |
43 | $s->delete('t1');
44 |
45 | $this->assertFileDoesNotExist($t1path, "t1 was not successfully deleted.");
46 |
47 | $this->assertNull($s->get('t1'), "Getting a nonexistent key did not return null");
48 | }
49 |
50 | public function testCleanUp() {
51 | $s = new FilesystemJsonStorage(TMP_DIR, SECRET);
52 | $s->put('t1', ['exp' => time() + 10]);
53 | $s->put('t2', ['exp' => time() - 10]);
54 | $s->deleteExpiredTokens();
55 | $this->assertIsArray($s->get('t1'), 't1 was not expired and should not have been deleted.');
56 | $this->assertNull($s->get('t2'), 't2 was not cleaned up after expiring.');
57 | }
58 |
59 | public function testMigrateFromV0_1_0ToV0_2_0() {
60 | $s = new FilesystemJsonStorage(TMP_DIR, SECRET);
61 |
62 | $oldUnexchangedCode = [
63 | 'me' => 'https://example.com',
64 | 'valid_until' => time() + 60 * 5
65 | ];
66 | $newUnexchangedCode = array_merge($oldUnexchangedCode, [
67 | 'code_exp' => $oldUnexchangedCode['valid_until']
68 | ]);
69 |
70 | $oldExchangedCode = [
71 | 'me' => 'https://example.com',
72 | 'exchanged_at' => time() + 60 * 1,
73 | 'valid_until' => time() + 60 * 5
74 | ];
75 | $newExchangedCode = array_merge($oldExchangedCode, [
76 | 'iat' => $oldExchangedCode['exchanged_at'],
77 | 'exp' => $oldExchangedCode['valid_until']
78 | ]);
79 |
80 | $s->put('t1', $oldUnexchangedCode);
81 | $s->put('t2', $oldExchangedCode);
82 |
83 | $migrationFunctions = require_once __DIR__ . '/../bin/migrate.php';
84 |
85 | $migrationFunctions['json_v0.1.0_v0.2.0'](TMP_DIR);
86 |
87 | $this->assertEquals($s->get('t1'), $newUnexchangedCode);
88 | $this->assertEquals($s->get('t2'), $newExchangedCode);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/IndieAuthException.php:
--------------------------------------------------------------------------------
1 | ['statusCode' => 500, 'name' => 'Internal Server Error', 'explanation' => 'An internal server error occurred.'],
29 | self::INTERNAL_ERROR_REDIRECT => ['statusCode' => 302, 'name' => 'Internal Server Error', 'error' => 'internal_error'],
30 | self::AUTHENTICATION_CALLBACK_INVALID_RETURN_VALUE => ['statusCode' => 302, 'name' => 'Internal Server Error', 'error' => 'internal_error'],
31 | self::AUTHENTICATION_CALLBACK_MISSING_ME_PARAM => ['statusCode' => 302, 'name' => 'Internal Server Error', 'error' => 'internal_error'],
32 | self::AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH => ['statusCode' => 302, 'name' => 'Request Missing Hash', 'error' => 'internal_error'],
33 | self::AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH => ['statusCode' => 302, 'name' => 'Request Hash Invalid', 'error' => 'internal_error'],
34 | // TODO: should this one be a 500 because it’s an internal server error, or a 400 because the client_id was likely invalid? Is anyone ever going to notice, or care?
35 | self::HTTP_EXCEPTION_FETCHING_CLIENT_ID => ['statusCode' => 500, 'name' => 'Error Fetching Client App URL', 'explanation' => 'Fetching the client app (client_id) failed.'],
36 | self::INTERNAL_EXCEPTION_FETCHING_CLIENT_ID => ['statusCode' => 500, 'name' => 'Internal Error fetching client app URI', 'explanation' => 'Fetching the client app (client_id) failed due to an internal error.'],
37 | self::INVALID_REDIRECT_URI => ['statusCode' => 400, 'name' => 'Invalid Client App Redirect URI', 'explanation' => 'The client app redirect URI (redirect_uri) either was not a valid URI, did not sufficiently match client_id, or did not exactly match any redirect URIs parsed from fetching the client_id.'],
38 | self::INVALID_CLIENT_ID => ['statusCode' => 400, 'name' => 'Invalid Client Identifier URI', 'explanation' => 'The Client Identifier was not valid.'],
39 | self::INVALID_STATE => ['statusCode' => 302, 'name' => 'Invalid state Parameter', 'error' => 'invalid_request'],
40 | self::INVALID_CODE_CHALLENGE => ['statusCode' => 302, 'name' => 'Invalid code_challenge Parameter', 'error' => 'invalid_request'],
41 | self::INVALID_SCOPE => ['statusCode' => 302, 'name' => 'Invalid scope Parameter', 'error' => 'invalid_request'],
42 | self::INVALID_GRANT => ['statusCode' => 400, 'name' => 'The provided credentials were not valid.', 'error' => 'invalid_grant'],
43 | self::INVALID_REQUEST => ['statusCode' => 400, 'name' => 'Invalid Request', 'error' => 'invalid_request'],
44 | self::INVALID_REQUEST_REDIRECT => ['statusCode' => 302, 'name' => 'Invalid Request', 'error' => 'invalid_request'],
45 | ];
46 |
47 | /** @var ServerRequestInterface $request */
48 | protected $request;
49 |
50 | public static function create(int $code, ServerRequestInterface $request, ?Throwable $previous=null): self {
51 | // Only accept known codes. Default to 0 (generic internal error) on an unrecognised code.
52 | if (!in_array($code, array_keys(self::EXC_INFO))) {
53 | $code = 0;
54 | }
55 | $message = self::EXC_INFO[$code]['name'];
56 | $e = new self($message, $code, $previous);
57 | $e->request = $request;
58 | return $e;
59 | }
60 |
61 | public function getStatusCode() {
62 | return $this->getInfo()['statusCode'] ?? 500;
63 | }
64 |
65 | public function getExplanation() {
66 | return $this->getInfo()['explanation'] ?? 'An unknown error occured.';
67 | }
68 |
69 | public function getInfo() {
70 | return self::EXC_INFO[$this->code] ?? self::EXC_INFO[self::INTERNAL_ERROR];
71 | }
72 |
73 | /**
74 | * Trust Query Params
75 | *
76 | * Only useful on authorization form submission requests. If this returns false,
77 | * the client_id and/or request_uri have likely been tampered with, and the error
78 | * page SHOULD NOT offer the user a link to them.
79 | */
80 | public function trustQueryParams() {
81 | return !in_array($this->code, [self::AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH, self::AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH]);
82 | }
83 |
84 | public function getRequest() {
85 | return $this->request;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/templates/index.php:
--------------------------------------------------------------------------------
1 | createServerRequest('GET', '/');
12 |
13 | if (empty($_GET['t'])) {
14 | ?>
15 | Templates:
16 |
25 | 'Demo App',
33 | 'url' => 'https://client.example.com/',
34 | 'photo' => 'http://waterpigs.co.uk/taproot/logo.png',
35 | 'author' => 'https://waterpigs.co.uk'
36 | ];
37 | break;
38 | case 'photo,full-author':
39 | $clientHApp = [
40 | 'name' => 'Demo App',
41 | 'url' => 'https://client.example.com/',
42 | 'photo' => 'http://waterpigs.co.uk/taproot/logo.png',
43 | 'author' => [
44 | 'name' => 'Barnaby Walters',
45 | 'photo' => 'https://waterpigs.co.uk/photo-2021-04-22-719w.jpg',
46 | 'url' => 'https://waterpigs.co.uk'
47 | ]
48 | ];
49 | break;
50 | case 'photo':
51 | $clientHApp = [
52 | 'name' => 'Demo App',
53 | 'url' => 'https://client.example.com/',
54 | 'photo' => 'http://waterpigs.co.uk/taproot/logo.png',
55 | ];
56 | break;
57 | case 'name':
58 | $clientHApp = [
59 | 'name' => 'Demo App',
60 | 'url' => 'https://client.example.com/'
61 | ];
62 | break;
63 | default:
64 | $clientHApp = null;
65 | break;
66 | }
67 |
68 | $_profile = empty($_GET['profile']) ? null : $_GET['profile'];
69 | switch ($_profile) {
70 | case 'photo':
71 | $user = [
72 | 'me' => 'https://me.example.com/',
73 | 'profile' => [
74 | 'name' => 'Demo User',
75 | 'photo' => 'https://waterpigs.co.uk/photo-2021-04-22-719w.jpg'
76 | ]
77 | ];
78 | break;
79 | case 'name':
80 | $user = [
81 | 'me' => 'https://me.example.com/',
82 | 'profile' => [
83 | 'name' => 'Demo User'
84 | ]
85 | ];
86 | break;
87 | default:
88 | $user = ['me' => 'https://me.example.com/'];
89 | break;
90 | }
91 |
92 | $_scopes = empty($_GET['scopes']) ? null : $_GET['scopes'];
93 | switch ($_scopes) {
94 | case 'descriptions':
95 | $scopes = [
96 | 'profile' => 'Allow the app to access to your profile data',
97 | 'create' => 'Allow the app to create new content on your site',
98 | 'update' => 'Allow the app to update content on your site'
99 | ];
100 | break;
101 | case 'keys':
102 | $scopes = [
103 | 'profile' => null,
104 | 'create' => null,
105 | 'update' => null
106 | ];
107 | break;
108 | default:
109 | $scopes = [];
110 | break;
111 | }
112 |
113 | if (array_key_exists('exception', $_GET)) {
114 | $exception = new Exception('An example exception which might have occurred.');
115 | } else {
116 | $exception = null;
117 | }
118 |
119 | $clientId = 'https://client.example.com/';
120 | $formAction = '';
121 | $csrfFormElement = '';
122 | $clientRedirectUri = 'https://client.example.com/redirect';
123 |
124 | include('default_authorization_page.html.php');
125 | } elseif ($_GET['t'] == 'single_user_password_authentication_form.html') {
126 | $csrfFormElement = '';
127 | $formAction = '';
128 | include('single_user_password_authentication_form.html.php');
129 | } elseif ($_GET['t'] == 'default_exception_response.html') {
130 | $exception = \Taproot\IndieAuth\IndieAuthException::create(\Taproot\IndieAuth\IndieAuthException::INTERNAL_ERROR, $request);
131 | include('default_exception_response.html.php');
132 | } else {
133 | // Unknown template, redirect to template choosing page
134 | header('Location: /');
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/docs/coverage/Storage/Sqlite3Storage.php.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Code Coverage for /Users/barnabywalters/Documents/Programming/taproot/indieauth/src/Storage/Sqlite3Storage.php
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Code Coverage
36 |
37 |
38 |
39 | Lines
40 | Functions and Methods
41 | Classes and Traits
42 |
43 |
44 |
45 |
46 | Total
47 |
48 | n/a
49 | 0 / 0
50 |
51 | n/a
52 | 0 / 0
53 | CRAP
54 |
55 | n/a
56 | 0 / 0
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 1 <?php declare ( strict_types = 1 ) ;
66 | 2
67 | 3 namespace Taproot\IndieAuth\Storage ;
68 | 4
69 | 5
70 | 6
71 | 7
72 |
73 |
74 |
75 |
76 |
77 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/tests/SingleUserPasswordAuthenticationCallbackTest.php:
--------------------------------------------------------------------------------
1 | 'blah'
19 | ], password_hash('password', PASSWORD_DEFAULT));
20 | $this->fail();
21 | } catch (BadMethodCallException $e) {
22 | $this->assertEquals('The $user array MUST contain a “me” key, the value which must be the user’s canonical URL as a string.', $e->getMessage());
23 | }
24 | }
25 |
26 | public function testThrowsExceptionIfSecretIsTooShort() {
27 | try {
28 | $c = new SingleUserPasswordAuthenticationCallback('not long enough', [
29 | 'me' => 'blah'
30 | ], password_hash('password', PASSWORD_DEFAULT));
31 | $this->fail();
32 | } catch (BadMethodCallException $e) {
33 | $this->assertEquals('$secret must be a string with a minimum length of 64 characters.', $e->getMessage());
34 | }
35 | }
36 |
37 | public function testThrowsExceptionIfHashedPasswordIsInvalid() {
38 | try {
39 | $c = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, [
40 | 'me' => 'https://me.example.com/'
41 | ], 'definitely not a hashed password');
42 | $this->fail();
43 | } catch (BadMethodCallException $e) {
44 | $this->assertTrue(true);
45 | }
46 | }
47 |
48 | public function testShowsAuthenticationFormOnUnauthenticatedRequest() {
49 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, [
50 | 'me' => 'https://me.example.com/'
51 | ], password_hash('password', PASSWORD_DEFAULT));
52 |
53 | $formAction = 'https://example.com/formaction';
54 |
55 | $req = (new ServerRequest('GET', 'https://example.com/login'))->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token');
56 | $res = $callback($req, $formAction);
57 |
58 | $this->assertEquals(200, $res->getStatusCode());
59 | $this->assertStringContainsString($formAction, (string) $res->getBody());
60 | $this->assertEquals('no-store', $res->getHeaderLine('cache-control'));
61 | $this->assertEquals('no-cache', $res->getHeaderLine('pragma'));
62 | $this->assertStringContainsString("frame-ancestors 'none'", $res->getHeaderLine('content-security-policy'));
63 | $this->assertStringContainsString("DENY", $res->getHeaderLine('x-frame-options'));
64 | }
65 |
66 | public function testReturnsCookieRedirectOnAuthenticatedRequest() {
67 | $userData = [
68 | 'me' => 'https://me.example.com',
69 | 'profile' => ['name' => 'Me']
70 | ];
71 |
72 | $password = 'my very secure password';
73 |
74 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, $userData, password_hash($password, PASSWORD_DEFAULT));
75 |
76 | $req = (new ServerRequest('POST', 'https://example.com/login'))
77 | ->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token')
78 | ->withParsedBody([
79 | SingleUserPasswordAuthenticationCallback::PASSWORD_FORM_PARAMETER => $password
80 | ]);
81 |
82 | $res = $callback($req, 'form_action');
83 |
84 | $this->assertEquals(302, $res->getStatusCode());
85 | $this->assertEquals('form_action', $res->getHeaderLine('location'));
86 | $resCookies = FigCookies\SetCookies::fromResponse($res);
87 | $hashCookie = $resCookies->get(SingleUserPasswordAuthenticationCallback::LOGIN_HASH_COOKIE);
88 | $this->assertEquals(hash_hmac('SHA256', json_encode($userData), SERVER_SECRET), $hashCookie->getValue());
89 | }
90 |
91 | public function testReturnsUserDataOnResponseWithValidHashCookie() {
92 | $userData = [
93 | 'me' => 'https://me.example.com',
94 | 'profile' => ['name' => 'Me']
95 | ];
96 |
97 | $password = 'my very secure password';
98 |
99 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, $userData, password_hash($password, PASSWORD_DEFAULT));
100 |
101 | $req = (new ServerRequest('POST', 'https://example.com/login'))
102 | ->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token')
103 | ->withCookieParams([
104 | SingleUserPasswordAuthenticationCallback::LOGIN_HASH_COOKIE => hash_hmac('SHA256', json_encode($userData), SERVER_SECRET)
105 | ]);
106 |
107 | $res = $callback($req, 'form_action');
108 |
109 | $this->assertEquals($userData, $res);
110 | }
111 |
112 | public function testAcceptsCallableTemplate() {
113 | $expected = 'the expected response';
114 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, [
115 | 'me' => 'https://me.example.com/'
116 | ], password_hash('password', PASSWORD_DEFAULT), function (array $context) use ($expected): string {
117 | return $expected;
118 | });
119 |
120 | $formAction = 'https://example.com/formaction';
121 |
122 | $req = (new ServerRequest('GET', 'https://example.com/login'))->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token');
123 | $res = $callback($req, $formAction);
124 |
125 | $this->assertEquals($expected, (string) $res->getBody());
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/tests/FunctionTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($len, strlen(hex2bin($rand)));
21 | }
22 |
23 | public function testBuildQueryString() {
24 | $testCases = [
25 | 'key=value' => ['key' => 'value'],
26 | 'k1=v1&k2=v2' => ['k1' => 'v1', 'k2' => 'v2']
27 | ];
28 |
29 | foreach ($testCases as $expected => $params) {
30 | $this->assertEquals($expected, IA\buildQueryString($params));
31 | }
32 | }
33 |
34 | public function testAppendQueryParams() {
35 | $testCases = [
36 | 'https://example.com/?k=v' => ['https://example.com/', ['k' => 'v']],
37 | 'https://example.com/?k=v' => ['https://example.com/?', ['k' => 'v']],
38 | 'https://example.com/?k=v' => ['https://example.com/?k=v', []],
39 | 'https://example.com/?k=v&k2=v2' => ['https://example.com/?k=v', ['k2' => 'v2']]
40 | ];
41 |
42 | foreach ($testCases as $expected => list($uri, $params)) {
43 | $this->assertEquals($expected, IA\appendQueryParams($uri, $params));
44 | }
45 | }
46 |
47 | public function testHashAuthorizationRequestParametersReturnsNullWhenParameterIsMissing() {
48 | $req = (new ServerRequest('GET', 'https://example.com'))->withQueryParams([]);
49 | $hash = IA\hashAuthorizationRequestParameters($req, 'super secret');
50 | $this->assertNull($hash);
51 | }
52 |
53 | public function testHashAuthorizationRequestParametersIgnoresExtraParameters() {
54 | $params = [
55 | 'client_id' => '1',
56 | 'redirect_uri' => '1',
57 | 'code_challenge' => '1',
58 | 'code_challenge_method' => '1'
59 | ];
60 | $req1 = (new ServerRequest('GET', 'https://example.com'))->withQueryParams($params);
61 | $req2 = (new ServerRequest('GET', 'https://example.com'))->withQueryParams(array_merge($params, [
62 | 'an_additional_parameter' => 'an additional value!'
63 | ]));
64 | $this->assertEquals(IA\hashAuthorizationRequestParameters($req1, 'super secret'), IA\hashAuthorizationRequestParameters($req2, 'super secret'));
65 | }
66 |
67 | // Taken straight from https://indieauth.spec.indieweb.org/#user-profile-url-li-6
68 | public function testIsProfileUrl() {
69 | $testCases = [
70 | 'https://example.com/' => true,
71 | 'https://example.com/username' => true,
72 | 'https://example.com/users?id=100' => true,
73 | 'example.com' => false,
74 | 'mailto:user@example.com' => false,
75 | 'https://example.com/foo/../bar' => false,
76 | 'https://example.com/#me' => false,
77 | 'https://user:pass@example.com/' => false,
78 | 'https://example.com:8443/' => false,
79 | 'https://172.28.92.51/' => false
80 | ];
81 |
82 | foreach ($testCases as $url => $expected) {
83 | $this->assertEquals($expected, isProfileUrl($url), "$url was not correctly validated as $expected");
84 | }
85 | }
86 |
87 | public function testIsClientIentifier() {
88 | $testCases = [
89 | 'https://example.com/' => true,
90 | 'https://example.com/username' => true,
91 | 'https://example.com/users?id=100' => true,
92 | 'https://example.com:8443/' => true,
93 | 'https://127.0.0.1/' => true,
94 | 'https://[1::]/' => true,
95 | 'example.com' => false,
96 | 'mailto:user@example.com' => false,
97 | 'https://example.com/foo/../bar' => false,
98 | 'https://example.com/#me' => false,
99 | 'https://user:pass@example.com/' => false,
100 | 'https://172.28.92.51/' => false
101 | ];
102 |
103 | foreach ($testCases as $url => $expected) {
104 | $this->assertEquals($expected, isClientIdentifier($url), "$url was not correctly validated as $expected");
105 | }
106 | }
107 |
108 | public function testIsValidState() {
109 | $testCases = [
110 | 'hisdfbusdgiueryb@#$%^&*(' => true,
111 | "\x19" => false
112 | ];
113 |
114 | foreach ($testCases as $test => $expected) {
115 | $this->assertEquals($expected, isValidState($test), "$test was not correctly validated as $expected");
116 | }
117 | }
118 |
119 | public function testIsValidScope() {
120 | $testCases = [
121 | '!#[]~' => true,
122 | '!#[]~ scope1 another_scope moar_scopes!' => true,
123 | '"' => false, // ASCII 0x22 not permitted
124 | '\\' => false, // ASCII 0x5C not permitted
125 | ];
126 |
127 | foreach ($testCases as $test => $expected) {
128 | $this->assertEquals($expected, isValidScope($test), "$test was not correctly validated as $expected");
129 | }
130 | }
131 |
132 | // https://github.com/Taproot/indieauth/issues/13
133 | public function testIsValidCodeChallenge() {
134 | $testCases = [
135 | generatePKCECodeChallenge('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~') => true,
136 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' => true,
137 | 'has_bad_characters_in_*%#ü____' => false
138 | ];
139 |
140 | foreach ($testCases as $test => $expected) {
141 | $this->assertEquals($expected, isValidCodeChallenge($test), "$test was not correctly validated as $expected");
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/docs/graphs/classes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
37 |
38 |
39 |
40 |
41 |
44 |
78 |
79 |
80 |
81 |
82 |
83 |
91 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/src/Callback/AuthorizationFormInterface.php:
--------------------------------------------------------------------------------
1 | getQueryParams()`. The parameters most likely to be of use to the authorization
26 | * form are:
27 | *
28 | * * `scope`: a space-separated list of scopes which the client app is requesting. May be absent.
29 | * * `client_id`: the URL of the client app. Should be shown to the user. This also makes a good “cancel” link.
30 | * * `redirect_uri`: the URI which the user will be redirected to on successful authorization.
31 | *
32 | * The form MUST submit a POST request to `$formAction`, with the `taproot_indieauth_action`
33 | * parameter set to `approve`.
34 | *
35 | * The form MUST additionally include any CSRF tokens required to protect the submission.
36 | * Refer to whatever CSRF protection code you’re using (e.g. {@see \Taproot\IndieAuth\Middleware\DoubleSubmitCookieCsrfMiddleware})
37 | * and make sure to include the required element. This will usually involve getting a
38 | * CSRF token with `$request->getAttribute()` and including it in an ` `.
39 | *
40 | * The form SHOULD offer the user the opportunity to choose which of the request scopes,
41 | * if any, they wish to grant. It should describe what effect each scope grants. If no scopes are
42 | * requested, tell the user that the app is only requesting authorization, not access to their data.
43 | *
44 | * The form MAY offer the user UIs for additional token configuration, e.g. a custom token lifetime.
45 | * You may have to refer to the documentation for your instance of {@see \Taproot\IndieAuth\Storage\TokenStorageInterface} to ensure
46 | * that lifetime configuration works correctly. Any other additional data is not used by the IndieAuth
47 | * library, but, if stored on the access token, will be available to your app for use.
48 | *
49 | * {@see \Taproot\IndieAuth\Server} adds the following headers to the response returned from `showForm()`:
50 | *
51 | * ```
52 | * Cache-Control: no-store
53 | * Pragma: no-cache
54 | * X-Frame-Options: DENY
55 | * Content-Security-Policy: frame-ancestors 'none'
56 | * ```
57 | *
58 | * These headers prevent the authorization form from being cached or embedded into a malicious webpage.
59 | * It may make sense for you to add additional `Content-Security-Policy` values appropriate to your implementation,
60 | * for example to prevent the execution of inline or 3rd party scripts.
61 | *
62 | * @param ServerRequestInterface $request The current request.
63 | * @param array $authenticationResult The array returned from the Authentication Handler. Guaranteed to contain a 'me' key, may also contain additional keys e.g. 'profile'.
64 | * @param string $formAction The URL which your form MUST submit to. Can also be used as the redirect URL for a logout process.
65 | * @param array|Exception|null $clientHApp If available, the microformats-2 structure representing the client app. An instance of Exception if fetching the `client_id` failed.
66 | * @return ResponseInterface A response containing the authorization form.
67 | */
68 | public function showForm(ServerRequestInterface $request, array $authenticationResult, string $formAction, $clientHAppOrException): ResponseInterface;
69 |
70 | /**
71 | * Transform Authorization Code
72 | *
73 | * This method is called on a successful authorization form submission. The `$code` array
74 | * is a partially-constructed authorization code array, which is guaranteed to have the
75 | * following keys:
76 | *
77 | * * `client_id`: the validated `client_id` request parameter
78 | * * `redirect_uri`: the validated `redirect_uri` request parameter
79 | * * `state`: the `state` request parameter
80 | * * `code_challenge`: the `code_challenge` request parameter
81 | * * `code_challenge_method`: the `code_challenge_method` request parameter
82 | * * `requested_scope`: the value of the `scope` request parameter
83 | * * `me`: the value of the `me` key from the authentication result returned from the authentication request handler callback
84 | *
85 | * It may also have additional keys, which can come from the following locations:
86 | *
87 | * * All keys from the the authentication request handler callback result which do not clash
88 | * with the keys listed above (with the exception of `me`, which is always present). Usually
89 | * this is a `profile` key, but you may choose to return additional data from the authentication
90 | * callback, which will be present in `$data`.
91 | *
92 | * This method should add any additional data to the auth code, before it is persisted and
93 | * returned to the client app. Typically, this involves setting the `scope` key to be a
94 | * valid space-separated scope string of any scopes granted by the user in the form.
95 | *
96 | * If the form offers additional token configuration, this method should set any relevant
97 | * keys in `$code` based on the form data in `$request`.
98 | *
99 | * @param array $code The base authorization code data, to be added to.
100 | * @param ServerRequestInterface $request The current request.
101 | * @return array The $code data after making any necessary changes.
102 | */
103 | public function transformAuthorizationCode(ServerRequestInterface $request, array $code): array;
104 | }
105 |
--------------------------------------------------------------------------------
/src/Middleware/DoubleSubmitCookieCsrfMiddleware.php:
--------------------------------------------------------------------------------
1 | attribute = $attribute ?? self::ATTRIBUTE;
74 | $this->ttl = $ttl ?? self::TTL;
75 | $this->tokenLength = $tokenLength ?? self::CSRF_TOKEN_LENGTH;
76 |
77 | if (!is_callable($errorResponse)) {
78 | if (!$errorResponse instanceof ResponseInterface) {
79 | if (!is_string($errorResponse)) {
80 | $errorResponse = self::DEFAULT_ERROR_RESPONSE_STRING;
81 | }
82 | $errorResponse = new Response(400, ['content-type' => 'text/plain'], $errorResponse);
83 | }
84 | $errorResponse = function (ServerRequestInterface $request) use ($errorResponse) { return $errorResponse; };
85 | }
86 | $this->errorResponse = $errorResponse;
87 |
88 | if (!$logger instanceof LoggerInterface) {
89 | $logger = new NullLogger();
90 | }
91 | $this->logger = $logger;
92 | }
93 |
94 | public function setLogger(LoggerInterface $logger): void {
95 | $this->logger = $logger;
96 | }
97 |
98 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
99 | // Generate a new CSRF token, add it to the request attributes, and as a cookie on the response.
100 | $csrfToken = generateRandomPrintableAsciiString($this->tokenLength);
101 | $request = $request->withAttribute($this->attribute, $csrfToken);
102 |
103 | // Add a pre-rendered CSRF form element to the request for convenience.
104 | $csrfFormElement = ' ';
105 | $request = $request->withAttribute("{$this->attribute}FormElement", $csrfFormElement);
106 |
107 | if (!in_array(strtoupper($request->getMethod()), self::READ_METHODS) && !$this->isValid($request)) {
108 | // This request is a write method with invalid CSRF parameters.
109 | $response = call_user_func($this->errorResponse, $request);
110 | } else {
111 | $response = $handler->handle($request);
112 | }
113 |
114 | // Add the new CSRF cookie, restricting its scope to match the current request.
115 | $response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create($this->attribute)
116 | ->withValue($csrfToken)
117 | ->withMaxAge($this->ttl)
118 | ->withSecure($request->getUri()->getScheme() == 'https')
119 | ->withDomain($request->getUri()->getHost())
120 | ->withHttpOnly(true)
121 | ->withPath($this->cookiePath ?? $request->getUri()->getPath()));
122 |
123 | return $response;
124 | }
125 |
126 | protected function isValid(ServerRequestInterface $request) {
127 | if (array_key_exists($this->attribute, $request->getParsedBody() ?? [])) {
128 | if (array_key_exists($this->attribute, $request->getCookieParams() ?? [])) {
129 | // TODO: make sure CSRF token isn’t the empty string, possibly also check that it’s the same length
130 | // as defined in $this->tokenLength.
131 | return hash_equals($request->getParsedBody()[$this->attribute], $request->getCookieParams()[$this->attribute]);
132 | }
133 | }
134 | return false;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Callback/DefaultAuthorizationForm.php:
--------------------------------------------------------------------------------
1 | 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' => ' '
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 | }
139 |
--------------------------------------------------------------------------------
/docs/reports/errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation » Compilation errors
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
49 |
50 |
51 |
52 |
53 |
56 |
90 |
91 |
92 |
95 |
96 |
97 |
Errors
98 |
99 |
100 |
No errors have been found in this project.
101 |
102 |
103 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/src/Callback/SingleUserPasswordAuthenticationCallback.php:
--------------------------------------------------------------------------------
1 | new IndieAuth\Callback\SingleUserPasswordAuthenticationCallback(
37 | * YOUR_SECRET,
38 | * ['me' => 'https://me.example.com/'],
39 | * YOUR_HASHED_PASSWORD
40 | * )
41 | * …
42 | * ]);
43 | * ```
44 | *
45 | * See documentation for `__construct()` for information about customising behaviour.
46 | */
47 | class SingleUserPasswordAuthenticationCallback {
48 | const PASSWORD_FORM_PARAMETER = 'taproot_indieauth_server_password';
49 | const LOGIN_HASH_COOKIE = 'taproot_indieauth_server_supauth_hash';
50 | const DEFAULT_COOKIE_TTL = 60 * 5;
51 |
52 | /** @var string $csrfKey */
53 | public $csrfKey;
54 |
55 | /** @var callable $formTemplateCallable */
56 | protected $formTemplateCallable;
57 |
58 | /** @var array $user */
59 | protected $user;
60 |
61 | /** @var string $hashedPassword */
62 | protected $hashedPassword;
63 |
64 | /** @var string $secret */
65 | protected $secret;
66 |
67 | /** @var int $ttl */
68 | protected $ttl;
69 |
70 | /**
71 | * Constructor
72 | *
73 | * @param string $secret A secret key used to encrypt cookies. Can be the same as the secret passed to IndieAuth\Server.
74 | * @param array $user An array representing the user, which will be returned on a successful authentication. MUST include a 'me' key, may also contain a 'profile' key, or other keys at your discretion.
75 | * @param string $hashedPassword The password used to authenticate as $user, hashed by `password_hash($pass, PASSWORD_DEFAULT)`
76 | * @param string|callable|null $formTemplate The path to a template used to render the sign-in form, or a template callable with the signature `function (array $context): string`. Uses default if null.
77 | * @param string|null $csrfKey The key under which to fetch a CSRF token from `$request` attributes, and as the CSRF token name in submitted form data. Defaults to the Server default, only change if you’re using a custom CSRF middleware.
78 | * @param int|null $ttl The lifetime of the authentication cookie, in seconds. Defaults to five minutes.
79 | */
80 | public function __construct(string $secret, array $user, string $hashedPassword, $formTemplate=null, ?string $csrfKey=null, ?int $ttl=null) {
81 | if (strlen($secret) < 64) {
82 | throw new BadMethodCallException("\$secret must be a string with a minimum length of 64 characters.");
83 | }
84 | $this->secret = $secret;
85 |
86 | $this->ttl = $ttl ?? self::DEFAULT_COOKIE_TTL;
87 |
88 | if (!isset($user['me'])) {
89 | throw new BadMethodCallException('The $user array MUST contain a “me” key, the value which must be the user’s canonical URL as a string.');
90 | }
91 |
92 | $hashAlgo = password_get_info($hashedPassword)['algo'];
93 | // Invalid algorithms are null in PHP 7.4, 0 in PHP 7.3.
94 | if (is_null($hashAlgo) or 0 === $hashAlgo) {
95 | throw new BadMethodCallException('The provided $hashedPassword was not a valid hash created by the password_hash() function.');
96 | }
97 | $this->user = $user;
98 | $this->hashedPassword = $hashedPassword;
99 |
100 | $formTemplate = $formTemplate ?? __DIR__ . '/../../templates/single_user_password_authentication_form.html.php';
101 | if (is_string($formTemplate)) {
102 | $formTemplate = function (array $context) use ($formTemplate): string {
103 | return renderTemplate($formTemplate, $context);
104 | };
105 | }
106 |
107 | if (!is_callable($formTemplate)) {
108 | throw new BadMethodCallException("\$formTemplate must be a string (path), a callable, or null.");
109 | }
110 |
111 | $this->formTemplateCallable = $formTemplate;
112 | $this->csrfKey = $csrfKey ?? \Taproot\IndieAuth\Server::DEFAULT_CSRF_KEY;
113 | }
114 |
115 | public function __invoke(ServerRequestInterface $request, string $formAction, ?string $normalizedMeUrl=null) {
116 | // If the request is logged in, return authentication data.
117 | $cookies = $request->getCookieParams();
118 | if (
119 | isset($cookies[self::LOGIN_HASH_COOKIE])
120 | && hash_equals(hash_hmac('SHA256', json_encode($this->user), $this->secret), $cookies[self::LOGIN_HASH_COOKIE])
121 | ) {
122 | return $this->user;
123 | }
124 |
125 | // If the request is a form submission with a matching password, return a redirect to the indieauth
126 | // flow, setting a cookie.
127 | if ($request->getMethod() == 'POST' && password_verify($request->getParsedBody()[self::PASSWORD_FORM_PARAMETER] ?? '', $this->hashedPassword)) {
128 | $response = new Response(302, ['Location' => $formAction]);
129 |
130 | // Set the user data hash cookie.
131 | $response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create(self::LOGIN_HASH_COOKIE)
132 | ->withValue(hash_hmac('SHA256', json_encode($this->user), $this->secret))
133 | ->withMaxAge($this->ttl)
134 | ->withSecure($request->getUri()->getScheme() == 'https')
135 | ->withDomain($request->getUri()->getHost())
136 | );
137 |
138 | return $response;
139 | }
140 |
141 | // Otherwise, return a response containing the password form.
142 | return (new Response(200, ['content-type' => 'text/html'], call_user_func($this->formTemplateCallable, [
143 | 'formAction' => $formAction,
144 | 'request' => $request,
145 | 'csrfFormElement' => ' '
146 | ])))->withAddedHeader('Cache-control', 'no-store')
147 | ->withAddedHeader('Pragma', 'no-cache')
148 | ->withAddedHeader('X-Frame-Options', 'DENY')
149 | ->withAddedHeader('Content-Security-Policy', "frame-ancestors 'none'");
150 | }
151 | }
--------------------------------------------------------------------------------
/docs/files/src-storage-sqlite3storage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | Sqlite3Storage.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/docs/reports/deprecated.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation » Deprecated elements
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
49 |
50 |
51 |
52 |
53 |
56 |
90 |
91 |
92 |
95 |
96 |
97 |
Deprecated
98 |
99 |
100 |
101 | No deprecated elements have been found in this project.
102 |
103 |
104 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/docs/packages/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | API Documentation
96 |
97 |
98 | Packages
99 |
100 |
101 |
102 |
103 | Application
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/docs/namespaces/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | API Documentation
96 |
97 |
98 |
99 | Namespaces
100 |
101 |
102 |
103 |
104 | Taproot
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/docs/namespaces/taproot.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | Taproot
96 |
97 |
98 |
99 | Namespaces
100 |
101 |
102 |
103 |
104 | IndieAuth
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/docs/coverage/Storage/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Code Coverage for /Users/barnabywalters/Documents/Programming/taproot/indieauth/src/Storage
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Code Coverage
36 |
37 |
38 |
39 | Lines
40 | Functions and Methods
41 | Classes and Traits
42 |
43 |
44 |
45 |
46 | Total
47 |
48 |
49 | 83.48% covered (warning)
50 |
51 |
52 |
53 | 83.48%
54 | 96 / 115
55 |
56 |
57 | 46.15% covered (danger)
58 |
59 |
60 |
61 | 46.15%
62 | 6 / 13
63 |
64 |
65 | 0.00% covered (danger)
66 |
67 |
68 |
69 | 0.00%
70 | 0 / 1
71 |
72 |
73 |
74 | FilesystemJsonStorage.php
75 |
76 |
77 | 83.48% covered (warning)
78 |
79 |
80 |
81 | 83.48%
82 | 96 / 115
83 |
84 |
85 | 46.15% covered (danger)
86 |
87 |
88 |
89 | 46.15%
90 | 6 / 13
91 |
92 |
93 | 0.00% covered (danger)
94 |
95 |
96 |
97 | 0.00%
98 | 0 / 1
99 |
100 |
101 |
102 | Sqlite3Storage.php
103 |
104 | n/a
105 | 0 / 0
106 |
107 | n/a
108 | 0 / 0
109 |
110 | n/a
111 | 0 / 0
112 |
113 |
114 |
115 | TokenStorageInterface.php
116 |
117 | n/a
118 | 0 / 0
119 |
120 | n/a
121 | 0 / 0
122 |
123 | n/a
124 | 0 / 0
125 |
126 |
127 |
128 |
129 |
130 |
131 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
Documentation
92 |
93 |
94 |
95 | Packages
96 |
97 |
98 |
99 |
100 | Application
101 |
102 |
103 |
104 | Namespaces
105 |
106 |
107 |
108 |
109 | Taproot
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/docs/css/template.css:
--------------------------------------------------------------------------------
1 | .phpdocumentor-summary {
2 | font-style: italic;
3 | }
4 | .phpdocumentor-description {
5 | margin-bottom: var(--spacing-md);
6 | }
7 | .phpdocumentor-element {
8 | position: relative;
9 | }
10 |
11 | .phpdocumentor .phpdocumentor-element__name {
12 | line-height: 1;
13 | }
14 |
15 | .phpdocumentor-element__package,
16 | .phpdocumentor-element__extends,
17 | .phpdocumentor-element__implements {
18 | display: block;
19 | font-size: var(--text-xxs);
20 | font-weight: normal;
21 | opacity: .7;
22 | }
23 |
24 | .phpdocumentor-element__package .phpdocumentor-breadcrumbs {
25 | display: inline;
26 | }
27 |
28 | .phpdocumentor-element:not(:last-child) {
29 | border-bottom: 1px solid var(--primary-color-lighten);
30 | padding-bottom: var(--spacing-lg);
31 | }
32 |
33 | .phpdocumentor-element.-deprecated .phpdocumentor-element__name {
34 | text-decoration: line-through;
35 | }
36 |
37 | .phpdocumentor-element__modifier {
38 | font-size: var(--text-xxs);
39 | padding: calc(var(--spacing-base-size) / 4) calc(var(--spacing-base-size) / 2);
40 | color: var(--text-color);
41 | background-color: var(--light-gray);
42 | border-radius: 3px;
43 | text-transform: uppercase;
44 | }
45 | .phpdocumentor-signature {
46 | display: inline-block;
47 | font-size: var(--text-sm);
48 | margin-bottom: var(--spacing-md);
49 | }
50 |
51 | .phpdocumentor-signature.-deprecated .phpdocumentor-signature__name {
52 | text-decoration: line-through;
53 | }
54 | .phpdocumentor-table-of-contents {
55 | }
56 |
57 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry {
58 | padding-top: var(--spacing-xs);
59 | margin-left: 2rem;
60 | display: flex;
61 | }
62 |
63 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry > a {
64 | flex: 0 1 auto;
65 | }
66 |
67 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry > span {
68 | flex: 1;
69 | white-space: nowrap;
70 | text-overflow: ellipsis;
71 | overflow: hidden;
72 | }
73 |
74 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry:after {
75 | content: '';
76 | height: 12px;
77 | width: 12px;
78 | left: 16px;
79 | position: absolute;
80 | }
81 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-private:after {
82 | background: url('data:image/svg+xml;utf8, ') no-repeat;
83 | }
84 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-protected:after {
85 | left: 13px;
86 | background: url('data:image/svg+xml;utf8, ') no-repeat;
87 | }
88 |
89 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry:before {
90 | width: 1.25rem;
91 | height: 1.25rem;
92 | line-height: 1.25rem;
93 | background: transparent url('data:image/svg+xml;utf8, ') no-repeat center center;
94 | content: '';
95 | position: absolute;
96 | left: 0;
97 | border-radius: 50%;
98 | font-weight: 600;
99 | color: white;
100 | text-align: center;
101 | font-size: .75rem;
102 | margin-top: .2rem;
103 | }
104 |
105 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-method:before {
106 | content: 'M';
107 | background-image: url('data:image/svg+xml;utf8, ');
108 | }
109 |
110 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-function:before {
111 | content: 'M';
112 | background-image: url('data:image/svg+xml;utf8, ');
113 | }
114 |
115 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-property:before {
116 | content: 'P'
117 | }
118 |
119 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-constant:before {
120 | content: 'C';
121 | background-color: transparent;
122 | background-image: url('data:image/svg+xml;utf8, ');
123 | }
124 |
125 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-class:before {
126 | content: 'C'
127 | }
128 |
129 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-interface:before {
130 | content: 'I'
131 | }
132 |
133 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-trait:before {
134 | content: 'T'
135 | }
136 |
137 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-namespace:before {
138 | content: 'N'
139 | }
140 |
141 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-package:before {
142 | content: 'P'
143 | }
144 |
145 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-enum:before {
146 | content: 'E'
147 | }
148 |
149 | .phpdocumentor-table-of-contents dd {
150 | font-style: italic;
151 | margin-left: 2rem;
152 | }
153 | .phpdocumentor-element-found-in {
154 | position: absolute;
155 | top: 0;
156 | right: 0;
157 | font-size: var(--text-sm);
158 | color: gray;
159 | }
160 |
161 | .phpdocumentor-element-found-in .phpdocumentor-element-found-in__source {
162 | flex: 0 1 auto;
163 | display: inline-flex;
164 | }
165 |
166 | .phpdocumentor-element-found-in .phpdocumentor-element-found-in__source:after {
167 | width: 1.25rem;
168 | height: 1.25rem;
169 | line-height: 1.25rem;
170 | background: transparent url('data:image/svg+xml;utf8, ') no-repeat center center;
171 | content: '';
172 | left: 0;
173 | border-radius: 50%;
174 | font-weight: 600;
175 | text-align: center;
176 | font-size: .75rem;
177 | margin-top: .2rem;
178 | }
179 | .phpdocumentor-class-graph {
180 | width: 100%; height: 600px; border:1px solid black; overflow: hidden
181 | }
182 |
183 | .phpdocumentor-class-graph__graph {
184 | width: 100%;
185 | }
186 | .phpdocumentor-tag-list__definition {
187 | display: flex;
188 | }
189 |
190 | .phpdocumentor-tag-link {
191 | margin-right: var(--spacing-sm);
192 | }
193 |
--------------------------------------------------------------------------------
/docs/files/src-server.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | Server.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | Server
113 | IndieAuth Server
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-indieauthexception.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | IndieAuthException.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | IndieAuthException
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-middleware-noopmiddleware.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | NoOpMiddleware.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | NoOpMiddleware
113 | No-Op Middleware
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-middleware-closurerequesthandler.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | ClosureRequestHandler.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | ClosureRequestHandler
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-middleware-responserequesthandler.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | ResponseRequestHandler.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | ResponseRequestHandler
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-storage-tokenstorageinterface.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | TokenStorageInterface.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 | TokenStorageInterface
112 | Token Storage Interface
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-storage-filesystemjsonstorage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | FilesystemJsonStorage.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | FilesystemJsonStorage
113 | Filesystem JSON Token Storage
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-callback-defaultauthorizationform.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | DefaultAuthorizationForm.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | DefaultAuthorizationForm
113 | Default Authorization Form
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/files/src-callback-authorizationforminterface.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | AuthorizationFormInterface.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 | AuthorizationFormInterface
112 | Authorization Form Interface
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/docs/js/search.js:
--------------------------------------------------------------------------------
1 | // Search module for phpDocumentor
2 | //
3 | // This module is a wrapper around fuse.js that will use a given index and attach itself to a
4 | // search form and to a search results pane identified by the following data attributes:
5 | //
6 | // 1. data-search-form
7 | // 2. data-search-results
8 | //
9 | // The data-search-form is expected to have a single input element of type 'search' that will trigger searching for
10 | // a series of results, were the data-search-results pane is expected to have a direct UL child that will be populated
11 | // with rendered results.
12 | //
13 | // The search has various stages, upon loading this stage the data-search-form receives the CSS class
14 | // 'phpdocumentor-search--enabled'; this indicates that JS is allowed and indices are being loaded. It is recommended
15 | // to hide the form by default and show it when it receives this class to achieve progressive enhancement for this
16 | // feature.
17 | //
18 | // After loading this module, it is expected to load a search index asynchronously, for example:
19 | //
20 | //
21 | //
22 | // In this script the generated index should attach itself to the search module using the `appendIndex` function. By
23 | // doing it like this the page will continue loading, unhindered by the loading of the search.
24 | //
25 | // After the page has fully loaded, and all these deferred indexes loaded, the initialization of the search module will
26 | // be called and the form will receive the class 'phpdocumentor-search--active', indicating search is ready. At this
27 | // point, the input field will also have it's 'disabled' attribute removed.
28 | var Search = (function () {
29 | var fuse;
30 | var index = [];
31 | var options = {
32 | shouldSort: true,
33 | threshold: 0.6,
34 | location: 0,
35 | distance: 100,
36 | maxPatternLength: 32,
37 | minMatchCharLength: 1,
38 | keys: [
39 | "fqsen",
40 | "name",
41 | "summary",
42 | "url"
43 | ]
44 | };
45 |
46 | // Credit David Walsh (https://davidwalsh.name/javascript-debounce-function)
47 | // Returns a function, that, as long as it continues to be invoked, will not
48 | // be triggered. The function will be called after it stops being called for
49 | // N milliseconds. If `immediate` is passed, trigger the function on the
50 | // leading edge, instead of the trailing.
51 | function debounce(func, wait, immediate) {
52 | var timeout;
53 |
54 | return function executedFunction() {
55 | var context = this;
56 | var args = arguments;
57 |
58 | var later = function () {
59 | timeout = null;
60 | if (!immediate) func.apply(context, args);
61 | };
62 |
63 | var callNow = immediate && !timeout;
64 | clearTimeout(timeout);
65 | timeout = setTimeout(later, wait);
66 | if (callNow) func.apply(context, args);
67 | };
68 | }
69 |
70 | function close() {
71 | // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/
72 | const scrollY = document.body.style.top;
73 | document.body.style.position = '';
74 | document.body.style.top = '';
75 | window.scrollTo(0, parseInt(scrollY || '0') * -1);
76 | // End scroll prevention
77 |
78 | var form = document.querySelector('[data-search-form]');
79 | var searchResults = document.querySelector('[data-search-results]');
80 |
81 | form.classList.toggle('phpdocumentor-search--has-results', false);
82 | searchResults.classList.add('phpdocumentor-search-results--hidden');
83 | var searchField = document.querySelector('[data-search-form] input[type="search"]');
84 | searchField.blur();
85 | }
86 |
87 | function search(event) {
88 | // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/
89 | document.body.style.position = 'fixed';
90 | document.body.style.top = `-${window.scrollY}px`;
91 | // End scroll prevention
92 |
93 | // prevent enter's from autosubmitting
94 | event.stopPropagation();
95 |
96 | var form = document.querySelector('[data-search-form]');
97 | var searchResults = document.querySelector('[data-search-results]');
98 | var searchResultEntries = document.querySelector('[data-search-results] .phpdocumentor-search-results__entries');
99 |
100 | searchResultEntries.innerHTML = '';
101 |
102 | if (!event.target.value) {
103 | close();
104 | return;
105 | }
106 |
107 | form.classList.toggle('phpdocumentor-search--has-results', true);
108 | searchResults.classList.remove('phpdocumentor-search-results--hidden');
109 | var results = fuse.search(event.target.value, {limit: 25});
110 |
111 | results.forEach(function (result) {
112 | var entry = document.createElement("li");
113 | entry.classList.add("phpdocumentor-search-results__entry");
114 | entry.innerHTML += '\n";
115 | entry.innerHTML += '' + result.fqsen + " \n";
116 | entry.innerHTML += '' + result.summary + '
';
117 | searchResultEntries.appendChild(entry)
118 | });
119 | }
120 |
121 | function appendIndex(added) {
122 | index = index.concat(added);
123 |
124 | // re-initialize search engine when appending an index after initialisation
125 | if (typeof fuse !== 'undefined') {
126 | fuse = new Fuse(index, options);
127 | }
128 | }
129 |
130 | function init() {
131 | fuse = new Fuse(index, options);
132 |
133 | var form = document.querySelector('[data-search-form]');
134 | var searchField = document.querySelector('[data-search-form] input[type="search"]');
135 |
136 | var closeButton = document.querySelector('.phpdocumentor-search-results__close');
137 | closeButton.addEventListener('click', function() { close() }.bind(this));
138 |
139 | var searchResults = document.querySelector('[data-search-results]');
140 | searchResults.addEventListener('click', function() { close() }.bind(this));
141 |
142 | form.classList.add('phpdocumentor-search--active');
143 |
144 | searchField.setAttribute('placeholder', 'Search (Press "/" to focus)');
145 | searchField.removeAttribute('disabled');
146 | searchField.addEventListener('keyup', debounce(search, 300));
147 |
148 | window.addEventListener('keyup', function (event) {
149 | if (event.key === '/') {
150 | searchField.focus();
151 | }
152 | if (event.code === 'Escape') {
153 | close();
154 | }
155 | }.bind(this));
156 | }
157 |
158 | return {
159 | appendIndex,
160 | init
161 | }
162 | })();
163 |
164 | window.addEventListener('DOMContentLoaded', function () {
165 | var form = document.querySelector('[data-search-form]');
166 |
167 | // When JS is supported; show search box. Must be before including the search for it to take effect immediately
168 | form.classList.add('phpdocumentor-search--enabled');
169 | });
170 |
171 | window.addEventListener('load', function () {
172 | Search.init();
173 | });
174 |
--------------------------------------------------------------------------------
/docs/files/src-middleware-doublesubmitcookiecsrfmiddleware.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Documentation
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
48 |
49 |
50 |
51 |
52 |
55 |
89 |
90 |
91 |
93 |
94 |
95 | DoubleSubmitCookieCsrfMiddleware.php
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Interfaces, Classes, Traits and Enums
107 |
108 |
109 |
110 |
111 |
112 | DoubleSubmitCookieCsrfMiddleware
113 | Double-Submit Cookie CSRF Middleware
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------