M composer.json => composer.json +0 -1
@@ 31,7 31,6 @@
"./src/Clock.php",
"./src/Renderer.php",
"./src/Session.php",
- "./src/http/Response.php",
"./src/Url.php"
]
},
M src/Application.php => src/Application.php +3 -3
@@ 19,9 19,9 @@ final class Application
// perhaps add a default logger
ErrorHandler::register($container['error_logger']);
- $this->addRoute('GET', '/', fn() => response("Hello World!\n", 200));
- $this->addRoute('GET', '/404', fn() => response("404 Page Not Found\n", 404));
- $this->addRoute('GET', '/405', fn() => response("405 Method Not Allowed\n", 405));
+ $this->addRoute('GET', '/', fn() => new Response("Hello World!\n", 200));
+ $this->addRoute('GET', '/404', fn() => new Response("404 Page Not Found\n", 404));
+ $this->addRoute('GET', '/405', fn() => new Response("405 Method Not Allowed\n", 405));
}
/**
D src/Console.php => src/Console.php +0 -126
@@ 1,126 0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Thirdplace;
-
-final class Console
-{
- private const NC = "\033[0m";
- private const GREEN = "\033[0;32m";
- private const YELLOW = "\033[1;33m";
- private const RED = "\033[0;31m";
-
- public function print(string $s, ...$args)
- {
- printf($s, ...$args);
- }
-
- public function println(string $s = '', ...$args)
- {
- $this->print($s . "\n", ...$args);
- }
-
- public function greenln(string $s, ...$args)
- {
- $this->green($s . "\n", ...$args);
- }
-
- public function green(string $s, ...$args)
- {
- $this->print(self::GREEN . $s . self::NC, ...$args);
- }
-
- public function yellow(string $s, ...$args)
- {
- $this->print(self::YELLOW . $s . self::NC, ...$args);
- }
-
- public function yellowln(string $s, ...$args)
- {
- $this->yellow($s . "\n", ...$args);
- }
-
- public function red(string $s, ...$args)
- {
- $this->print(self::RED . $s . self::NC, ...$args);
- }
-
- public function redln(string $s, ...$args)
- {
- $this->red($s . "\n", ...$args);
- }
-
- /**
- * @param int|string $status
- */
- public function exit($status = 0)
- {
- exit($status);
- }
-
- public function table(array $headers, array $rows, int $maxWidth = 50)
- {
- // Find the longest column value
- $columnWidth = 3;
- foreach (array_merge($rows, [$headers]) as $values) {
- foreach ($values as $value) {
- $columnWidth = max($columnWidth, mb_strlen((string) $value));
- }
- }
-
- $columnWidth = min($columnWidth, $maxWidth);
-
- // Truncate header values
- foreach ($headers as $i => $header) {
- $headers[$i] = $this->truncate((string) $header, $columnWidth);
- }
-
- // Truncate row values
- foreach ($rows as $i => $row) {
- foreach ($row as $j => $value) {
- $rows[$i][$j] = $this->truncate((string) ($value ?? 'NULL'), $columnWidth);
- }
- }
-
- // Create bar and row formatter
- $bar = '';
- $format = '';
- foreach ($headers as $value) {
- $bar .= '+' . str_repeat('-', $columnWidth +2);
- $format .= '| %-' . ($columnWidth +1 + strlen($value) - mb_strlen($value)) . 's';
- }
- $bar .= '+';
- $format .= '|';
-
- // Render bar
- $this->println($bar);
-
- // Render header
- $this->println($format, ...$headers);
-
- // Render bar
- $this->println($bar);
-
- // Render rows
- foreach ($rows as $row) {
- $format = '';
- foreach ($row as $value) {
- $format .= '| %-' . ($columnWidth +1 + strlen($value) - mb_strlen($value)) . 's';
- }
- $format .= '|';
- $this->println($format, ...$row);
- }
-
- // Render bar
- $this->println($bar);
- }
-
- private function truncate(string $str, int $length, $placeholder = '..'): string
- {
- if (mb_strlen($str) > $length) {
- return mb_substr($str, 0, $length - mb_strlen($placeholder)) . $placeholder;
- }
-
- return $str;
- }
-}
M src/Renderer.php => src/Renderer.php +0 -74
@@ 3,56 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
-final class Renderer
-{
- private array $config;
- private array $context;
-
- public function __construct(array $config = [])
- {
- $defaultConfig = [
- 'templates' => './',
- // Default context that is always available in templates
- 'context' => [],
- ];
- $this->config = array_merge($defaultConfig, $config);
- $this->context = $this->config['context'];
- }
-
- // This method not used much because each app typically customises it
- public function render(string $filePath, array $context = []): string
- {
- // might be bug here because the context contains values from previous calls
- $this->context = array_merge($this->context, $context);
- extract($this->context);
- ob_start();
-
- try {
- require $this->resolveFile($filePath);
- } catch (\Throwable $e) {
- ob_end_clean();
- throw $e;
- }
-
- return ob_get_clean();
- }
-
- private function resolveFile(string $filePath): string
- {
- if (is_file($filePath)) {
- return $filePath;
- }
-
- $candidate = sprintf('%s/%s', $this->config['templates'], $filePath);
-
- if (is_file($candidate)) {
- return $candidate;
- }
-
- throw new \Exception(sprintf('Unable to resolve file path: "%s"', $filePath));
- }
-}
-
function render_template(string $template, array $context = [])
{
extract($context);
@@ 89,27 39,3 @@ function sanitize(string $s): string
{
return filter_var($s, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
}
-
-/**
- * Truncate string
- */
-function truncate(string $s, int $length = 100, $marker = ' [...]'): string
-{
- $s = trim($s);
-
- if (mb_strlen($s) < $length) {
- return $s;
- }
-
- $naiveTruncate = mb_substr($s, 0, $length);
-
- $lastSpace = mb_strrpos($naiveTruncate, ' ');
-
- if ($lastSpace === false) {
- $lastSpace = $length;
- }
-
- $properTruncate = mb_substr($s, 0, $lastSpace);
-
- return $properTruncate . $marker;
-}
M src/Session.php => src/Session.php +0 -7
@@ 3,13 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
-function flash(string $message)
-{
- $messages = $_SESSION['messages'] ?? [];
- $messages[] = $message;
- $_SESSION['messages'] = $messages;
-}
-
class SessionMiddleware
{
private array $options;
D src/Shell.php => src/Shell.php +0 -26
@@ 1,26 0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Thirdplace;
-
-final class Shell
-{
- public static function execute(string $command, array $arguments = []): array
- {
- $argumentString = '';
- foreach ($arguments as $argument) {
- $argumentString .= ' ' . escapeshellarg($argument);
- }
-
- $_ = exec("$command $argumentString", $result, $status);
-
- switch ($status) {
- case 0:
- return $result;
- case 127:
- throw new \Exception(sprintf('Command not found: "%s"', $command));
- default:
- throw new \Exception(sprintf('Unsuccessful: "%s"', $command));
- }
- }
-}
M src/common.php => src/common.php +0 -48
@@ 4,51 4,3 @@ declare(strict_types=1);
namespace Thirdplace;
final class HttpException extends \Exception {}
-
-function retry(int $times, \Closure $fn, int $sleep = 0)
-{
- // todo: retry helper
-}
-
-function create_sane_stacktrace(\Throwable $e)
-{
- // todo: maybe accept trace instead of ex?
- $stackTrace[] = sprintf('%s:%s', $e->getFile(), $e->getLine());
- foreach ($e->getTrace() as $trace) {
- $stackTrace[] = sprintf(
- '%s:%s',
- $trace['file'] ?? '(no file)',
- $trace['line'] ?? '(no line)'
- );
- }
- return array_reverse($stackTrace);
-}
-
-/**
- * Beware: the entropy cannot be deduced from the length
- */
-function create_random_hex_string(int $length = 8)
-{
- $bytes = (int) ceil($length / 2);
- // todo: remove annoying chars such as 0, 0, 1, l
- return substr(bin2hex(openssl_random_pseudo_bytes($bytes)), 0, $length);
-}
-
-class ExceptionMiddleware
-{
- private $fn;
-
- public function __construct(callable $fn)
- {
- $this->fn = $fn;
- }
-
- function __invoke(Request $request, $next) {
- try {
- return $next($request);
- } catch (\Throwable $e) {
- // todo: consider wrapping this call in a try too?
- return ($this->fn)($e);
- }
- }
-}
M src/http/CachingHttpClient.php => src/http/CachingHttpClient.php +1 -1
@@ 58,7 58,7 @@ final class CachingHttpClient implements HttpClient
// Do we really want to cache this?
// This logic might not belong here
$this->cache->set($isCachedKey, true, 60 * 5);
- $this->cache->set($responseKey, response('', Response::SERVICE_UNAVAILABLE), 60 * 5);
+ $this->cache->set($responseKey, new Response('', Response::SERVICE_UNAVAILABLE), 60 * 5);
throw $e;
}
}
D src/http/Cookie.php => src/http/Cookie.php +0 -37
@@ 1,37 0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Thirdplace;
-
-final class Cookie
-{
- public string $name;
- public string $value;
-
- public static function fromString(string $cookieString): self
- {
- $cookieString = ltrim($cookieString, '; ');
- $parts = explode(';', $cookieString);
- $nameAndValue = explode('=', $parts[0]);
-
- // should this be checking nameAndValue instead?
- if (!isset($parts[1])) {
- throw new \Exception(sprintf('Unable to parse cookie string: %s', $cookieString));
- }
-
- return new self($nameAndValue[0], $nameAndValue[1]);
- }
-
- private function __construct(string $name, string $value)
- {
- $this->name = $name;
- $this->value = $value;
- }
-
- public function send(): void
- {
- $options = [
- ];
- setcookie($this->name, $this->value, $options);
- }
-}>
\ No newline at end of file
M src/http/CurlHttpClient.php => src/http/CurlHttpClient.php +9 -7
@@ 75,14 75,16 @@ final class CurlHttpClient implements HttpClient
if (preg_match('#^HTTP/(2|1.1|1.0)#', $rawHeader) || $rawHeader==="\r\n") {
return $len;
}
- // todo: maybe try..catch here
- $header = Header::fromString($rawHeader);
-
- if (!isset($responseHeaders[$header->name])) {
- $responseHeaders[$header->name] = [];
+ $header = explode(':', $rawHeader);
+ if (count($header) === 1) {
+ return $len;
}
- $responseHeaders[$header->name][] = $header->value;
-
+ $name = mb_strtolower(trim($header[0]));
+ $value = trim(implode(':', array_slice($header, 1)));
+ if (!isset($responseHeaders[$name])) {
+ $responseHeaders[$name] = [];
+ }
+ $responseHeaders[$name][] = $value;
return $len;
});
D => +0 -44
@@ 1,44 0,0 @@
<?php
declare(strict_types=1);
namespace Thirdplace;
final class Header
{
public const APPLICATION_ATOM_XML = 'application/atom+xml';
public const APPLICATION_JSON = 'application/json';
public const APPLICATION_RSS_XML = 'application/rss+xml';
public const APPLICATION_XML = 'application/xml';
public const APPLICATION_X_RSS_XML = 'application/x-rss+xml';
public const TEXT_PLAIN = 'text/plain';
public const TEXT_XML = 'text/xml';
public string $name;
public string $value;
public static function fromNameAndValue(string $name, string $value): self
{
return new self($name, $value);
}
public static function fromString(string $rawHeader): self
{
$header = explode(':', $rawHeader);
if (count($header) === 1) {
// todo: throw HeaderException
throw new \Exception(sprintf('Invalid header string: "%s"', $rawHeader));
}
$name = mb_strtolower(trim($header[0]));
$value = trim(implode(':', array_slice($header, 1)));
return new self($name, $value);
}
private function __construct(string $name, string $value)
{
$this->name = $name;
$this->value = $value;
}
}
\ No newline at end of file
M src/http/Request.php => src/http/Request.php +2 -3
@@ 31,15 31,14 @@ final class Request
throw new HttpException('Failed to read raw body');
}
+ $self->headers = [];
if (function_exists('getallheaders')) {
foreach (\getallheaders() as $name => $value) {
$self->headers[strtolower($name)] = $value;
}
- } else {
- $self->headers = [];
}
- $self->attributes = [];
+ $self->attributes = [];
return $self;
}
M src/http/Response.php => src/http/Response.php +4 -31
@@ 3,30 3,6 @@ declare(strict_types=1);
namespace Thirdplace;
-function response(string $body = '', int $code = 200): Response
-{
- return new Response($body, $code);
-}
-
-function text(string $body, int $code = 200): Response
-{
- return response($body, $code)->withHeader('content-type', 'text/plain');
-}
-
-function json(array $body, int $code = 200): Response
-{
- $json = Json::encode($body);
- return response($json, $code)->withHeader('content-type', 'application/json');
-}
-
-function redirect(string $url, string $flash = '')
-{
- if ($flash !== '') {
- flash($flash);
- }
- return response('', 302)->withHeader('location', $url);
-}
-
final class Response
{
public const OK = 200;
@@ 69,8 45,6 @@ final class Response
public int $code;
private array $headers;
-
- /** @var Cookie[] */
private array $cookies;
/**
@@ 121,11 95,10 @@ final class Response
return $lastHeader ?: $default;
}
- public function withCookie(Cookie $cookie): self
+ public function withCookie($cookie): self
{
- $clone = clone $this;
- $clone->cookies[$cookie->name] = $cookie;
- return $clone;
+ // TODO
+ throw new \Exception('Not implemented');
}
public function cookie(string $name): ?Cookie
@@ 142,7 115,7 @@ final class Response
}
foreach ($this->cookies as $cookie) {
- $cookie->send();
+ // TODO: setcookie
}
print $this->body;
D src/logger/MailgunHandler.php => src/logger/MailgunHandler.php +0 -50
@@ 1,50 0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace Thirdplace;
-
-final class MailgunHandler
-{
- private HttpClient $client;
- private array $config;
-
- public function __construct(
- HttpClient $client,
- array $config
- ) {
- $this->client = $client;
- $this->config = $config;
- }
-
- public function __invoke(array $record): void
- {
- if (isset($record['context']['e'])) {
- $record['context']['e'] = create_sane_stacktrace($record['context']['e']);
- }
- $subject = sprintf(
- "[%s] %s.%s %s",
- $record['created_at']->format('Y-m-d H:i:s'),
- $record['name'],
- $record['level_name'],
- str_replace(["\n", "\r"], '\n', $record['message'])
- );
- $context = Json::encode($record['context']) ?: '["Unable to json encode context"]';
- $url = sprintf('https://api.mailgun.net/v3/%s/messages', $this->config['domain']);
- $response = $this->client->request('POST', $url, [
- 'auth' => [
- 'user' => 'api',
- 'pass' => $this->config['key'],
- ],
- 'body' => [
- 'from' => $this->config['from'],
- 'to' => $this->config['to'],
- 'subject' => truncate($subject),
- 'text' => "$subject\n$context",
- ],
- ]);
- if ($response->code !== 200) {
- // Perhaps lets not throw since we might already be inside an exception?
- throw new \Exception("MailgunHandler: $response->body");
- }
- }
-}
M src/logger/StreamHandler.php => src/logger/StreamHandler.php +1 -13
@@ 14,13 14,6 @@ final class StreamHandler
public function __invoke(array $record): void
{
- if (isset($record['context']['e'])) {
- $e = $record['context']['e'];
- // todo: improve message
- $record['context']['message'] = $e->getMessage();
- $record['context']['code'] = $e->getCode();
- $record['context']['trace'] = create_sane_stacktrace($e);
- }
if ($record['context'] === []) {
$record['context'] = '';
} else {
@@ 42,11 35,6 @@ final class StreamHandler
$this->stream = fopen($this->stream, 'a');
}
- if (!fwrite($this->stream, $text)) {
- // Maybe drop this last effort to write to error log
- if (!error_log('Unable to write log record: ' . $record['message'])) {
- // todo: write to stderr or stdout?
- }
- }
+ $bytes = fwrite($this->stream, $text);
}
}
A tests/UrlTest.php => tests/UrlTest.php +36 -0
@@ 0,0 1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Thirdplace;
+
+use PHPUnit\Framework\TestCase;
+
+class UrlTest extends TestCase
+{
+ public function test()
+ {
+ $this->assertEquals('https://example.com:81/foo?bar=baz#kek', (string)url('https://example.com:81/foo?bar=baz#kek'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com'));
+ $this->assertEquals('https://example.com/foo', (string)url('https://example.com/foo'));
+ $this->assertEquals('https://example.com/foo/', (string)url('https://example.com/foo/'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com/'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com//'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com/./'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com//./'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com/.///'));
+ $this->assertEquals('https://example.com/a', (string)url('https://example.com/../a'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com/foo/..'));
+ $this->assertEquals('https://example.com/foo', (string)url('https://example.com/foo/bar/..'));
+ $this->assertEquals('https://example.com/foo?bar=', (string)url('https://example.com/foo?bar'));
+ $this->assertEquals('https://example.com/?baz=', (string)url('https://example.com/foo/..?baz='));
+ $this->assertEquals('https://example.com/foo/bar', (string)url('https://example.com/foo/bar/baz/..'));
+ $this->assertEquals('https://example.com/foo', (string)url('https://example.com/foo/bar/../baz/..'));
+ //assert$this->Equals('https://example.com', (string)url('https://example.com/foo/bar/../..'));
+ $this->assertEquals('https://example.com', (string)url('https://example.com/../..'));
+ $this->assertEquals('https://example.com/foo?foo=bar', (string) url('https://example.com/foo?foo=bar'));
+ $this->assertEquals('https://blog.zulip.com/2022/05/05/public-access-option', (string) url('https://blog.zulip.com/2022/05/05/public-access-option'));
+ $this->assertEquals('https://kevincox.ca/feed.atom', (string) url('https://kevincox.ca/2022/05/../../feed.atom'));
+
+ }
+}