Twig, Tests, AES,
This commit is contained in:
parent
25d47f7528
commit
918244125c
122
cli.php
122
cli.php
@ -203,7 +203,7 @@ function handleDatabase(array $argv) {
|
||||
// 2nd: delete!
|
||||
foreach ($tables as $table => $uids) {
|
||||
$success = $sql->delete($table)
|
||||
->where(new CondIn("uid", $uids))
|
||||
->where(new CondIn(new Column("uid"), $uids))
|
||||
->execute();
|
||||
|
||||
if (!$success) {
|
||||
@ -342,6 +342,18 @@ function onMaintenance(array $argv) {
|
||||
}
|
||||
}
|
||||
|
||||
function getConsoleWidth(): int {
|
||||
$width = getenv('COLUMNS');
|
||||
if (!$width) {
|
||||
$width = exec('tput cols');
|
||||
if (!$width) {
|
||||
$width = 80; // default gnome-terminal column count
|
||||
}
|
||||
}
|
||||
|
||||
return intval($width);
|
||||
}
|
||||
|
||||
function printTable(array $head, array $body) {
|
||||
|
||||
$columns = [];
|
||||
@ -349,6 +361,7 @@ function printTable(array $head, array $body) {
|
||||
$columns[$key] = strlen($key);
|
||||
}
|
||||
|
||||
$maxWidth = getConsoleWidth();
|
||||
foreach ($body as $row) {
|
||||
foreach ($head as $key) {
|
||||
$value = $row[$key] ?? "";
|
||||
@ -364,14 +377,61 @@ function printTable(array $head, array $body) {
|
||||
printLine();
|
||||
|
||||
foreach ($body as $row) {
|
||||
$line = 0;
|
||||
foreach ($head as $key) {
|
||||
echo str_pad($row[$key] ?? "", $columns[$key]) . ' ';
|
||||
$width = min(max($maxWidth - $line, 0), $columns[$key]);
|
||||
$line += $width;
|
||||
echo str_pad($row[$key] ?? "", $width) . ' ';
|
||||
}
|
||||
printLine();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add missing api functions (should be all internal only i guess)
|
||||
function onSettings(array $argv) {
|
||||
$user = getUser() or die();
|
||||
$action = $argv[2] ?? "list";
|
||||
|
||||
if ($action === "list" || $action === "get") {
|
||||
$key = (($action === "list" || count($argv) < 4) ? null : $argv[3]);
|
||||
$req = new Api\Settings\Get($user);
|
||||
$success = $req->execute(["key" => $key]);
|
||||
if (!$success) {
|
||||
_exit("Error listings settings: " . $req->getLastError());
|
||||
} else {
|
||||
$settings = [];
|
||||
foreach ($req->getResult()["settings"] as $key => $value) {
|
||||
$settings[] = ["key" => $key, "value" => $value];
|
||||
}
|
||||
printTable(["key", "value"], $settings);
|
||||
}
|
||||
} else if ($action === "set" || $action === "update") {
|
||||
if (count($argv) < 5) {
|
||||
_exit("Usage: $argv[0] settings $argv[2] <key> <value>");
|
||||
} else {
|
||||
$key = $argv[3];
|
||||
$value = $argv[4];
|
||||
$req = new Api\Settings\Set($user);
|
||||
$success = $req->execute(["settings" => [$key => $value]]);
|
||||
if (!$success) {
|
||||
_exit("Error updating settings: " . $req->getLastError());
|
||||
}
|
||||
}
|
||||
} else if ($action === "unset" || $action === "delete") {
|
||||
if (count($argv) < 4) {
|
||||
_exit("Usage: $argv[0] settings $argv[2] <key>");
|
||||
} else {
|
||||
$key = $argv[3];
|
||||
$req = new Api\Settings\Set($user);
|
||||
$success = $req->execute(["settings" => [$key => null]]);
|
||||
if (!$success) {
|
||||
_exit("Error updating settings: " . $req->getLastError());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_exit("Usage: $argv[0] settings <get|set|unset>");
|
||||
}
|
||||
}
|
||||
|
||||
function onRoutes(array $argv) {
|
||||
|
||||
$user = getUser() or die();
|
||||
@ -459,7 +519,60 @@ function onRoutes(array $argv) {
|
||||
}
|
||||
|
||||
function onTest($argv) {
|
||||
$files = glob(WEBROOT . '/test/*.test.php');
|
||||
$requestedTests = array_filter(array_slice($argv, 2), function ($t) {
|
||||
return !startsWith($t, "-");
|
||||
});
|
||||
$verbose = in_array("-v", $requestedTests);
|
||||
|
||||
foreach ($files as $file) {
|
||||
include_once $file;
|
||||
$baseName = substr(basename($file), 0, - strlen(".test.php"));
|
||||
if (!empty($requestedTests) && !in_array($baseName, $requestedTests)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$className = $baseName . "Test";
|
||||
if (class_exists($className)) {
|
||||
echo "=== Running $className ===" . PHP_EOL;
|
||||
$testClass = new \PHPUnit\Framework\TestSuite();
|
||||
$testClass->addTestSuite($className);
|
||||
$result = $testClass->run();
|
||||
echo "Done after " . $result->time() . "s" . PHP_EOL;
|
||||
$stats = [
|
||||
"total" => $result->count(),
|
||||
"skipped" => $result->skippedCount(),
|
||||
"error" => $result->errorCount(),
|
||||
"failure" => $result->failureCount(),
|
||||
"warning" => $result->warningCount(),
|
||||
];
|
||||
|
||||
// Summary
|
||||
echo implode(", ", array_map(function ($key) use ($stats) {
|
||||
return "$key: " . $stats[$key];
|
||||
}, array_keys($stats))) . PHP_EOL;
|
||||
|
||||
$reports = array_merge($result->errors(), $result->failures());
|
||||
foreach ($reports as $error) {
|
||||
$exception = $error->thrownException();
|
||||
echo $error->toString();
|
||||
if ($verbose) {
|
||||
echo ". Stacktrace:" . PHP_EOL . $exception->getTraceAsString() . PHP_EOL;
|
||||
} else {
|
||||
$location = array_filter($exception->getTrace(), function ($t) use ($file) {
|
||||
return isset($t["file"]) && $t["file"] === $file;
|
||||
});
|
||||
$location = array_reverse($location);
|
||||
$location = array_pop($location);
|
||||
if ($location) {
|
||||
echo " in " . substr($location["file"], strlen(WEBROOT)) . "#" . $location["line"] . PHP_EOL;
|
||||
} else {
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMail($argv) {
|
||||
@ -507,6 +620,9 @@ switch ($command) {
|
||||
case 'mail':
|
||||
onMail($argv);
|
||||
break;
|
||||
case 'settings':
|
||||
onSettings($argv);
|
||||
break;
|
||||
default:
|
||||
printLine("Unknown command '$command'");
|
||||
printLine();
|
||||
|
@ -38,9 +38,14 @@ namespace Api\Mail {
|
||||
use Api\MailAPI;
|
||||
use Api\Parameter\Parameter;
|
||||
use Api\Parameter\StringType;
|
||||
use DateTimeInterface;
|
||||
use Driver\SQL\Column\Column;
|
||||
use Driver\SQL\Condition\Compare;
|
||||
use Driver\SQL\Condition\CondBool;
|
||||
use Driver\SQL\Condition\CondIn;
|
||||
use Driver\SQL\Condition\CondNot;
|
||||
use Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Driver\SQL\Strategy\UpdateStrategy;
|
||||
use External\PHPMailer\Exception;
|
||||
use External\PHPMailer\PHPMailer;
|
||||
@ -100,6 +105,14 @@ namespace Api\Mail {
|
||||
$subject = $this->getParam('subject');
|
||||
$replyTo = $this->getParam('replyTo');
|
||||
$replyName = $this->getParam('replyName');
|
||||
$body = $this->getParam('body');
|
||||
|
||||
if (stripos($body, "<body") === false) {
|
||||
$body = "<body>$body</body>";
|
||||
}
|
||||
if (stripos($body, "<html") === false) {
|
||||
$body = "<html>$body</html>";
|
||||
}
|
||||
|
||||
try {
|
||||
$mail = new PHPMailer;
|
||||
@ -119,9 +132,9 @@ namespace Api\Mail {
|
||||
$mail->Username = $mailConfig->getLogin();
|
||||
$mail->Password = $mailConfig->getPassword();
|
||||
$mail->SMTPSecure = 'tls';
|
||||
$mail->IsHTML(true);
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Body = $this->getParam('body');
|
||||
$mail->msgHTML($body);
|
||||
$mail->AltBody = strip_tags($body);
|
||||
|
||||
$this->success = @$mail->Send();
|
||||
if (!$this->success) {
|
||||
@ -212,7 +225,7 @@ namespace Api\Mail {
|
||||
if ($this->success && count($entityIds) > 0) {
|
||||
$sql->update("EntityLog")
|
||||
->set("modified", $sql->now())
|
||||
->where(new CondIn("entityId", $entityIds))
|
||||
->where(new CondIn(new Column("entityId"), $entityIds))
|
||||
->execute();
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ namespace Api\Notifications {
|
||||
use Api\NotificationsAPI;
|
||||
use Api\Parameter\Parameter;
|
||||
use Api\Parameter\StringType;
|
||||
use Driver\SQL\Column\Column;
|
||||
use Driver\SQL\Condition\Compare;
|
||||
use Driver\SQL\Condition\CondIn;
|
||||
use Driver\SQL\Query\Select;
|
||||
@ -252,7 +253,7 @@ namespace Api\Notifications {
|
||||
if ($this->success) {
|
||||
$res = $sql->update("GroupNotification")
|
||||
->set("seen", true)
|
||||
->where(new CondIn("group_id",
|
||||
->where(new CondIn(new Column("group_id"),
|
||||
$sql->select("group_id")
|
||||
->from("UserGroup")
|
||||
->where(new Compare("user_id", $this->user->getId()))))
|
||||
|
@ -61,8 +61,9 @@ namespace Api\Permission {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->user->isLoggedIn() || empty(array_intersect($groups, array_keys($this->user->getGroups())))) {
|
||||
header('HTTP 1.1 401 Unauthorized');
|
||||
$userGroups = $this->user->getGroups();
|
||||
if (empty($userGroups) || empty(array_intersect($groups, array_keys($this->user->getGroups())))) {
|
||||
http_response_code(401);
|
||||
return $this->createError("Permission denied.");
|
||||
}
|
||||
}
|
||||
@ -197,7 +198,7 @@ namespace Api\Permission {
|
||||
if ($this->success) {
|
||||
$res = $sql->delete("ApiPermission")
|
||||
->where(new Compare("description", "")) // only delete non default permissions
|
||||
->where(new CondNot(new CondIn("method", $insertedMethods)))
|
||||
->where(new CondNot(new CondIn(new Column("method"), $insertedMethods)))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
|
@ -45,6 +45,17 @@ class Request {
|
||||
}
|
||||
}
|
||||
|
||||
protected function allowMethod($method) {
|
||||
$availableMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "TRACE", "CONNECT"];
|
||||
if (in_array($method, $availableMethods) && !in_array($method, $this->allowedMethods)) {
|
||||
$this->allowedMethods[] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRequestMethod() {
|
||||
return $_SERVER["REQUEST_METHOD"];
|
||||
}
|
||||
|
||||
public function parseParams($values, $structure = NULL): bool {
|
||||
|
||||
if ($structure === NULL) {
|
||||
@ -80,6 +91,11 @@ class Request {
|
||||
}
|
||||
}
|
||||
|
||||
// wrapper for unit tests
|
||||
protected function _die(string $data = ""): bool {
|
||||
die($data);
|
||||
}
|
||||
|
||||
public function execute($values = array()): bool {
|
||||
$this->params = array_merge([], $this->defaultParams);
|
||||
$this->success = false;
|
||||
@ -98,7 +114,7 @@ class Request {
|
||||
$values = array_merge($values, $jsonData);
|
||||
} else {
|
||||
$this->lastError = 'Invalid request body.';
|
||||
header('HTTP 1.1 400 Bad Request');
|
||||
http_response_code(400);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -106,39 +122,48 @@ class Request {
|
||||
|
||||
if ($this->isDisabled) {
|
||||
$this->lastError = "This function is currently disabled.";
|
||||
http_response_code(503);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->externalCall && !$this->isPublic) {
|
||||
$this->lastError = 'This function is private.';
|
||||
header('HTTP 1.1 403 Forbidden');
|
||||
http_response_code(403);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->externalCall) {
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204); # No content
|
||||
header("Allow: OPTIONS, " . implode(", ", $this->allowedMethods));
|
||||
return $this->_die();
|
||||
}
|
||||
|
||||
// check the request method
|
||||
if (!in_array($_SERVER['REQUEST_METHOD'], $this->allowedMethods)) {
|
||||
$this->lastError = 'This method is not allowed';
|
||||
header('HTTP 1.1 405 Method Not Allowed');
|
||||
http_response_code(405);
|
||||
return false;
|
||||
}
|
||||
|
||||
$apiKeyAuthorized = false;
|
||||
|
||||
// Logged in or api key authorized?
|
||||
if ($this->loginRequired) {
|
||||
if (isset($_SERVER["HTTP_AUTHORIZATION"]) && $this->apiKeyAllowed) {
|
||||
if (!$this->user->isLoggedIn() && $this->apiKeyAllowed) {
|
||||
if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
|
||||
$authHeader = $_SERVER["HTTP_AUTHORIZATION"];
|
||||
if (startsWith($authHeader, "Bearer ")) {
|
||||
$apiKey = substr($authHeader, strlen("Bearer "));
|
||||
$apiKeyAuthorized = $this->user->authorize($apiKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logged in or api key authorized?
|
||||
if ($this->loginRequired) {
|
||||
if (!$this->user->isLoggedIn() && !$apiKeyAuthorized) {
|
||||
$this->lastError = 'You are not logged in.';
|
||||
header('HTTP 1.1 401 Unauthorized');
|
||||
http_response_code(401);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -149,7 +174,7 @@ class Request {
|
||||
// if it's not a call with API_KEY, check for csrf_token
|
||||
if (!isset($values["csrf_token"]) || strcmp($values["csrf_token"], $this->user->getSession()->getCsrfToken()) !== 0) {
|
||||
$this->lastError = "CSRF-Token mismatch";
|
||||
header('HTTP 1.1 403 Forbidden');
|
||||
http_response_code(403);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -235,9 +260,6 @@ class Request {
|
||||
}
|
||||
|
||||
protected function disableOutputBuffer() {
|
||||
header('X-Accel-Buffering: no');
|
||||
header("Cache-Control: no-transform, no-store, max-age=0");
|
||||
|
||||
ob_implicit_flush(true);
|
||||
$levels = ob_get_level();
|
||||
for ( $i = 0; $i < $levels; $i ++ ) {
|
||||
@ -245,4 +267,84 @@ class Request {
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
protected function setupSSE() {
|
||||
$this->user->getSQL()->close();
|
||||
$this->user->sendCookies();
|
||||
set_time_limit(0);
|
||||
ignore_user_abort(true);
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
header('Cache-Control: no-cache');
|
||||
|
||||
$this->disableOutputBuffer();
|
||||
}
|
||||
|
||||
protected function processImageUpload(string $uploadDir, array $allowedExtensions = ["jpg","jpeg","png","gif"], $transformCallback = null) {
|
||||
if (empty($_FILES)) {
|
||||
return $this->createError("You need to upload an image.");
|
||||
} else if (count($_FILES) > 1) {
|
||||
return $this->createError("You can only upload one image at once.");
|
||||
}
|
||||
|
||||
$upload = array_values($_FILES)[0];
|
||||
if (is_array($upload["name"])) {
|
||||
return $this->createError("You can only upload one image at once.");
|
||||
} else if ($upload["error"] !== UPLOAD_ERR_OK) {
|
||||
return $this->createError("There was an error uploading the image, code: " . $upload["error"]);
|
||||
}
|
||||
|
||||
$imageName = $upload["name"];
|
||||
$ext = strtolower(pathinfo($imageName, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
return $this->createError("Only the following file extensions are allowed: " . implode(",", $allowedExtensions));
|
||||
}
|
||||
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0777, true)) {
|
||||
return $this->createError("Upload directory does not exist and could not be created.");
|
||||
}
|
||||
|
||||
$srcPath = $upload["tmp_name"];
|
||||
$mimeType = mime_content_type($srcPath);
|
||||
if (!startsWith($mimeType, "image/")) {
|
||||
return $this->createError("Uploaded file is not an image.");
|
||||
}
|
||||
|
||||
try {
|
||||
$image = new \Imagick($srcPath);
|
||||
|
||||
// strip exif
|
||||
$profiles = $image->getImageProfiles("icc", true);
|
||||
$image->stripImage();
|
||||
if (!empty($profiles)) {
|
||||
$image->profileImage("icc", $profiles['icc']);
|
||||
}
|
||||
} catch (\ImagickException $ex) {
|
||||
return $this->createError("Error loading image: " . $ex->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
if ($transformCallback) {
|
||||
$fileName = call_user_func([$this, $transformCallback], $image, $uploadDir);
|
||||
} else {
|
||||
|
||||
$image->writeImage($srcPath);
|
||||
$image->destroy();
|
||||
|
||||
$uuid = uuidv4();
|
||||
$fileName = "$uuid.$ext";
|
||||
$destPath = "$uploadDir/$fileName";
|
||||
if (!file_exists($destPath)) {
|
||||
if (!@move_uploaded_file($srcPath, $destPath)) {
|
||||
return $this->createError("Could not store uploaded file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$fileName, $imageName];
|
||||
} catch (\ImagickException $ex) {
|
||||
return $this->createError("Error processing image: " . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -141,7 +141,7 @@ namespace Api\Settings {
|
||||
$res = $sql->select("name")
|
||||
->from("Settings")
|
||||
->where(new CondBool("readonly"))
|
||||
->where(new CondIn("name", $keys))
|
||||
->where(new CondIn(new Column("name"), $keys))
|
||||
->limit(1)
|
||||
->execute();
|
||||
|
||||
@ -158,7 +158,7 @@ namespace Api\Settings {
|
||||
private function deleteKeys(array $keys) {
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->delete("Settings")
|
||||
->where(new CondIn("name", $keys))
|
||||
->where(new CondIn(new Column("name"), $keys))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
|
78
core/Api/TemplateAPI.class.php
Normal file
78
core/Api/TemplateAPI.class.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Api {
|
||||
|
||||
use Objects\User;
|
||||
|
||||
abstract class TemplateAPI extends Request {
|
||||
function __construct(User $user, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($user, $externalCall, $params);
|
||||
$this->isPublic = false; // internal API
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Api\Template {
|
||||
|
||||
use Api\Parameter\ArrayType;
|
||||
use Api\Parameter\Parameter;
|
||||
use Api\Parameter\StringType;
|
||||
use Api\TemplateAPI;
|
||||
use Objects\User;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
class Render extends TemplateAPI {
|
||||
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, [
|
||||
"file" => new StringType("file"),
|
||||
"parameters" => new ArrayType("parameters", Parameter::TYPE_MIXED, false, true, [])
|
||||
]);
|
||||
}
|
||||
|
||||
public function execute($values = array()): bool {
|
||||
if (!parent::execute($values)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$templateFile = $this->getParam("file");
|
||||
$parameters = $this->getParam("parameters");
|
||||
$extension = pathinfo($templateFile, PATHINFO_EXTENSION);
|
||||
$allowedExtensions = ["html", "twig"];
|
||||
|
||||
if (!in_array($extension, $allowedExtensions)) {
|
||||
return $this->createError("Invalid template file extension. Allowed: " . implode(",", $allowedExtensions));
|
||||
}
|
||||
|
||||
$templateDir = WEBROOT . "/core/Templates/";
|
||||
$templateCache = WEBROOT . "/core/TemplateCache/";
|
||||
$path = realpath($templateDir . $templateFile);
|
||||
if (!startsWith($path, realpath($templateDir))) {
|
||||
return $this->createError("Template file not in template directory");
|
||||
} else if (!is_file($path)) {
|
||||
return $this->createError("Template file not found");
|
||||
}
|
||||
|
||||
$twigLoader = new FilesystemLoader($templateDir);
|
||||
$twigEnvironment = new Environment($twigLoader, [
|
||||
'cache' => $templateCache,
|
||||
'auto_reload' => true
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->result["html"] = $twigEnvironment->render($templateFile, $parameters);
|
||||
} catch (LoaderError | RuntimeError | SyntaxError $e) {
|
||||
return $this->createError("Error rendering twig template: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -6,7 +6,7 @@ namespace Api {
|
||||
|
||||
abstract class UserAPI extends Request {
|
||||
|
||||
protected function userExists(?string $username, ?string $email = null) {
|
||||
protected function userExists(?string $username, ?string $email = null): bool {
|
||||
|
||||
$conditions = array();
|
||||
if ($username) {
|
||||
@ -42,8 +42,8 @@ namespace Api {
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
protected function checkPasswordRequirements($password, $confirmPassword) {
|
||||
if(strcmp($password, $confirmPassword) !== 0) {
|
||||
protected function checkPasswordRequirements($password, $confirmPassword): bool {
|
||||
if ((($password === null) !== ($confirmPassword === null)) || strcmp($password, $confirmPassword) !== 0) {
|
||||
return $this->createError("The given passwords do not match");
|
||||
} else if(strlen($password) < 6) {
|
||||
return $this->createError("The password should be at least 6 characters long");
|
||||
@ -91,7 +91,8 @@ namespace Api {
|
||||
|
||||
protected function getUser($id) {
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->select("User.uid as userId", "User.name", "User.email", "User.registered_at", "User.confirmed",
|
||||
$res = $sql->select("User.uid as userId", "User.name", "User.fullName", "User.email",
|
||||
"User.registered_at", "User.confirmed", "User.last_online", "User.profilePicture",
|
||||
"Group.uid as groupId", "Group.name as groupName", "Group.color as groupColor")
|
||||
->from("User")
|
||||
->leftJoin("UserGroup", "User.uid", "UserGroup.user_id")
|
||||
@ -105,24 +106,6 @@ namespace Api {
|
||||
return ($this->success && !empty($res) ? $res : array());
|
||||
}
|
||||
|
||||
protected function getMessageTemplate($key) {
|
||||
$req = new \Api\Settings\Get($this->user);
|
||||
$this->success = $req->execute(array("key" => "^($key|mail_enabled)$"));
|
||||
$this->lastError = $req->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$settings = $req->getResult()["settings"];
|
||||
$isEnabled = ($settings["mail_enabled"] ?? "0") === "1";
|
||||
if (!$isEnabled) {
|
||||
return $this->createError("Mail is not enabled.");
|
||||
}
|
||||
|
||||
return $settings[$key] ?? "{{link}}";
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
protected function invalidateToken($token) {
|
||||
$this->user->getSQL()
|
||||
->update("UserToken")
|
||||
@ -142,6 +125,14 @@ namespace Api {
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
protected function formatDuration(int $count, string $string): string {
|
||||
if ($count === 1) {
|
||||
return $string;
|
||||
} else {
|
||||
return "the next $count ${string}s";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -150,12 +141,17 @@ namespace Api\User {
|
||||
|
||||
use Api\Parameter\Parameter;
|
||||
use Api\Parameter\StringType;
|
||||
use Api\Template\Render;
|
||||
use Api\UserAPI;
|
||||
use Api\VerifyCaptcha;
|
||||
use DateTime;
|
||||
use Driver\SQL\Column\Column;
|
||||
use Driver\SQL\Condition\Compare;
|
||||
use Driver\SQL\Condition\CondBool;
|
||||
use Driver\SQL\Condition\CondIn;
|
||||
use Driver\SQL\Condition\CondNot;
|
||||
use Driver\SQL\Expression\JsonArrayAgg;
|
||||
use ImagickException;
|
||||
use Objects\User;
|
||||
|
||||
class Create extends UserAPI {
|
||||
@ -239,10 +235,10 @@ namespace Api\User {
|
||||
$this->success = ($res !== NULL);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$ids = array();
|
||||
foreach($res as $row) $ids[] = $row["uid"];
|
||||
return $ids;
|
||||
if ($this->success && is_array($res)) {
|
||||
return array_map(function ($row) {
|
||||
return intval($row["uid"]);
|
||||
}, $res);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -274,11 +270,12 @@ namespace Api\User {
|
||||
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->select("User.uid as userId", "User.name", "User.email", "User.registered_at", "User.confirmed",
|
||||
"Group.uid as groupId", "Group.name as groupName", "Group.color as groupColor")
|
||||
"User.profilePicture", "User.fullName", "Group.uid as groupId", "User.last_online",
|
||||
"Group.name as groupName", "Group.color as groupColor")
|
||||
->from("User")
|
||||
->leftJoin("UserGroup", "User.uid", "UserGroup.user_id")
|
||||
->leftJoin("Group", "Group.uid", "UserGroup.group_id")
|
||||
->where(new CondIn("User.uid", $userIds))
|
||||
->where(new CondIn(new Column("User.uid"), $userIds))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
@ -291,15 +288,29 @@ namespace Api\User {
|
||||
$groupId = intval($row["groupId"]);
|
||||
$groupName = $row["groupName"];
|
||||
$groupColor = $row["groupColor"];
|
||||
|
||||
$fullInfo = ($userId === $this->user->getId()) ||
|
||||
($this->user->hasGroup(USER_GROUP_ADMIN) || $this->user->hasGroup(USER_GROUP_SUPPORT));
|
||||
|
||||
if (!isset($this->result["users"][$userId])) {
|
||||
$this->result["users"][$userId] = array(
|
||||
$user = array(
|
||||
"uid" => $userId,
|
||||
"name" => $row["name"],
|
||||
"fullName" => $row["fullName"],
|
||||
"profilePicture" => $row["profilePicture"],
|
||||
"email" => $row["email"],
|
||||
"registered_at" => $row["registered_at"],
|
||||
"confirmed" => $sql->parseBool($row["confirmed"]),
|
||||
"groups" => array(),
|
||||
);
|
||||
|
||||
if ($fullInfo) {
|
||||
$user["registered_at"] = $row["registered_at"];
|
||||
$user["last_online"] = $row["last_online"];
|
||||
} else if (!$sql->parseBool($row["confirmed"])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->result["users"][$userId] = $user;
|
||||
}
|
||||
|
||||
if (!is_null($groupId)) {
|
||||
@ -323,6 +334,7 @@ namespace Api\User {
|
||||
parent::__construct($user, $externalCall, array(
|
||||
'id' => new Parameter('id', Parameter::TYPE_INT)
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function execute($values = array()): bool {
|
||||
@ -331,30 +343,79 @@ namespace Api\User {
|
||||
}
|
||||
|
||||
$sql = $this->user->getSQL();
|
||||
$id = $this->getParam("id");
|
||||
$user = $this->getUser($id);
|
||||
|
||||
$userId = $this->getParam("id");
|
||||
$user = $this->getUser($userId);
|
||||
if ($this->success) {
|
||||
if (empty($user)) {
|
||||
return $this->createError("User not found");
|
||||
} else {
|
||||
$this->result["user"] = array(
|
||||
"uid" => $user[0]["userId"],
|
||||
|
||||
$queriedUser = array(
|
||||
"uid" => $userId,
|
||||
"name" => $user[0]["name"],
|
||||
"fullName" => $user[0]["fullName"],
|
||||
"email" => $user[0]["email"],
|
||||
"registered_at" => $user[0]["registered_at"],
|
||||
"last_online" => $user[0]["last_online"],
|
||||
"profilePicture" => $user[0]["profilePicture"],
|
||||
"confirmed" => $sql->parseBool($user["0"]["confirmed"]),
|
||||
"groups" => array()
|
||||
"groups" => array(),
|
||||
);
|
||||
|
||||
foreach($user as $row) {
|
||||
if (!is_null($row["groupId"])) {
|
||||
$this->result["user"]["groups"][$row["groupId"]] = array(
|
||||
$queriedUser["groups"][$row["groupId"]] = array(
|
||||
"name" => $row["groupName"],
|
||||
"color" => $row["groupColor"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// either we are querying own info or we are internal employees
|
||||
// as internal employees can add arbitrary users to projects
|
||||
$canView = ($userId === $this->user->getId() ||
|
||||
$this->user->hasGroup(USER_GROUP_ADMIN) ||
|
||||
$this->user->hasGroup(USER_GROUP_SUPPORT));
|
||||
|
||||
// full info only when we have administrative privileges, or we are querying ourselves
|
||||
$fullInfo = ($userId === $this->user->getId()) ||
|
||||
($this->user->hasGroup(USER_GROUP_ADMIN) || $this->user->hasGroup(USER_GROUP_SUPPORT));
|
||||
|
||||
if (!$canView) {
|
||||
$res = $sql->select(new JsonArrayAgg(new Column("projectId"), "projectIds"))
|
||||
->from("ProjectMember")
|
||||
->where(new Compare("userId", $this->user->getId()), new Compare("userId", $userId))
|
||||
->groupBy("projectId")
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success ) {
|
||||
return false;
|
||||
} else if (is_array($res)) {
|
||||
foreach ($res as $row) {
|
||||
if (count(json_decode($row["projectIds"])) > 1) {
|
||||
$canView = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$canView) {
|
||||
return $this->createError("No permissions to access this user");
|
||||
}
|
||||
|
||||
if (!$fullInfo) {
|
||||
if (!$queriedUser["confirmed"]) {
|
||||
return $this->createError("No permissions to access this user");
|
||||
}
|
||||
unset($queriedUser["registered_at"]);
|
||||
unset($queriedUser["confirmed"]);
|
||||
unset($queriedUser["last_online"]);
|
||||
}
|
||||
|
||||
$this->result["user"] = $queriedUser;
|
||||
}
|
||||
}
|
||||
|
||||
@ -424,11 +485,6 @@ namespace Api\User {
|
||||
return false;
|
||||
}
|
||||
|
||||
$messageBody = $this->getMessageTemplate("message_accept_invite");
|
||||
if ($messageBody === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create user
|
||||
$id = $this->insertUser($username, $email, "", false);
|
||||
if (!$this->success) {
|
||||
@ -437,7 +493,8 @@ namespace Api\User {
|
||||
|
||||
// Create Token
|
||||
$token = generateRandomString(36);
|
||||
$valid_until = (new DateTime())->modify("+7 day");
|
||||
$validDays = 7;
|
||||
$valid_until = (new DateTime())->modify("+$validDays day");
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->insert("UserToken", array("user_id", "token", "token_type", "valid_until"))
|
||||
->addRow($id, $token, "invite", $valid_until)
|
||||
@ -449,29 +506,34 @@ namespace Api\User {
|
||||
if ($this->success) {
|
||||
|
||||
$settings = $this->user->getConfiguration()->getSettings();
|
||||
$baseUrl = htmlspecialchars($settings->getBaseUrl());
|
||||
$siteName = htmlspecialchars($settings->getSiteName());
|
||||
$baseUrl = $settings->getBaseUrl();
|
||||
$siteName = $settings->getSiteName();
|
||||
|
||||
$replacements = array(
|
||||
"link" => "$baseUrl/acceptInvite?token=$token",
|
||||
"site_name" => $siteName,
|
||||
"base_url" => $baseUrl,
|
||||
"username" => htmlspecialchars($username)
|
||||
);
|
||||
$req = new Render($this->user);
|
||||
$this->success = $req->execute([
|
||||
"file" => "mail/accept_invite.twig",
|
||||
"parameters" => [
|
||||
"link" => "$baseUrl/acceptInvite?token=$token",
|
||||
"site_name" => $siteName,
|
||||
"base_url" => $baseUrl,
|
||||
"username" => $username,
|
||||
"valid_time" => $this->formatDuration($validDays, "day")
|
||||
]
|
||||
]);
|
||||
$this->lastError = $req->getLastError();
|
||||
|
||||
foreach($replacements as $key => $value) {
|
||||
$messageBody = str_replace("{{{$key}}}", $value, $messageBody);
|
||||
if ($this->success) {
|
||||
$messageBody = $req->getResult()["html"];
|
||||
$request = new \Api\Mail\Send($this->user);
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] Account Invitation",
|
||||
"body" => $messageBody
|
||||
));
|
||||
|
||||
$this->lastError = $request->getLastError();
|
||||
}
|
||||
|
||||
$request = new \Api\Mail\Send($this->user);
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] Account Invitation",
|
||||
"body" => $messageBody
|
||||
));
|
||||
|
||||
$this->lastError = $request->getLastError();
|
||||
|
||||
if (!$this->success) {
|
||||
$this->lastError = "The invitation was created but the confirmation email could not be sent. " .
|
||||
"Please contact the server administration. Reason: " . $this->lastError;
|
||||
@ -607,7 +669,7 @@ namespace Api\User {
|
||||
|
||||
public function __construct($user, $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, array(
|
||||
'username' => new StringType('username', 32),
|
||||
'username' => new StringType('username'),
|
||||
'password' => new StringType('password'),
|
||||
'stayLoggedIn' => new Parameter('stayLoggedIn', Parameter::TYPE_BOOLEAN, true, true)
|
||||
));
|
||||
@ -641,7 +703,8 @@ namespace Api\User {
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->select("User.uid", "User.password", "User.confirmed")
|
||||
->from("User")
|
||||
->where(new Compare("User.name", $username))
|
||||
->where(new Compare("User.name", $username), new Compare("User.email", $username))
|
||||
->limit(1)
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
@ -753,35 +816,38 @@ namespace Api\User {
|
||||
return false;
|
||||
}
|
||||
|
||||
$messageBody = $this->getMessageTemplate("message_confirm_email");
|
||||
if ($messageBody === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->userId = $this->insertUser($username, $email, $password, false);
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// add internal group
|
||||
$this->user->getSQL()->insert("UserGroup", ["user_id", "group_id"])
|
||||
->addRow($this->userId, USER_GROUP_INTERNAL)
|
||||
->execute();
|
||||
|
||||
$validHours = 48;
|
||||
$this->token = generateRandomString(36);
|
||||
if ($this->insertToken($this->userId, $this->token, "email_confirm", 48)) {
|
||||
if ($this->insertToken($this->userId, $this->token, "email_confirm", $validHours)) {
|
||||
|
||||
$settings = $this->user->getConfiguration()->getSettings();
|
||||
$baseUrl = htmlspecialchars($settings->getBaseUrl());
|
||||
$siteName = htmlspecialchars($settings->getSiteName());
|
||||
|
||||
if ($this->success) {
|
||||
|
||||
$replacements = array(
|
||||
$baseUrl = $settings->getBaseUrl();
|
||||
$siteName = $settings->getSiteName();
|
||||
$req = new Render($this->user);
|
||||
$this->success = $req->execute([
|
||||
"file" => "mail/confirm_email.twig",
|
||||
"parameters" => [
|
||||
"link" => "$baseUrl/confirmEmail?token=$this->token",
|
||||
"site_name" => $siteName,
|
||||
"base_url" => $baseUrl,
|
||||
"username" => htmlspecialchars($username)
|
||||
);
|
||||
|
||||
foreach($replacements as $key => $value) {
|
||||
$messageBody = str_replace("{{{$key}}}", $value, $messageBody);
|
||||
}
|
||||
"username" => $username,
|
||||
"valid_time" => $this->formatDuration($validHours, "hour")
|
||||
]
|
||||
]);
|
||||
$this->lastError = $req->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$messageBody = $req->getResult()["html"];
|
||||
$request = new \Api\Mail\Send($this->user);
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
@ -862,6 +928,7 @@ namespace Api\User {
|
||||
parent::__construct($user, $externalCall, array(
|
||||
'id' => new Parameter('id', Parameter::TYPE_INT),
|
||||
'username' => new StringType('username', 32, true, NULL),
|
||||
'fullName' => new StringType('fullName', 64, true, NULL),
|
||||
'email' => new Parameter('email', Parameter::TYPE_EMAIL, true, NULL),
|
||||
'password' => new StringType('password', -1, true, NULL),
|
||||
'groups' => new Parameter('groups', Parameter::TYPE_ARRAY, true, NULL),
|
||||
@ -886,6 +953,7 @@ namespace Api\User {
|
||||
}
|
||||
|
||||
$username = $this->getParam("username");
|
||||
$fullName = $this->getParam("fullName");
|
||||
$email = $this->getParam("email");
|
||||
$password = $this->getParam("password");
|
||||
$groups = $this->getParam("groups");
|
||||
@ -913,6 +981,7 @@ namespace Api\User {
|
||||
|
||||
// Check for duplicate username, email
|
||||
$usernameChanged = !is_null($username) && strcasecmp($username, $user[0]["name"]) !== 0;
|
||||
$fullNameChanged = !is_null($fullName) && strcasecmp($fullName, $user[0]["fullName"]) !== 0;
|
||||
$emailChanged = !is_null($email) && strcasecmp($email, $user[0]["email"]) !== 0;
|
||||
if($usernameChanged || $emailChanged) {
|
||||
if (!$this->userExists($usernameChanged ? $username : NULL, $emailChanged ? $email : NULL)) {
|
||||
@ -924,6 +993,7 @@ namespace Api\User {
|
||||
$query = $sql->update("User");
|
||||
|
||||
if ($usernameChanged) $query->set("name", $username);
|
||||
if ($fullNameChanged) $query->set("fullName", $fullName);
|
||||
if ($emailChanged) $query->set("email", $email);
|
||||
if (!is_null($password)) $query->set("password", $this->hashPassword($password));
|
||||
|
||||
@ -1028,50 +1098,51 @@ namespace Api\User {
|
||||
}
|
||||
}
|
||||
|
||||
$messageBody = $this->getMessageTemplate("message_reset_password");
|
||||
if ($messageBody === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$email = $this->getParam("email");
|
||||
$user = $this->findUser($email);
|
||||
if ($user === false) {
|
||||
if ($this->success === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user !== null) {
|
||||
$validHours = 1;
|
||||
$token = generateRandomString(36);
|
||||
if (!$this->insertToken($user["uid"], $token, "password_reset", 1)) {
|
||||
if (!$this->insertToken($user["uid"], $token, "password_reset", $validHours)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$baseUrl = htmlspecialchars($settings->getBaseUrl());
|
||||
$siteName = htmlspecialchars($settings->getSiteName());
|
||||
$baseUrl = $settings->getBaseUrl();
|
||||
$siteName = $settings->getSiteName();
|
||||
|
||||
$replacements = array(
|
||||
"link" => "$baseUrl/resetPassword?token=$token",
|
||||
"site_name" => $siteName,
|
||||
"base_url" => $baseUrl,
|
||||
"username" => htmlspecialchars($user["name"])
|
||||
);
|
||||
$req = new Render($this->user);
|
||||
$this->success = $req->execute([
|
||||
"file" => "mail/reset_password.twig",
|
||||
"parameters" => [
|
||||
"link" => "$baseUrl/resetPassword?token=$token",
|
||||
"site_name" => $siteName,
|
||||
"base_url" => $baseUrl,
|
||||
"username" => $user["name"],
|
||||
"valid_time" => $this->formatDuration($validHours, "hour")
|
||||
]
|
||||
]);
|
||||
$this->lastError = $req->getLastError();
|
||||
|
||||
foreach($replacements as $key => $value) {
|
||||
$messageBody = str_replace("{{{$key}}}", $value, $messageBody);
|
||||
if ($this->success) {
|
||||
$messageBody = $req->getResult()["html"];
|
||||
$request = new \Api\Mail\Send($this->user);
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] Password Reset",
|
||||
"body" => $messageBody
|
||||
));
|
||||
$this->lastError = $request->getLastError();
|
||||
}
|
||||
|
||||
$request = new \Api\Mail\Send($this->user);
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] Password Reset",
|
||||
"body" => $messageBody
|
||||
));
|
||||
$this->lastError = $request->getLastError();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function findUser($email) {
|
||||
private function findUser($email): ?array {
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->select("User.uid", "User.name")
|
||||
->from("User")
|
||||
@ -1082,14 +1153,12 @@ namespace Api\User {
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if ($this->success) {
|
||||
if (empty($res)) {
|
||||
return null;
|
||||
} else {
|
||||
if (!empty($res)) {
|
||||
return $res[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1125,11 +1194,6 @@ namespace Api\User {
|
||||
}
|
||||
}
|
||||
|
||||
$messageBody = $this->getMessageTemplate("message_confirm_email");
|
||||
if ($messageBody === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$email = $this->getParam("email");
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->select("User.uid", "User.name", "UserToken.token", "UserToken.token_type", "UserToken.used")
|
||||
@ -1157,36 +1221,49 @@ namespace Api\User {
|
||||
}))
|
||||
);
|
||||
|
||||
$validHours = 48;
|
||||
if (!$token) {
|
||||
// no token generated yet, let's generate one
|
||||
$token = generateRandomString(36);
|
||||
if (!$this->insertToken($userId, $token, "email_confirm", 48)) {
|
||||
if (!$this->insertToken($userId, $token, "email_confirm", $validHours)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$sql->update("UserToken")
|
||||
->set("valid_until", (new DateTime())->modify("+$validHours hour"))
|
||||
->where(new Compare("token", $token))
|
||||
->execute();
|
||||
}
|
||||
|
||||
$username = $res[0]["name"];
|
||||
$baseUrl = htmlspecialchars($settings->getBaseUrl());
|
||||
$siteName = htmlspecialchars($settings->getSiteName());
|
||||
$replacements = array(
|
||||
"link" => "$baseUrl/confirmEmail?token=$token",
|
||||
"site_name" => $siteName,
|
||||
"base_url" => $baseUrl,
|
||||
"username" => htmlspecialchars($username)
|
||||
);
|
||||
$baseUrl = $settings->getBaseUrl();
|
||||
$siteName = $settings->getSiteName();
|
||||
|
||||
foreach($replacements as $key => $value) {
|
||||
$messageBody = str_replace("{{{$key}}}", $value, $messageBody);
|
||||
$req = new Render($this->user);
|
||||
$this->success = $req->execute([
|
||||
"file" => "mail/confirm_email.twig",
|
||||
"parameters" => [
|
||||
"link" => "$baseUrl/confirmEmail?token=$token",
|
||||
"site_name" => $siteName,
|
||||
"base_url" => $baseUrl,
|
||||
"username" => $username,
|
||||
"valid_time" => $this->formatDuration($validHours, "hour")
|
||||
]
|
||||
]);
|
||||
$this->lastError = $req->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$messageBody = $req->getResult()["html"];
|
||||
$request = new \Api\Mail\Send($this->user);
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] E-Mail Confirmation",
|
||||
"body" => $messageBody
|
||||
));
|
||||
|
||||
$this->lastError = $request->getLastError();
|
||||
}
|
||||
|
||||
$request = new \Api\Mail\Send($this->user);
|
||||
$this->success = $request->execute(array(
|
||||
"to" => $email,
|
||||
"subject" => "[$siteName] E-Mail Confirmation",
|
||||
"body" => $messageBody
|
||||
));
|
||||
|
||||
$this->lastError = $request->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
@ -1203,7 +1280,7 @@ namespace Api\User {
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
private function updateUser($uid, $password) {
|
||||
private function updateUser($uid, $password): bool {
|
||||
$sql = $this->user->getSQL();
|
||||
$res = $sql->update("User")
|
||||
->set("password", $this->hashPassword($password))
|
||||
@ -1254,7 +1331,10 @@ namespace Api\User {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, array(
|
||||
'username' => new StringType('username', 32, true, NULL),
|
||||
'fullName' => new StringType('fullName', 64, true, NULL),
|
||||
'password' => new StringType('password', -1, true, NULL),
|
||||
'confirmPassword' => new StringType('confirmPassword', -1, true, NULL),
|
||||
'oldPassword' => new StringType('oldPassword', -1, true, NULL),
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = true;
|
||||
@ -1267,14 +1347,17 @@ namespace Api\User {
|
||||
}
|
||||
|
||||
$newUsername = $this->getParam("username");
|
||||
$oldPassword = $this->getParam("oldPassword");
|
||||
$newPassword = $this->getParam("password");
|
||||
$newPasswordConfirm = $this->getParam("confirmPassword");
|
||||
$newFullName = $this->getParam("fullName");
|
||||
|
||||
if ($newUsername === null && $newPassword === null) {
|
||||
return $this->createError("You must either provide an updated username or password");
|
||||
if ($newUsername === null && $newPassword === null && $newPasswordConfirm === null && $newFullName === null) {
|
||||
return $this->createError("You must either provide an updated username, fullName or password");
|
||||
}
|
||||
|
||||
$sql = $this->user->getSQL();
|
||||
$query = $sql->update("User")->where(new Compare("id", $this->user->getId()));
|
||||
$query = $sql->update("User")->where(new Compare("uid", $this->user->getId()));
|
||||
if ($newUsername !== null) {
|
||||
if (!$this->checkUsernameRequirements($newUsername) || $this->userExists($newUsername)) {
|
||||
return false;
|
||||
@ -1283,10 +1366,29 @@ namespace Api\User {
|
||||
}
|
||||
}
|
||||
|
||||
if ($newPassword !== null) { // TODO: confirm password?
|
||||
if (!$this->checkPasswordRequirements($newPassword, $newPassword)) {
|
||||
if ($newFullName !== null) {
|
||||
$query->set("fullName", $newFullName);
|
||||
}
|
||||
|
||||
if ($newPassword !== null || $newPasswordConfirm !== null) {
|
||||
if (!$this->checkPasswordRequirements($newPassword, $newPasswordConfirm)) {
|
||||
return false;
|
||||
} else {
|
||||
$res = $sql->select("password")
|
||||
->from("User")
|
||||
->where(new Compare("uid", $this->user->getId()))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password_verify($oldPassword, $res[0]["password"])) {
|
||||
return $this->createError("Wrong password");
|
||||
}
|
||||
|
||||
$query->set("password", $this->hashPassword($newPassword));
|
||||
}
|
||||
}
|
||||
@ -1296,4 +1398,152 @@ namespace Api\User {
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class UploadPicture extends UserAPI {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, [
|
||||
"scale" => new Parameter("scale", Parameter::TYPE_FLOAT, true, NULL),
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
$this->forbidMethod("GET");
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ImagickException
|
||||
*/
|
||||
protected function onTransform(\Imagick $im, $uploadDir) {
|
||||
|
||||
$minSize = 75;
|
||||
$maxSize = 500;
|
||||
|
||||
$width = $im->getImageWidth();
|
||||
$height = $im->getImageHeight();
|
||||
$doResize = false;
|
||||
|
||||
if ($width < $minSize || $height < $minSize) {
|
||||
if ($width < $height) {
|
||||
$newWidth = $minSize;
|
||||
$newHeight = intval(($minSize / $width) * $height);
|
||||
} else {
|
||||
$newHeight = $minSize;
|
||||
$newWidth = intval(($minSize / $height) * $width);
|
||||
}
|
||||
|
||||
$doResize = true;
|
||||
} else if ($width > $maxSize || $height > $maxSize) {
|
||||
if ($width > $height) {
|
||||
$newWidth = $maxSize;
|
||||
$newHeight = intval($height * ($maxSize / $width));
|
||||
} else {
|
||||
$newHeight = $maxSize;
|
||||
$newWidth = intval($width * ($maxSize / $height));
|
||||
}
|
||||
|
||||
$doResize = true;
|
||||
} else {
|
||||
$newWidth = $width;
|
||||
$newHeight = $height;
|
||||
}
|
||||
|
||||
if ($width < $minSize || $height < $minSize) {
|
||||
return $this->createError("Error processing image. Bad dimensions.");
|
||||
}
|
||||
|
||||
if ($doResize) {
|
||||
$width = $newWidth;
|
||||
$height = $newHeight;
|
||||
$im->resizeImage($width, $height, \Imagick::FILTER_SINC, 1);
|
||||
}
|
||||
|
||||
$size = $this->getParam("size");
|
||||
if (is_null($size)) {
|
||||
$size = min($width, $height);
|
||||
}
|
||||
|
||||
$offset = [$this->getParam("offsetX"), $this->getParam("offsetY")];
|
||||
if ($size < $minSize or $size > $maxSize) {
|
||||
return $this->createError("Invalid size. Must be in range of $minSize-$maxSize.");
|
||||
}/* else if ($offset[0] < 0 || $offset[1] < 0 || $offset[0]+$size > $width || $offset[1]+$size > $height) {
|
||||
return $this->createError("Offsets out of bounds.");
|
||||
}*/
|
||||
|
||||
if ($offset[0] !== 0 || $offset[1] !== 0 || $size !== $width || $size !== $height) {
|
||||
$im->cropImage($size, $size, $offset[0], $offset[1]);
|
||||
}
|
||||
|
||||
$fileName = uuidv4() . ".jpg";
|
||||
$im->writeImage("$uploadDir/$fileName");
|
||||
$im->destroy();
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
public function execute($values = array()): bool {
|
||||
if (!parent::execute($values)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userId = $this->user->getId();
|
||||
$uploadDir = WEBROOT . "/img/uploads/user/$userId";
|
||||
list ($fileName, $imageName) = $this->processImageUpload($uploadDir, ["png","jpg","jpeg"], "onTransform");
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldPfp = $this->user->getProfilePicture();
|
||||
if ($oldPfp) {
|
||||
$path = "$uploadDir/$oldPfp";
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = $this->user->getSQL();
|
||||
$this->success = $sql->update("User")
|
||||
->set("profilePicture", $fileName)
|
||||
->where(new Compare("uid", $userId))
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
if ($this->success) {
|
||||
$this->result["profilePicture"] = $fileName;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class RemovePicture extends UserAPI {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, []);
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function execute($values = array()): bool {
|
||||
if (!parent::execute($values)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pfp = $this->user->getProfilePicture();
|
||||
if (!$pfp) {
|
||||
return $this->createError("You did not upload a profile picture yet");
|
||||
}
|
||||
|
||||
$userId = $this->user->getId();
|
||||
$sql = $this->user->getSQL();
|
||||
$this->success = $sql->update("User")
|
||||
->set("profilePicture", NULL)
|
||||
->where(new Compare("uid", $userId))
|
||||
->execute();
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$path = WEBROOT . "/img/uploads/user/$userId/$pfp";
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ class Settings {
|
||||
if ($success) {
|
||||
$result = $req->getResult()["settings"];
|
||||
$this->siteName = $result["site_name"] ?? $this->siteName;
|
||||
$this->baseUrl = $result["base_url"] ?? $this->baseUrl;
|
||||
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
|
||||
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
|
||||
$this->jwtSecret = $result["jwt_secret"] ?? $this->jwtSecret;
|
||||
|
@ -1,74 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Documents {
|
||||
|
||||
use Documents\Account\AccountBody;
|
||||
use Documents\Account\AccountHead;
|
||||
use Elements\Document;
|
||||
use Objects\User;
|
||||
namespace Documents;
|
||||
|
||||
class Account extends Document {
|
||||
public function __construct(User $user, ?string $view) {
|
||||
parent::__construct($user, AccountHead::class, AccountBody::class, $view);
|
||||
}
|
||||
}
|
||||
}
|
||||
use Elements\TemplateDocument;
|
||||
use Objects\User;
|
||||
|
||||
namespace Documents\Account {
|
||||
|
||||
use Elements\Head;
|
||||
use Elements\Link;
|
||||
use Elements\Script;
|
||||
use Elements\SimpleBody;
|
||||
|
||||
class AccountHead extends Head {
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
protected function initSources() {
|
||||
$this->loadJQuery();
|
||||
$this->addJS(Script::CORE);
|
||||
$this->addJS(Script::ACCOUNT);
|
||||
$this->loadBootstrap();
|
||||
$this->loadFontawesome();
|
||||
$this->addCSS(Link::CORE);
|
||||
}
|
||||
|
||||
protected function initMetas(): array {
|
||||
return array(
|
||||
array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'),
|
||||
array('name' => 'format-detection', 'content' => 'telephone=yes'),
|
||||
array('charset' => 'utf-8'),
|
||||
array("http-equiv" => 'expires', 'content' => '0'),
|
||||
array("name" => 'robots', 'content' => 'noarchive'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function initRawFields(): array {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function initTitle(): string {
|
||||
return "Account";
|
||||
}
|
||||
class Account extends TemplateDocument {
|
||||
public function __construct(User $user, ?string $template) {
|
||||
parent::__construct($user, $template);
|
||||
$this->enableCSP();
|
||||
}
|
||||
|
||||
class AccountBody extends SimpleBody {
|
||||
private function createError(string $message) {
|
||||
$this->parameters["view"]["success"] = false;
|
||||
$this->parameters["view"]["message"] = $message;
|
||||
}
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
protected function getContent(): string {
|
||||
|
||||
$view = $this->getDocument()->getView();
|
||||
if ($view === null) {
|
||||
return "The page you does not exist or is no longer valid. <a href='/'>Return to start page</a>";
|
||||
protected function loadParameters() {
|
||||
$this->parameters["view"] = ["success" => true];
|
||||
if ($this->getTemplateName() === "account/reset_password.twig") {
|
||||
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
|
||||
$this->parameters["view"]["token"] = $_GET["token"];
|
||||
$req = new \Api\User\CheckToken($this->getUser());
|
||||
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
|
||||
if ($this->parameters["view"]["success"]) {
|
||||
if (strcmp($req->getResult()["token"]["type"], "password_reset") !== 0) {
|
||||
$this->createError("The given token has a wrong type.");
|
||||
}
|
||||
} else {
|
||||
$this->createError("Error requesting password reset: " . $req->getLastError());
|
||||
}
|
||||
}
|
||||
} else if ($this->getTemplateName() === "account/register.twig") {
|
||||
$settings = $this->user->getConfiguration()->getSettings();
|
||||
if ($this->user->isLoggedIn()) {
|
||||
$this->createError("You are already logged in.");
|
||||
} else if (!$settings->isRegistrationAllowed()) {
|
||||
$this->createError("Registration is not enabled on this website.");
|
||||
}
|
||||
} else if ($this->getTemplateName() === "account/accept_invite.twig") {
|
||||
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
|
||||
$this->parameters["view"]["token"] = $_GET["token"];
|
||||
$req = new \Api\User\CheckToken($this->getUser());
|
||||
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
|
||||
if ($this->parameters["view"]["success"]) {
|
||||
if (strcmp($req->getResult()["token"]["type"], "invite") !== 0) {
|
||||
$this->createError("The given token has a wrong type.");
|
||||
} else {
|
||||
$this->parameters["view"]["invited_user"] = $req->getResult()["user"];
|
||||
}
|
||||
} else {
|
||||
$this->createError("Error confirming e-mail address: " . $req->getLastError());
|
||||
}
|
||||
} else {
|
||||
$this->createError("The link you visited is no longer valid");
|
||||
}
|
||||
|
||||
return $view->getCode();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Documents {
|
||||
namespace Documents;
|
||||
|
||||
use Documents\Admin\AdminHead;
|
||||
use Elements\Document;
|
||||
use Objects\User;
|
||||
use Views\Admin\AdminDashboardBody;
|
||||
use Views\Admin\LoginBody;
|
||||
use Elements\TemplateDocument;
|
||||
use Objects\User;
|
||||
|
||||
class Admin extends Document {
|
||||
public function __construct(User $user, ?string $view = NULL) {
|
||||
$body = $user->isLoggedIn() ? AdminDashboardBody::class : LoginBody::class;
|
||||
parent::__construct($user, AdminHead::class, $body, $view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Documents\Admin {
|
||||
|
||||
use Elements\Head;
|
||||
|
||||
class AdminHead extends Head {
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
protected function initSources() {
|
||||
$this->loadFontawesome();
|
||||
}
|
||||
|
||||
protected function initMetas(): array {
|
||||
return array(
|
||||
array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'),
|
||||
array('name' => 'format-detection', 'content' => 'telephone=yes'),
|
||||
array('charset' => 'utf-8'),
|
||||
array("http-equiv" => 'expires', 'content' => '0'),
|
||||
array("name" => 'robots', 'content' => 'noarchive'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function initRawFields(): array {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function initTitle(): string {
|
||||
return $this->getSiteName() . " - Administration";
|
||||
}
|
||||
class Admin extends TemplateDocument {
|
||||
public function __construct(User $user) {
|
||||
$template = $user->isLoggedIn() ? "admin.twig" : "redirect.twig";
|
||||
$params = $user->isLoggedIn() ? [] : ["url" => "/login"];
|
||||
parent::__construct($user, $template, $params);
|
||||
$this->enableCSP();
|
||||
}
|
||||
}
|
@ -1,64 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Documents {
|
||||
namespace Documents;
|
||||
|
||||
use Documents\Document404\Body404;
|
||||
use Documents\Document404\Head404;
|
||||
use Elements\Document;
|
||||
use Elements\TemplateDocument;
|
||||
use Objects\User;
|
||||
|
||||
class Document404 extends Document {
|
||||
public function __construct($user, ?string $view = NULL) {
|
||||
parent::__construct($user, Head404::class, Body404::class, $view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Documents\Document404 {
|
||||
|
||||
use Elements\Head;
|
||||
use Elements\SimpleBody;
|
||||
use Views\View404;
|
||||
|
||||
class Head404 extends Head {
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
protected function initSources() {
|
||||
}
|
||||
|
||||
protected function initMetas(): array {
|
||||
return array(
|
||||
array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'),
|
||||
array('name' => 'format-detection', 'content' => 'telephone=yes'),
|
||||
array('charset' => 'utf-8'),
|
||||
array("http-equiv" => 'expires', 'content' => '0'),
|
||||
array("name" => 'robots', 'content' => 'noarchive'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function initRawFields(): array {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function initTitle(): string {
|
||||
return "WebBase - Not Found";
|
||||
}
|
||||
}
|
||||
|
||||
class Body404 extends SimpleBody {
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
public function loadView() {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
protected function getContent(): string {
|
||||
return $this->load(View404::class);
|
||||
}
|
||||
class Document404 extends TemplateDocument {
|
||||
|
||||
public function __construct(User $user) {
|
||||
parent::__construct($user, "404.twig");
|
||||
}
|
||||
|
||||
public function loadParameters() {
|
||||
parent::loadParameters();
|
||||
http_response_code(404);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ namespace Documents {
|
||||
|
||||
use Documents\Install\InstallBody;
|
||||
use Documents\Install\InstallHead;
|
||||
use Elements\Document;
|
||||
use Elements\HtmlDocument;
|
||||
|
||||
class Install extends Document {
|
||||
class Install extends HtmlDocument {
|
||||
public function __construct($user) {
|
||||
parent::__construct($user, InstallHead::class, InstallBody::class);
|
||||
$this->databaseRequired = false;
|
||||
|
13
core/Driver/SQL/Column/BigIntColumn.php
Normal file
13
core/Driver/SQL/Column/BigIntColumn.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Driver\SQL\Query;
|
||||
|
||||
use Driver\SQL\Column\IntColumn;
|
||||
|
||||
class BigIntColumn extends IntColumn {
|
||||
|
||||
public function __construct(string $name, bool $nullable, $defaultValue, bool $unsigned) {
|
||||
parent::__construct($name, $nullable, $defaultValue, $unsigned);
|
||||
$this->type = "BIGINT";
|
||||
}
|
||||
}
|
@ -11,5 +11,9 @@ class EnumColumn extends Column {
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
public function addValues(string $value) {
|
||||
$this->values[] = $value;
|
||||
}
|
||||
|
||||
public function getValues(): array { return $this->values; }
|
||||
}
|
||||
|
@ -4,8 +4,20 @@ namespace Driver\SQL\Column;
|
||||
|
||||
class IntColumn extends Column {
|
||||
|
||||
public function __construct(string $name, bool $nullable = false, $defaultValue = NULL) {
|
||||
protected string $type;
|
||||
private bool $unsigned;
|
||||
|
||||
public function __construct(string $name, bool $nullable = false, $defaultValue = NULL, bool $unsigned = false) {
|
||||
parent::__construct($name, $nullable, $defaultValue);
|
||||
$this->type = "INTEGER";
|
||||
$this->unsigned = $unsigned;
|
||||
}
|
||||
|
||||
public function isUnsigned(): bool {
|
||||
return $this->unsigned;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,14 @@ namespace Driver\SQL\Condition;
|
||||
|
||||
class CondIn extends Condition {
|
||||
|
||||
private string $column;
|
||||
private $expression;
|
||||
private $needle;
|
||||
private $haystack;
|
||||
|
||||
public function __construct(string $column, $expression) {
|
||||
$this->column = $column;
|
||||
$this->expression = $expression;
|
||||
public function __construct($needle, $haystack) {
|
||||
$this->needle = $needle;
|
||||
$this->haystack = $haystack;
|
||||
}
|
||||
|
||||
public function getColumn(): string { return $this->column; }
|
||||
public function getExpression() { return $this->expression; }
|
||||
public function getNeedle() { return $this->needle; }
|
||||
public function getHaystack() { return $this->haystack; }
|
||||
}
|
18
core/Driver/SQL/Expression/JsonArrayAgg.class.php
Normal file
18
core/Driver/SQL/Expression/JsonArrayAgg.class.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Driver\SQL\Expression;
|
||||
|
||||
class JsonArrayAgg extends Expression {
|
||||
|
||||
private $value;
|
||||
private string $alias;
|
||||
|
||||
public function __construct($value, string $alias) {
|
||||
$this->value = $value;
|
||||
$this->alias = $alias;
|
||||
}
|
||||
|
||||
public function getValue() { return $this->value; }
|
||||
public function getAlias(): string { return $this->alias; }
|
||||
|
||||
}
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace Driver\SQL\Expression;
|
||||
|
||||
use Driver\SQL\Condition\Condition;
|
||||
|
||||
class Sum extends Expression {
|
||||
|
||||
private $value;
|
||||
|
@ -14,12 +14,12 @@ use \Driver\SQL\Column\DateTimeColumn;
|
||||
use Driver\SQL\Column\BoolColumn;
|
||||
use Driver\SQL\Column\JsonColumn;
|
||||
|
||||
use Driver\SQL\Condition\CondRegex;
|
||||
use Driver\SQL\Expression\Add;
|
||||
use Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Driver\SQL\Expression\DateAdd;
|
||||
use Driver\SQL\Expression\DateSub;
|
||||
use Driver\SQL\Expression\Expression;
|
||||
use Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Driver\SQL\Query\CreateProcedure;
|
||||
use Driver\SQL\Query\CreateTrigger;
|
||||
use Driver\SQL\Query\Query;
|
||||
@ -228,7 +228,8 @@ class MySQL extends SQL {
|
||||
} else if($column instanceof SerialColumn) {
|
||||
return "INTEGER AUTO_INCREMENT";
|
||||
} else if($column instanceof IntColumn) {
|
||||
return "INTEGER";
|
||||
$unsigned = $column->isUnsigned() ? " UNSIGNED" : "";
|
||||
return $column->getType() . $unsigned;
|
||||
} else if($column instanceof DateTimeColumn) {
|
||||
return "DATETIME";
|
||||
} else if($column instanceof BoolColumn) {
|
||||
@ -416,6 +417,10 @@ class MySQL extends SQL {
|
||||
return "$dateFunction($lhs, INTERVAL $rhs $unit)";
|
||||
} else if ($exp instanceof CurrentTimeStamp) {
|
||||
return "NOW()";
|
||||
} else if ($exp instanceof JsonArrayAgg) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "JSON_ARRAYAGG($value) as $alias";
|
||||
} else {
|
||||
return parent::createExpression($exp, $params);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ use Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Driver\SQL\Expression\DateAdd;
|
||||
use Driver\SQL\Expression\DateSub;
|
||||
use Driver\SQL\Expression\Expression;
|
||||
use Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Driver\SQL\Query\CreateProcedure;
|
||||
use Driver\SQL\Query\CreateTrigger;
|
||||
use Driver\SQL\Query\Insert;
|
||||
@ -219,7 +220,7 @@ class PostgreSQL extends SQL {
|
||||
} else if($column instanceof SerialColumn) {
|
||||
return "SERIAL";
|
||||
} else if($column instanceof IntColumn) {
|
||||
return "INTEGER";
|
||||
return $column->getType();
|
||||
} else if($column instanceof DateTimeColumn) {
|
||||
return "TIMESTAMP";
|
||||
} else if($column instanceof EnumColumn) {
|
||||
@ -439,6 +440,10 @@ class PostgreSQL extends SQL {
|
||||
return "$lhs $operator $rhs";
|
||||
} else if ($exp instanceof CurrentTimeStamp) {
|
||||
return "CURRENT_TIMESTAMP";
|
||||
} else if ($exp instanceof JsonArrayAgg) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "JSON_AGG($value) as $alias";
|
||||
} else {
|
||||
return parent::createExpression($exp, $params);
|
||||
}
|
||||
|
@ -3,15 +3,19 @@
|
||||
namespace Driver\SQL\Query;
|
||||
|
||||
use Driver\SQL\Column\Column;
|
||||
use Driver\SQL\Column\EnumColumn;
|
||||
use Driver\SQL\Constraint\Constraint;
|
||||
use Driver\SQL\Constraint\ForeignKey;
|
||||
use Driver\SQL\Constraint\PrimaryKey;
|
||||
use Driver\SQL\MySQL;
|
||||
use Driver\SQL\PostgreSQL;
|
||||
use Driver\SQL\SQL;
|
||||
|
||||
class AlterTable extends Query {
|
||||
|
||||
private string $table;
|
||||
private string $action;
|
||||
private $data;
|
||||
|
||||
private ?Column $column;
|
||||
private ?Constraint $constraint;
|
||||
@ -59,6 +63,13 @@ class AlterTable extends Query {
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addToEnum(EnumColumn $column, string $newValue): AlterTable {
|
||||
$this->action = "MODIFY";
|
||||
$this->column = $column;
|
||||
$this->data = $newValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAction(): string { return $this->action; }
|
||||
public function getColumn(): ?Column { return $this->column; }
|
||||
public function getConstraint(): ?Constraint { return $this->constraint; }
|
||||
@ -82,6 +93,15 @@ class AlterTable extends Query {
|
||||
$query .= $this->sql->columnName($column->getName());
|
||||
} else {
|
||||
// ADD or modify
|
||||
if ($column instanceof EnumColumn) {
|
||||
if ($this->sql instanceof PostgreSQL) {
|
||||
$typeName = $this->sql->getColumnType($column);
|
||||
$value = $this->sql->addValue($this->data, $params);
|
||||
return "ALTER TYPE $typeName ADD VALUE $value";
|
||||
}
|
||||
$column->addValue($this->data);
|
||||
}
|
||||
|
||||
$query .= $this->sql->getColumnDefinition($column);
|
||||
}
|
||||
} else if ($constraint) {
|
||||
|
@ -46,8 +46,13 @@ class CreateTable extends Query {
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addInt(string $name, bool $nullable = false, $defaultValue = NULL): CreateTable {
|
||||
$this->columns[$name] = new IntColumn($name, $nullable, $defaultValue);
|
||||
public function addInt(string $name, bool $nullable = false, $defaultValue = NULL, bool $unsigned = false): CreateTable {
|
||||
$this->columns[$name] = new IntColumn($name, $nullable, $defaultValue, $unsigned);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addBigInt(string $name, bool $nullable = false, $defaultValue = NULL, bool $unsigned = false): CreateTable {
|
||||
$this->columns[$name] = new BigIntColumn($name, $nullable, $defaultValue, $unsigned);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -278,22 +278,29 @@ abstract class SQL {
|
||||
}
|
||||
} else if($condition instanceof CondIn) {
|
||||
|
||||
$expression = $condition->getExpression();
|
||||
if (is_array($expression)) {
|
||||
$needle = $condition->getNeedle();
|
||||
$haystack = $condition->getHaystack();
|
||||
if (is_array($haystack)) {
|
||||
$values = array();
|
||||
foreach ($expression as $value) {
|
||||
foreach ($haystack as $value) {
|
||||
$values[] = $this->addValue($value, $params);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
} else if($expression instanceof Select) {
|
||||
$values = $expression->build($params);
|
||||
} else if($haystack instanceof Select) {
|
||||
$values = $haystack->build($params);
|
||||
} else {
|
||||
$this->lastError = "Unsupported in-expression value: " . get_class($condition);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->columnName($condition->getColumn()) . " IN ($values)";
|
||||
if ($needle instanceof Column) {
|
||||
$lhs = $this->createExpression($needle, $params);
|
||||
} else {
|
||||
$lhs = $this->addValue($needle, $params);
|
||||
}
|
||||
|
||||
return "$lhs IN ($values)";
|
||||
} else if($condition instanceof CondKeyword) {
|
||||
$left = $condition->getLeftExp();
|
||||
$right = $condition->getRightExp();
|
||||
@ -315,14 +322,14 @@ abstract class SQL {
|
||||
} else if ($condition instanceof Exists) {
|
||||
return "EXISTS(" .$condition->getSubQuery()->build($params) . ")";
|
||||
} else {
|
||||
$this->lastError = "Unsupported condition type: " . get_class($condition);
|
||||
$this->lastError = "Unsupported condition type: " . gettype($condition);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createExpression(Expression $exp, array &$params): ?string {
|
||||
if ($exp instanceof Column) {
|
||||
return $this->columnName($exp);
|
||||
return $this->columnName($exp->getName());
|
||||
} else if ($exp instanceof Query) {
|
||||
return "(" . $exp->build($params) . ")";
|
||||
} else if ($exp instanceof CaseWhen) {
|
||||
@ -335,7 +342,7 @@ abstract class SQL {
|
||||
return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END";
|
||||
} else if ($exp instanceof Sum) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $exp->getAlias();
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "SUM($value) AS $alias";
|
||||
} else {
|
||||
$this->lastError = "Unsupported expression type: " . get_class($exp);
|
||||
|
@ -2,69 +2,64 @@
|
||||
|
||||
namespace Elements;
|
||||
|
||||
use Driver\SQL\SQL;
|
||||
use Objects\User;
|
||||
|
||||
abstract class Document {
|
||||
|
||||
protected Head $head;
|
||||
protected Body $body;
|
||||
protected User $user;
|
||||
protected bool $databaseRequired;
|
||||
private ?string $activeView;
|
||||
private bool $cspEnabled;
|
||||
private ?string $cspNonce;
|
||||
|
||||
public function __construct(User $user, $headClass, $bodyClass, ?string $view = NULL) {
|
||||
public function __construct(User $user) {
|
||||
$this->user = $user;
|
||||
$this->head = new $headClass($this);
|
||||
$this->body = new $bodyClass($this);
|
||||
$this->cspEnabled = false;
|
||||
$this->cspNonce = null;
|
||||
$this->databaseRequired = true;
|
||||
$this->activeView = $view;
|
||||
}
|
||||
|
||||
public function getHead(): Head { return $this->head; }
|
||||
public function getBody(): Body { return $this->body; }
|
||||
public function getSQL(): ?\Driver\SQL\SQL { return $this->user->getSQL(); }
|
||||
public function getUser(): User { return $this->user; }
|
||||
|
||||
public function getView() : ?View {
|
||||
|
||||
if ($this->activeView === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$view = parseClass($this->activeView);
|
||||
$file = getClassPath($view);
|
||||
if(!file_exists($file) || !is_subclass_of($view, View::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new $view($this);
|
||||
public function getSQL(): ?SQL {
|
||||
return $this->user->getSQL();
|
||||
}
|
||||
|
||||
public function getRequestedView(): string {
|
||||
return $this->activeView;
|
||||
public function getUser(): User {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
function getCode(): string {
|
||||
public function getCSPNonce(): ?string {
|
||||
return $this->cspNonce;
|
||||
}
|
||||
|
||||
public function isCSPEnabled(): bool {
|
||||
return $this->cspEnabled;
|
||||
}
|
||||
|
||||
public function enableCSP() {
|
||||
$this->cspEnabled = true;
|
||||
$this->cspNonce = generateRandomString(16, "base62");
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
if ($this->databaseRequired) {
|
||||
$sql = $this->user->getSQL();
|
||||
if (is_null($sql)) {
|
||||
die("Database is not configured yet.");
|
||||
} else if(!$sql->isConnected()) {
|
||||
} else if (!$sql->isConnected()) {
|
||||
die("Database is not connected: " . $sql->getLastError());
|
||||
}
|
||||
}
|
||||
|
||||
$body = $this->body->getCode();
|
||||
$head = $this->head->getCode();
|
||||
$lang = $this->user->getLanguage()->getShortCode();
|
||||
if ($this->cspEnabled) {
|
||||
$csp = ["default-src 'self'", "object-src 'none'", "base-uri 'self'", "style-src 'self' 'unsafe-inline'", "script-src 'nonce-$this->cspNonce'"];
|
||||
if ($this->user->getConfiguration()->getSettings()->isRecaptchaEnabled()) {
|
||||
$csp[] = "frame-src https://www.google.com/ 'self'";
|
||||
}
|
||||
|
||||
$html = "<!DOCTYPE html>";
|
||||
$html .= "<html lang=\"$lang\">";
|
||||
$html .= $head;
|
||||
$html .= $body;
|
||||
$html .= "</html>";
|
||||
return $html;
|
||||
$compiledCSP = implode(";", $csp);
|
||||
header("Content-Security-Policy: $compiledCSP;");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
79
core/Elements/HtmlDocument.class.php
Normal file
79
core/Elements/HtmlDocument.class.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Elements;
|
||||
|
||||
use Objects\User;
|
||||
|
||||
class HtmlDocument extends Document {
|
||||
|
||||
protected Head $head;
|
||||
protected Body $body;
|
||||
private ?string $activeView;
|
||||
|
||||
public function __construct(User $user, $headClass, $bodyClass, ?string $view = NULL) {
|
||||
parent::__construct($user);
|
||||
$this->head = $headClass ? new $headClass($this) : null;
|
||||
$this->body = $bodyClass ? new $bodyClass($this) : null;
|
||||
$this->activeView = $view;
|
||||
}
|
||||
|
||||
public function getHead(): Head { return $this->head; }
|
||||
public function getBody(): Body { return $this->body; }
|
||||
|
||||
public function getView() : ?View {
|
||||
|
||||
if ($this->activeView === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$view = parseClass($this->activeView);
|
||||
$file = getClassPath($view);
|
||||
if(!file_exists($file) || !is_subclass_of($view, View::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new $view($this);
|
||||
}
|
||||
|
||||
public function createScript($type, $src, $content = ""): Script {
|
||||
$script = new Script($type, $src, $content);
|
||||
|
||||
if ($this->isCSPEnabled()) {
|
||||
$script->setNonce($this->getCSPNonce());
|
||||
}
|
||||
|
||||
return $script;
|
||||
}
|
||||
|
||||
public function getRequestedView(): string {
|
||||
return $this->activeView;
|
||||
}
|
||||
|
||||
function getCode(): string {
|
||||
|
||||
parent::getCode();
|
||||
|
||||
// generate body first, so we can modify head
|
||||
$body = $this->body->getCode();
|
||||
|
||||
if ($this->isCSPEnabled()) {
|
||||
foreach ($this->head->getSources() as $element) {
|
||||
if ($element instanceof Script || $element instanceof Link) {
|
||||
$element->setNonce($this->getCSPNonce());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$head = $this->head->getCode();
|
||||
$lang = $this->user->getLanguage()->getShortCode();
|
||||
|
||||
$html = "<!DOCTYPE html>";
|
||||
$html .= "<html lang=\"$lang\">";
|
||||
$html .= $head;
|
||||
$html .= $body;
|
||||
$html .= "</html>";
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -15,15 +15,30 @@ class Link extends StaticView {
|
||||
private string $type;
|
||||
private string $rel;
|
||||
private string $href;
|
||||
private ?string $nonce;
|
||||
|
||||
function __construct($rel, $href, $type = "") {
|
||||
$this->href = $href;
|
||||
$this->type = $type;
|
||||
$this->rel = $rel;
|
||||
$this->nonce = null;
|
||||
}
|
||||
|
||||
function getCode(): string {
|
||||
$type = (empty($this->type) ? "" : " type=\"$this->type\"");
|
||||
return "<link rel=\"$this->rel\" href=\"$this->href\"$type/>";
|
||||
$attributes = ["rel" => $this->rel, "href" => $this->href];
|
||||
|
||||
if (!empty($this->type)) {
|
||||
$attributes["type"] = $this->type;
|
||||
}
|
||||
if (!empty($this->nonce)) {
|
||||
$attributes["nonce"] = $this->nonce;
|
||||
}
|
||||
|
||||
$attributes = html_attributes($attributes);
|
||||
return "<link $attributes/>";
|
||||
}
|
||||
|
||||
public function setNonce(string $nonce) {
|
||||
$this->nonce = $nonce;
|
||||
}
|
||||
}
|
||||
|
@ -11,21 +11,35 @@ class Script extends StaticView {
|
||||
const INSTALL = "/js/install.js";
|
||||
const BOOTSTRAP = "/js/bootstrap.bundle.min.js";
|
||||
const ACCOUNT = "/js/account.js";
|
||||
const SECLAB = "/js/seclab.min.js";
|
||||
const FONTAWESOME = "/js/fontawesome-all.min.js";
|
||||
|
||||
private string $type;
|
||||
private string $content;
|
||||
private string $src;
|
||||
private ?string $nonce;
|
||||
|
||||
function __construct($type, $src, $content = "") {
|
||||
$this->src = $src;
|
||||
$this->type = $type;
|
||||
$this->content = $content;
|
||||
$this->nonce = null;
|
||||
}
|
||||
|
||||
function getCode(): string {
|
||||
$src = (empty($this->src) ? "" : " src=\"$this->src\"");
|
||||
return "<script type=\"$this->type\"$src>$this->content</script>";
|
||||
$attributes = ["type" => $this->type];
|
||||
if (!empty($this->src)) {
|
||||
$attributes["src"] = $this->src;
|
||||
}
|
||||
|
||||
if (!empty($this->nonce)) {
|
||||
$attributes["nonce"] = $this->nonce;
|
||||
}
|
||||
|
||||
$attributes = html_attributes($attributes);
|
||||
return "<script $attributes>$this->content</script>";
|
||||
}
|
||||
|
||||
public function setNonce(string $nonce) {
|
||||
$this->nonce = $nonce;
|
||||
}
|
||||
}
|
71
core/Elements/TemplateDocument.class.php
Normal file
71
core/Elements/TemplateDocument.class.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Elements;
|
||||
|
||||
use Objects\User;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
class TemplateDocument extends Document {
|
||||
|
||||
private string $templateName;
|
||||
protected array $parameters;
|
||||
private Environment $twigEnvironment;
|
||||
private FilesystemLoader $twigLoader;
|
||||
|
||||
public function __construct(User $user, string $templateName, array $initialParameters = []) {
|
||||
parent::__construct($user);
|
||||
$this->templateName = $templateName;
|
||||
$this->parameters = $initialParameters;
|
||||
$this->twigLoader = new FilesystemLoader(WEBROOT . '/core/Templates');
|
||||
$this->twigEnvironment = new Environment($this->twigLoader, [
|
||||
'cache' => WEBROOT . '/core/TemplateCache',
|
||||
'auto_reload' => true
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getTemplateName(): string {
|
||||
return $this->templateName;
|
||||
}
|
||||
|
||||
protected function loadParameters() {
|
||||
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
parent::getCode();
|
||||
$this->loadParameters();
|
||||
return $this->renderTemplate($this->templateName, $this->parameters);
|
||||
}
|
||||
|
||||
public function renderTemplate(string $name, array $params = []): string {
|
||||
try {
|
||||
|
||||
$params["user"] = [
|
||||
"lang" => $this->user->getLanguage()->getShortCode(),
|
||||
"loggedIn" => $this->user->isLoggedIn(),
|
||||
];
|
||||
|
||||
$settings = $this->user->getConfiguration()->getSettings();
|
||||
$params["site"] = [
|
||||
"name" => $settings->getSiteName(),
|
||||
"baseUrl" => $settings->getBaseUrl(),
|
||||
"recaptcha" => [
|
||||
"key" => $settings->isRecaptchaEnabled() ? $settings->getRecaptchaSiteKey() : null,
|
||||
"enabled" => $settings->isRecaptchaEnabled(),
|
||||
],
|
||||
"csp" => [
|
||||
"nonce" => $this->getCSPNonce(),
|
||||
"enabled" => $this->isCSPEnabled()
|
||||
]
|
||||
];
|
||||
|
||||
return $this->twigEnvironment->render($name, $params);
|
||||
} catch (LoaderError | RuntimeError | SyntaxError $e) {
|
||||
return "<b>Error rendering twig template: " . htmlspecialchars($e->getMessage()) . "</b>";
|
||||
}
|
||||
}
|
||||
}
|
1
core/External/.gitignore
vendored
Normal file
1
core/External/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
vendor/
|
5
core/External/PHPMailer/Exception.php
vendored
5
core/External/PHPMailer/Exception.php
vendored
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPMailer Exception class.
|
||||
* PHP Version 5.5.
|
||||
@ -9,7 +10,7 @@
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
* @author Brent R. Matzelle (original founder)
|
||||
* @copyright 2012 - 2017 Marcus Bointon
|
||||
* @copyright 2012 - 2020 Marcus Bointon
|
||||
* @copyright 2010 - 2012 Jim Jagielski
|
||||
* @copyright 2004 - 2009 Andy Prevost
|
||||
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||
@ -34,6 +35,6 @@ class Exception extends \Exception
|
||||
*/
|
||||
public function errorMessage()
|
||||
{
|
||||
return '<strong>' . htmlspecialchars($this->getMessage()) . "</strong><br />\n";
|
||||
return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n";
|
||||
}
|
||||
}
|
||||
|
5
core/External/PHPMailer/OAuth.php
vendored
5
core/External/PHPMailer/OAuth.php
vendored
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPMailer - PHP email creation and transport class.
|
||||
* PHP Version 5.5.
|
||||
@ -9,7 +10,7 @@
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
* @author Brent R. Matzelle (original founder)
|
||||
* @copyright 2012 - 2015 Marcus Bointon
|
||||
* @copyright 2012 - 2020 Marcus Bointon
|
||||
* @copyright 2010 - 2012 Jim Jagielski
|
||||
* @copyright 2004 - 2009 Andy Prevost
|
||||
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||
@ -122,7 +123,7 @@ class OAuth
|
||||
*/
|
||||
public function getOauth64()
|
||||
{
|
||||
// Get a new token if it's not available or has expired
|
||||
//Get a new token if it's not available or has expired
|
||||
if (null === $this->oauthToken || $this->oauthToken->hasExpired()) {
|
||||
$this->oauthToken = $this->getToken();
|
||||
}
|
||||
|
882
core/External/PHPMailer/PHPMailer.php
vendored
882
core/External/PHPMailer/PHPMailer.php
vendored
File diff suppressed because it is too large
Load Diff
101
core/External/PHPMailer/POP3.php
vendored
101
core/External/PHPMailer/POP3.php
vendored
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPMailer POP-Before-SMTP Authentication Class.
|
||||
* PHP Version 5.5.
|
||||
@ -9,7 +10,7 @@
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
* @author Brent R. Matzelle (original founder)
|
||||
* @copyright 2012 - 2019 Marcus Bointon
|
||||
* @copyright 2012 - 2020 Marcus Bointon
|
||||
* @copyright 2010 - 2012 Jim Jagielski
|
||||
* @copyright 2004 - 2009 Andy Prevost
|
||||
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||
@ -45,7 +46,7 @@ class POP3
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '6.1.4';
|
||||
const VERSION = '6.5.1';
|
||||
|
||||
/**
|
||||
* Default POP3 port number.
|
||||
@ -62,12 +63,16 @@ class POP3
|
||||
const DEFAULT_TIMEOUT = 30;
|
||||
|
||||
/**
|
||||
* Debug display level.
|
||||
* Options: 0 = no, 1+ = yes.
|
||||
* POP3 class debug output mode.
|
||||
* Debug output level.
|
||||
* Options:
|
||||
* @see POP3::DEBUG_OFF: No output
|
||||
* @see POP3::DEBUG_SERVER: Server messages, connection/server errors
|
||||
* @see POP3::DEBUG_CLIENT: Client and Server messages, connection/server errors
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $do_debug = 0;
|
||||
public $do_debug = self::DEBUG_OFF;
|
||||
|
||||
/**
|
||||
* POP3 mail server hostname.
|
||||
@ -130,6 +135,28 @@ class POP3
|
||||
*/
|
||||
const LE = "\r\n";
|
||||
|
||||
/**
|
||||
* Debug level for no output.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEBUG_OFF = 0;
|
||||
|
||||
/**
|
||||
* Debug level to show server -> client messages
|
||||
* also shows clients connection errors or errors from server
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEBUG_SERVER = 1;
|
||||
|
||||
/**
|
||||
* Debug level to show client -> server and server -> client messages.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEBUG_CLIENT = 2;
|
||||
|
||||
/**
|
||||
* Simple static wrapper for all-in-one POP before SMTP.
|
||||
*
|
||||
@ -172,13 +199,13 @@ class POP3
|
||||
public function authorise($host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0)
|
||||
{
|
||||
$this->host = $host;
|
||||
// If no port value provided, use default
|
||||
//If no port value provided, use default
|
||||
if (false === $port) {
|
||||
$this->port = static::DEFAULT_PORT;
|
||||
} else {
|
||||
$this->port = (int) $port;
|
||||
}
|
||||
// If no timeout value provided, use default
|
||||
//If no timeout value provided, use default
|
||||
if (false === $timeout) {
|
||||
$this->tval = static::DEFAULT_TIMEOUT;
|
||||
} else {
|
||||
@ -187,9 +214,9 @@ class POP3
|
||||
$this->do_debug = $debug_level;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
// Reset the error log
|
||||
//Reset the error log
|
||||
$this->errors = [];
|
||||
// connect
|
||||
//Connect
|
||||
$result = $this->connect($this->host, $this->port, $this->tval);
|
||||
if ($result) {
|
||||
$login_result = $this->login($this->username, $this->password);
|
||||
@ -199,7 +226,7 @@ class POP3
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// We need to disconnect regardless of whether the login succeeded
|
||||
//We need to disconnect regardless of whether the login succeeded
|
||||
$this->disconnect();
|
||||
|
||||
return false;
|
||||
@ -216,7 +243,7 @@ class POP3
|
||||
*/
|
||||
public function connect($host, $port = false, $tval = 30)
|
||||
{
|
||||
// Are we already connected?
|
||||
//Are we already connected?
|
||||
if ($this->connected) {
|
||||
return true;
|
||||
}
|
||||
@ -229,20 +256,22 @@ class POP3
|
||||
$port = static::DEFAULT_PORT;
|
||||
}
|
||||
|
||||
// connect to the POP3 server
|
||||
//Connect to the POP3 server
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$this->pop_conn = fsockopen(
|
||||
$host, // POP3 Host
|
||||
$port, // Port #
|
||||
$errno, // Error Number
|
||||
$errstr, // Error Message
|
||||
$host, //POP3 Host
|
||||
$port, //Port #
|
||||
$errno, //Error Number
|
||||
$errstr, //Error Message
|
||||
$tval
|
||||
); // Timeout (seconds)
|
||||
// Restore the error handler
|
||||
); //Timeout (seconds)
|
||||
//Restore the error handler
|
||||
restore_error_handler();
|
||||
|
||||
// Did we connect?
|
||||
//Did we connect?
|
||||
if (false === $this->pop_conn) {
|
||||
// It would appear not...
|
||||
//It would appear not...
|
||||
$this->setError(
|
||||
"Failed to connect to server $host on port $port. errno: $errno; errstr: $errstr"
|
||||
);
|
||||
@ -250,14 +279,14 @@ class POP3
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increase the stream time-out
|
||||
//Increase the stream time-out
|
||||
stream_set_timeout($this->pop_conn, $tval, 0);
|
||||
|
||||
// Get the POP3 server response
|
||||
//Get the POP3 server response
|
||||
$pop3_response = $this->getResponse();
|
||||
// Check for the +OK
|
||||
//Check for the +OK
|
||||
if ($this->checkResponse($pop3_response)) {
|
||||
// The connection is established and the POP3 server is talking
|
||||
//The connection is established and the POP3 server is talking
|
||||
$this->connected = true;
|
||||
|
||||
return true;
|
||||
@ -279,6 +308,7 @@ class POP3
|
||||
{
|
||||
if (!$this->connected) {
|
||||
$this->setError('Not connected to POP3 server');
|
||||
return false;
|
||||
}
|
||||
if (empty($username)) {
|
||||
$username = $this->username;
|
||||
@ -287,11 +317,11 @@ class POP3
|
||||
$password = $this->password;
|
||||
}
|
||||
|
||||
// Send the Username
|
||||
//Send the Username
|
||||
$this->sendString("USER $username" . static::LE);
|
||||
$pop3_response = $this->getResponse();
|
||||
if ($this->checkResponse($pop3_response)) {
|
||||
// Send the Password
|
||||
//Send the Password
|
||||
$this->sendString("PASS $password" . static::LE);
|
||||
$pop3_response = $this->getResponse();
|
||||
if ($this->checkResponse($pop3_response)) {
|
||||
@ -308,6 +338,15 @@ class POP3
|
||||
public function disconnect()
|
||||
{
|
||||
$this->sendString('QUIT');
|
||||
|
||||
// RFC 1939 shows POP3 server sending a +OK response to the QUIT command.
|
||||
// Try to get it. Ignore any failures here.
|
||||
try {
|
||||
$this->getResponse();
|
||||
} catch (Exception $e) {
|
||||
//Do nothing
|
||||
}
|
||||
|
||||
//The QUIT command may cause the daemon to exit, which will kill our connection
|
||||
//So ignore errors here
|
||||
try {
|
||||
@ -315,6 +354,10 @@ class POP3
|
||||
} catch (Exception $e) {
|
||||
//Do nothing
|
||||
}
|
||||
|
||||
// Clean up attributes.
|
||||
$this->connected = false;
|
||||
$this->pop_conn = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -327,7 +370,7 @@ class POP3
|
||||
protected function getResponse($size = 128)
|
||||
{
|
||||
$response = fgets($this->pop_conn, $size);
|
||||
if ($this->do_debug >= 1) {
|
||||
if ($this->do_debug >= self::DEBUG_SERVER) {
|
||||
echo 'Server -> Client: ', $response;
|
||||
}
|
||||
|
||||
@ -344,7 +387,7 @@ class POP3
|
||||
protected function sendString($string)
|
||||
{
|
||||
if ($this->pop_conn) {
|
||||
if ($this->do_debug >= 2) { //Show client messages when debug >= 2
|
||||
if ($this->do_debug >= self::DEBUG_CLIENT) { //Show client messages when debug >= 2
|
||||
echo 'Client -> Server: ', $string;
|
||||
}
|
||||
|
||||
@ -382,7 +425,7 @@ class POP3
|
||||
protected function setError($error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
if ($this->do_debug >= 1) {
|
||||
if ($this->do_debug >= self::DEBUG_SERVER) {
|
||||
echo '<pre>';
|
||||
foreach ($this->errors as $e) {
|
||||
print_r($e);
|
||||
|
237
core/External/PHPMailer/SMTP.php
vendored
237
core/External/PHPMailer/SMTP.php
vendored
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPMailer RFC821 SMTP email transport class.
|
||||
* PHP Version 5.5.
|
||||
@ -9,7 +10,7 @@
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
* @author Brent R. Matzelle (original founder)
|
||||
* @copyright 2012 - 2019 Marcus Bointon
|
||||
* @copyright 2012 - 2020 Marcus Bointon
|
||||
* @copyright 2010 - 2012 Jim Jagielski
|
||||
* @copyright 2004 - 2009 Andy Prevost
|
||||
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||
@ -34,7 +35,7 @@ class SMTP
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '6.1.4';
|
||||
const VERSION = '6.5.1';
|
||||
|
||||
/**
|
||||
* SMTP line break constant.
|
||||
@ -185,6 +186,7 @@ class SMTP
|
||||
'Amazon_SES' => '/[\d]{3} Ok (.*)/',
|
||||
'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
|
||||
'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
|
||||
'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -311,17 +313,11 @@ class SMTP
|
||||
*/
|
||||
public function connect($host, $port = null, $timeout = 30, $options = [])
|
||||
{
|
||||
static $streamok;
|
||||
//This is enabled by default since 5.0.0 but some providers disable it
|
||||
//Check this once and cache the result
|
||||
if (null === $streamok) {
|
||||
$streamok = function_exists('stream_socket_client');
|
||||
}
|
||||
// Clear errors to avoid confusion
|
||||
//Clear errors to avoid confusion
|
||||
$this->setError('');
|
||||
// Make sure we are __not__ connected
|
||||
//Make sure we are __not__ connected
|
||||
if ($this->connected()) {
|
||||
// Already connected, generate error
|
||||
//Already connected, generate error
|
||||
$this->setError('Already connected to a server');
|
||||
|
||||
return false;
|
||||
@ -329,18 +325,66 @@ class SMTP
|
||||
if (empty($port)) {
|
||||
$port = self::DEFAULT_PORT;
|
||||
}
|
||||
// Connect to the SMTP server
|
||||
//Connect to the SMTP server
|
||||
$this->edebug(
|
||||
"Connection: opening to $host:$port, timeout=$timeout, options=" .
|
||||
(count($options) > 0 ? var_export($options, true) : 'array()'),
|
||||
self::DEBUG_CONNECTION
|
||||
);
|
||||
|
||||
$this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
|
||||
|
||||
if ($this->smtp_conn === false) {
|
||||
//Error info already set inside `getSMTPConnection()`
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->edebug('Connection: opened', self::DEBUG_CONNECTION);
|
||||
|
||||
//Get any announcement
|
||||
$this->last_reply = $this->get_lines();
|
||||
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
|
||||
$responseCode = (int)substr($this->last_reply, 0, 3);
|
||||
if ($responseCode === 220) {
|
||||
return true;
|
||||
}
|
||||
//Anything other than a 220 response means something went wrong
|
||||
//RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
|
||||
//https://tools.ietf.org/html/rfc5321#section-3.1
|
||||
if ($responseCode === 554) {
|
||||
$this->quit();
|
||||
}
|
||||
//This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
|
||||
$this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
|
||||
$this->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create connection to the SMTP server.
|
||||
*
|
||||
* @param string $host SMTP server IP or host name
|
||||
* @param int $port The port number to connect to
|
||||
* @param int $timeout How long to wait for the connection to open
|
||||
* @param array $options An array of options for stream_context_create()
|
||||
*
|
||||
* @return false|resource
|
||||
*/
|
||||
protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
|
||||
{
|
||||
static $streamok;
|
||||
//This is enabled by default since 5.0.0 but some providers disable it
|
||||
//Check this once and cache the result
|
||||
if (null === $streamok) {
|
||||
$streamok = function_exists('stream_socket_client');
|
||||
}
|
||||
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
if ($streamok) {
|
||||
$socket_context = stream_context_create($options);
|
||||
set_error_handler([$this, 'errorHandler']);
|
||||
$this->smtp_conn = stream_socket_client(
|
||||
$connection = stream_socket_client(
|
||||
$host . ':' . $port,
|
||||
$errno,
|
||||
$errstr,
|
||||
@ -348,7 +392,6 @@ class SMTP
|
||||
STREAM_CLIENT_CONNECT,
|
||||
$socket_context
|
||||
);
|
||||
restore_error_handler();
|
||||
} else {
|
||||
//Fall back to fsockopen which should work in more places, but is missing some features
|
||||
$this->edebug(
|
||||
@ -356,17 +399,18 @@ class SMTP
|
||||
self::DEBUG_CONNECTION
|
||||
);
|
||||
set_error_handler([$this, 'errorHandler']);
|
||||
$this->smtp_conn = fsockopen(
|
||||
$connection = fsockopen(
|
||||
$host,
|
||||
$port,
|
||||
$errno,
|
||||
$errstr,
|
||||
$timeout
|
||||
);
|
||||
restore_error_handler();
|
||||
}
|
||||
// Verify we connected properly
|
||||
if (!is_resource($this->smtp_conn)) {
|
||||
restore_error_handler();
|
||||
|
||||
//Verify we connected properly
|
||||
if (!is_resource($connection)) {
|
||||
$this->setError(
|
||||
'Failed to connect to server',
|
||||
'',
|
||||
@ -381,22 +425,19 @@ class SMTP
|
||||
|
||||
return false;
|
||||
}
|
||||
$this->edebug('Connection: opened', self::DEBUG_CONNECTION);
|
||||
// SMTP server can take longer to respond, give longer timeout for first read
|
||||
// Windows does not have support for this timeout function
|
||||
|
||||
//SMTP server can take longer to respond, give longer timeout for first read
|
||||
//Windows does not have support for this timeout function
|
||||
if (strpos(PHP_OS, 'WIN') !== 0) {
|
||||
$max = (int) ini_get('max_execution_time');
|
||||
// Don't bother if unlimited
|
||||
if (0 !== $max && $timeout > $max) {
|
||||
$max = (int)ini_get('max_execution_time');
|
||||
//Don't bother if unlimited, or if set_time_limit is disabled
|
||||
if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
|
||||
@set_time_limit($timeout);
|
||||
}
|
||||
stream_set_timeout($this->smtp_conn, $timeout, 0);
|
||||
stream_set_timeout($connection, $timeout, 0);
|
||||
}
|
||||
// Get any announcement
|
||||
$announce = $this->get_lines();
|
||||
$this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
|
||||
|
||||
return true;
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -420,7 +461,7 @@ class SMTP
|
||||
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
|
||||
}
|
||||
|
||||
// Begin encrypted connection
|
||||
//Begin encrypted connection
|
||||
set_error_handler([$this, 'errorHandler']);
|
||||
$crypto_ok = stream_socket_enable_crypto(
|
||||
$this->smtp_conn,
|
||||
@ -458,11 +499,11 @@ class SMTP
|
||||
}
|
||||
|
||||
if (array_key_exists('EHLO', $this->server_caps)) {
|
||||
// SMTP extensions are available; try to find a proper authentication method
|
||||
//SMTP extensions are available; try to find a proper authentication method
|
||||
if (!array_key_exists('AUTH', $this->server_caps)) {
|
||||
$this->setError('Authentication is not allowed at this stage');
|
||||
// 'at this stage' means that auth may be allowed after the stage changes
|
||||
// e.g. after STARTTLS
|
||||
//'at this stage' means that auth may be allowed after the stage changes
|
||||
//e.g. after STARTTLS
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -506,22 +547,25 @@ class SMTP
|
||||
}
|
||||
switch ($authtype) {
|
||||
case 'PLAIN':
|
||||
// Start authentication
|
||||
//Start authentication
|
||||
if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
|
||||
return false;
|
||||
}
|
||||
// Send encoded username and password
|
||||
if (!$this->sendCommand(
|
||||
'User & Password',
|
||||
base64_encode("\0" . $username . "\0" . $password),
|
||||
235
|
||||
)
|
||||
//Send encoded username and password
|
||||
if (
|
||||
//Format from https://tools.ietf.org/html/rfc4616#section-2
|
||||
//We skip the first field (it's forgery), so the string starts with a null byte
|
||||
!$this->sendCommand(
|
||||
'User & Password',
|
||||
base64_encode("\0" . $username . "\0" . $password),
|
||||
235
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'LOGIN':
|
||||
// Start authentication
|
||||
//Start authentication
|
||||
if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
|
||||
return false;
|
||||
}
|
||||
@ -533,17 +577,17 @@ class SMTP
|
||||
}
|
||||
break;
|
||||
case 'CRAM-MD5':
|
||||
// Start authentication
|
||||
//Start authentication
|
||||
if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
|
||||
return false;
|
||||
}
|
||||
// Get the challenge
|
||||
//Get the challenge
|
||||
$challenge = base64_decode(substr($this->last_reply, 4));
|
||||
|
||||
// Build the response
|
||||
//Build the response
|
||||
$response = $username . ' ' . $this->hmac($challenge, $password);
|
||||
|
||||
// send encoded credentials
|
||||
//send encoded credentials
|
||||
return $this->sendCommand('Username', base64_encode($response), 235);
|
||||
case 'XOAUTH2':
|
||||
//The OAuth instance must be set up prior to requesting auth.
|
||||
@ -552,7 +596,7 @@ class SMTP
|
||||
}
|
||||
$oauth = $OAuth->getOauth64();
|
||||
|
||||
// Start authentication
|
||||
//Start authentication
|
||||
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
|
||||
return false;
|
||||
}
|
||||
@ -582,15 +626,15 @@ class SMTP
|
||||
return hash_hmac('md5', $data, $key);
|
||||
}
|
||||
|
||||
// The following borrowed from
|
||||
// http://php.net/manual/en/function.mhash.php#27225
|
||||
//The following borrowed from
|
||||
//http://php.net/manual/en/function.mhash.php#27225
|
||||
|
||||
// RFC 2104 HMAC implementation for php.
|
||||
// Creates an md5 HMAC.
|
||||
// Eliminates the need to install mhash to compute a HMAC
|
||||
// by Lance Rushing
|
||||
//RFC 2104 HMAC implementation for php.
|
||||
//Creates an md5 HMAC.
|
||||
//Eliminates the need to install mhash to compute a HMAC
|
||||
//by Lance Rushing
|
||||
|
||||
$bytelen = 64; // byte length for md5
|
||||
$bytelen = 64; //byte length for md5
|
||||
if (strlen($key) > $bytelen) {
|
||||
$key = pack('H*', md5($key));
|
||||
}
|
||||
@ -613,7 +657,7 @@ class SMTP
|
||||
if (is_resource($this->smtp_conn)) {
|
||||
$sock_status = stream_get_meta_data($this->smtp_conn);
|
||||
if ($sock_status['eof']) {
|
||||
// The socket is valid but we are not connected
|
||||
//The socket is valid but we are not connected
|
||||
$this->edebug(
|
||||
'SMTP NOTICE: EOF caught while checking if connected',
|
||||
self::DEBUG_CLIENT
|
||||
@ -623,7 +667,7 @@ class SMTP
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // everything looks good
|
||||
return true; //everything looks good
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -641,7 +685,7 @@ class SMTP
|
||||
$this->server_caps = null;
|
||||
$this->helo_rply = null;
|
||||
if (is_resource($this->smtp_conn)) {
|
||||
// close the connection and cleanup
|
||||
//Close the connection and cleanup
|
||||
fclose($this->smtp_conn);
|
||||
$this->smtp_conn = null; //Makes for cleaner serialization
|
||||
$this->edebug('Connection: closed', self::DEBUG_CONNECTION);
|
||||
@ -651,7 +695,7 @@ class SMTP
|
||||
/**
|
||||
* Send an SMTP DATA command.
|
||||
* Issues a data command and sends the msg_data to the server,
|
||||
* finializing the mail transaction. $msg_data is the message
|
||||
* finalizing the mail transaction. $msg_data is the message
|
||||
* that is to be send with the headers. Each header needs to be
|
||||
* on a single line followed by a <CRLF> with the message headers
|
||||
* and the message body being separated by an additional <CRLF>.
|
||||
@ -676,7 +720,7 @@ class SMTP
|
||||
* NOTE: this does not count towards line-length limit.
|
||||
*/
|
||||
|
||||
// Normalize line breaks before exploding
|
||||
//Normalize line breaks before exploding
|
||||
$lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
|
||||
|
||||
/* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
|
||||
@ -722,7 +766,8 @@ class SMTP
|
||||
|
||||
//Send the lines to the server
|
||||
foreach ($lines_out as $line_out) {
|
||||
//RFC2821 section 4.5.2
|
||||
//Dot-stuffing as per RFC5321 section 4.5.2
|
||||
//https://tools.ietf.org/html/rfc5321#section-4.5.2
|
||||
if (!empty($line_out) && $line_out[0] === '.') {
|
||||
$line_out = '.' . $line_out;
|
||||
}
|
||||
@ -756,7 +801,16 @@ class SMTP
|
||||
public function hello($host = '')
|
||||
{
|
||||
//Try extended hello first (RFC 2821)
|
||||
return $this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host);
|
||||
if ($this->sendHello('EHLO', $host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//Some servers shut down the SMTP service here (RFC 5321)
|
||||
if (substr($this->helo_rply, 0, 3) == '421') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->sendHello('HELO', $host);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -946,12 +1000,12 @@ class SMTP
|
||||
$this->client_send($commandstring . static::LE, $command);
|
||||
|
||||
$this->last_reply = $this->get_lines();
|
||||
// Fetch SMTP code and possible error code explanation
|
||||
//Fetch SMTP code and possible error code explanation
|
||||
$matches = [];
|
||||
if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
|
||||
$code = (int) $matches[1];
|
||||
$code_ex = (count($matches) > 2 ? $matches[2] : null);
|
||||
// Cut off error code from each response line
|
||||
//Cut off error code from each response line
|
||||
$detail = preg_replace(
|
||||
"/{$code}[ -]" .
|
||||
($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
|
||||
@ -959,7 +1013,7 @@ class SMTP
|
||||
$this->last_reply
|
||||
);
|
||||
} else {
|
||||
// Fall back to simple parsing if regex fails
|
||||
//Fall back to simple parsing if regex fails
|
||||
$code = (int) substr($this->last_reply, 0, 3);
|
||||
$code_ex = null;
|
||||
$detail = substr($this->last_reply, 4);
|
||||
@ -1058,8 +1112,10 @@ class SMTP
|
||||
{
|
||||
//If SMTP transcripts are left enabled, or debug output is posted online
|
||||
//it can leak credentials, so hide credentials in all but lowest level
|
||||
if (self::DEBUG_LOWLEVEL > $this->do_debug &&
|
||||
in_array($command, ['User & Password', 'Username', 'Password'], true)) {
|
||||
if (
|
||||
self::DEBUG_LOWLEVEL > $this->do_debug &&
|
||||
in_array($command, ['User & Password', 'Username', 'Password'], true)
|
||||
) {
|
||||
$this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
|
||||
} else {
|
||||
$this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
|
||||
@ -1113,7 +1169,7 @@ class SMTP
|
||||
if (!$this->server_caps) {
|
||||
$this->setError('No HELO/EHLO was sent');
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!array_key_exists($name, $this->server_caps)) {
|
||||
@ -1125,7 +1181,7 @@ class SMTP
|
||||
}
|
||||
$this->setError('HELO handshake was used; No information about server extensions available');
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->server_caps[$name];
|
||||
@ -1152,7 +1208,7 @@ class SMTP
|
||||
*/
|
||||
protected function get_lines()
|
||||
{
|
||||
// If the connection is bad, give up straight away
|
||||
//If the connection is bad, give up straight away
|
||||
if (!is_resource($this->smtp_conn)) {
|
||||
return '';
|
||||
}
|
||||
@ -1166,33 +1222,61 @@ class SMTP
|
||||
$selW = null;
|
||||
while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
|
||||
//Must pass vars in here as params are by reference
|
||||
if (!stream_select($selR, $selW, $selW, $this->Timelimit)) {
|
||||
//solution for signals inspired by https://github.com/symfony/symfony/pull/6540
|
||||
set_error_handler([$this, 'errorHandler']);
|
||||
$n = stream_select($selR, $selW, $selW, $this->Timelimit);
|
||||
restore_error_handler();
|
||||
|
||||
if ($n === false) {
|
||||
$message = $this->getError()['detail'];
|
||||
|
||||
$this->edebug(
|
||||
'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
|
||||
'SMTP -> get_lines(): select failed (' . $message . ')',
|
||||
self::DEBUG_LOWLEVEL
|
||||
);
|
||||
|
||||
//stream_select returns false when the `select` system call is interrupted
|
||||
//by an incoming signal, try the select again
|
||||
if (stripos($message, 'interrupted system call') !== false) {
|
||||
$this->edebug(
|
||||
'SMTP -> get_lines(): retrying stream_select',
|
||||
self::DEBUG_LOWLEVEL
|
||||
);
|
||||
$this->setError('');
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$n) {
|
||||
$this->edebug(
|
||||
'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
|
||||
self::DEBUG_LOWLEVEL
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
//Deliberate noise suppression - errors are handled afterwards
|
||||
$str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
|
||||
$this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
|
||||
$data .= $str;
|
||||
// If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
|
||||
// or 4th character is a space or a line break char, we are done reading, break the loop.
|
||||
// String array access is a significant micro-optimisation over strlen
|
||||
//If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
|
||||
//or 4th character is a space or a line break char, we are done reading, break the loop.
|
||||
//String array access is a significant micro-optimisation over strlen
|
||||
if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
|
||||
break;
|
||||
}
|
||||
// Timed-out? Log and break
|
||||
//Timed-out? Log and break
|
||||
$info = stream_get_meta_data($this->smtp_conn);
|
||||
if ($info['timed_out']) {
|
||||
$this->edebug(
|
||||
'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)',
|
||||
'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
|
||||
self::DEBUG_LOWLEVEL
|
||||
);
|
||||
break;
|
||||
}
|
||||
// Now check if reads took too long
|
||||
//Now check if reads took too long
|
||||
if ($endtime && time() > $endtime) {
|
||||
$this->edebug(
|
||||
'SMTP -> get_lines(): timelimit reached (' .
|
||||
@ -1344,6 +1428,7 @@ class SMTP
|
||||
} else {
|
||||
$this->last_smtp_transaction_id = false;
|
||||
foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
|
||||
$matches = [];
|
||||
if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
|
||||
$this->last_smtp_transaction_id = trim($matches[1]);
|
||||
break;
|
||||
|
164
core/External/ZipStream/File.php
vendored
164
core/External/ZipStream/File.php
vendored
@ -26,16 +26,16 @@ namespace External\ZipStream {
|
||||
private $content = '';
|
||||
private $fileHandle = false;
|
||||
private $lastModificationTimestamp;
|
||||
private $crc32 = null;
|
||||
private $fileSize = 0;
|
||||
private $compressedSize = 0;
|
||||
protected $fileSize = 0;
|
||||
protected $compressedSize = 0;
|
||||
private $offset = 0;
|
||||
private $bitField = 0;
|
||||
private $useCompression = true;
|
||||
protected $useCompression = true;
|
||||
private $deflateState = null;
|
||||
|
||||
//check for duplications //currently not used
|
||||
private $sha256;
|
||||
protected $crc32 = null;
|
||||
protected $sha256;
|
||||
|
||||
public const BIT_NO_SIZE_IN_HEADER = 0b0000000000001000;
|
||||
public const BIT_UTF8_NAMES = 0b0000100000000000;
|
||||
@ -45,12 +45,17 @@ namespace External\ZipStream {
|
||||
$this->lastModificationTimestamp = time();
|
||||
$this->crc32 = hash('crc32b', '', true);
|
||||
$this->compressedSize = 0;
|
||||
$this->fileSize = 0;
|
||||
|
||||
$this->bitField = 0;
|
||||
$this->bitField |= self::BIT_NO_SIZE_IN_HEADER;
|
||||
$this->bitField |= self::BIT_UTF8_NAMES;
|
||||
|
||||
$this->deflateState = deflate_init(ZLIB_ENCODING_RAW, ['level' => 9]);
|
||||
$this->deflateState = deflate_init(ZLIB_ENCODING_RAW);
|
||||
}
|
||||
|
||||
public function disableCompression() {
|
||||
$this->useCompression = false;
|
||||
}
|
||||
|
||||
public function setContent($content) {
|
||||
@ -68,13 +73,6 @@ namespace External\ZipStream {
|
||||
$this->fileHandle = fopen($filename, 'rb');
|
||||
}
|
||||
|
||||
public function loadFromBuffer($buf) {
|
||||
$this->crc32 = hash('crc32b', $buf, true);
|
||||
$this->sha256 = hash('sha256', $buf);
|
||||
$this->fileSize = strlen($buf);
|
||||
$this->content = $buf;
|
||||
}
|
||||
|
||||
public function name() {
|
||||
return $this->name;
|
||||
}
|
||||
@ -101,14 +99,14 @@ namespace External\ZipStream {
|
||||
($day);
|
||||
}
|
||||
|
||||
public function readLocalFileHeader() {
|
||||
public function readLocalFileHeader(bool $zip64 = false) {
|
||||
if (!$this->useCompression) {
|
||||
$this->compressedSize = $this->fileSize;
|
||||
}
|
||||
|
||||
|
||||
$header = "";
|
||||
$header .= "\x50\x4b\x03\x04";
|
||||
$header .= "\x14\x00"; //version 2.0 and MS-DOS compatible
|
||||
$header .= $zip64 ? "\x2d\x00" : "\x14\x00"; //version 2.0 and MS-DOS compatible
|
||||
$header .= pack("v", $this->bitField); //general purpose bit flag
|
||||
if ($this->useCompression) {
|
||||
$header .= "\x08\x00"; //compression Method - deflate
|
||||
@ -117,28 +115,59 @@ namespace External\ZipStream {
|
||||
}
|
||||
$header .= pack("v", $this->unixTimeToDosTime($this->lastModificationTimestamp)); //dos time
|
||||
$header .= pack("v", $this->unixTimeToDosDate($this->lastModificationTimestamp)); //dos date
|
||||
if ($this->bitField & self::BIT_NO_SIZE_IN_HEADER) {
|
||||
$header .= pack("V", 0); //crc32
|
||||
$header .= pack("V", 0); //compressed Size
|
||||
$header .= pack("V", 0); //uncompressed Size
|
||||
|
||||
if ($zip64) {
|
||||
if ($this->bitField & self::BIT_NO_SIZE_IN_HEADER) {
|
||||
$header .= pack("V", 0); //crc32
|
||||
} else {
|
||||
$header .= strrev($this->crc32);
|
||||
}
|
||||
$header .= "\xFF\xFF\xFF\xFF"; //compressed Size
|
||||
$header .= "\xFF\xFF\xFF\xFF"; //uncompressed Size
|
||||
} else {
|
||||
$header .= strrev($this->crc32);
|
||||
$header .= pack("V", $this->compressedSize); //compressed Size
|
||||
$header .= pack("V", $this->fileSize); //uncompressed Size
|
||||
if ($this->bitField & self::BIT_NO_SIZE_IN_HEADER) {
|
||||
$header .= pack("V", 0); //crc32
|
||||
$header .= pack("V", 0); //compressed Size
|
||||
$header .= pack("V", 0); //uncompressed Size
|
||||
} else {
|
||||
$header .= strrev($this->crc32);
|
||||
$header .= pack("V", $this->compressedSize); //compressed Size
|
||||
$header .= pack("V", $this->fileSize); //uncompressed Size
|
||||
}
|
||||
}
|
||||
|
||||
$header .= pack("v", strlen($this->name)); //filename
|
||||
$header .= "\x00\x00"; //extra field length
|
||||
$header .= $this->name;
|
||||
if ($zip64) {
|
||||
$header .= pack("v", 16+4); //extra field length (signatures + data)
|
||||
$header .= $this->name;
|
||||
$header .= pack("v", 0x0001); # Zip64 extended information extra field
|
||||
$header .= pack("v", 16); // 2 * 8 byte
|
||||
if ($this->bitField & self::BIT_NO_SIZE_IN_HEADER) {
|
||||
$header .= pack("P", 0);
|
||||
$header .= pack("P", 0);
|
||||
} else {
|
||||
$header .= pack("P", $this->compressedSize);
|
||||
$header .= pack("P", $this->fileSize);
|
||||
}
|
||||
} else {
|
||||
$header .= "\x00\x00"; //extra field length
|
||||
$header .= $this->name;
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
public function readDataDescriptor() {
|
||||
public function readDataDescriptor(bool $zip64 = false) {
|
||||
|
||||
if (!$this->useCompression) {
|
||||
$this->compressedSize = $this->fileSize;
|
||||
}
|
||||
|
||||
$data = "";
|
||||
$data .= "\x50\x4b\x07\x08";
|
||||
$data .= strrev($this->crc32);
|
||||
$data .= pack("V", $this->compressedSize); //compressed Size
|
||||
$data .= pack("V", $this->fileSize); //uncompressed Size
|
||||
$data .= $zip64 ? pack("P", $this->compressedSize) : pack("V", $this->compressedSize); //compressed Size
|
||||
$data .= $zip64 ? pack("P", $this->fileSize) : pack("V", $this->fileSize); //uncompressed Size
|
||||
return $data;
|
||||
}
|
||||
|
||||
@ -156,21 +185,28 @@ namespace External\ZipStream {
|
||||
return $ret;
|
||||
}
|
||||
|
||||
protected function compress($block) {
|
||||
|
||||
$ret = null;
|
||||
if ($this->deflateState !== null) {
|
||||
if (!empty($block)) {
|
||||
$ret = deflate_add($this->deflateState, $block, ZLIB_NO_FLUSH);
|
||||
} else {
|
||||
$ret = deflate_add($this->deflateState, '', ZLIB_FINISH);
|
||||
$this->deflateState = null;
|
||||
}
|
||||
|
||||
$this->compressedSize += strlen($ret);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function readFileData() {
|
||||
$ret = null;
|
||||
if ($this->useCompression) {
|
||||
$block = $this->readFileDataImp();
|
||||
if ($this->deflateState !== null) {
|
||||
if ($block !== null) {
|
||||
$ret = deflate_add($this->deflateState, $block, ZLIB_NO_FLUSH);
|
||||
} else {
|
||||
$ret = deflate_add($this->deflateState, '', ZLIB_FINISH);
|
||||
$this->deflateState = null;
|
||||
}
|
||||
}
|
||||
if ($ret !== null) {
|
||||
$this->compressedSize += strlen($ret);
|
||||
}
|
||||
$ret = $this->compress($block);
|
||||
} else {
|
||||
$ret = $this->readFileDataImp();
|
||||
}
|
||||
@ -181,27 +217,61 @@ namespace External\ZipStream {
|
||||
$this->offset = $offset;
|
||||
}
|
||||
|
||||
public function readCentralDirectoryHeader() {
|
||||
public function readCentralDirectoryHeader(bool $zip64 = false) {
|
||||
|
||||
$maxInt32 = 0xFFFFFFFF;
|
||||
$extraFields = "";
|
||||
|
||||
// Compressed Size
|
||||
if ($zip64 && $this->compressedSize >= $maxInt32) {
|
||||
$compressedSize = "\xFF\xFF\xFF\xFF";
|
||||
$extraFields .= pack("P", $this->compressedSize);
|
||||
} else {
|
||||
$compressedSize = pack("V", $this->compressedSize);
|
||||
}
|
||||
|
||||
// Uncompressed Size
|
||||
if ($zip64 && $this->fileSize >= $maxInt32) {
|
||||
$fileSize = "\xFF\xFF\xFF\xFF";
|
||||
$extraFields .= pack("P", $this->fileSize);
|
||||
} else {
|
||||
$fileSize = pack("V", $this->fileSize);
|
||||
}
|
||||
|
||||
// Offset
|
||||
if ($zip64 && $this->offset >= $maxInt32) {
|
||||
$offset = "\xFF\xFF\xFF\xFF";
|
||||
$extraFields .= pack("P", $this->offset);
|
||||
} else {
|
||||
$offset = pack("V", $this->offset);
|
||||
}
|
||||
|
||||
$header = "";
|
||||
$header .= "\x50\x4b\x01\x02";
|
||||
$header .= "\x14\x00"; //version 2.0 and MS-DOS compatible
|
||||
$header .= "\x14\x00"; //version 2.0 and MS-DOS compatible
|
||||
$header .= $zip64 ? "\x2d\x00" : "\x14\x00"; //version 2.0 and MS-DOS compatible
|
||||
$header .= $zip64 ? "\x2d\x00" : "\x14\x00"; //version 2.0 and MS-DOS compatible
|
||||
$header .= pack("v", $this->bitField); //general purpose bit flag
|
||||
$header .= "\x00\x00"; //compression Method - no
|
||||
$header .= $this->useCompression ? "\x08\x00" : "\x00\x00"; //compression Method - no
|
||||
$header .= pack("v", $this->unixTimeToDosTime($this->lastModificationTimestamp)); //dos time
|
||||
$header .= pack("v", $this->unixTimeToDosDate($this->lastModificationTimestamp)); //dos date
|
||||
$header .= strrev($this->crc32);
|
||||
$header .= pack("V", $this->compressedSize); //compressed Size
|
||||
$header .= pack("V", $this->fileSize); //uncompressed Size
|
||||
$header .= $compressedSize; //compressed Size
|
||||
$header .= $fileSize; //uncompressed Size
|
||||
$header .= pack("v", strlen($this->name)); //filename
|
||||
$header .= "\x00\x00"; //extra field length
|
||||
$header .= (strlen($extraFields) > 0) ? pack('v', strlen($extraFields) + 4) : "\x00\x00"; //extra field length
|
||||
$header .= "\x00\x00"; //comment length
|
||||
$header .= "\x00\x00"; //disk num start
|
||||
$header .= "\x00\x00"; //int file attr
|
||||
$header .= "\x00\x00\x00\x00"; //ext file attr
|
||||
$header .= pack("V", $this->offset); //relative offset
|
||||
$header .= $offset; //relative offset
|
||||
$header .= $this->name;
|
||||
|
||||
if (strlen($extraFields) > 0) {
|
||||
$header .= pack("v", 0x0001); # Zip64 extended information extra field
|
||||
$header .= pack("v", strlen($extraFields));
|
||||
$header .= $extraFields;
|
||||
}
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
|
46
core/External/ZipStream/FileStream.class.php
vendored
Normal file
46
core/External/ZipStream/FileStream.class.php
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace External\ZipStream {
|
||||
|
||||
use HashContext;
|
||||
use Objects\AesStream;
|
||||
|
||||
class FileStream extends File {
|
||||
|
||||
private AesStream $stream;
|
||||
private HashContext $crc32ctx;
|
||||
private HashContext $sha256ctx;
|
||||
|
||||
public function __construct(AesStream $stream, string $name) {
|
||||
parent::__construct($name);
|
||||
$this->stream = $stream;
|
||||
$this->crc32ctx = hash_init('crc32b');
|
||||
$this->sha256ctx = hash_init('sha256');
|
||||
}
|
||||
|
||||
public function getStream(): AesStream {
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
public function finalize() {
|
||||
$this->crc32 = hash_final($this->crc32ctx, true);
|
||||
$this->sha256 = hash_final($this->sha256ctx);
|
||||
return $this->compress(null);
|
||||
}
|
||||
|
||||
public function processChunk($chunk) {
|
||||
|
||||
hash_update($this->crc32ctx, $chunk);
|
||||
hash_update($this->sha256ctx, $chunk);
|
||||
$this->fileSize += strlen($chunk);
|
||||
|
||||
if ($this->useCompression) {
|
||||
$chunk = $this->compress($chunk);
|
||||
}
|
||||
|
||||
return $chunk;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
60
core/External/ZipStream/ZipStream.php
vendored
60
core/External/ZipStream/ZipStream.php
vendored
@ -23,10 +23,12 @@
|
||||
namespace External\ZipStream {
|
||||
class ZipStream {
|
||||
private $writer = null;
|
||||
private $files = [];
|
||||
private array $files = [];
|
||||
private bool $zip64;
|
||||
|
||||
public function __construct($writer) {
|
||||
public function __construct($writer, $zip64 = false) {
|
||||
$this->writer = $writer;
|
||||
$this->zip64 = $zip64;
|
||||
}
|
||||
|
||||
public function saveFile($file) {
|
||||
@ -40,32 +42,66 @@ namespace External\ZipStream {
|
||||
}
|
||||
}
|
||||
$file->setOffset($this->writer->offset());
|
||||
$this->writer->write($file->readLocalFileHeader());
|
||||
while (($buffer = $file->readFileData()) !== null) {
|
||||
$this->writer->write($buffer);
|
||||
$this->writer->write($file->readLocalFileHeader($this->zip64));
|
||||
|
||||
if ($file instanceof FileStream) {
|
||||
$file->getStream()->setOutput(function ($chunk) use ($file) {
|
||||
$this->writer->write($file->processChunk($chunk));
|
||||
});
|
||||
$file->getStream()->start();
|
||||
$this->writer->write($file->finalize());
|
||||
} else {
|
||||
while (($buffer = $file->readFileData()) !== null) {
|
||||
$this->writer->write($buffer);
|
||||
}
|
||||
}
|
||||
$this->writer->write($file->readDataDescriptor());
|
||||
|
||||
$this->writer->write($file->readDataDescriptor($this->zip64));
|
||||
$this->files[] = $file;
|
||||
$file->closeHandle();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Write end of central directory record
|
||||
public function close() {
|
||||
$size = 0;
|
||||
$offset = $this->writer->offset();
|
||||
foreach ($this->files as $file) {
|
||||
$size += $this->writer->write($file->readCentralDirectoryHeader());
|
||||
$size += $this->writer->write($file->readCentralDirectoryHeader($this->zip64));
|
||||
}
|
||||
|
||||
$data = "";
|
||||
if ($this->zip64) {
|
||||
// Size = SizeOfFixedFields + SizeOfVariableData - 12.
|
||||
$centralDirectorySize = 2*2 + 2*4 + 4*8;
|
||||
|
||||
$data .= "\x50\x4b\x06\x06";
|
||||
$data .= pack("P", $centralDirectorySize);
|
||||
$data .= "\x2d\x00"; // version 2.0 and MS-DOS compatible
|
||||
$data .= "\x2d\x00"; // version 2.0 and MS-DOS compatible
|
||||
$data .= "\x00\x00\x00\x00"; //number of disks
|
||||
$data .= "\x00\x00\x00\x00"; //number of the disk with the start of the central directory
|
||||
$data .= pack("P", count($this->files)); //total number of entries in the central directory on this disk
|
||||
$data .= pack("P", count($this->files)); //total number of entries in the central directory
|
||||
$data .= pack("P", $size); // size of the central directory
|
||||
$data .= pack("P", $offset); //offset of start of central directory with respect to the starting disk number
|
||||
|
||||
// end of central directory locator
|
||||
$data .= "\x50\x4b\x06\x07";
|
||||
$data .= "\x00\x00\x00\x00";
|
||||
$data .= pack("P", $this->writer->offset());
|
||||
$data .= pack('V', 1); //number of disks
|
||||
}
|
||||
|
||||
$data .= "\x50\x4b\x05\x06";
|
||||
$data .= "\x00\x00"; //number of disks
|
||||
$data .= "\x00\x00"; //number of the disk with the start of the central directory
|
||||
$data .= pack("v", count($this->files)); //total number of entries in the central directory on this disk
|
||||
$data .= pack("v", count($this->files)); //total number of entries in the central directory
|
||||
$data .= pack("V", $size); //size of the central directory
|
||||
$data .= pack("V", $offset); //offset of start of central directory with respect to the starting disk number
|
||||
$data .= "\x0\x0"; //comment length
|
||||
$data .= $this->zip64 ? "\xFF\xFF" : pack("v", count($this->files)); //total number of entries in the central directory on this disk
|
||||
$data .= $this->zip64 ? "\xFF\xFF" : pack("v", count($this->files)); //total number of entries in the central directory
|
||||
$data .= $this->zip64 ? "\xFF\xFF\xFF\xFF" : pack("V", $size); // size of the central directory
|
||||
$data .= $this->zip64 ? "\xFF\xFF\xFF\xFF" : pack("V", $offset); //offset of start of central directory with respect to the starting disk number
|
||||
$data .= "\x00\x00"; //comment length
|
||||
|
||||
$this->writer->write($data);
|
||||
}
|
||||
}
|
||||
|
8
core/External/composer.json
vendored
Normal file
8
core/External/composer.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"require": {
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5"
|
||||
}
|
||||
}
|
2269
core/External/composer.lock
generated
vendored
Normal file
2269
core/External/composer.lock
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -24,11 +24,11 @@ class AesStream {
|
||||
}
|
||||
}
|
||||
|
||||
public function setInput($file) {
|
||||
public function setInputFile(string $file) {
|
||||
$this->inputFile = $file;
|
||||
}
|
||||
|
||||
public function setOutput($callback) {
|
||||
public function setOutput(callable $callback) {
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
@ -36,51 +36,13 @@ class AesStream {
|
||||
$this->outputFile = $file;
|
||||
}
|
||||
|
||||
private function add(string $a, int $b): string {
|
||||
// counter $b is n = PHP_INT_SIZE bytes large
|
||||
$b_arr = pack('I', $b);
|
||||
$b_size = strlen($b_arr);
|
||||
$a_size = strlen($a);
|
||||
|
||||
$prefix = "";
|
||||
if ($a_size > $b_size) {
|
||||
$prefix = substr($a, 0, $a_size - $b_size);
|
||||
}
|
||||
|
||||
// xor last n bytes of $a with $b
|
||||
$xor = substr($a, strlen($prefix), $b_size);
|
||||
if (strlen($xor) !== strlen($b_arr)) {
|
||||
var_dump($xor);
|
||||
var_dump($b_arr);
|
||||
die();
|
||||
}
|
||||
$xor = $this->xor($xor, $b_arr);
|
||||
return $prefix . $xor;
|
||||
}
|
||||
|
||||
private function xor(string $a, string $b): string {
|
||||
$arr_a = str_split($a);
|
||||
$arr_b = str_split($b);
|
||||
if (strlen($a) !== strlen($b)) {
|
||||
var_dump($a);
|
||||
var_dump($b);
|
||||
var_dump(range(0, strlen($a) - 1));
|
||||
die();
|
||||
}
|
||||
|
||||
return implode("", array_map(function($i) use ($arr_a, $arr_b) {
|
||||
return chr(ord($arr_a[$i]) ^ ord($arr_b[$i]));
|
||||
}, range(0, strlen($a) - 1)));
|
||||
}
|
||||
|
||||
public function start(): bool {
|
||||
if (!$this->inputFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$blockSize = 16;
|
||||
$bitStrength = strlen($this->key) * 8;
|
||||
$aesMode = "AES-$bitStrength-ECB";
|
||||
$aesMode = $this->getCipherMode();
|
||||
|
||||
$outputHandle = null;
|
||||
$inputHandle = fopen($this->inputFile, "rb");
|
||||
@ -91,25 +53,30 @@ class AesStream {
|
||||
if ($this->outputFile !== null) {
|
||||
$outputHandle = fopen($this->outputFile, "wb");
|
||||
if (!$outputHandle) {
|
||||
fclose($inputHandle);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$counter = 0;
|
||||
while (!feof($inputHandle)) {
|
||||
$chunk = fread($inputHandle, 4096);
|
||||
$chunkSize = strlen($chunk);
|
||||
for ($offset = 0; $offset < $chunkSize; $offset += $blockSize) {
|
||||
$block = substr($chunk, $offset, $blockSize);
|
||||
if (strlen($block) !== $blockSize) {
|
||||
$padding = ($blockSize - strlen($block));
|
||||
$block .= str_repeat(chr($padding), $padding);
|
||||
}
|
||||
set_time_limit(0);
|
||||
|
||||
$ivCounter = $this->add($this->iv, $counter + 1);
|
||||
$encrypted = substr(openssl_encrypt($ivCounter, $aesMode, $this->key, OPENSSL_RAW_DATA), 0, $blockSize);
|
||||
$encrypted = $this->xor($encrypted, $block);
|
||||
if (is_callable($this->callback)) {
|
||||
$ivCounter = $this->iv;
|
||||
$modulo = \gmp_init("0x1" . str_repeat("00", $blockSize), 16);
|
||||
|
||||
while (!feof($inputHandle)) {
|
||||
$chunk = fread($inputHandle, 65536);
|
||||
$chunkSize = strlen($chunk);
|
||||
if ($chunkSize > 0) {
|
||||
$blockCount = intval(ceil($chunkSize / $blockSize));
|
||||
$encrypted = openssl_encrypt($chunk, $aesMode, $this->key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $ivCounter);
|
||||
|
||||
$ivNumber = \gmp_init(bin2hex($ivCounter), 16);
|
||||
$ivNumber = \gmp_add($ivNumber, $blockCount);
|
||||
$ivNumber = \gmp_mod($ivNumber, $modulo);
|
||||
$ivNumber = str_pad(\gmp_strval($ivNumber, 16), $blockSize * 2, "0", STR_PAD_LEFT);
|
||||
$ivCounter = hex2bin($ivNumber);
|
||||
|
||||
if ($this->callback !== null) {
|
||||
call_user_func($this->callback, $encrypted);
|
||||
}
|
||||
|
||||
@ -123,4 +90,17 @@ class AesStream {
|
||||
if ($outputHandle) fclose($outputHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCipherMode(): string {
|
||||
$bitStrength = strlen($this->key) * 8;
|
||||
return "aes-$bitStrength-ctr";
|
||||
}
|
||||
|
||||
public function getKey(): string {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getIV(): string {
|
||||
return $this->iv;
|
||||
}
|
||||
}
|
@ -27,9 +27,6 @@ namespace Objects {
|
||||
public function getCode(): string { return $this->langCode; }
|
||||
public function getShortCode() { return substr($this->langCode, 0, 2); }
|
||||
public function getName() { return $this->langName; }
|
||||
public function getIconPath() { return "/img/icons/lang/$this->langCode.gif"; }
|
||||
public function getEntries() { return $this->entries; }
|
||||
public function getModules() { return $this->modules; }
|
||||
|
||||
/**
|
||||
* @param $module LanguageModule class or object
|
||||
|
@ -4,6 +4,7 @@ namespace Objects;
|
||||
|
||||
use DateTime;
|
||||
use \Driver\SQL\Condition\Compare;
|
||||
use Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Exception;
|
||||
use External\JWT;
|
||||
|
||||
@ -118,7 +119,7 @@ class Session extends ApiObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function destroy() {
|
||||
public function destroy(): bool {
|
||||
return $this->user->getSQL()->update("Session")
|
||||
->set("active", false)
|
||||
->where(new Compare("Session.uid", $this->sessionId))
|
||||
@ -126,21 +127,26 @@ class Session extends ApiObject {
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function update() {
|
||||
public function update(): bool {
|
||||
$this->updateMetaData();
|
||||
$minutes = Session::DURATION;
|
||||
|
||||
$sql = $this->user->getSQL();
|
||||
return $sql->update("Session")
|
||||
->set("Session.expires", (new DateTime())->modify("+$minutes minute"))
|
||||
->set("Session.ipAddress", $this->ipAddress)
|
||||
->set("Session.os", $this->os)
|
||||
->set("Session.browser", $this->browser)
|
||||
->set("Session.data", json_encode($_SESSION))
|
||||
->set("Session.csrf_token", $this->csrfToken)
|
||||
->where(new Compare("Session.uid", $this->sessionId))
|
||||
->where(new Compare("Session.user_id", $this->user->getId()))
|
||||
->execute();
|
||||
return
|
||||
$sql->update("User")
|
||||
->set("last_online", new CurrentTimeStamp())
|
||||
->where(new Compare("uid", $this->user->getId()))
|
||||
->execute() &&
|
||||
$sql->update("Session")
|
||||
->set("Session.expires", (new DateTime())->modify("+$minutes minute"))
|
||||
->set("Session.ipAddress", $this->ipAddress)
|
||||
->set("Session.os", $this->os)
|
||||
->set("Session.browser", $this->browser)
|
||||
->set("Session.data", json_encode($_SESSION))
|
||||
->set("Session.csrf_token", $this->csrfToken)
|
||||
->where(new Compare("Session.uid", $this->sessionId))
|
||||
->where(new Compare("Session.user_id", $this->user->getId()))
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function getCsrfToken(): string {
|
||||
|
@ -17,7 +17,9 @@ class User extends ApiObject {
|
||||
private ?Session $session;
|
||||
private int $uid;
|
||||
private string $username;
|
||||
private string $fullName;
|
||||
private ?string $email;
|
||||
private ?string $profilePicture;
|
||||
private Language $language;
|
||||
private array $groups;
|
||||
|
||||
@ -55,6 +57,7 @@ class User extends ApiObject {
|
||||
public function getId(): int { return $this->uid; }
|
||||
public function isLoggedIn(): bool { return $this->loggedIn; }
|
||||
public function getUsername(): string { return $this->username; }
|
||||
public function getFullName(): string { return $this->fullName; }
|
||||
public function getEmail(): ?string { return $this->email; }
|
||||
public function getSQL(): ?SQL { return $this->sql; }
|
||||
public function getLanguage(): Language { return $this->language; }
|
||||
@ -63,6 +66,7 @@ class User extends ApiObject {
|
||||
public function getConfiguration(): Configuration { return $this->configuration; }
|
||||
public function getGroups(): array { return $this->groups; }
|
||||
public function hasGroup(int $group): bool { return isset($this->groups[$group]); }
|
||||
public function getProfilePicture() : ?string { return $this->profilePicture; }
|
||||
|
||||
public function __debugInfo(): array {
|
||||
$debugInfo = array(
|
||||
@ -83,6 +87,8 @@ class User extends ApiObject {
|
||||
return array(
|
||||
'uid' => $this->uid,
|
||||
'name' => $this->username,
|
||||
'fullName' => $this->fullName,
|
||||
'profilePicture' => $this->profilePicture,
|
||||
'email' => $this->email,
|
||||
'groups' => $this->groups,
|
||||
'language' => $this->language->jsonSerialize(),
|
||||
@ -99,8 +105,10 @@ class User extends ApiObject {
|
||||
$this->uid = 0;
|
||||
$this->username = '';
|
||||
$this->email = '';
|
||||
$this->groups = [];
|
||||
$this->loggedIn = false;
|
||||
$this->session = null;
|
||||
$this->profilePicture = null;
|
||||
}
|
||||
|
||||
public function logout(): bool {
|
||||
@ -137,9 +145,9 @@ class User extends ApiObject {
|
||||
* @param bool $sessionUpdate update session information, including session's lifetime and browser information
|
||||
* @return bool true, if the data could be loaded
|
||||
*/
|
||||
public function readData($userId, $sessionId, $sessionUpdate = true): bool {
|
||||
public function readData($userId, $sessionId, bool $sessionUpdate = true): bool {
|
||||
|
||||
$res = $this->sql->select("User.name", "User.email",
|
||||
$res = $this->sql->select("User.name", "User.email", "User.fullName", "User.profilePicture",
|
||||
"Language.uid as langId", "Language.code as langCode", "Language.name as langName",
|
||||
"Session.data", "Session.stay_logged_in", "Session.csrf_token", "Group.uid as groupId", "Group.name as groupName")
|
||||
->from("User")
|
||||
@ -162,7 +170,10 @@ class User extends ApiObject {
|
||||
$csrfToken = $row["csrf_token"];
|
||||
$this->username = $row['name'];
|
||||
$this->email = $row["email"];
|
||||
$this->fullName = $row["fullName"];
|
||||
$this->uid = $userId;
|
||||
$this->profilePicture = $row["profilePicture"];
|
||||
|
||||
$this->session = new Session($this, $sessionId, $csrfToken);
|
||||
$this->session->setData(json_decode($row["data"] ?? '{}'));
|
||||
$this->session->stayLoggedIn($this->sql->parseBool(["stay_logged_in"]));
|
||||
@ -183,16 +194,14 @@ class User extends ApiObject {
|
||||
}
|
||||
|
||||
private function parseCookies() {
|
||||
if(isset($_COOKIE['session'])
|
||||
&& is_string($_COOKIE['session'])
|
||||
&& !empty($_COOKIE['session'])) {
|
||||
if(isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) {
|
||||
try {
|
||||
$token = $_COOKIE['session'];
|
||||
$settings = $this->configuration->getSettings();
|
||||
$decoded = (array)JWT::decode($token, $settings->getJwtSecret());
|
||||
if(!is_null($decoded)) {
|
||||
$userId = (isset($decoded['userId']) ? $decoded['userId'] : NULL);
|
||||
$sessionId = (isset($decoded['sessionId']) ? $decoded['sessionId'] : NULL);
|
||||
$userId = ($decoded['userId'] ?? NULL);
|
||||
$sessionId = ($decoded['sessionId'] ?? NULL);
|
||||
if(!is_null($userId) && !is_null($sessionId)) {
|
||||
$this->readData($userId, $sessionId);
|
||||
}
|
||||
@ -226,7 +235,8 @@ class User extends ApiObject {
|
||||
return true;
|
||||
}
|
||||
|
||||
$res = $this->sql->select("ApiKey.user_id as uid", "User.name", "User.email", "User.confirmed",
|
||||
$res = $this->sql->select("ApiKey.user_id as uid", "User.name", "User.fullName", "User.email",
|
||||
"User.confirmed", "User.profilePicture",
|
||||
"Language.uid as langId", "Language.code as langCode", "Language.name as langName",
|
||||
"Group.uid as groupId", "Group.name as groupName")
|
||||
->from("ApiKey")
|
||||
@ -240,8 +250,8 @@ class User extends ApiObject {
|
||||
->execute();
|
||||
|
||||
$success = ($res !== FALSE);
|
||||
if($success) {
|
||||
if(empty($res)) {
|
||||
if ($success) {
|
||||
if (empty($res) || !is_array($res)) {
|
||||
$success = false;
|
||||
} else {
|
||||
$row = $res[0];
|
||||
@ -251,7 +261,9 @@ class User extends ApiObject {
|
||||
|
||||
$this->uid = $row['uid'];
|
||||
$this->username = $row['name'];
|
||||
$this->fullName = $row["fullName"];
|
||||
$this->email = $row['email'];
|
||||
$this->profilePicture = $row["profilePicture"];
|
||||
|
||||
if(!is_null($row['langId'])) {
|
||||
$this->setLanguage(Language::newInstance($row['langId'], $row['langCode'], $row['langName']));
|
||||
|
0
core/TemplateCache/.gitkeep
Normal file
0
core/TemplateCache/.gitkeep
Normal file
4
core/Templates/404.twig
Normal file
4
core/Templates/404.twig
Normal file
@ -0,0 +1,4 @@
|
||||
{% extends "base.twig" %}
|
||||
{% block body %}
|
||||
<b>Not found</b>
|
||||
{% endblock %}
|
36
core/Templates/account.twig
Normal file
36
core/Templates/account.twig
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "base.twig" %}
|
||||
|
||||
{% block head %}
|
||||
<script src="/js/jquery.min.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
<script src="/js/script.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
<script src="/js/account.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css" nonce="{{ site.csp.nonce }}">
|
||||
<script src="/js/bootstrap.bundle.min.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
<link rel="stylesheet" href="/css/fontawesome.min.css" nonce="{{ site.csp.nonce }}">
|
||||
<link rel="stylesheet" href="/css/account.css" nonce="{{ site.csp.nonce }}">
|
||||
<title>Account - {{ view_title }}</title>
|
||||
{% if site.recaptcha.enabled %}
|
||||
<script src="https://www.google.com/recaptcha/api.js?render={{ site.recaptcha.key }}" nonce="{{ site.csp.nonce }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-md-3 py-5 bg-primary text-white text-center" style='border-top-left-radius:.4em;border-bottom-left-radius:.4em;margin-left: auto'>
|
||||
<div class="card-body">
|
||||
<i class="fas fa-{{ view_icon }} fa-3x"></i>
|
||||
<h2 class="py-3">{{ view_title }}</h2>
|
||||
<p>{{ view_description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 pt-5 pb-2 border border-info" style='border-top-right-radius:.4em;border-bottom-right-radius:.4em;margin-right:auto'>
|
||||
{% block view_content %}{% endblock %}
|
||||
<div class='alert mt-2' style='display:none' id='alertMessage'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if site.recaptcha.enabled %}
|
||||
<input type='hidden' value='{{ site.recaptcha.key }}' id='siteKey' />
|
||||
{% endif %}
|
||||
{% endblock %}
|
46
core/Templates/account/accept_invite.twig
Normal file
46
core/Templates/account/accept_invite.twig
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends "account.twig" %}
|
||||
|
||||
{% set view_title = 'Invitation' %}
|
||||
{% set view_icon = 'user-check' %}
|
||||
{% set view_description = 'Finnish your account registration by choosing a password.' %}
|
||||
|
||||
{% block view_content %}
|
||||
|
||||
{% if not view.success %}
|
||||
<div class="alert alert-danger" role="alert">{{ view.message }}</div>
|
||||
<a href='/login' class='btn btn-primary'>Back to login</a>
|
||||
{% else %}
|
||||
<h4 class="pb-4">Please fill with your details</h4>
|
||||
<form>
|
||||
<input name='token' id='token' type='hidden' value='{{ view.token }}'/>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-hashtag"></i></span>
|
||||
</div>
|
||||
<input id="username" name="username" placeholder="Username" class="form-control" type="text" maxlength="32" value='{{ view.invited_user.name }}' disabled>
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
</div>
|
||||
<input type="email" name='email' id='email' class="form-control" placeholder="Email" maxlength="64" value='{{ view.invited_user.email }}' disabled>
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" autocomplete='new-password' name='password' id='password' class="form-control" placeholder="Password">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" autocomplete='new-password' name='confirmPassword' id='confirmPassword' class="form-control" placeholder="Confirm Password">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<button type="button" class="btn btn-success" id='btnAcceptInvite'>Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
37
core/Templates/account/confirm_email.twig
Normal file
37
core/Templates/account/confirm_email.twig
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "account.twig" %}
|
||||
|
||||
{% set view_title = 'Confirm Email' %}
|
||||
{% set view_icon = 'user-check' %}
|
||||
{% set view_description = 'Request a password reset, once you got the e-mail address, you can choose a new password' %}
|
||||
|
||||
{% block view_content %}
|
||||
<noscript>
|
||||
<div class="alert alert-danger">Javascript is required</div>
|
||||
</noscript>
|
||||
<div class="alert alert-info" id="confirm-status">
|
||||
Confirming email… <i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<a href='/login'><button class='btn btn-primary' style='position: absolute; bottom: 10px' type='button'>Proceed to Login</button></a>
|
||||
<script nonce="{{ site.csp.nonce }}">
|
||||
$(document).ready(function() {
|
||||
let token = jsCore.getParameter("token");
|
||||
let confirmStatus = $("#confirm-status");
|
||||
if (token) {
|
||||
jsCore.apiCall("/user/confirmEmail", { token: token }, (res) => {
|
||||
confirmStatus.removeClass("alert-info");
|
||||
if (!res.success) {
|
||||
confirmStatus.addClass("alert-danger");
|
||||
confirmStatus.text("Error confirming e-mail address: " + res.msg);
|
||||
} else {
|
||||
confirmStatus.addClass("alert-success");
|
||||
confirmStatus.text("Your e-mail address was successfully confirmed, you may now log in.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
confirmStatus.removeClass("alert-info");
|
||||
confirmStatus.addClass("alert-danger");
|
||||
confirmStatus.text("The link you visited is no longer valid");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
50
core/Templates/account/register.twig
Normal file
50
core/Templates/account/register.twig
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends "account.twig" %}
|
||||
|
||||
{% set view_title = 'Registration' %}
|
||||
{% set view_icon = 'user-plus' %}
|
||||
{% set view_description = 'Create a new account' %}
|
||||
|
||||
{% block view_content %}
|
||||
|
||||
{% if not view.success %}
|
||||
<div class="alert alert-danger" role="alert">{{ view.message }}</div>
|
||||
<a href='/login' class='btn btn-primary'>Go back</a>
|
||||
{% else %}
|
||||
<h4 class="pb-4">Please fill with your details</h4>
|
||||
<form>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-hashtag"></i></span>
|
||||
</div>
|
||||
<input id="username" autocomplete='username' name="username" placeholder="Username" class="form-control" type="text" maxlength="32">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
</div>
|
||||
<input type="email" autocomplete='email' name='email' id='email' class="form-control" placeholder="Email" maxlength="64">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" autocomplete='new-password' name='password' id='password' class="form-control" placeholder="Password">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" autocomplete='new-password' name='confirmPassword' id='confirmPassword' class="form-control" placeholder="Confirm Password">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<button type="button" class="btn btn-primary" id='btnRegister'>Submit</button>
|
||||
<a href='/login' style='margin-left: 10px'>
|
||||
<button class='btn btn-secondary' type='button'>
|
||||
Back to Login
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
27
core/Templates/account/resend_confirm_email.twig
Normal file
27
core/Templates/account/resend_confirm_email.twig
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends "account.twig" %}
|
||||
|
||||
{% set view_title = 'Resend Confirm Email' %}
|
||||
{% set view_icon = 'envelope' %}
|
||||
{% set view_description = 'Request a new confirmation email to finalize the account creation' %}
|
||||
|
||||
{% block view_content %}
|
||||
<p class='lead'>Enter your E-Mail address, to receive a new e-mail to confirm your registration.</p>
|
||||
<form>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
</div>
|
||||
<input id="email" autocomplete='email' name="email" placeholder="E-Mail address" class="form-control" type="email" maxlength="64" />
|
||||
</div>
|
||||
<div class="input-group mt-2" style='position: absolute;bottom: 15px'>
|
||||
<button id='btnResendConfirmEmail' class='btn btn-primary'>
|
||||
Request
|
||||
</button>
|
||||
<a href='/login' style='margin-left: 10px'>
|
||||
<button class='btn btn-secondary' type='button'>
|
||||
Back to Login
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
59
core/Templates/account/reset_password.twig
Normal file
59
core/Templates/account/reset_password.twig
Normal file
@ -0,0 +1,59 @@
|
||||
{% extends "account.twig" %}
|
||||
|
||||
{% set view_title = 'Reset Password' %}
|
||||
{% set view_icon = 'user-lock' %}
|
||||
{% set view_description = 'Request a password reset, once you got the e-mail address, you can choose a new password' %}
|
||||
|
||||
{% block view_content %}
|
||||
{% if view.token %}
|
||||
{% if not view.success %}
|
||||
<div class="alert alert-danger" role="alert">{{ view.message }}</div>
|
||||
<a href='/resetPassword' class='btn btn-primary'>Go back</a>
|
||||
{% else %}
|
||||
<h4 class="pb-4">Choose a new password</h4>
|
||||
<form>
|
||||
<input name='token' id='token' type='hidden' value='{{ view.token }}'/>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" autocomplete='new-password' name='password' id='password' class="form-control" placeholder="Password">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||
</div>
|
||||
<input type="password" autocomplete='new-password' name='confirmPassword' id='confirmPassword' class="form-control" placeholder="Confirm Password">
|
||||
</div>
|
||||
<div class="input-group mt-3">
|
||||
<button type="button" class="btn btn-primary" id='btnResetPassword'>Submit</button>
|
||||
<a href='/login' style='margin-left: 10px; display: none' id='backToLogin'>
|
||||
<button class='btn btn-success' type='button'>
|
||||
Back to Login
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class='lead'>Enter your E-Mail address, to receive a password reset token.</p>
|
||||
<form>
|
||||
<div class="input-group">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-at"></i></span>
|
||||
</div>
|
||||
<input id="email" autocomplete='email' name="email" placeholder="E-Mail address" class="form-control" type="email" maxlength="64" />
|
||||
</div>
|
||||
<div class="input-group mt-2" style='position: absolute;bottom: 15px'>
|
||||
<button id='btnRequestPasswordReset' class='btn btn-primary'>
|
||||
Request
|
||||
</button>
|
||||
<a href='/login' style='margin-left: 10px'>
|
||||
<button class='btn btn-secondary' type='button'>
|
||||
Back to Login
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
12
core/Templates/admin.twig
Normal file
12
core/Templates/admin.twig
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "base.twig" %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ site.name }} - Administration</title>
|
||||
<script src="/js/fontawesome-all.min.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<noscript>You need Javascript enabled to run this app</noscript>
|
||||
<div class="wrapper" id="root"></div>
|
||||
<script src="/js/admin.min.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
{% endblock %}
|
15
core/Templates/base.twig
Normal file
15
core/Templates/base.twig
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ user.lang }}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=yes" />
|
||||
{% block head %}
|
||||
<title>{{ site.title }}</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
8
core/Templates/mail/accept_invite.twig
Normal file
8
core/Templates/mail/accept_invite.twig
Normal file
@ -0,0 +1,8 @@
|
||||
Hello {{ username }},<br><br>
|
||||
You were invited to create an account on {{ site_name }}. Please click on the following link to confirm your email address and complete your registration by choosing a new password.
|
||||
If you want to decline the invitation, you can simply ignore this email. The link is valid for {{ valid_time }}:<br><br>
|
||||
|
||||
<a href="{{ link }}">{{ link }}</a><br><br>
|
||||
|
||||
Best Regards<br>
|
||||
{{ site_name }} Administration
|
8
core/Templates/mail/confirm_email.twig
Normal file
8
core/Templates/mail/confirm_email.twig
Normal file
@ -0,0 +1,8 @@
|
||||
Hello {{ username }},<br><br>
|
||||
You recently created an account on {{ site_name }}. Please click on the following link to confirm your email address and complete your registration.<br>
|
||||
If you haven't registered an account, you can simply ignore this email. The link is valid for {{ valid_time }}:<br><br>
|
||||
|
||||
<a href="{{ link }}">{{ link }}</a><br><br>
|
||||
|
||||
Best Regards<br>
|
||||
{{ site_name }} Administration
|
8
core/Templates/mail/reset_password.twig
Normal file
8
core/Templates/mail/reset_password.twig
Normal file
@ -0,0 +1,8 @@
|
||||
Hello {{ username }},<br>
|
||||
you requested a password reset on {{ site_name }}. Please click on the following link to choose a new password. <br>
|
||||
If this request was not intended, you can simply ignore the email. The Link is valid for {{ valid_time }}:<br><br>
|
||||
|
||||
<a href="{{ link }}">{{ link }}</a><br><br>
|
||||
|
||||
Best Regards<br>
|
||||
{{ site_name }} Administration
|
7
core/Templates/redirect.twig
Normal file
7
core/Templates/redirect.twig
Normal file
@ -0,0 +1,7 @@
|
||||
{% extends "base.twig" %}
|
||||
{% block head %}
|
||||
<meta http-equiv="refresh" content="0;url={{ url }}">
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
You will be automatically redirected to <b>{{ url }}</b>. If that doesn't work, click <a href="{{ url }}">here</a>.
|
||||
{% endblock %}
|
@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Views\Account;
|
||||
|
||||
|
||||
use Elements\Document;
|
||||
use Elements\View;
|
||||
|
||||
class AcceptInvite extends AccountView {
|
||||
|
||||
private bool $success;
|
||||
private string $message;
|
||||
private array $invitedUser;
|
||||
|
||||
public function __construct(Document $document, $loadView = true) {
|
||||
parent::__construct($document, $loadView);
|
||||
$this->title = "Invitation";
|
||||
$this->description = "Finnish your account registration by choosing a password.";
|
||||
$this->icon = "user-check";
|
||||
$this->success = false;
|
||||
$this->message = "No content";
|
||||
$this->invitedUser = array();
|
||||
}
|
||||
|
||||
public function loadView() {
|
||||
parent::loadView();
|
||||
|
||||
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
|
||||
$req = new \Api\User\CheckToken($this->getDocument()->getUser());
|
||||
$this->success = $req->execute(array("token" => $_GET["token"]));
|
||||
if ($this->success) {
|
||||
if (strcmp($req->getResult()["token"]["type"], "invite") !== 0) {
|
||||
$this->success = false;
|
||||
$this->message = "The given token has a wrong type.";
|
||||
} else {
|
||||
$this->invitedUser = $req->getResult()["user"];
|
||||
}
|
||||
} else {
|
||||
$this->message = "Error confirming e-mail address: " . $req->getLastError();
|
||||
}
|
||||
} else {
|
||||
$this->success = false;
|
||||
$this->message = "The link you visited is no longer valid";
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAccountContent() {
|
||||
if (!$this->success) {
|
||||
return $this->createErrorText($this->message);
|
||||
}
|
||||
|
||||
$token = htmlspecialchars($_GET["token"], ENT_QUOTES);
|
||||
$username = $this->invitedUser["name"];
|
||||
$emailAddress = $this->invitedUser["email"];
|
||||
|
||||
return "<h4 class=\"pb-4\">Please fill with your details</h4>
|
||||
<form>
|
||||
<input name='token' id='token' type='hidden' value='$token'/>
|
||||
<div class=\"input-group\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-hashtag\"></i></span>
|
||||
</div>
|
||||
<input id=\"username\" name=\"username\" placeholder=\"Username\" class=\"form-control\" type=\"text\" maxlength=\"32\" value='$username' disabled>
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-at\"></i></span>
|
||||
</div>
|
||||
<input type=\"email\" name='email' id='email' class=\"form-control\" placeholder=\"Email\" maxlength=\"64\" value='$emailAddress' disabled>
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-key\"></i></span>
|
||||
</div>
|
||||
<input type=\"password\" autocomplete='new-password' name='password' id='password' class=\"form-control\" placeholder=\"Password\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-key\"></i></span>
|
||||
</div>
|
||||
<input type=\"password\" autocomplete='new-password' name='confirmPassword' id='confirmPassword' class=\"form-control\" placeholder=\"Confirm Password\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<button type=\"button\" class=\"btn btn-success\" id='btnAcceptInvite'>Submit</button>
|
||||
</div>
|
||||
</form>";
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Views\Account;
|
||||
|
||||
use Elements\Document;
|
||||
use Elements\View;
|
||||
|
||||
abstract class AccountView extends View {
|
||||
|
||||
protected string $description;
|
||||
protected string $icon;
|
||||
|
||||
public function __construct(Document $document, $loadView = true) {
|
||||
parent::__construct($document, $loadView);
|
||||
$this->description = "";
|
||||
$this->icon = "image";
|
||||
}
|
||||
|
||||
public function loadView() {
|
||||
parent::loadView();
|
||||
|
||||
$document = $this->getDocument();
|
||||
$settings = $document->getUser()->getConfiguration()->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
$document->getHead()->loadGoogleRecaptcha($settings->getRecaptchaSiteKey());
|
||||
}
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
$html = parent::getCode();
|
||||
|
||||
$content = $this->getAccountContent();
|
||||
$icon = $this->createIcon($this->icon, "fas", "fa-3x");
|
||||
|
||||
$html .= "<div class=\"container mt-5\">
|
||||
<div class=\"row\">
|
||||
<div class=\"col-md-3 py-5 bg-primary text-white text-center\" style='border-top-left-radius:.4em;border-bottom-left-radius:.4em;margin-left: auto'>
|
||||
<div class=\"card-body\">
|
||||
$icon
|
||||
<h2 class=\"py-3\">$this->title</h2>
|
||||
<p>$this->description</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\"col-md-5 pt-5 pb-2 border border-info\" style='border-top-right-radius:.4em;border-bottom-right-radius:.4em;margin-right:auto'>
|
||||
$content
|
||||
<div class='alert mt-2' style='display:none' id='alertMessage'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>";
|
||||
|
||||
$settings = $this->getDocument()->getUser()->getConfiguration()->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
$siteKey = $settings->getRecaptchaSiteKey();
|
||||
$html .= "<input type='hidden' value='$siteKey' id='siteKey' />";
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
protected abstract function getAccountContent();
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Views\Account;
|
||||
|
||||
|
||||
use Elements\Document;
|
||||
use Elements\Script;
|
||||
|
||||
class ConfirmEmail extends AccountView {
|
||||
|
||||
public function __construct(Document $document, $loadView = true) {
|
||||
parent::__construct($document, $loadView);
|
||||
$this->title = "Confirm Email";
|
||||
$this->description = "Request a password reset, once you got the e-mail address, you can choose a new password";
|
||||
$this->icon = "user-check";
|
||||
}
|
||||
|
||||
public function loadView() {
|
||||
parent::loadView();
|
||||
$this->getDocument()->getHead()->addScript(Script::MIME_TEXT_JAVASCRIPT, "", '
|
||||
$(document).ready(function() {
|
||||
var token = jsCore.getParameter("token");
|
||||
if (token) {
|
||||
jsCore.apiCall("/user/confirmEmail", { token: token }, (res) => {
|
||||
$("#confirm-status").removeClass("alert-info");
|
||||
if (!res.success) {
|
||||
$("#confirm-status").addClass("alert-danger");
|
||||
$("#confirm-status").text("Error confirming e-mail address: " + res.msg);
|
||||
} else {
|
||||
$("#confirm-status").addClass("alert-success");
|
||||
$("#confirm-status").text("Your e-mail address was successfully confirmed, you may now log in.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#confirm-status").removeClass("alert-info");
|
||||
$("#confirm-status").addClass("alert-danger");
|
||||
$("#confirm-status").text("The link you visited is no longer valid");
|
||||
}
|
||||
});'
|
||||
);
|
||||
}
|
||||
|
||||
protected function getAccountContent() {
|
||||
|
||||
$spinner = $this->createIcon("spinner");
|
||||
$html = "<noscript><div class=\"alert alert-danger\">Javascript is required</div></noscript>
|
||||
<div class=\"alert alert-info\" id=\"confirm-status\">
|
||||
Confirming email… $spinner
|
||||
</div>";
|
||||
|
||||
$html .= "<a href='/login'><button class='btn btn-primary' style='position: absolute; bottom: 10px' type='button'>Proceed to Login</button></a>";
|
||||
return $html;
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Views\Account;
|
||||
|
||||
use Elements\Document;
|
||||
|
||||
class Register extends AccountView {
|
||||
|
||||
public function __construct(Document $document, $loadView = true) {
|
||||
parent::__construct($document, $loadView);
|
||||
$this->title = "Registration";
|
||||
$this->description = "Create a new account";
|
||||
$this->icon = "user-plus";
|
||||
}
|
||||
|
||||
public function getAccountContent() {
|
||||
|
||||
$user = $this->getDocument()->getUser();
|
||||
if ($user->isLoggedIn()) {
|
||||
header(302);
|
||||
header("Location: /");
|
||||
die("You are already logged in.");
|
||||
}
|
||||
|
||||
$settings = $user->getConfiguration()->getSettings();
|
||||
if (!$settings->isRegistrationAllowed()) {
|
||||
return $this->createErrorText(
|
||||
"Registration is not enabled on this website. If you are an administrator,
|
||||
goto <a href=\"/admin/settings\">/admin/settings</a>, to enable the user registration"
|
||||
);
|
||||
}
|
||||
|
||||
return "<h4 class=\"pb-4\">Please fill with your details</h4>
|
||||
<form>
|
||||
<div class=\"input-group\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-hashtag\"></i></span>
|
||||
</div>
|
||||
<input id=\"username\" autocomplete='username' name=\"username\" placeholder=\"Username\" class=\"form-control\" type=\"text\" maxlength=\"32\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-at\"></i></span>
|
||||
</div>
|
||||
<input type=\"email\" autocomplete='email' name='email' id='email' class=\"form-control\" placeholder=\"Email\" maxlength=\"64\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-key\"></i></span>
|
||||
</div>
|
||||
<input type=\"password\" autocomplete='new-password' name='password' id='password' class=\"form-control\" placeholder=\"Password\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-key\"></i></span>
|
||||
</div>
|
||||
<input type=\"password\" autocomplete='new-password' name='confirmPassword' id='confirmPassword' class=\"form-control\" placeholder=\"Confirm Password\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<button type=\"button\" class=\"btn btn-primary\" id='btnRegister'>Submit</button>
|
||||
<a href='/login' style='margin-left: 10px'>
|
||||
<button class='btn btn-secondary' type='button'>
|
||||
Back to Login
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>";
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Views\Account;
|
||||
|
||||
|
||||
use Elements\Document;
|
||||
|
||||
class ResendConfirmEmail extends AccountView {
|
||||
|
||||
public function __construct(Document $document, $loadView = true) {
|
||||
parent::__construct($document, $loadView);
|
||||
$this->title = "Resend Confirm Email";
|
||||
$this->description = "Request a new confirmation email to finalize the account creation";
|
||||
$this->icon = "envelope";
|
||||
}
|
||||
|
||||
protected function getAccountContent() {
|
||||
return "<p class='lead'>Enter your E-Mail address, to receive a new e-mail to confirm your registration.</p>
|
||||
<form>
|
||||
<div class=\"input-group\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-at\"></i></span>
|
||||
</div>
|
||||
<input id=\"email\" autocomplete='email' name=\"email\" placeholder=\"E-Mail address\" class=\"form-control\" type=\"email\" maxlength=\"64\" />
|
||||
</div>
|
||||
<div class=\"input-group mt-2\" style='position: absolute;bottom: 15px'>
|
||||
<button id='btnResendConfirmEmail' class='btn btn-primary'>
|
||||
Request
|
||||
</button>
|
||||
<a href='/login' style='margin-left: 10px'>
|
||||
<button class='btn btn-secondary' type='button'>
|
||||
Back to Login
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Views\Account;
|
||||
|
||||
|
||||
use Elements\Document;
|
||||
|
||||
class ResetPassword extends AccountView {
|
||||
|
||||
private bool $success;
|
||||
private string $message;
|
||||
private ?string $token;
|
||||
|
||||
public function __construct(Document $document, $loadView = true) {
|
||||
parent::__construct($document, $loadView);
|
||||
$this->title = "Reset Password";
|
||||
$this->description = "Request a password reset, once you got the e-mail address, you can choose a new password";
|
||||
$this->icon = "user-lock";
|
||||
$this->success = true;
|
||||
$this->message = "";
|
||||
$this->token = NULL;
|
||||
}
|
||||
|
||||
public function loadView() {
|
||||
parent::loadView();
|
||||
|
||||
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
|
||||
$this->token = $_GET["token"];
|
||||
$req = new \Api\User\CheckToken($this->getDocument()->getUser());
|
||||
$this->success = $req->execute(array("token" => $_GET["token"]));
|
||||
if ($this->success) {
|
||||
if (strcmp($req->getResult()["token"]["type"], "password_reset") !== 0) {
|
||||
$this->success = false;
|
||||
$this->message = "The given token has a wrong type.";
|
||||
}
|
||||
} else {
|
||||
$this->message = "Error requesting password reset: " . $req->getLastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAccountContent() {
|
||||
if (!$this->success) {
|
||||
$html = $this->createErrorText($this->message);
|
||||
if ($this->token !== null) {
|
||||
$html .= "<a href='/resetPassword' class='btn btn-primary'>Go back</a>";
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
if ($this->token === null) {
|
||||
return "<p class='lead'>Enter your E-Mail address, to receive a password reset token.</p>
|
||||
<form>
|
||||
<div class=\"input-group\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-at\"></i></span>
|
||||
</div>
|
||||
<input id=\"email\" autocomplete='email' name=\"email\" placeholder=\"E-Mail address\" class=\"form-control\" type=\"email\" maxlength=\"64\" />
|
||||
</div>
|
||||
<div class=\"input-group mt-2\" style='position: absolute;bottom: 15px'>
|
||||
<button id='btnRequestPasswordReset' class='btn btn-primary'>
|
||||
Request
|
||||
</button>
|
||||
<a href='/login' style='margin-left: 10px'>
|
||||
<button class='btn btn-secondary' type='button'>
|
||||
Back to Login
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
";
|
||||
} else {
|
||||
return "<h4 class=\"pb-4\">Choose a new password</h4>
|
||||
<form>
|
||||
<input name='token' id='token' type='hidden' value='$this->token'/>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-key\"></i></span>
|
||||
</div>
|
||||
<input type=\"password\" autocomplete='new-password' name='password' id='password' class=\"form-control\" placeholder=\"Password\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<div class=\"input-group-append\">
|
||||
<span class=\"input-group-text\"><i class=\"fas fa-key\"></i></span>
|
||||
</div>
|
||||
<input type=\"password\" autocomplete='new-password' name='confirmPassword' id='confirmPassword' class=\"form-control\" placeholder=\"Confirm Password\">
|
||||
</div>
|
||||
<div class=\"input-group mt-3\">
|
||||
<button type=\"button\" class=\"btn btn-success\" id='btnResetPassword'>Submit</button>
|
||||
</div>
|
||||
</form>";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Views\Admin;
|
||||
|
||||
use Elements\Body;
|
||||
use Elements\Script;
|
||||
|
||||
class AdminDashboardBody extends Body {
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
$html = parent::getCode();
|
||||
$script = new Script(Script::MIME_TEXT_JAVASCRIPT, "/js/admin.min.js");
|
||||
$html .= "<body><div class=\"wrapper\" id=\"root\">$script</div></body>";
|
||||
return $html;
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Views\Admin;
|
||||
|
||||
use Elements\Body;
|
||||
use Elements\Link;
|
||||
use Elements\Script;
|
||||
use Views\LanguageFlags;
|
||||
|
||||
class LoginBody extends Body {
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
public function loadView() {
|
||||
parent::loadView();
|
||||
$head = $this->getDocument()->getHead();
|
||||
$head->loadJQuery();
|
||||
$head->loadBootstrap();
|
||||
$head->addJS(Script::CORE);
|
||||
$head->addCSS(Link::CORE);
|
||||
$head->addJS(Script::ACCOUNT);
|
||||
$head->addCSS(Link::ACCOUNT);
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
$html = parent::getCode();
|
||||
|
||||
$username = L("Username");
|
||||
$password = L("Password");
|
||||
$login = L("Login");
|
||||
$backToStartPage = L("Back to Start Page");
|
||||
$stayLoggedIn = L("Stay logged in");
|
||||
|
||||
$flags = $this->load(LanguageFlags::class);
|
||||
$iconBack = $this->createIcon("arrow-circle-left");
|
||||
$domain = $_SERVER['HTTP_HOST'];
|
||||
$protocol = getProtocol();
|
||||
|
||||
$html .= "
|
||||
<body>
|
||||
<div class=\"container mt-4\">
|
||||
<div class=\"title text-center\">
|
||||
<h2>Admin Control Panel</h2>
|
||||
</div>
|
||||
<div class=\"row\">
|
||||
<div class=\"col-lg-6 col-12 m-auto\">
|
||||
<form class=\"loginForm\">
|
||||
<label for=\"username\">$username</label>
|
||||
<input type=\"text\" class=\"form-control\" name=\"username\" id=\"username\" placeholder=\"$username\" required autofocus />
|
||||
<label for=\"password\">$password</label>
|
||||
<input type=\"password\" class=\"form-control\" name=\"password\" id=\"password\" placeholder=\"$password\" required />
|
||||
<div class=\"form-check\">
|
||||
<input type=\"checkbox\" class=\"form-check-input\" id=\"stayLoggedIn\" name=\"stayLoggedIn\">
|
||||
<label class=\"form-check-label\" for=\"stayLoggedIn\">$stayLoggedIn</label>
|
||||
</div>
|
||||
<button class=\"btn btn-lg btn-primary btn-block\" id=\"btnLogin\" type=\"button\">$login</button>
|
||||
<div class=\"alert alert-danger\" style='display:none' role=\"alert\" id=\"alertMessage\"></div>
|
||||
<span class=\"flags position-absolute\">$flags</span>
|
||||
</form>
|
||||
<div class=\"p-1\">
|
||||
<a href=\"$protocol://$domain\">$iconBack $backToStartPage</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Views;
|
||||
|
||||
use Elements\View;
|
||||
|
||||
class View404 extends View {
|
||||
|
||||
public function getCode(): string {
|
||||
return parent::getCode() . "<b>Not found</b>";
|
||||
}
|
||||
|
||||
};
|
@ -1,14 +1,18 @@
|
||||
<?php
|
||||
|
||||
define("WEBBASE_VERSION", "1.3.0");
|
||||
require_once "External/vendor/autoload.php";
|
||||
|
||||
define("WEBBASE_VERSION", "1.3.0-beta");
|
||||
|
||||
spl_autoload_extensions(".php");
|
||||
spl_autoload_register(function($class) {
|
||||
$full_path = WEBROOT . "/" . getClassPath($class);
|
||||
if (file_exists($full_path)) {
|
||||
include_once $full_path;
|
||||
} else {
|
||||
include_once getClassPath($class, false);
|
||||
if (!class_exists($class)) {
|
||||
$full_path = WEBROOT . "/" . getClassPath($class);
|
||||
if (file_exists($full_path)) {
|
||||
include_once $full_path;
|
||||
} else {
|
||||
include_once getClassPath($class, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -24,6 +28,13 @@ function getProtocol(): string {
|
||||
return $isSecure ? 'https' : 'http';
|
||||
}
|
||||
|
||||
function uuidv4(): string {
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
function generateRandomString($length, $type = "ascii"): string {
|
||||
$randomString = '';
|
||||
|
||||
@ -31,12 +42,14 @@ function generateRandomString($length, $type = "ascii"): string {
|
||||
$uppercase = strtoupper($lowercase);
|
||||
$digits = "0123456789";
|
||||
$hex = $digits . substr($lowercase, 0, 6);
|
||||
$ascii = $lowercase . $uppercase . $digits;
|
||||
$ascii = $uppercase . $lowercase . $digits;
|
||||
|
||||
if ($length > 0) {
|
||||
$type = strtolower($type);
|
||||
if ($type === "hex") {
|
||||
$charset = $hex;
|
||||
} else if ($type === "base64") {
|
||||
$charset = $ascii . "/+";
|
||||
} else {
|
||||
$charset = $ascii;
|
||||
}
|
||||
@ -136,6 +149,13 @@ function urlId($str) {
|
||||
return urlencode(htmlspecialchars(preg_replace("[: ]","-", $str)));
|
||||
}
|
||||
|
||||
function html_attributes(array $attributes): string {
|
||||
return implode(" ", array_map(function ($key) use ($attributes) {
|
||||
$value = $attributes[$key];
|
||||
return "$key=\"$value\"";
|
||||
}, array_keys($attributes)));
|
||||
}
|
||||
|
||||
function getClassPath($class, $suffix = true): string {
|
||||
$path = str_replace('\\', '/', $class);
|
||||
$path = array_values(array_filter(explode("/", $path)));
|
||||
|
@ -1,46 +1,51 @@
|
||||
server {
|
||||
index index.php index.html;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.log;
|
||||
root /application;
|
||||
index index.php index.html;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.log;
|
||||
root /application;
|
||||
|
||||
# rewrite api
|
||||
rewrite ^/api(/.*)$ /index.php?api=$1;
|
||||
# rewrite api
|
||||
rewrite ^/api(/.*)$ /index.php?api=$1;
|
||||
|
||||
# deny access to .gitignore / .htaccess
|
||||
location ~ /\. {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
# deny access to .gitignore / .htaccess
|
||||
location ~ /\. {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
}
|
||||
|
||||
# deny access to docker-compose.yml
|
||||
location /docker-compose.yml {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
}
|
||||
|
||||
# deny access to specific directories
|
||||
location ~ ^/(files/uploaded|adminPanel|fileControlPanel|docker|core|test)/.*$ {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
}
|
||||
|
||||
# caching
|
||||
location ~ ^/(static|js|css)/.*$ {
|
||||
add_header "Cache-Control" "max-age=0; must-revalidate";
|
||||
}
|
||||
|
||||
# deny access to docker-compose.yml
|
||||
location /docker-compose.yml {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
}
|
||||
# try to find the specified file
|
||||
location / {
|
||||
try_files $uri $uri @redirectToIndex;
|
||||
}
|
||||
|
||||
# deny access to specific directories
|
||||
location ~ ^/(files/uploaded|adminPanel|fileControlPanel|docker|core)/.*$ {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
}
|
||||
# redirect to index.php
|
||||
location @redirectToIndex {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
}
|
||||
|
||||
# try to find the specified file
|
||||
location / {
|
||||
try_files $uri $uri @redirectToIndex;
|
||||
}
|
||||
|
||||
# redirect to index.php
|
||||
location @redirectToIndex {
|
||||
rewrite ^(.*)$ /index.php?site=$1;
|
||||
}
|
||||
|
||||
# serve .php files
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
# serve .php files
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
|
||||
}
|
15
index.php
15
index.php
@ -4,10 +4,11 @@ include_once 'core/core.php';
|
||||
include_once 'core/datetime.php';
|
||||
include_once 'core/constants.php';
|
||||
|
||||
if (is_file("MAINTENANCE")) {
|
||||
define("WEBROOT", realpath("."));
|
||||
|
||||
if (is_file("MAINTENANCE") && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
|
||||
http_response_code(503);
|
||||
$currentDir = dirname(__FILE__);
|
||||
serveStatic($currentDir, "/static/maintenance.html");
|
||||
serveStatic(WEBROOT, "/static/maintenance.html");
|
||||
die();
|
||||
}
|
||||
|
||||
@ -34,10 +35,10 @@ if(isset($_GET["api"]) && is_string($_GET["api"])) {
|
||||
} else {
|
||||
$apiFunction = $_GET["api"];
|
||||
if(empty($apiFunction)) {
|
||||
header("403 Forbidden");
|
||||
http_response_code(403);
|
||||
$response = "";
|
||||
} else if(!preg_match("/[a-zA-Z]+(\/[a-zA-Z]+)*/", $apiFunction)) {
|
||||
header("400 Bad Request");
|
||||
http_response_code(400);
|
||||
$response = createError("Invalid Method");
|
||||
} else {
|
||||
$apiFunction = array_filter(array_map('ucfirst', explode("/", $apiFunction)));
|
||||
@ -52,13 +53,13 @@ if(isset($_GET["api"]) && is_string($_GET["api"])) {
|
||||
try {
|
||||
$file = getClassPath($parentClass);
|
||||
if(!file_exists($file) || !class_exists($parentClass) || !class_exists($apiClass)) {
|
||||
header("404 Not Found");
|
||||
http_response_code(404);
|
||||
$response = createError("Not found");
|
||||
} else {
|
||||
$parentClass = new ReflectionClass($parentClass);
|
||||
$apiClass = new ReflectionClass($apiClass);
|
||||
if(!$apiClass->isSubclassOf(Request::class) || !$apiClass->isInstantiable()) {
|
||||
header("400 Bad Request");
|
||||
http_response_code(400);
|
||||
$response = createError("Invalid Method");
|
||||
} else {
|
||||
$request = $apiClass->newInstanceArgs(array($user, true));
|
||||
|
@ -123,8 +123,8 @@ $(document).ready(function () {
|
||||
if (!res.success) {
|
||||
showAlert("danger", res.msg);
|
||||
} else {
|
||||
showAlert("success", "Account successfully created. You may now login.");
|
||||
$("input").val("");
|
||||
document.location = "/login?success=" + encodeURIComponent("Account successfully created. You may now login.");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -182,6 +182,8 @@ $(document).ready(function () {
|
||||
} else {
|
||||
showAlert("success", "Your password was successfully changed. You may now login.");
|
||||
$("input:not([id='siteKey'])").val("");
|
||||
btn.hide();
|
||||
$("#backToLogin").show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
143
test/AesStream.test.php
Normal file
143
test/AesStream.test.php
Normal file
@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
use Objects\AesStream;
|
||||
|
||||
class AesStreamTest extends PHPUnit\Framework\TestCase {
|
||||
|
||||
static string $TEMP_FILE;
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
AesStreamTest::$TEMP_FILE = tempnam(sys_get_temp_dir(), 'aesTest');
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
unlink(AesStreamTest::$TEMP_FILE);
|
||||
}
|
||||
|
||||
public function testConstructorInvalidKey1() {
|
||||
$this->expectExceptionMessage("Invalid Key Size");
|
||||
$this->expectException(\Exception::class);
|
||||
new AesStream("", "");
|
||||
}
|
||||
|
||||
public function testConstructorInvalidKey2() {
|
||||
$this->expectExceptionMessage("Invalid Key Size");
|
||||
$this->expectException(\Exception::class);
|
||||
new AesStream(str_repeat("A",15), "");
|
||||
}
|
||||
|
||||
public function testConstructorInvalidKey3() {
|
||||
$this->expectExceptionMessage("Invalid Key Size");
|
||||
$this->expectException(\Exception::class);
|
||||
new AesStream(str_repeat("A",33), "");
|
||||
}
|
||||
|
||||
public function testConstructorInvalidIV1() {
|
||||
$this->expectExceptionMessage("Invalid IV Size");
|
||||
$this->expectException(\Exception::class);
|
||||
new AesStream(str_repeat("A",32), str_repeat("B", 17));
|
||||
}
|
||||
|
||||
public function testConstructorValid() {
|
||||
$key = str_repeat("A",32);
|
||||
$iv = str_repeat("B", 16);
|
||||
$aesStream = new AesStream($key, $iv);
|
||||
$this->assertInstanceOf(AesStream::class, $aesStream);
|
||||
$this->assertEquals($key, $aesStream->getKey());
|
||||
$this->assertEquals($iv, $aesStream->getIV());
|
||||
$this->assertEquals("aes-256-ctr", $aesStream->getCipherMode());
|
||||
}
|
||||
|
||||
private function getOutput(string $chunk, string &$data) {
|
||||
$data .= $chunk;
|
||||
}
|
||||
|
||||
public function testEncrypt() {
|
||||
$key = str_repeat("A", 32);
|
||||
$iv = str_repeat("B", 16);
|
||||
$aesStream = new AesStream($key, $iv);
|
||||
|
||||
$data = [
|
||||
"43" => "8c", # small block test 1 (1 byte)
|
||||
"abcd" => "6424", # small block test 2 (2 byte)
|
||||
"a37c599429cfdefde6546ad6d7082a" => "6c9539264abc8cae39308dbc86e768", # small block test 3 (15 byte)
|
||||
"43b3504077482bd9bf8c3c08ad3c937f" => "8c5a30f2143b798a60e8db62fcd3d1f7", # one block (16 byte)
|
||||
"9b241a3d7e9f03f6e66a8fa0cba3221008eda86f465e3fbfb0f3a4d3527cffb7"
|
||||
=> "54cd7a8f1dec51a5390e68ca9a4c60986aaafadd42b6960a09deedfa7f2cf1c3" # two blocks (16 byte)
|
||||
];
|
||||
|
||||
foreach ($data as $pt => $ct) {
|
||||
$output = "";
|
||||
file_put_contents(AesStreamTest::$TEMP_FILE, hex2bin($pt));
|
||||
$aesStream->setInputFile(AesStreamTest::$TEMP_FILE);
|
||||
$aesStream->setOutput(function($chunk) use (&$output) { $this->getOutput($chunk, $output); });
|
||||
$aesStream->start();
|
||||
$this->assertEquals($ct, bin2hex($output), $ct . " != " . bin2hex($output));
|
||||
}
|
||||
}
|
||||
|
||||
private function openssl(AesStream $aesStream) {
|
||||
// check if openssl util produce the same output
|
||||
$cmd = ["/usr/bin/openssl", $aesStream->getCipherMode(), "-K", bin2hex($aesStream->getKey()), "-iv", bin2hex($aesStream->getIV()), "-in", AesStreamTest::$TEMP_FILE];
|
||||
$proc = proc_open($cmd, [1 => ["pipe", "w"]], $pipes);
|
||||
$this->assertTrue(is_resource($proc));
|
||||
$this->assertTrue(is_resource($pipes[1]));
|
||||
$output = stream_get_contents($pipes[1]);
|
||||
proc_close($proc);
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function testEncryptDecrypt($key, $iv, $inputData) {
|
||||
$aesStream = new AesStream($key, $iv);
|
||||
$inputSize = strlen($inputData);
|
||||
file_put_contents(AesStreamTest::$TEMP_FILE, $inputData);
|
||||
|
||||
$output = "";
|
||||
$aesStream->setInputFile(AesStreamTest::$TEMP_FILE);
|
||||
$aesStream->setOutput(function($chunk) use (&$output) { $this->getOutput($chunk, $output); });
|
||||
$aesStream->start();
|
||||
|
||||
$this->assertEquals($inputSize, strlen($output));
|
||||
$this->assertNotEquals($inputData, $output);
|
||||
|
||||
// check if openssl util produce the same output
|
||||
$this->assertEquals($this->openssl($aesStream), $output);
|
||||
|
||||
file_put_contents(AesStreamTest::$TEMP_FILE, $output);
|
||||
$output = "";
|
||||
$aesStream->setInputFile(AesStreamTest::$TEMP_FILE);
|
||||
$aesStream->setOutput(function($chunk) use (&$output) { $this->getOutput($chunk, $output); });
|
||||
$aesStream->start();
|
||||
$this->assertEquals($inputData, $output);
|
||||
|
||||
// check if openssl util produce the same output
|
||||
$this->assertEquals($this->openssl($aesStream), $output);
|
||||
}
|
||||
|
||||
public function testEncryptDecryptRandom() {
|
||||
$chunkSize = 65536;
|
||||
$key = random_bytes(32);
|
||||
$iv = random_bytes(16);
|
||||
$inputSize = 10 * $chunkSize;
|
||||
$inputData = random_bytes($inputSize);
|
||||
$this->testEncryptDecrypt($key, $iv, $inputData);
|
||||
}
|
||||
|
||||
public function testEncryptDecryptLargeIV() {
|
||||
$chunkSize = 65536;
|
||||
$key = random_bytes(32);
|
||||
$iv = hex2bin(str_repeat("FF", 16));
|
||||
$inputSize = 10 * $chunkSize;
|
||||
$inputData = random_bytes($inputSize);
|
||||
$this->testEncryptDecrypt($key, $iv, $inputData);
|
||||
}
|
||||
|
||||
public function testEncryptDecryptZeroIV() {
|
||||
$chunkSize = 65536;
|
||||
$key = random_bytes(32);
|
||||
$iv = hex2bin(str_repeat("00", 16));
|
||||
$inputSize = 10 * $chunkSize;
|
||||
$inputData = random_bytes($inputSize);
|
||||
$this->testEncryptDecrypt($key, $iv, $inputData);
|
||||
}
|
||||
}
|
108
test/Parameter.test.php
Normal file
108
test/Parameter.test.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use Api\Parameter\ArrayType;
|
||||
use Api\Parameter\StringType;
|
||||
use Api\Parameter\Parameter;
|
||||
|
||||
class ParameterTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
public function testStringType() {
|
||||
|
||||
// test various string sizes
|
||||
$unlimited = new StringType("test_unlimited");
|
||||
$this->assertTrue($unlimited->parseParam(str_repeat("A", 1024)));
|
||||
|
||||
$empty = new StringType("test_empty", 0);
|
||||
$this->assertTrue($empty->parseParam(""));
|
||||
$this->assertTrue($empty->parseParam("A"));
|
||||
|
||||
$one = new StringType("test_one", 1);
|
||||
$this->assertTrue($one->parseParam(""));
|
||||
$this->assertTrue($one->parseParam("A"));
|
||||
$this->assertFalse($one->parseParam("AB"));
|
||||
|
||||
$randomSize = rand(1, 64);
|
||||
$random = new StringType("test_empty", $randomSize);
|
||||
$data = str_repeat("A", $randomSize);
|
||||
$this->assertTrue($random->parseParam(""));
|
||||
$this->assertTrue($random->parseParam("A"));
|
||||
$this->assertTrue($random->parseParam($data));
|
||||
$this->assertEquals($data, $random->value);
|
||||
|
||||
// test data types
|
||||
$this->assertFalse($random->parseParam(null));
|
||||
$this->assertFalse($random->parseParam(1));
|
||||
$this->assertFalse($random->parseParam(2.5));
|
||||
$this->assertFalse($random->parseParam(true));
|
||||
$this->assertFalse($random->parseParam(false));
|
||||
$this->assertFalse($random->parseParam(["key" => 1]));
|
||||
}
|
||||
|
||||
public function testArrayType() {
|
||||
|
||||
// int array type
|
||||
$arrayType = new ArrayType("int_array", Parameter::TYPE_INT);
|
||||
$this->assertTrue($arrayType->parseParam([1,2,3]));
|
||||
$this->assertTrue($arrayType->parseParam([1]));
|
||||
$this->assertTrue($arrayType->parseParam(["1"]));
|
||||
$this->assertTrue($arrayType->parseParam([1.0]));
|
||||
$this->assertTrue($arrayType->parseParam([]));
|
||||
$this->assertTrue($arrayType->parseParam(["1.0"]));
|
||||
$this->assertFalse($arrayType->parseParam([1.2]));
|
||||
$this->assertFalse($arrayType->parseParam(["1.5"]));
|
||||
$this->assertFalse($arrayType->parseParam([true]));
|
||||
$this->assertFalse($arrayType->parseParam(1));
|
||||
|
||||
// optional single value
|
||||
$arrayType = new ArrayType("int_array_single", Parameter::TYPE_INT, true);
|
||||
$this->assertTrue($arrayType->parseParam(1));
|
||||
|
||||
// mixed values
|
||||
$arrayType = new ArrayType("mixed_array", Parameter::TYPE_MIXED);
|
||||
$this->assertTrue($arrayType->parseParam([1, 2.5, "test", false]));
|
||||
}
|
||||
|
||||
public function testParseType() {
|
||||
// int
|
||||
$this->assertEquals(Parameter::TYPE_INT, Parameter::parseType(1));
|
||||
$this->assertEquals(Parameter::TYPE_INT, Parameter::parseType(1.0));
|
||||
$this->assertEquals(Parameter::TYPE_INT, Parameter::parseType("1"));
|
||||
$this->assertEquals(Parameter::TYPE_INT, Parameter::parseType("1.0"));
|
||||
|
||||
// array
|
||||
$this->assertEquals(Parameter::TYPE_ARRAY, Parameter::parseType([1, true]));
|
||||
|
||||
// float
|
||||
$this->assertEquals(Parameter::TYPE_FLOAT, Parameter::parseType(1.5));
|
||||
$this->assertEquals(Parameter::TYPE_FLOAT, Parameter::parseType(1.234e2));
|
||||
$this->assertEquals(Parameter::TYPE_FLOAT, Parameter::parseType("1.75"));
|
||||
|
||||
// boolean
|
||||
$this->assertEquals(Parameter::TYPE_BOOLEAN, Parameter::parseType(true));
|
||||
$this->assertEquals(Parameter::TYPE_BOOLEAN, Parameter::parseType(false));
|
||||
$this->assertEquals(Parameter::TYPE_BOOLEAN, Parameter::parseType("true"));
|
||||
$this->assertEquals(Parameter::TYPE_BOOLEAN, Parameter::parseType("false"));
|
||||
|
||||
// date
|
||||
$this->assertEquals(Parameter::TYPE_DATE, Parameter::parseType("2021-11-13"));
|
||||
$this->assertEquals(Parameter::TYPE_STRING, Parameter::parseType("2021-13-11")); # invalid date
|
||||
|
||||
// time
|
||||
$this->assertEquals(Parameter::TYPE_TIME, Parameter::parseType("10:11:12"));
|
||||
$this->assertEquals(Parameter::TYPE_STRING, Parameter::parseType("25:11:12")); # invalid time
|
||||
|
||||
// datetime
|
||||
$this->assertEquals(Parameter::TYPE_DATE_TIME, Parameter::parseType("2021-11-13 10:11:12"));
|
||||
$this->assertEquals(Parameter::TYPE_STRING, Parameter::parseType("2021-13-13 10:11:12")); # invalid date
|
||||
$this->assertEquals(Parameter::TYPE_STRING, Parameter::parseType("2021-13-11 10:61:12")); # invalid time
|
||||
|
||||
// email
|
||||
$this->assertEquals(Parameter::TYPE_EMAIL, Parameter::parseType("a@b.com"));
|
||||
$this->assertEquals(Parameter::TYPE_EMAIL, Parameter::parseType("test.123@example.com"));
|
||||
$this->assertEquals(Parameter::TYPE_STRING, Parameter::parseType("@example.com")); # invalid email
|
||||
$this->assertEquals(Parameter::TYPE_STRING, Parameter::parseType("test@")); # invalid email
|
||||
|
||||
// string, everything else
|
||||
$this->assertEquals(Parameter::TYPE_STRING, Parameter::parseType("test"));
|
||||
}
|
||||
}
|
192
test/Request.test.php
Normal file
192
test/Request.test.php
Normal file
@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
use Api\Request;
|
||||
use Configuration\Configuration;
|
||||
use Objects\User;
|
||||
|
||||
function __new_header_impl(string $line) {
|
||||
if (preg_match("/^HTTP\/([0-9.]+) (\d+) (.*)$/", $line, $m)) {
|
||||
RequestTest::$SENT_STATUS_CODE = intval($m[2]);
|
||||
return;
|
||||
}
|
||||
|
||||
$key = $line;
|
||||
$value = "";
|
||||
$index = strpos($key, ": ");
|
||||
if ($index !== false) {
|
||||
$key = substr($line, 0, $index);
|
||||
$value = substr($line, $index + 2);
|
||||
}
|
||||
|
||||
RequestTest::$SENT_HEADERS[$key] = $value;
|
||||
}
|
||||
|
||||
function __new_http_response_code_impl(int $code) {
|
||||
RequestTest::$SENT_STATUS_CODE = $code;
|
||||
}
|
||||
|
||||
function __new_die_impl($content) {
|
||||
RequestTest::$SENT_CONTENT = $content;
|
||||
}
|
||||
|
||||
class RequestTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
const FUNCTION_OVERRIDES = ["header", "http_response_code"];
|
||||
static User $USER;
|
||||
static User $USER_LOGGED_IN;
|
||||
|
||||
static ?string $SENT_CONTENT;
|
||||
static array $SENT_HEADERS;
|
||||
static ?int $SENT_STATUS_CODE;
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
|
||||
$config = new Configuration();
|
||||
RequestTest::$USER = new User($config);
|
||||
RequestTest::$USER_LOGGED_IN = new User($config);
|
||||
if (!RequestTest::$USER->getSQL() || !RequestTest::$USER->getSQL()->isConnected()) {
|
||||
throw new Exception("Could not establish database connection");
|
||||
} else {
|
||||
RequestTest::$USER->setLanguage(\Objects\Language::DEFAULT_LANGUAGE());
|
||||
}
|
||||
|
||||
if (!function_exists("runkit7_function_rename") || !function_exists("runkit7_function_remove")) {
|
||||
throw new Exception("Request Unit Test requires runkit7 extension");
|
||||
}
|
||||
|
||||
if (ini_get("runkit.internal_override") !== "1") {
|
||||
throw new Exception("Request Unit Test requires runkit7 with internal_override enabled to function properly");
|
||||
}
|
||||
|
||||
foreach (self::FUNCTION_OVERRIDES as $functionName) {
|
||||
runkit7_function_rename($functionName, "__orig_${functionName}_impl");
|
||||
runkit7_function_rename("__new_${functionName}_impl", $functionName);
|
||||
}
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
RequestTest::$USER->getSQL()->close();
|
||||
foreach (self::FUNCTION_OVERRIDES as $functionName) {
|
||||
runkit7_function_remove($functionName);
|
||||
runkit7_function_rename("__orig_${functionName}_impl", $functionName);
|
||||
}
|
||||
}
|
||||
|
||||
private function simulateRequest(Request $request, string $method, array $get = [], array $post = [], array $headers = []): bool {
|
||||
|
||||
if (!is_cli()) {
|
||||
self::throwException(new \Exception("Cannot simulate request outside cli"));
|
||||
}
|
||||
|
||||
$_SERVER = [];
|
||||
$_SERVER["REQUEST_METHOD"] = $method;
|
||||
self::$SENT_HEADERS = [];
|
||||
self::$SENT_STATUS_CODE = null;
|
||||
self::$SENT_CONTENT = null;
|
||||
|
||||
foreach ($headers as $key => $value) {
|
||||
$key = "HTTP_" . preg_replace("/\s/", "_", strtoupper($key));
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
|
||||
$_GET = $get;
|
||||
$_POST = $post;
|
||||
|
||||
return $request->execute();
|
||||
}
|
||||
|
||||
public function testAllMethods() {
|
||||
// all methods allowed
|
||||
$allMethodsAllowed = new RequestAllMethods(RequestTest::$USER, true);
|
||||
$this->assertTrue($this->simulateRequest($allMethodsAllowed, "GET"), $allMethodsAllowed->getLastError());
|
||||
$this->assertTrue($this->simulateRequest($allMethodsAllowed, "POST"), $allMethodsAllowed->getLastError());
|
||||
$this->assertFalse($this->simulateRequest($allMethodsAllowed, "PUT"), $allMethodsAllowed->getLastError());
|
||||
$this->assertFalse($this->simulateRequest($allMethodsAllowed, "DELETE"), $allMethodsAllowed->getLastError());
|
||||
$this->assertTrue($this->simulateRequest($allMethodsAllowed, "OPTIONS"), $allMethodsAllowed->getLastError());
|
||||
$this->assertEquals(204, self::$SENT_STATUS_CODE);
|
||||
$this->assertEquals(["Allow" => "OPTIONS, GET, POST"], self::$SENT_HEADERS);
|
||||
}
|
||||
|
||||
public function testOnlyPost() {
|
||||
// only post allowed
|
||||
$onlyPostAllowed = new RequestOnlyPost(RequestTest::$USER, true);
|
||||
$this->assertFalse($this->simulateRequest($onlyPostAllowed, "GET"));
|
||||
$this->assertEquals("This method is not allowed", $onlyPostAllowed->getLastError(), $onlyPostAllowed->getLastError());
|
||||
$this->assertEquals(405, self::$SENT_STATUS_CODE);
|
||||
$this->assertTrue($this->simulateRequest($onlyPostAllowed, "POST"), $onlyPostAllowed->getLastError());
|
||||
$this->assertTrue($this->simulateRequest($onlyPostAllowed, "OPTIONS"), $onlyPostAllowed->getLastError());
|
||||
$this->assertEquals(204, self::$SENT_STATUS_CODE);
|
||||
$this->assertEquals(["Allow" => "OPTIONS, POST"], self::$SENT_HEADERS);
|
||||
}
|
||||
|
||||
public function testPrivate() {
|
||||
// private method
|
||||
$privateExternal = new RequestPrivate(RequestTest::$USER, true);
|
||||
$this->assertFalse($this->simulateRequest($privateExternal, "GET"));
|
||||
$this->assertEquals("This function is private.", $privateExternal->getLastError());
|
||||
$this->assertEquals(403, self::$SENT_STATUS_CODE);
|
||||
|
||||
$privateInternal = new RequestPrivate(RequestTest::$USER, false);
|
||||
$this->assertTrue($privateInternal->execute());
|
||||
}
|
||||
|
||||
public function testDisabled() {
|
||||
// disabled method
|
||||
$disabledMethod = new RequestDisabled(RequestTest::$USER, true);
|
||||
$this->assertFalse($this->simulateRequest($disabledMethod, "GET"));
|
||||
$this->assertEquals("This function is currently disabled.", $disabledMethod->getLastError(), $disabledMethod->getLastError());
|
||||
$this->assertEquals(503, self::$SENT_STATUS_CODE);
|
||||
}
|
||||
|
||||
public function testLoginRequired() {
|
||||
$loginRequired = new RequestLoginRequired(RequestTest::$USER, true);
|
||||
$this->assertFalse($this->simulateRequest($loginRequired, "GET"));
|
||||
$this->assertEquals("You are not logged in.", $loginRequired->getLastError(), $loginRequired->getLastError());
|
||||
$this->assertEquals(401, self::$SENT_STATUS_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TestRequest extends Request {
|
||||
public function __construct(User $user, bool $externalCall = false, $params = []) {
|
||||
parent::__construct($user, $externalCall, $params);
|
||||
}
|
||||
|
||||
protected function _die(string $data = ""): bool {
|
||||
__new_die_impl($data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestAllMethods extends TestRequest {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, []);
|
||||
}
|
||||
}
|
||||
|
||||
class RequestOnlyPost extends TestRequest {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, []);
|
||||
$this->forbidMethod("GET");
|
||||
}
|
||||
}
|
||||
|
||||
class RequestPrivate extends TestRequest {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, []);
|
||||
$this->isPublic = false;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestDisabled extends TestRequest {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, []);
|
||||
$this->isDisabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
class RequestLoginRequired extends TestRequest {
|
||||
public function __construct(User $user, bool $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, []);
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user