First bits of Indexification.
This commit is contained in:
parent
21526491c6
commit
881426e950
30 changed files with 381 additions and 1823 deletions
.gitmodulesmisuzu.phpMemoizer.phpStream.phpTwigMisuzu.phpUri.php
public
src
Http
Filters
Handlers
AssetsHandler.phpAuthHandler.phpChangelogHandler.phpForumHandler.phpHomeHandler.phpInfoHandler.phpNewsHandler.phpSockChatHandler.php
Headers
HttpMessage.phpHttpRequestMessage.phpHttpResponseMessage.phpRouting
UploadedFile.phptemplates
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "lib/index"]
|
||||
path = lib/index
|
||||
url = git@github.com:flashwave/index.git
|
27
misuzu.php
27
misuzu.php
|
@ -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';
|
||||
|
|
195
public/index.php
195
public/index.php
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ namespace Misuzu;
|
|||
|
||||
use InvalidArgumentException;
|
||||
|
||||
// get rid of this garbage as soon as possible
|
||||
|
||||
class Memoizer {
|
||||
private $collection = [];
|
||||
|
||||
|
|
193
src/Stream.php
193
src/Stream.php
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
149
src/Uri.php
149
src/Uri.php
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 : '')) }}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue