mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-29 04:33:25 +00:00
[Web] Add Mailcow\ class structure and imapsync module classes
This commit is contained in:
26 files changed
+2821
No files matched your search
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Core;
|
||||
|
||||
/**
|
||||
* The caller may not do this (or may not see the object). Always shown as `access_denied`.
|
||||
*/
|
||||
class AccessDenied extends UserError
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('access_denied');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Core;
|
||||
|
||||
/**
|
||||
* Collects the messages of one API call and writes them to $_SESSION['return'] in the mailcow format
|
||||
* (`type`, `log`, `msg`), which json_api.php and the UI read. This is the only class that touches the session.
|
||||
*
|
||||
* Values of the configured secret keys are replaced by '*' inside every array of the `log` part,
|
||||
* so passwords or client secrets never reach the mailcow log or an API response.
|
||||
*/
|
||||
class ApiResponse
|
||||
{
|
||||
private array $messages = [];
|
||||
|
||||
public function __construct(private array $secretKeys = [])
|
||||
{
|
||||
}
|
||||
|
||||
/** Returns true, so callers can `return $response->success(...)` */
|
||||
public function success(string|array $msg, array $log): bool
|
||||
{
|
||||
$this->add('success', $msg, $log);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns false, so callers can `return $response->danger(...)` */
|
||||
public function danger(string|array $msg, array $log): bool
|
||||
{
|
||||
$this->add('danger', $msg, $log);
|
||||
return false;
|
||||
}
|
||||
|
||||
public function error(UserError $error, array $log): bool
|
||||
{
|
||||
return $this->danger($error->msg(), $log);
|
||||
}
|
||||
|
||||
/** Messages collected since the last flush() */
|
||||
public function messages(): array
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/** Appends the collected messages to $_SESSION['return'] and starts over */
|
||||
public function flush(): void
|
||||
{
|
||||
foreach ($this->messages as $message) {
|
||||
$_SESSION['return'][] = $message;
|
||||
}
|
||||
$this->messages = [];
|
||||
}
|
||||
|
||||
private function add(string $type, string|array $msg, array $log): void
|
||||
{
|
||||
foreach ($log as $i => $part) {
|
||||
if (!is_array($part)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($this->secretKeys as $key) {
|
||||
if (isset($part[$key]) && $part[$key] !== '') {
|
||||
$log[$i][$key] = '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->messages[] = ['type' => $type, 'log' => $log, 'msg' => $msg];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Core;
|
||||
|
||||
/**
|
||||
* Who is calling: role, username, ACL flags and the access-all exception of the session.
|
||||
*
|
||||
* Services get a Context instead of reading $_SESSION. Build it once per request in the entry point:
|
||||
* `Context::fromSession($_SESSION)` for web requests, `Context::system()` for cron/CLI code paths
|
||||
* that do no permission checks.
|
||||
*/
|
||||
class Context
|
||||
{
|
||||
/**
|
||||
* @param array $acl ACL flags of the session (user_acl / da_acl, always set for admins)
|
||||
* @param bool $accessAll the session may ignore object ownership (`access_all_exception`)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $role,
|
||||
public readonly ?string $username,
|
||||
private readonly array $acl = [],
|
||||
public readonly bool $accessAll = false
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromSession(array $session): self
|
||||
{
|
||||
$username = $session['mailcow_cc_username'] ?? null;
|
||||
return new self(
|
||||
(string)($session['mailcow_cc_role'] ?? ''),
|
||||
is_scalar($username) ? (string)$username : null,
|
||||
is_array($session['acl'] ?? null) ? $session['acl'] : [],
|
||||
($session['access_all_exception'] ?? null) == '1'
|
||||
);
|
||||
}
|
||||
|
||||
/** Caller for cron and CLI code: not a logged-in account, so every permission check fails */
|
||||
public static function system(): self
|
||||
{
|
||||
return new self('system', null);
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role === 'admin';
|
||||
}
|
||||
|
||||
public function isDomainAdmin(): bool
|
||||
{
|
||||
return $this->role === 'domainadmin';
|
||||
}
|
||||
|
||||
public function isUser(): bool
|
||||
{
|
||||
return $this->role === 'user';
|
||||
}
|
||||
|
||||
public function hasAcl(string $name): bool
|
||||
{
|
||||
return isset($this->acl[$name]) && $this->acl[$name] == '1';
|
||||
}
|
||||
|
||||
/** Bridge to the legacy hasMailboxObjectAccess() until functions.inc.php is converted */
|
||||
public function canAccessMailbox(string $mailbox): bool
|
||||
{
|
||||
return (bool)hasMailboxObjectAccess($this->username, $this->role, $mailbox);
|
||||
}
|
||||
|
||||
/** Bridge to the legacy hasDomainAccess() until functions.inc.php is converted */
|
||||
public function canAccessDomain(string $domain): bool
|
||||
{
|
||||
return (bool)hasDomainAccess($this->username, $this->role, $domain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Core;
|
||||
|
||||
/**
|
||||
* Helpers for loosely typed request values. Form values arrive as strings, JSON bodies as any type,
|
||||
* and 011-api.js turns JSON-looking strings into arrays, so services must not trust the PHP type.
|
||||
*/
|
||||
class Input
|
||||
{
|
||||
/**
|
||||
* Scalar value as trimmed string; '' for null; null for arrays/objects (= invalid).
|
||||
*/
|
||||
public static function text(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
if (!is_scalar($value)) {
|
||||
return null;
|
||||
}
|
||||
return trim((string)$value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Integer within [$min, $max] (filter_var rules: "12" and 12 pass, "12abc" and "1.5" do not), else null.
|
||||
*/
|
||||
public static function intInRange(mixed $value, int $min, int $max): ?int
|
||||
{
|
||||
$int = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => $max]]);
|
||||
return ($int === false) ? null : $int;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Core;
|
||||
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Base class of the repositories: the PDO plus the helpers every repository needs.
|
||||
* Named parameters are passed without the colon (['id' => 5]).
|
||||
*/
|
||||
class Repository
|
||||
{
|
||||
public function __construct(protected PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
/** Prepares and executes one statement */
|
||||
protected function query(string $sql, array $params = []): PDOStatement
|
||||
{
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
/** Inserts one row (column => value) and returns the new id */
|
||||
protected function insertRow(string $table, array $values): int
|
||||
{
|
||||
$columns = array_keys($values);
|
||||
$this->query("INSERT INTO `$table` (`" . implode('`, `', $columns) . "`) VALUES (:" . implode(', :', $columns) . ")", $values);
|
||||
return (int)$this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/** Updates the columns (column => value) of the row with the given `id` */
|
||||
protected function updateRow(string $table, int $id, array $values): void
|
||||
{
|
||||
$set = implode(', ', array_map(fn(string $column): string => "`$column` = :$column", array_keys($values)));
|
||||
$this->query("UPDATE `$table` SET $set WHERE `id` = :id", $values + ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs $work in a transaction and returns its result. Inside a transaction of the caller it just runs,
|
||||
* so the caller keeps control over commit and rollback.
|
||||
*/
|
||||
protected function transaction(callable $work): mixed
|
||||
{
|
||||
if ($this->pdo->inTransaction()) {
|
||||
return $work();
|
||||
}
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
$result = $work();
|
||||
$this->pdo->commit();
|
||||
return $result;
|
||||
} catch (Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Core;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* An expected error that is shown to the user (validation failed, not found, not allowed).
|
||||
*
|
||||
* Carries a lang key of the `danger` section plus optional message arguments, e.g.
|
||||
* `throw new UserError('object_exists', [$name]);`
|
||||
* Controllers turn it into an ApiResponse message; anything else that is thrown is a real failure.
|
||||
*/
|
||||
class UserError extends RuntimeException
|
||||
{
|
||||
public function __construct(public readonly string $key, public readonly array $args = [])
|
||||
{
|
||||
parent::__construct($key);
|
||||
}
|
||||
|
||||
/** The `msg` value of a mailcow message: the lang key alone, or [key, arg1, ...] when there are arguments */
|
||||
public function msg(): string|array
|
||||
{
|
||||
return empty($this->args) ? $this->key : array_merge([$this->key], array_values($this->args));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user