Новости Joomla

Вышли релизы Joomla 6.1 и Joomla 5.4.5: новые возможности и стабильность

Релиз Joomla 6.1.0

Проект Joomla! объявил о доступности Joomla 6.1 [Nyota] — новой минорной версии шестой серии, а также о выпуске релиза исправлений ошибок Joomla 5.4.5. Релиз 6.1 приносит ряд долгожданных функций, повышающих удобство управления контентом и защиту от спама.

👩‍💻 Компонент "CS Афиши" для Joomla.

👩‍💻 Компонент "CS Афиши" для Joomla.

Расширение "CS Афиши" позволяет выводить список мероприятий, фильтровать их по датам, поиск по заголовкам и описанию.

В состав пакета расширений входят:
- Компонент "CS Афиши"
- Модуль "Календарь событий"
- Модуль "Предстоящие события"
- Библиотека "ImgResize"

Расширение "CS Афиши" позволяет выводить список мероприятий, фильтровать их по датам, поиск по заголовкам и описанию.

Модуль "Календарь событий" отображает предстоящие и прошедшие мероприятие на календаре, с отображением мероприятий на конкретную дату во всплывающем окне.

Модуль "Предстоящие события" показывает список предстоящий событий по порядку их наступления.

P.S. Расширение платное, но плата символическая, чисто для отработки приема платежей. Ключи без ограничения по времени, купившие сейчас - смогут обновляться без ограничений.

Разработчик - участник нашего сообщества Дмитрий Денисов (@codersite).

Страница расширения
Демо

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

creative3d

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

fsv

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

creative3d

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

creative3d

  • Захожу иногда
  • 205
  • 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

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

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

Автор sergspb

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

Автор homelux

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

Автор web1

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

Автор creative3d

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

Автор donaire

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