First bits of Indexification.

This commit is contained in:
flash 2022-02-26 01:27:52 +00:00
parent 21526491c6
commit 881426e950
30 changed files with 381 additions and 1823 deletions

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "lib/index"]
path = lib/index
url = git@github.com:flashwave/index.git

View file

@ -2,6 +2,8 @@
namespace Misuzu;
use PDO;
use Index\Autoloader;
use Index\Environment;
use Misuzu\Net\GeoIP;
use Misuzu\Net\IPAddress;
use Misuzu\Users\User;
@ -16,18 +18,14 @@ define('MSZ_DEBUG', is_file(MSZ_ROOT . '/.debug'));
define('MSZ_PHP_MIN_VER', '7.4.0');
define('MSZ_PUBLIC', MSZ_ROOT . '/public');
define('MSZ_SOURCE', MSZ_ROOT . '/src');
define('MSZ_LIBRARIES', MSZ_ROOT . '/lib');
define('MSZ_CONFIG', MSZ_ROOT . '/config');
define('MSZ_TEMPLATES', MSZ_ROOT . '/templates');
if(version_compare(PHP_VERSION, MSZ_PHP_MIN_VER, '<'))
die("Misuzu requires <i>at least</i> PHP <b>" . MSZ_PHP_MIN_VER . "</b> to run.\r\n");
if(!extension_loaded('curl') || !extension_loaded('intl') || !extension_loaded('json')
|| !extension_loaded('mbstring') || !extension_loaded('pdo') || !extension_loaded('readline')
|| !extension_loaded('xml') || !extension_loaded('zip'))
die("An extension required by Misuzu hasn't been installed.\r\n");
require_once MSZ_LIBRARIES . '/index/index.php';
error_reporting(MSZ_DEBUG ? -1 : 0);
ini_set('display_errors', MSZ_DEBUG ? 'On' : 'Off');
Autoloader::addNamespace(__NAMESPACE__, MSZ_SOURCE);
Environment::setDebug(MSZ_DEBUG);
mb_internal_encoding('utf-8');
date_default_timezone_set('utc');
@ -58,19 +56,6 @@ set_error_handler(function(int $errno, string $errstr, string $errfile, int $err
require_once 'vendor/autoload.php';
spl_autoload_register(function(string $className) {
$parts = explode('\\', trim($className, '\\'), 2);
if($parts[0] !== 'Misuzu')
return;
$classPath = MSZ_SOURCE . '/' . str_replace('\\', '/', $parts[1]) . '.php';
if(is_file($classPath))
require_once $classPath;
});
class_alias(\Misuzu\Http\HttpResponseMessage::class, '\HttpResponse');
class_alias(\Misuzu\Http\HttpRequestMessage::class, '\HttpRequest');
require_once 'utility.php';
require_once 'src/perms.php';
require_once 'src/manage.php';

View file

@ -1,108 +1,105 @@
<?php
namespace Misuzu;
use Misuzu\Http\HttpRequestMessage;
use Misuzu\Http\Routing\Router;
use Misuzu\Http\Routing\Route;
use Misuzu\Template;
use Index\Http\HttpFx;
use Index\Http\HttpRequest;
require_once __DIR__ . '/../misuzu.php';
$request = HttpRequestMessage::fromGlobals();
Router::setHandlerFormat('\Misuzu\Http\Handlers\%sHandler');
Router::setFilterFormat('\Misuzu\Http\Filters\%sFilter');
Router::addRoutes(
// Home
Route::get('/', 'index', 'Home'),
// Assets
Route::group('/assets', 'Assets')->addChildren(
Route::get('/([a-zA-Z0-9\-]+)\.(css|js)', 'serveComponent'),
Route::get('/avatar/([0-9]+)(?:\.png)?', 'serveAvatar'),
Route::get('/profile-background/([0-9]+)(?:\.png)?', 'serveProfileBackground'),
),
// Info
Route::get('/info', 'index', 'Info'),
Route::get('/info/([A-Za-z0-9_/]+)', 'page', 'Info'),
// Changelog
Route::get('/changelog', 'index', 'Changelog')->addChildren(
Route::get('.atom', 'feedAtom'),
Route::get('.rss', 'feedRss'),
Route::get('/change/([0-9]+)', 'change'),
),
// News
Route::get('/news', 'index', 'News')->addChildren(
Route::get('.atom', 'feedIndexAtom'),
Route::get('.rss', 'feedIndexRss'),
Route::get('/([0-9]+)', 'viewCategory'),
Route::get('/([0-9]+).atom', 'feedCategoryAtom'),
Route::get('/([0-9]+).rss', 'feedCategoryRss'),
Route::get('/post/([0-9]+)', 'viewPost')
),
// Forum
Route::group('/forum', 'Forum')->addChildren(
Route::get('/mark-as-read', 'markAsReadGET')->addFilters('EnforceLogIn'),
Route::post('/mark-as-read', 'markAsReadPOST')->addFilters('EnforceLogIn', 'ValidateCsrf'),
),
// Sock Chat
Route::create(['GET', 'POST'], '/_sockchat.php', 'phpFile', 'SockChat'),
Route::group('/_sockchat', 'SockChat')->addChildren(
Route::get('/emotes', 'emotes'),
Route::get('/login', 'login'),
Route::get('/resolve', 'resolve'),
Route::post('/bump', 'bump'),
Route::post('/verify', 'verify'),
Route::create(['GET', 'OPTIONS'], '/token', 'token'),
Route::create(['GET', 'OPTIONS'], '/profile-check', 'profileCheck'),
Route::get('/bans', 'bans')->addChildren(
Route::get('/check', 'checkBan'),
Route::post('/create', 'createBan'),
Route::delete('/remove', 'removeBan'),
),
),
// Redirects
Route::get('/index.php', url('index')),
Route::get('/info.php', url('info')),
Route::get('/settings.php', url('settings-index')),
Route::get('/changelog.php', 'legacy', 'Changelog'),
Route::get('/info.php/([A-Za-z0-9_/]+)', 'redir', 'Info'),
Route::get('/auth.php', 'legacy', 'Auth'),
Route::get('/news.php', 'legacy', 'News'),
Route::get('/news.php/rss', 'legacy', 'News'),
Route::get('/news.php/atom', 'legacy', 'News'),
Route::get('/news/index.php', 'legacy', 'News'),
Route::get('/news/category.php', 'legacy', 'News'),
Route::get('/news/post.php', 'legacy', 'News'),
Route::get('/news/feed.php', 'legacy', 'News'),
Route::get('/news/feed.php/rss', 'legacy', 'News'),
Route::get('/news/feed.php/atom', 'legacy', 'News'),
Route::get('/user-assets.php', 'serveLegacy', 'Assets'),
);
$response = Router::handle($request);
$response->setHeader('X-Powered-By', 'Misuzu');
$responseStatus = $response->getStatusCode();
header('HTTP/' . $response->getProtocolVersion() . ' ' . $responseStatus . ' ' . $response->getReasonPhrase());
foreach($response->getHeaders() as $name => $lines) {
$firstLine = true;
foreach($lines as $line) {
header("{$name}: {$line}", $firstLine);
$firstLine = false;
}
function msz_compat_handler(string $className, string $method) {
return function(...$args) use ($className, $method) {
$className = "\\Misuzu\\Http\\Handlers\\{$className}Handler";
return (new $className)->{$method}(...$args);
};
}
$responseBody = $response->getBody();
function msz_compat_redirect(string $target) {
return function($response) use ($target) {
$response->redirect($target, true);
};
}
if($responseStatus >= 400 && $responseStatus <= 599 && $responseBody === null)
echo render_error($responseStatus);
else
echo (string)$responseBody;
$router = new HttpFx;
$router->use('/', function($response) {
$response->setPoweredBy('Misuzu');
});
$router->addErrorHandler(400, function($response) {
$response->setContent(Template::renderRaw('errors.404'));
});
$router->addErrorHandler(403, function($response) {
$response->setContent(Template::renderRaw('errors.404'));
});
$router->addErrorHandler(404, function($response) {
$response->setContent(Template::renderRaw('errors.404'));
});
$router->addErrorHandler(500, function($response) {
$response->setContent(file_get_contents(MSZ_TEMPLATES . '/500.html'));
});
$router->addErrorHandler(503, function($response) {
$response->setContent(file_get_contents(MSZ_TEMPLATES . '/503.html'));
});
$request = HttpRequest::fromRequest();
if(strpos($request->getPath(), '.php') !== false) {
$router->get('/index.php', msz_compat_redirect(url('index')));
$router->get('/info.php', msz_compat_redirect(url('info')));
$router->get('/settings.php', msz_compat_redirect(url('settings-index')));
$router->get('/changelog.php', msz_compat_handler('Changelog', 'legacy'));
$router->get('/info.php/:name', msz_compat_handler('Info', 'redir'));
$router->get('/auth.php', msz_compat_handler('Auth', 'legacy'));
$router->get('/news.php', msz_compat_handler('News', 'legacy'));
$router->get('/news.php/rss', msz_compat_handler('News', 'legacy'));
$router->get('/news.php/atom', msz_compat_handler('News', 'legacy'));
$router->get('/news/index.php', msz_compat_handler('News', 'legacy'));
$router->get('/news/category.php', msz_compat_handler('News', 'legacy'));
$router->get('/news/post.php', msz_compat_handler('News', 'legacy'));
$router->get('/news/feed.php', msz_compat_handler('News', 'legacy'));
$router->get('/news/feed.php/rss', msz_compat_handler('News', 'legacy'));
$router->get('/news/feed.php/atom', msz_compat_handler('News', 'legacy'));
$router->get('/user-assets.php', msz_compat_handler('Assets', 'serveLegacy'));
$router->get('/_sockchat.php', msz_compat_handler('SockChat', 'phpFile'));
$router->post('/_sockchat.php', msz_compat_handler('SockChat', 'phpFile'));
} else {
$router->get('/', msz_compat_handler('Home', 'index'));
$router->get('/assets/:filename', msz_compat_handler('Assets', 'serveComponent'));
$router->get('/assets/avatar/:filename', msz_compat_handler('Assets', 'serveAvatar'));
$router->get('/assets/profile-background/:filename', msz_compat_handler('Assets', 'serveProfileBackground'));
$router->get('/info', msz_compat_handler('Info', 'index'));
$router->get('/info/:name', msz_compat_handler('Info', 'page'));
$router->get('/info/:project/:name', msz_compat_handler('Info', 'page'));
$router->get('/changelog', msz_compat_handler('Changelog', 'index'));
$router->get('/changelog.rss', msz_compat_handler('Changelog', 'feedRss'));
$router->get('/changelog.atom', msz_compat_handler('Changelog', 'feedAtom'));
$router->get('/changelog/change/:id', msz_compat_handler('Changelog', 'change'));
$router->get('/news', msz_compat_handler('News', 'index'));
$router->get('/news.rss', msz_compat_handler('News', 'feedIndexRss'));
$router->get('/news.atom', msz_compat_handler('News', 'feedIndexAtom'));
$router->get('/news/:category', msz_compat_handler('News', 'viewCategory'));
$router->get('/news/post/:id', msz_compat_handler('News', 'viewPost'));
$router->get('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadGET'));
$router->post('/forum/mark-as-read', msz_compat_handler('Forum', 'markAsReadPOST'));
$router->get('/_sockchat/emotes', msz_compat_handler('SockChat', 'emotes'));
$router->get('/_sockchat/login', msz_compat_handler('SockChat', 'login'));
$router->get('/_sockchat/resolve', msz_compat_handler('SockChat', 'resolve'));
$router->post('/_sockchat/bump', msz_compat_handler('SockChat', 'bump'));
$router->post('/_sockchat/verify', msz_compat_handler('SockChat', 'verify'));
$router->get('/_sockchat/token', msz_compat_handler('SockChat', 'token'));
$router->options('/_sockchat/token', msz_compat_handler('SockChat', 'token'));
$router->get('/_sockchat/profile-check', msz_compat_handler('SockChat', 'profileCheck'));
$router->options('/_sockchat/profile-check', msz_compat_handler('SockChat', 'profileCheck'));
$router->get('/_sockchat/bans', msz_compat_handler('SockChat', 'bans'));
$router->get('/_sockchat/bans/check', msz_compat_handler('SockChat', 'checkBan'));
$router->post('/_sockchat/bans/create', msz_compat_handler('SockChat', 'createBan'));
$router->delete('/_sockchat/bans/remove', msz_compat_handler('SockChat', 'removeBan'));
}
$router->dispatch($request);

View file

@ -1,16 +0,0 @@
<?php
namespace Misuzu\Http\Filters;
use Misuzu\Http\HttpResponseMessage;
use Misuzu\Http\HttpRequestMessage;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
class EnforceLogInFilter implements FilterInterface {
public function process(HttpRequestMessage $request): ?HttpResponseMessage {
if(!UserSession::hasCurrent() || !User::hasCurrent())
return new HttpResponseMessage(403);
return null;
}
}

View file

@ -1,16 +0,0 @@
<?php
namespace Misuzu\Http\Filters;
use Misuzu\Http\HttpResponseMessage;
use Misuzu\Http\HttpRequestMessage;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
class EnforceLogOutFilter implements FilterInterface {
public function process(HttpRequestMessage $request): ?HttpResponseMessage {
if(UserSession::hasCurrent() || User::hasCurrent())
return new HttpResponseMessage(404);
return null;
}
}

View file

@ -1,9 +0,0 @@
<?php
namespace Misuzu\Http\Filters;
use Misuzu\Http\HttpResponseMessage;
use Misuzu\Http\HttpRequestMessage;
interface FilterInterface {
public function process(HttpRequestMessage $request): ?HttpResponseMessage;
}

View file

@ -1,22 +0,0 @@
<?php
namespace Misuzu\Http\Filters;
use Misuzu\CSRF;
use Misuzu\Http\HttpResponseMessage;
use Misuzu\Http\HttpRequestMessage;
class ValidateCsrfFilter implements FilterInterface {
public function process(HttpRequestMessage $request): ?HttpResponseMessage {
if($request->getMethod() !== 'GET' && $request->getMethod() !== 'DELETE') {
$token = $request->getHeaderLine('X-Misuzu-CSRF');
if(empty($token))
$token = $request->getBodyParam('_csrf');
if(empty($token) || !CSRF::validate($token))
return new HttpResponseMessage(400);
}
return null;
}
}

View file

@ -1,8 +1,6 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
use Misuzu\GitInfo;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
@ -50,12 +48,15 @@ final class AssetsHandler extends Handler {
return $str;
}
public function serveComponent(HttpResponse $response, HttpRequest $request, string $name, string $type) {
$entityTag = sprintf('W/"%s.%s/%s"', $name, $type, GitInfo::hash());
public function serveComponent($response, $request, string $fileName) {
$name = pathinfo($fileName, PATHINFO_FILENAME);
$type = pathinfo($fileName, PATHINFO_EXTENSION);
$entityTag = sprintf('%s.%s/%s', $name, $type, GitInfo::hash());
if(!MSZ_DEBUG && $name === 'debug')
return 404;
if(!MSZ_DEBUG && $request->getHeaderLine('If-None-Match') === $entityTag)
if(!MSZ_DEBUG && $request->getHeaderFirstLine('If-None-Match') === '"' . $entityTag . '"')
return 304;
if(array_key_exists($type, self::TYPES)) {
@ -63,29 +64,29 @@ final class AssetsHandler extends Handler {
$path = ($type['root'] ?? '') . '/' . $name;
if(is_dir($path)) {
$response->setHeader('Content-Type', $type['mime'] ?? 'application/octet-stream');
$response->setHeader('Cache-Control', MSZ_DEBUG ? 'no-cache' : 'must-revalidate');
$response->setHeader('ETag', $entityTag);
$response->setContentType($type['mime'] ?? 'application/octet-stream');
$response->setCacheControl(MSZ_DEBUG ? 'no-cache' : 'must-revalidate');
$response->setEntityTag($entityTag);
return self::recurse($path);
}
}
}
private function canViewAsset(HttpRequest $request, User $assetUser): bool {
private function canViewAsset($request, User $assetUser): bool {
return !$assetUser->isBanned() || (
User::hasCurrent()
&& parse_url($request->getHeaderLine('Referer'), PHP_URL_PATH) === url('user-profile')
&& parse_url($request->getHeaderFirstLine('Referer'), PHP_URL_PATH) === url('user-profile')
&& perms_check_user(MSZ_PERMS_USER, User::getCurrent()->getId(), MSZ_PERM_USER_MANAGE_USERS)
);
}
private function serveUserAsset(HttpResponse $response, HttpRequest $request, UserImageAssetInterface $assetInfo): void {
private function serveUserAsset($response, $request, UserImageAssetInterface $assetInfo): void {
$contentType = $assetInfo->getMimeType();
$publicPath = $assetInfo->getPublicPath();
$fileName = $assetInfo->getFileName();
if($assetInfo instanceof UserAssetScalableInterface) {
$dimensions = (int)($request->getQueryParam('res', FILTER_SANITIZE_NUMBER_INT) ?? $request->getQueryParam('r', FILTER_SANITIZE_NUMBER_INT));
$dimensions = (int)($request->getParam('res', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('r', FILTER_SANITIZE_NUMBER_INT));
if($dimensions > 0) {
$assetInfo->ensureScaledExists($dimensions);
@ -95,12 +96,18 @@ final class AssetsHandler extends Handler {
}
}
$response->setHeader('X-Accel-Redirect', $publicPath);
$response->setHeader('Content-Type', $contentType);
$response->setHeader('Content-Disposition', sprintf('inline; filename="%s"', $fileName));
$response->accelRedirect($publicPath);
$response->setContentType($contentType);
$response->setFileName($fileName, false);
}
public function serveAvatar(HttpResponse $response, HttpRequest $request, int $userId) {
public function serveAvatar($response, $request, string $fileName) {
$userId = intval(pathinfo($fileName, PATHINFO_FILENAME));
$type = pathinfo($fileName, PATHINFO_EXTENSION);
if($type !== '' && $type !== 'png')
return 404;
$assetInfo = new StaticUserImageAsset(MSZ_PUBLIC . '/images/no-avatar.png', MSZ_PUBLIC);
try {
@ -116,23 +123,29 @@ final class AssetsHandler extends Handler {
$this->serveUserAsset($response, $request, $assetInfo);
}
public function serveProfileBackground(HttpResponse $response, HttpRequest $request, int $userId) {
public function serveProfileBackground($response, $request, string $fileName) {
$userId = intval(pathinfo($fileName, PATHINFO_FILENAME));
$type = pathinfo($fileName, PATHINFO_EXTENSION);
if($type !== '' && $type !== 'png')
return 404;
try {
$userInfo = User::byId($userId);
} catch(UserNotFoundException $ex) {}
if(empty($userInfo) || !$userInfo->hasBackground() || !$this->canViewAsset($request, $userInfo)) {
$response->setText('');
$response->setContent('');
return 404;
}
$this->serveUserAsset($response, $request, $userInfo->getBackgroundInfo());
}
public function serveLegacy(HttpResponse $response, HttpRequest $request) {
$assetUserId = (int)$request->getQueryParam('u', FILTER_SANITIZE_NUMBER_INT);
public function serveLegacy($response, $request) {
$assetUserId = (int)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT);
switch($request->getQueryParam('m')) {
switch($request->getParam('m')) {
case 'avatar':
$this->serveAvatar($response, $request, $assetUserId);
return;
@ -141,7 +154,7 @@ final class AssetsHandler extends Handler {
return;
}
$response->setText('');
$response->setContent('');
return 404;
}
}

View file

@ -1,17 +1,14 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
final class AuthHandler extends Handler {
public function __construct() {
$GLOBALS['misuzuBypassLockdown'] = true;
parent::__construct();
}
public static function legacy(HttpResponse $response, HttpRequest $request): void {
$mode = $request->getQueryParam('m');
public function legacy($response, $request): void {
$mode = $request->getParam('m');
$destination = [
'logout' => 'auth-logout',
'reset' => 'auth-reset',

View file

@ -2,10 +2,9 @@
namespace Misuzu\Http\Handlers;
use ErrorException;
use HttpResponse;
use HttpRequest;
use Misuzu\Config;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\Changelog\ChangelogChangeNotFoundException;
use Misuzu\Changelog\ChangelogTag;
@ -18,10 +17,10 @@ use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class ChangelogHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request) {
$filterDate = $request->getQueryParam('date');
$filterUser = $request->getQueryParam('user', FILTER_SANITIZE_NUMBER_INT);
//$filterTags = $request->getQueryParam('tags');
public function index($response, $request) {
$filterDate = $request->getParam('date');
$filterUser = $request->getParam('user', FILTER_SANITIZE_NUMBER_INT);
//$filterTags = $request->getParam('tags');
if($filterDate !== null)
try {
@ -58,26 +57,26 @@ class ChangelogHandler extends Handler {
if(empty($changes))
return 404;
$response->setTemplate('changelog.index', [
$response->setContent(Template::renderRaw('changelog.index', [
'changelog_infos' => $changes,
'changelog_date' => $filterDate,
'changelog_user' => $filterUser,
'changelog_pagination' => $pagination,
'comments_user' => User::getCurrent(),
]);
]));
}
public function change(HttpResponse $response, HttpRequest $request, int $changeId) {
public function change($response, $request, int $changeId) {
try {
$changeInfo = ChangelogChange::byId($changeId);
} catch(ChangelogChangeNotFoundException $ex) {
return 404;
}
$response->setTemplate('changelog.change', [
$response->setContent(Template::renderRaw('changelog.change', [
'change_info' => $changeInfo,
'comments_user' => User::getCurrent(),
]);
]));
}
private function createFeed(string $feedMode): Feed {
@ -106,26 +105,26 @@ class ChangelogHandler extends Handler {
return $feed;
}
public function feedAtom(HttpResponse $response, HttpRequest $request) {
public function feedAtom($response, $request) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(self::createFeed('atom'));
}
public function feedRss(HttpResponse $response, HttpRequest $request) {
public function feedRss($response, $request) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(self::createFeed('rss'));
}
public function legacy(HttpResponse $response, HttpRequest $request) {
$changeId = $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT);
public function legacy($response, $request) {
$changeId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
if($changeId) {
$response->redirect(url('changelog-change', ['change' => $changeId]), true);
return;
}
$response->redirect(url('changelog-index', [
'date' => $request->getQueryParam('d'),
'user' => $request->getQueryParam('u', FILTER_SANITIZE_NUMBER_INT),
'date' => $request->getParam('d'),
'user' => $request->getParam('u', FILTER_SANITIZE_NUMBER_INT),
]), true);
}
}

View file

@ -1,32 +1,49 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
use Misuzu\CSRF;
use Misuzu\Template;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
final class ForumHandler extends Handler {
public function markAsReadGET(HttpResponse $response, HttpRequest $request): void {
$forumId = (int)$request->getQueryParam('forum', FILTER_SANITIZE_NUMBER_INT);
$response->setTemplate('confirm', [
public function markAsReadGET($response, $request) {
if(!UserSession::hasCurrent() || !User::hasCurrent())
return 403;
$forumId = (int)$request->getParam('forum', FILTER_SANITIZE_NUMBER_INT);
$response->setContent(Template::renderRaw('confirm', [
'title' => 'Mark forum as read',
'message' => 'Are you sure you want to mark ' . ($forumId === 0 ? 'the entire' : 'this') . ' forum as read?',
'return' => url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]),
'params' => [
'forum' => $forumId,
]
]);
]));
}
public function markAsReadPOST(HttpResponse $response, HttpRequest $request) {
$forumId = (int)$request->getBodyParam('forum', FILTER_SANITIZE_NUMBER_INT);
forum_mark_read($forumId, User::getCurrent()->getId());
public function markAsReadPOST($response, $request) {
if(!UserSession::hasCurrent() || !User::hasCurrent())
return 403;
$response->redirect(
url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]),
false,
$request->hasHeader('X-Misuzu-XHR')
);
if(!$request->isFormContent())
return 400;
$token = $request->getHeaderLine('X-Misuzu-CSRF');
if(empty($token))
$token = $request->getBodyParam('_csrf');
if(empty($token) || !CSRF::validate($token))
return 400;
$forumId = (int)$request->getContent()->getParam('forum', FILTER_SANITIZE_NUMBER_INT);
forum_mark_read($forumId, User::getCurrent()->getId());
$redirect = url($forumId ? 'forum-category' : 'forum-index', ['forum' => $forumId]);
if($request->hasHeader('X-Misuzu-XHR')) {
$response->setStatusCode(302);
$response->setHeader('X-Misuzu-Location', $redirect);
} else
$response->redirect($redirect, false);
}
}

View file

@ -1,25 +1,24 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
use Misuzu\Config;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Changelog\ChangelogChange;
use Misuzu\News\NewsPost;
use Misuzu\Users\User;
use Misuzu\Users\UserSession;
final class HomeHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request): void {
public function index($response, $request): void {
if(UserSession::hasCurrent())
$this->home($response, $request);
else
$this->landing($response, $request);
}
public function landing(HttpResponse $response, HttpRequest $request): void {
public function landing($response, $request): void {
$linkedData = Config::get('social.embed_linked', Config::TYPE_BOOL)
? [
'name' => Config::get('site.name', Config::TYPE_STR, 'Misuzu'),
@ -92,17 +91,17 @@ final class HomeHandler extends Handler {
}
}
$response->setTemplate('home.landing', [
$response->setContent(Template::renderRaw('home.landing', [
'statistics' => $stats,
'online_users' => $onlineUsers,
'featured_news' => $featuredNews,
'linked_data' => $linkedData,
'forum_popular' => $popularTopics,
'forum_active' => $activeTopics,
]);
]));
}
public function home(HttpResponse $response, HttpRequest $request): void {
public function home($response, $request): void {
$featuredNews = NewsPost::all(new Pagination(5), true);
$stats = DB::query(
@ -130,13 +129,13 @@ final class HomeHandler extends Handler {
. ' LIMIT 104'
)->fetchAll();
$response->setTemplate('home.home', [
$response->setContent(Template::renderRaw('home.home', [
'statistics' => $stats,
'latest_user' => $latestUser,
'online_users' => $onlineUsers,
'birthdays' => $birthdays,
'featured_changelog' => $changelog,
'featured_news' => $featuredNews,
]);
]));
}
}

View file

@ -1,42 +1,55 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
use Misuzu\Template;
use Misuzu\Parsers\Parser;
final class InfoHandler extends Handler {
public function index(HttpResponse $response): void {
$response->setTemplate('info.index');
public function index($response): void {
$response->setContent(Template::renderRaw('info.index'));
}
public function redir(HttpResponse $response, HttpRequest $request, string $name = ''): void {
public function redir($response, $request, string $name = ''): void {
$response->redirect(url('info', ['title' => $name]), true);
}
public function page(HttpResponse $response, HttpRequest $request, string $name) {
public function page($response, $request, string ...$parts) {
$name = implode('/', $parts);
$document = [
'content' => '',
'title' => '',
];
$isIndexDoc = $name === 'index' || str_starts_with($name, 'index/');
$isMisuzuDoc = $name === 'misuzu' || str_starts_with($name, 'misuzu/');
if($isMisuzuDoc) {
$filename = substr($name, 7);
$filename = empty($filename) ? 'README' : strtoupper($filename);
if($filename !== 'README')
$fileName = substr($name, 7);
$fileName = empty($fileName) ? 'README' : strtoupper($fileName);
if($fileName !== 'README')
$titleSuffix = ' - Misuzu Project';
} else $filename = strtolower($name);
} elseif($isIndexDoc) {
$fileName = substr($name, 6);
$fileName = empty($fileName) ? 'README' : strtoupper($fileName);
if($fileName !== 'README')
$titleSuffix = ' - Index Project';
} else $fileName = strtolower($name);
if(!preg_match('#^([A-Za-z0-9_]+)$#', $filename))
if(!preg_match('#^([A-Za-z0-9_]+)$#', $fileName))
return 404;
if($filename !== 'LICENSE')
$filename .= '.md';
if($fileName !== 'LICENSE' && $fileName !== 'LICENCE')
$fileName .= '.md';
$filename = MSZ_ROOT . ($isMisuzuDoc ? '/' : '/docs/') . $filename;
$document['content'] = is_file($filename) ? file_get_contents($filename) : '';
$pfx = '';
if($isIndexDoc)
$pfx = '/lib/index';
elseif(!$isMisuzuDoc)
$pfx = '/docs';
$fileName = MSZ_ROOT . $pfx . '/' . $fileName;
$document['content'] = is_file($fileName) ? file_get_contents($fileName) : '';
if(empty($document['content']))
return 404;
@ -47,7 +60,7 @@ final class InfoHandler extends Handler {
$document['title'] = trim(substr($document['content'], 2, $titleOffset - 1));
$document['content'] = substr($document['content'], $titleOffset);
} else
$document['title'] = ucfirst(basename($filename));
$document['title'] = ucfirst(basename($fileName));
if(!empty($titleSuffix))
$document['title'] .= $titleSuffix;
@ -55,8 +68,8 @@ final class InfoHandler extends Handler {
$document['content'] = Parser::instance(Parser::MARKDOWN)->parseText($document['content']);
$response->setTemplate('info.view', [
$response->setContent(Template::renderRaw('info.view', [
'document' => $document,
]);
]));
}
}

View file

@ -1,11 +1,10 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
use Misuzu\Config;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Template;
use Misuzu\Feeds\Feed;
use Misuzu\Feeds\FeedItem;
use Misuzu\Feeds\AtomFeedSerializer;
@ -18,39 +17,49 @@ use Misuzu\Parsers\Parser;
use Misuzu\Users\User;
final class NewsHandler extends Handler {
public function index(HttpResponse $response, HttpRequest $request) {
public function index($response, $request) {
$categories = NewsCategory::all();
$newsPagination = new Pagination(NewsPost::countAll(true), 5);
if(!$newsPagination->hasValidOffset())
return 404;
$response->setTemplate('news.index', [
$response->setContent(Template::renderRaw('news.index', [
'categories' => $categories,
'posts' => NewsPost::all($newsPagination, true),
'news_pagination' => $newsPagination,
]);
]));
}
public function viewCategory(HttpResponse $response, HttpRequest $request, int $categoryId) {
public function viewCategory($response, $request, string $fileName) {
$categoryId = intval(pathinfo($fileName, PATHINFO_FILENAME));
$type = pathinfo($fileName, PATHINFO_EXTENSION);
try {
$categoryInfo = NewsCategory::byId($categoryId);
} catch(NewsCategoryNotFoundException $ex) {
return 404;
}
if($type === 'atom')
return $this->feedCategoryAtom($response, $request, $categoryInfo);
elseif($type === 'rss')
return $this->feedCategoryRss($response, $request, $categoryInfo);
elseif($type !== '')
return 404;
$categoryPagination = new Pagination(NewsPost::countByCategory($categoryInfo), 5);
if(!$categoryPagination->hasValidOffset())
return 404;
$response->setTemplate('news.category', [
$response->setContent(Template::renderRaw('news.category', [
'category_info' => $categoryInfo,
'posts' => $categoryInfo->posts($categoryPagination),
'news_pagination' => $categoryPagination,
]);
]));
}
public function viewPost(HttpResponse $response, HttpRequest $request, int $postId) {
public function viewPost($response, $request, int $postId) {
try {
$postInfo = NewsPost::byId($postId);
} catch(NewsPostNotFoundException $ex) {
@ -63,12 +72,11 @@ final class NewsHandler extends Handler {
$postInfo->ensureCommentsCategory();
$commentsInfo = $postInfo->getCommentsCategory();
$response->setTemplate('news.post', [
$response->setContent(Template::renderRaw('news.post', [
'post_info' => $postInfo,
'comments_info' => $commentsInfo,
'comments_user' => User::getCurrent(),
]);
]));
}
private function createFeed(string $feedMode, ?NewsCategory $categoryInfo, array $posts): Feed {
@ -107,66 +115,54 @@ final class NewsHandler extends Handler {
return $feed;
}
public function feedIndexAtom(HttpResponse $response, HttpRequest $request) {
public function feedIndexAtom($response, $request) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(
self::createFeed('atom', null, NewsPost::all(new Pagination(10), true))
);
}
public function feedIndexRss(HttpResponse $response, HttpRequest $request) {
public function feedIndexRss($response, $request) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(
self::createFeed('rss', null, NewsPost::all(new Pagination(10), true))
);
}
public function feedCategoryAtom(HttpResponse $response, HttpRequest $request, int $categoryId) {
try {
$categoryInfo = NewsCategory::byId($categoryId);
} catch(NewsCategoryNotFoundException $ex) {
return 404;
}
public function feedCategoryAtom($response, $request, NewsCategory $categoryInfo) {
$response->setContentType('application/atom+xml; charset=utf-8');
return (new AtomFeedSerializer)->serializeFeed(
self::createFeed('atom', $categoryInfo, $categoryInfo->posts(new Pagination(10)))
);
}
public function feedCategoryRss(HttpResponse $response, HttpRequest $request, int $categoryId) {
try {
$categoryInfo = NewsCategory::byId($categoryId);
} catch(NewsCategoryNotFoundException $ex) {
return 404;
}
public function feedCategoryRss($response, $request, NewsCategory $categoryInfo) {
$response->setContentType('application/rss+xml; charset=utf-8');
return (new RssFeedSerializer)->serializeFeed(
self::createFeed('rss', $categoryInfo, $categoryInfo->posts(new Pagination(10)))
);
}
public function legacy(HttpResponse $response, HttpRequest $request) {
public function legacy($response, $request) {
$location = url('news-index');
switch('/' . trim($request->getUri()->getPath(), '/')) {
switch($request->getPath()) {
case '/news/index.php':
$location = url('news-index', [
'page' => $request->getQueryParam('page', FILTER_SANITIZE_NUMBER_INT),
'page' => $request->getParam('page', FILTER_SANITIZE_NUMBER_INT),
]);
break;
case '/news/category.php':
$location = url('news-category', [
'category' => $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT),
'page' => $request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT),
'category' => $request->getParam('c', FILTER_SANITIZE_NUMBER_INT),
'page' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]);
break;
case '/news/post.php':
$location = url('news-post', [
'post' => $request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT),
'post' => $request->getParam('p', FILTER_SANITIZE_NUMBER_INT),
]);
break;
@ -175,21 +171,21 @@ final class NewsHandler extends Handler {
case '/news/feed.php/rss':
case '/news/feed.php/atom':
$feedType = basename($request->getUri()->getPath());
$catId = $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT);
$feedType = basename($request->getPath());
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? "news-category-feed-{$feedType}" : "news-feed-{$feedType}", ['category' => $catId]);
break;
case '/news.php/rss':
case '/news.php/atom':
$feedType = basename($request->getUri()->getPath());
$feedType = basename($request->getPath());
case '/news.php':
$postId = $request->getQueryParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getQueryParam('p', FILTER_SANITIZE_NUMBER_INT);
$postId = $request->getParam('n', FILTER_SANITIZE_NUMBER_INT) ?? $request->getParam('p', FILTER_SANITIZE_NUMBER_INT);
if($postId > 0)
$location = url('news-post', ['post' => $postId]);
else {
$catId = $request->getQueryParam('c', FILTER_SANITIZE_NUMBER_INT);
$pageId = $request->getQueryParam('page', FILTER_SANITIZE_NUMBER_INT);
$catId = $request->getParam('c', FILTER_SANITIZE_NUMBER_INT);
$pageId = $request->getParam('page', FILTER_SANITIZE_NUMBER_INT);
$location = url($catId > 0 ? (isset($feedType) ? "news-category-feed-{$feedType}" : 'news-category') : (isset($feedType) ? "news-feed-{$feedType}" : 'news-index'), ['category' => $catId, 'page' => $pageId]);
}
break;

View file

@ -1,8 +1,6 @@
<?php
namespace Misuzu\Http\Handlers;
use HttpResponse;
use HttpRequest;
use Misuzu\AuthToken;
use Misuzu\Base64;
use Misuzu\Config;
@ -15,6 +13,7 @@ use Misuzu\Users\UserSession;
use Misuzu\Users\UserSessionNotFoundException;
use Misuzu\Users\UserWarning;
use Misuzu\Users\UserWarningCreationFailedException;
use Index\Http\HttpRequestBuilder;
final class SockChatHandler extends Handler {
private string $hashKey = 'woomy';
@ -56,40 +55,62 @@ final class SockChatHandler extends Handler {
parent::__construct();
}
public function phpFile(HttpResponse $response, HttpRequest $request) {
$query = $request->getQueryParams();
if(isset($query['emotes']))
public function phpFile($response, $request) {
if($request->hasParam('emotes'))
return $this->emotes($response, $request);
if(isset($query['bans']) && is_string($query['bans']))
return $this->bans($response, $request->setHeader('X-SharpChat-Signature', $query['bans']));
$reqb = new HttpRequestBuilder;
$reqb->setMethod($request->getMethod());
$reqb->setPath($request->getPath());
$reqb->setCookies($request->getCookies());
$body = $request->getParsedBody();
if(isset($body['bump'], $body['hash']) && is_string($body['bump']) && is_string($body['hash'])) {
$this->bump(
$response,
$request->setHeader('X-SharpChat-Signature', $body['hash'])
->setBody(Stream::create($body['bump']))
);
return;
foreach($request->getHeaders() as $header) {
$name = $header->getName();
foreach($header->getLines() as $line)
$reqb->addHeader($name, $line);
}
$source = isset($body['user_id']) ? $body : $query;
if($request->hasParam('bans')) {
$reqb->setHeader('X-SharpChat-Signature', $request->getParam('bans'));
return $this->bans($response, $reqb->toRequest());
}
if(isset($source['user_id'], $source['token'], $source['ip'], $source['hash'])
&& is_string($source['user_id']) && is_string($source['token'])
&& is_string($source['ip']) && is_string($source['hash']))
return $this->verify(
$response,
$request->setHeader('X-SharpChat-Signature', $source['hash'])
->setBody(Stream::create(json_encode([
'user_id' => $source['user_id'],
'token' => $source['token'],
'ip' => $source['ip'],
])))
);
$isForm = $request->isFormContent();
if($isForm) {
$content = $request->getContent();
if($content->hasParam('bump') && $content->hasParam('hash')) {
$reqb->setHeader('X-SharpChat-Signature', $content->getParam('hash'));
$reqb->setContent($content->getParam('bump'));
$this->bump($response, $reqb->toRequest());
return;
}
if($content->hasParam('user_id') && $content->hasParam('token')
&& $content->hasParam('ip') && $content->hasParam('hash')) {
$reqb->setHeader('X-SharpChat-Signature', $content->getParam('hash'));
$reqb->setContent(json_encode([
'user_id' => $content->getParam('user_id'),
'token' => $content->getParam('token'),
'ip' => $content->getParam('ip'),
]));
return $this->verify($response, $reqb->toRequest());
}
} else {
if($request->hasParam('user_id') && $request->hasParam('token')
&& $request->hasParam('ip') && $request->hasParam('hash')) {
$reqb->setHeader('X-SharpChat-Signature', $request->getParam('hash'));
$reqb->setContent(json_encode([
'user_id' => $request->getParam('user_id'),
'token' => $request->getParam('token'),
'ip' => $request->getParam('ip'),
]));
return $this->verify($response, $reqb->toRequest());
}
}
return $this->login($response, $request);
}
@ -109,9 +130,9 @@ final class SockChatHandler extends Handler {
return $perms;
}
public function emotes(HttpResponse $response, HttpRequest $request): array {
$response->setHeader('Access-Control-Allow-Origin', '*')
->setHeader('Access-Control-Allow-Methods', 'GET');
public function emotes($response, $request): array {
$response->setHeader('Access-Control-Allow-Origin', '*');
$response->setHeader('Access-Control-Allow-Methods', 'GET');
$raw = Emoticon::all();
$out = [];
@ -133,8 +154,8 @@ final class SockChatHandler extends Handler {
return $out;
}
public function bans(HttpResponse $response, HttpRequest $request): array {
$userHash = $request->getHeaderLine('X-SharpChat-Signature');
public function bans($response, $request): array {
$userHash = $request->hasHeader('X-SharpChat-Signature') ? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$realHash = hash_hmac('sha256', 'givemethebeans', $this->hashKey);
if(!hash_equals($realHash, $userHash))
@ -165,10 +186,10 @@ final class SockChatHandler extends Handler {
return $bans;
}
public function checkBan(HttpResponse $response, HttpRequest $request): array {
$userHash = $request->getHeaderLine('X-SharpChat-Signature');
$ipAddress = (string)$request->getQueryParam('a');
$userId = (int)$request->getQueryParam('u', FILTER_SANITIZE_NUMBER_INT);
public function checkBan($response, $request): array {
$userHash = $request->hasHeader('X-SharpChat-Signature') ? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$ipAddress = (string)$request->getParam('a');
$userId = (int)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT);
$realHash = hash_hmac('sha256', "check#{$ipAddress}#{$userId}", $this->hashKey);
if(!hash_equals($realHash, $userHash))
@ -193,13 +214,17 @@ final class SockChatHandler extends Handler {
return $response;
}
public function createBan(HttpResponse $response, HttpRequest $request): int {
$userHash = $request->getHeaderLine('X-SharpChat-Signature');
$userId = (int)$request->getBodyParam('u', FILTER_SANITIZE_NUMBER_INT);
$modId = (int)$request->getBodyParam('m', FILTER_SANITIZE_NUMBER_INT);
$duration = (int)$request->getBodyParam('d', FILTER_SANITIZE_NUMBER_INT);
$isPermanent = (int)$request->getBodyParam('p', FILTER_SANITIZE_NUMBER_INT);
$reason = (string)$request->getBodyParam('r');
public function createBan($response, $request): int {
if(!$request->isFormContent())
return 400;
$userHash = $request->hasHeader('X-SharpChat-Signature') ? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$content = $request->getContent();
$userId = (int)$content->getParam('u', FILTER_SANITIZE_NUMBER_INT);
$modId = (int)$content->getParam('m', FILTER_SANITIZE_NUMBER_INT);
$duration = (int)$content->getParam('d', FILTER_SANITIZE_NUMBER_INT);
$isPermanent = (int)$content->getParam('p', FILTER_SANITIZE_NUMBER_INT);
$reason = (string)$content->getParam('r');
$realHash = hash_hmac('sha256', "create#{$userId}#{$modId}#{$duration}#{$isPermanent}#{$reason}", $this->hashKey);
if(!hash_equals($realHash, $userHash))
@ -240,10 +265,10 @@ final class SockChatHandler extends Handler {
return 201;
}
public function removeBan(HttpResponse $response, HttpRequest $request): int {
$userHash = $request->getHeaderLine('X-SharpChat-Signature');
$type = (string)$request->getQueryParam('t');
$subject = (string)$request->getQueryParam('s');
public function removeBan($response, $request): int {
$userHash = $request->hasHeader('X-SharpChat-Signature') ? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$type = (string)$request->getParam('t');
$subject = (string)$request->getParam('s');
$realHash = hash_hmac('sha256', "remove#{$type}#{$subject}", $this->hashKey);
if(!hash_equals($realHash, $userHash))
@ -268,10 +293,9 @@ final class SockChatHandler extends Handler {
return 204;
}
public function login(HttpResponse $response, HttpRequest $request) {
public function login($response, $request) {
$currentUser = User::getCurrent();
$params = $request->getQueryParams();
$configKey = isset($params['legacy']) ? 'sockChat.chatPath.legacy' : 'sockChat.chatPath.normal';
$configKey = $request->hasParam('legacy') ? 'sockChat.chatPath.legacy' : 'sockChat.chatPath.normal';
$chatPath = Config::get($configKey, Config::TYPE_STR, '/');
$response->redirect(
@ -281,9 +305,12 @@ final class SockChatHandler extends Handler {
);
}
public function bump(HttpResponse $response, HttpRequest $request): void {
$userHash = $request->getHeaderLine('X-SharpChat-Signature');
$bumpString = (string)$request->getBody();
public function bump($response, $request) {
if(!$request->isStringContent())
return 400;
$userHash = $request->hasHeader('X-SharpChat-Signature') ? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$bumpString = (string)$request->getContent();
$realHash = hash_hmac('sha256', $bumpString, $this->hashKey);
if(!hash_equals($realHash, $userHash))
@ -300,13 +327,16 @@ final class SockChatHandler extends Handler {
} catch(UserNotFoundException $ex) {}
}
public function verify(HttpResponse $response, HttpRequest $request): array {
$userHash = $request->getHeaderLine('X-SharpChat-Signature');
public function verify($response, $request): array {
if(!$request->isStringContent())
return 400;
$userHash = $request->hasHeader('X-SharpChat-Signature') ? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
if(strlen($userHash) !== 64)
return ['success' => false, 'reason' => 'length'];
$authInfo = json_decode((string)$request->getBody());
$authInfo = json_decode((string)$request->getContent());
if(!isset($authInfo->user_id, $authInfo->token, $authInfo->ip))
return ['success' => false, 'reason' => 'data'];
@ -364,10 +394,10 @@ final class SockChatHandler extends Handler {
];
}
public function resolve(HttpResponse $response, HttpRequest $request): array {
$userHash = $request->getHeaderLine('X-SharpChat-Signature');
$method = (string)$request->getQueryParam('m');
$param = (string)$request->getQueryParam('p');
public function resolve($response, $request): array {
$userHash = $request->hasHeader('X-SharpChat-Signature') ? $request->getHeaderFirstLine('X-SharpChat-Signature') : '';
$method = (string)$request->getParam('m');
$param = (string)$request->getParam('p');
$realHash = hash_hmac('sha256', "resolve#{$method}#{$param}", $this->hashKey);
if(!hash_equals($realHash, $userHash))
@ -397,10 +427,10 @@ final class SockChatHandler extends Handler {
];
}
public function token(HttpResponse $response, HttpRequest $request) {
$host = $request->getHeaderLine('Host');
$origin = $request->getHeaderLine('Origin');
$originHost = strtolower(parse_url($origin, PHP_URL_HOST));
public function token($response, $request) {
$host = $request->hasHeader('Host') ? $request->getHeaderFirstLine('Host') : '';
$origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : '';
$originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');
if(!empty($originHost) && $originHost !== $host) {
$whitelist = Config::get('sockChat.origins', Config::TYPE_ARR, []);
@ -434,10 +464,10 @@ final class SockChatHandler extends Handler {
];
}
public function profileCheck(HttpResponse $response, HttpRequest $request) {
$host = $request->getHeaderLine('Host');
$origin = $request->getHeaderLine('Origin');
$originHost = strtolower(parse_url($origin, PHP_URL_HOST));
public function profileCheck($response, $request) {
$host = $request->hasHeader('Host') ? $request->getHeaderFirstLine('Host') : '';
$origin = $request->hasHeader('Origin') ? $request->getHeaderFirstLine('Origin') : '';
$originHost = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');
if(!empty($originHost) && $originHost !== $host) {
$whitelist = Config::get('sockChat.origins', Config::TYPE_ARR, []);
@ -457,8 +487,8 @@ final class SockChatHandler extends Handler {
if($request->getMethod() === 'OPTIONS')
return 204;
$userId = (int)$request->getQueryParam('u', FILTER_SANITIZE_NUMBER_INT);
$extendedInfo = $request->hasQueryParam('e');
$userId = (int)$request->getParam('u', FILTER_SANITIZE_NUMBER_INT);
$extendedInfo = $request->hasParam('e');
if($userId < 1)
$userInfo = User::getCurrent();

View file

@ -1,135 +0,0 @@
<?php
namespace Misuzu\Http\Headers;
use InvalidArgumentException;
use Misuzu\HasQualityInterface;
use Misuzu\MediaType;
class AcceptHttpHeaderChild implements HasQualityInterface {
private $value = '';
private $quality = null;
public function __construct($string) {
$parts = explode(';', $string);
$this->value = $parts[0];
if(isset($parts[1])) {
$split = explode('=', $parts[1], 2);
if($split[0] === 'q' && isset($split[1]))
$this->quality = max(min(round((float)filter_var($split[1], FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION), 2), 1), 0);
}
}
public function getValue(): string {
return $this->value;
}
public function getQuality(): float {
return $this->quality ?? 1.0;
}
public function __toString() {
$string = $this->value;
if($this->quality !== null)
$string .= ';q=' . $this->quality;
return $string;
}
}
class AcceptHttpHeader extends HttpHeader {
private $accepted = null;
private $rejected = null;
public function isMultiline(): bool {
return false;
}
private function objectType(): string {
if($this->getName() === 'Accept')
return MediaType::class;
return AcceptHttpHeaderChild::class;
}
private function splitAccept(string $line): array {
$split = explode(',', $line);
$types = [];
$objectType = $this->objectType();
foreach($split as $type) {
try {
$types[] = new $objectType(trim($type));
} catch(InvalidArgumentException $ex) {
// Just ignore invalid types
}
}
return $types;
}
public function set($line): void {
$this->accepted = $this->rejected = null;
if(is_string($line))
$this->lines = $this->splitAccept($line);
elseif(is_array($line)) { // please don't
$this->lines = [];
foreach($line as $item)
if($item instanceof HasQualityInterface)
$this->lines[] = $item;
} elseif($line instanceof HasQualityInterface)
$this->lines = [$line];
else
throw new InvalidArgumentException('$line must inherit HasQualityInterface or parseable as one.');
}
public function append($line): void {
$this->accepted = $this->rejected = null;
if(is_string($line))
$this->lines += $this->splitAccept($line);
elseif(is_array($line)) { // please don't
foreach($line as $item)
if($item instanceof HasQualityInterface && !in_array($item, $this->lines))
$this->lines[] = $item;
} elseif($line instanceof HasQualityInterface)
$this->lines[] = $line;
else
throw new InvalidArgumentException('$line must inherit HasQualityInterface or parseable as one.');
}
public function getAccepted(): array {
if($this->accepted !== null)
return $this->accepted;
$accepted = [];
foreach($this->getLines() as $line)
if($line->getQuality() > 0)
$accepted[] = $line;
usort($accepted, function($a, $b) {
return $b->getQuality() <=> $a->getQuality();
});
return $this->accepted = $accepted;
}
public function getRejected(): array {
if($this->rejected !== null)
return $this->rejected;
$rejected = [];
foreach($this->getLines() as $line)
if($line->getQuality() === 0.0)
$rejected[] = $line;
return $this->rejected = $rejected;
}
public function checkAcceptance($type): bool {
if(is_string($type))
try {
$objectType = $this->objectType();
$type = new $objectType($type);
} catch(InvalidArgumentException $ex) {}
if(!($type instanceof HasQualityInterface))
throw new InvalidArgumentException('$type must inherit HasQualityInterface or parseable as one.');
foreach($this->getRejected() as $reject)
if($reject->matchType($reject))
return false;
return true;
}
}

View file

@ -1,58 +0,0 @@
<?php
namespace Misuzu\Http\Headers;
// specially parsed headers should inherit this and provide their own specific things
class HttpHeader {
private $name = '';
protected $lines = [];
public function __construct(string $name, $line = null) {
$this->name = $name;
if($line !== null)
$this->set($line);
}
public function getName(): string {
return $this->name;
}
public function isMultiline(): bool {
return true;
}
public function getLines(): array {
return $this->lines;
}
// make sure you only call these two with shit that can be cast to string or you will implode
// expect InvalidArgumentError throws for subclasses
public function set($line): void {
$this->lines = [$line];
}
public function append($line): void {
$this->lines[] = $line;
}
public function getHeaders(): array {
$headers = [];
if($this->isMultiline()) {
foreach($this->getLines() as $line)
$headers[] = sprintf('%s: %s', $this->getName(), $line);
} else
$headers[] = sprintf('%s: %s', $this->getName(), implode(', ', $this->getLines()));
return $headers;
}
public static function create(string $name, $line = null): HttpHeader {
switch($name) {
case 'Accept':
case 'Accept-Charset':
case 'Accept-Language':
case 'Accept-Encoding':
case 'TE':
return new AcceptHttpHeader($name, $line);
default:
return new HttpHeader($name, $line);
}
}
}

View file

@ -1,115 +0,0 @@
<?php
namespace Misuzu\Http;
use InvalidArgumentException;
use Misuzu\Stream;
abstract class HttpMessage {
protected $version = '';
protected $headers = [];
protected $body = null;
public function __construct(array $headers = [], string $version = '1.1') {
$this->setProtocolVersion($version)->setHeaders($headers);
}
public function getProtocolVersion() {
return $this->version;
}
public function setProtocolVersion(string $version): self {
$this->version = $version;
return $this;
}
public function withProtocolVersion($version) {
return (clone $this)->setProtocolVersion($version);
}
public function getBody() {
return $this->body;
}
public function setBody(Stream $body): self {
$this->body = $body;
return $this;
}
public function withBody(Stream $stream) {
return (clone $this)->setBody($stream);
}
public function getHeaders() {
return $this->headers;
}
public function setHeaders(array $headers): self {
foreach($headers as $name => $value)
$this->setHeader($name, $value);
return $this;
}
public function getHeaderName(string $name, bool $nullOnNone = false): ?string {
$lowerName = strtolower($name);
foreach($this->headers as $headerName => $_)
if(strtolower($headerName) === $lowerName)
return $headerName;
return $nullOnNone ? null : $name;
}
public function hasHeader($name) {
return $this->getHeaderName($name, true) !== null;
}
public function getHeader($name) {
return $this->headers[$this->getHeaderName($name)] ?? [];
}
public function getHeaderLine($name) {
$header = $this->getHeader($name);
return implode(',', $header);
}
public function withHeader($name, $value) {
if(!is_string($name) || empty($name))
throw new InvalidArgumentException('Header name must be a string.');
return (clone $this)->setHeader($name, $value);
}
public function setHeader(string $name, $value): self {
if(!($isString = is_string($value)) && !is_array($value))
throw new InvalidArgumentException('Value must be of type string or array.');
$this->removeHeader($name);
$this->headers[$name] = $isString ? [$value] : $value;
return $this;
}
public function withAddedHeader($name, $value) {
if(!is_string($name) || empty($name))
throw new InvalidArgumentException('Header name must be a string.');
return (clone $this)->appendHeader($name, $value);
}
public function appendHeader(string $name, $value): self {
if(!($isString = is_string($value)) && !is_array($value))
throw new InvalidArgumentException('Value must be of type string or array.');
$existingName = $this->getHeaderName($name, true);
$value = $isString ? [$value] : $value;
if($existingName === null) {
$this->headers[$existingName] = $value;
} else {
$this->headers[$name] = array_merge($this->headers, $value);
}
return $this;
}
public function withoutHeader($name) {
if(!is_string($name) || empty($name))
throw new InvalidArgumentException('Header name must be a string.');
return (clone $this)->removeHeader($name);
}
public function removeHeader(string $name): self {
$name = $this->getHeaderName($name, true);
if($name !== null)
unset($this->headers[$name]);
return $this;
}
}

View file

@ -1,219 +0,0 @@
<?php
namespace Misuzu\Http;
use InvalidArgumentException;
use Misuzu\Stream;
use Misuzu\Uri;
class HttpRequestMessage extends HttpMessage {
private $method = null;
private $uri = null;
private $requestTarget = null;
private $server = [];
private $cookies = [];
private $query = [];
private $files = [];
private $parsedBody = null;
private $bodyStream = null;
public function __construct(
string $method,
Uri $uri,
array $serverParams = [],
array $headers = [],
array $queryParams = [],
array $uploadedFiles = [],
$parsedBody = null,
Stream $rawBody = null
) {
parent::__construct($headers);
$this->setMethod($method);
$this->setUri($uri);
$this->setServerParams($serverParams);
$this->setQueryParams($queryParams);
$this->setUploadedFiles($uploadedFiles);
$this->setParsedBody($parsedBody);
$this->setBody($rawBody);
}
public function getMethod() {
return $this->method;
}
private function setMethod(string $method): self {
if(empty($method))
throw new InvalidArgumentException('Invalid method name.');
$this->method = $method;
return $this;
}
public function getUri() {
return $this->uri;
}
private function setUri(Uri $uri, bool $preserveHost = false): self {
$this->uri = $uri;
if(!$preserveHost || !$this->hasHeader('Host'))
$this->applyHostHeader();
return $this;
}
private function applyHostHeader(): void {
$uri = $this->getUri();
if(empty($host = $uri->getHost()))
return;
if(($port = $uri->getPort()) !== null)
$host .= ":{$port}";
$headerName = $this->getHeaderName('Host');
$this->headers = [$headerName => [$host]] + $this->headers;
}
public function getRequestTarget() {
if($this->requestTarget !== null)
return $this->requestTarget;
$uri = $this->getUri();
$target = $uri->getPath();
$query = $uri->getQuery();
if(empty($target))
$target = '/';
if(!empty($query))
$target .= '?' . $query;
return $target;
}
private function setRequestTarget(?string $requestTarget): self {
if(preg_match('#\s#', $requestTarget))
throw new InvalidArgumentException('Request target may not contain spaces.');
$this->requestTarget = $requestTarget;
return $this;
}
public function getServerParams() {
return $this->server;
}
private function setServerParams(array $serverParams): self {
$this->server = $serverParams;
return $this;
}
public function getServerParam(string $name, int $filter = FILTER_DEFAULT, $options = null) {
if(!isset($this->server[$name]))
return null;
return filter_var($this->server[$name], $filter, $options);
}
public function getRemoteAddress(): string {
return $this->server['REMOTE_ADDR'] ?? '::1';
}
public function getCookieParams() {
return $this->cookies;
}
private function setCookieParams(array $cookies): self {
$this->cookies = $cookies;
return $this;
}
public function getCookieParam(string $name, int $filter = FILTER_DEFAULT, $options = 0) {
if(!isset($this->cookies[$name]))
return null;
return filter_var($this->cookies[$name], $filter, $options);
}
public function getQueryParams() {
return $this->query;
}
private function setQueryParams(array $query): self {
$this->query = $query;
return $this;
}
public function getQueryParam(string $name, int $filter = FILTER_DEFAULT, $options = 0) {
if(!isset($this->query[$name]))
return null;
return filter_var($this->query[$name], $filter, $options);
}
public function hasQueryParam(string $name): bool {
return isset($this->query[$name]);
}
public function getUploadedFiles() {
return $this->files;
}
private function setUploadedFiles(array $uploadedFiles): self {
$this->files = $uploadedFiles;
return $this;
}
public function getParsedBody() {
return $this->parsedBody;
}
private function setParsedBody($data): self {
if(!is_array($data) && !is_object($data) && $data !== null)
throw new InvalidArgumentException('Parsed body must by of type array, object or null.');
$this->parsedBody = $data;
return $this;
}
public function getBodyParam(string $name, int $filter = FILTER_DEFAULT, $options = 0) {
if($this->parsedBody === null)
return null;
$value = null;
if(is_object($this->parsedBody) && isset($this->parsedBody->{$name})) {
$value = $this->parsedBody->{$name};
} elseif(is_array($this->parsedBody) && isset($this->parsedBody[$name])) {
$value = $this->parsedBody[$name];
}
return filter_var($value, $filter, $options);
}
private static function getRequestHeaders(): array {
if(function_exists('getallheaders'))
return getallheaders();
$headers = [];
foreach($_SERVER as $key => $value) {
if(substr($key, 0, 5) === 'HTTP_') {
$key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
$reqHeaders[$key] = $value;
} elseif($key === 'CONTENT_TYPE') {
$reqHeaders['Content-Type'] = $value;
} elseif($key === 'CONTENT_LENGTH') {
$reqHeaders['Content-Length'] = $value;
}
}
if(empty($headers['Authorization'])) {
if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif(isset($_SERVER['PHP_AUTH_USER'])) {
$password = $_SERVER['PHP_AUTH_PW'] ?? '';
$headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $password);
} elseif(isset($_SERVER['PHP_AUTH_DIGEST'])) {
$headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
}
}
return $headers;
}
public static function fromGlobals(): HttpRequestMessage {
return new static(
$_SERVER['REQUEST_METHOD'],
new Uri('/' . trim($_SERVER['REQUEST_URI'] ?? '', '/')),
$_SERVER,
self::getRequestHeaders(),
$_GET,
UploadedFile::createFromFILES($_FILES),
$_POST,
Stream::createFromFile('php://input')
);
}
}

View file

@ -1,159 +0,0 @@
<?php
namespace Misuzu\Http;
use InvalidArgumentException;
use Misuzu\Template;
use Misuzu\Stream;
class HttpResponseMessage extends HttpMessage {
private $statusCode = 0;
private $reasonPhrase = '';
public function __construct(int $statusCode = 200, string $reasonPhrase = '', array $headers = [], string $version = '1.1') {
parent::__construct($headers, $version);
$this->setStatusCode($statusCode)->setReasonPhrase($reasonPhrase);
}
public function getStatusCode() {
return $this->statusCode;
}
public function setStatusCode(int $code): self {
if($code < 100 || $code > 599)
throw new InvalidArgumentException('Invalid status code.');
$this->statusCode = $code;
return $this;
}
public function getReasonPhrase() {
return !empty($this->reasonPhrase) ? $this->reasonPhrase : (
array_key_exists($statusCode = $this->getStatusCode(), self::PHRASES) ? self::PHRASES[$statusCode] : ''
);
}
public function setReasonPhrase(string $reasonPhrase): self {
$this->reasonPhrase = $reasonPhrase;
return $this;
}
public function getContentType(): string {
return $this->getHeaderLine('Content-Type');
}
public function setContentType(string $type): self {
$this->setHeader('Content-Type', $type);
return $this;
}
public function setText(string $text): self {
$body = $this->getBody();
if($body !== null)
$body->close();
$this->setBody(Stream::create($text));
return $this;
}
public function appendText(string $text): self {
$body = $this->getBody();
if($body === null)
$this->setBody(Stream::create($text));
else
$body->write($text);
return $this;
}
public function setHtml(string $content, bool $html = true): self {
if(empty($this->getContentType()))
$this->setContentType('text/html; charset=utf-8');
$this->setText($content);
return $this;
}
public function setTemplate(string $file, array $vars = []): self {
return $this->setHtml(Template::renderRaw($file, $vars));
}
public function setJson($content, int $options = 0): self {
$this->setContentType('application/json; charset=utf-8');
$this->setText(json_encode($content, $options));
return $this;
}
public function redirect(string $path, bool $permanent = false, bool $xhr = false): self {
$this->setStatusCode($permanent ? 301 : 302);
$this->setHeader($xhr ? 'X-Misuzu-Location' : 'Location', $path);
return $this;
}
public function getContents(): string {
return (string)($this->getBody() ?? '');
}
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
private const PHRASES = [
// 1xx: Informational
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
// 2xx: Success
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
// 3xx: Redirection
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
// 4xx: Client Error
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Payload Too Large',
414 => 'URI Too Long',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Too Early',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
451 => 'Unavailable For Legal Reasons',
// 5xx: Server Error
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required',
];
}

View file

@ -1,112 +0,0 @@
<?php
namespace Misuzu\Http\Routing;
use InvalidArgumentException;
use Misuzu\Http\HttpRequestMessage;
use Misuzu\Http\HttpResponseMessage;
class Route {
private $methods = [];
private $path = '';
private $children = [];
private $filters = [];
private $parentRoute = null;
private $handlerClass = null;
private $handlerMethod = null;
public function __construct(array $methods, string $path, ?string $method = null, ?string $class = null) {
$this->methods = array_map('strtoupper', $methods);
$this->path = $path;
$this->handlerClass = $class;
$this->handlerMethod = $method;
}
public static function create(array $methods, string $path, ?string $method = null, ?string $class = null): self {
return new Route($methods, $path, $method, $class);
}
public static function get(string $path, ?string $method = null, ?string $class = null): self {
return self::create(['GET'], $path, $method, $class);
}
public static function post(string $path, ?string $method = null, ?string $class = null): self {
return self::create(['POST'], $path, $method, $class);
}
public static function delete(string $path, ?string $method = null, ?string $class = null): self {
return self::create(['DELETE'], $path, $method, $class);
}
public static function group(string $path, ?string $class = null): self {
return self::create([''], $path, null, $class);
}
public function getHandlerClass(): string {
return $this->handlerClass ?? ($this->parentRoute === null ? '' : $this->parentRoute->getHandlerClass());
}
public function setHandlerClass(string $class): self {
$this->handlerClass = $class;
return $this;
}
public function getHandlerMethod() {
return $this->handlerMethod;
}
public function getParent(): ?self {
return $this->parentRoute;
}
public function setParent(self $route): self {
$this->parentRoute = $route;
return $this;
}
public function getPath(): string {
$path = $this->path;
if($this->parentRoute !== null)
$path = $this->parentRoute->getPath() . ($path[0] !== '.' ? '/' : '') . trim($path, '/');
return $path;
}
public function setPath(string $path): self {
$this->path = $path;
return $this;
}
public function addFilters(string ...$filters): self {
$this->filters = array_merge($this->filters, $filters);
return $this;
}
public function getFilters(): array {
$filters = $this->filters;
if($this->parentRoute !== null)
$filters += $this->parentRoute->getFilters();
return $filters;
}
public function getChildren(): array {
return $this->children;
}
public function addChildren(Route ...$routes): self {
foreach($routes as $route)
$this->children[] = $route->setParent($this);
return $this;
}
public function match(HttpRequestMessage $request, array &$matches): bool {
$matches = [];
if(!in_array($request->getMethod(), $this->methods))
return false;
return preg_match('#^' . $this->getPath() . '$#', '/' . trim($request->getUri()->getPath(), '/'), $matches) === 1;
}
public function __serialize(): array {
return [
$this->methods,
$this->getPath(),
$this->getFilters(),
$this->getHandlerClass(),
$this->getHandlerMethod(),
];
}
public function __unserialize(array $data): void {
[$this->methods, $this->path, $this->filters, $this->handlerClass, $this->handlerMethod] = $data;
}
}

View file

@ -1,127 +0,0 @@
<?php
namespace Misuzu\Http\Routing;
use Misuzu\Http\HttpRequestMessage;
use Misuzu\Http\HttpResponseMessage;
class Router {
private static $instance = null;
private $handlerFormat = '';
private $filterFormat = '';
private $routes = [];
public function __call(string $name, array $args) {
if($name[0] === '_')
return null;
return $this->{'_' . $name}(...$args);
}
public static function __callStatic(string $name, array $args) {
if($name[0] === '_')
return null;
if(self::$instance === null)
(new Router)->setInstance();
return self::$instance->{'_' . $name}(...$args);
}
public function setInstance(): self {
return self::$instance = $this;
}
public function _getHandlerFormat(): string {
return $this->handlerFormat;
}
public function _setHandlerFormat(string $format): self {
$this->handlerFormat = $format;
return $this;
}
public function _getFilterFormat(): string {
return $this->filterFormat;
}
public function _setFilterFormat(string $format): self {
$this->filterFormat = $format;
return $this;
}
public function _addRoutes(Route ...$routes): self {
foreach($routes as $route) {
$this->routes[] = $route;
$this->addRoutes(...$route->getChildren());
}
return $this;
}
// Unused, might be useful for the future to immediately smash the routes list together
// without having to propagate children.
// Should obviously not be in debug mode and should also be nuked after a pull on stable.
public function _getData(): string {
return serialize([$this->routes]);
}
public function _setData(string $data): void {
[$this->routes] = unserialize($data);
}
private function match(HttpRequestMessage $request, array &$matches): ?Route {
foreach($this->routes as $route)
if($route->match($request, $matches))
return $route;
return null;
}
public function _handle(HttpRequestMessage $request): HttpResponseMessage {
$matches = [];
$route = $this->match($request, $matches);
array_shift($matches);
if($route === null)
return new HttpResponseMessage(404);
foreach($route->getFilters() as $filter) {
if(!class_exists($filter))
$filter = sprintf($this->_getFilterFormat(), $filter);
$response = (new $filter)->process($request, $this);
if($response !== null)
return $response;
}
$response = new HttpResponseMessage(200);
$result = null;
array_unshift($matches, $response, $request);
$handlerMethod = $route->getHandlerMethod();
if(is_callable($handlerMethod)) {
$result = call_user_func_array($handlerMethod, $matches);
} elseif($handlerMethod[0] === '/') {
$response->redirect($handlerMethod);
} else {
$handlerClass = $route->getHandlerClass();
if(!empty($handlerClass)) {
if(!class_exists($handlerClass))
$handlerClass = sprintf($this->_getHandlerFormat(), $handlerClass);
$handlerClass = new $handlerClass($response, $request);
$result = $handlerClass->{$handlerMethod}(...$matches);
}
}
if($result !== null) {
$resultType = gettype($result);
switch($resultType) {
case 'array':
case 'object':
$response->setJson($result);
break;
case 'integer':
$response->setStatusCode($result);
break;
default:
$response->setHtml($result);
break;
}
}
return $response;
}
}

View file

@ -1,174 +0,0 @@
<?php
namespace Misuzu\Http;
use InvalidArgumentException;
use RuntimeException;
use Misuzu\Stream;
class UploadedFile {
private $stream = null;
private $fileName = null;
private $size = null;
private $error = 0;
private $clientFileName = null;
private $clientMimeType = null;
private $hasMoved = false;
public function __construct(
$fileNameOrStream,
int $error,
?int $size = null,
?string $clientName = null,
?string $clientType = null
) {
$this->error = $error;
$this->size = $size;
$this->clientFileName = $clientName;
$this->clientMimeType = $clientType;
if($error === UPLOAD_ERR_OK) {
if($fileNameOrStream === null)
throw new InvalidArgumentException('No stream or filename provided.');
if(is_string($fileNameOrStream))
$this->fileName = $fileNameOrStream;
elseif(is_resource($fileNameOrStream))
$this->stream = Stream::create($fileNameOrStream);
elseif($fileNameOrStream instanceof Stream)
$this->stream = $fileNameOrStream;
if($size === null && $this->stream !== null)
$this->size = $this->stream->getSize();
}
}
public function getStream() {
if($this->getError() !== UPLOAD_ERR_OK)
throw new RuntimeException('Can\'t open stream because of an upload error.');
if($this->hasMoved)
throw new RuntimeException('Can\'t open stream because file has already been moved.');
if($this->stream === null)
$this->stream = Stream::createFromFile($this->fileName);
return $this->stream;
}
public function moveTo($targetPath) {
if($this->getError() !== UPLOAD_ERR_OK)
throw new RuntimeException('Can\'t move file because of an upload error.');
if($this->hasMoved)
throw new RuntimeException('This uploaded file has already been moved.');
if(!is_string($targetPath) || empty($targetPath))
throw new InvalidArgumentException('$targetPath is not a valid path.');
if($this->fileName !== null) {
$this->hasMoved = PHP_SAPI === 'CLI'
? rename($this->fileName, $targetPath)
: move_uploaded_file($this->fileName, $targetPath);
} else {
$stream = $this->getStream();
if($stream->isSeekable())
$stream->rewind();
$target = Stream::createFromFile($targetPath, 'wb');
while(!$stream->eof() && $target->write($stream->read(0x100000)) > 0);
$this->hasMoved = true;
}
if(!$this->hasMoved)
throw new RuntimeException('Failed to move file to ' . $targetPath);
}
public function getSize() {
return $this->size;
}
public function getError() {
return $this->error;
}
public function getClientFilename() {
return $this->clientFileName;
}
public function getClientMediaType() {
return $this->clientMimeType;
}
public static function createFromFILE(array $file): self {
return new UploadedFile(
$file['tmp_name'] ?? '',
$file['error'] ?? UPLOAD_ERR_NO_FILE,
$file['size'] ?? null,
$file['name'] ?? null,
$file['type'] ?? null
);
}
private static function traverseFILES(array $files, string $keyName): array {
$arr = [];
foreach($files as $key => $val) {
$key = "_{$key}";
if(is_array($val)) {
$arr[$key] = self::traverseFILES($val, $keyName);
} else {
$arr[$key][$keyName] = $val;
}
}
return $arr;
}
private static function normalizeFILES(array $files): array {
$out = [];
foreach($files as $key => $arr) {
if(empty($arr))
continue;
$key = '_' . $key;
if(is_int($arr['error'])) {
$out[$key] = $arr;
continue;
}
if(is_array($arr['error'])) {
$keys = array_keys($arr);
foreach($keys as $keyName) {
$out[$key] = array_merge_recursive($out[$key] ?? [], self::traverseFILES($arr[$keyName], $keyName));
}
continue;
}
}
return $out;
}
private static function createObjectInstances(array $files): array {
$coll = [];
foreach($files as $key => $val) {
$key = substr($key, 1);
if(isset($val['error'])) {
$coll[$key] = self::createFromFILE($val);
} else {
$coll[$key] = self::createObjectInstances($val);
}
}
return $coll;
}
public static function createFromFILES(array $files): array {
if(empty($files))
return [];
return self::createObjectInstances(self::normalizeFILES($files));
}
}

View file

@ -3,6 +3,8 @@ namespace Misuzu;
use InvalidArgumentException;
// get rid of this garbage as soon as possible
class Memoizer {
private $collection = [];

View file

@ -1,193 +0,0 @@
<?php
namespace Misuzu;
use Exception;
use InvalidArgumentException;
use RuntimeException;
class Stream {
private $stream = null;
private $metaData = [];
private $seekable = false;
private $readable = false;
private $writable = false;
private $uri = null;
private $size = null;
private const READABLE = [
'r', 'rb', 'r+', 'r+b', 'w+', 'w+b',
'a+', 'a+b', 'x+', 'x+b', 'c+', 'c+b',
];
private const WRITABLE = [
'r+', 'r+b', 'w', 'wb', 'w+', 'w+b',
'a', 'ab', 'a+', 'a+b', 'x', 'xb',
'x+', 'x+b', 'c', 'cb', 'c+', 'c+b',
];
public function __construct($resource) {
if(is_string($resource)) {
$mem = fopen('php://temp', 'rb+');
fwrite($mem, $resource);
$resource = $mem;
}
if(!is_resource($resource))
throw new InvalidArgumentException('Provided argument is not valid.');
$this->stream = $resource;
$metaData = $this->getMetadata();
$this->uri = $metaData['uri'] ?? null;
$this->readable = in_array($metaData['mode'], self::READABLE);
$this->writable = in_array($metaData['mode'], self::WRITABLE);
$this->seekable = $metaData['seekable'] && fseek($this->stream, 0, SEEK_CUR) === 0;
}
public static function create($contents = ''): Stream {
if($contents instanceof Stream)
return $contents;
return new Stream($contents);
}
public static function createFromFile(string $filename, string $mode = 'rb'): Stream {
if(!in_array($mode[0], ['r', 'w', 'a', 'x', 'c']))
throw new InvalidArgumentException("Provided mode ({$mode}) is invalid.");
$file = @fopen($filename, $mode);
if($file === false)
throw new RuntimeException("Wasn't able to open '{$filename}'.");
return self::create($file);
}
public function getMetadata($key = null) {
$hasKey = $key !== null;
if(!isset($this->stream))
return $hasKey ? null : [];
$metaData = stream_get_meta_data($this->stream);
if(!$hasKey)
return $metaData;
return $metaData[$key] ?? null;
}
public function isReadable(): bool {
return $this->readable;
}
public function read($length): string {
if(!$this->isReadable())
throw new RuntimeException('Can\'t read from this stream.');
return fread($this->stream, $length);
}
public function getContents(): string {
if(!isset($this->stream))
throw new RuntimeException('Can\'t read contents of stream.');
if(($contents = stream_get_contents($this->stream)) === false)
throw new RuntimeException('Failed to read contents of stream.');
return $contents;
}
public function isWritable(): bool {
return $this->writable;
}
public function write($string): int {
if(!$this->isWritable())
throw new RuntimeException('Can\'t write to this stream.');
$this->size = null;
if(($count = fwrite($this->stream, $string)) === false)
throw new RuntimeException('Failed to write to this stream.');
return $count;
}
public function isSeekable(): bool {
return $this->seekable;
}
public function seek($offset, $whence = SEEK_SET): void {
if(!$this->isSeekable())
throw new RuntimeException('Can\'t seek in this stream.');
if(fseek($this->stream, $offset, $whence) === -1)
throw new RuntimeException("Failed to seek to position {$offset} ({$whence}).");
}
public function rewind(): void {
$this->seek(0);
}
public function tell(): int {
if(!isset($this->stream))
throw new RuntimeException('Can\'t determine the position of a detached stream.');
if(($pos = ftell($this->stream)) === false)
throw new RuntimeException('Can\'t tell position in stream.');
return $pos;
}
public function eof(): bool {
return !isset($this->stream) || feof($this->stream);
}
public function getSize(): ?int {
if($this->size !== null)
return $this->size;
if(!isset($this->stream))
return null;
if(!empty($this->uri))
clearstatcache($this->uri);
$stats = fstat($this->stream);
if(isset($stats['size']))
return $this->size = $stats['size'];
return null;
}
public function detach() {
if(!isset($this->stream))
return null;
$stream = $this->stream;
$this->stream = $this->size = $this->uri = null;
$this->readable = $this->writable = $this->seekable = false;
return $stream;
}
public function close(): void {
$stream = $this->detach();
if(is_resource($stream))
fclose($stream);
}
public function __toString() {
try {
if($this->isSeekable())
$this->rewind();
return $this->getContents();
} catch(Exception $ex) {
return '';
}
}
public function __destruct() {
$this->close();
}
}

View file

@ -6,6 +6,7 @@ use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\Environment as TwigEnvironment;
use Misuzu\Parsers\Parser;
use Index\Environment;
final class TwigMisuzu extends AbstractExtension {
public function getFilters() {
@ -35,6 +36,7 @@ final class TwigMisuzu extends AbstractExtension {
new TwigFunction('git_branch', fn() => GitInfo::branch()),
new TwigFunction('startup_time', fn(float $time = MSZ_STARTUP) => microtime(true) - $time),
new TwigFunction('sql_query_count', fn() => DB::queries()),
new TwigFunction('ndx_version', fn() => Environment::getIndexVersion()),
];
}

View file

@ -1,149 +0,0 @@
<?php
namespace Misuzu;
use InvalidArgumentException;
class Uri {
private $scheme = '';
private $user = '';
private $password = '';
private $host = '';
private $port = null;
private $path = '';
private $query = '';
private $fragment = '';
private $originalString = '';
public function __construct(string $uriString = '') {
$this->originalString = $uriString;
if(!empty($uriString)) {
$uri = parse_url($uriString);
if($uri === false)
throw new InvalidArgumentException('URI cannot be parsed.');
$this->setScheme($uri['scheme'] ?? '');
$this->setUserInfo($uri['user'] ?? '', $uri['pass'] ?? null);
$this->setHost($uri['host'] ?? '');
$this->setPort($uri['port'] ?? null);
$this->setPath($uri['path'] ?? '');
$this->setQuery($uri['query'] ?? '');
$this->setFragment($uri['fragment'] ?? '');
}
}
public function getOriginalString(): string {
return $this->originalString;
}
public function getScheme() {
return $this->scheme;
}
public function setScheme(string $scheme): self {
$this->scheme = $scheme;
return $this;
}
public function getAuthority() {
$authority = '';
if(!empty($userInfo = $this->getUserInfo()))
$authority .= $userInfo . '@';
$authority .= $this->getHost();
if(($port = $this->getPort()) !== null)
$authority .= ':' . $port;
return $authority;
}
public function getUserInfo() {
$userInfo = $this->user;
if(!empty($this->password))
$userInfo .= ':' . $this->password;
return $userInfo;
}
public function setUserInfo(string $user, ?string $password = null): self {
$this->user = $user;
$this->password = $password;
return $this;
}
public function getHost() {
return $this->host;
}
public function setHost(string $host): self {
$this->host = $host;
return $this;
}
public function getPort() {
return $this->port;
}
public function setPort(?int $port): self {
if($port !== null && ($port < 1 || $port > 0xFFFF))
throw new InvalidArgumentException('Invalid port.');
$this->port = $port;
return $this;
}
public function getPath() {
return $this->path;
}
public function setPath(string $path): self {
$this->path = $path;
return $this;
}
public function getQuery() {
return $this->query;
}
public function setQuery(string $query): self {
$this->query = $query;
return $this;
}
public function getFragment() {
return $this->fragment;
}
public function setFragment(string $fragment): self {
$this->fragment = $fragment;
return $this;
}
public function __toString() {
$string = '';
if(!empty($scheme = $this->getScheme()))
$string .= $scheme . ':';
$authority = $this->getAuthority();
$hasAuthority = !empty($authority);
if($hasAuthority)
$string .= '//' . $authority;
$path = $this->getPath();
$hasPath = !empty($path);
if($hasAuthority && (!$hasPath || $path[0] !== '/'))
$string .= '/';
elseif(!$hasAuthority && $path[1] === '/')
$path = '/' . trim($path, '/');
$string .= $path;
if(!empty($query = $this->getQuery()))
$string .= '?' . $query;
if(!empty($fragment = $this->getFragment()))
$string .= '#' . $fragment;
return $string;
}
}

View file

@ -14,6 +14,7 @@
{% endif %}
# <a href="https://github.com/flashwave/misuzu/commit/{{ git_commit_hash(true) }}" target="_blank" rel="noreferrer noopener" class="footer__link">{{ git_commit_hash() }}</a>
{% if constant('MSZ_DEBUG') or current_user.super|default(false) %}
/ Index {{ ndx_version() }}
/ SQL Queries: {{ sql_query_count()|number_format }}
/ Took: {{ startup_time()|number_format(5) }} seconds
/ Load: {{ (startup_time() - startup_time(constant('MSZ_TPL_RENDER')))|number_format(5) }} seconds

View file

@ -1,6 +1,9 @@
{% extends 'master.twig' %}
{% from 'macros.twig' import container_title %}
{# fuck it #}
{% set http_code = http_code|default(error_code) %}
{% block content %}
<div class="container">
{{ container_title((title|default(error_code|default(http_code) >= 400 ? '<i class="fas fa-exclamation-circle fa-fw"></i> Error' : '<i class="fas fa-info-circle fa-fw"></i> Information')) ~ ' ' ~ (error_code|default(http_code >= 400 ? http_code : '')) ~ (error_text is defined ? ' - ' ~ error_text : '')) }}

View file

@ -16,9 +16,14 @@
<h2>Misuzu Project</h2>
<ul>
<li><a href="{{ url('info', {'title': 'misuzu'}) }}">Read me</a></li>
<li><a href="{{ url('info', {'title': 'misuzu/license'}) }}">License</a></li>
<li><a href="{{ url('info', {'title': 'misuzu/license'}) }}">Licence</a></li>
<li><a href="{{ url('info', {'title': 'misuzu/code_of_conduct'}) }}">Code of Conduct</a></li>
</ul>
<h2>Index Project</h2>
<ul>
<li><a href="{{ url('info', {'title': 'index'}) }}">Read me</a></li>
<li><a href="{{ url('info', {'title': 'index/licence'}) }}">Licence</a></li>
</ul>
</div>
</div>
{% endblock %}