Что нам стоит дом построить?
В наше время форумный движок 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);