diff --git a/mirzaev/notchat/system/controllers/core.php b/mirzaev/notchat/system/controllers/core.php index 455c220..fc430f6 100755 --- a/mirzaev/notchat/system/controllers/core.php +++ b/mirzaev/notchat/system/controllers/core.php @@ -6,7 +6,10 @@ namespace mirzaev\notchat\controllers; // Files of the project use mirzaev\notchat\views\templater, - mirzaev\notchat\models\core as models; + mirzaev\notchat\models\core as models, + mirzaev\notchat\models\log, + mirzaev\notchat\models\enumerations\log as type, + mirzaev\notchat\models\firewall; // Framework for PHP use mirzaev\minimal\controller; @@ -30,7 +33,7 @@ class core extends controller protected array $errors = []; /** - * Constructor of an instance + * Constructor * * @param bool $initialize Initialize a controller? * @@ -44,14 +47,51 @@ class core extends controller if ($initialize) { // Initializing is requested + // Write to the log of connections + log::write( + type::CONNECTIONS, + trim("[{$_SERVER['REMOTE_ADDR']}] " + . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") + . (empty($_SERVER['HTTP_REFERER']) ? '' : "[{$_SERVER['HTTP_REFERER']}] ") + . (empty($_SERVER['HTTP_USER_AGENT']) ? '' : "[{$_SERVER['HTTP_USER_AGENT']}]"), ' ') + ); + + // Initializing of preprocessor of views + $this->view = new templater(); + + // Checking for ban + if ( + (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && firewall::banned($_SERVER['HTTP_X_FORWARDED_FOR'])) + || (isset($_SERVER['REMOTE_ADDR']) && firewall::banned($_SERVER['REMOTE_ADDR'])) + ) { + // IP-address is banned + + // Sending a reply + echo $this->view->render('pages/ban.html'); + + // Exit (success) + die; + } + // Initializing of models core new models(); - - // Initializing of preprocessor of views - $this->view = new templater(); + + // Initializing a response headers + header('Service-Worker-Allowed: /'); } } + /** + * Destructor + * + * @return void + */ + public function __destruct() + { + // Analyze recent requests + firewall::analyze(); + } + /** * Check of initialization * @@ -68,5 +108,4 @@ class core extends controller default => isset($this->{$name}) }; } - } diff --git a/mirzaev/notchat/system/controllers/index.php b/mirzaev/notchat/system/controllers/index.php index 02f4516..fd645d2 100755 --- a/mirzaev/notchat/system/controllers/index.php +++ b/mirzaev/notchat/system/controllers/index.php @@ -6,7 +6,9 @@ namespace mirzaev\notchat\controllers; // Files of the project use mirzaev\notchat\controllers\core, - mirzaev\notchat\models\server; + mirzaev\notchat\models\dns, + mirzaev\notchat\models\server, + mirzaev\notchat\models\text; /** * Index controller @@ -23,6 +25,62 @@ final class index extends core */ public function index(array $parameters = []): ?string { + // Initializing a list with languages + $this->view->languages = text::list($this->errors); + + // Инициализация бегущей строки + $this->view->hotline = [ + 'id' => 'hotline' + ]; + + // Инициализация параметров бегущей строки + $this->view->hotline = [ + 'parameters' => [ + 'step' => '0.3' + ] + ] + $this->view->hotline; + + // Инициализация аттрибутов бегущей строки + $this->view->hotline = [ + 'attributes' => [] + ] + $this->view->hotline; + + // Инициализация элементов бегущей строки + $this->view->hotline = [ + 'elements' => [ + ['html' => ''], + [ + 'tag' => 'article', + 'attributes' => [ + 'class' => 'trash' + ], + 'html' => $this->view->render(DIRECTORY_SEPARATOR . 'hotline' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . 'trash.html', [ + 'id' => 'trash_1', + 'title' => 'Linoleum', + 'main' => '

Do you really like the rotting smell, dull sound and disgusting greasy shine of parquet-like fake pattern on a polymer toxic film? Are you fucking insane?

', + 'image' => [ + 'src' => 'https://virus.mirzaev.sexy/images/trash/linoleum.png', + 'alt' => 'Linoleum' + ] + ]) + ], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''], + ['html' => ''] + ] + ] + $this->view->hotline; + + // Exit (success) if ($_SERVER['REQUEST_METHOD'] === 'GET') return $this->view->render('chats.html'); else if ($_SERVER['REQUEST_METHOD'] === 'POST') return $this->view->render('chats.html'); @@ -31,6 +89,106 @@ final class index extends core return null; } + /** + * Render the servers section + * + * @param array $parameters Parameters of the request (POST + GET) + * + * @return void Generated JSON to the output buffer + */ + /* public function cache(array $parameters = []): void + { + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // GET + + if (file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'manifest')) { + // File found + + // Clearing the output buffer + if (ob_get_level()) ob_end_clean(); + + // Initializing of the output buffer + ob_start(); + + // Initializing a response headers + header('Content-Type: text/cache-manifest'); + + // Generating the reponse + if ($file = fopen($path, 'r')) { + // File open + + // Reading file + while (!feof($file)) echo fread($path, 1024); + + // Closing file + fclose($file); + } + + // Initializing a response headers + header('Content-Length: ' . ob_get_length()); + + // Sending and deinitializing of the output buffer + ob_end_flush(); + flush(); + } + } + } */ + + /** + * + * + * @return void Generated JSON to the output buffer + */ + /* public function cache(array $parameters = []): void + { + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // GET + + if (file_exists($path = INDEX . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'cache.js')) { + // File found + + // Clearing the output buffer + if (ob_get_level()) ob_end_clean(); + + // Initializing of the output buffer + ob_start(); + + // Initializing a response headers + header('Content-Type: application/javascript charset=utf-8'); + + // Generating the reponse + if ($file = fopen($path, 'r')) { + // File open + + // Reading file + while (!feof($file)) echo fread($file, 1024); + + // Closing file + fclose($file); + } + + // Initializing a response headers + header('Content-Length: ' . ob_get_length()); + + // Sending and deinitializing of the output buffer + ob_end_flush(); + flush(); + } + } + } */ + + /** + * Render the offline page + */ + public function offline(): ?string + { + // Initializing of the title + $this->view->title = 'bye'; + + // Exit (success) + return $this->view->render('pages/offline.html'); + } + /** * Render the servers section * @@ -54,7 +212,17 @@ final class index extends core // Generating the reponse echo json_encode( [ - 'html' => $this->view->render('sections/servers.html', ['current' => isset($parameters['server']) ? server::read($parameters['server'], errors: $this->errors) : null, 'servers' => server::all(100, errors: $this->errors) ?? []]), + 'html' => $this->view->render( + 'sections/servers.html', + [ + 'current' => isset($parameters['server']) + && ($server = server::read(domain: dns::domain($parameters['server'], errors: $this->errors), errors: $this->errors)) + ? json_decode($server, true, 8) + : null, + 'servers' => server::all(100, errors: $this->errors) ?? [] + ] + ), + 'status' => isset($server) ? 'connected' : 'disconnected', 'errors' => null ] ); diff --git a/mirzaev/notchat/system/controllers/server.php b/mirzaev/notchat/system/controllers/server.php index 7f8c086..d219024 100755 --- a/mirzaev/notchat/system/controllers/server.php +++ b/mirzaev/notchat/system/controllers/server.php @@ -7,7 +7,13 @@ namespace mirzaev\notchat\controllers; // Files of the project use mirzaev\notchat\controllers\core, mirzaev\notchat\controllers\traits\errors, - mirzaev\notchat\models\server as model; + mirzaev\notchat\models\dns, + mirzaev\notchat\models\server as model, + mirzaev\notchat\models\log, + mirzaev\notchat\models\enumerations\log as type; + +// Built-in libraries +use exception; /** * Server controller @@ -34,7 +40,7 @@ final class server extends core // POST // Create a file with server data - model::write($parameters['domain'], file_get_contents('php://input'), $this->errors); + model::write(dns::domain($parameters['server'], errors: $this->errors), file_get_contents('php://input'), $this->errors); // Initializing a response headers header('Content-Type: application/json'); @@ -74,11 +80,39 @@ final class server extends core if ($_SERVER['REQUEST_METHOD'] === 'POST') { // POST - // Read a file with server data - $server = json_decode(model::read(model::domain($parameters['server']), $this->errors), true, 8); + // Initializing of buffer of response + $return = []; - // Remove protected parameters - unset($server['key']); + try { + // Decode of user input + $parameters['server'] = urldecode($parameters['server']); + + // Validation of user input + if (mb_strlen($parameters['server']) > 512) throw new exception('Server address longer than 512 characters'); + + if ($domain = dns::domain($parameters['server'], errors: $this->errors)) { + if ($raw = model::read(domain: $domain)) { + // File found and read + + // Decoding server data to remove protected parameters + $return['server'] = json_decode($raw, true, 8); + + // Remove protected parameters + unset($return['server']['key']); + } else throw new exception('Server offline'); + } else throw new exception('Server not found'); + } catch (exception $e) { + // Write to the buffer of errors + $this->errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } // Initializing a response headers header('Content-Type: application/json'); @@ -90,8 +124,7 @@ final class server extends core // Generating the reponse echo json_encode( - [ - 'server' => $server, + $return + [ 'errors' => static::text($this->errors) ] ); diff --git a/mirzaev/notchat/system/models/core.php b/mirzaev/notchat/system/models/core.php index 4ee9b6e..58af191 100755 --- a/mirzaev/notchat/system/models/core.php +++ b/mirzaev/notchat/system/models/core.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace mirzaev\notchat\models; +// Files of the project +use mirzaev\notchat\models\enumerations\log as type; + // Framework for PHP use mirzaev\minimal\model; @@ -24,7 +27,12 @@ class core extends model final public const POSTFIX = ''; /** - * Path to storage + * Path to public directory + */ + final public const PUBLIC = '..' . DIRECTORY_SEPARATOR . 'public'; + + /** + * Path to storage directory */ final public const STORAGE = '..' . DIRECTORY_SEPARATOR . 'storage'; diff --git a/mirzaev/notchat/system/models/dns.php b/mirzaev/notchat/system/models/dns.php index ba28aae..9b20dac 100755 --- a/mirzaev/notchat/system/models/dns.php +++ b/mirzaev/notchat/system/models/dns.php @@ -4,15 +4,14 @@ declare(strict_types=1); namespace mirzaev\notchat\models; -// Framework for PHP -use mirzaev\minimal\model; +// Files of the project +use mirzaev\notchat\models\enumerations\log as type; // Built-in libraries -use exception, - DirectoryIterator as parser; +use exception; /** - * Core of DNS registry + * DNS registry * * @package mirzaev\notchat\models * @author Arsen Mirzaev Tatyano-Muradovich @@ -41,9 +40,9 @@ class dns extends core { try { // Open file with DNS records - $dns = fopen(static::DNS, 'r'); + $dns = fopen(static::DNS, 'c+'); - while (($row = fgets($dns)) !== false) { + while (($row = fgets($dns, 512)) !== false) { // Iterate over rows // Initializing values of the server data @@ -52,27 +51,30 @@ class dns extends core // Incrementing the line read counter ++$line; - if ($domain === $_domain || $ip === $_ip || $port === $_port) { - // Server found + if ($domain === $_domain || ($port && $ip === $_ip && $port === $_port) || (!$port && $ip === $_ip || $port === $_port)) { + // Server found (domain, ip, ip + port) // Close file with DNS fclose($dns); // Exit (success) - return $record; + return array_combine(['domain', 'ip', 'port'], $record); } } // Close file with DNS fclose($dns); } catch (exception $e) { - // Write to buffer of errors + // Write to the buffer of errors $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); } // Exit (fail) @@ -100,23 +102,23 @@ class dns extends core // Initializing part the file buffer (rows before target) $after = []; + // Initializing the status that the DNS record has been found + $found = false; + if (file_exists(static::DNS) && filesize(static::DNS) > 0) { // File exists and not empty - // Initializing the status that the DNS record has been found - $found = false; - // Open file with DNS records - $dns = fopen(static::DNS, 'r'); + $dns = fopen(static::DNS, 'c+'); - while (($row = fgets($dns)) !== false) { + while (($row = fgets($dns, 512)) !== false) { // Iterate over rows // Initializing values of the server data [$_domain] = explode(' ', $row); // Writing the row to the file buffer (except the target record) - if ($domain === $_domain) $found = true; + if ($domain === $_domain) $found = $row; else ${$found ? 'after' : 'before'}[] = $row; } @@ -131,10 +133,10 @@ class dns extends core // File locked // Clear file - ftruncate($dns, 0); + ftruncate($dns, 0); // Write a new record to the DNS registry - fwrite($dns, trim(implode("", $before)) . "\n$domain $ip $port\n" . trim(implode("", $after))); + fwrite($dns, (count($before) > 0 ? trim(implode("", $before)) . "\n" : '') . "$domain $ip $port" . (count($after) ? "\n" . trim(implode("", $after)) : '')); // Apply changes fflush($dns); @@ -142,14 +144,20 @@ class dns extends core // Unlock file flock($dns, LOCK_UN); } + + // Write to the log + log::write(type::DNS, $found ? "[UPDATE] $found -> $domain $ip $port" : "[CREATE] $domain $ip $port"); } catch (exception $e) { - // Write to buffer of errors + // Write to the buffer of errors $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); } } @@ -159,32 +167,39 @@ class dns extends core * Convert domain or IP-address to domain * * @param string $server Domain or IP-address of the server + * @param bool $strict Check for port compliance? * @param array &$errors Buffer of errors * * @return string|null Domain of the server */ - public static function domain(string $server, &$errors = []): ?string + public static function domain(string $server, bool $strict = true, &$errors = []): ?string { try { - if (preg_match('/^(https:\/\/)?\d+\..*\d\/?$/', $server) === 1) { + if (preg_match('/^(?:https:\/\/)?([\d\.]*)(?:$|:?(\d.*\d)?\/?$)/', $server, $matches) === 1) { // IP-address + // Initializing of parts of address + @[, $ip, $port] = $matches; + // Exit (success) - return static::read(ip: $server, errors: $errors)['domain']; + return static::read(ip: $ip, port: $strict ? $port : null, errors: $errors)['domain'] ?? null; } else { // Domain (implied) // Exit (success) - return $server; + return $server ?? null; } } catch (exception $e) { - // Write to buffer of errors + // Write to the buffer of errors $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); } // Exit (fail) @@ -204,25 +219,31 @@ class dns extends core public static function ip(string $server, &$errors = []): ?string { try { - if (preg_match('/^(https:\/\/)?\d+\..*\d\/?$/', $server) === 1) { + if (preg_match('/^(?:https:\/\/)?(\d+\..*):?(\d.*\d)?\/?$/', $server, $matches) === 1) { // IP-address + // Initializing of parts of address + [, $ip, $port] = $matches; + // Exit (success) - return $server; + return $ip ?? null; } else { // Domain (implied) // Exit (success) - return static::read(domain: $server, errors: $errors)['ip']; + return static::read(domain: $server, errors: $errors)['ip'] ?? null; } } catch (exception $e) { - // Write to buffer of errors + // Write to the buffer of errors $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); } // Exit (fail) diff --git a/mirzaev/notchat/system/models/enumerations/log.php b/mirzaev/notchat/system/models/enumerations/log.php new file mode 100755 index 0000000..6dffed1 --- /dev/null +++ b/mirzaev/notchat/system/models/enumerations/log.php @@ -0,0 +1,22 @@ + + */ +enum log: string +{ + case ERRORS = 'errors.log'; + case CONNECTIONS = 'connections.log'; + case BANS = 'bans.log'; + case FIREWALL = 'firewall.log'; + case SESSIONS = 'sessions.log'; + case SERVERS = 'servers.log'; + case DNS = 'dns.log'; +} diff --git a/mirzaev/notchat/system/models/firewall.php b/mirzaev/notchat/system/models/firewall.php new file mode 100755 index 0000000..9601e5a --- /dev/null +++ b/mirzaev/notchat/system/models/firewall.php @@ -0,0 +1,211 @@ + + */ +class firewall extends core +{ + use file, read { + file::read as protected file; + read::ban as protected _ban; + } + + /** + * Write + * + * @param int $range Time range for reading last connections (seconds) + * @param int $limit Limit on the number of requests in the allotted time + * @param array &$errors Buffer of errors + * + * @return void + * + * @todo + * 1. Dividing logs based on volume achieved + */ + public static function analyze(int $range = 10, int $limit = 20, &$errors = []): void + { + try { + // Initializing of path to the file with connections + $path = log::LOGS . DIRECTORY_SEPARATOR . type::CONNECTIONS->value; + + if (file_exists($path)) { + // The file exists + + // Initializing of current time + $current = new datetime(); + + // Initializing of target past time + $past = (clone $current)->modify("-$range seconds"); + + // Initializing of the buffer of IP-addresses found in the connections log [ip => amount of connections per $time] + $ips = []; + + // Open file with connections + $connections = fopen($path, 'r'); + + foreach (static::file($connections, 300, 0, -1) as $row) { + // Reading a file backwards (rows from end) + + // Skipping of empty rows + if (empty($row)) continue; + + try { + // Deserializing a row + $parameters = static::connection($row, $errors); + + if ($parameters !== null && is_array($parameters)) { + // Parameters have been initialized + + // Initializing of parameters of connection + [, $date, $ip, $forwarded, $referer, $useragent] = $parameters; + + // Initializing of date of connection + $date = DateTime::createFromFormat('Y.m.d H:i:s', $date); + + if (0 <= $elapsed = $date->getTimestamp() - $past->getTimestamp()) { + // No more than $range seconds have passed since connection + + // Initializing of counter of connections + $ips[$ip] ??= 0; + + // Incrementing of counter of connections + ++$ips[$ip]; + } + } + } catch (exception $e) { + continue; + } + } + + // Close file with connections + fclose($connections); + } + + // Ban IP-addresses that do not meet the conditions + foreach ($ips ?? [] as $ip => $connections) if ($connections >= $limit) static::ban($ip, new datetime('+2 minutes')); + } catch (exception $e) { + // Write to buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + } + + /** + * Ban + * + * @param string $ip IP-address + * @param datetime $end Date for unban + * @param array &$errors Buffer of errors + * + * @return void + */ + public static function ban(string $ip, datetime $end, &$errors = []): void + { + try { + // Write to the log of bans + log::write(type::BANS, "[{$end->format('Y.m.d H:i:s')}] [$ip]"); + } catch (exception $e) { + // Write to buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + } + + /** + * Check for ban + * + * Search in the ban log + * + * @param string $ip IP-address + * @param array &$errors Buffer of errors + * + * @return bool IP-address is banned? (null - has errors) + * + * @todo Count rows of file for reading instead of constant value (500 rows) + */ + public static function banned(string $ip, &$errors = []): ?bool + { + try { + // Initializing of path to the file with bans + $path = log::LOGS . DIRECTORY_SEPARATOR . type::BANS->value; + + if (file_exists($path)) { + // The file exists + + // Open file with bans + $bans = fopen($path, 'r'); + + foreach (static::file($bans, 500, 0, -1) as $row) { + // Reading a file backwards (rows from end) + + // Skipping of empty rows + if (empty($row)) continue; + + try { + // Deserializing a row + $parameters = static::_ban($row, $errors); + + if ($parameters !== null && is_array($parameters)) { + // Parameters have been initialized + + // Initializing of parameters of connection + [, $from, $to, $_ip] = static::_ban($row, $errors); + + // Check for ban and exit (success) + if ($ip === $_ip && (new datetime)->getTimestamp() - DateTime::createFromFormat('Y.m.d H:i:s', $to)->getTimestamp() < 0) return true; + } + } catch (exception $e) { + continue; + } + } + } + + // Exit (success) + return false; + } catch (exception $e) { + // Write to buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + + // Exit (fail) + return null; + } +} diff --git a/mirzaev/notchat/system/models/log.php b/mirzaev/notchat/system/models/log.php new file mode 100755 index 0000000..14d1446 --- /dev/null +++ b/mirzaev/notchat/system/models/log.php @@ -0,0 +1,72 @@ + + */ +class log extends core +{ + /** + * Path to DNS of storaged servers + */ + final public const LOGS = core::STORAGE . DIRECTORY_SEPARATOR . 'logs'; + + /** + * Write + * + * @param type $type Type of log + * @param string $value Text to write + * @param array &$errors Buffer of errors + * + * @return void + * + * @todo + * 1. Dividing magazines based on volume achieved + */ + public static function write(type $type, string $value, &$errors = []): void + { + try { + // Initializing of path to the file of the log + $path = static::LOGS . DIRECTORY_SEPARATOR . $type->value; + + // Open file of the log + $log = fopen($path, 'a'); + + if (flock($log, LOCK_EX)) { + // File locked + + // Initializing of date + $date = date_format(date_create(), 'Y.m.d H:i:s'); + + // Write to the log + fwrite($log, (filesize($path) === 0 ? '' : PHP_EOL) . "[$date] $value"); + + // Apply changes + fflush($log); + + // Unlock file + flock($log, LOCK_UN); + } + } catch (exception $e) { + // Write to buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + } + } +} diff --git a/mirzaev/notchat/system/models/server.php b/mirzaev/notchat/system/models/server.php index 3f927e3..8984b7e 100755 --- a/mirzaev/notchat/system/models/server.php +++ b/mirzaev/notchat/system/models/server.php @@ -4,15 +4,15 @@ declare(strict_types=1); namespace mirzaev\notchat\models; -// Framework for PHP -use mirzaev\minimal\model; +// Files of the project +use mirzaev\notchat\models\enumerations\log as type; // Built-in libraries use exception, DirectoryIterator as parser; /** - * Core of models + * Server * * @package mirzaev\notchat\models * @author Arsen Mirzaev Tatyano-Muradovich @@ -38,6 +38,7 @@ class server extends core public static function write(string $domain, string $json = '', array &$errors = []): void { try { + // if (strlen($domain) > 32) throw new exception('Domain cannot be longer than 32 characters'); // Initializing of path to file @@ -57,7 +58,7 @@ class server extends core // File found // Open file with server data - $file = fopen($path, "r"); + $file = fopen($path, "c+"); // Read server data $old = json_decode(fread($file, filesize($path)), true, 8); @@ -69,7 +70,7 @@ class server extends core // The keys match or the file has not been updated for more than 3 days // Open file with server data - $file = fopen($path, "w"); + $file = fopen($path, "c"); // Write server data fwrite($file, json_encode($new)); @@ -79,12 +80,15 @@ class server extends core // Write DNS record dns::write(domain: $new['domain'], ip: $new['ip'], port: $new['port'], errors: $errors); + + // Write to the log of servers + log::write(type::SERVERS, "[UPDATE] {$old['domain']} {$old['ip']}:{$old['port']} -> {$new['domain']} {$new['ip']}:{$new['port']}"); } else throw new exception('Public keys do not match'); } else { // File is not found // Open file with server data - $file = fopen($path, "w"); + $file = fopen($path, "c"); // Write server data fwrite($file, json_encode($new)); @@ -94,15 +98,21 @@ class server extends core // Write DNS record dns::write(domain: $new['domain'], ip: $new['ip'], port: $new['port'], errors: $errors); + + // Write to the log of errors + log::write(type::SERVERS, "[CREATE] {$new['domain']} {$new['ip']}:{$new['port']}"); } } catch (exception $e) { - // Write to buffer of errors + // Write to the buffer of errors $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); } } @@ -112,35 +122,47 @@ class server extends core * Read JSON from file of server * * @param string $domain Domain of the server + * @param int $time Number of seconds since the file was last edited (86400 seconds is 1 day) * @param array &$errors Buffer of errors * * @return string|null JSON with data of the server */ - public static function read(string $domain, &$errors = []): ?string + public static function read(string $domain, int $time = 86400, &$errors = []): ?string { try { // Initializing of path to file $path = static::SERVERS . DIRECTORY_SEPARATOR . "$domain.json"; - // Open file with server data - $file = fopen($path, "r"); + if (file_exists($path) && filesize($path) > 0) { + // File exists and not empty - // Read server data - $server = fread($file, filesize($path)); + if (time() - filectime($path) < $time && is_readable($path)) { + // The file is actual (1 day by default) and writable - // Close file with server data - fclose($file); + // Open file with server data + $file = fopen($path, 'c+'); - // Exit (success) - return $server; + // Read server data + $server = fread($file, filesize($path)); + + // Close file with server data + fclose($file); + + // Exit (success) + return $server; + } + } } catch (exception $e) { - // Write to buffer of errors + // Write to the buffer of errors $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); } // Exit (fail) @@ -175,6 +197,8 @@ class server extends core $skip = $page * $amount; foreach (new parser(static::SERVERS) as $file) { + // Iterate through all files in a directory + // Skipping unnecessary files if (--$skip > $amount) continue; @@ -182,31 +206,39 @@ class server extends core if ($file->isDot()) continue; if (time() - $file->getCTime() < $time && $file->isReadable()) { - // The file is actual (3 days by default) and writable + // The file is actual (1 day by default) and readable - // Open file with server data - $server = $file->openFile('r'); + if (($size = $file->getSize()) > 0) { + // The file is not empty - // Write server data to the output buffer - $buffer[] = json_decode($server->fread($file->getSize())); + // Open the file with server data + $server = $file->openFile('c+'); - // Close file with server data - unset($file); + // Write server data to the output buffer + $buffer[] = json_decode($server->fread($size)); + + // Close the file with server data + unset($file); + } } + // Exit (success) if (--$amount < 1) break; } // Exit (success) return $buffer; } catch (exception $e) { - // Write to buffer of errors + // Write to the buffer of errors $errors[] = [ 'text' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'stack' => $e->getTrace() ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); } // Exit (fail) diff --git a/mirzaev/notchat/system/models/text.php b/mirzaev/notchat/system/models/text.php new file mode 100755 index 0000000..4baa5f8 --- /dev/null +++ b/mirzaev/notchat/system/models/text.php @@ -0,0 +1,126 @@ + + */ +class text extends core +{ + /** + * Path to the directory with translation files (json) + */ + final public const LANGUAGES = core::PUBLIC . DIRECTORY_SEPARATOR . 'languages'; + + /** + * Default language + */ + final public const LANGUAGE = 'english'; + + + /** + * Read + * + * @param string $id Identifier + * @param string $language Language (name of thw file without ".json") + * @param array &$errors Buffer of errors + * + * @return string|null Text, if found + */ + public static function read(string $id, string $language = 'english', &$errors = []): ?string + { + try { + // Initializing of path to the file of the log + $path = static::LANGUAGES . DIRECTORY_SEPARATOR . "$language.json"; + + if (file_exists($path)) { + // The file exists + + // Open the file of translation + $json = file_get_contents($path); + + if (!empty($json)) { + // The file is not empty + + // Decoding JSON to Array + $text = json_decode($json, true, 8); + + // Exit (success) + return $text[$id] ?? throw new exception('Could not find the text in translation file'); + } + } + } catch (exception $e) { + // Write to buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + + // Exit (fail) + return null; + } + + /** + * Generate a list of available languages + * + * @param array &$errors Buffer of errors + * + * @return array|null Languages, if they found + */ + public static function list(&$errors = []): ?array + { + try { + // Initializing of the buffer of languages + $languages = []; + + foreach (new parser(static::LANGUAGES) as $file) { + // Iterate through all files in the languages directory + + // Skipping system shortcuts + if ($file->isDot()) continue; + + if ($file->isReadable() && $file->getSize() > 0) { + // The file is readable and not empty + + // Write a language to the buffer registry of available languages + $languages[] = $file->getBasename('.json'); + } + } + + // Exit (success) + return $languages; + } catch (exception $e) { + // Write to buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + + // Exit (fail) + return null; + } +} diff --git a/mirzaev/notchat/system/models/traits/file.php b/mirzaev/notchat/system/models/traits/file.php new file mode 100755 index 0000000..b8b8505 --- /dev/null +++ b/mirzaev/notchat/system/models/traits/file.php @@ -0,0 +1,86 @@ + + */ +trait file +{ + /** + * Read + * + * @param resource $file File pointer (fopen()) + * @param int $limit Maximum limit of iterations (rows) + * @param int $position Initial cursor position + * @param int $step Row reading step + * @param array &$errors Buffer of errors + * + * @return generator|string|null + */ + private static function read($file, int $limit = 500, int $position = 0, int $step = 1, &$errors = []): ?generator + { + try { + while ($limit-- > 0) { + // Recursive execution until $limit reaches 0 + + // Initializing of the buffer of row + $row = ''; + + // Initializing the character buffer to generate $row + $character = ''; + + do { + // Iterate over rows + + // End (or beginning) of file reached (success) + if (feof($file)) return; + + // Reading a row + $row = $character . $row; + + // Move to next position + fseek($file, $position += $step, SEEK_END); + + // Read a character + $character = fgetc($file); + + // Is the character a carriage return? (end of row) + } while ($character != PHP_EOL); + + // Exit (success) + yield $row; + } + + // Exit (success) + return null; + } catch (exception $e) { + // Write to the buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + log::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + + // Exit (fail) + return null; + } +} diff --git a/mirzaev/notchat/system/models/traits/log.php b/mirzaev/notchat/system/models/traits/log.php new file mode 100755 index 0000000..614c298 --- /dev/null +++ b/mirzaev/notchat/system/models/traits/log.php @@ -0,0 +1,93 @@ + + */ +trait log +{ + /** + * Deserializing of type::CONNECTIONS + * + * @param string $row Row from the log type::CONNECTIONS + * @param array &$errors Buffer of errors + * + * @return array [$row, $date, $ip, $forwarded, $referer, $useragent] + */ + private static function connection(string $row, &$errors = []): ?array + { + try { + // Search for parameters of connection + preg_match('/(?:^\[(\d{4}\.\d{2}\.\d{2}\s\d{2}:\d{2}:\d{2})\]\s?)(?:\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]\s?)(?:\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]\s?)?(?:\[([^\]]+)\]\s[^$]?)?(?:\[([^\]]+)\]\s?)?$/', trim($row, PHP_EOL), $matches); + + // Have all 5 parameters been detected? + if (count($matches) !== 6) throw new exception('Failed to deserialize row'); + + // Exit (success) + return $matches; + } catch (exception $e) { + // Write to the buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + model::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + + // Exit (fail) + return null; + } + + /** + * Deserializing of type::BANS + * + * @param string $row Row from the log type::BANS + * @param array &$errors Buffer of errors + * + * @return array [$row, $from, $to, $ip] + */ + private static function ban(string $row, &$errors = []): ?array + { + try { + // Search for parameters of ban + preg_match('/(?:^\[(\d{4}\.\d{2}\.\d{2}\s\d{2}:\d{2}:\d{2})\]\s?)(?:\[(\d{4}\.\d{2}\.\d{2}\s\d{2}:\d{2}:\d{2})\]\s?)(?:\[([^\]]+)\]\s?)$/', trim($row, PHP_EOL), $matches); + + // Have all 3 parameters been detected? + if (count($matches) !== 4) throw new exception('Failed to deserialize row'); + + // Exit (success) + return $matches; + } catch (exception $e) { + // Write to the buffer of errors + $errors[] = [ + 'text' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'stack' => $e->getTrace() + ]; + + // Write to the log of errors + model::write(type::ERRORS, "[{$_SERVER['REMOTE_ADDR']}] " . (empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? '' : "[{$_SERVER['HTTP_X_FORWARDED_FOR']}] ") . $e->getMessage()); + } + + // Exit (fail) + return null; + } +} diff --git a/mirzaev/notchat/system/public/index.php b/mirzaev/notchat/system/public/index.php index b7a975c..d4c1116 100755 --- a/mirzaev/notchat/system/public/index.php +++ b/mirzaev/notchat/system/public/index.php @@ -21,7 +21,7 @@ define('STORAGE', realpath('..' . DIRECTORY_SEPARATOR . 'storage')); define('INDEX', __DIR__); // Автозагрузка -require __DIR__ . DIRECTORY_SEPARATOR +require INDEX . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR @@ -33,10 +33,14 @@ require __DIR__ . DIRECTORY_SEPARATOR $router = new router; // Запись маршрутов -$router->write('/', 'index', 'index'); +$router->write('/', 'index', 'index', 'GET'); +$router->write('/', 'index', 'index', 'POST'); +$router->write('/manifest', 'index', 'manifest', 'GET'); +$router->write('/offline', 'index', 'offline', 'GET'); +$router->write('/cache.js', 'index', 'cache', 'GET'); $router->write('/server/read/$server', 'server', 'read', 'POST'); +$router->write('/server/write/$server', 'server', 'write', 'POST'); $router->write('/servers', 'index', 'servers', 'POST'); -$router->write('/servers/connect/$domain', 'server', 'write', 'POST'); // Инициализация ядра $core = new core(namespace: __NAMESPACE__, router: $router, controller: new controller(false), model: new model(false)); diff --git a/mirzaev/notchat/system/public/js/cache.js b/mirzaev/notchat/system/public/js/cache.js new file mode 100644 index 0000000..759fa73 --- /dev/null +++ b/mirzaev/notchat/system/public/js/cache.js @@ -0,0 +1,83 @@ +"use strict"; + +const VERSION = "0.1.0"; +const EXPIRED = 86400000; + +self.addEventListener("install", (event) => { +}); + +self.addEventListener("fetch", (event) => { + event + .respondWith( + caches + .match(event.request) + .then((response) => { + if (response) { + // Found file in cache + + if ( + Date.now() - new Date(response.headers.get("last-modified")) > + EXPIRED + ) { + // Expired period of storage response + + return fetch(event.request.clone()) + .then((response) => { + if (response.status === 200) { + // Downloaded new version + + return caches + .open(VERSION) + .then((cache) => { + // Writing new version to cache + cache.put(event.request, response.clone()); + + // Exit (success) + return response; + }); + } else throw "Failed to download new version"; + }) + .catch(() => { + // Exit (success) (return old version) + return response; + }); + } else return response; + } else { + // Not found file in cache + + return fetch(event.request.clone()) + .then((response) => { + if (response.status === 200) { + // Downloaded + + return caches + .open(VERSION) + .then((cache) => { + // Writing to cache + cache.put(event.request, response.clone()); + + // Exit (success) + return response; + }); + } else throw "Failed to download"; + }); + } + }) + .catch(() => { + return caches.match("/offline"); + }), + ); +}); + +/* self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keyList) => { + return Promise.all( + keyList.map((key) => { + // Deleting old versions of cache + if (VERSION.indexOf(key) === -1) return caches.delete(key); + }), + ); + }), + ); +}); */ diff --git a/mirzaev/notchat/system/public/js/chats.js b/mirzaev/notchat/system/public/js/chats.js index 7a087ee..2b2cff3 100755 --- a/mirzaev/notchat/system/public/js/chats.js +++ b/mirzaev/notchat/system/public/js/chats.js @@ -10,27 +10,161 @@ if (typeof window.chats !== "function") { */ static server = { /** - * Select server + * Select a server (dampered) * - * @param {string} server Domain or IP-address of the server (from cache by default) + * @param {string} server Domain or IP-address:port of the server (from cache by default) + * @param {bool} force Force execution * * @return {void} Into the document will be generated and injected an HTML-element */ - select(server = localStorage.server_ip ?? localStorage.server_domain) { - if (typeof server === "string" && server.length > 0) { - if (core.servers instanceof HTMLElement) { - core.request(`/server/read/${server}`).then((json) => { - if ( - json.errors !== null && typeof json.errors === "object" && - json.errors.length > 0 - ) {} else { - document.querySelector('figcaption[data-server="ip"]').innerText = `${json.ip}:${json.port}`; - document.querySelector('figcaption[data-server="description"]').innerText = `${json.description}`; - } - }); - } - } + select( + server = localStorage.server_ip && localStorage.server_port + ? localStorage.server_ip + ":" + localStorage.server_port + : localStorage.server_domain, + force = false, + ) { + // Writing status: "connecting" + core.menu.setAttribute("data-menu-status", "connecting"); + + // Deinitializing animation of opening + core.menu.getElementsByTagName("output")[0].classList.remove( + "slide-down", + ); + + // Initializing animation of closing + core.menu.getElementsByTagName("output")[0].classList.add( + "slide-down-revert", + ); + + // Disabled for animation + /* core.menu.querySelector('figcaption[data-server="domain"]') + .innerText = + core.menu.querySelector('pre[data-server="description"]') + .innerText = + ""; */ + + this._select(server, force); }, + + /** + * Select a server + * + * @param {string} server Domain or IP-address:port of the server (from cache by default) + * @param {bool} force Force execution + * + * @return {void} Into the document will be generated and injected an HTML-element + */ + _select: damper( + ( + server = localStorage.server_ip && localStorage.server_port + ? localStorage.server_ip + ":" + localStorage.server_port + : localStorage.server_domain, + force = false, + ) => { + if (server.length > 512) { + notifications.write(text.read("CHATS_SERVER_ERROR_LENGTH_MAX")); + } else if (typeof server === "string" && server.length > 0) { + if ( + core.menu instanceof HTMLElement && + core.menu.getAttribute("data-menu") === "chats" + ) { + // + + // Initializing the unlock function + function unblock() { + // Writing status: "empty" + if ( + core.menu.querySelector('figcaption[data-server="domain"]') + .innerText.length === 0 && + core.menu.querySelector('pre[data-server="description"]') + .innerText.length === 0 + ) { + core.menu.getElementsByTagName("search")[0] + .getElementsByTagName("label")[0].classList.add( + "empty", + ); + } + + // Writing status: "disconnected" + core.menu.setAttribute("data-menu-status", "disconnected"); + } + + // Initiating a unlock delay in case the server does not respond + const timeout = setTimeout(() => { + // this.errors(["Server does not respond"]); + unblock(); + }, 5000); + + core.request( + `/server/read/${encodeURIComponent(server)}`, + `language=${localStorage.language ?? "english"}`, + ).then((json) => { + // Deinitializing of unlock delay + clearTimeout(timeout); + + if ( + json.errors !== null && typeof json.errors === "object" && + json.errors.length > 0 + ) { + // Generating notifications with errors + // for (const error of json.errors) notifications.write(error); + + // Writing status: "disconnected" + core.menu.setAttribute("data-menu-status", "disconnected"); + + // Writind domain of the server + core.menu.querySelector('figcaption[data-server="domain"]') + .innerText = ""; + + // Writing description of the server + core.menu.querySelector('pre[data-server="description"]') + .innerText = ""; + + // Writing status: "empty" (for opening the description window) + core.menu.getElementsByTagName("search")[0] + .getElementsByTagName("label")[0].classList.add( + "empty", + ); + } else { + // Writing status: "connected" + core.menu.setAttribute("data-menu-status", "connected"); + + // Writind domain of the server + core.menu.querySelector('figcaption[data-server="domain"]') + .innerText = `${json.server.domain}`; + + // Writing description of the server + core.menu.querySelector('pre[data-server="description"]') + .innerText = `${json.server.description}`; + + // Deleting status: "empty" (for opening the description window) (it is implied that the response from the server cannot be empty) + core.menu.getElementsByTagName("search")[0] + .getElementsByTagName("label")[0].classList.remove( + "empty", + ); + + // Deinitializing animation of closing + core.menu.getElementsByTagName("output")[0].classList.remove( + "slide-down-revert", + ); + + // Initializing animation of opening + core.menu.getElementsByTagName("output")[0].classList.add( + "slide-down", + ); + + // Writing data of the server to local storage (browser) + localStorage.server_domain = json.server.domain; + localStorage.server_ip = json.server.ip; + localStorage.server_port = json.server.port; + } + }); + } + } + }, + 800, + 1, + ), }; /** @@ -44,25 +178,40 @@ if (typeof window.chats !== "function") { * * @return {void} Into the document will be generated and injected an HTML-element */ - servers(server = localStorage.server_ip ?? localStorage.server_domain) { + servers( + server = `${localStorage.server_ip}:${localStorage.server_port}`, + ) { core.request( "/servers", typeof server === "string" && server.length > 0 - ? `server=${server}}` + ? `server=${server}` : "", ).then((json) => { if (core.servers instanceof HTMLElement) core.servers.remove(); if ( json.errors !== null && typeof json.errors === "object" && json.errors.length > 0 - ) {} else { - const element = document.createElement("div"); - core.header.after(element); - element.outerHTML = json.html; + ) { + // Generating notifications with errors + for (const error of json.errors) notifications.write(error); + } else { + if (core.menu instanceof HTMLElement) { + // Writing status of connection (hack with replaying animations) + core.menu.setAttribute('data-menu-status', 'disconnected'); + setTimeout(() => core.menu.setAttribute('data-menu-status', json.status ?? 'disconnected'), 100); - core.servers = document.body.querySelector( - "section[data-section='servers']", - ); + const element = document.createElement("search"); + + const search = core.menu.getElementsByTagName("search")[0]; + if (search instanceof HTMLElement) search.remove(); + + core.menu.prepend(element); + element.outerHTML = json.html; + + core.menu = document.body.querySelector( + "section[data-section='menu']", + ); + } } }); }, @@ -77,7 +226,11 @@ if (typeof window.chats !== "function") { if ( json.errors !== null && typeof json.errors === "object" && json.errors.length > 0 - ) {} else { + ) { + // Generating notifications with errors + for (const error of json.errors) notifications.write(error); + } else { + // сосать бебру const element = document.createElement("div"); const position = core.main.children.length; element.style.setProperty("--position", position); diff --git a/mirzaev/notchat/system/public/js/core.js b/mirzaev/notchat/system/public/js/core.js index 58a591f..60552e8 100755 --- a/mirzaev/notchat/system/public/js/core.js +++ b/mirzaev/notchat/system/public/js/core.js @@ -5,8 +5,11 @@ if (typeof window.core !== "function") { // Initialize of the class in global namespace window.core = class core { - // Label for the
element - static main = document.body.getElementsByTagName('main')[0]; + // Domain + static domain = window.location.hostname; + + // Animations are enabled? + static animations = getComputedStyle(document.body).getPropertyValue('--animations') === '1'; // Label for the
element static header = document.body.getElementsByTagName('header')[0]; @@ -14,11 +17,11 @@ if (typeof window.core !== "function") { // Label for the