| 1 | <?php | 
    | 2 |  | 
    | 3 | namespace Taproot\IndieAuth; | 
    | 4 |  | 
    | 5 | use Exception; | 
    | 6 | use Psr\Http\Message\ServerRequestInterface; | 
    | 7 | use Psr\Log\LoggerAwareInterface; | 
    | 8 | use Psr\Log\LoggerInterface; | 
    | 9 |  | 
    | 10 |  | 
    | 11 | function generateRandomString($numBytes) { | 
    | 12 | if (function_exists('random_bytes')) { | 
    | 13 | $bytes = random_bytes($numBytes); | 
    | 14 |  | 
    | 15 |  | 
    | 16 | } elseif (function_exists('openssl_random_pseudo_bytes')){ | 
    | 17 | $bytes = openssl_random_pseudo_bytes($numBytes); | 
    | 18 | } else { | 
    | 19 | $bytes = ''; | 
    | 20 | for($i=0, $bytes=''; $i < $numBytes; $i++) { | 
    | 21 | $bytes .= chr(mt_rand(0, 255)); | 
    | 22 | } | 
    | 23 |  | 
    | 24 | } | 
    | 25 | return bin2hex($bytes); | 
    | 26 | } | 
    | 27 |  | 
    | 28 | function generateRandomPrintableAsciiString(int $length) { | 
    | 29 | $chars = []; | 
    | 30 | while (count($chars) < $length) { | 
    | 31 |  | 
    | 32 | $chars[] = chr(random_int(0x21, 0x7E)); | 
    | 33 | } | 
    | 34 | return join('', $chars); | 
    | 35 | } | 
    | 36 |  | 
    | 37 | function generatePKCECodeChallenge($plaintext) { | 
    | 38 | return base64_urlencode(hash('sha256', $plaintext, true)); | 
    | 39 | } | 
    | 40 |  | 
    | 41 | function base64_urlencode($string) { | 
    | 42 | return rtrim(strtr(base64_encode($string), '+/', '-_'), '='); | 
    | 43 | } | 
    | 44 |  | 
    | 45 | function hashAuthorizationRequestParameters(ServerRequestInterface $request, string $secret, ?string $algo=null, ?array $hashedParameters=null): ?string { | 
    | 46 | $hashedParameters = $hashedParameters ?? ['client_id', 'redirect_uri', 'code_challenge', 'code_challenge_method']; | 
    | 47 | $algo = $algo ?? 'sha256'; | 
    | 48 |  | 
    | 49 | $queryParams = $request->getQueryParams() ?? []; | 
    | 50 | $data = ''; | 
    | 51 | foreach ($hashedParameters as $key) { | 
    | 52 | if (!array_key_exists($key, $queryParams)) { | 
    | 53 | return null; | 
    | 54 | } | 
    | 55 | $data .= $queryParams[$key]; | 
    | 56 | } | 
    | 57 | return hash_hmac($algo, $data, $secret); | 
    | 58 | } | 
    | 59 |  | 
    | 60 | function isIndieAuthAuthorizationCodeRedeemingRequest(ServerRequestInterface $request) { | 
    | 61 | return strtolower($request->getMethod()) == 'post' | 
    | 62 | && array_key_exists('grant_type', $request->getParsedBody() ?? []) | 
    | 63 | && $request->getParsedBody()['grant_type'] == 'authorization_code'; | 
    | 64 | } | 
    | 65 |  | 
    | 66 | function isIndieAuthAuthorizationRequest(ServerRequestInterface $request, $permittedMethods=['get']) { | 
    | 67 | return in_array(strtolower($request->getMethod()), array_map('strtolower', $permittedMethods)) | 
    | 68 | && array_key_exists('response_type', $request->getQueryParams() ?? []) | 
    | 69 | && $request->getQueryParams()['response_type'] == 'code'; | 
    | 70 | } | 
    | 71 |  | 
    | 72 | function isAuthorizationApprovalRequest(ServerRequestInterface $request) { | 
    | 73 | return strtolower($request->getMethod()) == 'post' | 
    | 74 | && array_key_exists('taproot_indieauth_action', $request->getParsedBody() ?? []) | 
    | 75 | && $request->getParsedBody()[Server::APPROVE_ACTION_KEY] == Server::APPROVE_ACTION_VALUE; | 
    | 76 | } | 
    | 77 |  | 
    | 78 | function buildQueryString(array $parameters) { | 
    | 79 | $qs = []; | 
    | 80 | foreach ($parameters as $k => $v) { | 
    | 81 | $qs[] = urlencode($k) . '=' . urlencode($v); | 
    | 82 | } | 
    | 83 | return join('&', $qs); | 
    | 84 | } | 
    | 85 |  | 
    | 86 | function urlComponentsMatch($url1, $url2, ?array $components=null): bool { | 
    | 87 | $validComponents = [PHP_URL_HOST, PHP_URL_PASS, PHP_URL_PATH, PHP_URL_PORT, PHP_URL_USER, PHP_URL_QUERY, PHP_URL_SCHEME, PHP_URL_FRAGMENT]; | 
    | 88 | $components = $components ?? $validComponents; | 
    | 89 |  | 
    | 90 | foreach ($components as $cmp) { | 
    | 91 | if (!in_array($cmp, $validComponents)) { | 
    | 92 | throw new Exception("Invalid parse_url() component passed: $cmp"); | 
    | 93 | } | 
    | 94 |  | 
    | 95 | if (parse_url($url1, $cmp) !== parse_url($url2, $cmp)) { | 
    | 96 | return false; | 
    | 97 | } | 
    | 98 | } | 
    | 99 |  | 
    | 100 | return true; | 
    | 101 | } | 
    | 102 |  | 
    | 103 |  | 
    | 104 |  | 
    | 105 |  | 
    | 106 |  | 
    | 107 |  | 
    | 108 |  | 
    | 109 |  | 
    | 110 | function appendQueryParams(string $uri, array $queryParams) { | 
    | 111 | if (empty($queryParams)) { | 
    | 112 | return $uri; | 
    | 113 | } | 
    | 114 |  | 
    | 115 | $queryString = buildQueryString($queryParams); | 
    | 116 | $separator = parse_url($uri, \PHP_URL_QUERY) ? '&' : '?'; | 
    | 117 | $uri = rtrim($uri, '?&'); | 
    | 118 | return "{$uri}{$separator}{$queryString}"; | 
    | 119 | } | 
    | 120 |  | 
    | 121 |  | 
    | 122 |  | 
    | 123 |  | 
    | 124 |  | 
    | 125 |  | 
    | 126 |  | 
    | 127 | function trySetLogger($target, LoggerInterface $logger) { | 
    | 128 | if ($target instanceof LoggerAwareInterface) { | 
    | 129 | $target->setLogger($logger); | 
    | 130 | } | 
    | 131 | return $target; | 
    | 132 | } | 
    | 133 |  | 
    | 134 | function renderTemplate(string $template, array $context=[]) { | 
    | 135 | $render = function ($__template, $__templateData) { | 
    | 136 | $render = function ($template, $data){ | 
    | 137 | return renderTemplate($template, $data); | 
    | 138 | }; | 
    | 139 | ob_start(); | 
    | 140 | extract($__templateData); | 
    | 141 | unset($__templateData); | 
    | 142 | include $__template; | 
    | 143 | return ob_get_clean(); | 
    | 144 | }; | 
    | 145 | return $render($template, $context); | 
    | 146 | } | 
    | 147 |  | 
    | 148 |  | 
    | 149 |  | 
    | 150 |  | 
    | 151 |  | 
    | 152 |  | 
    | 153 |  | 
    | 154 |  | 
    | 155 |  | 
    | 156 |  | 
    | 157 |  | 
    | 158 |  | 
    | 159 | function isClientIdentifier(string $client_id): bool { | 
    | 160 | return ($url_components = parse_url($client_id)) && | 
    | 161 | in_array($url_components['scheme'] ?? '', ['http', 'https']) && | 
    | 162 | 0 < strlen($url_components['path'] ?? '') && | 
    | 163 | false === strpos($url_components['path'], '/./') && | 
    | 164 | false === strpos($url_components['path'], '/../') && | 
    | 165 | false === isset($url_components['fragment']) && | 
    | 166 | false === isset($url_components['user']) && | 
    | 167 | false === isset($url_components['pass']) && | 
    | 168 | ( | 
    | 169 | false === filter_var($url_components['host'], FILTER_VALIDATE_IP) || | 
    | 170 | ($url_components['host'] ?? null) == '127.0.0.1' || | 
    | 171 | ($url_components['host'] ?? null) == '[::1]' | 
    | 172 | ) | 
    | 173 | ; | 
    | 174 | } | 
    | 175 |  | 
    | 176 |  | 
    | 177 |  | 
    | 178 |  | 
    | 179 |  | 
    | 180 |  | 
    | 181 |  | 
    | 182 |  | 
    | 183 | function isProfileUrl(string $profile_url): bool { | 
    | 184 | return ($url_components = parse_url($profile_url)) && | 
    | 185 | in_array($url_components['scheme'] ?? '', ['http', 'https']) && | 
    | 186 | 0 < strlen($url_components['path'] ?? '') && | 
    | 187 | false === strpos($url_components['path'], '/./') && | 
    | 188 | false === strpos($url_components['path'], '/../') && | 
    | 189 | false === isset($url_components['fragment']) && | 
    | 190 | false === isset($url_components['user']) && | 
    | 191 | false === isset($url_components['pass']) && | 
    | 192 | false === isset($url_components['port']) && | 
    | 193 | false === filter_var($url_components['host'], FILTER_VALIDATE_IP) | 
    | 194 | ; | 
    | 195 | } | 
    | 196 |  | 
    | 197 |  | 
    | 198 |  | 
    | 199 |  | 
    | 200 |  | 
    | 201 |  | 
    | 202 | function isValidState(string $state): bool { | 
    | 203 | return false !== filter_var($state, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[\x20-\x7E]*$/']]); | 
    | 204 | } | 
    | 205 |  | 
    | 206 |  | 
    | 207 |  | 
    | 208 |  | 
    | 209 |  | 
    | 210 |  | 
    | 211 | function isValidCodeChallenge(string $challenge): bool { | 
    | 212 | return false !== filter_var($challenge, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[A-Za-z0-9_-]+$/']]); | 
    | 213 | } | 
    | 214 |  | 
    | 215 |  | 
    | 216 |  | 
    | 217 |  | 
    | 218 |  | 
    | 219 | function isValidScope(string $scope): bool { | 
    | 220 | return false !== filter_var($scope, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^([\x21\x23-\x5B\x5D-\x7E]+( [\x21\x23-\x5B\x5D-\x7E]+)*)?$/']]); | 
    | 221 | } |