Built redirector service into Misuzu.
This commit is contained in:
parent
a83cfdc595
commit
f2233c5390
18 changed files with 612 additions and 2 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
20250104
|
||||
20250129
|
||||
|
|
25
assets/redir-bsky.js/main.js
Normal file
25
assets/redir-bsky.js/main.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
(async () => {
|
||||
const status = document.querySelector('.js-status');
|
||||
status.textContent = 'Looking up DID...';
|
||||
|
||||
let did = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${location.protocol}//${BSKY_HANDLE}/.well-known/atproto-did`);
|
||||
did = await response.text();
|
||||
} catch(ex) {
|
||||
status.style.color = 'red';
|
||||
status.textContent = `Could not find DID! ${ex}`;
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof did !== 'string' || !did.startsWith('did:')) {
|
||||
status.style.color = 'red';
|
||||
status.textContent = 'Look up result was not a valid DID.';
|
||||
return;
|
||||
}
|
||||
|
||||
const url = BSKY_FORMAT.replace('%s', did);
|
||||
status.textContent = `Redirecting to ${url}...`;
|
||||
location.replace(url);
|
||||
})();
|
39
assets/redir-fedi.js/main.js
Normal file
39
assets/redir-fedi.js/main.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
(async () => {
|
||||
const status = document.querySelector('.js-status');
|
||||
status.textContent = 'Looking up Fediverse profile...';
|
||||
|
||||
let url = null;
|
||||
try {
|
||||
const response = await fetch(`${location.protocol}//${FEDI_INSTANCE}/.well-known/webfinger?format=json&resource=acct:${FEDI_USERNAME}@${FEDI_INSTANCE}`);
|
||||
const webfinger = await response.json();
|
||||
|
||||
if(typeof webfinger === 'object') {
|
||||
if(Array.isArray(webfinger.links))
|
||||
for(const link of webfinger.links)
|
||||
if(typeof link === 'object' && link.rel === 'http://webfinger.net/rel/profile-page' && typeof link.href === 'string') {
|
||||
url = link.href;
|
||||
break;
|
||||
}
|
||||
|
||||
if(typeof url !== 'string' && Array.isArray(webfinger.aliases))
|
||||
for(const alias of webfinger.aliases)
|
||||
if(typeof alias === 'string') {
|
||||
url = alias;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch(ex) {
|
||||
status.style.color = 'red';
|
||||
status.textContent = `Could not complete Webfinger lookup! ${ex}`;
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof url !== 'string' || (!url.startsWith('https://') && !url.startsWith('http://'))) {
|
||||
status.style.color = 'red';
|
||||
status.textContent = 'Could not find an acceptable profile URL.';
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = `Redirecting to ${url}...`;
|
||||
location.replace(url);
|
||||
})();
|
2
build.js
2
build.js
|
@ -18,6 +18,8 @@ const fs = require('fs');
|
|||
const tasks = {
|
||||
js: [
|
||||
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
|
||||
{ source: 'redir-bsky.js', target: '/assets', name: 'redir-bsky.{hash}.js', },
|
||||
{ source: 'redir-fedi.js', target: '/assets', name: 'redir-fedi.{hash}.js', },
|
||||
],
|
||||
css: [
|
||||
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
|
||||
|
|
|
@ -6,3 +6,6 @@ database:dsn mariadb://<user>:<pass>@<host>/<name>?charset=utf8mb4
|
|||
;sentry:dsn https://sentry dsn here
|
||||
;sentry:tracesRate 1.0
|
||||
;sentry:profilesRate 1.0
|
||||
|
||||
domain:localhost main redirect
|
||||
domain:localhost:redirect:path /go
|
||||
|
|
28
database/2025_01_29_002819_create_redirect_tables.php
Normal file
28
database/2025_01_29_002819_create_redirect_tables.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
use Index\Db\DbConnection;
|
||||
use Index\Db\Migration\DbMigration;
|
||||
|
||||
final class CreateRedirectTables_20250129_002819 implements DbMigration {
|
||||
public function migrate(DbConnection $conn): void {
|
||||
$conn->execute(<<<SQL
|
||||
CREATE TABLE msz_redirects_named (
|
||||
redir_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
redir_vanity VARCHAR(255) NOT NULL COLLATE 'ascii_general_ci',
|
||||
redir_url VARCHAR(255) NOT NULL COLLATE 'ascii_bin',
|
||||
redir_clicks BIGINT(20) UNSIGNED NOT NULL DEFAULT '0',
|
||||
redir_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (redir_id),
|
||||
UNIQUE INDEX redirects_vanity_unique (redir_vanity)
|
||||
) ENGINE=InnoDB COLLATE='utf8mb4_bin'
|
||||
SQL);
|
||||
|
||||
$conn->execute(<<<SQL
|
||||
CREATE TABLE msz_redirects_incremental (
|
||||
redir_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
redir_url VARCHAR(255) NOT NULL COLLATE 'utf8mb4_bin',
|
||||
redir_created TIMESTAMP NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (redir_id)
|
||||
) ENGINE=InnoDB COLLATE='utf8mb4_bin'
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ use Misuzu\Messages\MessagesContext;
|
|||
use Misuzu\News\News;
|
||||
use Misuzu\Perms\Permissions;
|
||||
use Misuzu\Profile\ProfileFields;
|
||||
use Misuzu\Redirects\RedirectsContext;
|
||||
use Misuzu\Routing\{BackedRoutingContext,RoutingContext};
|
||||
use Misuzu\Users\{UsersContext,UserInfo};
|
||||
use RPCii\HmacVerificationProvider;
|
||||
|
@ -43,8 +44,9 @@ class MisuzuContext {
|
|||
|
||||
public private(set) AuthContext $authCtx;
|
||||
public private(set) ForumContext $forumCtx;
|
||||
private MessagesContext $messagesCtx;
|
||||
public private(set) MessagesContext $messagesCtx;
|
||||
public private(set) UsersContext $usersCtx;
|
||||
public private(set) RedirectsContext $redirectsCtx;
|
||||
|
||||
public private(set) ProfileFields $profileFields;
|
||||
|
||||
|
@ -68,6 +70,7 @@ class MisuzuContext {
|
|||
$this->forumCtx = new ForumContext($dbConn);
|
||||
$this->messagesCtx = new MessagesContext($dbConn);
|
||||
$this->usersCtx = new UsersContext($dbConn);
|
||||
$this->redirectsCtx = new RedirectsContext($dbConn, $config->scopeTo('redirects'));
|
||||
|
||||
$this->auditLog = new AuditLog($dbConn);
|
||||
$this->changelog = new Changelog($dbConn);
|
||||
|
@ -286,6 +289,23 @@ class MisuzuContext {
|
|||
));
|
||||
}
|
||||
|
||||
if(in_array('redirect', $purposes)) {
|
||||
$scopedInfo = $hostInfo->scopeTo('redirect');
|
||||
$scopedCtx = $routingCtx->scopeTo(
|
||||
$scopedInfo->getString('name'),
|
||||
$scopedInfo->getString('path')
|
||||
);
|
||||
|
||||
$scopedCtx->register(new Redirects\LandingRedirectsRoutes);
|
||||
$scopedCtx->register(new Redirects\AliasRedirectsRoutes($this->redirectsCtx));
|
||||
$scopedCtx->register(new Redirects\IncrementalRedirectsRoutes($this->redirectsCtx));
|
||||
$scopedCtx->register(new Redirects\SocialRedirectsRoutes(
|
||||
$this->redirectsCtx,
|
||||
$this->getWebAssetInfo(...)
|
||||
));
|
||||
$scopedCtx->register(new Redirects\NamedRedirectsRoutes($this->redirectsCtx));
|
||||
}
|
||||
|
||||
return $routingCtx;
|
||||
}
|
||||
}
|
||||
|
|
49
src/Redirects/AliasRedirectsRoutes.php
Normal file
49
src/Redirects/AliasRedirectsRoutes.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use Index\Config\Config;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
|
||||
|
||||
class AliasRedirectsRoutes implements RouteHandler {
|
||||
use RouteHandlerTrait;
|
||||
|
||||
private Config $config;
|
||||
|
||||
public function __construct(
|
||||
private RedirectsContext $redirectsCtx,
|
||||
) {
|
||||
$this->config = $redirectsCtx->config->scopeTo('alias');
|
||||
}
|
||||
|
||||
private function redirect(HttpResponseBuilder $response, HttpRequest $request, string $configKey, string $value) {
|
||||
$url = sprintf($this->config->getString($configKey), rawurlencode($value));
|
||||
|
||||
$params = $request->getParamString();
|
||||
if(!empty($params))
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . $params;
|
||||
|
||||
$response->redirect($url, true);
|
||||
}
|
||||
|
||||
#[HttpGet('/[up]([0-9]+)')]
|
||||
#[HttpGet('/[up]/([A-Za-z0-9\-_]+)')]
|
||||
public function getProfileRedirect(HttpResponseBuilder $response, HttpRequest $request, string $userId) {
|
||||
$this->redirect($response, $request, 'user_profile', $userId);
|
||||
}
|
||||
|
||||
#[HttpGet('/fc?/?([0-9]+)')]
|
||||
public function getForumCategoryRedirect(HttpResponseBuilder $response, HttpRequest $request, string $categoryId) {
|
||||
$this->redirect($response, $request, 'forum_category', $categoryId);
|
||||
}
|
||||
|
||||
#[HttpGet('/ft/?([0-9]+)')]
|
||||
public function getForumTopicRedirect(HttpResponseBuilder $response, HttpRequest $request, string $topicId) {
|
||||
$this->redirect($response, $request, 'forum_topic', $topicId);
|
||||
}
|
||||
|
||||
#[HttpGet('/fp/?([0-9]+)')]
|
||||
public function getForumPostRedirect(HttpResponseBuilder $response, HttpRequest $request, string $postId) {
|
||||
$this->redirect($response, $request, 'forum_post', $postId);
|
||||
}
|
||||
}
|
30
src/Redirects/IncrementalRedirectInfo.php
Normal file
30
src/Redirects/IncrementalRedirectInfo.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\XNumber;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class IncrementalRedirectInfo implements RedirectInfo {
|
||||
public function __construct(
|
||||
public private(set) string $id,
|
||||
public private(set) string $url,
|
||||
public private(set) int $createdTime,
|
||||
) {}
|
||||
|
||||
public static function fromResult(DbResult $result): IncrementalRedirectInfo {
|
||||
return new IncrementalRedirectInfo(
|
||||
id: $result->getString(0),
|
||||
url: $result->getString(1),
|
||||
createdTime: $result->getInteger(2),
|
||||
);
|
||||
}
|
||||
|
||||
public string $id62 {
|
||||
get => XNumber::toBase62($this->id);
|
||||
}
|
||||
|
||||
public CarbonImmutable $createdAt {
|
||||
get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
|
||||
}
|
||||
}
|
67
src/Redirects/IncrementalRedirectsData.php
Normal file
67
src/Redirects/IncrementalRedirectsData.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Db\{DbConnection,DbStatementCache};
|
||||
|
||||
class IncrementalRedirectsData {
|
||||
private DbStatementCache $cache;
|
||||
|
||||
public function __construct(DbConnection $dbConn) {
|
||||
$this->cache = new DbStatementCache($dbConn);
|
||||
}
|
||||
|
||||
public function getIncrementalRedirects(): iterable {
|
||||
$query = <<<SQL
|
||||
SELECT redir_id, redir_url, UNIX_TIMESTAMP(redir_created)
|
||||
FROM msz_redirects_incremental
|
||||
SQL;
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
$stmt->execute();
|
||||
|
||||
return $stmt->getResult()->getIterator(IncrementalRedirectInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public const int INC_BY_ID = 1;
|
||||
public const int INC_BY_URL = 2;
|
||||
|
||||
public function getIncrementalRedirect(string $value, int $field = self::INC_BY_ID): IncrementalRedirectInfo {
|
||||
$args = 0;
|
||||
$query = <<<SQL
|
||||
SELECT redir_id, redir_url, UNIX_TIMESTAMP(redir_created)
|
||||
FROM msz_redirects_incremental
|
||||
SQL;
|
||||
if(($field & self::INC_BY_ID) > 0) {
|
||||
++$args;
|
||||
$query .= ' WHERE redir_id = ?';
|
||||
}
|
||||
if(($field & self::INC_BY_URL) > 0)
|
||||
$query .= sprintf(' %s redir_url = ?', ++$args > 1 ? 'OR' : 'WHERE');
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
while(--$args >= 0)
|
||||
$stmt->nextParameter($value);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
if(!$result->next())
|
||||
throw new RuntimeException('incremental redirect not found');
|
||||
|
||||
return IncrementalRedirectInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public function createIncrementalRedirect(string $url): IncrementalRedirectInfo {
|
||||
$stmt = $this->cache->get('INSERT INTO msz_redirects_incremental (redir_url) VALUES (?)');
|
||||
$stmt->nextParameter($url);
|
||||
$stmt->execute();
|
||||
|
||||
return $this->getIncrementalRedirect((string)$stmt->getLastInsertId());
|
||||
}
|
||||
|
||||
public function deleteIncrementalRedirect(IncrementalRedirectInfo|string $redirectInfo): void {
|
||||
$stmt = $this->cache->get('DELETE msz_redirects_incremental WHERE redir_id = ?');
|
||||
$stmt->nextParameter($redirectInfo instanceof IncrementalRedirectInfo ? $redirectInfo->id : $redirectInfo);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
67
src/Redirects/IncrementalRedirectsRoutes.php
Normal file
67
src/Redirects/IncrementalRedirectsRoutes.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\XNumber;
|
||||
use Index\Http\{FormHttpContent,HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler,RouteHandlerTrait};
|
||||
|
||||
class IncrementalRedirectsRoutes implements RouteHandler {
|
||||
use RouteHandlerTrait;
|
||||
|
||||
public function __construct(
|
||||
private RedirectsContext $redirectsCtx,
|
||||
) {}
|
||||
|
||||
#[HttpGet('/[bg]/([A-Za-z0-9]+)')]
|
||||
public function getIncrementalRedirect(HttpResponseBuilder $response, HttpRequest $request, string $linkId) {
|
||||
$linkId = XNumber::fromBase62($linkId);
|
||||
|
||||
try {
|
||||
$redirectInfo = $this->redirectsCtx->incremental->getIncrementalRedirect($linkId);
|
||||
} catch(RuntimeException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
$response->redirect($redirectInfo->url, true);
|
||||
}
|
||||
|
||||
#[HttpPost('/satori/create')]
|
||||
public function postIncrementalRedirect(HttpResponseBuilder $response, HttpRequest $request) {
|
||||
$content = $request->getContent();
|
||||
if(!($content instanceof FormHttpContent))
|
||||
return 400;
|
||||
|
||||
$config = $this->redirectsCtx->config->scopeTo('incremental');
|
||||
$url = (string)$content->getParam('u');
|
||||
$time = (int)$content->getParam('t', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sign = base64_decode((string)$content->getParam('s'));
|
||||
$hash = hash_hmac('sha256', "satori#create#{$time}#{$url}", $config->getString('secret'), true);
|
||||
|
||||
if(!hash_equals($hash, $sign))
|
||||
return 403;
|
||||
|
||||
$currentTime = time();
|
||||
if($time < $currentTime - 30 || $time > $currentTime + 30)
|
||||
return 403;
|
||||
|
||||
try {
|
||||
$redirectInfo = $this->redirectsCtx->incremental->getIncrementalRedirect(
|
||||
$url,
|
||||
IncrementalRedirectsData::INC_BY_URL
|
||||
);
|
||||
} catch(RuntimeException $ex) {
|
||||
try {
|
||||
$redirectInfo = $this->redirectsCtx->incremental->createIncrementalRedirect($url);
|
||||
} catch(\Throwable $ex) {
|
||||
return [
|
||||
'url' => (string)$ex,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'url' => sprintf($config->getString('format'), $redirectInfo->id62),
|
||||
];
|
||||
}
|
||||
}
|
21
src/Redirects/LandingRedirectsRoutes.php
Normal file
21
src/Redirects/LandingRedirectsRoutes.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
|
||||
|
||||
class LandingRedirectsRoutes implements RouteHandler {
|
||||
use RouteHandlerTrait;
|
||||
|
||||
#[HttpGet('/')]
|
||||
public function getIndex() {
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Redirects</title>
|
||||
<p>Short URL Service for <a href=/home rel=noopener>Flashii</a></p>
|
||||
<p>This is a temporary landing page until backend is less tied up.</p>
|
||||
<p><a href=https://flash.moe target=_blank rel=noopener>flashwave</a> 2017-2025</p>
|
||||
HTML;
|
||||
}
|
||||
}
|
30
src/Redirects/NamedRedirectInfo.php
Normal file
30
src/Redirects/NamedRedirectInfo.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Index\XNumber;
|
||||
use Index\Db\DbResult;
|
||||
|
||||
class NamedRedirectInfo implements RedirectInfo {
|
||||
public function __construct(
|
||||
public private(set) string $id,
|
||||
public private(set) ?string $vanity,
|
||||
public private(set) string $url,
|
||||
public private(set) int $clicks,
|
||||
public private(set) int $createdTime,
|
||||
) {}
|
||||
|
||||
public static function fromResult(DbResult $result): NamedRedirectInfo {
|
||||
return new NamedRedirectInfo(
|
||||
id: $result->getString(0),
|
||||
vanity: $result->getStringOrNull(1),
|
||||
url: $result->getString(2),
|
||||
clicks: $result->getInteger(3),
|
||||
createdTime: $result->getInteger(4),
|
||||
);
|
||||
}
|
||||
|
||||
public CarbonImmutable $createdAt {
|
||||
get => CarbonImmutable::createFromTimestampUTC($this->createdTime);
|
||||
}
|
||||
}
|
80
src/Redirects/NamedRedirectsData.php
Normal file
80
src/Redirects/NamedRedirectsData.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Db\{DbConnection,DbStatementCache};
|
||||
|
||||
class NamedRedirectsData {
|
||||
private DbStatementCache $cache;
|
||||
|
||||
public function __construct(DbConnection $dbConn) {
|
||||
$this->cache = new DbStatementCache($dbConn);
|
||||
}
|
||||
|
||||
public function getNamedRedirects(): iterable {
|
||||
$query = <<<SQL
|
||||
SELECT redir_id, redir_vanity, redir_url, redir_clicks, UNIX_TIMESTAMP(redir_created)
|
||||
FROM msz_redirects_named
|
||||
SQL;
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
$stmt->execute();
|
||||
|
||||
return $stmt->getResult()->getIterator(NamedRedirectInfo::fromResult(...));
|
||||
}
|
||||
|
||||
public const int NAMED_BY_ID = 1;
|
||||
public const int NAMED_BY_VANITY = 2;
|
||||
public const int NAMED_BY_URL = 4;
|
||||
|
||||
public function getNamedRedirect(string $value, int $field = self::NAMED_BY_ID): NamedRedirectInfo {
|
||||
$args = 0;
|
||||
$query = <<<SQL
|
||||
SELECT redir_id, redir_vanity, redir_url, redir_clicks, UNIX_TIMESTAMP(redir_created)
|
||||
FROM msz_redirects_named
|
||||
SQL;
|
||||
if(($field & self::NAMED_BY_ID) > 0) {
|
||||
++$args;
|
||||
$query .= ' WHERE redir_id = ?';
|
||||
}
|
||||
if(($field & self::NAMED_BY_VANITY) > 0)
|
||||
$query .= sprintf(' %s redir_vanity = ?', ++$args > 1 ? 'OR' : 'WHERE');
|
||||
if(($field & self::NAMED_BY_URL) > 0)
|
||||
$query .= sprintf(' %s redir_url = ?', ++$args > 1 ? 'OR' : 'WHERE');
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
while(--$args >= 0)
|
||||
$stmt->nextParameter($value);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
if(!$result->next())
|
||||
throw new RuntimeException('named redirect not found');
|
||||
|
||||
return NamedRedirectInfo::fromResult($result);
|
||||
}
|
||||
|
||||
public function createNamedRedirect(
|
||||
string $vanity,
|
||||
string $url
|
||||
): NamedRedirectInfo {
|
||||
$stmt = $this->cache->get('INSERT INTO msz_redirects_named (redir_vanity, redir_url) VALUES (?, ?)');
|
||||
$stmt->nextParameter($vanity);
|
||||
$stmt->nextParameter($url);
|
||||
$stmt->execute();
|
||||
|
||||
return $this->getNamedRedirect((string)$stmt->getLastInsertId(), self::NAMED_BY_ID);
|
||||
}
|
||||
|
||||
public function deleteNamedRedirect(NamedRedirectInfo|string $redirectInfo): void {
|
||||
$stmt = $this->cache->get('DELETE msz_redirects_named WHERE redir_id = ?');
|
||||
$stmt->nextParameter($redirectInfo instanceof NamedRedirectInfo ? $redirectInfo->id : $redirectInfo);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function incrementNamedRedirectClicks(NamedRedirectInfo|string $redirectInfo): void {
|
||||
$stmt = $this->cache->get('UPDATE msz_redirects_named SET redir_clicks = redir_clicks + 1 WHERE redir_id = ?');
|
||||
$stmt->nextParameter($redirectInfo instanceof NamedRedirectInfo ? $redirectInfo->id : $redirectInfo);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
38
src/Redirects/NamedRedirectsRoutes.php
Normal file
38
src/Redirects/NamedRedirectsRoutes.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Config\Config;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
|
||||
|
||||
class NamedRedirectsRoutes implements RouteHandler {
|
||||
use RouteHandlerTrait;
|
||||
|
||||
private Config $config;
|
||||
|
||||
public function __construct(
|
||||
private RedirectsContext $redirectsCtx,
|
||||
) {
|
||||
$this->config = $redirectsCtx->config->scopeTo('named');
|
||||
}
|
||||
|
||||
#[HttpGet('/([A-Za-z0-9\-_]+)')]
|
||||
public function getNamedRedirect(HttpResponseBuilder $response, HttpRequest $request, string $name) {
|
||||
try {
|
||||
$redirectInfo = $this->redirectsCtx->named->getNamedRedirect(
|
||||
$name,
|
||||
NamedRedirectsData::NAMED_BY_ID | NamedRedirectsData::NAMED_BY_VANITY
|
||||
);
|
||||
} catch(RuntimeException $ex) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
$url = $redirectInfo->url;
|
||||
$params = $request->getParamString();
|
||||
if(!empty($params))
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . $params;
|
||||
|
||||
$response->redirect($url, true);
|
||||
}
|
||||
}
|
11
src/Redirects/RedirectInfo.php
Normal file
11
src/Redirects/RedirectInfo.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
interface RedirectInfo {
|
||||
public string $id { get; }
|
||||
public string $url { get; }
|
||||
public int $createdTime { get; }
|
||||
public CarbonImmutable $createdAt { get; }
|
||||
}
|
18
src/Redirects/RedirectsContext.php
Normal file
18
src/Redirects/RedirectsContext.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use Index\Config\Config;
|
||||
use Index\Db\DbConnection;
|
||||
|
||||
class RedirectsContext {
|
||||
public private(set) NamedRedirectsData $named;
|
||||
public private(set) IncrementalRedirectsData $incremental;
|
||||
|
||||
public function __construct(
|
||||
DbConnection $dbConn,
|
||||
public private(set) Config $config
|
||||
) {
|
||||
$this->named = new NamedRedirectsData($dbConn);
|
||||
$this->incremental = new IncrementalRedirectsData($dbConn);
|
||||
}
|
||||
}
|
82
src/Redirects/SocialRedirectsRoutes.php
Normal file
82
src/Redirects/SocialRedirectsRoutes.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
namespace Misuzu\Redirects;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\Config\Config;
|
||||
use Index\Http\{HttpRequest,HttpResponseBuilder};
|
||||
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
|
||||
|
||||
class SocialRedirectsRoutes implements RouteHandler {
|
||||
use RouteHandlerTrait;
|
||||
|
||||
private Config $config;
|
||||
private $getWebAssetInfo;
|
||||
|
||||
public function __construct(
|
||||
RedirectsContext $redirectsCtx,
|
||||
$getWebAssetInfo
|
||||
) {
|
||||
$this->config = $redirectsCtx->config->scopeTo('social');
|
||||
if(!is_callable($getWebAssetInfo))
|
||||
throw new InvalidArgumentException('$getWebAssetInfo must be callable');
|
||||
$this->getWebAssetInfo = $getWebAssetInfo;
|
||||
}
|
||||
|
||||
#[HttpGet('/bsky/((did:[a-z0-9]+:[A-Za-z0-9.\-_:%]+)|(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])))')]
|
||||
public function getBlueskyRedirect(HttpResponseBuilder $response, HttpRequest $request, string $handle) {
|
||||
$did = null;
|
||||
|
||||
if(str_starts_with($handle, 'did:'))
|
||||
$did = $handle;
|
||||
else {
|
||||
$timeout = ini_get('default_socket_timeout');
|
||||
try {
|
||||
ini_set('default_socket_timeout', 3);
|
||||
$records = dns_get_record(sprintf('_atproto.%s', $handle), DNS_TXT);
|
||||
|
||||
if(is_array($records))
|
||||
foreach($records as $record)
|
||||
if(array_key_exists('txt', $record) && str_starts_with($record['txt'], 'did=')) {
|
||||
$did = trim(substr(trim($record['txt']), 4));
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
ini_set('default_socket_timeout', $timeout);
|
||||
}
|
||||
}
|
||||
|
||||
$format = $this->config->getString('bsky_profile', 'https://bsky.app/profile/%s');
|
||||
if(is_string($did)) {
|
||||
$response->redirect(sprintf($format, $did), true);
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = rawurlencode($handle);
|
||||
$script = ($this->getWebAssetInfo)()->{'redir-bsky.js'} ?? '';
|
||||
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Redirecting to Bluesky profile...</title>
|
||||
<div class=js-status><noscript>Javascript must be enabled for Bluesky redirects to work.</noscript></div>
|
||||
<script>const BSKY_FORMAT = '{$format}'; const BSKY_HANDLE = decodeURIComponent('{$handle}');</script>
|
||||
<script src="{$script}"></script>
|
||||
HTML;
|
||||
}
|
||||
|
||||
#[HttpGet('/fedi/([A-Za-z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})')]
|
||||
public function getFediverseRedirect(HttpResponseBuilder $response, HttpRequest $request, string $userName, string $instance) {
|
||||
$userName = rawurlencode($userName);
|
||||
$instance = rawurlencode($instance);
|
||||
$script = ($this->getWebAssetInfo)()->{'redir-fedi.js'} ?? '';
|
||||
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Redirecting to Fediverse profile...</title>
|
||||
<div class=js-status><noscript>Javascript must be enabled for Fediverse redirects to work.</noscript></div>
|
||||
<script>const FEDI_USERNAME = '{$userName}'; const FEDI_INSTANCE = decodeURIComponent('{$instance}');</script>
|
||||
<script src="{$script}"></script>
|
||||
HTML;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue