From 39193c88c0cae3d7c1b0073c077789c37439e74c Mon Sep 17 00:00:00 2001 From: themohooks <81331307+themohooks@users.noreply.github.com> Date: Sun, 25 May 2025 15:04:50 +0300 Subject: [PATCH] update services --- app/Services/DB.php | 254 +++++++++++++++++++++++++++---- app/Services/Emoji.php | 77 ++++++++++ app/Services/Image.php | 272 ++++++++++++++++++++++++++++++++++ app/Services/Router.php | 24 +++ app/Services/ThemeManager.php | 143 ++++++++++++++++++ app/Services/Upload.php | 94 +++++++++--- app/Services/Word.php | 28 +++- 7 files changed, 838 insertions(+), 54 deletions(-) create mode 100644 app/Services/Emoji.php create mode 100644 app/Services/Image.php create mode 100644 app/Services/ThemeManager.php diff --git a/app/Services/DB.php b/app/Services/DB.php index 5d53106..6d86e15 100644 --- a/app/Services/DB.php +++ b/app/Services/DB.php @@ -1,49 +1,241 @@ 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); + } } -?> diff --git a/app/Services/Emoji.php b/app/Services/Emoji.php new file mode 100644 index 0000000..d0ef950 --- /dev/null +++ b/app/Services/Emoji.php @@ -0,0 +1,77 @@ +"; + } + + 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 ""; + } + + return $matches[0]; + }, $content); + } +} diff --git a/app/Services/Image.php b/app/Services/Image.php new file mode 100644 index 0000000..aed40ba --- /dev/null +++ b/app/Services/Image.php @@ -0,0 +1,272 @@ +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='; + } +} diff --git a/app/Services/Router.php b/app/Services/Router.php index 4ad11ba..79d03fe 100644 --- a/app/Services/Router.php +++ b/app/Services/Router.php @@ -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) diff --git a/app/Services/ThemeManager.php b/app/Services/ThemeManager.php new file mode 100644 index 0000000..0e2da73 --- /dev/null +++ b/app/Services/ThemeManager.php @@ -0,0 +1,143 @@ +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(); + } + } +} diff --git a/app/Services/Upload.php b/app/Services/Upload.php index c38dcfc..b8f7d3f 100644 --- a/app/Services/Upload.php +++ b/app/Services/Upload.php @@ -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; } } - diff --git a/app/Services/Word.php b/app/Services/Word.php index c707825..50a6258 100644 --- a/app/Services/Word.php +++ b/app/Services/Word.php @@ -1,4 +1,5 @@ ' + . '@' . $username + . ''; + }, + $text + ); +} + + +}