mirror of
https://github.com/claradex/nativegallery.git
synced 2025-06-05 05:47:00 +03:00
update services
This commit is contained in:
parent
950d856489
commit
39193c88c0
7 changed files with 838 additions and 54 deletions
|
@ -1,49 +1,241 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use \PDO;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use RuntimeException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class DB {
|
||||
private static $pdoInstance = null;
|
||||
private static $cache = [];
|
||||
class DB
|
||||
{
|
||||
const DRIVERS = ['mysql', 'pgsql', 'sqlite'];
|
||||
|
||||
public static function connect() {
|
||||
if (self::$pdoInstance === null) {
|
||||
$dsn = 'mysql:host='.NGALLERY['root']['db']['host'].';dbname='.NGALLERY['root']['db']['name'].';charset=utf8mb4';
|
||||
$username = NGALLERY['root']['db']['login'];
|
||||
$password = NGALLERY['root']['db']['password'];
|
||||
private static $config = [];
|
||||
private static $queryLog = [];
|
||||
private static $logger;
|
||||
private static $connectionPool = [];
|
||||
private static $poolSize = 5;
|
||||
|
||||
try {
|
||||
self::$pdoInstance = new PDO($dsn, $username, $password);
|
||||
self::$pdoInstance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch (PDOException $ex) {
|
||||
die("Connection failed: " . $ex->getMessage());
|
||||
}
|
||||
/* Инициализация и конфигурация */
|
||||
public static function init(array $config): void
|
||||
{
|
||||
self::validateConfig($config);
|
||||
self::$config = array_merge([
|
||||
'driver' => 'mysql',
|
||||
'host' => 'localhost',
|
||||
'database' => '',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'prefix' => '',
|
||||
'log_file' => null,
|
||||
'cache' => null,
|
||||
'benchmark' => false,
|
||||
'pool_size' => 5
|
||||
], $config);
|
||||
|
||||
self::$poolSize = self::$config['pool_size'];
|
||||
|
||||
if (self::$config['log_file']) {
|
||||
self::$logger = new class(self::$config['log_file']) {
|
||||
private $file;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->file = fopen($path, 'a');
|
||||
}
|
||||
|
||||
public function log(string $message): void
|
||||
{
|
||||
fwrite($this->file, date('[Y-m-d H:i:s] ') . $message . PHP_EOL);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
fclose($this->file);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return self::$pdoInstance;
|
||||
}
|
||||
|
||||
public static function query($query, $params = array(), $useCache = false) {
|
||||
if ($useCache && isset(self::$cache[$query])) {
|
||||
return self::$cache[$query];
|
||||
public static function lastInsertId() {
|
||||
return self::getConnection()->lastInsertId();
|
||||
}
|
||||
|
||||
private static function validateConfig(array $config): void
|
||||
{
|
||||
if (!in_array($config['driver'], self::DRIVERS)) {
|
||||
throw new InvalidArgumentException('Invalid database driver');
|
||||
}
|
||||
}
|
||||
|
||||
/* Пул соединений */
|
||||
private static function getConnection(): PDO
|
||||
{
|
||||
if (!empty(self::$connectionPool)) {
|
||||
return array_shift(self::$connectionPool);
|
||||
}
|
||||
|
||||
$statement = self::connect()->prepare($query);
|
||||
$dsn = match (self::$config['driver']) {
|
||||
'mysql' => "mysql:host=" . self::$config['host'] . ";dbname=" . self::$config['database'] . ";charset=utf8mb4",
|
||||
'pgsql' => "pgsql:host=" . self::$config['host'] . ";dbname=" . self::$config['database'] . "",
|
||||
'sqlite' => "sqlite:" . self::$config['database'] . ""
|
||||
};
|
||||
|
||||
try {
|
||||
$statement->execute($params);
|
||||
$pdo = new PDO($dsn, self::$config['username'], self::$config['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_PERSISTENT => true
|
||||
]);
|
||||
|
||||
if (explode(' ', $query)[0] === 'SELECT' || explode(' ', $query)[0] === 'SHOW' || explode(' ', $query)[0] === 'DESCRIBE') {
|
||||
$data = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($useCache) {
|
||||
self::$cache[$query] = $data;
|
||||
}
|
||||
return $data;
|
||||
if (self::$config['driver'] === 'mysql') {
|
||||
$pdo->exec("SET SQL_MODE='STRICT_ALL_TABLES'");
|
||||
}
|
||||
} catch (PDOException $ex) {
|
||||
die("Query failed: " . $ex->getMessage());
|
||||
|
||||
return $pdo;
|
||||
} catch (PDOException $e) {
|
||||
self::logError($e);
|
||||
throw new RuntimeException("Connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static function releaseConnection(PDO $connection): void
|
||||
{
|
||||
if (count(self::$connectionPool) < self::$poolSize) {
|
||||
self::$connectionPool[] = $connection;
|
||||
}
|
||||
}
|
||||
|
||||
/* Основные методы */
|
||||
public static function query(string $sql, array $params = []): array
|
||||
{
|
||||
$start = microtime(true);
|
||||
$conn = self::getConnection();
|
||||
|
||||
try {
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (self::$config['benchmark']) {
|
||||
self::$queryLog[] = [
|
||||
'query' => $sql,
|
||||
'params' => $params,
|
||||
'time' => microtime(true) - $start
|
||||
];
|
||||
}
|
||||
|
||||
self::releaseConnection($conn);
|
||||
return $result;
|
||||
} catch (PDOException $e) {
|
||||
self::logError($e);
|
||||
throw new RuntimeException("Query failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/* Транзакции */
|
||||
public static function transaction(callable $callback)
|
||||
{
|
||||
$conn = self::getConnection();
|
||||
try {
|
||||
$conn->beginTransaction();
|
||||
$result = $callback();
|
||||
$conn->commit();
|
||||
self::releaseConnection($conn);
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
$conn->rollBack();
|
||||
self::releaseConnection($conn);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Построитель запросов */
|
||||
public static function table(string $table): QueryBuilder
|
||||
{
|
||||
return new QueryBuilder(
|
||||
self::$config['prefix'] . $table,
|
||||
self::$config['driver']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static function logError(\Throwable $e): void
|
||||
{
|
||||
if (self::$logger) {
|
||||
self::$logger->log("ERROR: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
class QueryBuilder
|
||||
{
|
||||
private $wheres = [];
|
||||
private $joins = [];
|
||||
private $columns = ['*'];
|
||||
private $bindings = [];
|
||||
|
||||
public function __construct(
|
||||
private string $table,
|
||||
private string $driver
|
||||
) {
|
||||
// Добавляем проверку
|
||||
if (empty($this->driver)) {
|
||||
throw new RuntimeException('Database driver not configured');
|
||||
}
|
||||
}
|
||||
|
||||
public function select(array $columns): self
|
||||
{
|
||||
$this->columns = $columns;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function join(string $table, string $first, string $operator, string $second, string $type = 'INNER'): self
|
||||
{
|
||||
$this->joins[] = "$type JOIN $table ON $first $operator $second";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where(string $column, string $operator, $value): self
|
||||
{
|
||||
$param = 'where_' . count($this->bindings);
|
||||
$this->wheres[] = "$column $operator :$param";
|
||||
$this->bindings[$param] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
$sql = "SELECT " . implode(', ', $this->columns) . " FROM $this->table";
|
||||
|
||||
if (!empty($this->joins)) {
|
||||
$sql .= " " . implode(' ', $this->joins);
|
||||
}
|
||||
|
||||
if (!empty($this->wheres)) {
|
||||
$sql .= " WHERE " . implode(' AND ', $this->wheres);
|
||||
}
|
||||
|
||||
return DB::query($sql, $this->bindings);
|
||||
}
|
||||
|
||||
public function create(array $columns): void
|
||||
{
|
||||
$definitions = [];
|
||||
foreach ($columns as $name => $type) {
|
||||
$definitions[] = "$name $type";
|
||||
}
|
||||
|
||||
$sql = "CREATE TABLE $this->table (" . implode(', ', $definitions) . ")";
|
||||
DB::query($sql);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
77
app/Services/Emoji.php
Normal file
77
app/Services/Emoji.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
|
||||
class Emoji
|
||||
{
|
||||
|
||||
public static function parseSmileys($text) {
|
||||
return preg_replace_callback(
|
||||
'/\[(\d+\/[\w-]+)\]/',
|
||||
function($matches) {
|
||||
$parts = explode('/', $matches[1]);
|
||||
$dir = $parts[0];
|
||||
$name = $parts[1];
|
||||
|
||||
$files = glob($_SERVER['DOCUMENT_ROOT']."/static/img/smileys/$dir/$name.*");
|
||||
|
||||
if ($files) {
|
||||
$ext = pathinfo($files[0], PATHINFO_EXTENSION);
|
||||
return "<img src='/static/img/smileys/$dir/$name.$ext'
|
||||
class='emoji'
|
||||
data-code='".$matches[0]."'>";
|
||||
}
|
||||
|
||||
return htmlspecialchars($matches[0]);
|
||||
},
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
public static function getAllSmileys() {
|
||||
// Пример реализации для файловой системы
|
||||
$smileys = [];
|
||||
$directories = glob($_SERVER['DOCUMENT_ROOT'].'/static/img/smileys/*', GLOB_ONLYDIR);
|
||||
|
||||
foreach ($directories as $dir) {
|
||||
$dirName = basename($dir);
|
||||
$files = glob($dir.'/*.{gif,png,jpg,webp}', GLOB_BRACE);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
$code = "[{$dirName}/{$filename}]";
|
||||
|
||||
$smileys[] = [
|
||||
'code' => $code,
|
||||
'url' => "/static/img/smileys/{$dirName}/{$filename}.{$ext}"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $smileys;
|
||||
}
|
||||
|
||||
|
||||
public static function expandSmileys($content)
|
||||
{
|
||||
$pattern = '/\[([0-9]+\/[a-zA-Z0-9_-]+)\]/';
|
||||
|
||||
return preg_replace_callback($pattern, function ($matches) {
|
||||
$path = explode('/', $matches[1]);
|
||||
$dir = $path[0];
|
||||
$name = $path[1];
|
||||
|
||||
$files = glob($_SERVER['DOCUMENT_ROOT'] . "/static/img/smileys/{$dir}/{$name}.*");
|
||||
|
||||
if (count($files) > 0) {
|
||||
$file = basename($files[0]);
|
||||
return "<img src=\"/static/img/smileys/{$dir}/{$file}\" " .
|
||||
"class=\"editor-emoji\" data-code=\"{$matches[0]}\">";
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
}, $content);
|
||||
}
|
||||
}
|
272
app/Services/Image.php
Normal file
272
app/Services/Image.php
Normal file
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
use GdImage;
|
||||
|
||||
class Image
|
||||
{
|
||||
private const CACHE_DIR = __DIR__ . '/../../cdn/';
|
||||
private const LOCK_DIR = __DIR__ . '/../../storage/locks/';
|
||||
private const QUEUE_FILE = __DIR__ . '/../../storage/queue/image_processing.queue';
|
||||
private const MAX_FILE_SIZE = 5242880; // 5MB
|
||||
|
||||
public static function generateBlurredPlaceholder(string $imageUrl, int $quality = 30): string
|
||||
{
|
||||
try {
|
||||
self::checkDirectories();
|
||||
$cacheFile = self::CACHE_DIR . md5($imageUrl) . '.jpg';
|
||||
|
||||
if (self::isValidCache($cacheFile)) {
|
||||
return self::getCachedImage($cacheFile);
|
||||
}
|
||||
|
||||
if (!self::isProcessing($imageUrl)) {
|
||||
self::addToQueue($imageUrl, $quality);
|
||||
error_log("Added to queue: " . $imageUrl);
|
||||
}
|
||||
|
||||
return self::getTransparentPixel();
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in generateBlurredPlaceholder: " . $e->getMessage());
|
||||
return self::getTransparentPixel();
|
||||
}
|
||||
}
|
||||
|
||||
private static function checkDirectories(): void
|
||||
{
|
||||
try {
|
||||
$dirs = [
|
||||
self::CACHE_DIR,
|
||||
self::LOCK_DIR,
|
||||
dirname(self::QUEUE_FILE)
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
error_log("Created directory: $dir");
|
||||
}
|
||||
|
||||
// Проверяем права записи
|
||||
if (!is_writable($dir)) {
|
||||
throw new Exception("Directory not writable: $dir");
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем файл очереди
|
||||
if (!file_exists(self::QUEUE_FILE)) {
|
||||
touch(self::QUEUE_FILE);
|
||||
chmod(self::QUEUE_FILE, 0666);
|
||||
error_log("Created queue file: " . self::QUEUE_FILE);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Directory error: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function processQueue(): void
|
||||
{
|
||||
self::checkDirectories();
|
||||
if (!file_exists(self::QUEUE_FILE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$queue = file(self::QUEUE_FILE, FILE_IGNORE_NEW_LINES);
|
||||
foreach ($queue as $line) {
|
||||
try {
|
||||
[$hash, $data] = explode('|', $line, 2);
|
||||
$task = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
self::processImageTask($task['url'], $task['quality']);
|
||||
self::removeFromQueue($hash);
|
||||
} catch (Exception $e) {
|
||||
error_log('Queue processing error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function processImageTask(string $imageUrl, int $quality): void
|
||||
{
|
||||
$cacheFile = self::CACHE_DIR . md5($imageUrl) . '.jpg';
|
||||
$lockFile = self::LOCK_DIR . md5($imageUrl) . '.lock';
|
||||
|
||||
$lockHandle = fopen($lockFile, 'w');
|
||||
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$imageData = self::fetchImage($imageUrl);
|
||||
$processedImage = self::createBlurredImage($imageData, $quality);
|
||||
file_put_contents($cacheFile, $processedImage);
|
||||
} finally {
|
||||
flock($lockHandle, LOCK_UN);
|
||||
fclose($lockHandle);
|
||||
@unlink($lockFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static function createBlurredImage(string $imageData, int $quality): string
|
||||
{
|
||||
$tempFile = tmpfile();
|
||||
try {
|
||||
fwrite($tempFile, $imageData);
|
||||
$tempPath = stream_get_meta_data($tempFile)['uri'];
|
||||
|
||||
$img = self::createImageResource($tempPath);
|
||||
$scaled = self::scaleAndBlurImage($img);
|
||||
|
||||
ob_start();
|
||||
imagejpeg($scaled, null, $quality);
|
||||
$contents = ob_get_clean();
|
||||
|
||||
if (empty($contents)) {
|
||||
throw new Exception('JPEG generation failed');
|
||||
}
|
||||
|
||||
return $contents;
|
||||
} finally {
|
||||
if (isset($img) && $img instanceof GdImage) {
|
||||
imagedestroy($img);
|
||||
}
|
||||
if (isset($scaled) && $scaled instanceof GdImage) {
|
||||
imagedestroy($scaled);
|
||||
}
|
||||
fclose($tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static function createImageResource(string $path): GdImage
|
||||
{
|
||||
$mime = (new \finfo(FILEINFO_MIME_TYPE))->file($path);
|
||||
|
||||
return match ($mime) {
|
||||
'image/jpeg' => imagecreatefromjpeg($path),
|
||||
'image/png' => self::createTrueColorPng($path),
|
||||
'image/gif' => imagecreatefromgif($path),
|
||||
default => throw new Exception("Unsupported MIME type: $mime")
|
||||
};
|
||||
}
|
||||
|
||||
private static function createTrueColorPng(string $path): GdImage
|
||||
{
|
||||
$img = imagecreatefrompng($path);
|
||||
if (!imageistruecolor($img)) {
|
||||
imagepalettetotruecolor($img);
|
||||
}
|
||||
return $img;
|
||||
}
|
||||
|
||||
private static function scaleAndBlurImage(GdImage $img): GdImage
|
||||
{
|
||||
$scaled = imagescale($img, 10, 10, IMG_BICUBIC);
|
||||
if (!$scaled || !imagefilter($scaled, IMG_FILTER_GAUSSIAN_BLUR)) {
|
||||
throw new Exception('Image processing failed');
|
||||
}
|
||||
return $scaled;
|
||||
}
|
||||
|
||||
private static function fetchImage(string $imageUrl): string
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'header' => "Range: bytes=0-" . self::MAX_FILE_SIZE
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false
|
||||
]
|
||||
]);
|
||||
|
||||
$imageData = file_get_contents($imageUrl, false, $context);
|
||||
if ($imageData === false) {
|
||||
throw new Exception("Failed to download image");
|
||||
}
|
||||
return $imageData;
|
||||
}
|
||||
|
||||
private static function isValidCache(string $cacheFile): bool
|
||||
{
|
||||
return file_exists($cacheFile) && filesize($cacheFile) > 0;
|
||||
}
|
||||
|
||||
|
||||
private static function isProcessing(string $imageUrl): bool
|
||||
{
|
||||
return
|
||||
file_exists(self::LOCK_DIR . md5($imageUrl) . '.lock') ||
|
||||
self::isInQueue(md5($imageUrl));
|
||||
}
|
||||
|
||||
|
||||
private static function isInQueue(string $fileHash): bool
|
||||
{
|
||||
if (!file_exists(self::QUEUE_FILE)) return false;
|
||||
|
||||
$handle = fopen(self::QUEUE_FILE, 'r');
|
||||
if (!$handle) return false;
|
||||
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
if (strpos($line, $fileHash) === 0) {
|
||||
fclose($handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static function addToQueue(string $imageUrl, int $quality): void
|
||||
{
|
||||
$data = [
|
||||
'url' => $imageUrl,
|
||||
'quality' => $quality,
|
||||
'created_at' => time()
|
||||
];
|
||||
|
||||
$queueEntry = md5($imageUrl) . '|' . json_encode($data);
|
||||
|
||||
if (file_put_contents(
|
||||
self::QUEUE_FILE,
|
||||
$queueEntry . PHP_EOL,
|
||||
FILE_APPEND | LOCK_EX
|
||||
) === false) {
|
||||
throw new Exception("Failed to write to queue file");
|
||||
}
|
||||
}
|
||||
|
||||
private static function removeFromQueue(string $fileHash): void
|
||||
{
|
||||
$queue = file(self::QUEUE_FILE, FILE_IGNORE_NEW_LINES);
|
||||
$newQueue = [];
|
||||
|
||||
foreach ($queue as $line) {
|
||||
if (!str_starts_with($line, $fileHash)) {
|
||||
$newQueue[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents(self::QUEUE_FILE, implode(PHP_EOL, $newQueue));
|
||||
}
|
||||
|
||||
|
||||
private static function getCachedImage(string $cacheFile): string
|
||||
{
|
||||
$content = file_get_contents($cacheFile);
|
||||
if ($content === false) {
|
||||
throw new Exception("Failed to read cached image");
|
||||
}
|
||||
return 'data:image/jpeg;base64,' . base64_encode($content);
|
||||
}
|
||||
|
||||
private static function getTransparentPixel(): string
|
||||
{
|
||||
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBgYAAAAAQAAHpQoNMAAAAASUVORK5CYII=';
|
||||
}
|
||||
}
|
|
@ -7,16 +7,26 @@ use \App\Core\Page;
|
|||
class Router
|
||||
{
|
||||
|
||||
protected static $routes = [];
|
||||
|
||||
|
||||
private static function addRoute($method, $route)
|
||||
{
|
||||
self::$routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $route
|
||||
];
|
||||
}
|
||||
public static function get($route, $path_to_include)
|
||||
{
|
||||
self::addRoute('GET', $route);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
}
|
||||
public static function post($route, $path_to_include)
|
||||
{
|
||||
self::addRoute('POST', $route);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
|
@ -38,9 +48,23 @@ class Router
|
|||
if ($_SERVER['REQUEST_METHOD'] == 'DELETE') {
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
}
|
||||
public static function getRouteSegments()
|
||||
{
|
||||
$segments = [];
|
||||
foreach (self::$routes as $route) {
|
||||
$parts = explode('/', $route['path']);
|
||||
foreach ($parts as $part) {
|
||||
if (!empty($part) && !str_starts_with($part, '$')) {
|
||||
$segments[] = $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_unique($segments);
|
||||
}
|
||||
public static function any($route, $path_to_include)
|
||||
{
|
||||
self::addRoute('ANY', $route);
|
||||
self::route($route, $path_to_include);
|
||||
}
|
||||
public static function route($route, $path_to_include)
|
||||
|
|
143
app/Services/ThemeManager.php
Normal file
143
app/Services/ThemeManager.php
Normal file
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use RuntimeException, InvalidArgumentException;
|
||||
|
||||
|
||||
class ThemeManager
|
||||
{
|
||||
private $themepacksPath;
|
||||
private $loadedThemes = [];
|
||||
private $defaultThemeId = 'standard';
|
||||
|
||||
public function __construct(?string $themepacksPath = null)
|
||||
{
|
||||
$this->themepacksPath = $themepacksPath ?? $_SERVER['DOCUMENT_ROOT'] . '/static/themepacks';
|
||||
}
|
||||
|
||||
public function loadThemes(): void
|
||||
{
|
||||
if (!is_dir($this->themepacksPath)) return;
|
||||
|
||||
$folders = array_diff(scandir($this->themepacksPath), ['..', '.']);
|
||||
|
||||
foreach ($folders as $folder) {
|
||||
$themePath = $this->themepacksPath . '/' . $folder;
|
||||
$configFile = $themePath . '/theme.yaml';
|
||||
$cssFile = $themePath . '/root.css';
|
||||
if (is_dir($themePath)) {
|
||||
try {
|
||||
if (!file_exists($configFile)) {
|
||||
throw new RuntimeException("Missing theme.yaml in {$folder}");
|
||||
}
|
||||
if (!file_exists($cssFile)) {
|
||||
throw new RuntimeException("Missing root.css in {$folder}");
|
||||
}
|
||||
|
||||
$config = Yaml::parseFile($configFile);
|
||||
$this->validateThemeConfig($config, $folder);
|
||||
|
||||
$this->loadedThemes[$folder] = [
|
||||
'id' => $folder,
|
||||
'config' => $config,
|
||||
'stylesheet' => '/static/themepacks/' . $folder . '/root.css'
|
||||
];
|
||||
} catch (ParseException | RuntimeException $e) {
|
||||
error_log("Theme load error ({$folder}): " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getAllThemes(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->loadedThemes as $theme) {
|
||||
$result[] = [
|
||||
'id' => $theme['id'],
|
||||
'name' => $theme['config']['name'],
|
||||
'version' => $theme['config']['version'],
|
||||
'author' => $theme['config']['author'],
|
||||
'supported_nativegallery' => $theme['config']['supported_nativegallery'],
|
||||
'stylesheet' => $theme['stylesheet']
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getThemeStylesheet(): ?string
|
||||
{
|
||||
$this->startSession();
|
||||
|
||||
$themeId = $_SESSION['selected_theme'] ?? $this->defaultThemeId;
|
||||
|
||||
if ($themeId === $this->defaultThemeId || !isset($this->loadedThemes[$themeId])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->loadedThemes[$themeId]['stylesheet'];
|
||||
}
|
||||
|
||||
public function saveThemeToProfile(string $themeId): bool
|
||||
{
|
||||
if ($themeId !== $this->defaultThemeId && !isset($this->loadedThemes[$themeId])) {
|
||||
throw new InvalidArgumentException("Theme {$themeId} not found");
|
||||
}
|
||||
|
||||
$this->startSession();
|
||||
$_SESSION['selected_theme'] = $themeId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAvailableThemes(): array
|
||||
{
|
||||
return $this->loadedThemes;
|
||||
}
|
||||
|
||||
private function validateThemeConfig(array $config, string $folder): void
|
||||
{
|
||||
$requiredFields = [
|
||||
'name',
|
||||
'version',
|
||||
'supported_nativegallery',
|
||||
'author'
|
||||
];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!isset($config[$field])) {
|
||||
throw new RuntimeException(
|
||||
"Missing required field '{$field}' in theme: {$folder}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCurrentThemeId(): string {
|
||||
$this->startSession();
|
||||
return $_SESSION['selected_theme'] ?? $this->defaultThemeId;
|
||||
}
|
||||
|
||||
public function getCurrentThemeName(): string {
|
||||
$themeId = $this->getCurrentThemeId();
|
||||
return $themeId === $this->defaultThemeId
|
||||
? 'Стандартная'
|
||||
: $this->loadedThemes[$themeId]['config']['name'] ?? 'Неизвестная тема';
|
||||
}
|
||||
|
||||
public function getThemeNameById(string $themeId): ?string
|
||||
{
|
||||
return $this->loadedThemes[$themeId]['config']['name'] ?? null;
|
||||
}
|
||||
|
||||
private function startSession(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,17 +10,18 @@ class Upload
|
|||
public $src;
|
||||
public $size;
|
||||
public $name;
|
||||
public $previewUrl;
|
||||
|
||||
private static function human_filesize($bytes, $dec = 2): string
|
||||
{
|
||||
{
|
||||
|
||||
$size = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
if ($factor == 0)
|
||||
$dec = 0;
|
||||
$size = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
if ($factor == 0)
|
||||
$dec = 0;
|
||||
|
||||
|
||||
return sprintf("%.{$dec}f %s", $bytes / (1024 ** $factor), $size[$factor]);
|
||||
return sprintf("%.{$dec}f %s", $bytes / (1024 ** $factor), $size[$factor]);
|
||||
}
|
||||
public function __construct($file, $location)
|
||||
{
|
||||
|
@ -39,14 +40,13 @@ class Upload
|
|||
$filecdn = bin2hex(openssl_random_pseudo_bytes(64, $cstrong)) . '.' . $fileext;
|
||||
$folder = $location . $filecdn;
|
||||
|
||||
if (strtolower (NGALLERY['root']['storage']['type']) == "s3")
|
||||
{
|
||||
if (strtolower(NGALLERY['root']['storage']['type']) == "s3") {
|
||||
|
||||
if (NGALLERY['root']['video']['upload']['cloudflare-bypass'] === true) {
|
||||
if ($location === 'cdn/video') {
|
||||
if (filesize($_SERVER['DOCUMENT_ROOT'].'/'.$location.$filecdn) >= 94371840) {
|
||||
if (filesize($_SERVER['DOCUMENT_ROOT'] . '/' . $location . $filecdn) >= 94371840) {
|
||||
mkdir("{$_SERVER['DOCUMENT_ROOT']}/uploads/{$location}", 0777, true);
|
||||
move_uploaded_file ($tmpname, "{$_SERVER['DOCUMENT_ROOT']}/uploads/{$folder}");
|
||||
move_uploaded_file($tmpname, "{$_SERVER['DOCUMENT_ROOT']}/uploads/{$folder}");
|
||||
$this->type = $type;
|
||||
$this->src = "/uploads/{$folder}";
|
||||
$this->size = self::human_filesize(filesize($tmpname));
|
||||
|
@ -67,33 +67,84 @@ class Upload
|
|||
|
||||
$s3->putObject([
|
||||
'Bucket' => NGALLERY['root']['storage']['s3']['credentials']['bucket'],
|
||||
'Key' => $location.$filecdn,
|
||||
'Key' => $location . $filecdn,
|
||||
'SourceFile' => $tmpname
|
||||
]);
|
||||
$this->type = $type;
|
||||
$this->src = NGALLERY['root']['storage']['s3']['domains']['public'] . '/' . $location . $filecdn;
|
||||
$this->size = self::human_filesize(filesize($tmpname));
|
||||
$this->name = $name;
|
||||
}
|
||||
else
|
||||
{
|
||||
$location = "your-location";
|
||||
$folder = "{$location}/" . basename($tmpname);
|
||||
|
||||
$uploadDir = "{$_SERVER['DOCUMENT_ROOT']}/uploads/{$location}";
|
||||
} else {
|
||||
// Формирование путей
|
||||
$uploadDir = $_SERVER['DOCUMENT_ROOT'] . "/uploads{$location}";
|
||||
$destination = "{$uploadDir}/{$filecdn}";
|
||||
|
||||
// Создание директории
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
$destination = "{$uploadDir}/" . basename($tmpname);
|
||||
// Перемещение файла
|
||||
if (is_uploaded_file($tmpname)) {
|
||||
move_uploaded_file($tmpname, $destination);
|
||||
} else {
|
||||
rename($tmpname, $destination);
|
||||
}
|
||||
|
||||
// Установка свойств
|
||||
$this->type = $type;
|
||||
$this->src = "/uploads/{$folder}";
|
||||
$this->size = self::human_filesize(filesize($tmpname));
|
||||
$this->src = "/uploads/{$location}/{$filecdn}"; // Корректный URL
|
||||
$this->size = self::human_filesize(filesize($destination));
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
|
||||
public function generatePreview($width, $height)
|
||||
{
|
||||
if ($this->type !== 'image') return;
|
||||
|
||||
$src = $_SERVER['DOCUMENT_ROOT'] . $this->src;
|
||||
$image = null;
|
||||
|
||||
switch (mime_content_type($src)) {
|
||||
case 'image/jpeg':
|
||||
$image = imagecreatefromjpeg($src);
|
||||
break;
|
||||
case 'image/png':
|
||||
$image = imagecreatefrompng($src);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$image = imagecreatefromgif($src);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
$originalWidth = imagesx($image);
|
||||
$originalHeight = imagesy($image);
|
||||
|
||||
$preview = imagecreatetruecolor($width, $height);
|
||||
imagecopyresampled(
|
||||
$preview,
|
||||
$image,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$width,
|
||||
$height,
|
||||
$originalWidth,
|
||||
$originalHeight
|
||||
);
|
||||
|
||||
$previewPath = $_SERVER['DOCUMENT_ROOT'] . '/cdn/previews/' . basename($this->src);
|
||||
imagejpeg($preview, $previewPath, 85);
|
||||
imagedestroy($preview);
|
||||
|
||||
$this->previewUrl = '/cdn/previews/' . basename($this->src);
|
||||
}
|
||||
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
|
@ -114,4 +165,3 @@ class Upload
|
|||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\GenerateRandomStr;
|
||||
|
@ -17,4 +18,29 @@ class Word
|
|||
}
|
||||
return $len;
|
||||
}
|
||||
}
|
||||
public static function processMentions($text) {
|
||||
return preg_replace_callback(
|
||||
'/@\[(\d++):([^\]\r\n]+)\]/u',
|
||||
function ($matches) {
|
||||
if (count($matches) !== 3) {
|
||||
return $matches[0] ?? '';
|
||||
}
|
||||
|
||||
$userId = (int)$matches[1];
|
||||
$username = trim($matches[2]);
|
||||
|
||||
// Экранируем только для HTML-атрибута, а не для видимой части
|
||||
$attrUsername = htmlspecialchars($username, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
return '<span class="user-mention" '
|
||||
. 'data-user-id="' . $userId . '" '
|
||||
. 'data-username="' . $attrUsername . '">'
|
||||
. '@' . $username
|
||||
. '</span>';
|
||||
},
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue