From e0b96d350170349a1e413cc3b60d6f7a144baf4b Mon Sep 17 00:00:00 2001 From: REDL Date: Thu, 30 Jul 2026 02:25:34 +0000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=B2=D1=83=D1=8F=D0=B7=D1=8B=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=20(RU/EN)=20=D1=81=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=D0=BC=20+=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D0=B4=D0=B2?= =?UTF-8?q?=D1=83=D1=85=20=D1=8F=D0=B7=D1=8B=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Панель: - engine/main/lang.php: класс Lang, автоопределение языка (?lang= -> cookie -> Accept-Language -> конфиг) - перевод применяется к готовому ответу через ob_start(): покрывает всю панель, админку и письма, не требуя правки 200 файлов шаблонов; отсутствующая фраза остаётся русской - замена только на границах слов, иначе короткий ключ портил длинные слова (Модуль -> Modуль) - AJAX-ответы переводятся отдельно (json_encode экранирует кириллицу в \uXXXX) - application/lang/en.php: 657 переводов; ru.php как точка расширения - переключатель RU/EN в шапке кабинета, админки и в подвале страницы входа - 'lang' в config.php — язык по умолчанию - проверено обходом 20 разделов: 0 непереведённых фраз, 0 мешанины языков, 0 фаталов Документация — теперь на русском и английском: - README.en.md, CHANGES.en.md, SECURITY.en.md, GUIDE.en.md - переключатели языка в начале каждого документа - раздел «Язык интерфейса» в инструкции: как работает, как добавить свой язык --- CHANGES.en.md | 220 ++++++ GUIDE.en.md | 396 ++++++++++ README.en.md | 161 ++++ README.md | 5 + SECURITY.en.md | 243 ++++++ panel/application/config.php | 1 + panel/application/lang/en.php | 727 ++++++++++++++++++ panel/application/lang/ru.php | 18 + panel/application/views/common/admheader.php | 11 + panel/application/views/common/header.php | 11 + .../application/views/common/loginheader.php | 5 + panel/engine/main/lang.php | 259 +++++++ panel/index.php | 7 + БЕЗОПАСНОСТЬ.md | 2 + ИЗМЕНЕНИЯ.md | 27 +- ИНСТРУКЦИЯ.md | 63 +- 16 files changed, 2145 insertions(+), 11 deletions(-) create mode 100644 CHANGES.en.md create mode 100644 GUIDE.en.md create mode 100644 README.en.md create mode 100644 SECURITY.en.md create mode 100644 panel/application/lang/en.php create mode 100644 panel/application/lang/ru.php create mode 100644 panel/engine/main/lang.php diff --git a/CHANGES.en.md b/CHANGES.en.md new file mode 100644 index 0000000..ffba786 --- /dev/null +++ b/CHANGES.en.md @@ -0,0 +1,220 @@ +[Русский](ИЗМЕНЕНИЯ.md) · **English** + +# What changed compared to the original + +Original: **HostinPL 5.6** as a "nulled" build (the `Xopowblu-4EJlOBEK/HostinPL-5.6` fork), +written for Debian 9 and PHP 7.0. + +Below is everything we changed, with file names and line numbers. Panel code changes live in `panel/`. + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** + +--- + +## 1. The panel now runs on modern PHP + +The original targets PHP 7.0. On PHP 8 parts of it died outright. It now runs on **PHP 8.4**: +walking all 21 sections (home, servers, news, status, tickets, web hosting and the whole admin area) +returns **200 on every one and zero errors in the log**. + +| File | Before | After | +|------|--------|-------| +| `engine/engine_ftp/elFinder.class.php:4497` | `utf8_encode()` — removed in PHP 8.2, the file manager died with a fatal error | Wrapped in `function_exists()`, falls back to `mb_convert_encoding($str,'UTF-8','ISO-8859-1')` | +| `application/views/admin/checksys/index.php:22` | `apache_get_modules()` — only exists under Apache with mod_php, so the "System check" page returned 500 under nginx | Wrapped in `function_exists()`, otherwise checked via `REQUEST_URI` | +| `application/views/admin/index.php:215` | `$item['invoice_ammount']` used after the `foreach`, producing an Undefined variable when there were no invoices | `isset()` check | +| `application/models/users.php:199-206` | `$city[1]`, `$country[1]`, `$countryCode[1]` used without checking that the regex matched | `isset()` checks | +| `application/controllers/common/loginheader.php:35` | `$_GET['ref']` without `isset` | `isset()` plus a cast to `(int)` | + +### What turned out to be a false alarm + +The bundled **phpseclib 1.x** uses `create_function()`, which PHP 8 removed. It looked like a blocker, +but checking showed that **phpseclib is not included anywhere in the panel** — it is dead code. +Communication with game nodes goes through the native `php-ssh2` extension +(`engine/libs/ssh2.php` → `ssh2_exec`). Nothing needed fixing. + +`get_magic_quotes_gpc()` in `elFinderConnector.class.php:320` is equally harmless: it sits behind +`version_compare(PHP_VERSION,'5.4','<') && ...`, so on PHP 8 it is never reached. + +--- + +## 2. The captcha is off and managed from the admin area + +The captcha used to be hard-wired: without valid Google keys **it was impossible to log in**, +and the form displayed "ERROR for site owner: Invalid site key". There is now a `captcha_enable` +flag in `application/config.php`, defaulting to `0` (off). + +**Backend.** All four validators received the line +`if($this->config->captcha_enable != '1') return $result;` before the captcha check: + +* `application/controllers/account/login.php` — login +* `application/controllers/common/loginheader.php` — registration and the contact form (2 places) +* `application/controllers/tickets/create.php` — ticket creation + +**Markup.** The five captcha widgets (4 in `views/common/loginheader.php`, 1 in +`views/tickets/create.php`) are wrapped in ``. Google's `api.js` +is only loaded when the captcha is on. Every `grecaptcha.reset(...)` call became +`window.grecaptcha && grecaptcha.reset(...)` — otherwise, with the captcha off, JavaScript threw +inside the error handlers and the forms stopped responding. + +**Admin area.** `views/admin/settings.php`, the "Other settings" tab, gained a +"Bot protection (reCAPTCHA v2)" block: an Off/On selector plus Site key and Secret key fields. + +Verified both ways: with the captcha off, login and registration succeed and the user is really +created in the database; with it on, a fresh session gets "Confirm that you are not a robot!" +and the widget returns to the page. + +> **Careful when adding your own settings.** The settings writer matches configuration lines +> **by substring** (`strpos`). That is why the flag is called `captcha_enable` and not `captcha`: +> the string `captcha` occurs inside `recaptcha` and `secret_recaptcha`, and saving would have +> overwritten the wrong parameter. New keys must not be substrings of existing ones. + +--- + +## 3. Vulnerabilities fixed + +In brief (full detail with code in [SECURITY.en.md](SECURITY.en.md)): + +* **Two pre-authentication SQL injections** in `application/models/users.php` (`createAuthLog()`): + the login journal received the password from the form and the IP from the `CF-Connecting-IP` + header — which was not validated at all — without escaping. Every value now goes through + `$this->db->escape()` or a cast to `(int)`, and the header is validated with + `filter_var(..., FILTER_VALIDATE_IP)`. +* **Plaintext passwords**: `login.php` wrote the real password into the `authlog` table on every + login attempt, including failed ones. Removed. +* **XSS** in the hidden `ref` field of the registration form — `htmlspecialchars()` added. +* The request to the `ip-api.com` geolocation service now only runs for a valid IP and uses `urlencode()`. + +--- + +## 4. The installer was rewritten from scratch + +The original `install` was dangerous on any current system: + +* `echo "deb ... stretch main" > /etc/apt/sources.list` — **wiped the repository list** and replaced + it with Debian 9, after which apt was broken +* installed `php7.0` and hard-coded edits to `/etc/php/7.0/apache2/php.ini` +* switched MariaDB to `bind-address = 0.0.0.0` without a word of warning +* only ran when `/etc/issue.net` said `Debian9`, and otherwise refused to start +* generated passwords and tokens but saved them nowhere — you could only copy them off the screen +* carried on after any failure: every command ended in `> /dev/null 2>&1` + +The new scripts: + +**`install-panel.sh`** — nginx, PHP 8.x (version detected automatically), MariaDB, a database with a +random password, configuration, scheduler, autostart watchdog, administrator creation, and a final +check that the login page really is served with its form. Idempotent: running it again does not wipe +a database that is already loaded. Credentials are written to `/root/.redl-panel-credentials` +(chmod 600). With `set -euo pipefail` the script stops on error instead of pretending all is well. + +**`install-node.sh`** — Docker from the official repository, image build, the +`/home/cp/gameservers/files` layout, the `gameservers` group, MariaDB for game server databases, +SteamCMD, ProFTPD, and sshd configuration that rolls back if `sshd -t` fails. **Before doing anything +it checks whether Docker can work on this machine at all** (`unshare -Ur`) and says so plainly if it +cannot. At the end it prints ready-to-use location details and firewall commands. + +Neither script touches `/etc/apt/sources.list`. + +--- + +## 5. The game server image was rebuilt + +The original `docker/Dockerfile.original-stretch` is based on `debian:stretch`, and the Debian 9 +repositories were shut down in 2023 — **that image no longer builds**, `apt-get update` inside it fails. + +The new `docker/Dockerfile` is based on Debian 12 (bookworm) but **must still be tagged +`debian:stretch`**: that name is hard-coded in the panel +(`application/models/servers.php:642`, `docker create ... debian:stretch`) and cannot be changed +without editing the panel. + +What is inside: 32-bit libraries (SA-MP, CRMP, MTA and older CS builds are i386), `screen` for +consoles, Java for Minecraft, Node.js 20 for RAGE:MP, and `gdb` for crash analysis. +Node.js comes from NodeSource **over HTTPS with repository key verification**, unlike before. + +--- + +## 6. Working without systemd + +The panel assumes systemd is present. Container VPSes do not have it, so watchdogs were added on +cron: `/usr/local/bin/hostinpl-guard` (panel) and `/usr/local/bin/gamenode-guard` (node). +Once a minute (every 2 minutes on a node) they check MariaDB, PHP-FPM, nginx, Docker and cron and +restart whatever died, plus an `@reboot` job to bring everything up after a restart. + +Panel liveness is determined by an **HTTP request** to the login page rather than by searching for a +process name — a pattern search would have matched the watchdog's own command line. + +--- + +## 7. Scheduler + +The original pointed its 9 jobs at the panel's public domain. They now go to `127.0.0.1` +(independent of DNS and external reachability) and carry `-m` timeouts so that a hung request does +not pile up processes. + +> **Token pitfall.** You cannot extract the scheduler token from the config with a plain +> `grep "'token'"` — the line `'yk_password1' => 'token'` matches the same pattern, two values end +> up in the URL separated by a newline, and the jobs then fail silently. The installer writes the +> token directly when it generates the configuration. + +--- + +## 8. Interface languages: Russian and English + +The original was Russian-only, with every string hard-coded in the templates. The build now has a +translation layer: + +* `engine/main/lang.php` — the `Lang` class and the global `t()` helper +* `application/lang/ru.php`, `application/lang/en.php` — dictionaries +* Language selection order: `?lang=` parameter → `lang` cookie → **the browser's `Accept-Language` + header** → panel default +* A switcher (RU / EN) in the header of the client area and on the login page +* Anything not yet translated falls back to the original Russian text, so nothing can disappear + from the interface + +Details and current coverage: see [GUIDE.en.md](GUIDE.en.md), the "Interface language" section. + +--- + +## 9. Branding and dates + +* The year in footers: `2020©` → `2026©` (`views/common/footer.php`, `views/common/loginheader.php`) +* Name and description are REDL.IO: `application/config.php` (`description`, `keywords`, + `mail_sender`, `mail_from`), footers, the "System check" page +* Links to the previous build owner's sites (`hostinpl.ru`, `osmp.ga`) replaced with `redl.io` +* "VK community" links pointing at somebody else's community replaced with `redl.io` + (`footer.php`, `views/main/index.php`, `views/offline/index.php`) +* All 15 e-mail templates: a "REDL.IO / AI-powered hosting" header and the signature + "Sincerely, the REDL.IO team" + +**The authors' copyright headers were not removed** — they remain in every file that had them. + +--- + +## 10. phpMyAdmin + +The panel links to `/phpmyadmin` from the admin area. The original installer set phpMyAdmin up +through Apache, which did not work under nginx. It is now installed from the distribution repository +and served by nginx itself. + +> **Reverse proxy pitfall.** For the address `/phpmyadmin` (without a trailing slash) nginx replies +> with a redirect and by default puts **its own port** into it, for example +> `http://panel.example.com:8095/phpmyadmin/`. If the panel sits behind a reverse proxy that address +> is unreachable from outside and the admin link does not open. The cure is +> `absolute_redirect off; port_in_redirect off;`, already present in the configuration the installer +> generates. + +--- + +## What we did NOT do + +* We did not rewrite password hashing. The panel stores passwords as **unsalted MD5** + (`md5($password)`, a `varchar(32)` column). Moving to `password_hash()` affects login, + registration, recovery and password changes, and requires migrating existing users. +* We did not move game server creation away from Docker. +* We did not delete `panel/application/public/js/proxy/proxy.php` — the file is defanged but kept as + evidence (see [SECURITY.en.md](SECURITY.en.md)). +* We did not test the payment gateways with real payments, nor VK login. +* We did not verify the game servers themselves: that needs a node with Docker. + +--- + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** diff --git a/GUIDE.en.md b/GUIDE.en.md new file mode 100644 index 0000000..b5d13d5 --- /dev/null +++ b/GUIDE.en.md @@ -0,0 +1,396 @@ +[Русский](ИНСТРУКЦИЯ.md) · **English** + +# Complete guide + +From an empty server to a hosting service that can take clients. + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** + +Contents: + +1. [How it all fits together](#1-how-it-all-fits-together) +2. [Installing the panel](#2-installing-the-panel) +3. [Installing a game node](#3-installing-a-game-node) +4. [Connecting the node to the panel](#4-connecting-the-node-to-the-panel) +5. [Game server files](#5-game-server-files) +6. [Configuring the panel](#6-configuring-the-panel) +7. [Interface language](#7-interface-language) +8. [Captcha](#8-captcha) +9. [HTTPS and domain](#9-https-and-domain) +10. [Maintenance](#10-maintenance) +11. [Troubleshooting](#11-troubleshooting) + +--- + +## 1. How it all fits together + +Two roles, usually on separate machines: + +**The panel** — the website, the database, client areas, tickets, payments. It does not need Docker +and runs happily on a cheap VPS. + +**A game node (location)** — where the game servers themselves run. The panel connects to it +**over SSH** and creates a separate Docker container for every ordered server: + +``` +docker create --tty --rm --name=gs --network=host \ + --cpus="" --memory=M \ + --volume="/home/gs/:/home/container/" \ + --workdir=/home/container debian:stretch +``` + +Hence two hard requirements for a node: **working Docker** and **a local image tagged +`debian:stretch`** (the name is hard-coded in the panel). Server files live on the node in +`/home/gs` and appear inside the container as `/home/container`. + +You can have several nodes — they are added to the panel as separate locations, and the client +picks one when ordering. + +> **Important about nodes.** Docker does not start inside container-based VPSes (LXC, OpenVZ and +> similar) because the kernel forbids nested namespaces. A node needs **a dedicated server or KVM**. +> The installer checks this first and warns you. + +--- + +## 2. Installing the panel + +Ubuntu 24.04 / 22.04 or Debian 12 / 13. Minimum: 1 core, 1 GB RAM, 10 GB disk. + +```bash +apt-get update && apt-get install -y git +git clone https://github.com/RedlHosting/redl-gamepanel.git +cd redl-gamepanel +sudo bash install-panel.sh +``` + +The script asks three things: + +* **the panel's domain or IP** — it goes into the configuration as the base address, and all links + and e-mails are built from it; +* **the administrator e-mail** — also the login; +* **the administrator password** — at least 6 characters. + +Everything after that is automatic: packages, a database with a random password, the schema +(27 tables), configuration, permissions, PHP, nginx, the scheduler, an autostart watchdog, an +administrator with full access, and a final check that the login page opens. + +It takes 2–5 minutes. The panel address and login are printed at the end. + +**A non-standard port** (if 80 is taken, or the panel sits behind a reverse proxy): + +```bash +sudo PORT=8095 bash install-panel.sh +``` + +**Where things end up:** + +| What | Path | +|------|------| +| Panel code | `/var/www/hostinpl` | +| Configuration | `/var/www/hostinpl/application/config.php` | +| Database password and scheduler token | `/root/.redl-panel-credentials` | +| nginx configuration | `/etc/nginx/sites-available/hostinpl` | +| Scheduler | `/etc/cron.d/hostinpl` | +| Autostart watchdog | `/usr/local/bin/hostinpl-guard` | +| Database | MariaDB, database `hostin` | + +**phpMyAdmin** (optional; the panel links to it from the admin area): + +```bash +sudo apt-get install -y phpmyadmin # do NOT let it configure a web server, decline +sudo ln -sfn /usr/share/phpmyadmin /var/www/hostinpl/phpmyadmin +``` + +Log in with the database user from `/root/.redl-panel-credentials`. Remember that phpMyAdmin will be +reachable by anyone who knows the address: either restrict it by IP in nginx, or do not install it. + +--- + +## 3. Installing a game node + +You need **a dedicated server or KVM** (not LXC/OpenVZ). Minimum: 2 cores, 2 GB RAM, 100 GB disk, +counting 1–2 GB per game server. + +```bash +apt-get update && apt-get install -y git +git clone https://github.com/RedlHosting/redl-gamepanel.git +cd redl-gamepanel +sudo bash install-node.sh +``` + +The script asks: + +* **this node's IP** — how the panel will reach it; +* **whether to allow root SSH login with a password** — the panel can only connect with a login and + password, and its commands require root. Answer `yes` and the script generates a strong root + password and shows it at the end. + +What it does: checks that the machine is suitable, installs Docker from the official repository, +builds the `debian:stretch` image (Debian 12 inside, plus 32-bit libraries, Java, Node.js and +screen), creates `/home/cp/gameservers/files` and the `gameservers` group, installs MariaDB for game +server databases, SteamCMD and ProFTPD, configures sshd, and installs a Docker watchdog. + +The first run takes 5–15 minutes, mostly building the image. + +**Close the ports immediately afterwards.** The script prints ready-made commands; substitute the +panel's address: + +```bash +ufw allow from PANEL_IP to any port 22 proto tcp +ufw allow from PANEL_IP to any port 3306 proto tcp +ufw deny 3306 +# leave the game ports open: 7777 (SA-MP), 22005 (RAGE:MP), 25565 (Minecraft) and so on +``` + +If you also connect to the node over SSH yourself, **allow your own IP first** or you will lock +yourself out: + +```bash +ufw allow from YOUR_IP to any port 22 proto tcp +``` + +--- + +## 4. Connecting the node to the panel + +1. Sign in to the panel as an administrator +2. Go to **Admin → Locations → Add location** +3. Fill in: + +| Field | What to enter | +|-------|---------------| +| Name | Anything meaningful: "Frankfurt", "Helsinki" | +| IP | The node's IP | +| User | `root` | +| Password | The root password printed by `install-node.sh` | +| Games | Tick the games available on this node | + +4. Save and open **Admin → Locations** — if the panel connected, the location shows its real cores, + RAM and disk (refreshed hourly by the scheduler). + +If it does not connect, check from the panel machine: + +```bash +ssh root@NODE_IP # does the password work? +php -m | grep ssh2 # is the php-ssh2 extension installed? +``` + +If the extension is missing: `apt-get install -y php-ssh2`, then restart PHP-FPM. + +--- + +## 5. Game server files + +**They are not included.** The original installer downloaded them from third-party sites over plain +HTTP with no signature checking, so we removed that (details in [SECURITY.en.md](SECURITY.en.md)). + +Lay the files out on the node like this: + +``` +/home/cp/gameservers/files// +``` + +Game codes are listed under **Admin → Games** (for example `samp`, `crmp`, `mta`, `minecraft`, +`cs`, `css`, `ragemp`). When a server is created, the panel copies the contents of that directory +into `/home/gs/`. + +Only take builds from primary sources: SA-MP from `sa-mp.com`, MTA from `mtasa.com`, +Minecraft (Paper) from `papermc.io`, RAGE:MP from `rage.mp`, Steam games via SteamCMD +(already installed in `/root/steamcmd`). + +Then test the whole chain: order a server as a test client and confirm that it is created and starts. + +--- + +## 6. Configuring the panel + +**Admin → Settings**, four tabs: + +* **General settings** — name, description, contacts, logo +* **Payment gateways** — Unitpay, Enot, AnyPay, LiteKassa, RoboKassa, FreeKassa, YooKassa, QIWI. + You need your own merchant accounts. We never tested a gateway with a real payment — test each one + with your own money before taking clients. +* **Other settings** — trial period, e-mail confirmation, **captcha**, maintenance mode, VK login +* **Information and bonuses** — top-up bonuses, referral percentages + +Plans and games live under **Admin → Games** (versions, RAM and core limits, prices). + +> When you save settings the panel rewrites `config.php` entirely. It matches configuration lines +> **by substring**, so do not create parameters whose names are contained in other names. +> After editing the file by hand, restart PHP-FPM **fully** (`restart`, not `reload`) — otherwise a +> cached copy is served. + +--- + +## 7. Interface language + +The panel speaks **Russian and English**. The language is chosen in this order: + +1. `?lang=en` or `?lang=ru` in the URL — an explicit choice, remembered in a cookie for a year +2. the `lang` cookie — the previous choice +3. **the browser's `Accept-Language` header** — automatic detection +4. the `lang` value in `application/config.php` (default `ru`) + +Automatic detection deliberately keeps Russian for Russian-speaking locales (ru, uk, be, kk and +others) and switches to English for everything else, so a visitor from Germany or Brazil gets the +English interface without touching anything. + +**The switcher** sits in the top bar of the client area and the admin area (the `RU` / `EN` buttons +next to the balance) and in the footer of the login page. + +**How it is built.** The panel was written with Russian text directly in the templates — about +1500 strings across 200 files. Instead of rewriting all of them, translation happens on the finished +response: `index.php` wraps the output in `ob_start()`, and before the page is sent to the browser +the strings are replaced using a dictionary (`engine/main/lang.php`). This covers the whole panel at +once, including the admin area and e-mails, and anything missing from the dictionary simply stays +Russian — nothing can vanish from the screen. + +Replacement only happens on word boundaries, so a short dictionary key cannot corrupt a longer word. + +**Adding your own language**, for example German: + +```bash +cp panel/application/lang/en.php panel/application/lang/de.php +# translate the values in de.php, then add 'de' to Lang::AVAILABLE +# in panel/engine/main/lang.php +``` + +Dictionary coverage is verified by crawling the pages: all 20 sections we walk through render with +no Russian left over. If you spot an untranslated phrase, add it to `application/lang/en.php` — +the key is the Russian text exactly as it appears. + +Translation only runs when the language is not Russian; in Russian mode the dictionary is not even +loaded, so there is no overhead. + +--- + +## 8. Captcha + +By default it is **off** — registration and login work without it, and the field is simply absent. + +To enable it: + +1. Get **reCAPTCHA v2 "I'm not a robot"** keys at + [google.com/recaptcha/admin](https://www.google.com/recaptcha/admin) and specify the panel's domain +2. Go to **Admin → Settings → Other settings → "Bot protection (reCAPTCHA v2)"** +3. Paste the **Site key** and the **Secret key** +4. Switch to "Enabled" and save + +**The order matters.** Enable the captcha without keys and the form will show +"ERROR for site owner: Invalid site key", locking everyone out. If that happens, open +`/var/www/hostinpl/application/config.php`, set `'captcha_enable' => '0'` and restart PHP-FPM: + +```bash +sudo sed -i "s/'captcha_enable' => '1'/'captcha_enable' => '0'/" /var/www/hostinpl/application/config.php +sudo service php8.4-fpm restart # use your PHP version +``` + +The captcha covers login, registration, password recovery and ticket creation — all four forms at once. + +--- + +## 9. HTTPS and domain + +The panel is installed over HTTP. For a certificate: + +```bash +sudo apt-get install -y certbot python3-certbot-nginx +sudo certbot --nginx -d panel.example.com +``` + +After the certificate is issued, update the address in the configuration, otherwise links in +e-mails and redirects will stay on `http://`: + +```bash +sudo sed -i "s|'url' => 'http://|'url' => 'https://|" /var/www/hostinpl/application/config.php +sudo service php8.4-fpm restart +``` + +If the panel sits behind a reverse proxy (Caddy, nginx, Cloudflare), its nginx configuration already +contains `absolute_redirect off; port_in_redirect off;` — without them redirects point at the +internal port and links such as `/phpmyadmin` do not open from outside. + +--- + +## 10. Maintenance + +**Backups.** Keep the database and the configuration: + +```bash +mysqldump hostin > /root/hostin-$(date +%F).sql +cp /var/www/hostinpl/application/config.php /root/config-$(date +%F).php +``` + +Game server files live on the node in `/home/gs` — back those up on the node. + +**Logs:** + +| What to look at | Where | +|-----------------|-------| +| PHP and nginx errors | `/var/log/nginx/error.log` | +| Scheduler hits | `grep 'main/cron' /var/log/nginx/access.log` | +| Login journal | the `authlog` table | +| Node image build | `/tmp/redl-docker-build.log` | + +**Health check:** **Admin → System check** — PHP extensions, versions, scheduler links. Every item +should be green. + +**Updating the code** from the repository: + +```bash +cd redl-gamepanel && git pull +sudo cp -a panel/application panel/engine /var/www/hostinpl/ # config.php is not overwritten +sudo chown -R www-data:www-data /var/www/hostinpl +sudo service php8.4-fpm restart +``` + +--- + +## 11. Troubleshooting + +**"Error: could not load the controller …"** — the panel found no controller for that address. +Usually it is a link to something that does not exist, for example `/phpmyadmin` when phpMyAdmin is +not installed (see section 2). If the section really should exist, check that the files are in place +and owned by `www-data`, then restart PHP-FPM **fully**: opcache may have cached a file that was +caught mid-write. + +**The login page is blank or has no form** — look at `/var/log/nginx/error.log`. A common cause is +`short_open_tag = Off`. The panel uses short `` tags; the installer turns the option on, but you +have to restore it if you reinstall PHP. + +**Cannot log in, the form shows "Invalid site key"** — the captcha is on without keys, see section 8. + +**The scheduler is not running** (statistics not updating, expired servers not suspended): + +```bash +grep -c 'main/cron' /var/log/nginx/access.log # should grow +pgrep -a cron # is the daemon running? +cat /etc/cron.d/hostinpl # every job must be on ONE line +``` + +If a job is split across two lines, the token got into the file with a newline — rewrite the file, +taking the token from `/root/.redl-panel-credentials`. + +**A server is not created on the node** — check on the node: + +```bash +docker info # is the daemon up? +docker image inspect debian:stretch # is the image there? +docker run --rm debian:stretch echo ok # does a container start? +ls /home/cp/gameservers/files/ # are the game files in place? +``` + +**The server is created but dies immediately** — look at the container log on the node: +`docker logs gs`. Most often the 32-bit libraries are missing (rebuild the image from our +`docker/Dockerfile`) or the game build is damaged. + +**Everything worked until a reboot** — on machines without systemd, the watchdogs on cron bring +things back. Check `pgrep -a cron` and run `/usr/local/bin/hostinpl-guard` by hand +(`gamenode-guard` on a node). + +**A phrase in the interface is still Russian in English mode** — add it to +`panel/application/lang/en.php`; the key is the Russian text exactly as it appears on the page. + +--- + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..c75a003 --- /dev/null +++ b/README.en.md @@ -0,0 +1,161 @@ +[Русский](README.md) · **English** + +# Game Hosting Control Panel · REDL build + +A control panel for selling and managing game servers: SA-MP, CRMP, MTA, Minecraft, CS 1.6, CS:S, RAGE:MP. +Client area, ticket system, payment gateways, automated game server deployment onto game nodes. + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** + +This build is a modernisation of the HostinPL 5.6 panel. The original was written for Debian 9 and PHP 7.0 +(both end-of-life since 2022) and simply would not start on a current server. Here it runs on an up-to-date +stack, the vulnerabilities we found are fixed, and one-click installers are included. + +--- + +## ⚠️ Legal status — read this before installing + +The original **HostinPL 5.6 is a commercial, proprietary product**. Its authors are Samir Shelenko and +Alexander Zemlyanoy. The file `panel/LICENSE` is their licence, and it grants no permission to +redistribute or modify the software. + +The sources this build is based on were circulated as "nulled" — a cracked copy of a paid product, +published without the authors' consent. We are not hiding that or rewriting history: the authors' +copyright headers are preserved throughout the code and were deliberately not removed. + +What this means in practice: + +* using it for commercial hosting infringes copyright, with all the risks that follow; +* the legal route is to buy a licence from the authors, or use an open-source alternative + (Pterodactyl, Pelican); +* this repository is only useful for studying the code, auditing it, and internal experiments. + +Responsibility for use rests with whoever installs it. + +--- + +## What is in this repository + +| Path | What it is | +|------|------------| +| `install-panel.sh` | One-click panel install (nginx + PHP 8 + MariaDB + scheduler + admin user) | +| `install-node.sh` | One-click game node install (Docker + image + directories + SSH/FTP + SteamCMD) | +| `panel/` | Panel source code with all of our changes | +| `docker/Dockerfile` | Modern game server image (Debian 12; the original stretch-based one no longer builds) | +| `docker/Dockerfile.original-stretch` | The original Dockerfile, kept for comparison | +| [`CHANGES.en.md`](CHANGES.en.md) | Full list of differences from the original | +| [`SECURITY.en.md`](SECURITY.en.md) | Audit results: vulnerabilities, backdoors, what was fixed | +| [`GUIDE.en.md`](GUIDE.en.md) | Complete guide: install, connecting a node, configuration, maintenance | + +--- + +## Requirements + +**For the panel** (the website and database — Docker is not needed): + +* Ubuntu 24.04 / 22.04 or Debian 12 / 13 +* 1 core, 1 GB RAM, 10 GB disk +* Any VPS will do, including container-based ones (LXC/OpenVZ) + +**For a game node** (this is where the game servers actually run): + +* Ubuntu 24.04 / 22.04 or Debian 12 / 13 +* 2 cores, 2 GB RAM, 100 GB disk (budget 1–2 GB per game server) +* **Full kernel access is mandatory: a dedicated server or KVM.** + The panel creates a separate Docker container for every game server, and Docker does not start + inside a container-based VPS (LXC/OpenVZ) — the kernel forbids nested namespaces. The installer + checks for this and tells you plainly if the machine is unsuitable. + +The panel and a node can share one machine, but separating them is better: keep the website on a +cheap VPS and put the games on real hardware. + +--- + +## One-click install + +### Panel + +```bash +apt-get update && apt-get install -y git +git clone https://github.com/RedlHosting/redl-gamepanel.git +cd redl-gamepanel +sudo bash install-panel.sh +``` + +The script asks only for the panel address, the administrator e-mail and password — everything else +is automatic: it installs nginx, PHP 8 and MariaDB, creates a database with a random password, loads +the schema, writes the configuration and the scheduler, sets up an autostart watchdog, creates the +administrator, and verifies that the login page actually opens. + +To run the panel on a non-standard port, set it via an environment variable: + +```bash +sudo PORT=8095 bash install-panel.sh +``` + +### Game node + +```bash +git clone https://github.com/RedlHosting/redl-gamepanel.git +cd redl-gamepanel +sudo bash install-node.sh +``` + +At the end the script prints the IP, SSH login and password — enter those in the panel under +**Admin → Locations → Add location**. See [GUIDE.en.md](GUIDE.en.md) for details. + +--- + +## What already works + +* Login, registration, password recovery — **without a captcha** (it is enabled from the admin area, see below) +* Client area: balance, invoices, transfers, bonuses, promo codes, referral system +* Ticket system with categories and attachments +* Admin area: users, servers, locations, games, news, statistics, promo codes, settings +* System check (Admin → System check) — every item green +* Payment gateways: Unitpay, Enot, AnyPay, LiteKassa, RoboKassa, FreeKassa, YooKassa, QIWI + (credentials are entered in the admin area; we did not test any gateway with a real payment) +* Web hosting (the WEB section) — via ISPmanager on a separate machine +* Scheduler: statistics, tasks, automatic suspension of expired servers +* **Two interface languages: Russian and English**, with automatic detection from the browser + and a switcher in the header (see [GUIDE.en.md](GUIDE.en.md)) + +### The captcha is off and is enabled with a single switch + +By default there is no captcha anywhere — not on login, registration or tickets; the field simply +does not exist. When you need it: + +1. Get reCAPTCHA v2 ("I'm not a robot") keys at [google.com/recaptcha](https://www.google.com/recaptcha/admin) +2. Go to **Admin → Settings → Other settings → "Bot protection (reCAPTCHA v2)"** +3. Paste the Site key and the Secret key, switch it to "Enabled", save + +The order matters: keys first, switch second — otherwise the form shows +"ERROR for site owner: Invalid site key" and nobody can log in. + +## What does not work + +* **Creating game servers on a container-based VPS.** You need a node with real Docker + (dedicated or KVM) — see Requirements for why. +* **Game server files are not included.** The original installer downloaded them from third-party + sites over plain HTTP with no signature checking, so we removed that (see [SECURITY.en.md](SECURITY.en.md)). + You need to place the files into `/home/cp/gameservers/files//` yourself. +* **Payments and VK login** require your own keys and merchant accounts. + +--- + +## Differences from the original, in brief + +* Runs on **PHP 8.4** instead of PHP 7.0 — a walk through all 21 sections of the panel produces + zero errors in the log +* **The installer was rewritten from scratch**: the original overwrote `/etc/apt/sources.list` with + Debian 9 repositories and broke apt on any current system +* **Two pre-authentication SQL injections were fixed**, and plaintext password storage was removed +* The captcha became manageable from the admin area +* Russian and English interface with automatic language detection +* The scheduler and autostart work even where systemd is absent + +The full list is in [CHANGES.en.md](CHANGES.en.md) and [SECURITY.en.md](SECURITY.en.md). + +--- + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** diff --git a/README.md b/README.md index 995d9e5..0d90f8f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +**Русский** · [English](README.en.md) + # Панель управления игровым хостингом · сборка REDL Панель для продажи и управления игровыми серверами: SA-MP, CRMP, MTA, Minecraft, CS 1.6, CS:S, RAGE:MP. @@ -114,6 +116,8 @@ sudo bash install-node.sh (реквизиты вписываются в админке; ни один шлюз мы не тестировали живыми платежами) * Веб-хостинг (раздел WEB) — через ISPmanager на отдельной машине * Планировщик: статистика, задачи, автоотключение просроченных серверов +* **Два языка интерфейса: русский и английский**, с автоопределением по браузеру + и переключателем в шапке (см. [ИНСТРУКЦИЯ.md](ИНСТРУКЦИЯ.md)) ### Капча — выключена, включается одним переключателем @@ -145,6 +149,7 @@ sudo bash install-node.sh Debian 9 и ломал apt на любой современной системе * **Закрыты две SQL-инъекции, доступные без авторизации**, и убрано хранение паролей в открытом виде * Капча стала управляемой из админки +* Русский и английский интерфейс с автоопределением языка * Планировщик и автозапуск работают и там, где нет systemd Полностью — в [ИЗМЕНЕНИЯ.md](ИЗМЕНЕНИЯ.md) и [БЕЗОПАСНОСТЬ.md](БЕЗОПАСНОСТЬ.md). diff --git a/SECURITY.en.md b/SECURITY.en.md new file mode 100644 index 0000000..af69bf6 --- /dev/null +++ b/SECURITY.en.md @@ -0,0 +1,243 @@ +[Русский](БЕЗОПАСНОСТЬ.md) · **English** + +# Security audit: what we found and what we fixed + +An audit of the HostinPL 5.6 build (a "nulled" copy). We looked for hidden backdoors and webshells, +obfuscation, data being sent to third-party hosts, SQL injections, RCE, authorisation bypasses, +password storage, and problems in the installer and the Dockerfile. + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** + +--- + +## The short version + +The fork claimed: "all shells/holes have been cut out of the panel". Our check shows that claim is +**only partly true**. We found no working backdoors, and there is no obfuscation in the panel code. +But we did find: + +* **proof that webshells really were present in this lineage** — one was found, already defanged; +* **two SQL injections reachable without authentication** — not mentioned anywhere in the fork's description; +* **user passwords written to the database in plaintext**; +* code being installed onto game nodes **over plain HTTP with no signature verification**. + +We fixed the first three. Details below. + +--- + +## 1. A webshell in the build's lineage — defanged + +**File:** `panel/application/public/js/proxy/proxy.php` — all 7 lines of it: + +```php + +``` + +The file takes a `?cmd=` parameter — the classic signature of a command-accepting webshell. Here the +body has been replaced with a message, so **this particular shell is defanged**. But the implication +is unambiguous: backdoors were planted deliberately in this build's distribution chain, and one of +them sat in a scripts folder nobody normally looks at. + +**Our decision:** the file is kept as evidence. It executes nothing. If you do not want it: +`rm panel/application/public/js/proxy/proxy.php`. + +**Takeaway:** any other copy of this panel obtained anywhere but this repository may contain a +*working* shell. Before installing someone else's build, check at least this much: + +```bash +grep -rIn --include=*.php -E "eval\(|assert\(|base64_decode|gzinflate|\\\$_REQUEST\[" . +find . -path ./engine/libs -prune -o -name '*.php' -print | grep -E '(assets|public|tmp)/' +``` + +--- + +## 2. What we did not find (the good news) + +Checked specifically, result negative: + +* **No obfuscation in the panel code.** Zero occurrences of `eval`, `assert`, `create_function`, + `gzinflate`, `$$` or `$_REQUEST` outside vendor libraries. Not a single long base64 blob. +* **No stray PHP files** in the asset, upload or temporary directories — apart from the `proxy.php` + described above. +* **No data leaking to the author's hosts.** `vipadmin.club` appeared only in the keywords meta tag + and the mail sender address; `osmp.ga` was a footer link. Nothing sends passwords, licence data or + database credentials anywhere. +* **The admin gate is intact.** Every controller under `application/controllers/admin/**` performs a + `getAccessLevel()` check (level 2 for the admin area, 3 for games and locations). Not one file + without it. +* **The panel never invokes a shell directly.** A naive `grep` reports 54 `exec` and 8 `system`, but + all of them are inside vendor libraries: in phpseclib, `exec()` is an SSH method rather than a + process launcher, and the rest is in elFinder. The panel's own code calls no shell. + +--- + +## 3. Two pre-authentication SQL injections — FIXED + +The most serious finding, and one the fork's description says nothing about. + +**File:** `panel/application/models/users.php`, function `createAuthLog()`. + +The function records login attempts. It used to interpolate values straight into SQL: + +```php +$query=$this->db->query("INSERT INTO `authlog` (... ,`ip`, ... ,`password`) + VALUES (NULL, '".$userid."', '".$ip."', ... , '".$password."');"); +``` + +**Vector 1 — via the login form.** `$password` is the raw password from `$_POST`, unescaped. It is +called from `account/login.php` on **every** login attempt, including failed ones — that is, **before +any authentication**. Anybody off the street could inject SQL through the login form. + +**Vector 2 — via a forged header.** `$ip` came from `getRealIpAdress()` (`engine/main/user.php`), +which returned the `CF-Connecting-IP` header **with no validation at all** — the early `return` sat +before the `filter_var` block: + +```php +if (!empty($_SERVER["HTTP_CF_CONNECTING_IP"])) { + return $_SERVER["HTTP_CF_CONNECTING_IP"]; // as-is, unvalidated +} +``` + +That header is supplied by the client, so an attacker fully controlled the value reaching SQL. +The same unvalidated IP was also sent to the external `ip-api.com` service. + +**What we did:** + +* every value in the `INSERT` now goes through `$this->db->escape()` or is cast to `(int)`; +* `CF-Connecting-IP` is validated with `filter_var($ip, FILTER_VALIDATE_IP)` and otherwise ignored + in favour of `REMOTE_ADDR`; +* the `ip-api.com` request only runs for a valid IP and uses `urlencode()`. + +**Verified on a live installation:** injection attempts through the password field and through a +forged header no longer get through, the `authlog` table is intact, and all 27 tables are present. + +--- + +## 4. Plaintext passwords — FIXED + +`account/login.php` passed the **real password** from the form into `createAuthLog()`: + +```php +$this->usersModel->createAuthLog($userid['user_id'], $ip, '1', $password); // successful login +$this->usersModel->createAuthLog($userid['user_id'], $ip, '0', $password); // and failed ones too +``` + +So the `authlog` table accumulated every user's password in plaintext — including typos and +passwords for other services that people enter by accident. Anyone with database access could read +them, and given the injection in section 3, so could an outsider. + +**What we did:** the password is no longer passed; an empty string is stored instead. +Verified: after both a successful and a failed login, the column is empty. + +--- + +## 5. XSS in the registration form — FIXED + +`panel/application/views/common/loginheader.php`: the `?ref=` parameter (referral code) was echoed +into a hidden form field without escaping — ``. +`htmlspecialchars(..., ENT_QUOTES, 'UTF-8')` was added, and the controller casts the value to `(int)`. + +--- + +## 6. Passwords stored as unsalted MD5 — NOT fixed + +`login.php:114` — `md5($password)`, column `user_password varchar(32)`. + +Unsalted MD5 is brute-forced at billions of hashes per second on a consumer GPU, and rainbow tables +for common passwords are freely available. If the database leaks, treat every user password as known. + +We **did not change this**: moving to `password_hash()` touches login, registration, recovery and +password changes, and requires migrating existing users — that is a project, not a one-line edit. + +**What to do:** if the panel is used with real people, migrate to `password_hash()` transparently — +when a user logs in successfully against the old MD5 hash, re-save it with the new algorithm. + +--- + +## 7. Code installed onto game nodes over HTTP — source removed + +`panel/engine/games/game_settings.php:54-69` — 18 Node.js modules for RAGE:MP are downloaded +**over plain HTTP** from a third-party host, `mc.hostinpl.ru`, as zip archives with no signature and +no checksum verification. + +Any intermediary on the path — or the owner of that host — can replace the contents, and the code +then runs on your game nodes. The original installer likewise pulled game server builds from +`dl.und3x.ru` and `vipadmin.club`. + +**What we did:** our node installer no longer downloads builds from those hosts — you place the +files into `/home/cp/gameservers/files//` yourself. The lines in `game_settings.php` were +left alone (that is panel functionality), but you should not rely on them: either replace the URLs +with your own over HTTPS, or deploy the modules by hand. + +Node.js in our `docker/Dockerfile` is installed from NodeSource **over HTTPS with repository key +verification**. + +--- + +## 8. The panel connects to nodes with the root password — by design + +The `locations` table stores `location_user` and `location_password` (`varchar(32)`) **in plaintext**, +and the connection uses `ssh2_auth_password()`. The panel's commands (`useradd`, `docker`, +`chown /home`) require root, so in practice the panel logs into the node as root with a password. + +This cannot be changed without reworking the panel: it supports neither keys nor `sudo`. Measures +that reduce the risk: + +```bash +# on the node: SSH and MySQL reachable only from the panel's address +ufw allow from PANEL_IP to any port 22 proto tcp +ufw allow from PANEL_IP to any port 3306 proto tcp +ufw deny 3306 +``` + +The node installer prints these commands when it finishes. Additionally: use a separate password per +node, and keep access to the panel's database to a minimum — whoever reads `locations` gets root on +every node. + +--- + +## 9. MariaDB on the node listens on all interfaces — the panel requires it + +The panel creates game server databases on the node itself and connects to them over the network, +so `bind-address = 0.0.0.0` is mandatory. Our installer does that but **prints a warning** and the +firewall commands — unlike the original, which changed the setting silently. + +--- + +## 10. The registration e-mail contains the password + +`panel/application/views/mail/account/register.php` sends the new user their password in plaintext by +e-mail. That is the original design; we did not change it so as not to break the registration and +activation flow. Bear in mind the message stays in the mailbox and on intermediate servers. + +--- + +## Overall assessment + +| Item | Status | +|------|--------| +| Working backdoors in the panel code | none found | +| Webshell in the build's lineage | found already defanged, kept as evidence | +| Obfuscation, data exfiltration | none found | +| Admin authorisation bypass | none found | +| Pre-auth SQL injections (2) | **fixed** | +| Plaintext passwords in the database | **fixed** | +| XSS in the registration form | **fixed** | +| Unsalted MD5 | remains, needs separate work | +| Code installed over HTTP onto nodes | source removed from the installer | +| Root password used for node access | by design, mitigate with a firewall | +| Password inside the registration e-mail | remains | + +**This panel should not be used for commercial hosting with real clients and payments** — because of +its legal status (see the README), because of the MD5 passwords, and because the provenance of the +code does not deserve trust. For study, internal tasks and auditing, it is fine. + +--- + +**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.** diff --git a/panel/application/config.php b/panel/application/config.php index 9ff4929..eb11c2d 100644 --- a/panel/application/config.php +++ b/panel/application/config.php @@ -61,6 +61,7 @@ $config = array( 'serv_test' => '0', 'offline' => '0', 'offline_res' => 'Хостинг временно закрыт на обслуживание! ', +'lang' => 'ru', 'captcha_enable' => '0', 'recaptcha' => 'ReCaptchaV2', 'secret_recaptcha' => '2ReCaptchaV2', diff --git a/panel/application/lang/en.php b/panel/application/lang/en.php new file mode 100644 index 0000000..e28e266 --- /dev/null +++ b/panel/application/lang/en.php @@ -0,0 +1,727 @@ + 'Cancel', +'Сохранить изменения' => 'Save changes', +'Сохранить' => 'Save', +'Применить' => 'Apply', +'Отправить' => 'Send', +'Написать' => 'Write', +'Открыть' => 'Open', +'Установить' => 'Install', +'Продлить' => 'Renew', +'Оплатить' => 'Pay', +'Пополнить баланс' => 'Top up balance', +'Пополнить' => 'Top up', +'Сменить' => 'Change', +'Изменить пароль' => 'Change password', +'Сменить пароль' => 'Change password', +'Скачать файл' => 'Download file', +'Распечатать' => 'Print', +'Войти' => 'Log in', +'Выйти' => 'Log out', +'Создать аккаунт' => 'Create account', +'Читать полностью' => 'Read more', +'Оставить комментарий' => 'Leave a comment', +'Перевести средства' => 'Transfer funds', +'Обменять бонусы' => 'Exchange bonuses', +'Управление' => 'Manage', +'Выбор' => 'Selection', + +// ── навигация и разделы ────────────────────────────────────────────────────── +'Главная' => 'Home', +'Сервера' => 'Servers', +'Заказать Сервер' => 'Order a server', +'Мои Сервера' => 'My servers', +'Мои сервера' => 'My servers', +'Список серверов' => 'Server list', +'Заказать веб-хостинг' => 'Order web hosting', +'Мои веб-сайты' => 'My websites', +'Веб-хостинг: ws' => 'Web hosting: ws', +'Тикеты' => 'Tickets', +'Мои тикеты' => 'My tickets', +'Мои запросы' => 'My requests', +'Создать запрос' => 'New request', +'Список запросов' => 'Request list', +'Поддержка' => 'Support', +'Служба поддержки' => 'Support team', +'Техническая поддержка' => 'Technical support', +'База знаний (FAQ)' => 'Knowledge base (FAQ)', +'База знаний' => 'Knowledge base', +'Финансы' => 'Billing', +'Баланс' => 'Balance', +'История баланса' => 'Balance history', +'Список пополнений' => 'Top-up list', +'История операций' => 'Transaction history', +'Список операций' => 'Transaction list', +'Перевод средств' => 'Funds transfer', +'Обещанный платеж' => 'Deferred payment', +'Партнерская программа' => 'Affiliate programme', +'Профиль' => 'Profile', +'Мой профиль' => 'My profile', +'Личные данные' => 'Personal details', +'Авторизация' => 'Sign in', +'История авторизации' => 'Login history', +'Статистика' => 'Statistics', +'Новости' => 'News', +'Информация' => 'Information', +'Обратная связь' => 'Contact us', +'Пользователи' => 'Users', +'Автоустановка' => 'Auto install', +'Репозиторий' => 'Repository', +'Друзья' => 'Friends', +'Гость' => 'Guest', +'Смена дизайна' => 'Change theme', +'Сменить дизайн' => 'Change theme', +'Автообновление' => 'Auto refresh', +'Доступные команды' => 'Available commands', + +// ── формы и поля ───────────────────────────────────────────────────────────── +'Введите свой E-Mail' => 'Enter your e-mail', +'Введите свой Пароль' => 'Enter your password', +'Пароль пользователя' => 'User password', +'RCON Пароль' => 'RCON password', +'Повторите пароль' => 'Repeat password', +'Новый пароль' => 'New password', +'Ваш новый пароль:' => 'Your new password:', +'Пароль' => 'Password', +'Имя пользователя' => 'Username', +'Введите сумму' => 'Enter the amount', +'Введите команду!' => 'Enter a command!', +'Есть купон?' => 'Have a coupon?', +'Название' => 'Name', +'Статус' => 'Status', +'Дата' => 'Date', +'Сумма' => 'Amount', +'Адрес' => 'Address', +'Локация' => 'Location', +'Клиент' => 'Client', +'Мод' => 'Mod', +'Задача' => 'Task', +'Телефон:' => 'Phone:', +'Платежная система' => 'Payment gateway', +'Ид платежа' => 'Payment ID', +'Оплачен до' => 'Paid until', +'Смена E-Mail' => 'Change e-mail', +'Секретный ключ' => 'Secret key', + +// ── деньги, периоды, единицы ───────────────────────────────────────────────── +'рублей' => 'RUB', +'руб.' => 'RUB', +'руб!' => 'RUB!', +'руб' => 'RUB', +'0.00 руб.' => '0.00 RUB', +'Итого к оплате:' => 'Total due:', +'Общая сумма пополнений' => 'Total topped up', +'Пополнение баланса' => 'Balance top-up', +'Пополнение баланса пользователя' => 'User balance top-up', +'Оплата счета' => 'Invoice payment', +'Недостаточно средств' => 'Insufficient funds', +'МБ' => 'MB', +'слотов.' => 'slots.', +'слотов' => 'slots', +'15 дней' => '15 days', +'30 дней' => '30 days', +'60 дней' => '60 days', +'90 дней (-5%)' => '90 days (-5%)', +'180 дней (-10%)' => '180 days (-10%)', +'360 дней (-15%)' => '360 days (-15%)', +'90 дней' => '90 days', +'180 дней' => '180 days', +'360 дней' => '360 days', +'дней в неделю' => 'days a week', +'часа в сутки' => 'hours a day', +'Осталось %d дней' => '%d days left', +'дней' => 'days', +'дня' => 'days', +'день' => 'day', +'Одноразовая' => 'One-off', +'Повторяющаяся' => 'Recurring', + +// ── состояния ──────────────────────────────────────────────────────────────── +'Включена' => 'Enabled', +'Выключена' => 'Disabled', +'Включен' => 'Enabled', +'Выключен' => 'Disabled', +'В сети' => 'Online', +'Не в сети' => 'Offline', +'На данный момент список пуст.' => 'The list is currently empty.', +'На данный момент у вас нет запросов.' => 'You have no requests at the moment.', +'На данный момент у вас нет счетов.' => 'You have no invoices at the moment.', +'На данный момент у вас нет операций со счетом.'=> 'You have no account transactions yet.', +'У вас нет активов.' => 'You have no assets.', +'Список логов пуст :(' => 'The log is empty :(', +'На данный момент нет доступных локаций' => 'No locations are available at the moment', +'К сожалению, на данный момент на хостинге нет новостей' => 'There is no hosting news at the moment', +'Получение данных...' => 'Loading data…', +'Ваш запрос обрабатывается, пожалуйста, подождите...' => 'Your request is being processed, please wait…', +'Пожалуйста, подождите 10 секунд.'=> 'Please wait 10 seconds.', + +// ── авторизация, регистрация, пароль ──────────────────────────────────────── +'Вы не авторизированы!' => 'You are not signed in!', +'Вы не авторизированы' => 'You are not signed in', +'Вы уже авторизированы!' => 'You are already signed in!', +'Вы успешно авторизировались!' => 'You have signed in successfully!', +'Вы успешно вошли!' => 'You have signed in!', +'Вы успешно вышли из своего аккаунта' => 'You have signed out of your account', +'Вы ввели не верный логин или пароль!' => 'Wrong e-mail or password!', +'Вы не залогинены!' => 'You are not logged in!', +'Нет доступа!' => 'Access denied!', +'У вас нет доступа к данному разделу!' => 'You do not have access to this section!', +'Авторизуйтесь для входа в панель управления' => 'Sign in to open the control panel', +'Регистрация аккаунта' => 'Account registration', +'Вы успешно зарегистрировались!' => 'You have registered successfully!', +'Завершение регистрации!' => 'Registration complete!', +'Завершение регистрации' => 'Registration complete', +'Ваш аккаунт подтвержден!' => 'Your account is confirmed!', +'Указанный E-Mail уже зарегистрирован!' => 'That e-mail is already registered!', +'Указанный E-Mail уже используется!' => 'That e-mail is already in use!', +'Пользователь с указанным E-Mail не зарегистрирован!' => 'No user is registered with that e-mail!', +'Укажите свой реальный E-Mail!' => 'Enter a valid e-mail address!', +'Укажите свое реальное имя!' => 'Enter your real first name!', +'Укажите свою реальную фамилию!' => 'Enter your real last name!', +'Введенные вами пароли не совпадают!' => 'The passwords do not match!', +'Пароль должен содержать от 6 до 32 латинских букв, цифр и знаков ,.!?_-!' => 'The password must be 6 to 32 Latin letters, digits or the characters ,.!?_-!', +'Пароль должен содержать от 8 до 32 латинских букв, цифр и знаков ,.!?_-!' => 'The password must be 8 to 32 Latin letters, digits or the characters ,.!?_-!', +'Восстановление пароля' => 'Password recovery', +'На ваш E-Mail отправлена информация для восстановления пароля!' => 'Password recovery instructions have been sent to your e-mail!', +'Указанный ключ восстановления неверный!' => 'That recovery key is not valid!', +'Ваш пароль успешно был изменен' => 'Your password has been changed', +'Пароль был успешно изменен!' => 'The password has been changed!', +'Подтвердите, что вы не робот!' => 'Please confirm that you are not a robot!', +'Укажите правильный код с картинки! Попробуйте нажать на картинку, чтобы обновить ее.' => 'Enter the correct code from the image. Click the image to refresh it.', +'Укажите правильный код с картинки!' => 'Enter the correct code from the image!', +'Проверьте правильность капчи!' => 'Check the captcha and try again!', +'Проверьте вашу почту,' => 'Check your e-mail,', +'на нее должно прийти сообщение с дальнейшими инструкциями.' => 'a message with further instructions should arrive there.', +'Повторите вашу попытку снова.' => 'Please try again.', +'Через 5 секунд вы будете перенаправлены на домашнюю страницу' => 'You will be redirected to the home page in 5 seconds', +'Изменения сохранены!' => 'Changes saved!', +'Ошибка! Повторите действие.' => 'Error! Please try again.', +'Неизвестная ошибка!' => 'Unknown error!', +'Внутренняя ошибка!(Либо не привязана учётка)' => 'Internal error (or the account is not linked)', +'Не POST запрос!' => 'Not a POST request!', +'Не POST запрос' => 'Not a POST request', +'Не POST данные' => 'Not POST data', +'Упс... Что-то пошло не так.' => 'Oops… something went wrong.', +'Вернутся на главную' => 'Back to home', +'Ошибка!' => 'Error!', +'Критическая ошибка' => 'Critical error', + +// ── VK и сессии ────────────────────────────────────────────────────────────── +'Профиль привязан!' => 'Profile linked!', +'Профиль не привязан' => 'Profile not linked', +'Данный профиль уже привязан к другому аккаунту!' => 'That profile is already linked to another account!', +'Данный аккаунт уже привязан!' => 'That account is already linked!', +'Вы успешно отвязали VK!' => 'VK has been unlinked!', +'На данный момент у Вас не привязан VK!' => 'You have no VK account linked!', +'Сеанс не найден!' => 'Session not found!', +'Сеанс успешно завершён!' => 'Session ended successfully!', +'Вы не можете завершить текущий сеанс!' => 'You cannot end your current session!', + +// ── баланс, платежи, бонусы ────────────────────────────────────────────────── +'Ваш баланс успешно пополнен!' => 'Your balance has been topped up!', +'Укажите сумму пополнения в допустимом формате!' => 'Enter the top-up amount in a valid format!', +'Укажите сумму от 10 до 5000 рублей!' => 'Enter an amount between 10 and 5000 RUB!', +'Укажите сумму от 0 до 5000 рублей!' => 'Enter an amount between 0 and 5000 RUB!', +'Укажите реальное число!' => 'Enter a valid number!', +'Данная платежная система отключена!' => 'This payment gateway is disabled!', +'Вы указали недопустимый период оплаты!' => 'You selected an invalid payment period!', +'У вас недостаточно средств!' => 'You do not have enough funds!', +'На Вашем счету не хватает' => 'Your account is short of', +'На Вашем счету недостаточно средств' => 'There are not enough funds in your account', +'Средства переведены!' => 'Funds transferred!', +'Перевод средств пользователю ID-' => 'Funds transfer to user ID-', +'Деньги отправлены. Вам начисленно'=> 'Money sent. You received', +'Вы указали свой ID' => 'You entered your own ID', +'Данного ID не сушествует!' => 'That ID does not exist!', +'Здесь вы можете обменять свои бонусные баллы.' => 'Here you can exchange your bonus points.', +'У вас недостаточно монет!' => 'You do not have enough coins!', +'Бонус с реферала ID-' => 'Referral bonus, ID-', +'Бонус за приглашенного реферала ID-' => 'Bonus for an invited referral, ID-', +'Вы активировали скидку' => 'You activated a discount of', +'Данного кода не существует' => 'That code does not exist', +'Вы активирали обещаный платёж!' => 'Deferred payment activated!', +'На данный момент Вам недоступна данная функция!' => 'This feature is not available to you right now.', +'Внутренняя ошибка. Возможные причины:' => 'Internal error. Possible reasons:', +'- Неверная сумма платежа.' => '- Wrong payment amount.', +'- Неверный ID магазина.' => '- Wrong shop ID.', +'- Не верный ID платежа.' => '- Wrong payment ID.', +'- Данный счет уже оплачен.' => '- This invoice is already paid.', +'- Платеж был отменен.' => '- The payment was cancelled.', +'Если здесь нет выявленной вами причины, то обратитесь в' => 'If your case is not listed here, please contact', +'Ошибка - Неверная сумма платежа!' => 'Error — wrong payment amount!', +'Ошибка - Shop ID!' => 'Error — Shop ID!', +'Ошибка - Не верный ID платежа!' => 'Error — wrong payment ID!', +'Ошибка - Данный счет уже оплачен!' => 'Error — this invoice is already paid!', + +// ── заказ и жизненный цикл игрового сервера ───────────────────────────────── +'Выберите локацию' => 'Choose a location', +'Выберите период оплаты' => 'Choose a payment period', +'Информация о заказе' => 'Order details', +'Игровой сервер: gs' => 'Game server: gs', +'Сервер успешно поставлен в очередь на установку.' => 'The server has been queued for installation.', +'Сервер успешно поставлен на переустановку!' => 'The server has been queued for reinstallation.', +'Идет установка сервера!' => 'Server installation in progress!', +'Идет переустановка сервера!' => 'Server reinstallation in progress!', +'Идет создание BackUP сервера!' => 'Server backup in progress!', +'Идет восстанновление сервера из BackUP!' => 'Server restore from backup in progress!', +'Идет обновление сервера!' => 'Server update in progress!', +'Сервер заблокирован!' => 'The server is suspended!', +'Сервер должен быть выключен!' => 'The server must be stopped!', +'Сервер должен быть вылючен!' => 'The server must be stopped!', +'Сервер должен быть включен!' => 'The server must be running!', +'Сервер должен быть включён!' => 'The server must be running!', +'Запрашиваемый сервер не существует!' => 'The requested server does not exist!', +'Вы выбрали несуществующее действие!' => 'You selected an action that does not exist!', +'Вы не можете использовать это действие!' => 'You cannot use this action!', +'Вы указали несуществующую локацию!' => 'You selected a location that does not exist!', +'Вы указали несуществующую игру!' => 'You selected a game that does not exist!', +'Данная игра не доступна для указанной локации!' => 'This game is not available in the selected location!', +'На выбранной Вами локации нет свободных портов для данной игры' => 'The selected location has no free ports for this game', +'Успешный запуск сервера' => 'Server started', +'Вы успешно запустили сервер!' => 'You have started the server!', +'Успешный перезапуск сервера' => 'Server restarted', +'Вы успешно перезапустили сервер!'=> 'You have restarted the server!', +'Успешное выключение сервера' => 'Server stopped', +'Вы успешно выключили сервер!' => 'You have stopped the server!', +'Команда успешно отправлена!' => 'Command sent!', +'Консоль успешно очищена!' => 'Console cleared!', +'Ошибка подключения к rcon!' => 'RCON connection failed!', +'Возникла ошибка запроса к серверу. Код ошибки:' => 'The request to the server failed. Error code:', +'Вы привысили ограничение размера директории на' => 'You have exceeded the directory size limit by', +'Продление сервера' => 'Server renewal', +'Сервер успешно продлен!' => 'The server has been renewed!', +'Смена ядра' => 'Change core', +'Смена билда' => 'Change build', +'Параметры запуска' => 'Startup parameters', +'Параметры запуска успешно изменены!' => 'Startup parameters updated!', +'Вы успешно начали смену ядра!' => 'Core change started!', +'Вы указали неверное ядро!' => 'That core is not valid!', +'Вы успешно начали смену билда!' => 'Build change started!', +'Вы указали неверный билд!' => 'That build is not valid!', +'Вы успешно сменили карту на' => 'Map changed to', +'Вы успешно сменили порт сервера на' => 'Server port changed to', +'Выбранный порт не может быть использован!' => 'That port cannot be used!', +'Выбранный порт уже занят другим сервером!' => 'That port is already taken by another server!', +'Параметр Tickrate указан неверно!' => 'The tickrate value is not valid!', +'Параметр FPS указан неверно!' => 'The FPS value is not valid!', +'RCON Пароль должен содержать от 2 до 18 символов!' => 'The RCON password must be 2 to 18 characters!', +'Пароль отправленный на сервер не соответствует нормам!' => 'The password sent to the server does not meet the requirements!', +'Вы не указали rcon пароль в параметрах запуска сервера!' => 'You have not set an RCON password in the startup parameters!', +'Пароль FTP успешно изменен!!' => 'FTP password changed!', +'Пароль MySql успешно изменен!!' => 'MySQL password changed!', +'Пароль доступа успешно изменен!' => 'Access password changed!', +'Необходимо удалить все заблокированные IP адреса!' => 'You must remove all blocked IP addresses first!', +'Вы не ввели IP' => 'You did not enter an IP', +'Вы успешно заблокировали' => 'You have blocked', +'Вы успешно разблокировали IP!' => 'You have unblocked the IP!', + +// ── слоты, бекапы, база данных, моды ──────────────────────────────────────── +'Вы успешно установили' => 'You have set', +'Вы указали недопустимое количество слотов!' => 'That number of slots is not allowed!', +'Укажите количество слотов в допустимом формате!' => 'Enter the number of slots in a valid format!', +'Невозможно установить слоты ниже заданных.' => 'Slots cannot be set below the current value.', +'Слоты не были изменены.' => 'The slots were not changed.', +'Увеличено до' => 'Increased to', +'Успешное начало создания BackUP сервера.' => 'Server backup started.', +'Вы успешно начали создание BackUP сервера!' => 'You have started a server backup!', +'Вы успешно восстановили BackUP сервера!' => 'The server has been restored from backup!', +'Успешное удаление BackUP сервера'=> 'Backup deleted', +'Вы успешно удалили BackUP сервера!' => 'You have deleted the server backup!', +'У вас нет BackUP сервера!' => 'You have no server backup!', +'Успешное создание BackUP.' => 'Backup created.', +'Успешное восстановление из BackUP.' => 'Restored from backup.', +'Успешная установка сервера.' => 'Server installed successfully.', +'Успешная переустановка сервера.' => 'Server reinstalled successfully.', +'Успешная смена ядра.' => 'Core changed successfully.', +'Успешная смена билда.' => 'Build changed successfully.', +'Вы успешно создали базу данных' => 'You have created the database', +'Вы успешно включили базу данных' => 'You have enabled the database', +'Вы успешно выключили базу данных'=> 'You have disabled the database', +'База данных и так включена либо вы ее еще не создали!' => 'The database is already enabled, or you have not created it yet!', +'База данных и так выключена!' => 'The database is already disabled!', +'База данных и так создана!' => 'The database already exists!', +'Создайте базу MySQL и включите ее!' => 'Create a MySQL database and enable it!', +'Данный мод не доступен для установки!' => 'This mod is not available for installation!', +'Запрашиваемый мод не существует!'=> 'The requested mod does not exist!', +'Установка мода' => 'Mod installation', +'Не для Вашей игры!' => 'Not for your game!', +'Вы успешно загрузили на сервер модуль' => 'You have uploaded the module to the server', +'Данный файл не доступен!' => 'This file is not available!', +'Запрашиваемый файл не существует!' => 'The requested file does not exist!', +'Покупка файла' => 'File purchase', +'Файл' => 'File', + +// ── задачи планировщика ────────────────────────────────────────────────────── +'Задача успешно создана!' => 'Task created!', +'Задача успешно удалена!' => 'Task deleted!', +'Задача не найдена!' => 'Task not found!', +'Вы указали несуществующую задачу!' => 'You selected a task that does not exist!', +'Вы не можете создать данную задачу!' => 'You cannot create this task!', +'Данная задача уже создана!' => 'This task already exists!', +'Вы указали несуществующий тип задачи!' => 'You selected a task type that does not exist!', +'Данный тип задачи недоступен для задачи!' => 'This task type is not available for the task!', +'Вы указали недопустимое время выполнения!' => 'That execution time is not allowed!', +'Вы не можете создавать больше 4-х задач!' => 'You cannot create more than 4 tasks!', +'[Планировщик] Успешный запуск сервера' => '[Scheduler] Server started', +'[Планировщик] Успешное выключение сервера' => '[Scheduler] Server stopped', +'[Планировщик] Успешный перезапуск сервера' => '[Scheduler] Server restarted', +'Сервер был восcтановлен после падения' => 'The server was recovered after a crash', + +// ── друзья / совладельцы сервера ───────────────────────────────────────────── +'Вы успешно добавили друга!' => 'Friend added!', +'Вы успешно удалили друга!' => 'Friend removed!', +'Вы не являетесь владельцем сервера. У вас нет доступа к разделу!' => 'You are not the server owner, so you cannot access this section!', +'Вы указали недоступный ID пользователя!' => 'That user ID is not available!', +'Запрашиваемый пользователь не существует!' => 'The requested user does not exist!', +'Запрашиваемый пользователь уже добавлен!' => 'That user has already been added!', +'Запрашиваемый пользователь является владельцем сервера!' => 'That user is the server owner!', + +// ── тестовый период ────────────────────────────────────────────────────────── +'Вы уже брали тестовый период, либо у вас нет одобрения администратора!' => 'You have already used the trial period, or an administrator has not approved it!', +'Тестовый период отключен на хостинге!' => 'The trial period is disabled on this hosting!', + +// ── тикеты ─────────────────────────────────────────────────────────────────── +'Вы успешно создали запрос!' => 'Your request has been created!', +'Вы успешно закрыли запрос!' => 'You have closed the request!', +'Вы успешно отправили сообщение!' => 'Your message has been sent!', +'Данный запрос закрыт, оставить новое сообщение невозможно!' => 'This request is closed, you cannot add a new message!', +'Запрашиваемый запрос не существует!' => 'The requested ticket does not exist!', +'Название тикета должно содержать от 6 до 32 символов!' => 'The ticket subject must be 6 to 32 characters!', +'Текст тикета должен содержать от 10 до 350 символов!' => 'The ticket text must be 10 to 350 characters!', +'Текст сообщения должен содержать от 1 до 350 символов.' => 'The message text must be 1 to 350 characters.', +'Нельзя создать тикет, так как за сегодня вы создали больше 3 тикетов.' => 'You cannot create a ticket: you have already created more than 3 today.', +'Новые запросы' => 'New requests', + +// ── загрузка изображений ───────────────────────────────────────────────────── +'Можно загружать только изображения в форматах jpg, jpeg и png' => 'Only jpg, jpeg and png images can be uploaded', +'Изображение не загружено!' => 'The image was not uploaded!', +'Аватар успешно был загружен!' => 'Avatar uploaded!', +'Загружаемое изображение превышает допустимые нормы!' => 'The uploaded image is too large!', +'Размер изображения не должено превышать 512Кб' => 'The image must not exceed 512 KB', +'Размер изображения не должено превышать 5120Кб' => 'The image must not exceed 5120 KB', +'Изображение:' => 'Image:', + +// ── новости ────────────────────────────────────────────────────────────────── +'Вы успешно создали коментарий!' => 'Your comment has been posted!', +'Запрашиваемая новость не существует!' => 'The requested news item does not exist!', +'Текст коментариф должен содержать от 10 до 128 символов.' => 'The comment must be 10 to 128 characters.', +'Новости и статусы на REDL.IO' => 'News and status on REDL.IO', + +// ── веб-хостинг ────────────────────────────────────────────────────────────── +'Веб-хостинг №' => 'Web hosting no.', +'успешно заказан.' => 'ordered successfully.', +'Веб.хостинг не доступен для заказа!' => 'Web hosting is not available for ordering!', +'Вы указали несуществующий тариф!'=> 'You selected a plan that does not exist!', +'Данный тариф не доступен для указанной локации!' => 'This plan is not available in the selected location!', +'Веб-хостинг заблокирован!' => 'Web hosting is suspended!', +'Запрашиваемый веб-хостинг не существует!' => 'The requested web hosting does not exist!', + +// ── темы оформления ────────────────────────────────────────────────────────── +'Тема установлена!' => 'Theme applied!', +'Тема Default успешно установлена!' => 'The Default theme has been applied!', +'Тема Material успешно установлена!' => 'The Material theme has been applied!', +'Тема Material gray успешно установлена!'=> 'The Material gray theme has been applied!', +'Тема Material light успешно установлена!'=> 'The Material light theme has been applied!', +'Тема Default' => 'Default theme', + +// ── главная страница и маркетинг ───────────────────────────────────────────── +'Удобная панель управления' => 'A convenient control panel', +'С нашей панелью управления, управлять сервером легко и просто с ней справится как опытный пользователь так и новичок.' => 'Our control panel makes managing a server simple — both for experienced users and for beginners.', +'Преимущества нашего хостинга' => 'Why choose our hosting', +'Мощные процессоры Intel' => 'Powerful Intel processors', +'Защита от DDoS атак на оборудование.' => 'Hardware-level DDoS protection.', +'Полный доступ к FTP' => 'Full FTP access', +'Создание резервных копий backup' => 'Backup creation', +'Скоростные SSD диски' => 'Fast SSD storage', +'Хостинг с искусственным интеллектом' => 'AI-powered hosting', +'С уважением, Администрация REDL.IO!' => 'Sincerely, the REDL.IO team!', + +// ── месяцы (графики и даты) ───────────────────────────────────────────────── +'Январь' => 'January', 'Февраль' => 'February', 'Март' => 'March', +'Апрель' => 'April', 'Май' => 'May', 'Июнь' => 'June', +'Июль' => 'July', 'Август' => 'August', 'Сентябрь' => 'September', +'Октябрь'=> 'October', 'Ноябрь' => 'November', 'Декабрь' => 'December', +'Янв.' => 'Jan', 'Фев.' => 'Feb', 'Март.' => 'Mar', 'Апр.' => 'Apr', +'Май.' => 'May', 'Июнь.' => 'Jun', 'Июль.' => 'Jul', 'Авг.' => 'Aug', +'Сент.'=> 'Sep', 'Окт.' => 'Oct', 'Ноя.' => 'Nov', 'Дек.' => 'Dec', + +// ── элементы графиков ──────────────────────────────────────────────────────── +'Скачать PNG' => 'Download PNG', +'Скачать CSV' => 'Download CSV', +'Скачать SVG' => 'Download SVG', +'Выбор Zoom' => 'Zoom selection', +'Увеличить' => 'Zoom in', +'Уменьшить' => 'Zoom out', +'Панорамирование'=> 'Panning', +'Сброс Масштаба' => 'Reset zoom', + +// ── служебные ошибки движка ────────────────────────────────────────────────── +'Ошибка: Не удалось загрузить контроллер' => 'Error: could not load the controller', +'Ошибка: Не удалось загрузить шаблон' => 'Error: could not load the template', +'Ошибка: Не удалось загрузить модель' => 'Error: could not load the model', +'Ошибка: Не удалось загрузить библиотеку' => 'Error: could not load the library', +'Ошибка: Не удалось загрузить драйвер базы данных' => 'Error: could not load the database driver', +'Ошибка: Не удалось загрузить файл конфигурации игр!' => 'Error: could not load the games configuration file!', +'Ошибка: Не удалось загрузить файл конфигурации!' => 'Error: could not load the configuration file!', +'Укажите допустимый статус!' => 'Choose a valid status!', + +// ── админка: часто встречающиеся подписи ──────────────────────────────────── +'Введите название локации' => 'Enter the location name', +'Название локации должно содержать от 2 до 32 символов!' => 'The location name must be 2 to 32 characters!', +'Введите ID' => 'Enter the ID', +'Введите имя пользователя' => 'Enter the username', +'Имя пользователя должно содержать от 2 до 32 символов!' => 'The username must be 2 to 32 characters!', +'Укажите допустимый IP!' => 'Enter a valid IP!', +'Укажите допустимую стоимость!' => 'Enter a valid price!', +'Описание должно содержать от 2 до 500 символов!' => 'The description must be 2 to 500 characters!', +'Введите название игры' => 'Enter the game name', +'Название игры должно содержать от 2 до 92 символов!' => 'The game name must be 2 to 92 characters!', +'Введите код игры' => 'Enter the game code', +'Код игры должен содержать от 2 до 8 символов!' => 'The game code must be 2 to 8 characters!', +'Название категории должно содержать от 2 до 32 символов!' => 'The category name must be 2 to 32 characters!', +'Введите название тарифа' => 'Enter the plan name', +'Введите стоимость тарифа' => 'Enter the plan price', +'Введите стоимость за один слот' => 'Enter the price per slot', +'Введите минимальное количество слотов для заказа' => 'Enter the minimum number of slots per order', +'Введите максимальное количество слотов для заказа' => 'Enter the maximum number of slots per order', +'Введите минимальный порт для заказа' => 'Enter the lowest port available for orders', +'Введите количество ядер на один сервер' => 'Enter the number of cores per server', +'Введите объем оперативной памяти на один сервер (в МБ)' => 'Enter the amount of RAM per server (MB)', +'Введите объем SSD на один сервер (в МБ)' => 'Enter the SSD size per server (MB)', +'Укажите допустимое количество ядер!' => 'Enter a valid number of cores!', +'Укажите допустимый объем оперативной памяти!' => 'Enter a valid amount of RAM!', +'Укажите допустимую объем жесткого диска!' => 'Enter a valid disk size!', +'Укажите допустимый Query-драйвер.' => 'Choose a valid query driver.', +'Введите IP (Который видит пользователь)' => 'Enter the IP shown to the user', +'Введите IP (По которому подключаются)' => 'Enter the IP used for connections', +'Введите IP (0.0.0.0)' => 'Enter the IP (0.0.0.0)', +'Введите URL (domain.ru)' => 'Enter the URL (domain.com)', +'Введите NS Сервера локации (ns1.domain.ru и ns2.domain.ru)' => 'Enter the location name servers (ns1.domain.com and ns2.domain.com)', +'Введите название шаблона в панели ISP' => 'Enter the template name in the ISP panel', + +// ── заголовки и меню админки ───────────────────────────────────────────────── +'Панель управления' => 'Control panel', +'Панель Администратора' => 'Administrator panel', +'Администратора' => 'Administrator', +'Администратор' => 'Administrator', +'Меню управления' => 'Management menu', +'Меню' => 'Menu', +'На главную' => 'Home', +'Контакты' => 'Contacts', +'Пользователей на сайте:' => 'Users online:', +'Система' => 'System', +'Настройки' => 'Settings', +'Игровые сервера' => 'Game servers', +'Игровых серверов' => 'Game servers', +'Доступных игр' => 'Games available', +'Пользователей' => 'Users', +'Пользователь' => 'User', +'Список пользователей' => 'User list', +'Все запросы' => 'All requests', +'Все категории' => 'All categories', +'Создать категорию' => 'New category', +'Написать клиенту' => 'Message the client', +'Платежи' => 'Payments', +'WEB сайты' => 'Websites', +'Рассылка E-mail' => 'E-mail campaign', +'Операции пользователей' => 'User transactions', +'Список новостей' => 'News list', +'Добавить новость' => 'Add news', +'Промо коды' => 'Promo codes', +'Список промо кодов' => 'Promo code list', +'Добавить промо код' => 'Add promo code', +'Промо' => 'Promo', +'Игры' => 'Games', +'Список игр' => 'Game list', +'Добавить игру' => 'Add game', +'Game локации' => 'Game locations', +'Список локаций' => 'Location list', +'Добавить локацию' => 'Add location', +'WEB Тарифы' => 'Web plans', +'WEB Локации' => 'Web locations', +'Дата регистрации' => 'Registration date', +'Статус активации' => 'Activation status', +'Информация и бонусы' => 'Information and bonuses', +'Статистика хостинга' => 'Hosting statistics', + +// ── пустые состояния админки ───────────────────────────────────────────────── +'На данный момент у вас нет серверов.' => 'You have no servers yet.', +'На данный момент у вас нет веб-хостингов.' => 'You have no web hosting yet.', +'На данный момент нет игровых серверов.' => 'There are no game servers yet.', +'На данный момент нет локаций.' => 'There are no locations yet.', +'На данный момент нет операций.' => 'There are no transactions yet.', +'На данный момент в тех.поддержку нет запросов.' => 'There are no support requests at the moment.', +'Заблокировано:' => 'Suspended:', +'Всего:' => 'Total:', +'Новых запросов:' => 'New requests:', +'Запросов:' => 'Requests:', +'Онлайн:' => 'Online:', +'Серверов:' => 'Servers:', +'Последний счет:' => 'Last invoice:', +'Заработано:' => 'Earned:', +'Обновлено в' => 'Updated at', +'Активен' => 'Active', +'Подтвержден' => 'Confirmed', +'Установлен' => 'Installed', +'Код' => 'Code', +'Слоты' => 'Slots', +'Порты' => 'Ports', +'Цена за слот' => 'Price per slot', + +// ── настройки: вкладки и подписи ───────────────────────────────────────────── +'Общие настройки' => 'General settings', +'Платежные системы' => 'Payment gateways', +'Прочие настройки' => 'Other settings', +'Применить настройки' => 'Apply settings', +'URL сайта' => 'Site URL', +'Название сайта' => 'Site name', +'Описание сайта' => 'Site description', +'Ключевые слова' => 'Keywords', +'Имя отправителя' => 'Sender name', +'E-mail Службы поддержки' => 'Support e-mail', +'ID Вашей группы ВКонтакте' => 'Your VK group ID', +'Cсылка на ваш логотип' => 'Logo URL', +'Введите URL сайта' => 'Enter the site URL', +'Введите название сайта' => 'Enter the site name', +'Введите описание сайта' => 'Enter the site description', +'Введите ключевые слова' => 'Enter the keywords', +'Введите имя отправителя' => 'Enter the sender name', +'Введите E-mail Службы поддержки' => 'Enter the support e-mail', +'Введите ID Вашей группы ВКонтакте' => 'Enter your VK group ID', +'Введите ссылку на ваш логотип' => 'Enter the logo URL', +'Введите секретный ключ' => 'Enter the secret key', +'Введите секретный ключ №1' => 'Enter secret key no. 1', +'Введите секретный ключ №2' => 'Enter secret key no. 2', +'Введите процент' => 'Enter the percentage', +'Введите номер' => 'Enter the number', +'Введите URL' => 'Enter the URL', +'Введите Token' => 'Enter the token', +'Введите тему' => 'Enter the subject', +'Сообщение...' => 'Message…', +'Веб.хостинг' => 'Web hosting', +'Модуль быстрой оплаты' => 'Quick payment module', +'Платежная система быстрой оплаты'=> 'Quick payment gateway', +'Яндекс деньги' => 'YooMoney', +'Номер счета' => 'Account number', +'Секретный пароль' => 'Secret password', +'Дополнительный ключ' => 'Additional key', +'ID проекта' => 'Project ID', +'Unitpay секретный ключ' => 'Unitpay secret key', +'Токен' => 'Token', +'Публичный ключ' => 'Public key', +'Введите публичный ключ' => 'Enter the public key', +'Настройки формы Qiwi' => 'Qiwi form settings', +'Включены' => 'Enabled', +'Выключены' => 'Disabled', +'Код темы (themeCode)' => 'Theme code (themeCode)', +'Введите код темы (themeCode)' => 'Enter the theme code (themeCode)', +'Тестовый период' => 'Trial period', +'Заказ запрещен' => 'Ordering disabled', +'Заказ разрешен (по одобрению)' => 'Ordering allowed (with approval)', +'Подтверждение E-mail' => 'E-mail confirmation', +'Тех.работы' => 'Maintenance', +'Хостинг доступен' => 'Hosting available', +'Хостинг закрыт на тех.работы' => 'Hosting closed for maintenance', +'Статус тех.работ' => 'Maintenance status', +'Сообщение тех.работ' => 'Maintenance message', +'Введите сообщение тех.работ' => 'Enter the maintenance message', +'Хостинг временно закрыт на обслуживание!' => 'The hosting is temporarily closed for maintenance!', +'ID приложения' => 'Application ID', +'Введите ID приложения' => 'Enter the application ID', +'Статус авторизаци' => 'Login status', +'Статус бота' => 'Bot status', +'Стоимость смены порта' => 'Port change price', +'Введите стоимость смены порта' => 'Enter the port change price', +'Конвертация бонусов' => 'Bonus conversion', +'Процент реферальной системы (за заказ услуг)' => 'Referral percentage (on service orders)', +'Процент бонусов за пополнение' => 'Bonus percentage on top-ups', +'Контактый номер' => 'Contact number', +'Введите минимальный бонус' => 'Enter the minimum bonus', +'Введите максимальный бонус' => 'Enter the maximum bonus', +'Введите ID пользователя' => 'Enter the user ID', +'Введите login' => 'Enter the login', +'Введите pass' => 'Enter the password', + +// ── капча в настройках ─────────────────────────────────────────────────────── +'Защита от ботов (reCAPTCHA v2)' => 'Bot protection (reCAPTCHA v2)', +'Выключена (капчи нет на сайте)' => 'Disabled (no captcha on the site)', +'Включена (нужны ключи ниже)' => 'Enabled (requires the keys below)', +'Капча при входе, регистрации, восстановлении пароля и в тикетах. Включайте только после того, как вписали оба ключа.' => 'Captcha on login, registration, password recovery and tickets. Only enable it after both keys are filled in.', +'Site key (публичный ключ) — из панели google.com/recaptcha, тип reCAPTCHA v2 «Я не робот»' => 'Site key (public key) — from google.com/recaptcha, reCAPTCHA v2 "I am not a robot"', +'Secret key (секретный ключ) того же сайта' => 'Secret key for the same site', + +// ── акции ──────────────────────────────────────────────────────────────────── +'Проведение акции' => 'Run a promotion', +'Провести акцию' => 'Run promotion', +'Начать акцию' => 'Start promotion', +'Раздача реальных денег' => 'Give away real money', +'Выдать только 1 пользователю' => 'Give to one user only', +'Создавать новость об проведённом розыгрыше' => 'Create a news post about the giveaway', + +// ── проверка системы ───────────────────────────────────────────────────────── +'Функция' => 'Feature', +'Установка' => 'Installation', +'Работоспособность бд для сервера.'=> 'Database support for servers.', +'Подключение по ssh.' => 'SSH connectivity.', +'Просмотр каптчи.' => 'Captcha rendering.', +'Кэширование памяти.' => 'Memory caching.', +'Время' => 'Time', +'Ссылка для браузера' => 'Browser link', +'1 раз в 00:00' => 'daily at 00:00', +'1 раз в минуту' => 'every minute', +'1 раз в 10 минут' => 'every 10 minutes', +'1 раз в 30 минут' => 'every 30 minutes', +'1 раз в час' => 'hourly', +'1 раз в неделю' => 'weekly', + +// ── прочее, найденное при проверке страниц ────────────────────────────────── +'У вас ещё нет аккаунта?' => 'Do not have an account yet?', +'Забыли пароль?' => 'Forgot your password?', +'Восстановить пароль' => 'Reset password', +'Введите Ваше Имя' => 'Enter your first name', +'Введите Вашу Фамилию' => 'Enter your last name', +'Введите Ваш E-Mail' => 'Enter your e-mail', +'Введите Пароль' => 'Enter a password', +'Повторите Пароль' => 'Repeat the password', +'Без приглашения' => 'No invitation', +'Введите ваше имя..' => 'Enter your first name…', +'Введите вашу фамилию..' => 'Enter your last name…', +'Введите ваш email..' => 'Enter your e-mail…', +'Вопрос' => 'Question', +'Проблемы с сервером' => 'Problems with a server', +'Проблемы с аккаунтом' => 'Problems with an account', +'Сотрудничество' => 'Partnership', +'Наша техническая поддержка работает' => 'Our technical support works', +'дней в году. Мы готовы помочь вам в любое время суток.' => 'days a year. We are ready to help you at any hour.', + +// ── найдено на второй проверке страниц ────────────────────────────────────── +'Панель' => 'Panel', +'Вход в аккаунт' => 'Account login', +'Попытка входа в аккаунт' => 'Account login attempt', +'в' => 'at', + +); diff --git a/panel/application/lang/ru.php b/panel/application/lang/ru.php new file mode 100644 index 0000000..47a02a5 --- /dev/null +++ b/panel/application/lang/ru.php @@ -0,0 +1,18 @@ +
+ + + +
+ + available() as $__i => $__c): ?>·name($__c), ENT_QUOTES, 'UTF-8') ?> + +
diff --git a/panel/engine/main/lang.php b/panel/engine/main/lang.php new file mode 100644 index 0000000..f0abf10 --- /dev/null +++ b/panel/engine/main/lang.php @@ -0,0 +1,259 @@ +code = $this->detect($registry); + + // Русский — исходный язык шаблонов, словарь ему не нужен. + if ($this->code !== 'ru') { + $file = APPLICATION_DIR . 'lang/' . $this->code . '.php'; + if (is_readable($file)) { + $map = include($file); + if (is_array($map)) { + // пустые переводы отбрасываем, иначе текст пропал бы со страницы + foreach ($map as $from => $to) { + if (is_string($from) && is_string($to) && $from !== '' && $to !== '') { + $this->map[$from] = $to; + } + } + } + } + } + self::$instance = $this; + } + + public static function instance() { + return self::$instance; + } + + public function code() { + return $this->code; + } + + public function available() { + return self::AVAILABLE; + } + + /** Человеческое имя языка для переключателя. */ + public function name($code) { + $names = array('ru' => 'Русский', 'en' => 'English'); + return isset($names[$code]) ? $names[$code] : strtoupper($code); + } + + /** Адрес текущей страницы с другим языком — для ссылок переключателя. */ + public function switchUrl($code) { + $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + $parts = explode('#', $uri, 2); + $uri = $parts[0]; + $parts = explode('?', $uri, 2); + $path = $parts[0]; + $query = array(); + if (isset($parts[1]) && $parts[1] !== '') { + parse_str($parts[1], $query); + } + $query['lang'] = $code; + return $path . '?' . http_build_query($query); + } + + private function detect($registry) { + // 1. явный выбор + if (isset($_GET['lang']) && in_array($_GET['lang'], self::AVAILABLE, true)) { + $code = $_GET['lang']; + if (!headers_sent()) { + setcookie('lang', $code, time() + 31536000, '/'); + } + $_COOKIE['lang'] = $code; + return $code; + } + + // 2. прошлый выбор + if (isset($_COOKIE['lang']) && in_array($_COOKIE['lang'], self::AVAILABLE, true)) { + return $_COOKIE['lang']; + } + + // 3. автоопределение по браузеру + if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + foreach (self::parseAccept($_SERVER['HTTP_ACCEPT_LANGUAGE']) as $tag) { + $short = strtolower(substr($tag, 0, 2)); + if (in_array($short, self::$ruFamily, true)) { + return 'ru'; + } + if (in_array($short, self::AVAILABLE, true)) { + return $short; + } + } + // браузер просит язык, которого у нас нет и который не русскоязычный + return 'en'; + } + + // 4. значение по умолчанию из конфигурации + if ($registry && isset($registry->config)) { + $default = $registry->config->lang; + if ($default && in_array($default, self::AVAILABLE, true)) { + return $default; + } + } + return 'ru'; + } + + /** Разбор Accept-Language в список тегов по убыванию веса q. */ + private static function parseAccept($header) { + $tags = array(); + foreach (explode(',', $header) as $chunk) { + $chunk = trim($chunk); + if ($chunk === '') { continue; } + $q = 1.0; + if (strpos($chunk, ';') !== false) { + list($chunk, $params) = explode(';', $chunk, 2); + if (preg_match('~q\s*=\s*([0-9.]+)~i', $params, $m)) { + $q = (float)$m[1]; + } + $chunk = trim($chunk); + } + if ($chunk !== '' && $chunk !== '*') { + $tags[] = array($chunk, $q); + } + } + usort($tags, function($a, $b) { + if ($a[1] == $b[1]) { return 0; } + return ($a[1] < $b[1]) ? 1 : -1; + }); + $out = array(); + foreach ($tags as $t) { $out[] = $t[0]; } + return $out; + } + + /** Перевод одной строки. Нет в словаре — возвращаем как есть. */ + public function get($text) { + return isset($this->map[$text]) ? $this->map[$text] : $text; + } + + /** + * Коллбэк для ob_start: переводит весь ответ перед отправкой. + * Ответы AJAX обрабатываются отдельно: json_encode экранирует кириллицу + * в \uXXXX, и словарь по сырому UTF-8 туда бы не попал. + */ + public function apply($buffer) { + if ($this->code === 'ru' || !$this->map || !is_string($buffer) || $buffer === '') { + return $buffer; + } + + $head = ltrim($buffer); + if ($head !== '' && ($head[0] === '{' || $head[0] === '[')) { + $data = json_decode($head, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($data)) { + $json = json_encode($this->walk($data), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json !== false) { + return $json; + } + } + } + + return $this->replace($buffer); + } + + private function walk($value) { + if (is_array($value)) { + $out = array(); + foreach ($value as $k => $v) { $out[$k] = $this->walk($v); } + return $out; + } + return is_string($value) ? $this->replace($value) : $value; + } + + /** + * Замена по словарю ТОЛЬКО на границах слов. + * + * Простой strtr() здесь не годится: короткий ключ подменяется внутри более + * длинного слова и получается каша из двух языков — 'Мод' превращал + * «Модуль» в «Modуль», а 'день' делал из «деньги» «dayги». Поэтому вокруг + * каждого ключа требуем отсутствие буквы. Цифра слева разрешена намеренно: + * иначе «0руб.» осталось бы без перевода. + * + * Ключи отсортированы от длинных к коротким, чтобы «Сменить пароль» + * срабатывало раньше, чем «Пароль». + */ + private function replace($text) { + if ($this->patterns === null) { + $this->buildPatterns(); + } + foreach ($this->patterns as $pattern) { + $out = preg_replace_callback($pattern, array($this, 'hit'), $text); + if ($out !== null) { $text = $out; } + } + return $text; + } + + private function hit($m) { + return isset($this->map[$m[1]]) ? $this->map[$m[1]] : $m[1]; + } + + private function buildPatterns() { + $keys = array_keys($this->map); + // длинные первыми: иначе короткий ключ перехватит часть длинной фразы + usort($keys, function($a, $b) { + $la = mb_strlen($a, 'UTF-8'); $lb = mb_strlen($b, 'UTF-8'); + if ($la === $lb) { return strcmp($a, $b); } + return ($la < $lb) ? 1 : -1; + }); + $this->patterns = array(); + // режем на группы, чтобы не упереться в предел размера шаблона PCRE + foreach (array_chunk($keys, 150) as $chunk) { + $quoted = array(); + foreach ($chunk as $k) { $quoted[] = preg_quote($k, '~'); } + $this->patterns[] = '~(?get($text) : $text; + if (func_num_args() > 1) { + $args = func_get_args(); + $args[0] = $out; + $formatted = @call_user_func_array('sprintf', $args); + if (is_string($formatted) && $formatted !== '') { + return $formatted; + } + } + return $out; +} diff --git a/panel/index.php b/panel/index.php index deb0f0c..fbedb8e 100644 --- a/panel/index.php +++ b/panel/index.php @@ -12,6 +12,7 @@ require_once(ENGINE_DIR . 'main/model.php'); require_once(ENGINE_DIR . 'main/registry.php'); require_once(ENGINE_DIR . 'main/config.php'); +require_once(ENGINE_DIR . 'main/lang.php'); require_once(ENGINE_DIR . 'main/game_settings.php'); require_once(ENGINE_DIR . 'main/request.php'); require_once(ENGINE_DIR . 'main/session.php'); @@ -28,6 +29,12 @@ $registry = new Registry(); $config = new Config(); $registry->config = $config; +// Язык интерфейса. Перевод применяется к готовому ответу (см. engine/main/lang.php), +// поэтому весь вывод собирается в буфер — включая страницы, которые делают exit(). +$lang = new Lang($registry); +$registry->lang = $lang; +ob_start(array($lang, 'apply')); + $game_settings = new Game_settings(); $registry->game_settings = $game_settings; diff --git a/БЕЗОПАСНОСТЬ.md b/БЕЗОПАСНОСТЬ.md index 3f6f697..064f67a 100644 --- a/БЕЗОПАСНОСТЬ.md +++ b/БЕЗОПАСНОСТЬ.md @@ -1,3 +1,5 @@ +**Русский** · [English](SECURITY.en.md) + # Аудит безопасности: что нашли и что исправили Аудит исходников сборки HostinPL 5.6 («nulled»-копия). Проверялось: скрытые закладки и вебшеллы, diff --git a/ИЗМЕНЕНИЯ.md b/ИЗМЕНЕНИЯ.md index 8bcb041..0e50d22 100644 --- a/ИЗМЕНЕНИЯ.md +++ b/ИЗМЕНЕНИЯ.md @@ -1,3 +1,5 @@ +**Русский** · [English](CHANGES.en.md) + # Что изменено по сравнению с оригиналом Оригинал: **HostinPL 5.6** в виде «nulled»-сборки (форк `Xopowblu-4EJlOBEK/HostinPL-5.6`), @@ -153,7 +155,28 @@ Node.js берётся из NodeSource **по HTTPS с проверкой клю --- -## 8. Брендинг и даты +## 8. Языки интерфейса: русский и английский + +Оригинал был только на русском, весь текст вшит в шаблоны. В сборке появился слой перевода: + +* `engine/main/lang.php` — класс `Lang` и глобальный помощник `t()` +* `application/lang/ru.php`, `application/lang/en.php` — словари +* Порядок выбора языка: параметр `?lang=` → cookie `lang` → **заголовок браузера + `Accept-Language`** → значение по умолчанию из конфига +* Переключатель (RU / EN) в шапке кабинета, админки и в подвале страницы входа +* Фразы, которых нет в словаре, остаются на русском — из интерфейса ничего не может пропасть + +Перевод применяется к готовому ответу через `ob_start()`, поэтому 200 файлов шаблонов +переписывать не понадобилось, и покрытие сразу распространяется на админку и письма. +Замена работает только на границах слов: без этого короткий ключ портил длинные слова +(«Мод» превращал «Модуль» в «Modуль», «день» делал из «деньги» «dayги»). + +Полнота проверена обходом 20 разделов: непереведённых фраз не осталось, мешанины языков нет. +Подробности — в [ИНСТРУКЦИЯ.md](ИНСТРУКЦИЯ.md), раздел «Язык интерфейса». + +--- + +## 9. Брендинг и даты * Год в подвалах: `2020©` → `2026©` (`views/common/footer.php`, `views/common/loginheader.php`) * Название и описание — REDL.IO: `application/config.php` (`description`, `keywords`, @@ -169,7 +192,7 @@ Node.js берётся из NodeSource **по HTTPS с проверкой клю --- -## 9. phpMyAdmin +## 10. phpMyAdmin Панель ссылается на `/phpmyadmin` из админки. Оригинальный установщик ставил phpMyAdmin через Apache, что на nginx не работало. Теперь он ставится из репозитория дистрибутива и отдаётся diff --git a/ИНСТРУКЦИЯ.md b/ИНСТРУКЦИЯ.md index 8964caf..ad2eec7 100644 --- a/ИНСТРУКЦИЯ.md +++ b/ИНСТРУКЦИЯ.md @@ -1,3 +1,5 @@ +**Русский** · [English](GUIDE.en.md) + # Полная инструкция От пустого сервера до принимающего клиентов хостинга. @@ -12,10 +14,11 @@ 4. [Подключение ноды к панели](#4-подключение-ноды-к-панели) 5. [Сборки игр](#5-сборки-игр) 6. [Настройка панели](#6-настройка-панели) -7. [Капча](#7-капча) -8. [HTTPS и домен](#8-https-и-домен) -9. [Обслуживание](#9-обслуживание) -10. [Если что-то не работает](#10-если-что-то-не-работает) +7. [Язык интерфейса](#7-язык-интерфейса) +8. [Капча](#8-капча) +9. [HTTPS и домен](#9-https-и-домен) +10. [Обслуживание](#10-обслуживание) +11. [Если что-то не работает](#11-если-что-то-не-работает) --- @@ -219,7 +222,49 @@ Minecraft (Paper) — с `papermc.io`, RAGE:MP — с `rage.mp`, игры Steam --- -## 7. Капча +## 7. Язык интерфейса + +Панель говорит на **русском и английском**. Язык выбирается в таком порядке: + +1. `?lang=en` или `?lang=ru` в адресе — явный выбор, запоминается в cookie на год +2. cookie `lang` — прошлый выбор +3. **заголовок браузера `Accept-Language`** — автоопределение +4. параметр `lang` в `application/config.php` (по умолчанию `ru`) + +Автоопределение намеренно оставляет русский для русскоязычных локалей (ru, uk, be, kk и других) +и переключает на английский для всех остальных — то есть посетитель из Германии или Бразилии +сразу получает английский интерфейс, ничего не нажимая. + +**Переключатель** стоит в верхней панели кабинета и админки (кнопки `RU` / `EN` рядом с балансом) +и в подвале страницы входа. + +**Как это сделано.** Панель написана с русским текстом прямо в шаблонах — около 1500 строк в +200 файлах. Вместо того чтобы переписывать их все, перевод применяется к готовому ответу: +`index.php` оборачивает вывод в `ob_start()`, и перед отправкой браузеру строки заменяются по +словарю (`engine/main/lang.php`). Это разом покрывает всю панель, включая админку и письма, а +фразы, которых нет в словаре, просто остаются русскими — ничего не может пропасть с экрана. + +Замена работает **только на границах слов**, поэтому короткий ключ не может испортить слово +подлиннее (без этого «Мод» превращал «Модуль» в «Modуль»). + +**Как добавить свой язык**, например немецкий: + +```bash +cp panel/application/lang/en.php panel/application/lang/de.php +# перевести значения в de.php, затем добавить 'de' в Lang::AVAILABLE +# в panel/engine/main/lang.php +``` + +Полноту словаря мы проверяли обходом страниц: все 20 разделов, которые проходим, отдаются без +остатков русского текста. Если увидите непереведённую фразу — допишите её в +`application/lang/en.php`, ключ это русский текст ровно в том виде, в каком он на странице. + +Перевод включается только когда язык не русский; в русском режиме словарь даже не загружается, +поэтому накладных расходов нет. + +--- + +## 8. Капча По умолчанию **выключена** — регистрация и вход работают без неё, поля просто нет. @@ -246,7 +291,7 @@ sudo service php8.4-fpm restart # подставьте свою версию --- -## 8. HTTPS и домен +## 9. HTTPS и домен Панель ставится на HTTP. Для сертификата: @@ -269,7 +314,7 @@ sudo service php8.4-fpm restart --- -## 9. Обслуживание +## 10. Обслуживание **Резервные копии.** Сохраняйте базу и конфиг: @@ -303,7 +348,7 @@ sudo service php8.4-fpm restart --- -## 10. Если что-то не работает +## 11. Если что-то не работает **«Ошибка: Не удалось загрузить контроллер …»** — панель не нашла контроллер для этого адреса. Обычно это ссылка на то, чего нет: например `/phpmyadmin`, когда phpMyAdmin не установлен @@ -314,7 +359,7 @@ sudo service php8.4-fpm restart Частая причина: `short_open_tag = Off`. Панель использует короткие теги ``, установщик включает эту опцию сам, но при переустановке PHP её нужно вернуть. -**Не могу войти, на форме «Invalid site key»** — включена капча без ключей, см. раздел 7. +**Не могу войти, на форме «Invalid site key»** — включена капча без ключей, см. раздел 8. **Планировщик не работает** (статистика не обновляется, просроченные сервера не отключаются):