Compare commits

...

17 Commits
v3 ... oauth1

Author SHA1 Message Date
Diogo Peralta Cordeiro b999c1bd62
yet another iteration 2022-01-22 18:49:25 +00:00
Diogo Peralta Cordeiro 9dc6243822
fix firewalls a little 2022-01-22 18:49:25 +00:00
Diogo Peralta Cordeiro ce8f54dc46
support json on oauth 2022-01-22 18:49:24 +00:00
Diogo Peralta Cordeiro 9e7db08e50
more grants 2022-01-22 18:49:24 +00:00
Diogo Peralta Cordeiro 841d10cde0
buah 2022-01-22 18:49:24 +00:00
Diogo Peralta Cordeiro 95c8f3bdc7
damn 2022-01-22 18:49:24 +00:00
Diogo Peralta Cordeiro b82818646f
wip42 2022-01-22 18:49:24 +00:00
Diogo Peralta Cordeiro 5ac764f3e5
move to plugin 2022-01-22 18:49:24 +00:00
Diogo Peralta Cordeiro 4ad1de2616
some logic 2022-01-22 18:49:23 +00:00
Diogo Peralta Cordeiro 29f53bb698
wip5 2022-01-22 18:49:23 +00:00
Diogo Peralta Cordeiro cb16b627b4
cenas 2022-01-22 18:49:23 +00:00
Diogo Peralta Cordeiro 19dd4ba368
wip4 2022-01-22 18:49:23 +00:00
Diogo Peralta Cordeiro 53a1a3fad1
wip3 2022-01-22 18:49:23 +00:00
Diogo Peralta Cordeiro 737648359d
wip2 2022-01-22 18:49:23 +00:00
Diogo Peralta Cordeiro 57c09c6f8f
wip 2022-01-22 18:49:22 +00:00
Diogo Peralta Cordeiro 08e3da092b
[OAuth2] Add scopes 2022-01-22 18:49:22 +00:00
Diogo Peralta Cordeiro 7959ea497b
[PLUGIN][OAuth2] Add OAuth2 support 2022-01-22 18:49:22 +00:00
17 changed files with 1311 additions and 100 deletions

View File

@ -20,7 +20,7 @@ class HostMeta extends XrdController
public function setXRD()
{
if (Event::handle('StartHostMetaLinks', [&$this->xrd->links])) {
if (Event::handle('StartHostMetaLinks', [&$this->xrd->links]) !== Event::stop) {
Event::handle('EndHostMetaLinks', [&$this->xrd->links]);
}
}

View File

@ -19,12 +19,11 @@
"lstrojny/functional-php": "^1.17",
"masterminds/html5": "^2.7",
"mf2/mf2": "^0.4.6",
"nyholm/psr7": "^1.4",
"odolbeau/phone-number-bundle": "^3.1",
"oro/doctrine-extensions": "^2.0",
"php-ds/php-ds": "^1.2",
"phpdocumentor/reflection-docblock": "^5.2",
"sensio/framework-extra-bundle": "6.*",
"sensio/framework-extra-bundle": "^5.5",
"someonewithpc/memcached-polyfill": "^1.0",
"someonewithpc/redis-polyfill": "dev-master",
"symfony/asset": "5.4.*",

775
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,18 @@ security:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_apps:
pattern: ^/api/v1/apps$
security: false
api_token:
pattern: ^/oauth/token$
security: false
api:
provider: local_user
pattern: ^/api/
security: true
stateless: true
main:
entry_point: App\Security\Authenticator
guard:
@ -53,3 +65,4 @@ security:
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/settings, roles: ROLE_USER }
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED }

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types = 1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
/**
* ActivityPub implementation for GNU social
*
* @package OAuth2
* @category API
*
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\IndieAuth\Controller;
use App\Core\Controller;
use App\Core\DB\DB;
use App\Core\Log;
use App\Util\Common;
use Plugin\IndieAuth\Entity\OAuth2Client;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* App Management Endpoint
*
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class Apps extends Controller
{
public function onPost(): JsonResponse
{
Log::debug('OAuth2 Apps: Received a POST request.');
Log::debug('OAuth2 Apps: Request content: ', [$body = $this->request->getContent()]);
$args = json_decode($body, true);
$identifier = hash('md5', random_bytes(16));
// Random string Length should be between 43 and 128
$secret = Common::base64url_encode(hash('sha256', random_bytes(57)));
DB::persist($app = OAuth2Client::create([
'identifier' => $identifier,
'secret' => $secret,
'redirect_uris' => $args['redirect_uris'],
'grants' => 'client_credentials authorization_code',
'scopes' => $args['scopes'],
'active' => true,
'allow_plain_text_pkce' => false,
'client_name' => $args['client_name'],
'website' => $args['website'],
]));
Log::debug('OAuth2 Apps: Created App: ', [$app]);
DB::flush();
// Success
return new JsonResponse([
'name' => $app->getClientName(),
'website' => $app->getWebsite(),
'redirect_uri' => $app->getRedirectUris()[0],
'client_id' => $app->getIdentifier(),
'client_secret' => $app->getSecret(),
], status: 200, headers: ['content_type' => 'application/json; charset=utf-8']);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types = 1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
/**
* ActivityPub implementation for GNU social
*
* @package OAuth2
* @category API
*
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\IndieAuth\Controller;
use App\Core\Controller;
use Nyholm\Psr7\Factory\Psr17Factory;
use Plugin\IndieAuth\IndieAuth;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* App Management Endpoint
*
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class OAuth2 extends Controller
{
private ServerRequestInterface $psrRequest;
public function __construct(RequestStack $requestStack)
{
parent::__construct($requestStack);
$psr17Factory = new Psr17Factory();
$psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$this->psrRequest = $psrHttpFactory->createRequest($this->request);
}
public function handleAuthorizationEndpointRequest(): ResponseInterface
{
return IndieAuth::$server->handleAuthorizationEndpointRequest($this->psrRequest);
}
public function handleTokenEndpointRequest(): ResponseInterface
{
return IndieAuth::$server->handleTokenEndpointRequest($this->psrRequest);
}
}

View File

@ -0,0 +1,225 @@
<?php
declare(strict_types = 1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
/**
* ActivityPub implementation for GNU social
*
* @package GNUsocial
* @category OAuth2
*
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\IndieAuth\Entity;
use App\Core\Entity;
use DateTimeInterface;
/**
* OAuth application registration record
*
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class OAuth2Client extends Entity
{
// {{{ Autocode
// @codeCoverageIgnoreStart
private string $identifier;
private ?string $secret;
private string $redirect_uris = '';
private string $grants = '';
private string $scopes = '';
private bool $active = true;
private bool $allow_plain_text_pkce = false;
private ?string $client_name = null;
private ?string $website = null;
private DateTimeInterface $created;
private DateTimeInterface $modified;
public function __toString(): string
{
return $this->getIdentifier();
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getSecret(): ?string
{
return $this->secret;
}
public function setSecret(?string $secret): self
{
$this->secret = $secret;
return $this;
}
public function getRedirectUris(): array
{
return explode(' ', $this->redirect_uris);
}
public function setRedirectUris(string ...$redirect_uris): self
{
$this->redirect_uris = implode(' ', $redirect_uris);
return $this;
}
public function getGrants(): array
{
return explode(' ', $this->grants);
}
public function setGrants(string ...$grants): self
{
$this->grants = implode(' ', $grants);
return $this;
}
public function getScopes(): array
{
return explode(' ', $this->scopes);
}
public function setScopes(string ...$scopes): self
{
$this->scopes = implode(' ', $scopes);
return $this;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): self
{
$this->active = $active;
return $this;
}
public function isConfidential(): bool
{
return !empty($this->secret);
}
public function isPlainTextPkceAllowed(): bool
{
return $this->allow_plain_text_pkce;
}
public function setAllowPlainTextPkce(bool $allow_plain_text_pkce): self
{
$this->allow_plain_text_pkce = $allow_plain_text_pkce;
return $this;
}
public function setIdentifier(string $identifier): self
{
$this->identifier = $identifier;
return $this;
}
public function getClientName(): string
{
return $this->client_name;
}
public function setClientName(string $client_name): self
{
$this->client_name = $client_name;
return $this;
}
public function getWebsite(): ?string
{
return $this->website;
}
public function setWebsite(?string $website): self
{
$this->website = $website;
return $this;
}
public function setCreated(DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
public function getCreated(): DateTimeInterface
{
return $this->created;
}
public function setModified(DateTimeInterface $modified): self
{
$this->modified = $modified;
return $this;
}
public function getModified(): DateTimeInterface
{
return $this->modified;
}
// @codeCoverageIgnoreEnd
// }}} Autocode
/**
* Return table definition for Schema setup and Entity usage.
*
* @return array array of column definitions
*/
public static function schemaDef(): array
{
return [
'name' => 'oauth2_client',
'fields' => [
'identifier' => ['type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'foreign key to oauth2_client->identifier'],
'secret' => ['type' => 'varchar', 'length' => 128, 'not null' => false, 'description' => 'foreign key to oauth2_client->identifier'],
'client_name' => ['type' => 'varchar', 'length' => 191, 'not null' => false, 'description' => 'name of the application'],
'redirect_uris' => ['type' => 'text', 'not null' => false, 'description' => 'application homepage - used for source link'],
'grants' => ['type' => 'text', 'not null' => true, 'default' => '', 'description' => 'application homepage - used for source link'],
'scopes' => ['type' => 'text', 'not null' => true, 'default' => '', 'description' => 'application homepage - used for source link'],
'active' => ['type' => 'bool', 'not null' => true, 'description' => 'was this note generated by a local actor'],
'allow_plain_text_pkce' => ['type' => 'bool', 'not null' => true, 'default' => false, 'description' => 'was this note generated by a local actor'],
'website' => ['type' => 'text', 'not null' => false, 'description' => 'application homepage - used for source link'],
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
],
'primary key' => ['identifier'],
];
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types = 1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
/**
* ActivityPub implementation for GNU social
*
* @package GNUsocial
* @category API
*
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\IndieAuth;
use App\Core\Event;
use App\Core\Log;
use App\Core\Modules\Plugin;
use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Util\Common;
use Nyholm\Psr7\Response;
use Plugin\IndieAuth\Controller\Apps;
use Plugin\IndieAuth\Controller\OAuth2;
use Psr\Http\Message\ServerRequestInterface;
use Taproot\IndieAuth\Server;
use XML_XRD_Element_Link;
/**
* Adds OAuth2 support to GNU social when enabled
*
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class IndieAuth extends Plugin
{
public const OAUTH_ACCESS_TOKEN_REL = 'http://apinamespace.org/oauth/access_token';
public const OAUTH_REQUEST_TOKEN_REL = 'http://apinamespace.org/oauth/request_token';
public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize';
public static Server $server;
public function onInitializePlugin()
{
self::$server = new Server([
'secret' => 'YOUR_APP_INDIEAUTH_SECRET$config["secret"] must be a string with a minimum length of 64 characters.yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'logger' => Log::getLogger(),
'requirePKCE' => false,
// A path to store token data, or an object implementing TokenStorageInterface.
'tokenStorage' => '/../data/auth_tokens/',
// An authentication callback function, which either returns data about the current user,
// or redirects to/implements an authentication flow.
'authenticationHandler' => function (ServerRequestInterface $request, string $authenticationRedirect, ?string $normalizedMeUrl) {
// If the request is authenticated, return an array with a `me` key containing the
// canonical URL of the currently logged-in user.
if ($actor = Common::actor()) {
return ['me' => $actor->getUri(Router::ABSOLUTE_URL)];
}
// Otherwise, redirect the user to a login page, ensuring that they will be redirected
// back to the IndieAuth flow with query parameters intact once logged in.
return new Response(302, ['Location' => Router::url('security_login') . '?returnUrl=' . urlencode($authenticationRedirect)]);
},
]);
}
public function version(): string
{
return '3.0.0';
}
/**
* This code executes when GNU social creates the page routing, and we hook
* on this event to add our Inbox and Outbox handler for ActivityPub.
*
* @param RouteLoader $r the router that was initialized
*/
public function onAddRoute(RouteLoader $r): bool
{
$r->connect(
'oauth2_apps',
'/api/v1/apps',
Apps::class,
['http-methods' => ['POST']],
);
$r->connect(
'oauth2_authorization_code',
'/oauth/authorize',
[OAuth2::class, 'handleAuthorizationEndpointRequest'],
);
$r->connect(
'oauth2_token',
'/oauth/token',
[OAuth2::class, 'handleTokenEndpointRequest'],
);
return Event::next;
}
public function onEndHostMetaLinks(array &$links): bool
{
$links[] = new XML_XRD_Element_link(self::OAUTH_REQUEST_TOKEN_REL, Router::url('oauth2_apps', type: Router::ABSOLUTE_URL));
$links[] = new XML_XRD_Element_link(self::OAUTH_AUTHORIZE_REL, Router::url('oauth2_authorize', type: Router::ABSOLUTE_URL));
$links[] = new XML_XRD_Element_link(self::OAUTH_ACCESS_TOKEN_REL, Router::url('oauth2_token', type: Router::ABSOLUTE_URL));
return Event::next;
}
}

View File

@ -0,0 +1,7 @@
{
"require": {
"nyholm/psr7": "^1.4",
"symfony/psr-http-message-bridge": "^2.1",
"taproot/indieauth": "^0.1.0"
}
}

View File

@ -36,6 +36,7 @@ use LogicException;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
@ -49,11 +50,13 @@ class Security extends Controller
/**
* Log a user in
*/
public function login(AuthenticationUtils $authenticationUtils)
public function login(AuthenticationUtils $authenticationUtils): RedirectResponse|array
{
// Skip if already logged in
if ($this->getUser()) {
return $this->redirectToRoute('root');
// TODO: Fix the Open Redirect security flaw here.
$targetPath = Common::getRequest()->query->get('returnUrl');
return \is_null($targetPath) ? $this->redirectToRoute('root') : new RedirectResponse($targetPath);
}
// get the login error if there is one

View File

@ -180,8 +180,15 @@ abstract class Controller extends AbstractController implements EventSubscriberI
$event->getResponse()->headers->set('permissions-policy', 'interest-cohort=()');
$event->getResponse()->headers->set('strict-transport-security', 'max-age=15768000; preload;');
$event->getResponse()->headers->set('vary', 'Accept-Encoding,Cookie');
$event->getResponse()->headers->set('x-frame-options', 'SAMEORIGIN');
$event->getResponse()->headers->set('x-frame-options', 'DENY');
$event->getResponse()->headers->set('x-xss-protection', '1; mode=block');
$event->getResponse()->headers->set('x-content-type-options', 'nosniff');
$event->getResponse()->headers->set('x-download-options', 'noopen');
$event->getResponse()->headers->set('x-permitted-cross-domain-policies', 'none');
$event->getResponse()->headers->set('access-control-allow-credentials', true);
$event->getResponse()->headers->set('access-control-allow-origin', '*');
$event->getResponse()->headers->set('referrer-policy', 'same-origin');
$event->getResponse()->headers->set('access-control-expose-headers', 'Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id,Idempotency-Key');
$policy = "default-src 'self' 'unsafe-inline'; frame-ancestors 'self'; form-action 'self'; style-src 'self' 'unsafe-inline'; img-src * blob: data:;";
$event->getResponse()->headers->set('Content-Security-Policy', $policy);
$event->getResponse()->headers->set('X-Content-Security-Policy', $policy);
@ -257,6 +264,7 @@ abstract class Controller extends AbstractController implements EventSubscriberI
} else {
return null;
}
// no break
case 'params':
return $this->request->query->all();
default:

View File

@ -226,7 +226,7 @@ class GNUsocial implements EventSubscriberInterface
public static function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{
// Overriding doesn't work as we want, overrides the top-most key, do it manually
$local_file = INSTALLDIR . '/social.local.yaml';
$local_file = INSTALLDIR . \DIRECTORY_SEPARATOR . 'social.local.yaml';
if (!file_exists($local_file)) {
file_put_contents($local_file, "parameters:\n locals:\n gnusocial:\n");
}

View File

@ -58,6 +58,11 @@ abstract class Log
self::$logger = $l;
}
public static function getLogger(): LoggerInterface
{
return self::$logger;
}
/**
* Log a critical error when a really unexpected exception occured. This indicates a bug in the software
*

View File

@ -35,7 +35,6 @@ use App\Util\Exception\NicknameNotAllowedException;
use App\Util\Exception\NicknameTakenException;
use App\Util\Exception\NicknameTooLongException;
use App\Util\Nickname;
use DateTimeInterface;
use Exception;
use libphonenumber\PhoneNumber;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;

View File

@ -74,7 +74,8 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
public function supports(Request $request): bool
{
return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST');
return (self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST'))
|| ('oauth2_authorize' === $request->attributes->get('_route'));
}
/**
@ -156,7 +157,9 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
$nickname,
);
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
// TODO: Fix the Open Redirect security flaw here.
$targetPath = $request->query->get('returnUrl');
if ($targetPath ??= $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
@ -177,8 +180,8 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
);
}
protected function getLoginUrl()
protected function getLoginUrl(int $type = Router::ABSOLUTE_PATH): string
{
return Router::url(self::LOGIN_ROUTE);
return Router::url(self::LOGIN_ROUTE, type: $type);
}
}

View File

@ -67,6 +67,14 @@ abstract class Common
self::$request = $req;
}
/**
* Don't use this
*/
public static function getRequest(): ?Request
{
return self::$request;
}
public static function route()
{
return self::$request->attributes->get('_route');
@ -135,17 +143,20 @@ abstract class Common
public static function userNickname(): ?string
{
return self::ensureLoggedIn()->getNickname();
return self::ensureLoggedIn()?->getNickname();
}
public static function userId(): ?int
{
return self::ensureLoggedIn()->getId();
return self::ensureLoggedIn()?->getId();
}
/**
* @throws NoLoggedInUser
*/
public static function ensureLoggedIn(): LocalUser
{
if (($user = self::user()) == null) {
if (\is_null($user = self::user())) {
throw new NoLoggedInUser();
// TODO Maybe redirect to login page and back
} else {
@ -160,7 +171,7 @@ abstract class Common
*/
public static function isLoggedIn(): bool
{
return self::user() != null;
return !\is_null(self::user());
}
/**
@ -325,4 +336,18 @@ abstract class Common
{
return self::actor()?->getTopLanguage() ?? Language::getByLocale(self::$request->headers->has('accept-language') ? I18n::clientPreferredLanguage(self::$request->headers->get('accept-language')) : self::config('site', 'language'));
}
// Convert the ArrayBuffer to string using Uint8 array.
// btoa takes chars from 0-255 and base64 encodes.
// Then convert the base64 encoded to base64url encoded.
// (replace + with -, replace / with _, trim trailing =)
public static function base64url_encode(string $data): string
{
return rtrim(strtr(strtr(base64_encode($data), '+', '-'), '/', '_'), '=');
}
public static function base64url_decode(string $data): string
{
return base64_decode(str_pad(strtr(strtr($data, '_', '/'), '-', '+'), mb_strlen($data) % 4, '=', \STR_PAD_RIGHT));
}
}

View File

@ -2,6 +2,9 @@
"alchemy/binary-driver": {
"version": "v5.2.0"
},
"barnabywalters/mf-cleaner": {
"version": "v0.1.4"
},
"behat/gherkin": {
"version": "v4.9.0"
},
@ -65,6 +68,9 @@
"composer/xdebug-handler": {
"version": "1.4.6"
},
"dflydev/fig-cookies": {
"version": "v3.0.0"
},
"doctrine/annotations": {
"version": "1.0",
"recipe": {
@ -202,6 +208,15 @@
"guzzlehttp/psr7": {
"version": "2.1.0"
},
"indieauth/client": {
"version": "1.1.5"
},
"indieweb/link-rel-parser": {
"version": "0.1.3"
},
"indieweb/representative-h-card": {
"version": "0.1.2"
},
"jchook/phpunit-assert-throws": {
"version": "v1.0.3"
},
@ -283,6 +298,9 @@
"oscarotero/html-parser": {
"version": "v0.1.6"
},
"p3k/http": {
"version": "0.1.12"
},
"paragonie/constant_time_encoding": {
"version": "v2.4.0"
},
@ -372,6 +390,12 @@
"psr/http-message": {
"version": "1.0.1"
},
"psr/http-server-handler": {
"version": "1.0.1"
},
"psr/http-server-middleware": {
"version": "1.0.1"
},
"psr/link": {
"version": "1.1.1"
},
@ -704,6 +728,9 @@
"symfony/proxy-manager-bridge": {
"version": "v5.2.4"
},
"symfony/psr-http-message-bridge": {
"version": "v2.1.2"
},
"symfony/redis-messenger": {
"version": "v5.4.0"
},
@ -844,6 +871,9 @@
"symfonycasts/verify-email-bundle": {
"version": "v1.3.0"
},
"taproot/indieauth": {
"version": "v0.1.0"
},
"tgalopin/html-sanitizer": {
"version": "1.4.0"
},
@ -886,6 +916,9 @@
"webmozart/assert": {
"version": "1.10.0"
},
"webmozart/path-util": {
"version": "2.3.0"
},
"wikimedia/composer-merge-plugin": {
"version": "v2.0.1"
},