Новости Joomla

‼️👩‍💻 Релиз безопасности Astroid 3.3.11 - шаблона-конструктора для Joomla.

‼️👩‍💻 Релиз безопасности Astroid 3.3.11 - шаблона-конструктора для Joomla.

14 часов назад (на момент публикации заметки) была обнаружена уязвимость в популярном шаблоне-конструкторе Astroid Framework. При атаке на сайт устанавливается бэкдор — системный плагин под названием plg_system_blpayload. Если вы обнаружили этот плагин на своем веб-сайте, значит, он скомпрометирован, и вам необходимо восстановить чистую резервную копию, созданную до установки плагина.

Уязвимость позволяет загружать файлы на сайт и в дальнейшем получить права администратора Joomla.
В рамках атаки (из-за которой и была обнаружена уязвимость) на сайт устанавливался плагин plg_system_blpayload, который при каждой загрузке страницы снаружи он скрытно связывается с (платформой для SEO, работающей на черном рынке (ссылку помещать не будем, просим поверить на слово). Получает список скрытых спам-ссылок (сайты азартных игр, фишинга, мошенничества), подобранный под ваш домен, затем внедряет эти ссылки в HTML-код вашей страницы непосредственно перед рендером - невидимые для посетителей, но полностью читаемые поисковыми роботами. Это называется "отравление SEO" ("отрпавление поисковой выдачи").

Однако, эта уязвимость может использоваться в других целях. Поэтому необходимо срочно проверить ваши сайты, где стоит Astroid Framework и обновить его до версии не ниже 3.3.11. Релиз безопасности выпущен 4 часа назад (на момент написания заметки).

Скачать релиз безопасности Astroid

@joomlafeed

0 Пользователей и 2 Гостей просматривают эту тему.
  • 4 Ответов
  • 991 Просмотров
*

creative3d

  • Захожу иногда
  • 203
  • 6 / 0
Здравствуйте. Завис на этом вопросе.
Проблемы: спамеры, соблюдение закона, отсутствие компонентов.
- Через обычную авторизацию спам-боты влегкую регистрируются. Нашел вариант с Akeeba SocialLogin + капча от Google - это должно решить проблему с ботами и спамерами. Но там получается только 3 зарубежных соцсети по хорошему. Нужна как минимум еще и ВК.
- Нужно соблюдать закон о персональных данных, а у меня сервер не в России. Подумал можно будет не личную информацию хранить, а только идентификаторы какие-нибудь, но вчера новость увидел:
"В России могут начать штрафовать владельцев сайтов за авторизацию с помощью иностранных сервисов. Соответствующий законопроект внесен в Госдуму, сообщили в соцсетях.
«Новая статья 13.53 КоАП устанавливает ответственность для владельцев сайтов за „неисполнение обязанности по проведению авторизации пользователей“», — пишет telegram-канал «Осторожно Новости». Отмечается, что за нарушение граждане могут получить штраф 10-20 тысяч рублей, а юрлица — 500-700 тысяч рублей. Согласно законодательству, как пишет канал, регистрироваться на российских сайтах можно через биометрию, телефон или сервисы, которыми владеют компании в России.
... два года назад в силу вступила норма, по которой авторизация пользователей, находящихся на территории России, возможна одним из следующих способов: с использованием номера мобильного телефона; с использованием ЕСИА или ЕБС; с использованием российского сервиса авторизации."
- получается мне надо уходить с зарубежного сервера на русский или есть какие-то варианты, чтобы не уходить? И как вообще по мнению "депутатов" люди не из России должны авторизовываться? т.е. а если человек из России, но захочет через зарубежную соцсеть войти, я у него паспорт должен проверять?
- Далее и отсутствие компонентов, как это всё реализовать на Joomla 5/6?
*

fsv

  • Живу я здесь
  • 2787
  • 413 / 2
Обычная форма регистрации. Переопределить макет, удалить поле с токеном, js-ом вставлять в форму это поле через 3-5 сек. Спамеры сразу все пропали. Это на клиентских сайтах как-то было.
Веб-разработка: заказ. Только новая разработка.
*

creative3d

  • Захожу иногда
  • 203
  • 6 / 0
Любопытно...
Я вот сейчас думаю, что вместе с ИИ получится расширить Akeeba SocialLogin, добавив туда ВК, как минимум. И, может быть, получится не хранить персональную информацию.
Update: глянул План интеграции VK ID, вряд ли его сделать частью Akeeba SocialLogin, надо свой плагин или компонент делать. В интернете кто-то уже сделал, за 5 тыс. продает. Но попозже поразбираюсь, может получится в итоге самому. А лучше если бы спецы подключились, важная задача для всех, общем-то.
« Последнее редактирование: 16.11.2025, 14:35:16 от creative3d »
*

creative3d

  • Захожу иногда
  • 203
  • 6 / 0
С ИИ написал авторизацию. Нужно в VK ID бизнес зарегистрироваться. Бизнесменом быть необязательно, сказали в поддержке, просто не будет расширенных функций.
Код
<!-- Place this code in your Joomla login template -->
<div class="auth-section" id="authSection">
  <button class="auth-btn" id="authLoginBtn">
    <svg class="auth-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
      <path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
    </svg>
    Вход
  </button>

  <div id="authLoggedIn" style="display:none; align-items:center; gap:8px;">
    <span id="authUserIcon" style="cursor:pointer; display:inline-flex; align-items:center;">
      <svg class="auth-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
        <path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/>
      </svg>
    </span>
    <button class="auth-btn auth-btn-logout" id="authLogoutBtn">
      <svg class="auth-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
        <path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/>
      </svg>
      Выход
    </button>
  </div>
</div>

<script>
(function() {
  'use strict';

  var loginBtn  = document.getElementById('authLoginBtn');
  var logoutBtn = document.getElementById('authLogoutBtn');

  // =============================================
  // 1. МОДАЛЬНОЕ ОКНО
  // =============================================

  var overlay = document.createElement('div');
  overlay.style.cssText = 'display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.6);z-index:2147483647;justify-content:center;align-items:center;font-family:Arial,sans-serif;';

  var modal = document.createElement('div');
  modal.style.cssText = 'background:#fff;border-radius:16px;padding:32px;max-width:400px;width:90%;position:relative;box-shadow:0 20px 60px rgba(0,0,0,0.3);text-align:center;';

  var closeBtn = document.createElement('button');
  closeBtn.innerHTML = '&times;';
  closeBtn.style.cssText = 'position:absolute;top:12px;right:16px;font-size:28px;cursor:pointer;color:#999;background:none;border:none;line-height:1;padding:4px 8px;z-index:10;';

  var title = document.createElement('div');
  title.style.cssText = 'font-size:20px;font-weight:600;margin-bottom:20px;color:#333;';
  title.textContent = 'Вход на сайт';

  var vkContainer = document.createElement('div');
  vkContainer.id = 'vkid-auth-box';
  vkContainer.style.cssText = 'min-height:44px;';

  var terms = document.createElement('div');
  terms.style.cssText = 'margin-top:16px;font-size:12px;color:#999;line-height:1.4;';
  terms.innerHTML = 'Нажимая «Войти», вы принимаете <a href="/privacy-policy" target="_blank" style="color:#4a76a8;text-decoration:underline;">условия использования сервиса</a>.';

  modal.appendChild(closeBtn);
  modal.appendChild(title);
  modal.appendChild(vkContainer);
  modal.appendChild(terms);
  overlay.appendChild(modal);
  document.body.appendChild(overlay);

  // =============================================
  // 2. ОТКРЫТИЕ / ЗАКРЫТИЕ
  // =============================================

  function openModal() {
    overlay.style.display = 'flex';
    document.body.style.overflow = 'hidden';
  }

  function closeModal() {
    overlay.style.display = 'none';
    document.body.style.overflow = '';
  }

  if (loginBtn) {
    loginBtn.addEventListener('click', openModal);
  }

  closeBtn.addEventListener('click', function(e) {
    e.stopPropagation();
    closeModal();
  });

  overlay.addEventListener('click', function(e) {
    if (e.target === overlay) closeModal();
  });

  document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape' && overlay.style.display === 'flex') closeModal();
  });

  // =============================================
  // 3. ПРОВЕРКА АВТОРИЗАЦИИ + ВЫХОД + ТУЛТИП
  // =============================================

  var loggedInBlock = document.getElementById('authLoggedIn');
  var isLoggedIn = (document.cookie.indexOf('joomla_user_state=logged_in') !== -1);

  if (isLoggedIn) {
    if (loginBtn)      loginBtn.style.display      = 'none';
    if (loggedInBlock)  loggedInBlock.style.display  = 'flex';

    var loginMatch = document.cookie.match(/joomla_user_login=([^;]+)/);
    var userLogin = loginMatch ? decodeURIComponent(loginMatch[1]) : '';

    var tip = document.createElement('div');
    tip.style.cssText = 'display:none;position:fixed;background:#f0f0f0;color:#333;padding:10px 14px;border-radius:8px;font-size:13px;z-index:2147483647;box-shadow:0 2px 12px rgba(0,0,0,0.2);font-family:Arial,sans-serif;';

    var tipLabel = document.createElement('div');
    tipLabel.style.cssText = 'margin-bottom:6px;color:#666;font-size:12px;';
    tipLabel.textContent = 'Ваш логин:';

    var tipRow = document.createElement('div');
    tipRow.style.cssText = 'display:flex;align-items:center;gap:8px;';

    var tipLogin = document.createElement('span');
    tipLogin.style.cssText = 'font-weight:600;font-size:14px;user-select:all;';
    tipLogin.textContent = userLogin;

    var tipCopy = document.createElement('button');
    tipCopy.style.cssText = 'background:#fff;border:1px solid #ccc;border-radius:4px;padding:3px 8px;font-size:12px;cursor:pointer;color:#333;white-space:nowrap;';
    tipCopy.textContent = 'Скопировать';

    tipRow.appendChild(tipLogin);
    tipRow.appendChild(tipCopy);
    tip.appendChild(tipLabel);
    tip.appendChild(tipRow);
    document.body.appendChild(tip);

    var tipOpen = false;
    var userIcon = document.getElementById('authUserIcon');

    function showTip() {
      var rect = userIcon.getBoundingClientRect();
      tip.style.display = 'block';
      var tipW = tip.offsetWidth;
      var left = rect.left + (rect.width / 2) - (tipW / 2);
      if (left + tipW > window.innerWidth - 8) left = window.innerWidth - tipW - 8;
      if (left < 8) left = 8;
      tip.style.left = left + 'px';
      tip.style.top  = (rect.bottom + 8) + 'px';
      tipOpen = true;
    }

    function hideTip() {
      tip.style.display = 'none';
      tipOpen = false;
      tipCopy.textContent = 'Скопировать';
    }

    if (userIcon) {
      userIcon.addEventListener('click', function(e) {
        e.stopPropagation();
        tipOpen ? hideTip() : showTip();
      });
    }

    tipCopy.addEventListener('click', function(e) {
      e.stopPropagation();
      if (navigator.clipboard) {
        navigator.clipboard.writeText(userLogin).then(function() {
          tipCopy.textContent = '✓ Скопировано';
          setTimeout(function() { tipCopy.textContent = 'Скопировать'; }, 2000);
        });
      } else {
        var ta = document.createElement('textarea');
        ta.value = userLogin;
        ta.style.cssText = 'position:fixed;opacity:0;';
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        document.body.removeChild(ta);
        tipCopy.textContent = '✓ Скопировано';
        setTimeout(function() { tipCopy.textContent = 'Скопировать'; }, 2000);
      }
    });

    tip.addEventListener('click', function(e) { e.stopPropagation(); });
    document.addEventListener('click', function() { if (tipOpen) hideTip(); });

    logoutBtn.addEventListener('click', function() {
      fetch('/index.php?option=com_ajax&plugin=vkid&group=system&format=raw&task=logout', {
        method: 'GET',
        credentials: 'same-origin'
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (data.success) {
          document.cookie = 'joomla_user_state=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
          document.cookie = 'joomla_user_login=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
          window.location.reload();
        }
      })
      .catch(function() {
        document.cookie = 'joomla_user_state=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
        document.cookie = 'joomla_user_login=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
        window.location.reload();
      });
    });

    return;
  }

  // =============================================
  // 4. VK ID SDK
  // =============================================

  var sdkScript = document.createElement('script');
  sdkScript.src = 'https://unpkg.com/@vkid/sdk@<3.0.0/dist-sdk/umd/index.js';
  sdkScript.onload = initVKID;
  sdkScript.onerror = function() { console.error('VK ID SDK: failed to load'); };
  document.head.appendChild(sdkScript);

  function initVKID() {
    if (!window.VKIDSDK) return;

    var VKID = window.VKIDSDK;
    var SITE_BASE = 'https://ваш-сайт.ru';
    var APP_ID = ваш app_id;

    fetch(SITE_BASE + '/index.php?option=com_ajax&plugin=vkid&group=system&format=raw&task=init', {
      method: 'GET',
      credentials: 'same-origin'
    })
    .then(function(r) { return r.json(); })
    .then(function(data) {
      if (!data.success) return;

      VKID.Config.init({
        app: APP_ID,
        redirectUrl: SITE_BASE + '/vkid-callback.php',
        responseMode: VKID.ConfigResponseMode.Redirect,
        source: VKID.ConfigSource.LOWCODE,
        scope: '',
        codeChallenge: data.code_challenge,
        codeChallengeMethod: 'S256',
        state: data.state,
      });

      var container = document.getElementById('vkid-auth-box');
      if (!container) return;

      var oneTap = new VKID.OneTap();
      oneTap.render({
        container: container,
        showAlternativeLogin: true,
        oauthList: [VKID.OAuthName.OK, VKID.OAuthName.MAIL],
        scheme: VKID.Scheme.LIGHT,
        lang: VKID.Languages.RUS,
        styles: { borderRadius: 8 }
      });
    })
    .catch(function(err) { console.error('VKID error:', err); });
  }

})();
</script>
Далее плагин со структурой:
Код
vkid.zip
├── vkid.xml
├── services/
│   └── provider.php
├── src/
│   └── Extension/
│       └── Vkid.php
└── language/
    ├── en-GB/
    │   └── plg_system_vkid.ini
    └── ru-RU/
        └── plg_system_vkid.ini
provider.php
Код
<?php

defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\Vkid\Extension\Vkid;

return new class implements ServiceProviderInterface
{
    public function register(Container $container): void
    {
        $container->set(
            PluginInterface::class,
            function (Container $container) {
                $plugin = new Vkid(
                    $container->get(DispatcherInterface::class),
                    (array) PluginHelper::getPlugin('system', 'vkid')
                );
                $plugin->setApplication(Factory::getApplication());

                return $plugin;
            }
        );
    }
};
Vkid.php
Код
<?php

namespace Joomla\Plugin\System\Vkid\Extension;

defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserHelper;
use Joomla\CMS\Http\HttpFactory;
use Joomla\Event\SubscriberInterface;

class Vkid extends CMSPlugin implements SubscriberInterface
{
    private const VK_AUTH_URL     = 'https://id.vk.ru/oauth2/auth';
    private const VK_USERINFO_URL = 'https://id.vk.ru/oauth2/user_info';
    private const SESSION_PREFIX  = 'plg_vkid_';
    private const ENCRYPT_KEY     = 'your_secret_key_2026';
    private const ENCRYPT_IV_SEED = 'your_iv_stable';

    public static function getSubscribedEvents(): array
    {
        return [
            'onAfterInitialise' => 'onAfterInitialise',
            'onAjaxVkid'        => 'onAjaxVkid',
        ];
    }

    public function onAfterInitialise(): void
    {
        try {
            $app  = $this->getApplication();
            $code = $app->getInput()->cookie->getString('vkid_code', '');

            if (empty($code)) {
                return;
            }

            $state    = $app->getInput()->cookie->getString('vkid_state', '');
            $deviceId = $app->getInput()->cookie->getString('vkid_device_id', '');

            setcookie('vkid_code', '', time() - 3600, '/');
            setcookie('vkid_state', '', time() - 3600, '/');
            setcookie('vkid_device_id', '', time() - 3600, '/');

            if (empty($state)) {
                return;
            }

            $this->processVkAuth($code, $state, $deviceId);

        } catch (\Exception $e) {
            $this->writeLog('ERROR: ' . $e->getMessage());
        }
    }

    public function onAjaxVkid(): void
    {
        $task = $this->getApplication()->getInput()->getCmd('task', '');

        try {
            if ($task === 'init') {
                $this->handleInit();
            } elseif ($task === 'logout') {
                $this->handleLogout();
            } else {
                $this->respondJson(['success' => false, 'error' => 'Invalid task']);
            }
        } catch (\Exception $e) {
            $this->writeLog('ERROR: ' . $e->getMessage());
            $this->respondJson(['success' => false, 'error' => $e->getMessage()]);
        }
    }

    private function handleInit(): void
    {
        $session = Factory::getApplication()->getSession();

        $codeVerifier  = $this->generateRandomString(64);
        $codeChallenge = $this->generateCodeChallenge($codeVerifier);
        $state         = $this->generateRandomString(43);

        $session->set(self::SESSION_PREFIX . 'code_verifier', $codeVerifier);
        $session->set(self::SESSION_PREFIX . 'state', $state);

        $this->respondJson([
            'success'        => true,
            'code_challenge' => $codeChallenge,
            'state'          => $state,
        ]);
    }

    private function handleLogout(): void
    {
        $app  = $this->getApplication();
        $user = $app->getIdentity();

        if ($user && !$user->guest) {
            $app->logout($user->id);
        }

        setcookie('joomla_user_state', '', time() - 3600, '/');
        setcookie('joomla_user_login', '', time() - 3600, '/');

        $this->respondJson(['success' => true]);
    }

    private function processVkAuth(string $code, string $state, string $deviceId): void
    {
        $app     = $this->getApplication();
        $session = $app->getSession();

        $codeVerifier = $session->get(self::SESSION_PREFIX . 'code_verifier', '');

        if (empty($codeVerifier)) {
            return;
        }

        $session->clear(self::SESSION_PREFIX . 'code_verifier');
        $session->clear(self::SESSION_PREFIX . 'state');

        $tokenData = $this->exchangeCodeForToken($code, $deviceId, $codeVerifier, $state);

        if (empty($tokenData['access_token'])) {
            return;
        }

        $userInfo = $this->getUserInfo($tokenData['access_token']);
        $vkUserId = $userInfo['user_id'] ?? '';

        if (empty($vkUserId)) {
            return;
        }

        $joomlaUser = $this->findOrCreateUser($vkUserId);

        $this->loginUser($joomlaUser);

        $cookieExpires = time() + 30 * 24 * 3600;

        setcookie('joomla_user_state', 'logged_in', [
            'expires'  => $cookieExpires,
            'path'     => '/',
            'secure'   => true,
            'httponly' => false,
            'samesite' => 'Lax',
        ]);

        setcookie('joomla_user_login', $joomlaUser->username, [
            'expires'  => $cookieExpires,
            'path'     => '/',
            'secure'   => true,
            'httponly' => false,
            'samesite' => 'Lax',
        ]);

        $newSessionId = $app->getSession()->getId();
        $cookieName   = $this->getSessionCookieName();

        if (!empty($cookieName)) {
            setcookie($cookieName, $newSessionId, [
                'expires'  => time() + 30 * 24 * 3600,
                'path'     => '/',
                'secure'   => true,
                'httponly' => true,
                'samesite' => 'Lax',
            ]);
        }

        $app->getSession()->close();
        header('Location: /');
        exit;
    }

    private function getSessionCookieName(): string
    {
        foreach ($_COOKIE as $name => $value) {
            if (preg_match('/^[a-f0-9]{32}$/', $name)) {
                return $name;
            }
        }
        return '';
    }

    private function exchangeCodeForToken(
        string $code,
        string $deviceId,
        string $codeVerifier,
        string $state
    ): array {
        $clientId    = $this->params->get('client_id', '');
        $siteDomain  = $this->params->get('site_domain', '');
        $redirectUri = 'https://' . $siteDomain . '/vkid-callback.php';

        $http     = HttpFactory::getHttp();
        $response = $http->post(
            self::VK_AUTH_URL,
            http_build_query([
                'grant_type'    => 'authorization_code',
                'code'          => $code,
                'code_verifier' => $codeVerifier,
                'client_id'     => $clientId,
                'device_id'     => $deviceId,
                'state'         => $state,
                'redirect_uri'  => $redirectUri,
            ]),
            ['Content-Type' => 'application/x-www-form-urlencoded']
        );

        $data = json_decode($response->body, true);
        return (!empty($data) && !isset($data['error'])) ? $data : [];
    }

    private function getUserInfo(string $accessToken): array
    {
        $http     = HttpFactory::getHttp();
        $response = $http->post(
            self::VK_USERINFO_URL,
            http_build_query([
                'client_id'    => $this->params->get('client_id', ''),
                'access_token' => $accessToken,
            ]),
            ['Content-Type' => 'application/x-www-form-urlencoded']
        );

        $data = json_decode($response->body, true);
        return $data['user'] ?? [];
    }

    private function encryptVkId(string $vkUserId): string
    {
        $key = hash('sha256', self::ENCRYPT_KEY, true);
        $iv  = substr(hash('sha256', self::ENCRYPT_IV_SEED, true), 0, 16);
        $encrypted = openssl_encrypt($vkUserId, 'aes-256-cbc', $key, 0, $iv);
        return rtrim(strtr($encrypted, '+/', '-_'), '=');
    }

    private function decryptVkId(string $encoded): string
    {
        $key    = hash('sha256', self::ENCRYPT_KEY, true);
        $iv     = substr(hash('sha256', self::ENCRYPT_IV_SEED, true), 0, 16);
        $len    = strlen($encoded);
        $padded = strtr($encoded, '-_', '+/') . str_repeat('=', (4 - $len % 4) % 4);
        $result = openssl_decrypt($padded, 'aes-256-cbc', $key, 0, $iv);
        return ($result !== false) ? $result : '';
    }

    private function findOrCreateUser(string $vkUserId): User
    {
        $hash       = $this->encryptVkId($vkUserId);
        $username   = 'u_' . $hash;
        $siteDomain = $this->params->get('site_domain', 'your.ru');

        $userId = UserHelper::getUserId($username);

        if ($userId) {
            return User::getInstance($userId);
        }

        $db       = Factory::getContainer()->get('DatabaseDriver');
        $password = UserHelper::hashPassword(UserHelper::genRandomPassword(32));
        $now      = Factory::getDate()->toSql();

        $query = $db->getQuery(true)
            ->insert($db->quoteName('#__users'))
            ->columns($db->quoteName(['name', 'username', 'email', 'password', 'block', 'registerDate', 'params']))
            ->values(implode(',', [
                $db->quote('Пользователь'),
                $db->quote($username),
                $db->quote($username . '@' . $siteDomain),
                $db->quote($password),
                0,
                $db->quote($now),
                $db->quote('{}'),
            ]));

        $db->setQuery($query);
        $db->execute();
        $newId = (int) $db->insertid();

        $query = $db->getQuery(true)
            ->insert($db->quoteName('#__user_usergroup_map'))
            ->columns([$db->quoteName('user_id'), $db->quoteName('group_id')])
            ->values($newId . ', 2');
        $db->setQuery($query);
        $db->execute();

        return User::getInstance($newId);
    }

    private function loginUser(User $user): void
    {
        $app = $this->getApplication();
        $db  = Factory::getContainer()->get('DatabaseDriver');

        if ($user->block) {
            return;
        }

        $tempPassword = bin2hex(random_bytes(16));
        $hash = UserHelper::hashPassword($tempPassword);

        $db->setQuery(
            $db->getQuery(true)
                ->update($db->quoteName('#__users'))
                ->set($db->quoteName('password') . ' = ' . $db->quote($hash))
                ->where($db->quoteName('id') . ' = ' . (int) $user->id)
        );
        $db->execute();

        $app->login(
            ['username' => $user->username, 'password' => $tempPassword],
            ['remember' => false]
        );

        $newHash = UserHelper::hashPassword(bin2hex(random_bytes(16)));
        $db->setQuery(
            $db->getQuery(true)
                ->update($db->quoteName('#__users'))
                ->set($db->quoteName('password') . ' = ' . $db->quote($newHash))
                ->where($db->quoteName('id') . ' = ' . (int) $user->id)
        );
        $db->execute();
    }

    private function generateCodeChallenge(string $v): string
    {
        return rtrim(strtr(base64_encode(hash('sha256', $v, true)), '+/', '-_'), '=');
    }

    private function generateRandomString(int $len = 64): string
    {
        $c = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
        $m = strlen($c) - 1;
        $r = '';
        for ($i = 0; $i < $len; $i++) {
            $r .= $c[random_int(0, $m)];
        }
        return $r;
    }

    private function respondJson(array $data): void
    {
        $app = $this->getApplication();
        $app->setHeader('Content-Type', 'application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE);
        $app->close();
    }

    private function writeLog(string $msg): void
    {
        $f = JPATH_ROOT . '/logs/vkid_debug.log';
        @file_put_contents($f, '[' . date('Y-m-d H:i:s') . '] ' . $msg . "\n", FILE_APPEND);
    }
}
plg_system_vkid.ini
Код
PLG_SYSTEM_VKID="System - VK ID Authentication"
PLG_SYSTEM_VKID_XML_DESCRIPTION="Authenticates users via VK ID (VKontakte, OK, Mail) for Joomla 5/6. Does not store personal data."

PLG_SYSTEM_VKID_CLIENT_ID="VK ID App ID"
PLG_SYSTEM_VKID_CLIENT_ID_DESC="Application ID from VK ID cabinet (id.vk.com)"
PLG_SYSTEM_VKID_CLIENT_SECRET="Client Secret"
PLG_SYSTEM_VKID_CLIENT_SECRET_DESC="Secret key from VK ID cabinet"
PLG_SYSTEM_VKID_SITE_DOMAIN="Site Domain"
PLG_SYSTEM_VKID_SITE_DOMAIN_DESC="Domain without http/https, e.g.: website.ru"
PLG_SYSTEM_VKID_REDIRECT="Redirect after login"
PLG_SYSTEM_VKID_REDIRECT_DESC="Path to redirect after successful login, e.g.: / or /profile"
PLG_SYSTEM_VKID_LOGGING="Debug Logging"
plg_system_vkid.ini
Код
PLG_SYSTEM_VKID="Система - Авторизация VK ID"
PLG_SYSTEM_VKID_XML_DESCRIPTION="Авторизация пользователей через VK ID (ВКонтакте, ОК, Mail) для Joomla 5/6. Персональные данные не сохраняются."

PLG_SYSTEM_VKID_CLIENT_ID="ID приложения VK ID"
PLG_SYSTEM_VKID_CLIENT_ID_DESC="ID приложения из кабинета VK ID (id.vk.com)"
PLG_SYSTEM_VKID_CLIENT_SECRET="Секретный ключ"
PLG_SYSTEM_VKID_CLIENT_SECRET_DESC="Секретный ключ из кабинета VK ID"
PLG_SYSTEM_VKID_SITE_DOMAIN="Домен сайта"
PLG_SYSTEM_VKID_SITE_DOMAIN_DESC="Домен без http/https, например: website.ru"
PLG_SYSTEM_VKID_REDIRECT="Редирект после входа"
PLG_SYSTEM_VKID_REDIRECT_DESC="Путь для перенаправления после успешного входа, например: / или /profile"
PLG_SYSTEM_VKID_LOGGING="Логирование"
vkid.xml
Код
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
    <name>plg_system_vkid</name>
    <version>1.0.0</version>
    <creationDate>2025-01-01</creationDate>
    <author>Lingust</author>
    <authorUrl>https://website.ru</authorUrl>
    <description>PLG_SYSTEM_VKID_XML_DESCRIPTION</description>
    <namespace path="src">Joomla\Plugin\System\Vkid</namespace>

    <files>
        <folder plugin="vkid">services</folder>
        <folder>src</folder>
    </files>

    <languages folder="language">
        <language tag="en-GB">en-GB/plg_system_vkid.ini</language>
        <language tag="ru-RU">ru-RU/plg_system_vkid.ini</language>
    </languages>

    <config>
        <fields name="params">
            <fieldset name="basic">
                <field
                    name="client_id"
                    type="text"
                    label="PLG_SYSTEM_VKID_CLIENT_ID"
                    description="PLG_SYSTEM_VKID_CLIENT_ID_DESC"
                    required="true"
                />

                <field
                    name="client_secret"
                    type="password"
                    label="PLG_SYSTEM_VKID_CLIENT_SECRET"
                    description="PLG_SYSTEM_VKID_CLIENT_SECRET_DESC"
                    required="true"
                />

                <field
                    name="site_domain"
                    type="text"
                    label="PLG_SYSTEM_VKID_SITE_DOMAIN"
                    description="PLG_SYSTEM_VKID_SITE_DOMAIN_DESC"
                    required="true"
                    hint="lingust.ru"
                />

                <field
                    name="redirect_after_login"
                    type="text"
                    label="PLG_SYSTEM_VKID_REDIRECT"
                    description="PLG_SYSTEM_VKID_REDIRECT_DESC"
                    default="/"
                />

                <field
                    name="enable_logging"
                    type="radio"
                    label="PLG_SYSTEM_VKID_LOGGING"
                    default="0"
                    layout="joomla.form.field.radio.switcher">
                    <option value="1">JYES</option>
                    <option value="0">JNO</option>
                </field>
            </fieldset>
        </fields>
    </config>
</extension>
В корень сайта помещаем vkid-callback.php:
Код
<?php
/**
 * VK ID OAuth2 Callback
 * Сохраняет данные в cookie и редиректит на главную.
 * Плагин подхватит при загрузке страницы.
 */

$code     = $_GET['code']      ?? '';
$state    = $_GET['state']     ?? '';
$deviceId = $_GET['device_id'] ?? '';

if (empty($code)) {
    header('Location: /');
    exit;
}

// Сохраняем в cookie (короткоживущие, httponly)
$opts = [
    'expires'  => time() + 120,  // 2 минуты
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
];

// Cookie для JavaScript — чтобы знал что пользователь залогинен
setcookie('joomla_user_state', 'logged_in', [
    'expires'  => time() + 30 * 24 * 3600,
    'path'     => '/',
    'secure'   => true,
    'httponly' => false,  // JavaScript должен читать!
    'samesite' => 'Lax',
]);

setcookie('vkid_code',      $code,     $opts);
setcookie('vkid_state',     $state,    $opts);
setcookie('vkid_device_id', $deviceId, $opts);

// Редирект на главную — плагин подхватит cookie
header('Location: /');
exit;
В приложении на сайте VK ID указываем Доверенный Redirect URL = https://ваш-сайт.ru/vkid-callback.php
« Последнее редактирование: 17.02.2026, 12:44:13 от creative3d »
*

creative3d

  • Захожу иногда
  • 203
  • 6 / 0
Выше подредактировал файлы. Проверил сейчас. Всё работает. Дальше я уже буду детали под себя менять.
Здесь нет хранения персональных данных и не нужно будет париться с ботами, они зарегистрироваться не смогут (вроде).
Чтобы оставить сообщение,
Вам необходимо Войти или Зарегистрироваться
 

Как сделать всплывающее (выдвигающееся) окно на Joomla 5?

Автор sergspb

Ответов: 4
Просмотров: 656
Последний ответ 02.12.2025, 15:03:06
от sergspb
Как правильно сделать цикл?

Автор homelux

Ответов: 1
Просмотров: 526
Последний ответ 28.11.2025, 10:52:55
от fbr
Верхнее меню в версии для ПК как сделать на весь экран?

Автор web1

Ответов: 0
Просмотров: 660
Последний ответ 23.10.2025, 19:52:02
от web1
Как правильно сделать faq?

Автор creative3d

Ответов: 4
Просмотров: 1167
Последний ответ 03.07.2025, 09:13:15
от creative3d
Как правильно создать структуру сайта?

Автор donaire

Ответов: 2
Просмотров: 1100
Последний ответ 11.02.2025, 23:29:46
от Tavol