Pull to refresh

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

Reading time 8 min
Views 1.8K

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


В наше время форумный движок 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);
Tags:
Hubs:
+6
Comments 9
Comments Comments 9

Articles