From 09a91a998ea47ae40cfa96a523b93f79ac0a4535 Mon Sep 17 00:00:00 2001 From: Alex Cabal Date: Mon, 9 Sep 2024 20:34:30 -0500 Subject: [PATCH] Update framework standards --- lib/Constants.php | 8 +- lib/DbConnection.php | 16 +-- lib/Enums/HttpCode.php | 26 ++++ lib/{ => Enums}/HttpMethod.php | 2 + lib/{ => Enums}/HttpRequestType.php | 2 + lib/{ => Enums}/HttpVariableSource.php | 2 + lib/{ => Enums}/HttpVariableType.php | 2 + lib/HttpInput.php | 175 ++++++++++++++++++------ lib/Traits/Accessor.php | 89 +++++++++--- www/artworks/post.php | 8 +- www/newsletter/subscriptions/delete.php | 6 +- www/newsletter/subscriptions/post.php | 18 +-- www/polls/votes/post.php | 10 +- www/sessions/post.php | 10 +- www/webhooks/github.php | 2 +- www/webhooks/postmark.php | 2 +- www/webhooks/zoho.php | 2 +- 17 files changed, 282 insertions(+), 98 deletions(-) create mode 100644 lib/Enums/HttpCode.php rename lib/{ => Enums}/HttpMethod.php (89%) rename lib/{ => Enums}/HttpRequestType.php (73%) rename lib/{ => Enums}/HttpVariableSource.php (82%) rename lib/{ => Enums}/HttpVariableType.php (86%) diff --git a/lib/Constants.php b/lib/Constants.php index 4fc184a8..1ee8a286 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -45,10 +45,10 @@ const CAPTCHA_IMAGE_HEIGHT = 72; const CAPTCHA_IMAGE_WIDTH = 230; // These are defined for convenience, so that getting HTTP input isn't so wordy -const GET = HttpVariableSource::Get; -const POST = HttpVariableSource::Post; -const SESSION = HttpVariableSource::Session; -const COOKIE = HttpVariableSource::Cookie; +const GET = Enums\HttpVariableSource::Get; +const POST = Enums\HttpVariableSource::Post; +const SESSION = Enums\HttpVariableSource::Session; +const COOKIE = Enums\HttpVariableSource::Cookie; define('NO_REPLY_EMAIL_ADDRESS', get_cfg_var('se.secrets.email.no_reply_address')); define('ADMIN_EMAIL_ADDRESS', get_cfg_var('se.secrets.email.admin_address')); diff --git a/lib/DbConnection.php b/lib/DbConnection.php index 5e9b4730..0c07774c 100644 --- a/lib/DbConnection.php +++ b/lib/DbConnection.php @@ -9,7 +9,7 @@ class DbConnection{ public int $LastQueryAffectedRowCount = 0; /** - * Create a new database connection. + * Create a database connection. * * @param ?string $defaultDatabase The default database to connect to, or `null` not to define one. * @param string $host The database hostname. @@ -28,7 +28,7 @@ class DbConnection{ $connectionString = 'mysql:'; - if(stripos($host, ':') !== false){ + if(mb_stripos($host, ':') !== false){ $port = null; preg_match('/([^:]*):([0-9]+)/ius', $host, $matches); $host = $matches[1]; @@ -113,7 +113,7 @@ class DbConnection{ * * The result is an array of rows. Each row is an array of objects, with each object containing its columns and values. For example, * - * ``` + * ```php * [ * [ * 'Users' => { @@ -123,7 +123,7 @@ class DbConnection{ * 'Posts' => { * 'PostId' => 222, * 'Title' => 'Lorem Ipsum' - * }, + * } * ], * [ * 'Users' => { @@ -146,14 +146,14 @@ class DbConnection{ * @param array $params An array of parameters to bind to the SQL statement. * @param class-string $class The class to instantiate for each row, or `null` to return an array of rows. * - * @return array|array> An array of `$class` if `$class` is not `null`, otherwise an array of rows of the form `["LeftTableName" => $stdClass, "RightTableName" => $stdClass]`. + * @return array|array> An array of `$class` if `$class` is not `null`, otherwise an array of rows of the form `["LeftTableName" => $stdClass, "RightTableName" => $stdClass]`. * - * @throws Exceptions\AppException If a class was specified but the class doesn't have a `FromMultiTableRow()` method. + * @throws Exceptions\MultiSelectMethodNotFoundException If a class was specified but the class doesn't have a `FromMultiTableRow()` method. * @throws Exceptions\DatabaseQueryException When an error occurs during execution of the query. */ public function MultiTableSelect(string $sql, array $params = [], ?string $class = null): array{ if($class !== null && !method_exists($class, 'FromMultiTableRow')){ - throw new Exceptions\AppException('Multi table select attempted, but class ' . $class . ' doesn\'t have a FromMultiTableRow() method.'); + throw new Exceptions\MultiSelectMethodNotFoundException($class); } $handle = $this->PreparePdoHandle($sql, $params); @@ -316,7 +316,7 @@ class DbConnection{ * @param \PdoStatement $handle The PDO handle to execute. * @param class-string $class The class to instantiate for each row, or `null` to return an array of rows. * - * @return array|array> An array of `$class` if `$class` is not `null`, otherwise an array of rows of the form `["LeftTableName" => $stdClass, "RightTableName" => $stdClass]`. + * @return array|array> An array of `$class` if `$class` is not `null`, otherwise an array of rows of the form `["LeftTableName" => $stdClass, "RightTableName" => $stdClass]`. * * @throws \PDOException When an error occurs during execution of the query. */ diff --git a/lib/Enums/HttpCode.php b/lib/Enums/HttpCode.php new file mode 100644 index 00000000..afa489fe --- /dev/null +++ b/lib/Enums/HttpCode.php @@ -0,0 +1,26 @@ + $allowedHttpMethods An array containing a list of allowed HTTP methods, or null if any valid HTTP method is allowed. - * @param bool $throwException If the request HTTP method isn't allowed, then throw an exception; otherwise, output HTTP 405 and exit the script immediately. - * @throws Exceptions\InvalidHttpMethodException If the HTTP method is not recognized and `$throwException` is `true`. - * @throws Exceptions\HttpMethodNotAllowedException If the HTTP method is not in the list of allowed methods and `$throwException` is `true`. + * Calculate the HTTP method of the request, then include `.php` and exit. */ - public static function ValidateRequestMethod(?array $allowedHttpMethods = null, bool $throwException = false): HttpMethod{ + public static function DispatchRest(): void{ try{ - $requestMethod = HttpMethod::from($_POST['_method'] ?? $_SERVER['REQUEST_METHOD']); + $httpMethod = HttpInput::ValidateRequestMethod(null, true); + + $filename = mb_strtolower($httpMethod->value) . '.php'; + + if(!file_exists($filename)){ + throw new Exceptions\InvalidHttpMethodException(); + } + + if($httpMethod == Enums\HttpMethod::Post){ + // If we're a HTTP POST, then we got here from a POST request initially, so just continue + return; + } + + include($filename); + + exit(); + } + catch(Exceptions\InvalidHttpMethodException | Exceptions\HttpMethodNotAllowedException){ + $filenames = glob('{delete,get,patch,post,put}.php', GLOB_BRACE); + + if(sizeof($filenames) > 0){ + header('Allow: ' . implode(',', array_map(fn($filename): string => mb_strtoupper(preg_replace('/^([a-z]+)[\.\-].+$/i', '\1', $filename)), $filenames))); + } + + http_response_code(Enums\HttpCode::MethodNotAllowed->value); + exit(); + } + } + + /** + * Check that the request's HTTP method is in a list of allowed HTTP methods. + * @param ?array $allowedHttpMethods An array containing a list of allowed HTTP methods, or null if any valid HTTP method is allowed. + * @param bool $throwException If the request HTTP method isn't allowed, then throw an exception; otherwise, output HTTP 405 and exit the script immediately. + * @throws Exceptions\InvalidHttpMethodException If the HTTP method is not recognized, and `$throwException` is `true`. + * @throws Exceptions\HttpMethodNotAllowedException If the HTTP method is recognized but not allowed, and `$throwException` is `true`. + */ + public static function ValidateRequestMethod(?array $allowedHttpMethods = null, bool $throwException = false): Enums\HttpMethod{ + try{ + $requestMethod = Enums\HttpMethod::from($_POST['_method'] ?? $_GET['_method'] ?? $_SERVER['REQUEST_METHOD']); if($allowedHttpMethods !== null){ $isRequestMethodAllowed = false; foreach($allowedHttpMethods as $allowedHttpMethod){ @@ -41,7 +78,7 @@ class HttpInput{ if($allowedHttpMethods !== null){ header('Allow: ' . implode(',', array_map(fn($httpMethod): string => $httpMethod->value, $allowedHttpMethods))); } - http_response_code(405); + http_response_code(Enums\HttpCode::MethodNotAllowed->value); exit(); } } @@ -53,7 +90,7 @@ class HttpInput{ * @return int The maximum size for an HTTP POST request, in bytes. */ public static function GetMaxPostSize(): int{ - $post_max_size = ini_get('post_max_size'); + $post_max_size = ini_get('upload_max_filesize'); $unit = substr($post_max_size, -1); $size = (int) substr($post_max_size, 0, -1); @@ -71,16 +108,35 @@ class HttpInput{ return true; } } + elseif(sizeof($_FILES) > 0){ + // We received files but may have an error because the size exceeded our limit. + foreach($_FILES as $file){ + $error = $file['error'] ?? UPLOAD_ERR_OK; + + if($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE){ + return true; + } + } + } return false; } - public static function GetRequestType(): HttpRequestType{ - return preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT'] ?? '') ? HttpRequestType::Web : HttpRequestType::Rest; + public static function GetRequestType(): Enums\HttpRequestType{ + return preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT'] ?? '') ? Enums\HttpRequestType::Web : Enums\HttpRequestType::Rest; } - public static function Str(HttpVariableSource $set, string $variable, bool $allowEmptyString = false): ?string{ - $var = self::GetHttpVar($variable, HttpVariableType::String, $set); + /** + * Get a string from an HTTP variable set. + * + * If the variable is set but empty, returns `null` unless `$allowEmptyString` is **`TRUE`**, in which case it returns an empty string. + * + * @param Enums\HttpVariableSource $set + * @param string $variable + * @param bool $allowEmptyString If the variable exists but is empty, return an empty string instead of `null`. + */ + public static function Str(Enums\HttpVariableSource $set, string $variable, bool $allowEmptyString = false): ?string{ + $var = self::GetHttpVar($variable, Enums\HttpVariableType::String, $set); if(is_array($var)){ return null; @@ -94,24 +150,51 @@ class HttpInput{ return $var; } - public static function Int(HttpVariableSource $set, string $variable): ?int{ + public static function Int(Enums\HttpVariableSource $set, string $variable): ?int{ /** @var ?int */ - return self::GetHttpVar($variable, HttpVariableType::Integer, $set); + return self::GetHttpVar($variable, Enums\HttpVariableType::Integer, $set); } - public static function Bool(HttpVariableSource $set, string $variable): ?bool{ + public static function Bool(Enums\HttpVariableSource $set, string $variable): ?bool{ /** @var ?bool */ - return self::GetHttpVar($variable, HttpVariableType::Boolean, $set); + return self::GetHttpVar($variable, Enums\HttpVariableType::Boolean, $set); } - public static function Dec(HttpVariableSource $set, string $variable): ?float{ + public static function Dec(Enums\HttpVariableSource $set, string $variable): ?float{ /** @var ?float */ - return self::GetHttpVar($variable, HttpVariableType::Decimal, $set); + return self::GetHttpVar($variable, Enums\HttpVariableType::Decimal, $set); } - public static function Date(HttpVariableSource $set, string $variable): ?DateTimeImmutable{ + public static function Date(Enums\HttpVariableSource $set, string $variable): ?DateTimeImmutable{ /** @var ?DateTimeImmutable */ - return self::GetHttpVar($variable, HttpVariableType::DateTime, $set); + return self::GetHttpVar($variable, Enums\HttpVariableType::DateTime, $set); + } + + /** + * Return an object of type `$class` from `$_SESSION`, or `null` of no object of that type exists in `$_SESSION`. + * + * @template T of object + * @param string $variable + * @param class-string|array> $class The class of the object to return, or an array of possible classes to return. + * + * @return ?T An object of type `$class`, or `null` if no object of that type exists in `$_SESSION`. + */ + public static function SessionObject(string $variable, string|array $class): ?object{ + if(!is_array($class)){ + $class = [$class]; + } + + $object = $_SESSION[$variable] ?? null; + + if($object !== null){ + foreach($class as $c){ + if(is_a($object, $c)){ + return $object; + } + } + } + + return null; } /** @@ -137,85 +220,95 @@ class HttpInput{ * @param string $variable * @return array */ - public static function Array(HttpVariableSource $set, string $variable): ?array{ + public static function Array(Enums\HttpVariableSource $set, string $variable): ?array{ /** @var array */ - return self::GetHttpVar($variable, HttpVariableType::Array, $set); + return self::GetHttpVar($variable, Enums\HttpVariableType::Array, $set); } /** * @return array|array|array|array|string|int|float|bool|DateTimeImmutable|null */ - private static function GetHttpVar(string $variable, HttpVariableType $type, HttpVariableSource $set): mixed{ + private static function GetHttpVar(string $variable, Enums\HttpVariableType $type, Enums\HttpVariableSource $set): mixed{ + // Note that in Core.php we parse the request body of DELETE, PATCH, and PUT into $_POST. + $vars = []; switch($set){ - case HttpVariableSource::Get: + case Enums\HttpVariableSource::Get: $vars = $_GET; break; - case HttpVariableSource::Post: + case Enums\HttpVariableSource::Post: $vars = $_POST; break; - case HttpVariableSource::Cookie: + case Enums\HttpVariableSource::Cookie: $vars = $_COOKIE; break; - case HttpVariableSource::Session: + case Enums\HttpVariableSource::Session: $vars = $_SESSION; break; } if(isset($vars[$variable])){ - if($type == HttpVariableType::Array && is_array($vars[$variable])){ + if($type == Enums\HttpVariableType::Array && is_array($vars[$variable])){ // We asked for an array, and we got one return $vars[$variable]; } - elseif($type !== HttpVariableType::Array && is_array($vars[$variable])){ + elseif($type !== Enums\HttpVariableType::Array && is_array($vars[$variable])){ // We asked for not an array, but we got an array return null; } elseif(is_string($vars[$variable])){ - $var = trim($vars[$variable]); + // HTML `