MODx и Vbulletin 3.8.x — мир, дружба, жвачка

    Что нам стоит дом построить?


    В наше время форумный движок vbulletin знаком многим. Кто-то его использует, кто-то тихо ненавидит, а кто-то выпрашивает у начальства финансы на покупку лицензии.
    И в силу его известности поддержка этого движка есть у многих CMS, да только вот у MODx я не нашел нормального плагина/сниппета, а существующие были достаточно сыры, чтобы их использовать.
    Дамы и господа, прошу любить и жаловать:
    Эксперимент был проведен на MODx версии 1.0.2 и vbulletin версии 3.8.1

    Тише едешь, дальше будешь


    Для начала нам необходимо подготовиться, и первым будет файл global.php форума.
    Я сделал так: скопировал его в global_modx.php (.../forum/global_modx.php), открыл для редактирования и удалил все после 891 строки (напоминаю, версия форума 3.8.1) — подключение стилей и т.д., т.е. последней строчкой в файле у меня вызов функции verify_ip_ban();

    Заметка: возникали проблемы с подключением global — он выполнял выход (die(); или exit();) посреди скрипта из-за каких-то своих внутренних проверок, поэтому советую делать все на тестовой версии для начала.

    Теперь, в файле index.php, святая святых MODx'са, вставляем такие волшебные строчки кода:

    $VBDIR = "../forum/"; //путь к форумной директории, лучше полный, а не относительный
    $CURDIR = getcwd();
    chdir($VBDIR);
    require_once($VBDIR."global_modx.php");
    chdir($CURDIR);

    Та-да! Уже сейчас MODx видит все форумные данные! В любом сниппете мы можем использовать (объявив его глобально) стандартный объект $vbulletin (командой print_r($vbulletin); можно увидеть все доступные поля, но их там ОЧЕНЬ много, да и скорее всего потребуется только print_r($vbulletin->userinfo);).

    Продолжим наши изыскания, господа


    Теперь нам надо внедрить вопрос авторизации на сайт. Создаем чанк forum_login_form, который скопирует стандартную форму входа с «булки»:

    <!-- login form -->
    <form action="[+forumLink+]login.php?do=login" method="post" onsubmit="md5hash(vb_login_password, vb_login_md5password, vb_login_md5password_utf, 0)">
    <script type="text/javascript" src="[+forumLink+]clientscript/vbulletin_md5.js?v=381"></script>
    <table cellpadding="0" cellspacing="3" border="0">
       <tr>
          <td><input type="text" value="Логин" name="vb_login_username" id="navbar_username" accesskey="u" tabindex="101"  onfocus="javascript: if (this.defaultValue == this.value) this.value = ''; else if (this.value == '') this.value = this.defaultValue;" onblur="javascript: if (this.defaultValue == this.value) this.value = ''; else if (this.value == '') this.value = this.defaultValue;" class="input" /></td>
          <td><input type="checkbox" name="cookieuser" value="1" tabindex="103" id="cb_cookieuser_navbar" accesskey="c" /></td>
          <td><label for="cb_cookieuser_navbar">Запомнить?</label></td>
       </tr>
       <tr>
           <td><input type="password" value="Пароль" name="vb_login_password" id="navbar_password" tabindex="102"  onfocus="javascript: if (this.defaultValue == this.value) this.value = ''; else if (this.value == '') this.value = this.defaultValue;" onblur="javascript: if (this.defaultValue == this.value) this.value = ''; else if (this.value == '') this.value = this.defaultValue;" class="input"/></td>
           <td></td>
           <td><input type="submit" class="button" value="Вход" tabindex="104" title="Введите ваше имя пользователя и пароль, чтобы войти, или нажмите кнопку 'Регистрация', чтобы зарегистрироваться." accesskey="s" /></td>
       </tr>
    </table>
    <input type="hidden" name="s" value="" />
    <input type="hidden" name="securitytoken" value="guest" />
    <input type="hidden" name="do" value="login" />
    <input type="hidden" name="vb_login_md5password" />
    <input type="hidden" name="vb_login_md5password_utf" />
    </form>
    <!-- / login form -->
    

    Обратите внимание на [+forumLink+], адрес будет подставляться при вызове сниппета для удобства управления.
    Заметка: во всяком случае, эта форма различна для разных стилей, поэтому удобнее всего вырезать ее из вашего форума и вставить в этот чанк.

    Далее, создаем чанк forum_login_logged, отображающийся залогиненым пользователям:
    <strong>Здравствуйте <a href="[+profile+]" style="white-space:nowrap;">[+loginName+]</a>! </strong><br />
    <a href="[+action+]" class="button">Выйти.</a>
    

    Угумс, сделали view, теперь сделаем controller и создадим сниппет login, которым в последствии заменим вызов стандартного модыксовского WebLogin:
    <?php
    global $vbulletin, $modx;
    
    $forumlink = (isset($forumlink)) ? $forumlink : 'http://PATH_TO_YOU_FORUM/'; //если не указан в параметре вызова сниппета адрес форума, то берем стандартный
    
    if ($vbulletin->userinfo['userid']==0) {  //проверка, авторизован ли пользователь
        //нет? отображаем форму входа
        echo $modx->parseChunk('forum_login_form', array('forumLink'=>$forumlink), '[+', '+]');
       // сделаем проверку, что пользователь авторизован на сайте, но на форуме - нет. и если так, то удалим сессию
       // todo: удалять только сессию ВЕБ-пользователя, не трогая МЕНЕДЖЕРСКУЮ сессию
        if (!empty($_SESSION['webInternalKey'])) {
            session_destroy(); 
            session_unset();
        }
    }
    else { //если пользователь авторизован на форуме - даем информацию модыксу
        
        //проверяем, создана ли учетная пользователя на сайте?
        $sql = "SELECT id FROM ".$modx->getFullTableName("web_users")." WHERE id='".$vbulletin->userinfo['userid']."'";
        $rs = $modx->db->query($sql);
        $limit = $modx->db->getRecordCount($rs);
    
        if($limit==0) { 
            // не знает модыкс такого пользователя
            //  создаем
            $sql = "INSERT INTO ".$modx->getFullTableName("web_users")." (id, username, password) 
                    VALUES(".$vbulletin->userinfo['userid'].", '".$vbulletin->userinfo['username']."', md5('empty'));";
            $rs = $modx->db->query($sql);      
            
            // сохраняем атрибуты пользователя
            $sql = "INSERT INTO ".$modx->getFullTableName("web_user_attributes")." (internalKey, fullname, email, zip, state, country) 
                    VALUES(".$vbulletin->userinfo['userid'].", '".$vbulletin->userinfo['username']."', '".$vbulletin->userinfo['email']."', '', '', '');";
            $rs = $modx->db->query($sql);
            $sql = "INSERT INTO ".$modx->getFullTableName("web_groups")." (webgroup, webuser) 
                    VALUES(2, ".$vbulletin->userinfo['userid'].");";
            $rs = $modx->db->query($sql);
            
            $modx->logEvent(998, 1, 'Создан аккаунт на сайте.', 'Создан аккаунт на сайте.', 'login snippet');
        }        
        
        if (!$modx->userLoggedIn()) {    //Теперь проверяем, если не авторизован пользователь
            //вносим данные форума в сессию модыкса
            $_SESSION['webShortname'] = $vbulletin->userinfo['username'];
            $_SESSION['webFullname'] = $vbulletin->userinfo['username'];
            $_SESSION['webEmail'] = $vbulletin->userinfo['email'];
            $_SESSION['webValidated'] = 1;
            $_SESSION['webInternalKey'] = $vbulletin->userinfo['userid'];
            $_SESSION['webValid'] = base64_encode($vbulletin->userinfo['password']);
            $_SESSION['webUser'] = base64_encode($vbulletin->userinfo['username']);
            $_SESSION['webFailedlogins'] = 0;
            $_SESSION['webLastlogin'] = $vbulletin->userinfo['lastactivity'];
            $_SESSION['webnrlogins'] = 0;
            $_SESSION['usertype'] = 'web'; 
            $_SESSION['webUserGroupNames'] = ''; // reset user group names
              
            // грубо говоря проверяем к каким группам документов пользователь имеет доступ,
            // и если их изменили уже авторизованному пользователю, последнему прийдется "перезайти"
            $dg='';
            $i=0;
            $tblug = $modx->getFullTableName("web_groups");
            $tbluga = $modx->getFullTableName("webgroup_access");
            $sql = "SELECT uga.documentgroup
                    FROM $tblug ug
                    INNER JOIN $tbluga uga ON uga.webgroup=ug.webgroup
                    WHERE ug.webuser =".$vbulletin->userinfo['userid'];
            $ds = $modx->db->query($sql);
            while ($row = $modx->db->getRow($ds,'num')) $dg[$i++]=$row[0];
            $_SESSION['webDocgroups'] = $dg;
        }
        //парсим форму "выхода"
        echo $modx->parseChunk('forum_login_logged', 
                array('action' => $forumlink.'login.php?do=logout&logouthash='.$vbulletin->userinfo['securitytoken'],
                      'profile' => $forumlink.'usercp.php',
                      'loginName' => $vbulletin->userinfo['username']),
                '[+',
                '+]'
             );
      
        //часть записывающая активность пользователя
        if (getenv("HTTP_CLIENT_IP")) $ip = getenv("HTTP_CLIENT_IP");
        else if(getenv("HTTP_X_FORWARDED_FOR")) $ip = getenv("HTTP_X_FORWARDED_FOR");
        else if(getenv("REMOTE_ADDR")) $ip = getenv("REMOTE_ADDR");
        else $ip = "UNKNOWN";$_SESSION['ip'] = $ip;
    
        $itemid = isset($_REQUEST['id']) ? $_REQUEST['id'] : 'NULL' ;$lasthittime = time();$a = 998;
        
        $sql = "REPLACE INTO ".$modx->getFullTableName("active_users")." (internalKey, username, lasthit, action, id, ip) values(-".$_SESSION['webInternalKey'].", '".$_SESSION['webShortname']."', '".$lasthittime."', '".$a."', ".$itemid.", '$ip')";
        $rs = $modx->db->query($sql);
    }
    ?>

    Код достаточно прокомментирован, вопросов быть не должно; запросы делал отдельно (а не через db->select, db->insert) по привычке и удобству для отладки.

    Еще чуть-чуть до полной эйфории


    Сделав все написанное выше, мы имеем:
    • Единую с форумом авторизацию — вход и выход
    • Доступ к форумным данным на любой странице сайта
    • Выполнив вход на сайте или на форуме авторизация сработает везде (с редиректом на просматриваемую страницу обратно)
    • Создание пользователей МОДх, т.е. он тоже распознает своих пользователей, что позволяет ограничивать доступ к документам и т.д.
    • Одинаковые ID пользователей на сайте/форуме
    • Уничтожение сессии авторизации на сайте, если нет авторизации на форуме


    И чтобы внедрить эту плюшку осталось в шаблонах сайта прописать вызов сниппета [[login]], после чего останется только наслаждаться проделанной работой. Один минус данного подхода: пока пользователь форума не зайдет на сайт ограничить его права доступа к страницам будет невозможно, если конечно не создать ручками в базе такого пользователя.

    В отличие от метода BanzaiTokyo, на основе которого начиналась разработка и от которого осталось только внедрение в index.php файла global на форуме, не требуется создавать продукт и модули к нему, для управления выходом с форума и регистрацией — все происходит автоматически непосредственно на сайте.

    28 июля 2010 UPD:
    Программисты, которые использовали данное решение на своих сайтах! ВНИМАНИЕ! Тут допущена уязвимость, позволяющая выполнять blind-SQL:
    $itemid = isset($_REQUEST['id'])? $_REQUEST['id']: 'NULL' ;$lasthittime = time();$a = 998;
    $sql = «REPLACE INTO ».$modx->getFullTableName(«active_users»)." (internalKey, username, lasthit, action, id, ip) values(-".$_SESSION['webInternalKey'].", '".$_SESSION['webShortname']."', '".$lasthittime."', '".$a."', ".$itemid.", '$ip')";
    $rs = $modx->db->query($sql);

    Если подставить в id определенный подзапрос, то можно выполнить операцию сравнения и легко сбрутфорсить любые данные. К сожалению через эту уязвимость меня и хакнули(( Решил просто:
    $itemid = isset($_REQUEST['id'])? (is_numeric($_REQUEST['id'])?$_REQUEST['id']:'NULL'): 'NULL' ;$lasthittime = time();$a = 998;
    $sql = «REPLACE INTO ».$modx->getFullTableName(«active_users»)." (internalKey, username, lasthit, action, id, ip) values(-".$_SESSION['webInternalKey'].", '".$_SESSION['webShortname']."', '".$lasthittime."', '".$a."', ".$itemid.", '$ip')";
    $rs = $modx->db->query($sql);
    Share post

    Comments 9

      +1
      Спасибо, полезно, добавил в закладки.
        +1
        Спасибо за решение задачи.
        Одним из минусов ModX мне кажется его невнятная документация и вялый набор сниппетов и плагинов на офф. сайте. Переходил на него с друпала, с его огромным складом плагинов, наработок и штучек, активно используемым сообществом. Надеюсь с ModX будет также хорошо в ближайшем будущем, ибо плюсов у него очень много, начиная с удобств при верстке, и заканчивая его гибкостью.
          +1
          Абсолютно согласен, но у MODx и требования к администратору сайта гораздо выше, ведь на друпале(да и других цмс) чтобы админить сайт не обязательно быть пхп-программистом, достаточно просто знать основы, ведь модули к ним идут как правило «безразборные», в то время как тут любой модуль разбирается на винтики достаточно просто.
            +1
            Вопрос в том, какой подход лучше, наверно) я бы хотел нечто среднее и, в тоже время, гибкое.
              +1
              ЗЫ. Также хочется разграничить сайто-строителя и админа/модера сайта. Если первый будет его строить через админку, то второй должен в той же админке материалом править, файлы грузить. И выходит все в одной куче, что не очень красиво, на самом деле.
                0
                MODx это в первую очередь SMF, а не CMS, при правильной настройке и организации контента, её так же легко админить как и множество других систем. Наоборот приятно что с MODx является именно фреймворком ;)
              0
              Полезная статья получилась, опубликуйте на community.modx-cms.ru/blog/addons/

              А как в быть с обновлениями версий форума?
                +1
                Тут острее стоит вопрос «как быть с обновлением МОДх», потому что форумной части здесь только: структура объекта $vbulletin->userinfo (какие поля будут хранить информацию) и файл global.php — как его заставить отработать без генерации заглавной страницы форума (тупо убрать весь вывод). Причем эта информация хорошо задокументирована на саппорте булки.

                А вот в различных версиях МОДх таблицы юзверей могут различаться, сессионные переменные тоже, и методы обработки… если честно не изучал этот вопрос, но вероятнее всего именно так, поэтому прийдется адаптировать сниппет именно под версию цмс, а не булки.
                  0
                  Пора уже под Революшн делать ;)

              Only users with full accounts can post comments. Log in, please.