Router beginnings.
This commit is contained in:
parent
4512040362
commit
364fa3c24f
23 changed files with 1664 additions and 202 deletions
|
@ -13,6 +13,7 @@
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
|
"ext-readline": "*",
|
||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"twig/twig": "^2.0",
|
"twig/twig": "^2.0",
|
||||||
|
@ -22,7 +23,9 @@
|
||||||
"twig/extensions": "^1.5",
|
"twig/extensions": "^1.5",
|
||||||
"filp/whoops": "^2.2",
|
"filp/whoops": "^2.2",
|
||||||
"jublonet/codebird-php": "^3.1",
|
"jublonet/codebird-php": "^3.1",
|
||||||
"chillerlan/php-qrcode": "^3.0"
|
"chillerlan/php-qrcode": "^3.0",
|
||||||
|
"psr/http-message": "^1.0",
|
||||||
|
"psr/http-server-handler": "^1.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
|
|
106
composer.lock
generated
106
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "400b588939ef5651adf2ca9ca06dd090",
|
"content-hash": "0649c35f94ec31b6b23ad77ce1fd744f",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "chillerlan/php-qrcode",
|
"name": "chillerlan/php-qrcode",
|
||||||
|
@ -742,6 +742,109 @@
|
||||||
"homepage": "https://github.com/maxmind/web-service-common-php",
|
"homepage": "https://github.com/maxmind/web-service-common-php",
|
||||||
"time": "2019-12-12T15:56:05+00:00"
|
"time": "2019-12-12T15:56:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-message",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-message.git",
|
||||||
|
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||||
|
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.3.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "http://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP messages",
|
||||||
|
"homepage": "https://github.com/php-fig/http-message",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-message",
|
||||||
|
"psr",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"time": "2016-08-06T14:39:51+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "psr/http-server-handler",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-server-handler.git",
|
||||||
|
"reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
|
||||||
|
"reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.0",
|
||||||
|
"psr/http-message": "^1.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Server\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "http://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP server-side request handler",
|
||||||
|
"keywords": [
|
||||||
|
"handler",
|
||||||
|
"http",
|
||||||
|
"http-interop",
|
||||||
|
"psr",
|
||||||
|
"psr-15",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response",
|
||||||
|
"server"
|
||||||
|
],
|
||||||
|
"time": "2018-10-30T16:46:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/log",
|
"name": "psr/log",
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
|
@ -1280,6 +1383,7 @@
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
|
"ext-readline": "*",
|
||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"ext-zip": "*"
|
"ext-zip": "*"
|
||||||
},
|
},
|
||||||
|
|
17
misuzu.php
17
misuzu.php
|
@ -26,6 +26,9 @@ set_include_path(get_include_path() . PATH_SEPARATOR . MSZ_ROOT);
|
||||||
|
|
||||||
require_once 'vendor/autoload.php';
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
|
class_alias(\Misuzu\Http\HttpServerRequestMessage::class, '\Misuzu\Http\Handlers\Request');
|
||||||
|
class_alias(\Misuzu\Http\Routing\RouterResponseMessage::class, '\Misuzu\Http\Handlers\Response');
|
||||||
|
|
||||||
$errorHandler = new \Whoops\Run;
|
$errorHandler = new \Whoops\Run;
|
||||||
$errorHandler->pushHandler(
|
$errorHandler->pushHandler(
|
||||||
MSZ_CLI
|
MSZ_CLI
|
||||||
|
@ -74,18 +77,7 @@ if(empty($dbConfig)) {
|
||||||
|
|
||||||
$dbConfig = $dbConfig['Database'] ?? $dbConfig['Database.mysql-main'] ?? [];
|
$dbConfig = $dbConfig['Database'] ?? $dbConfig['Database.mysql-main'] ?? [];
|
||||||
|
|
||||||
DB::init(DB::buildDSN($dbConfig), $dbConfig['username'] ?? '', $dbConfig['password'] ?? '', [
|
DB::init(DB::buildDSN($dbConfig), $dbConfig['username'] ?? '', $dbConfig['password'] ?? '', DB::ATTRS);
|
||||||
PDO::ATTR_CASE => PDO::CASE_NATURAL,
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
|
|
||||||
PDO::ATTR_STRINGIFY_FETCHES => false,
|
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
PDO::MYSQL_ATTR_INIT_COMMAND => "
|
|
||||||
SET SESSION
|
|
||||||
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
|
|
||||||
time_zone = '+00:00';
|
|
||||||
",
|
|
||||||
]);
|
|
||||||
|
|
||||||
Config::init();
|
Config::init();
|
||||||
Mailer::init(Config::get('mail.method', Config::TYPE_STR), [
|
Mailer::init(Config::get('mail.method', Config::TYPE_STR), [
|
||||||
|
@ -407,7 +399,6 @@ MIG;
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove this block at the start of April, 2 months is plenty for this to propagate
|
|
||||||
if(!empty($_COOKIE['msz_uid']) && !empty($_COOKIE['msz_sid'])
|
if(!empty($_COOKIE['msz_uid']) && !empty($_COOKIE['msz_sid'])
|
||||||
&& ctype_digit($_COOKIE['msz_uid']) && ctype_xdigit($_COOKIE['msz_sid'])
|
&& ctype_digit($_COOKIE['msz_uid']) && ctype_xdigit($_COOKIE['msz_sid'])
|
||||||
&& strlen($_COOKIE['msz_sid']) === 64) {
|
&& strlen($_COOKIE['msz_sid']) === 64) {
|
||||||
|
|
9
msz.php
Normal file
9
msz.php
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/misuzu.php';
|
||||||
|
|
||||||
|
if(!MSZ_CLI)
|
||||||
|
die('This tool is meant to be used through command line only.' . PHP_EOL);
|
||||||
|
|
||||||
|
// todo: make improved command line interface and remove the things in misuzu.php
|
|
@ -1,29 +1,2 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
require_once __DIR__ . '/index.php';
|
||||||
|
|
||||||
require_once '../misuzu.php';
|
|
||||||
|
|
||||||
$mode = !empty($_GET['m']) && is_string($_GET['m']) ? $_GET['m'] : '';
|
|
||||||
|
|
||||||
switch($mode) {
|
|
||||||
case 'logout':
|
|
||||||
Template::render('auth.logout');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'reset':
|
|
||||||
url_redirect('auth-reset');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'forgot':
|
|
||||||
url_redirect('auth-forgot');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'login':
|
|
||||||
default:
|
|
||||||
url_redirect('auth-login');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'register':
|
|
||||||
url_redirect('auth-register');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
135
public/index.php
135
public/index.php
|
@ -1,103 +1,48 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
|
use Misuzu\Http\HttpServerRequestMessage;
|
||||||
|
use Misuzu\Http\Handlers\Handler;
|
||||||
|
use Misuzu\Http\Routing\Router;
|
||||||
|
use Misuzu\Http\Routing\Route;
|
||||||
|
|
||||||
require_once '../misuzu.php';
|
require_once '../misuzu.php';
|
||||||
|
|
||||||
$requestPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
$request = HttpServerRequestMessage::fromGlobals();
|
||||||
|
|
||||||
if($requestPath !== '/' && $requestPath !== $_SERVER['PHP_SELF']) {
|
$router = new Router;
|
||||||
echo render_error(404);
|
$router->setInstance();
|
||||||
return;
|
$router->addRoutes(
|
||||||
|
// Home
|
||||||
|
Route::get('/', Handler::call('index@HomeHandler')),
|
||||||
|
|
||||||
|
// Info
|
||||||
|
Route::get('/info', Handler::call('index@InfoHandler')),
|
||||||
|
Route::get('/info/([A-Za-z0-9_/]+)', true, Handler::call('page@InfoHandler')),
|
||||||
|
|
||||||
|
// Redirects
|
||||||
|
Route::get('/index.php', Handler::redirect(url('index'), true)),
|
||||||
|
Route::get('/info.php', Handler::redirect(url('info'), true)),
|
||||||
|
Route::get('/info.php/([A-Za-z0-9_/]+)', true, Handler::call('redir@InfoHandler')),
|
||||||
|
Route::get('/auth.php', Handler::call('legacy@AuthHandler'))
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $router->handle($request);
|
||||||
|
$response->setHeader('X-Powered-By', 'Misuzu');
|
||||||
|
|
||||||
|
$responseStatus = $response->getStatusCode();
|
||||||
|
|
||||||
|
header('HTTP/' . $response->getProtocolVersion() . ' ' . $responseStatus . ' ' . $response->getReasonPhrase());
|
||||||
|
|
||||||
|
foreach($response->getHeaders() as $headerName => $headerSet)
|
||||||
|
foreach($headerSet as $headerLine)
|
||||||
|
header("{$headerName}: {$headerLine}");
|
||||||
|
|
||||||
|
$responseBody = $response->getBody();
|
||||||
|
|
||||||
|
if($responseStatus >= 400 && $responseStatus <= 599 && ($responseBody === null || $responseBody->getSize() < 1)) {
|
||||||
|
echo render_error($responseStatus);
|
||||||
|
} else {
|
||||||
|
echo (string)$responseBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Config::get('social.embed_linked', Config::TYPE_BOOL)) {
|
|
||||||
Template::set('linked_data', [
|
|
||||||
'name' => Config::get('site.name', Config::TYPE_STR, 'Misuzu'),
|
|
||||||
'url' => Config::get('site.url', Config::TYPE_STR),
|
|
||||||
'logo' => Config::get('site.ext_logo', Config::TYPE_STR),
|
|
||||||
'same_as' => Config::get('social.linked', Config::TYPE_ARR),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$news = news_posts_get(0, 5, null, true);
|
|
||||||
|
|
||||||
$stats = DB::query('
|
|
||||||
SELECT
|
|
||||||
(
|
|
||||||
SELECT COUNT(`user_id`)
|
|
||||||
FROM `msz_users`
|
|
||||||
WHERE `user_deleted` IS NULL
|
|
||||||
) AS `count_users_all`,
|
|
||||||
(
|
|
||||||
SELECT COUNT(`user_id`)
|
|
||||||
FROM `msz_users`
|
|
||||||
WHERE `user_active` >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
|
||||||
) AS `count_users_online`,
|
|
||||||
(
|
|
||||||
SELECT COUNT(`user_id`)
|
|
||||||
FROM `msz_users`
|
|
||||||
WHERE `user_active` >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
|
||||||
) AS `count_users_active`,
|
|
||||||
(
|
|
||||||
SELECT COUNT(`comment_id`)
|
|
||||||
FROM `msz_comments_posts`
|
|
||||||
WHERE `comment_deleted` IS NULL
|
|
||||||
) AS `count_comments`,
|
|
||||||
(
|
|
||||||
SELECT COUNT(`topic_id`)
|
|
||||||
FROM `msz_forum_topics`
|
|
||||||
WHERE `topic_deleted` IS NULL
|
|
||||||
) AS `count_forum_topics`,
|
|
||||||
(
|
|
||||||
SELECT COUNT(`post_id`)
|
|
||||||
FROM `msz_forum_posts`
|
|
||||||
WHERE `post_deleted` IS NULL
|
|
||||||
) AS `count_forum_posts`
|
|
||||||
')->fetch();
|
|
||||||
|
|
||||||
$changelog = DB::query('
|
|
||||||
SELECT
|
|
||||||
`change_id`, `change_log`, `change_action`,
|
|
||||||
DATE(`change_created`) AS `change_date`,
|
|
||||||
!ISNULL(`change_text`) AS `change_has_text`
|
|
||||||
FROM `msz_changelog_changes`
|
|
||||||
ORDER BY `change_created` DESC
|
|
||||||
LIMIT 10
|
|
||||||
')->fetchAll();
|
|
||||||
|
|
||||||
$birthdays = user_session_active() ? user_get_birthdays() : [];
|
|
||||||
|
|
||||||
$latestUser = DB::query('
|
|
||||||
SELECT
|
|
||||||
u.`user_id`, u.`username`, u.`user_created`,
|
|
||||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
|
||||||
FROM `msz_users` as u
|
|
||||||
LEFT JOIN `msz_roles` as r
|
|
||||||
ON r.`role_id` = u.`display_role`
|
|
||||||
WHERE `user_deleted` IS NULL
|
|
||||||
ORDER BY u.`user_id` DESC
|
|
||||||
LIMIT 1
|
|
||||||
')->fetch();
|
|
||||||
|
|
||||||
$onlineUsers = DB::query('
|
|
||||||
SELECT
|
|
||||||
u.`user_id`, u.`username`,
|
|
||||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
|
||||||
FROM `msz_users` as u
|
|
||||||
LEFT JOIN `msz_roles` as r
|
|
||||||
ON r.`role_id` = u.`display_role`
|
|
||||||
WHERE u.`user_active` >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
|
||||||
ORDER BY u.`user_active` DESC
|
|
||||||
LIMIT 104
|
|
||||||
')->fetchAll();
|
|
||||||
|
|
||||||
Template::set([
|
|
||||||
'statistics' => $stats,
|
|
||||||
'latest_user' => $latestUser,
|
|
||||||
'online_users' => $onlineUsers,
|
|
||||||
'birthdays' => $birthdays,
|
|
||||||
'featured_changelog' => $changelog,
|
|
||||||
'featured_news' => $news,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Template::render('home.landing');
|
|
||||||
|
|
|
@ -1,64 +1,2 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
require_once __DIR__ . '/index.php';
|
||||||
|
|
||||||
require_once '../misuzu.php';
|
|
||||||
|
|
||||||
$pathInfo = $_SERVER['PATH_INFO'] ?? '';
|
|
||||||
|
|
||||||
if(empty($pathInfo) || $pathInfo === '/') {
|
|
||||||
Template::render('info.index');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$document = [
|
|
||||||
'content' => '',
|
|
||||||
'title' => '',
|
|
||||||
];
|
|
||||||
|
|
||||||
$isMisuzuDoc = $pathInfo === '/misuzu' || starts_with($pathInfo, '/misuzu/');
|
|
||||||
|
|
||||||
if($isMisuzuDoc) {
|
|
||||||
$filename = substr($pathInfo, 8);
|
|
||||||
$filename = empty($filename) ? 'README' : strtoupper($filename);
|
|
||||||
|
|
||||||
if($filename !== 'README') {
|
|
||||||
$titleSuffix = ' - Misuzu Project';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$filename = strtolower(substr($pathInfo, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!preg_match('#^([A-Za-z0-9_]+)$#', $filename)) {
|
|
||||||
echo render_error(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($filename !== 'LICENSE') {
|
|
||||||
$filename .= '.md';
|
|
||||||
}
|
|
||||||
|
|
||||||
$filename = MSZ_ROOT . ($isMisuzuDoc ? '/' : '/docs/') . $filename;
|
|
||||||
$document['content'] = is_file($filename) ? file_get_contents($filename) : '';
|
|
||||||
|
|
||||||
if(empty($document['content'])) {
|
|
||||||
echo render_error(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(empty($document['title'])) {
|
|
||||||
if(starts_with($document['content'], '# ')) {
|
|
||||||
$titleOffset = strpos($document['content'], "\n");
|
|
||||||
$document['title'] = trim(substr($document['content'], 2, $titleOffset - 1));
|
|
||||||
$document['content'] = substr($document['content'], $titleOffset);
|
|
||||||
} else {
|
|
||||||
$document['title'] = ucfirst(basename($filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!empty($titleSuffix)) {
|
|
||||||
$document['title'] .= $titleSuffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Template::render('info.view', [
|
|
||||||
'document' => $document,
|
|
||||||
]);
|
|
||||||
|
|
14
src/DB.php
14
src/DB.php
|
@ -1,11 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Misuzu;
|
namespace Misuzu;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
use Misuzu\Database\Database;
|
use Misuzu\Database\Database;
|
||||||
|
|
||||||
final class DB {
|
final class DB {
|
||||||
private static $instance;
|
private static $instance;
|
||||||
|
|
||||||
|
public const ATTRS = [
|
||||||
|
PDO::ATTR_CASE => PDO::CASE_NATURAL,
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
|
||||||
|
PDO::ATTR_STRINGIFY_FETCHES => false,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => "
|
||||||
|
SET SESSION
|
||||||
|
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
|
||||||
|
time_zone = '+00:00';
|
||||||
|
",
|
||||||
|
];
|
||||||
|
|
||||||
public static function init(...$args) {
|
public static function init(...$args) {
|
||||||
self::$instance = new Database(...$args);
|
self::$instance = new Database(...$args);
|
||||||
}
|
}
|
||||||
|
|
16
src/Http/Handlers/AuthHandler.php
Normal file
16
src/Http/Handlers/AuthHandler.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http\Handlers;
|
||||||
|
|
||||||
|
final class AuthHandler extends Handler {
|
||||||
|
public static function legacy(Response $response, Request $request): void {
|
||||||
|
$query = $request->getQueryParams();
|
||||||
|
$mode = isset($query['m']) && is_string($query['m']) ? $query['m'] : '';
|
||||||
|
$destination = [
|
||||||
|
'logout' => 'auth-logout',
|
||||||
|
'reset' => 'auth-reset',
|
||||||
|
'forgot' => 'auth-forgot',
|
||||||
|
'register' => 'auth-register',
|
||||||
|
][$mode] ?? 'auth-login';
|
||||||
|
$response->redirect(url($destination), true);
|
||||||
|
}
|
||||||
|
}
|
15
src/Http/Handlers/Handler.php
Normal file
15
src/Http/Handlers/Handler.php
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http\Handlers;
|
||||||
|
|
||||||
|
abstract class Handler {
|
||||||
|
public static function call(string $name): array {
|
||||||
|
[$funcName, $className] = explode('@', $name, 2);
|
||||||
|
return [__NAMESPACE__ . '\\' . $className, $funcName];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function redirect(string $location, bool $permanent = false): callable {
|
||||||
|
return function (Response $resp) use ($location, $permanent): void {
|
||||||
|
$resp->redirect($location, $permanent);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
100
src/Http/Handlers/HomeHandler.php
Normal file
100
src/Http/Handlers/HomeHandler.php
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http\Handlers;
|
||||||
|
|
||||||
|
use Misuzu\Config;
|
||||||
|
use Misuzu\DB;
|
||||||
|
|
||||||
|
final class HomeHandler extends Handler {
|
||||||
|
public function index(Response $response, Request $request): void {
|
||||||
|
if(Config::get('social.embed_linked', Config::TYPE_BOOL)) {
|
||||||
|
$linkedData = [
|
||||||
|
'name' => Config::get('site.name', Config::TYPE_STR, 'Misuzu'),
|
||||||
|
'url' => Config::get('site.url', Config::TYPE_STR),
|
||||||
|
'logo' => Config::get('site.ext_logo', Config::TYPE_STR),
|
||||||
|
'same_as' => Config::get('social.linked', Config::TYPE_ARR),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$news = news_posts_get(0, 5, null, true);
|
||||||
|
|
||||||
|
$stats = DB::query('
|
||||||
|
SELECT
|
||||||
|
(
|
||||||
|
SELECT COUNT(`user_id`)
|
||||||
|
FROM `msz_users`
|
||||||
|
WHERE `user_deleted` IS NULL
|
||||||
|
) AS `count_users_all`,
|
||||||
|
(
|
||||||
|
SELECT COUNT(`user_id`)
|
||||||
|
FROM `msz_users`
|
||||||
|
WHERE `user_active` >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
||||||
|
) AS `count_users_online`,
|
||||||
|
(
|
||||||
|
SELECT COUNT(`user_id`)
|
||||||
|
FROM `msz_users`
|
||||||
|
WHERE `user_active` >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
|
) AS `count_users_active`,
|
||||||
|
(
|
||||||
|
SELECT COUNT(`comment_id`)
|
||||||
|
FROM `msz_comments_posts`
|
||||||
|
WHERE `comment_deleted` IS NULL
|
||||||
|
) AS `count_comments`,
|
||||||
|
(
|
||||||
|
SELECT COUNT(`topic_id`)
|
||||||
|
FROM `msz_forum_topics`
|
||||||
|
WHERE `topic_deleted` IS NULL
|
||||||
|
) AS `count_forum_topics`,
|
||||||
|
(
|
||||||
|
SELECT COUNT(`post_id`)
|
||||||
|
FROM `msz_forum_posts`
|
||||||
|
WHERE `post_deleted` IS NULL
|
||||||
|
) AS `count_forum_posts`
|
||||||
|
')->fetch();
|
||||||
|
|
||||||
|
$changelog = DB::query('
|
||||||
|
SELECT
|
||||||
|
`change_id`, `change_log`, `change_action`,
|
||||||
|
DATE(`change_created`) AS `change_date`,
|
||||||
|
!ISNULL(`change_text`) AS `change_has_text`
|
||||||
|
FROM `msz_changelog_changes`
|
||||||
|
ORDER BY `change_created` DESC
|
||||||
|
LIMIT 10
|
||||||
|
')->fetchAll();
|
||||||
|
|
||||||
|
$birthdays = user_session_active() ? user_get_birthdays() : [];
|
||||||
|
|
||||||
|
$latestUser = DB::query('
|
||||||
|
SELECT
|
||||||
|
u.`user_id`, u.`username`, u.`user_created`,
|
||||||
|
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||||
|
FROM `msz_users` as u
|
||||||
|
LEFT JOIN `msz_roles` as r
|
||||||
|
ON r.`role_id` = u.`display_role`
|
||||||
|
WHERE `user_deleted` IS NULL
|
||||||
|
ORDER BY u.`user_id` DESC
|
||||||
|
LIMIT 1
|
||||||
|
')->fetch();
|
||||||
|
|
||||||
|
$onlineUsers = DB::query('
|
||||||
|
SELECT
|
||||||
|
u.`user_id`, u.`username`,
|
||||||
|
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||||
|
FROM `msz_users` as u
|
||||||
|
LEFT JOIN `msz_roles` as r
|
||||||
|
ON r.`role_id` = u.`display_role`
|
||||||
|
WHERE u.`user_active` >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
||||||
|
ORDER BY u.`user_active` DESC
|
||||||
|
LIMIT 104
|
||||||
|
')->fetchAll();
|
||||||
|
|
||||||
|
$response->setTemplate('home.landing', [
|
||||||
|
'statistics' => $stats,
|
||||||
|
'latest_user' => $latestUser,
|
||||||
|
'online_users' => $onlineUsers,
|
||||||
|
'birthdays' => $birthdays,
|
||||||
|
'featured_changelog' => $changelog,
|
||||||
|
'featured_news' => $news,
|
||||||
|
'linked_data' => $linkedData ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
65
src/Http/Handlers/InfoHandler.php
Normal file
65
src/Http/Handlers/InfoHandler.php
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http\Handlers;
|
||||||
|
|
||||||
|
final class InfoHandler extends Handler {
|
||||||
|
public function index(Response $response): void {
|
||||||
|
$response->setTemplate('info.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function redir(Response $response, Request $request, string $name = ''): void {
|
||||||
|
$response->redirect(url('info', ['title' => $name]), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function page(Response $response, Request $request, string $name) {
|
||||||
|
$document = [
|
||||||
|
'content' => '',
|
||||||
|
'title' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$isMisuzuDoc = $name === 'misuzu' || starts_with($name, 'misuzu/');
|
||||||
|
|
||||||
|
if($isMisuzuDoc) {
|
||||||
|
$filename = substr($name, 7);
|
||||||
|
$filename = empty($filename) ? 'README' : strtoupper($filename);
|
||||||
|
|
||||||
|
if($filename !== 'README') {
|
||||||
|
$titleSuffix = ' - Misuzu Project';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$filename = strtolower($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!preg_match('#^([A-Za-z0-9_]+)$#', $filename)) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($filename !== 'LICENSE') {
|
||||||
|
$filename .= '.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = MSZ_ROOT . ($isMisuzuDoc ? '/' : '/docs/') . $filename;
|
||||||
|
$document['content'] = is_file($filename) ? file_get_contents($filename) : '';
|
||||||
|
|
||||||
|
if(empty($document['content'])) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($document['title'])) {
|
||||||
|
if(starts_with($document['content'], '# ')) {
|
||||||
|
$titleOffset = strpos($document['content'], "\n");
|
||||||
|
$document['title'] = trim(substr($document['content'], 2, $titleOffset - 1));
|
||||||
|
$document['content'] = substr($document['content'], $titleOffset);
|
||||||
|
} else {
|
||||||
|
$document['title'] = ucfirst(basename($filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($titleSuffix)) {
|
||||||
|
$document['title'] .= $titleSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->setTemplate('info.view', [
|
||||||
|
'document' => $document,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
116
src/Http/HttpMessage.php
Normal file
116
src/Http/HttpMessage.php
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Psr\Http\Message\MessageInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
abstract class HttpMessage implements MessageInterface {
|
||||||
|
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(StreamInterface $body): self {
|
||||||
|
$this->body = $body;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withBody(StreamInterface $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) === $name)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
86
src/Http/HttpRequestMessage.php
Normal file
86
src/Http/HttpRequestMessage.php
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
class HttpRequestMessage extends HttpMessage implements RequestInterface {
|
||||||
|
private $method = null;
|
||||||
|
private $uri = null;
|
||||||
|
private $requestTarget = null;
|
||||||
|
|
||||||
|
public function __construct(string $method, UriInterface $uri, array $headers = []) {
|
||||||
|
parent::__construct($headers);
|
||||||
|
$this->setMethod($method);
|
||||||
|
$this->setUri($uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMethod() {
|
||||||
|
return $this->method;
|
||||||
|
}
|
||||||
|
public function setMethod(string $method): self {
|
||||||
|
if(empty($method))
|
||||||
|
throw new InvalidArgumentException('Invalid method name.');
|
||||||
|
$this->method = $method;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withMethod($method) {
|
||||||
|
return (clone $this)->setMethod($method);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUri() {
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
public function setUri(UriInterface $uri, bool $preserveHost = false): self {
|
||||||
|
$this->uri = $uri;
|
||||||
|
|
||||||
|
if(!$preserveHost || !$this->hasHeader('Host'))
|
||||||
|
$this->applyHostHeader();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withUri($uri, $preserveHost = false) {
|
||||||
|
return (clone $this)->setUri($uri, $preserveHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
public 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 withRequestTarget($requestTarget) {
|
||||||
|
return (clone $this)->setRequestTarget($requestTarget);
|
||||||
|
}
|
||||||
|
}
|
112
src/Http/HttpResponseMessage.php
Normal file
112
src/Http/HttpResponseMessage.php
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
class HttpResponseMessage extends HttpMessage implements ResponseInterface {
|
||||||
|
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 withStatus($code, $reasonPhrase = '') {
|
||||||
|
return (clone $this)->setStatusCode($code)->setReasonPhrase($reasonPhrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
}
|
160
src/Http/HttpServerRequestMessage.php
Normal file
160
src/Http/HttpServerRequestMessage.php
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
class HttpServerRequestMessage extends HttpRequestMessage implements ServerRequestInterface {
|
||||||
|
private $server = [];
|
||||||
|
private $cookies = [];
|
||||||
|
private $query = [];
|
||||||
|
private $files = [];
|
||||||
|
private $attributes = [];
|
||||||
|
private $parsedBody = null;
|
||||||
|
private $bodyStream = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $method,
|
||||||
|
UriInterface $uri,
|
||||||
|
array $serverParams = [],
|
||||||
|
array $headers = [],
|
||||||
|
array $queryParams = [],
|
||||||
|
array $uploadedFiles = [],
|
||||||
|
$parsedBody = null,
|
||||||
|
StreamInterface $rawBody = null
|
||||||
|
) {
|
||||||
|
parent::__construct($method, $uri, $headers);
|
||||||
|
$this->setServerParams($serverParams);
|
||||||
|
$this->setQueryParams($queryParams);
|
||||||
|
$this->setUploadedFiles($uploadedFiles);
|
||||||
|
$this->setParsedBody($parsedBody);
|
||||||
|
$this->setBody($rawBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServerParams() {
|
||||||
|
return $this->server;
|
||||||
|
}
|
||||||
|
public function setServerParams(array $serverParams): self {
|
||||||
|
$this->server = $serverParams;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCookieParams() {
|
||||||
|
return $this->cookies;
|
||||||
|
}
|
||||||
|
public function setCookieParams(array $cookies): self {
|
||||||
|
$this->cookies = $cookies;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withCookieParams(array $cookies) {
|
||||||
|
return (clone $this)->setCookieParams($cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryParams() {
|
||||||
|
return $this->query;
|
||||||
|
}
|
||||||
|
public function setQueryParams(array $query): self {
|
||||||
|
$this->query = $query;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withQueryParams(array $query) {
|
||||||
|
return (clone $this)->setQueryParams($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUploadedFiles() {
|
||||||
|
return $this->files;
|
||||||
|
}
|
||||||
|
public function setUploadedFiles(array $uploadedFiles): self {
|
||||||
|
$this->files = $uploadedFiles;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withUploadedFiles(array $uploadedFiles): self {
|
||||||
|
return (clone $this)->setUploadedFiles($uploadedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParsedBody() {
|
||||||
|
return $this->parsedBody;
|
||||||
|
}
|
||||||
|
public 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 withParsedBody($data) {
|
||||||
|
return (clone $this)->setParsedBody($data);
|
||||||
|
}
|
||||||
|
public function getBodyParam(string $name, ?string $default = null): ?string {
|
||||||
|
if(!is_array($this->parsedBody))
|
||||||
|
return $default;
|
||||||
|
|
||||||
|
return $this->parsedBody[$name] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAttributes() {
|
||||||
|
return $this->attributes;
|
||||||
|
}
|
||||||
|
public function getAttribute($name, $default = null) {
|
||||||
|
return $this->attributes[$name] ?? $default;
|
||||||
|
}
|
||||||
|
public function setAttribute(string $name, $value): self {
|
||||||
|
$this->attributes[$name] = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withAttribute($name, $value) {
|
||||||
|
return (clone $this)->setAttribute($name, $value);
|
||||||
|
}
|
||||||
|
public function removeAttribute(string $name): self {
|
||||||
|
unset($this->attributes[$name]);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withoutAttribute($name) {
|
||||||
|
return (clone $this)->removeAttribute($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(!isset($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(): HttpServerRequestMessage {
|
||||||
|
return new static(
|
||||||
|
$_SERVER['REQUEST_METHOD'],
|
||||||
|
new Uri('/' . trim($_SERVER['REQUEST_URI'] ?? '', '/')),
|
||||||
|
$_SERVER,
|
||||||
|
self::getRequestHeaders(),
|
||||||
|
$_GET,
|
||||||
|
UploadedFile::createFromFILES($_FILES),
|
||||||
|
$_POST,
|
||||||
|
Stream::createFromFile('php://input')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
130
src/Http/Routing/Route.php
Normal file
130
src/Http/Routing/Route.php
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http\Routing;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
class Route {
|
||||||
|
private $methods = [];
|
||||||
|
private $path = '';
|
||||||
|
private $regex = false;
|
||||||
|
private $handler;
|
||||||
|
private $children = [];
|
||||||
|
private $filters = [];
|
||||||
|
|
||||||
|
public function __construct(array $methods, string $path, bool $regex, $handler) {
|
||||||
|
$this->methods = array_map('strtoupper', $methods);
|
||||||
|
$this->regex = $regex;
|
||||||
|
$this->handler = $handler;
|
||||||
|
$this->path = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(array $methods, string $path, $regex = null, $callable = null): self {
|
||||||
|
return new static($methods, $path, is_bool($regex) ? $regex : false, is_bool($regex) ? $callable : $regex);
|
||||||
|
}
|
||||||
|
public static function get(string $path, $regex = null, $callable = null): self {
|
||||||
|
return self::create(['GET'], $path, $regex, $callable);
|
||||||
|
}
|
||||||
|
public static function post(string $path, $regex = null, $callable = null): self {
|
||||||
|
return self::create(['POST'], $path, $regex, $callable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isRegex(): bool {
|
||||||
|
return $this->regex;
|
||||||
|
}
|
||||||
|
public function setRegex(): self {
|
||||||
|
$this->regex = true;
|
||||||
|
foreach($this->children as $child)
|
||||||
|
$child->setRegex();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): string {
|
||||||
|
return $this->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);
|
||||||
|
foreach($this->children as $child)
|
||||||
|
$child->addFilters(...$filters);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function getFilters(): array {
|
||||||
|
return $this->filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChildren(): array {
|
||||||
|
return $this->children;
|
||||||
|
}
|
||||||
|
public function addChildren(Route ...$routes): self {
|
||||||
|
foreach($routes as $route) {
|
||||||
|
$route->setPrefix($this->getPath())->addFilters(...$this->getFilters());
|
||||||
|
|
||||||
|
if($this->isRegex())
|
||||||
|
$route->setRegex();
|
||||||
|
|
||||||
|
$this->children[] = $route;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPrefix(string $prefix): self {
|
||||||
|
foreach($this->children as $child)
|
||||||
|
$child->setPrefix($prefix);
|
||||||
|
return $this->setPath($prefix . '/' . trim($this->getPath(), '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function match(RequestInterface $request, array &$matches): bool {
|
||||||
|
$matches = [$this->getPath()];
|
||||||
|
|
||||||
|
if(!in_array($request->getMethod(), $this->methods))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
$requestPath = $request->getUri()->getPath();
|
||||||
|
|
||||||
|
return $this->isRegex()
|
||||||
|
? preg_match('#^' . $this->getPath() . '$#', $requestPath, $matches)
|
||||||
|
: $this->getPath() === $requestPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(ServerRequestInterface $request, ...$args): ResponseInterface {
|
||||||
|
$response = new RouterResponseMessage(200);
|
||||||
|
$result = null;
|
||||||
|
|
||||||
|
array_unshift($args, $response, $request);
|
||||||
|
|
||||||
|
if(is_array($this->handler)) {
|
||||||
|
if(method_exists($this->handler[0] ?? '', $this->handler[1] ?? '')) {
|
||||||
|
$handlerClass = new $this->handler[0]($response, $request);
|
||||||
|
$result = $handlerClass->{$this->handler[1]}(...$args);
|
||||||
|
}
|
||||||
|
} elseif(is_callable($this->handler)) {
|
||||||
|
$result = call_user_func_array($this->handler, $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
76
src/Http/Routing/Router.php
Normal file
76
src/Http/Routing/Router.php
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http\Routing;
|
||||||
|
|
||||||
|
use Misuzu\Http\HttpResponseMessage;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
|
class Router implements RequestHandlerInterface {
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
private $routesExact = [];
|
||||||
|
private $routesRegex = [];
|
||||||
|
|
||||||
|
public static function __callStatic(string $name, array $args) {
|
||||||
|
return is_null(self::$instance) ? null : self::$instance->{$name}(...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct() {}
|
||||||
|
|
||||||
|
public function setInstance(): self {
|
||||||
|
return self::$instance = $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRoutes(Route ...$routes): self {
|
||||||
|
foreach($routes as $route) {
|
||||||
|
if($route->isRegex()) {
|
||||||
|
$this->routesRegex[] = $route;
|
||||||
|
} else {
|
||||||
|
$this->routesExact[] = $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->addRoutes(...$route->getChildren());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function matchExact(RequestInterface $request, array &$matches): ?Route {
|
||||||
|
foreach($this->routesExact as $route)
|
||||||
|
if($route->match($request, $matches))
|
||||||
|
return $route;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function matchRegex(RequestInterface $request, array &$matches): ?Route {
|
||||||
|
foreach($this->routesRegex as $route)
|
||||||
|
if($route->match($request, $matches))
|
||||||
|
return $route;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function match(RequestInterface $request, array &$matches): ?Route {
|
||||||
|
return $this->matchExact($request, $matches) ?? $this->matchRegex($request, $matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(ServerRequestInterface $request): ResponseInterface {
|
||||||
|
$matches = [];
|
||||||
|
$route = $this->match($request, $matches);
|
||||||
|
|
||||||
|
if($route === null)
|
||||||
|
return new HttpResponseMessage(404);
|
||||||
|
|
||||||
|
foreach($route->getFilters() as $filter) {
|
||||||
|
$response = (new $filter)->process($request, $this);
|
||||||
|
|
||||||
|
if($response !== null)
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $route->dispatch($request, ...array_slice($matches, 1));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
58
src/Http/Routing/RouterResponseMessage.php
Normal file
58
src/Http/Routing/RouterResponseMessage.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http\Routing;
|
||||||
|
|
||||||
|
use Misuzu\Template;
|
||||||
|
use Misuzu\Http\HttpResponseMessage;
|
||||||
|
use Misuzu\Http\Stream;
|
||||||
|
|
||||||
|
class RouterResponseMessage extends HttpResponseMessage {
|
||||||
|
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 {
|
||||||
|
$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): self {
|
||||||
|
$this->setStatusCode($permanent ? 301 : 302);
|
||||||
|
$this->setHeader('Location', $path);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContents(): string {
|
||||||
|
return (string)($this->getBody() ?? '');
|
||||||
|
}
|
||||||
|
}
|
194
src/Http/Stream.php
Normal file
194
src/Http/Stream.php
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
class Stream implements StreamInterface {
|
||||||
|
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 = ''): StreamInterface {
|
||||||
|
if($contents instanceof StreamInterface)
|
||||||
|
return $contents;
|
||||||
|
|
||||||
|
return new static($contents);
|
||||||
|
}
|
||||||
|
public static function createFromFile(string $filename, string $mode = 'rb'): StreamInterface {
|
||||||
|
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() {
|
||||||
|
return $this->readable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read($length) {
|
||||||
|
if(!$this->isReadable())
|
||||||
|
throw RuntimeException('Can\'t read from this stream.');
|
||||||
|
|
||||||
|
return fread($this->stream, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContents() {
|
||||||
|
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() {
|
||||||
|
return $this->writable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write($string) {
|
||||||
|
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() {
|
||||||
|
return $this->seekable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function seek($offset, $whence = SEEK_SET) {
|
||||||
|
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() {
|
||||||
|
$this->seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tell() {
|
||||||
|
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() {
|
||||||
|
return !isset($this->stream) || feof($this->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSize() {
|
||||||
|
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() {
|
||||||
|
$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();
|
||||||
|
}
|
||||||
|
}
|
174
src/Http/UploadedFile.php
Normal file
174
src/Http/UploadedFile.php
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Psr\Http\Message\UploadedFileInterface;
|
||||||
|
|
||||||
|
class UploadedFile implements UploadedFileInterface {
|
||||||
|
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 StreamInterface)
|
||||||
|
$this->stream = $fileNameOrStream;
|
||||||
|
|
||||||
|
if($size === null && $this->stream !== null)
|
||||||
|
$this->size = $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->steam === 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 static(
|
||||||
|
$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));
|
||||||
|
}
|
||||||
|
}
|
183
src/Http/Uri.php
Normal file
183
src/Http/Uri.php
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
<?php
|
||||||
|
namespace Misuzu\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
class Uri implements UriInterface {
|
||||||
|
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 withScheme($scheme) {
|
||||||
|
if(!is_string($scheme))
|
||||||
|
throw new InvalidArgumentException('Scheme must be a string.');
|
||||||
|
|
||||||
|
return (clone $this)->setScheme($scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 withUserInfo($user, $password = null) {
|
||||||
|
return (clone $this)->setUserInfo($user, $password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHost() {
|
||||||
|
return $this->host;
|
||||||
|
}
|
||||||
|
public function setHost(string $host): self {
|
||||||
|
$this->host = $host;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withHost($host) {
|
||||||
|
if(!is_string($host))
|
||||||
|
throw new InvalidArgumentException('Hostname must be a string.');
|
||||||
|
|
||||||
|
return (clone $this)->setHost($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 withPort($port) {
|
||||||
|
return (clone $this)->setPort($port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath() {
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
public function setPath(string $path): self {
|
||||||
|
$this->path = $path;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withPath($path) {
|
||||||
|
if(!is_string($path))
|
||||||
|
throw new InvalidArgumentException('Path must be a string.');
|
||||||
|
|
||||||
|
return (clone $this)->setPath($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQuery() {
|
||||||
|
return $this->query;
|
||||||
|
}
|
||||||
|
public function setQuery(string $query): self {
|
||||||
|
$this->query = $query;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withQuery($query) {
|
||||||
|
if(!is_string($query))
|
||||||
|
throw new InvalidArgumentException('Query string must be a string.');
|
||||||
|
|
||||||
|
return (clone $this)->setQuery($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFragment() {
|
||||||
|
return $this->fragment;
|
||||||
|
}
|
||||||
|
public function setFragment(string $fragment): self {
|
||||||
|
$this->fragment = $fragment;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
public function withFragment($fragment) {
|
||||||
|
return (clone $this)->setFragment($fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@
|
||||||
// text surrounded by { and } will be replaced by a CSRF token with the given text as its realm, this will have no effect in a sessionless environment
|
// text surrounded by { and } will be replaced by a CSRF token with the given text as its realm, this will have no effect in a sessionless environment
|
||||||
define('MSZ_URLS', [
|
define('MSZ_URLS', [
|
||||||
'index' => ['/'],
|
'index' => ['/'],
|
||||||
'info' => ['/info.php/<title>'],
|
'info' => ['/info/<title>'],
|
||||||
'media-proxy' => ['/proxy.php/<hash>/<url>'],
|
'media-proxy' => ['/proxy.php/<hash>/<url>'],
|
||||||
|
|
||||||
'search-index' => ['/search.php'],
|
'search-index' => ['/search.php'],
|
||||||
|
|
Loading…
Add table
Reference in a new issue