Получил я задачу перенести форум одного сайта на Битрикс, форум этого сайта сделан на MyBB. Задача оказалась не самой простой, так как был выявлен ряд проблем:
1. Так как форум существует не первый год, его БД успела прилично прибавить в весе. Из-за этого скрипт переноса форума будет требовать приличного объема опрератиной памяти, да и по времени будет выполняться не одну минуту, а max_execution_time не каждый хостинг даст установить в нуль. Эта проблема решается добавлением пошаговости в скрипт. Основа для функционала пошаговости взята со статьи Дениса Шаромова "Боремся с вирусами" или "Мастер-класс пошаговых скриптов".
2. ID объектов форума в MyBB и новые id этих сущностей в битриксе не будут совпадать, поэтому привязки форума к группе форумов, топика к форуму и т.д. не получится указывать значением из БД исходного форума. Нужно, при переносе объекта, сохранять его id из MyBB в поле xml_id объекта в битриксе и по нему уже получать новый id объекта.
3. Пароли пользователей кодируются в MyBB не таким же алгоритмом как в битриксе, поэтому после переноса, пользователь не сможет авторизоваться в битриксе. Проблема решается обработчиком события OnBeforeUserLogin в файле init.php, который будет перед авторизацией пользователя проверять пароль старым алгоритмом.
Вроде все задачи описал, теперь сам код. Основная часть скрипта реализует пошаговость и переход от миграции одних объектов к другим:
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
while(ob_end_flush());
$APPLICATION->SetTitle("Миграция");
if ($_REQUEST['go'])
{
CModule::IncludeModule("forum");
// ошибки будут фиксироваться в лог файле и выводиться пользователю в iframe
define('LOG_FILENAME',$_SERVER["DOCUMENT_ROOT"].'/log.txt');
// $BREAK_POINT - метка в которой хранится позиция с которой
// будет начинаться следующий шаг минрации
if ($_REQUEST['break_point']) $BREAK_POINT = $_REQUEST['break_point'];
else @unlink(LOG_FILENAME);
if ($_REQUEST['START']) $START = $_REQUEST['START'];
// Функция миграции групп пользователей,
// пользователи выгружаются пока не будет импортирован последний
// На последнем шаге функции MigrateUserGroups устанавливается $START = 'Users'
// и скрипт переходит к функции MigrateUsers
if (!$START) MigrateUserGroups($BREAK_POINT);
if ($START == 'Users') MigrateUsers($BREAK_POINT);
if ($START == 'ForumGroups') MigrateForumGroups($BREAK_POINT);
if ($START == 'Forums') MigrateForums($BREAK_POINT);
if ($START == 'Topics') MigrateTopics($BREAK_POINT);
if ($START == 'Posts') MigratePosts($BREAK_POINT);
// Форма которая отпраляет запрос для запуска нового шага
// Отправка формы реализуется яваскриптом
// В запросе хранится метка break_point и метка START
if ($BREAK_POINT && !defined('END'))
{?>
<form method=post id=postform>
<input type=hidden name=go value=y>
<input type=hidden name=break_point value="<?=htmlspecialchars($BREAK_POINT)?>">
<input type=hidden name=START value="<?=htmlspecialchars($START)?>">
</form>
<?//текст показывает текущий статус импорта?>
Идет импорт ...<br>
<i><?=$TEXT?></i><br>
<script>window.setTimeout("document.getElementById('postform').submit()",150);</script><?
}
// iframe выводит содержимое файла log.txt
// который содержит ошибки возникающие в процессе выгрузки
$iframe = "<iframe src=/log.txt width=100% height=100%></iframe>";
if (file_exists(LOG_FILENAME))
echo $iframe;
}
else
{
global $DB;
// создаем таблицу для хранения данных авторизации пользователей,
// полученных методом кодирования паролей в MyBB(см. задача №3)
$DB -> Query("CREATE TABLE IF NOT EXISTS old_user (
LOGIN varchar(60) NOT NULL,
PASSWORD varchar(64) NOT NULL,
SALT varchar(64) NOT NULL,
PRIMARY KEY (LOGIN));"
);
// Эта форма запускает сам скрипт
echo"<form method=post>
<input type=submit name=go value='GO!'>
</form>";
}
Далее пройдемся по самим функциям переноса разных объектов. Начнем с групп пользователей. Тут все просто.
function MigrateUserGroups($ID)
{
global $BREAK_POINT, $START, $TEXT;
// подключение к БД вынес в отдельную функцию
$link = ConnectDb();
if (!$ID) $ID=0;
// один шаг = одна запись в из таблицы групп пользователей в MyBB
$step = 1;
// получаем одну запись из таблицы mybb_usergroups,
// где ID больше чем ID на котором завершился прошлый шаг выполнения этой функции
$query ="SELECT * FROM mybb_usergroups where gid > $ID LIMIT $step";
$resUserGroups = mysql_query($query , $link) or die("Invalid query: " . mysql_error());
// устанавливаем флаг окончания работы функции MigrateUserGroups в true
$bFinish = true;
$ObGroup = new CGroup;
while ($userGroup = mysql_fetch_assoc($resUserGroups))
{
// пока $bFinish!=true на следующем шаге будет выполняться MigrateUserGroups
$bFinish = false;
$arFields = Array(
"ACTIVE" => "Y",
"C_SORT" => 100,
"NAME" => $userGroup['title'],
"DESCRIPTION" => $userGroup['description'],
"STRING_ID" => $userGroup['gid']//сюда записываем id из старой БД(см. задача №2)
);
$GID = $ObGroup->Add($arFields);
if (strlen($ObGroup -> LAST_ERROR)>0)
{ //записываем в файл log.txt ошибки, которые будут выводится во фрейме
AddMessage2Log($ObGroup -> LAST_ERROR);
}
//запоминаем id группы с которого будет начинаться следующий шаг
$BREAK_POINT = $userGroup['gid'];
}
// Если заходили в цикл while ($userGroup = mysql_fetch_assoc($resUserGroups)),
// то $bFinish = false;
// Если не заходили в цикл, то группы закончились, $bFinish = true, переходим к функции MigrateUsers
if ($bFinish)
{
//если все группы пользователей импортированы,
//то переходим к выгрузке самих пользователей
$START = 'Users';
$BREAK_POINT = 0;
}
else
$TEXT = "Импортируется группа пользователей: $BREAK_POINT";
}
Далее пользователи:
function MigrateUsers($ID)
{
global $BREAK_POINT, $START, $TEXT, $DB;
$link = ConnectDb();
$ObUser = new CUser;
// один шаг = 100 записей
$step = 100;
$query ="SELECT * FROM mybb_users where uid > $ID LIMIT $step";
$resUsers = mysql_query($query , $link) or die("Invalid query: " . mysql_error());
$bFinish = true;
while ($user = mysql_fetch_assoc($resUsers))
{
$bFinish = false;
$avatarPath = substr($user['avatar'], 1, strlen($user['avatar']));
// получаем новые id групп пользователей по старому id в MyBB, хранящемуся в STRING_ID(см. задача №2)
$obGroups = CGroup::GetList(($by="ID"), ($order="desc"), array('STRING_ID' => $user['usergroup']));
while ($arGroup=$obGroups->GetNext()) :
$groupId[] = $arGroup['ID'];
endwhile;
$arFields = Array(
"XML_ID" => $user['uid'],//сюда записываем id из старой БД(см. задача №2)
"LOGIN" => $user['username'],
"PASSWORD" => $user['password'],
"CONFIRM_PASSWORD" => $user['password'],
"ACTIVE" => "Y",
"NAME" => $user['username'],
"SECOND_NAME" => '',
"LAST_NAME" => '',
"EMAIL" => $user['email'],
"LAST_LOGIN" => ConvertTimeStamp($user['lastactive'],"FULL"),
"LAST_ACTIVITY_DATE" => ConvertTimeStamp($user['lastactive'],"FULL"),
"LID" => 's1',
"GROUP_ID" => $groupId,//записываем актуальные id групп пользователей
"PERSONAL_ICQ" => !$user['icq'] ? '' : $user['icq'],
"PERSONAL_PHOTO" => CFile::MakeFileArray('/home/blablabla/'.$avatarPath),
);
$UID = $ObUser->Add($arFields);
//Если, при добавлении пользователя, возникла ошибка, то пишем её в log.txt
if (strlen($ObUser->LAST_ERROR)>0)
{
AddMessage2Log($ObUser->LAST_ERROR);
}
//иначе добавляем пользователя форума
else
{
$arFields = Array(
"USER_ID" => $UID,
"DESCRIPTION" => $user['usertitle'],
"IP_ADDRESS" => $user['lastip'],
"AVATAR" => CFile::MakeFileArray('/home/blablabla/'.$avatarPath),
"NUM_POSTS" => $user['postnum'],
"LAST_POST" => $user['lastpost'],
"LAST_VISIT" => ConvertTimeStamp($user['lastvisit'],"FULL"),
"DATE_REG" => ConvertTimeStamp($user['regdate'],"FULL"),
"REAL_IP_ADDRESS" => $user['lastip'],
"SIGNATURE" => $user['signature'],
);
if (!CForumUser::Add($arFields))
{
$e = $GLOBALS['APPLICATION']->GetException();
if ($e && $str = $e->GetString())
AddMessage2Log("Ошибка: ".$str);
else
AddMessage2Log("Неизвестная ошибка");
}
// Добавляем в таблицу old_user данные авторизации
//закодированные алгоритмом MyBB(см. задача № 3)
$DB->Insert("old_user",
array(
"LOGIN" => "'".$DB->ForSQL($user['username'])."'",
"PASSWORD" => "'".$DB->ForSQL($user['password'])."'",
"SALT" => "'".$DB->ForSQL($user['salt'])."'",
)
);
}
$BREAK_POINT = $user['uid'];
}
if ($bFinish)
{
$START = 'ForumGroups';
$BREAK_POINT = 0;
}
else
$TEXT = "Группы пользователей импортированы.<br>
Импортируется пользователь: $BREAK_POINT";
}
Перенос групп форумов:
function MigrateForumGroups($ID)
{
global $BREAK_POINT, $START, $TEXT;
$link = ConnectDb();
$step = 1;
// группы форумов и сами форумы хранятся в MyBB в одной таблице
// отличаются они значением в поле type, для групп форумов type = 'c'
$query = "SELECT * from mybb_forums where fid > $ID and type = 'c' LIMIT $step";
$resForumGroups = mysql_query($query , $link) or die("Invalid query: " . mysql_error());
$arSysLangs = array();
$arGroupForumsId = array();
// Получам языки сайта
$ObLangs = CLanguage::GetList($by="lid", $order="desc", Array());
while($lang = $ObLangs->Fetch())
{
$arSysLangs[] = $lang['LID'];
}
$bFinish = true;
while ($forumGroup = mysql_fetch_assoc($resForumGroups))
{
$bFinish = false;
for ($i = 0; $i<count($arSysLangs); $i++)
{
$arFields["LANG"][$i] = array(
"LID" => $arSysLangs[$i],
"NAME" => $forumGroup['name'],
"DESCRIPTION" => $forumGroup['description'],
);
$arFields["XML_ID"] = $forumGroup['fid'];
}
$FGID = CForumGroup::Add($arFields);
if (!$FGID)
{
$e = $GLOBALS['APPLICATION']->GetException();
if ($e && $str = $e->GetString())
AddMessage2Log("Ошибка: $str");
else
AddMessage2Log("Неизвестная ошибка");
}
$BREAK_POINT = $forumGroup['fid'];
}
if ($bFinish)
{
$START = 'Forums';
$BREAK_POINT = 0;
}
else
$TEXT = "Группы пользователей импортированы<br>
Пользователи импортированы<br>
Импортируется группа $BREAK_POINT форума";
}
Перенос форумов:
function MigrateForums($ID)
{
global $BREAK_POINT, $START, $TEXT;
$link = ConnectDb();
$step = 5;
// группы форумов и сами форумы хранятся в MyBB в одной таблице
// отличаются они значением в поле type, для форумов type = 'f'
$query = "SELECT * from mybb_forums where fid > $ID and type = 'f' LIMIT $step" ;
$resForums = mysql_query($query , $link) or die("Invalid query: " . mysql_error());
$Sites = CSite::GetList();
while ($Site = $Sites->Fetch())
{
$arSites[$Site['LID']] = '/forum/index.php?PAGE_NAME=message&FID=#FORUM_ID#&TID=#TOPIC_ID#&MID=#MESSAGE_ID#';
}
$bFinish = true;
while ($forum = mysql_fetch_array($resForums))
{
$bFinish = false;
// Получаем новые id групп пользователей, по старым, хранящимся в XML_ID (см. задача №2)
// Стандартно не работает фильтрация групп форумов по xml_id
// Нужно в \bitrix\modules\forum\classes\general\forum_new.php
// для функции CForumGroup::GetList(строка 2014) добавить case "XML_ID":
$obForumGroup = CForumGroup::GetList(array(), array('XML_ID' => $forum['pid']));
$forumGroup=$obForumGroup->GetNext();
$forumGroupId = $forumGroup['ID'];
$arFields = Array(
"ACTIVE" => "Y",
"NAME" => $forum['name'],
"DESCRIPTION" => $forum['description'],
"FORUM_GROUP_ID" => $forumGroupId,
"SITES" => $arSites,
"TOPICS" => $forum['threads'],
"POSTS" => $forum['posts'],
"LAST_POSTER_ID" => $forum['lastposteruid'],
"LAST_POSTER_NAME" => $forum['lastposter'],
"LAST_MESSAGE_ID" => $forum['lastpost'],
"XML_ID" => $forum['fid']
);
if (!CForumNew::Add($arFields))
{
$e = $GLOBALS['APPLICATION']->GetException();
if ($e && $str = $e->GetString())
AddMessage2Log("Ошибка: ".$str);
else
AddMessage2Log("Неизвестная ошибка");
}
$BREAK_POINT = $forum['fid'];
}
if ($bFinish)
{
$START = 'Topics';
$BREAK_POINT = 0;
}
else
$TEXT = "Группы пользователей импортированы<br>
Пользователи импортированы<br>
Группы форумов импортированы<br>
Импортируется форум: $BREAK_POINT";
}
function ConnectDb()
{
$link = mysql_connect('host', 'root', 'pwd', true);
mysql_query("SET NAMES 'utf-8'", $link);
$db_selected = mysql_select_db('my_db', $link);
if (!$db_selected)
{
echo('Ошибка подключения к БД');
die();
}
return ($link);
}
Теперь приведу обработчик OnBeforeUserLogin для реализации возможности авторизации пользователей с использованием пароля закодированного иным алгоритмом, нежели в битриксе. Обработчик взят со статьи Александра Корякина "Перенос пользователей из другой системы".
AddEventHandler("main", "OnBeforeUserLogin", "__beforeUserLogin");
function __beforeUserLogin($arParams)
{
global $DB;
$login = $DB->ForSQL($arParams["LOGIN"]);
$password = $arParams["PASSWORD"];
$from_old = "FROM old_user where LOGIN='$login';";
// Ищем пользователя с таким логином в "old_user"
$rsOLDUser = $DB->Query("SELECT * $from_old");
// Если такого логина нет, то пользователь новый, а не импортированный со старого форума
// в таком случае просто выходим из обработчика
if(!($arOLDUser = $rsOLDUser->GetNext()))
return true;
// Ищем пользователя с таким логином в базе Битрикса
$rsBXUser = CUser::GetByLogin($login);
if(!($arBXUser = $rsBXUser->GetNext()))
return true;
// Проверяем правильность присланного пароля
// алгоритмом старой системы
if($arOLDUser['PASSWORD'] != md5(md5($arOLDUser['SALT']).md5($arParams['PASSWORD'])))
return true;
// Обновляем пароль пользователя в базе Битрикса, если пароль введенный пользователем подходит
$USER = new CUser;
$bUbdate = $USER->Update($arBXUser["ID"], array("PASSWORD" => $password));
unset($USER);
if(!$bUbdate)
return true;
// А затем удаляем пользователя из "old_user"
$DB->Query("DELETE $from_old");
return true;
}
Вроде всё Исходники мигратора во вложении. Надеюсь кому-нибудь пригодится.
Группы на сайте создаются не только сотрудниками «1С-Битрикс», но и партнерами компании. Поэтому мнения участников групп могут не совпадать с позицией компании «1С-Битрикс».