Добавьте возможность устанавливать распакованные расширения

  • Автор темы Автор темы BAZAg
  • Дата начала Дата начала

BAZAg

Client
Регистрация
08.11.2015
Сообщения
2 394
Реакции
2 799
Баллы
113
Иногда бывает необходимость получить доступ к возможностям хрома от лица расширения (в моем случае убрать заголовок ответов CSP).
Но, расширения которые находятся в магазине Google не работают корректно (пробовал вот это).
В таком случае есть возможность попросить Chat-GPT и собрать нужное расширение с одной единственной функцией.
После чего если установить его в Зеннопостер - можно получить доступ к необходимому функционалу.

Но... В текущий момент установить расширение из исходных файлов возможности нет (или я её не нашел...).
Прошу добавить функционал позволяющий устанавливать в браузер Зеннопостера распакованное расширение (исходники).
 
Здравствуйте,

Файлы можно запаковать в .crх через сам Chrome и потом использовать в ZB и ZP.

Через сам Chrome (Самый быстрый):
  1. Открой Chrome и перейди по адресу chrome://extensions/.
  2. Включи «Режим разработчика» (Developer mode) в правом верхнем углу.
  3. Нажми кнопку «Упаковать расширение» (Pack extension).
  4. В поле «Корневой каталог расширения» выбери папку, где лежат твои файлы (manifest.json и остальные).
  5. Поле «Файл закрытого ключа» оставь пустым (при первой упаковке браузер создаст его сам).
  6. Нажми «Упаковать расширение».
 
  • Оценить
Реакции: BAZAg
Спасибо!
Выглядит вроде не сложно - прошел руками - отлично получилось.
Попробовал шаблон собрать (чтобы не помнить всю эту инструкцию).
В итоге - переключатель Режим разработчика - кликать наверно как поиск по картинке.
Кнопку Упаковать - также как поиск по картинке.
А вот выбрать папку - не знаю как установить...
По идее должно срабатывать что-то типа Выбрать файл для загрузки на сервер, но что-то не сработало.
Установить значение вручную в поле - как-то не выходит (разве что кликать по координатам - и тогда емуляцию клавиатуры делать что ли).
Потом по кнопке - точно также наверно только поиск по картинке.

В целом я понял как решать эту задачу руками.
А вот как решать её автоматически грамотно, чтобы не мучить себя - это пока не знаю.
Может где-то на форуме уже валяется какой-то шаблон, куда на вход папку - на выходе путь к файлу?
 
@BAZAg Как вариант, можно скачивать расширения с официальной страницы магазина Хром через расширение CRX Extractor/Downloader, там проще будет автоматизацию накинуть. Расширение может скачать и CRX и ZIP.
 
  • Оценить
Реакции: Dmitriy_Zenno и BAZAg
@BAZAg Как вариант, можно скачивать расширения с официальной страницы магазина Хром через расширение CRX Extractor/Downloader, там проще будет автоматизацию накинуть. Расширение может скачать и CRX и ZIP.
Тут дело больше в своих расширениях (те что GPT нагенерит)...

Ну, типа сгенировал расширение - установил - посмотрел что да как - изменил - установил - изменил - установил.
В итоге, как-то бы эту установку/сборку автоматизировать, чтобы не бегать постоянно руками.
 
  • Увидел
Реакции: Sergodjan
Тут дело больше в своих расширениях (те что GPT нагенерит)...
Возможно, GPT может быть сумеет сразу в ZIP сгенерить?
Если там нет каких-то подводных камней.
Или сумеет сконвертнуть из CRX в ZIP?
Перплексити обнадежил вроде, что можно сконвертнуть.

1788513824610.png
 
  • Оценить
Реакции: BAZAg
  • Оценить
Реакции: BAZAg
Просто регистрация в Перплексити нужна, бесплатная.
Теперь пропустило. Спасибо! Буду изучать!

Основные два метода от перплексити, которые ведут к переводу с CRX => ZIP.
А дальше ещё наверно пообщаться нужно, чтобы сделать обратно с ZIP => CRX (типа контрольную сумму дописывать).
C#:
Развернуть Свернуть Копировать
private static byte[] ExtractZipFromCrx(string crxPath)
        {
            byte[] data = File.ReadAllBytes(crxPath);

            if (data.Length < 16)
                throw new InvalidDataException("Файл слишком мал для CRX");

            // Проверка magic "Cr24"
            if (data[0] != 'C' || data[1] != 'r' || data[2] != '2' || data[3] != '4')
                throw new InvalidDataException("Неверный CRX: отсутствует магическая сигнатура 'Cr24'");

            uint version = BitConverter.ToUInt32(data, 4);

            int headerSize;

            if (version == 2)
            {
                uint pubkeyLen = BitConverter.ToUInt32(data, 8);
                uint sigLen = BitConverter.ToUInt32(data, 12);
                headerSize = 16 + (int)pubkeyLen + (int)sigLen;
            }
            else if (version == 3)
            {
                uint headerLen = BitConverter.ToUInt32(data, 8);
                headerSize = 12 + (int)headerLen;
            }
            else
            {
                throw new InvalidDataException($"Неподдерживаемая версия CRX: {version}");
            }

            if (headerSize >= data.Length)
                throw new InvalidDataException("Некорректный CRX: размер заголовка больше размера файла");

            byte[] zipData = new byte[data.Length - headerSize];
            Buffer.BlockCopy(data, headerSize, zipData, 0, zipData.Length);

            if (zipData.Length < 4)
                throw new InvalidDataException("CRX payload слишком мал для ZIP");

            bool isZip =
                (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x03 && zipData[3] == 0x04) ||
                (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x01 && zipData[3] == 0x02) ||
                (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x05 && zipData[3] == 0x06);

            if (!isZip)
                throw new InvalidDataException("CRX payload не похож на ZIP-архив");

            return zipData;
        }

        private static void ExtractZipToFolder(byte[] zipData, string folderPath)
        {
            string tempZip = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
            try
            {
                File.WriteAllBytes(tempZip, zipData);
                ZipFile.ExtractToDirectory(tempZip, folderPath, overwriteFiles: true);
            }
            finally
            {
                if (File.Exists(tempZip))
                    File.Delete(tempZip);
            }
        }
 
Последнее редактирование:
  • Оценить
Реакции: Dmitriy_Zenno и Sergodjan
Я выпросил у Перплексити двусторонний ковертер: CRX -> ZIP и ZIP -> CRX.
Скину код, проверил, работает. Но это winforms утилита, для ручной работы.
Приложил также скомпилированное в .exe.

C#:
Развернуть Свернуть Копировать
using System;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Windows.Forms;

namespace CrxConverterWinForms
{
    internal static class Program
    {
        [STAThread]
        private static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
    }

    public class MainForm : Form
    {
        private readonly RadioButton rbCrxToZip;
        private readonly RadioButton rbZipToCrx;

        private readonly TextBox txtCrxPath;
        private readonly Button btnBrowseCrx;
        private readonly TextBox txtZipPath;
        private readonly Button btnBrowseZip;
        private readonly TextBox txtFolderPath;
        private readonly Button btnBrowseFolder;

        private readonly Button btnConvert;
        private readonly Label lblStatus;

        public MainForm()
        {
            Text = "CRX ↔ ZIP Converter";
            Width = 750;
            Height = 340;
            StartPosition = FormStartPosition.CenterScreen;
            FormBorderStyle = FormBorderStyle.FixedSingle;
            MaximizeBox = false;

            int y = 15;
            int textBoxX = 80;
            int textBoxW = 510;
            int buttonX = 600;
            int buttonW = 120;

            // Режим работы
            var lblMode = new Label
            {
                Text = "Режим:",
                Location = new System.Drawing.Point(10, y),
                AutoSize = true
            };
            Controls.Add(lblMode);

            rbCrxToZip = new RadioButton
            {
                Text = "CRX → ZIP + папка",
                Location = new System.Drawing.Point(80, y),
                AutoSize = true,
                Checked = true
            };
            rbCrxToZip.CheckedChanged += RbMode_CheckedChanged;
            Controls.Add(rbCrxToZip);

            rbZipToCrx = new RadioButton
            {
                Text = "ZIP → CRX",
                Location = new System.Drawing.Point(260, y),
                AutoSize = true
            };
            rbZipToCrx.CheckedChanged += RbMode_CheckedChanged;
            Controls.Add(rbZipToCrx);

            y += 35;

            // CRX
            var lblCrx = new Label
            {
                Text = "CRX:",
                Location = new System.Drawing.Point(10, y),
                AutoSize = true
            };
            Controls.Add(lblCrx);

            txtCrxPath = new TextBox
            {
                Location = new System.Drawing.Point(textBoxX, y),
                Width = textBoxW
            };
            Controls.Add(txtCrxPath);

            btnBrowseCrx = new Button
            {
                Text = "Обзор...",
                Location = new System.Drawing.Point(buttonX, y),
                Width = buttonW
            };
            btnBrowseCrx.Click += BtnBrowseCrx_Click;
            Controls.Add(btnBrowseCrx);

            y += 35;

            // ZIP
            var lblZip = new Label
            {
                Text = "ZIP:",
                Location = new System.Drawing.Point(10, y),
                AutoSize = true
            };
            Controls.Add(lblZip);

            txtZipPath = new TextBox
            {
                Location = new System.Drawing.Point(textBoxX, y),
                Width = textBoxW
            };
            Controls.Add(txtZipPath);

            btnBrowseZip = new Button
            {
                Text = "Обзор...",
                Location = new System.Drawing.Point(buttonX, y),
                Width = buttonW
            };
            btnBrowseZip.Click += BtnBrowseZip_Click;
            Controls.Add(btnBrowseZip);

            y += 35;

            // Folder
            var lblFolder = new Label
            {
                Text = "Папка:",
                Location = new System.Drawing.Point(10, y),
                AutoSize = true
            };
            Controls.Add(lblFolder);

            txtFolderPath = new TextBox
            {
                Location = new System.Drawing.Point(textBoxX, y),
                Width = textBoxW
            };
            Controls.Add(txtFolderPath);

            btnBrowseFolder = new Button
            {
                Text = "Обзор...",
                Location = new System.Drawing.Point(buttonX, y),
                Width = buttonW
            };
            btnBrowseFolder.Click += BtnBrowseFolder_Click;
            Controls.Add(btnBrowseFolder);

            y += 45;

            // Convert
            btnConvert = new Button
            {
                Text = "Конвертировать",
                Location = new System.Drawing.Point(260, y),
                Width = 160,
                Height = 30
            };
            btnConvert.Click += BtnConvert_Click;
            Controls.Add(btnConvert);

            y += 45;

            // Status
            lblStatus = new Label
            {
                Location = new System.Drawing.Point(10, y),
                Width = Width - 40,
                Height = 20,
                AutoEllipsis = true
            };
            Controls.Add(lblStatus);

            UpdateControlsByMode();
        }

        private void RbMode_CheckedChanged(object? sender, EventArgs e)
        {
            UpdateControlsByMode();
        }

        private void UpdateControlsByMode()
        {
            bool crxToZip = rbCrxToZip.Checked;

            txtCrxPath.Enabled = crxToZip;
            btnBrowseCrx.Enabled = crxToZip;

            txtFolderPath.Enabled = crxToZip;
            btnBrowseFolder.Enabled = crxToZip;

            txtZipPath.Enabled = true;
            btnBrowseZip.Enabled = true;

            if (crxToZip)
            {
                // Подсказка: ZIP и папка используются
                lblStatus.Text = "Режим: CRX → ZIP + папка";
            }
            else
            {
                // Режим ZIP → CRX: используется только ZIP и CRX
                lblStatus.Text = "Режим: ZIP → CRX";
            }
        }

        private void BtnBrowseCrx_Click(object? sender, EventArgs e)
        {
            using var dlg = new OpenFileDialog
            {
                Filter = "CRX файлы|*.crx|Все файлы|*.*",
                Title = "Выберите CRX файл"
            };
            if (dlg.ShowDialog() == DialogResult.OK)
            {
                txtCrxPath.Text = dlg.FileName;

                if (rbCrxToZip.Checked)
                {
                    if (string.IsNullOrWhiteSpace(txtZipPath.Text))
                        txtZipPath.Text = Path.ChangeExtension(dlg.FileName, ".zip");

                    if (string.IsNullOrWhiteSpace(txtFolderPath.Text))
                    {
                        string baseName = Path.GetFileNameWithoutExtension(dlg.FileName);
                        string dir = Path.GetDirectoryName(dlg.FileName)!;
                        txtFolderPath.Text = Path.Combine(dir, baseName + "_extracted");
                    }
                }
            }
        }

        private void BtnBrowseZip_Click(object? sender, EventArgs e)
        {
            if (rbCrxToZip.Checked)
            {
                // Сохранение ZIP
                using var dlg = new SaveFileDialog
                {
                    Filter = "ZIP архив|*.zip|Все файлы|*.*",
                    Title = "Сохранить ZIP как",
                    FileName = Path.GetFileName(txtZipPath.Text)
                };
                if (dlg.ShowDialog() == DialogResult.OK)
                {
                    txtZipPath.Text = dlg.FileName;
                }
            }
            else
            {
                // Выбор исходного ZIP для упаковки в CRX
                using var dlg = new OpenFileDialog
                {
                    Filter = "ZIP архив|*.zip|Все файлы|*.*",
                    Title = "Выберите ZIP файл"
                };
                if (dlg.ShowDialog() == DialogResult.OK)
                {
                    txtZipPath.Text = dlg.FileName;

                    if (string.IsNullOrWhiteSpace(txtCrxPath.Text))
                    {
                        string baseName = Path.GetFileNameWithoutExtension(dlg.FileName);
                        string dir = Path.GetDirectoryName(dlg.FileName)!;
                        txtCrxPath.Text = Path.Combine(dir, baseName + ".crx");
                    }
                }
            }
        }

        private void BtnBrowseFolder_Click(object? sender, EventArgs e)
        {
            using var dlg = new FolderBrowserDialog
            {
                Description = "Выберите папку для распаковки",
                UseDescriptionForTitle = true
            };
            if (dlg.ShowDialog() == DialogResult.OK)
            {
                string crxName = Path.GetFileNameWithoutExtension(txtCrxPath.Text);
                if (string.IsNullOrWhiteSpace(crxName))
                    crxName = "extension";

                txtFolderPath.Text = Path.Combine(dlg.SelectedPath, crxName + "_extracted");
            }
        }

        private void BtnConvert_Click(object? sender, EventArgs e)
        {
            if (rbCrxToZip.Checked)
                ConvertCrxToZip();
            else
                ConvertZipToCrx();
        }

        private void ConvertCrxToZip()
        {
            string crxPath = txtCrxPath.Text.Trim();
            string zipPath = txtZipPath.Text.Trim();
            string folderPath = txtFolderPath.Text.Trim();

            if (string.IsNullOrEmpty(crxPath))
            {
                MessageBox.Show(this, "Укажите путь к CRX файлу.", "Ошибка",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            if (!File.Exists(crxPath))
            {
                MessageBox.Show(this, $"CRX файл не найден:\n{crxPath}", "Ошибка",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            if (string.IsNullOrEmpty(zipPath))
                zipPath = Path.ChangeExtension(crxPath, ".zip");

            if (string.IsNullOrEmpty(folderPath))
            {
                string baseName = Path.GetFileNameWithoutExtension(crxPath);
                string dir = Path.GetDirectoryName(crxPath)!;
                folderPath = Path.Combine(dir, baseName + "_extracted");
            }

            try
            {
                lblStatus.Text = "Чтение CRX...";
                Application.DoEvents();

                byte[] zipData = ExtractZipFromCrx(crxPath);

                lblStatus.Text = "Сохранение ZIP...";
                Application.DoEvents();

                File.WriteAllBytes(zipPath, zipData);

                lblStatus.Text = "Распаковка в папку...";
                Application.DoEvents();

                Directory.CreateDirectory(folderPath);
                ExtractZipToFolder(zipData, folderPath);

                lblStatus.Text = "Готово.";
                MessageBox.Show(this,
                    $"Конвертация завершена.\n\nZIP: {zipPath}\nПапка: {folderPath}",
                    "Успех",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                lblStatus.Text = "Ошибка.";
                MessageBox.Show(this,
                    $"Ошибка при конвертации:\n{ex.Message}",
                    "Ошибка",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
            }
        }

        private void ConvertZipToCrx()
        {
            string zipPath = txtZipPath.Text.Trim();
            string crxPath = txtCrxPath.Text.Trim();

            if (string.IsNullOrEmpty(zipPath))
            {
                MessageBox.Show(this, "Укажите путь к ZIP файлу.", "Ошибка",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            if (!File.Exists(zipPath))
            {
                MessageBox.Show(this, $"ZIP файл не найден:\n{zipPath}", "Ошибка",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            if (string.IsNullOrEmpty(crxPath))
            {
                string baseName = Path.GetFileNameWithoutExtension(zipPath);
                string dir = Path.GetDirectoryName(zipPath)!;
                crxPath = Path.Combine(dir, baseName + ".crx");
            }

            try
            {
                lblStatus.Text = "Чтение ZIP...";
                Application.DoEvents();

                byte[] zipData = File.ReadAllBytes(zipPath);

                // Быстрая проверка, что это ZIP
                if (zipData.Length < 4)
                    throw new InvalidDataException("ZIP файл слишком мал");

                bool isZip =
                    (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x03 && zipData[3] == 0x04) ||
                    (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x01 && zipData[3] == 0x02) ||
                    (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x05 && zipData[3] == 0x06);

                if (!isZip)
                    throw new InvalidDataException("Файл не похож на ZIP-архив");

                lblStatus.Text = "Создание CRX...";
                Application.DoEvents();

                byte[] crxData = CreateCrxFromZip(zipData);

                lblStatus.Text = "Сохранение CRX...";
                Application.DoEvents();

                File.WriteAllBytes(crxPath, crxData);

                lblStatus.Text = "Готово.";
                MessageBox.Show(this,
                    $"CRX создан:\n{crxPath}",
                    "Успех",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information);
            }
            catch (Exception ex)
            {
                lblStatus.Text = "Ошибка.";
                MessageBox.Show(this,
                    $"Ошибка при конвертации:\n{ex.Message}",
                    "Ошибка",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
            }
        }

        // ---------- CRX → ZIP ----------

        private static byte[] ExtractZipFromCrx(string crxPath)
        {
            byte[] data = File.ReadAllBytes(crxPath);

            if (data.Length < 16)
                throw new InvalidDataException("Файл слишком мал для CRX");

            if (data[0] != 'C' || data[1] != 'r' || data[2] != '2' || data[3] != '4')
                throw new InvalidDataException("Неверный CRX: отсутствует магическая сигнатура 'Cr24'");

            uint version = BitConverter.ToUInt32(data, 4);

            int headerSize;

            if (version == 2)
            {
                uint pubkeyLen = BitConverter.ToUInt32(data, 8);
                uint sigLen = BitConverter.ToUInt32(data, 12);
                headerSize = 16 + (int)pubkeyLen + (int)sigLen;
            }
            else if (version == 3)
            {
                uint headerLen = BitConverter.ToUInt32(data, 8);
                headerSize = 12 + (int)headerLen;
            }
            else
            {
                throw new InvalidDataException($"Неподдерживаемая версия CRX: {version}");
            }

            if (headerSize >= data.Length)
                throw new InvalidDataException("Некорректный CRX: размер заголовка больше размера файла");

            byte[] zipData = new byte[data.Length - headerSize];
            Buffer.BlockCopy(data, headerSize, zipData, 0, zipData.Length);

            if (zipData.Length < 4)
                throw new InvalidDataException("CRX payload слишком мал для ZIP");

            bool isZip =
                (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x03 && zipData[3] == 0x04) ||
                (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x01 && zipData[3] == 0x02) ||
                (zipData[0] == 'P' && zipData[1] == 'K' && zipData[2] == 0x05 && zipData[3] == 0x06);

            if (!isZip)
                throw new InvalidDataException("CRX payload не похож на ZIP-архив");

            return zipData;
        }

        private static void ExtractZipToFolder(byte[] zipData, string folderPath)
        {
            string tempZip = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip");
            try
            {
                File.WriteAllBytes(tempZip, zipData);
                ZipFile.ExtractToDirectory(tempZip, folderPath, overwriteFiles: true);
            }
            finally
            {
                if (File.Exists(tempZip))
                    File.Delete(tempZip);
            }
        }

        // ---------- ZIP → CRX (версия 2) ----------

        private static byte[] CreateCrxFromZip(byte[] zipData)
        {
            // Генерируем фиктивные ключ и подпись (по 512 байт, как в типичных CRX2)
            // Это позволяет браузеру принять CRX как «расширение без проверки подписи»
            // для локального использования (через загрузку unpacked / developer mode).
            using var rng = RandomNumberGenerator.Create();

            int keyLen = 512;
            int sigLen = 512;

            byte[] pubKey = new byte[keyLen];
            byte[] signature = new byte[sigLen];

            rng.GetBytes(pubKey);
            rng.GetBytes(signature);

            // CRX2 структура:
            // 4  - magic "Cr24"
            // 4  - version (2)
            // 4  - pubkey_len
            // 4  - sig_len
            // N  - pubkey
            // M  - signature
            // K  - ZIP data

            byte[] header = new byte[16 + pubKey.Length + signature.Length];

            // Magic
            header[0] = (byte)'C';
            header[1] = (byte)'r';
            header[2] = (byte)'2';
            header[3] = (byte)'4';

            // Version = 2
            BitConverter.GetBytes(2).CopyTo(header, 4);

            // pubkey_len
            BitConverter.GetBytes(pubKey.Length).CopyTo(header, 8);

            // sig_len
            BitConverter.GetBytes(signature.Length).CopyTo(header, 12);

            // pubkey + signature
            Buffer.BlockCopy(pubKey, 0, header, 16, pubKey.Length);
            Buffer.BlockCopy(signature, 0, header, 16 + pubKey.Length, signature.Length);

            byte[] crxData = new byte[header.Length + zipData.Length];
            Buffer.BlockCopy(header, 0, crxData, 0, header.Length);
            Buffer.BlockCopy(zipData, 0, crxData, header.Length, zipData.Length);

            return crxData;
        }
    }
}

1788523081126.png
 

Вложения

  • Огонь
Реакции: BAZAg
Я выпросил у Перплексити
Нам бы такого помощника в 2014 году - уверен, сейчас вместо форума отдыхали бы под пальмами...
Имею ввиду AI... Когда интернет был простым сам по себе, а знаний нехватало.
Сейчас знаний и инструментов хватает - но интернет настолько сложный, что не понятно что с ним делать.
 
  • Оценить
Реакции: Sergodjan
Нам бы такого помощника в 2014 году - уверен, сейчас вместо форума отдыхали бы под пальмами...
Имею ввиду AI... Когда интернет был простым сам по себе, а знаний нехватало.
Сейчас знаний и инструментов хватает - но интернет настолько сложный, что не понятно что с ним делать.
Дааа. Вспоминаются времена, когда дорвеи делали почти вручную и почти штучно.
Потом появились Доргены... Вот теперь AI... :) Что дальше будет? И интересно и страшно одновременно.
 
  • Оценить
Реакции: BAZAg
Последнее редактирование:
  • Оценить
Реакции: BAZAg

Кто просматривает тему: (Всего: 0, Пользователи: 0, Гости: 0)