CanUpdateDatabase()) { class CLearnInstall201203ConvertDBTimeOut extends Exception { } class CLearnInstall201203ConvertDBException extends Exception { } class CLearnInstall201203ConvertDB { const MODULE_ID = 'learning'; const OPTION_ID = '~LearnInstall201203ConvertDB::_IsAlreadyConverted'; // don't change this constant, NEVER! const STATUS_INSTALL_COMPLETE = '1'; const STATUS_INSTALL_NEVER_START = '2'; const STATUS_INSTALL_INCOMPLETE = '3'; const JOURNAL_STATUS_UNPROCESSED = -1; const JOURNAL_STATUS_COURSE_LINKED = 1; const JOURNAL_STATUS_CHAPTER_COPIED = 2; const JOURNAL_STATUS_LESSON_EDGES_CREATED = 3; public static function run() { global $DB; $msg = $step = false; $errorMessage = ''; // If data tables are not installed - nothing to do if ( ! $DB->TableExists('b_learn_lesson') ) return ($errorMessage); try { if ( ! self::IsNewRightsModelInitialized($step, $msg) ) { self::InitializeNewRightsModel(); $step = false; if ( ! self::IsNewRightsModelInitialized($step, $msg) ) { $errorMessage .= 'FAILED on step ' . $step . '; msg = ' . $msg . '.'; return ($errorMessage); // FATAL } } self::StartTransaction(); self::ReCreateTriggersForMSSQL(); self::Commit(); self::StartTransaction(); self::ConvertDB($errorMessage); self::Commit(); } catch (CLearnInstall201203ConvertDBException $e) { self::Rollback(); $errorMessage .= "Cautch exception at line: " . $e->getLine() . "; with message: " . $e->getMessage(); } catch (CLearnInstall201203ConvertDBTimeOut $e) { self::Commit(); /* $errorMessage .= "Timeout occured at line: " . $e->getLine() . ". Convertation is incomplete and should be executed another time."; */ $errorMessage .= ''; } catch (Exception $e) { self::Rollback(); $errorMessage .= "Cautch general exception at line: " . $e->getLine() . "; with message: " . $e->getMessage(); } return ($errorMessage); } protected static function StartTransaction() { global $DB; $DB->StartTransaction(); } protected static function Rollback() { global $DB; $DB->Rollback(); } protected static function Commit() { global $DB; $DB->Commit(); } protected static function ReCreateTriggersForMSSQL() { } protected static function _RightsModelGetTasksWithOperations() { $arTasksOperations = array( 'learning_lesson_access_denied' => array(), 'learning_lesson_access_read' => array( 'lesson_read' ), 'learning_lesson_access_manage_basic' => array( 'lesson_read', 'lesson_create', 'lesson_write', 'lesson_remove' ), 'learning_lesson_access_linkage_as_child' => array( 'lesson_read', 'lesson_link_to_parents', 'lesson_unlink_from_parents' ), 'learning_lesson_access_linkage_as_parent' => array( 'lesson_read', 'lesson_link_descendants', 'lesson_unlink_descendants' ), 'learning_lesson_access_linkage_any' => array( 'lesson_read', 'lesson_link_to_parents', 'lesson_unlink_from_parents', 'lesson_link_descendants', 'lesson_unlink_descendants' ), 'learning_lesson_access_manage_as_child' => array( 'lesson_read', 'lesson_create', 'lesson_write', 'lesson_remove', 'lesson_link_to_parents', 'lesson_unlink_from_parents' ), 'learning_lesson_access_manage_as_parent' => array( 'lesson_read', 'lesson_create', 'lesson_write', 'lesson_remove', 'lesson_link_descendants', 'lesson_unlink_descendants' ), 'learning_lesson_access_manage_dual' => array( 'lesson_read', 'lesson_create', 'lesson_write', 'lesson_remove', 'lesson_link_to_parents', 'lesson_unlink_from_parents', 'lesson_link_descendants', 'lesson_unlink_descendants' ), 'learning_lesson_access_manage_full' => array( 'lesson_read', 'lesson_create', 'lesson_write', 'lesson_remove', 'lesson_link_to_parents', 'lesson_unlink_from_parents', 'lesson_link_descendants', 'lesson_unlink_descendants', 'lesson_manage_rights' ) ); return ($arTasksOperations); } protected static function _RightsModelGetAllOperations() { $arAllOperations = array( 'lesson_read', 'lesson_create', 'lesson_write', 'lesson_remove', 'lesson_link_to_parents', 'lesson_unlink_from_parents', 'lesson_link_descendants', 'lesson_unlink_descendants', 'lesson_manage_rights' ); return ($arAllOperations); } /** * @return array of operations with IDs */ protected static function _CheckOperationsInDB() { global $DB; $arAllOperations = self::_RightsModelGetAllOperations(); $rc = $DB->Query ("SELECT ID, NAME, BINDING FROM b_operation WHERE MODULE_ID = 'learning'", true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $arOperationsInDB = array(); while ($arOperation = $rc->Fetch()) { if (mb_substr($arOperation['NAME'], 0, 7) === 'lesson_') $binding = 'lesson'; else $binding = 'module'; if ($arOperation['BINDING'] !== $binding) throw new Exception(); $arOperationsInDB[$arOperation['NAME']] = $arOperation['ID']; } if (count($arOperationsInDB) !== count($arAllOperations)) throw new Exception(); // not all operations in DB foreach ($arAllOperations as $operationName) { if ( ! isset($arOperationsInDB[$operationName]) ) throw new Exception(); // not all operations in DB } return ($arOperationsInDB); } protected static function _CheckTasksInDB($arTasksOperations) { global $DB; $rc = $DB->Query ("SELECT ID, NAME, BINDING FROM b_task WHERE MODULE_ID = 'learning'", true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $arTasksInDB = array(); while ($arTask = $rc->Fetch()) { if (mb_substr($arTask['NAME'], 0, 16) === 'learning_lesson_') $binding = 'lesson'; else $binding = 'module'; if ($arTask['BINDING'] !== $binding) throw new Exception(); $arTasksInDB[$arTask['NAME']] = $arTask['ID']; } if (count($arTasksInDB) !== count($arTasksOperations)) { throw new Exception('count($arTasksInDB) = ' . count($arTasksInDB) . '; count($arTasksOperations) = ' . count($arTasksOperations)); // not all tasks in DB } foreach (array_keys($arTasksOperations) as $taskName) { if ( ! isset($arTasksInDB[$taskName]) ) throw new Exception(); // not all tasks in DB } return ($arTasksInDB); } protected static function _CheckTasksOperationsRelations($arOperationsInDB, $arTasksInDB, $arTasksOperations) { global $DB; foreach ($arTasksInDB as $taskName => $taskId) { if ( ! isset($arTasksOperations[$taskName]) ) throw new Exception(); $arCurTaskOperations = $arTasksOperations[$taskName]; $arCurTaskOperationsIDs = array(); foreach ($arCurTaskOperations as $operationName) { if ( ! isset($arOperationsInDB[$operationName]) ) throw new Exception(); $operationId = $arOperationsInDB[$operationName]; $arCurTaskOperationsIDs[$operationId] = 'operation'; } // Get list of task's operations reltaions $rc = $DB->Query ("SELECT OPERATION_ID FROM b_task_operation WHERE TASK_ID = " . ($taskId + 0), true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); while ($arRelation = $rc->Fetch()) { if ( ! isset($arCurTaskOperationsIDs[$arRelation['OPERATION_ID']]) ) throw new Exception(); unset ($arCurTaskOperationsIDs[$arRelation['OPERATION_ID']]); } if (count($arCurTaskOperationsIDs) > 0) throw new Exception(); } } protected static function IsNewRightsModelInitialized(&$step, &$msg) { try { $arTasksOperations = self::_RightsModelGetTasksWithOperations(); // Compare list of operations in DB $arOperationsInDB = self::_CheckOperationsInDB(); // Compare list of tasks in DB $arTasksInDB = self::_CheckTasksInDB($arTasksOperations); // Compare relations between tasks and operations self::_CheckTasksOperationsRelations($arOperationsInDB, $arTasksInDB, $arTasksOperations); return (true); // new rights model correctly initialized } catch (Exception $e) { $step = $e->getLine(); $msg = $e->getMessage(); return (false); // new rights model not initialized } } protected static function _RightsModelCreateOperations() { global $DB; $arAllOperations = self::_RightsModelGetAllOperations(); $arOperationsInDB = array(); foreach ($arAllOperations as $operationName) { if (mb_substr($operationName, 0, 7) === 'lesson_') $binding = 'lesson'; else $binding = 'module'; $arFields = array( 'NAME' => "'" . $DB->ForSql($operationName) . "'", 'MODULE_ID' => "'learning'", 'DESCRIPTION' => 'NULL', 'BINDING' => "'" . $binding . "'" ); $id = $DB->Insert( 'b_operation', $arFields, "", // $error_position false, // $debug "", // $exist_id false // don't ignore errors, due to the bug in Database::Insert (it don't checks Query return status) ); if ($id === false) throw new Exception(); $arOperationsInDB[$operationName] = $id; } return ($arOperationsInDB); } protected static function _RightsModelCreateTasksAndRelation($arOperationsInDB) { global $DB, $APPLICATION; $arOld2NewRightsMatrix = array( 'D' => 'learning_lesson_access_read', 'W' => 'learning_lesson_access_manage_full' ); $module_id = 'learning'; $arDefaultRights = array ( 'learning_lesson_access_read' => array(), 'learning_lesson_access_manage_dual' => array('CR'), // Author 'learning_lesson_access_manage_full' => array('G1') // Admins ); $rc = CGroup::GetList('sort', 'asc', array('ACTIVE' => 'Y')); while($zr = $rc->Fetch()) { $group_id = $zr['ID']; $oldSymbol = $APPLICATION->GetGroupRight( $module_id, array($group_id), $use_default_level = "N", $max_right_for_super_admin = "N", $site_id = false); if (isset($arOld2NewRightsMatrix[$oldSymbol])) { $newSymbol = $arOld2NewRightsMatrix[$oldSymbol]; if (isset($arDefaultRights[$newSymbol])) $arDefaultRights[$newSymbol][] = 'G' . $group_id; } } $arTasksOperations = self::_RightsModelGetTasksWithOperations(); foreach ($arTasksOperations as $taskName => $arOperationsForTask) { if (mb_substr($taskName, 0, 16) === 'learning_lesson_') $binding = 'lesson'; else $binding = 'module'; $arFields = array( 'NAME' => "'" . $DB->ForSql($taskName) . "'", 'LETTER' => 'NULL', 'MODULE_ID' => "'learning'", 'SYS' => "'Y'", 'DESCRIPTION' => 'NULL', 'BINDING' => "'" . $binding . "'" ); $taskId = $DB->Insert( 'b_task', $arFields, "", // $error_position false, // $debug "", // $exist_id false // don't ignore errors, due to the bug in Database::Insert (it don't checks Query return status) ); if ($taskId === false) throw new Exception(); // Create relation for every operation per task foreach ($arOperationsForTask as $operationName) { if ( ! isset($arOperationsInDB[$operationName]) ) throw new Exception(); $operationId = (int) $arOperationsInDB[$operationName]; $rc = $DB->Query( "INSERT INTO b_task_operation (TASK_ID, OPERATION_ID) VALUES (" . (int) $taskId . ", " . (int) $operationId . ")", true); if ($rc === false) throw new Exception(); } // Add default rights for this task, if it exists if ( array_key_exists($taskName, $arDefaultRights) ) { $arDefaultRights[$taskName] = array_unique($arDefaultRights[$taskName]); foreach ($arDefaultRights[$taskName] as $subject_id) { $rc = $DB->Query ( "INSERT INTO b_learn_rights_all (SUBJECT_ID, TASK_ID) VALUES ('" . $DB->ForSQL($subject_id) . "', " . (int) $taskId . ")", true); if ($rc === false) throw new Exception(); } } } } protected static function _RightsModelPurge() { global $DB; $arQueries = array( "DELETE FROM b_task_operation WHERE TASK_ID IN (SELECT ID FROM b_task WHERE MODULE_ID = 'learning') OR OPERATION_ID IN (SELECT ID FROM b_operation WHERE MODULE_ID = 'learning')", "DELETE FROM b_operation WHERE MODULE_ID = 'learning'", "DELETE FROM b_task WHERE MODULE_ID = 'learning'" ); foreach ($arQueries as $key => $query) { $rc = $DB->Query($query, true); // ignore_errors = true if ($rc === false) throw new Exception ('EA_SQLERROR in query #' . $key); } } protected static function InitializeNewRightsModel() { global $DB; // Clean up learning module operations and tasks (if exists) self::_RightsModelPurge(); if ( ! $DB->TableExists('b_learn_rights_all') ) self::_CreateTblRightsAll(); $arOperationsInDB = self::_RightsModelCreateOperations(); self::_RightsModelCreateTasksAndRelation($arOperationsInDB); } protected static function _CreateTblRightsAll () { global $DB; if ( ! $DB->TableExists('b_learn_rights_all') ) { // Prepare sql code for adding fields $sql_tbl_b_learn_rights_all = " CREATE TABLE b_learn_rights_all ( SUBJECT_ID VARCHAR( 100 ) NOT NULL , TASK_ID INT NOT NULL , PRIMARY KEY ( SUBJECT_ID ) )"; $rc = $DB->Query($sql_tbl_b_learn_rights_all); if ($rc === false) throw new CLearnInstall201203ConvertDBException(__LINE__ . '/tbl: sql_tbl_b_learn_rights_all'); } } /** * @return int items processed */ public static function ConvertDB(&$errorMessage) { global $DB; // Check, was DB already converted? if (self::_IsAlreadyConverted() === true) return (true); // Mark that db convert process started $rc = COption::SetOptionString(self::MODULE_ID, self::OPTION_ID, self::STATUS_INSTALL_INCOMPLETE); if ($rc === false) throw new CLearnInstall201203ConvertDBException('SetOptionString() failed!'); // Create fields `CODE`, `WAS_CHAPTER_ID` in `b_learn_lesson` (if they doesn't exists yet) // and `JOURNAL_STATUS` in `b_learn_chapter` // and `LINKED_LESSON_ID` in `b_learn_course` self::_CreateFieldsInTbls(); /** * Our plan: * 1) Create lesson for every course and links them * (`b_learn_course`.`LINKED_LESSON_ID` = id_of_new_lesson). * Than update `b_learn_course`.`JOURNAL_STATUS` to self::JOURNAL_STATUS_COURSE_LINKED * * 2) Copy all chapters to lessons table and than update * `b_learn_chapter`.`JOURNAL_STATUS` = self::JOURNAL_STATUS_CHAPTER_COPIED * * 3) Build all edges between lessons. Firstly, build edges for simple lessons * (not that was a chapter or course), and than for lessons-chapters. */ $items_processed = 0; // Process courses $items_processed += self::_processCourses(); // Process chapters $items_processed += self::_processChapters(); // Creates table for edges, if it doesn't exists yet. self::_CreateEdgesTbl(); // Build edges for lessons and chapters (`WAS_COURSE_ID` === NULL) $items_processed += self::_buildEdges($errorMessage); // Convert old permissions to new self::ConvertPermissions(); // Add new path: COURSE_ID=#COURSE_ID# // ?LESSON_PATH=#LESSON_PATH# self::AddPath(); // Remove b_learn_course_permission, if exists self::_RemoveOrphanedTables(); // Mark that db convert process complete $rc = COption::SetOptionString(self::MODULE_ID, self::OPTION_ID, self::STATUS_INSTALL_COMPLETE); if ($rc === false) throw new CLearnInstall201203ConvertDBException('SetOptionString() failed!'); } protected static function ConvertPermissions() { global $DB; $arTaskIdByOldSymbol = array(); $arTasks = array( 'R' => 'learning_lesson_access_read', 'W' => 'learning_lesson_access_manage_basic', 'X' => 'learning_lesson_access_manage_full'); foreach ($arTasks as $oldSymbol => $taskName) { $rc = $DB->Query ( "SELECT ID FROM b_task WHERE NAME = '" . $taskName . "'", true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $row = $rc->Fetch(); if ( ! isset($row['ID']) ) throw new CLearnInstall201203ConvertDBException('EA_LOGIC'); $arTaskIdByOldSymbol[$oldSymbol] = (int) $row['ID']; } $sql = "SELECT TLL.ID, TLCP.PERMISSION, TLCP.USER_GROUP_ID FROM b_learn_lesson TLL INNER JOIN b_learn_course_permission TLCP ON TLL.COURSE_ID = TLCP.COURSE_ID WHERE TLL.COURSE_ID > 0 AND TLCP.PERMISSION != 'D' UNION SELECT TLL.ID, TLCP.PERMISSION, TLCP.USER_GROUP_ID FROM b_learn_lesson TLL INNER JOIN b_learn_course_permission TLCP ON TLL.WAS_COURSE_ID = TLCP.COURSE_ID WHERE TLL.COURSE_ID = 0 AND TLL.WAS_COURSE_ID > 0 AND TLCP.PERMISSION != 'D' "; $res = $DB->Query($sql, true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); while ($row = $res->Fetch()) { $lessonId = $row['ID']; $permission = $row['PERMISSION']; $user_group_id = $row['USER_GROUP_ID']; $group = 'G' . $user_group_id; // Determine task id if ( ! in_array($permission, array('R', 'W', 'X'), true) ) continue; // skip elements with D $task_id = $arTaskIdByOldSymbol[$permission]; $rc = $DB->Query ( "DELETE FROM b_learn_rights WHERE LESSON_ID = " . (int) $lessonId . " AND SUBJECT_ID = '" . $DB->ForSql($group) . "'", true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $rc = $DB->Query ( "INSERT INTO b_learn_rights (LESSON_ID, SUBJECT_ID, TASK_ID) VALUES (" . (int) $lessonId . ", '" . $DB->ForSql($group) . "', '" . $task_id . "')", true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); } } protected static function AddPath() { global $DB; $res = $DB->Query( "SELECT DISTINCT SITE_ID FROM b_learn_site_path WHERE 1=1", true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $arSitesIds = array(); while ($row = $res->Fetch()) $arSitesIds[] = $row['SITE_ID']; foreach ($arSitesIds as $k => $siteId) { $res = $DB->Query ( "DELETE FROM b_learn_site_path WHERE SITE_ID = '" . $DB->ForSql($siteId) . "' AND TYPE = 'U'", true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $res = $DB->Query ( "SELECT TSP.PATH FROM b_learn_site_path TSP WHERE TYPE = 'C' AND SITE_ID = '" . $DB->ForSql($siteId) . "'", true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $row = $res->Fetch(); if (isset($row['PATH'])) { $path = str_replace('COURSE_ID=#COURSE_ID#', 'LESSON_PATH=#LESSON_PATH#', $row['PATH']); $path = str_replace('&INDEX=Y', '', $path); } else $path = '/services/learning/course.php?LESSON_PATH=#LESSON_PATH#'; $DB->Insert( 'b_learn_site_path', array( 'SITE_ID' => "'" . $DB->ForSql($siteId) . "'", 'PATH' => "'" . $DB->ForSql($path) . "'", 'TYPE' => "'U'" ) ); } } /** * @return int items processed */ public static function _buildEdges(&$errorMessage) { global $DB; $items_processed = 0; // For lessons, on which b_learn_course.LINKED_LESSON_ID linked we don't need edges, because they are tops nodes now. $res = $DB->Query ( "SELECT ID, COURSE_ID, CHAPTER_ID, ACTIVE, SORT, WAS_CHAPTER_ID, WAS_PARENT_CHAPTER_ID, WAS_PARENT_COURSE_ID FROM b_learn_lesson WHERE JOURNAL_STATUS != " . self::JOURNAL_STATUS_LESSON_EDGES_CREATED . " AND WAS_COURSE_ID IS NULL", $ignore_errors = true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); while ($arLesson = $res->Fetch()) { $sort = $arLesson['SORT']; $childNodeId = $arLesson['ID']; // Determine, who is immediate parent of lesson - chapter or course if ($arLesson['WAS_CHAPTER_ID'] === NULL) // current node wasn't a chapter { if ($arLesson['CHAPTER_ID'] !== NULL) { // intermediate parent is chapter, get it id in new data model $parentNodeId = self::_GetChapterIdInNewDataModel ($arLesson['CHAPTER_ID']); } elseif ($arLesson['COURSE_ID'] !== NULL) { // intermediate parent is course, get it id in new data model $parentNodeId = self::_GetCourseIdInNewDataModel ($arLesson['COURSE_ID']); } else { // No parent? It's very strange for old data model, but it's OK for new data model. // So, nothing to do here. $parentNodeId = NULL; } } else // current node was a chapter { if ($arLesson['WAS_PARENT_CHAPTER_ID'] !== NULL) { // intermediate parent is chapter, get it id in new data model $parentNodeId = self::_GetChapterIdInNewDataModel ($arLesson['WAS_PARENT_CHAPTER_ID']); } elseif ($arLesson['WAS_PARENT_COURSE_ID'] !== NULL) { // intermediate parent is course, get it id in new data model $parentNodeId = self::_GetCourseIdInNewDataModel ($arLesson['WAS_PARENT_COURSE_ID']); } else { // No parent? It's very strange for old data model, but it's OK for new data model. // So, nothing to do here. $parentNodeId = NULL; } } if ($parentNodeId === NULL) ; // nothing to do elseif ($parentNodeId === -1) { /** * An error occured (chapter or course not found in new data model) * In old data model, this shouldn't be. * So, it's may be: * 1) our bug during importing chapter/courses to lessons * 2) data inconsistency in target database * 3) third-party UPDATE/INSERT/DELETE was processed on lessons/chapters/courses during convertation * In any case, this situation is not good (except, probabaly, case #2). * But, best we can do is continue convertation. * Because, if we restart convertation, it can be infinitly looped. * * So, nothing to do here. */ $errorMessage .= "Problem occured with CHAPTER_ID = " . $arLesson['CHAPTER_ID'] . "; COURSE_ID = " . $arLesson['COURSE_ID'] . "
\n"; } elseif ($parentNodeId <= 0) { // This is invalid value throw new CLearnInstall201203ConvertDBException('EA_OTHER: invalid parentNodeId for lesson_id = ' . $arLesson['ID']); } else { // All is OK, so create edge for nodes self::_CreateEdgeForNodes ($parentNodeId, $childNodeId, $sort); } // Mark lesson as processed self::_MarkLessonAsProcessed ($arLesson['ID']); ++$items_processed; // This function throws exception CLearnInstall201203ConvertDBTimeOut, if it's low time left. self::avoidTimeout(); } return ($items_processed); } public static function _MarkLessonAsProcessed ($lessonId) { global $DB; // Mark this course as processed $rc = $DB->Update('b_learn_lesson', array ('JOURNAL_STATUS' => self::JOURNAL_STATUS_LESSON_EDGES_CREATED), "WHERE ID = '" . ($lessonId + 0) . "'", $error_position = "", $debug = false, $ignore_errors = true ); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); } public static function _CreateEdgeForNodes ($parentNodeId, $childNodeId, $sort) { global $DB; $parentNodeId += 0; $childNodeId += 0; // Firstly, remove such edge, if exists $rc = $DB->Query ( "DELETE FROM b_learn_lesson_edges WHERE SOURCE_NODE = '" . $parentNodeId . "' AND TARGET_NODE = '" . $childNodeId . "'", $ignore_errors = true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); // Now, create edge $rc = $DB->Query ( "INSERT INTO b_learn_lesson_edges (SOURCE_NODE, TARGET_NODE, SORT) VALUES ('" . $parentNodeId . "', '" . $childNodeId . "', '" . $sort . "')", $ignore_errors = true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); } /** * @param int chapter_id in old data model (in table b_learn_chapter) * @return int chapter_id in new data model (in table b_learn_lesson) OR -1 on error */ public static function _GetChapterIdInNewDataModel ($b_learn_chapter_ID) { global $DB; $res = $DB->Query ( "SELECT ID FROM b_learn_lesson WHERE WAS_CHAPTER_ID = '" . ($b_learn_chapter_ID + 0) . "'", $ignore_errors = true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); if ($arLesson = $res->Fetch()) return ($arLesson['ID'] + 0); else return (-1); } /** * @param int course_id in old data model (in table b_learn_course) * @return int course_id in new data model (in table b_learn_lesson) OR -1 on error */ public static function _GetCourseIdInNewDataModel ($b_learn_course_ID) { global $DB; $res = $DB->Query ( "SELECT ID FROM b_learn_lesson WHERE WAS_COURSE_ID = '" . ($b_learn_course_ID + 0) . "'", $ignore_errors = true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); if ($arLesson = $res->Fetch()) return ($arLesson['ID'] + 0); else return (-1); } /** * @return int items processed */ public static function _processCourses() { global $DB; $items_processed = 0; $res = $DB->Query ("SELECT * FROM b_learn_course WHERE JOURNAL_STATUS != " . self::JOURNAL_STATUS_COURSE_LINKED, $ignore_errors = true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); while ($arCourses = $res->Fetch()) { $arFields = array( 'ACTIVE' => $arCourses['ACTIVE'], 'NAME' => ($arCourses['NAME'] === NULL) ? false : $arCourses['NAME'], 'CODE' => ($arCourses['CODE'] === NULL) ? false : $arCourses['CODE'], 'SORT' => $arCourses['SORT'], 'PREVIEW_PICTURE' => ($arCourses['PREVIEW_PICTURE'] === NULL) ? false : $arCourses['PREVIEW_PICTURE'], 'DETAIL_PICTURE' => ($arCourses['PREVIEW_PICTURE'] === NULL) ? false : $arCourses['PREVIEW_PICTURE'], 'PREVIEW_TEXT_TYPE' => $arCourses['PREVIEW_TEXT_TYPE'], 'DETAIL_TEXT_TYPE' => $arCourses['DESCRIPTION_TYPE'], 'LAUNCH' => '', 'JOURNAL_STATUS' => self::JOURNAL_STATUS_UNPROCESSED, 'WAS_CHAPTER_ID' => false, 'WAS_PARENT_CHAPTER_ID' => false, 'WAS_PARENT_COURSE_ID' => false, 'WAS_COURSE_ID' => $arCourses['ID'], 'PREVIEW_TEXT' => $arCourses['PREVIEW_TEXT'], 'DETAIL_TEXT' => $arCourses['DESCRIPTION'] ); // Creates new lesson (unprocessed duplicates will be removed first) $id_of_new_lesson = self::_UnrepeatableCreateLesson ($arFields); // Link course to this lesson $rc = $DB->Update ('b_learn_course', array ('LINKED_LESSON_ID' => $id_of_new_lesson), "WHERE ID = '" . ($arCourses['ID'] + 0) . "'", $error_position = "", $debug = false, $ignore_errors = true ); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); // Mark this course as processed $rc = $DB->Update('b_learn_course', array ('JOURNAL_STATUS' => self::JOURNAL_STATUS_COURSE_LINKED), "WHERE ID = '" . ($arCourses['ID'] + 0) . "'", $error_position = "", $debug = false, $ignore_errors = true ); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); ++$items_processed; // This function throws exception CLearnInstall201203ConvertDBTimeOut, if it's low time left. self::avoidTimeout(); } return ($items_processed); } /** * This function throws exception CLearnInstall201203ConvertDBTimeOut, if it's low time left. */ public static function avoidTimeout() { static $started_at = false; static $time_limit = false; if ($started_at === false) { $started_at = microtime (true); $rc = ini_get('max_execution_time'); if (($rc === false) || ($rc === '') || ($rc < 0)) { // We fail to determine max_execution_time, try to set it set_time_limit (28); // Ensure, that max_execution_time was set $rc = ini_get('max_execution_time'); } if (($rc === false) || ($rc === '') || ($rc < 0)) { /** * Hmmm... WTF?! * Lets think, that our limit is 25 seconds. * If it actually less, there is nothing wrong, * because current algorithm should be firm to breaks in any place. */ $time_limit = 25; } elseif ($rc == 0) { /** * We have unlimited time, but some troubles can occur in IIS, * so limit time to 20 seconds. */ $time_limit = 20; } else { $time_limit = ($rc + 0); } } $time_executed = microtime(true) - $started_at; $time_left = $time_limit - $time_executed; if ($time_left < 4) throw new CLearnInstall201203ConvertDBTimeOut(); } public static function _processChapters() { global $DB; $items_processed = 0; $res = $DB->Query ("SELECT * FROM b_learn_chapter WHERE JOURNAL_STATUS != " . self::JOURNAL_STATUS_CHAPTER_COPIED, $ignore_errors = true); if ($res === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); while ($arChapter = $res->Fetch()) { $arFields = array ( 'ACTIVE' => $arChapter['ACTIVE'], 'NAME' => ($arChapter['NAME'] === NULL) ? false : $arChapter['NAME'], 'CODE' => ($arChapter['CODE'] === NULL) ? false : $arChapter['CODE'], 'SORT' => (string) (1000000 + (int) $arChapter['SORT']), 'PREVIEW_PICTURE' => ($arChapter['PREVIEW_PICTURE'] === NULL) ? false : $arChapter['PREVIEW_PICTURE'], 'PREVIEW_TEXT' => $arChapter['PREVIEW_TEXT'], 'PREVIEW_TEXT_TYPE' => $arChapter['PREVIEW_TEXT_TYPE'], 'DETAIL_PICTURE' => ($arChapter['DETAIL_PICTURE'] === NULL) ? false : $arChapter['DETAIL_PICTURE'], 'DETAIL_TEXT' => $arChapter['DETAIL_TEXT'], 'DETAIL_TEXT_TYPE' => $arChapter['DETAIL_TEXT_TYPE'], 'LAUNCH' => '', 'JOURNAL_STATUS' => self::JOURNAL_STATUS_UNPROCESSED, 'WAS_CHAPTER_ID' => ($arChapter['ID']), 'WAS_PARENT_CHAPTER_ID' => ($arChapter['CHAPTER_ID'] === NULL) ? false : $arChapter['CHAPTER_ID'], 'WAS_PARENT_COURSE_ID' => ($arChapter['COURSE_ID'] === NULL) ? false : $arChapter['COURSE_ID'], 'WAS_COURSE_ID' => false ); // Creates new lesson (unprocessed duplicates will be removed first) $id_of_new_lesson = self::_UnrepeatableCreateLesson ($arFields); // Now we must replace QUESTIONS_FROM_ID (where now is $arChapter['ID']) // in b_learn_test for QUESTIONS_FROM=='H'. Replace them to QUESTIONS_FROM='' // UPDATE b_learn_test SET QUESTIONS_FROM = 'R', QUESTIONS_FROM_ID = 150 WHERE QUESTIONS_FROM = 'H' AND QUESTIONS_FROM_ID = '1' $rc = $DB->Query( "UPDATE b_learn_test SET TIMESTAMP_X = " . $DB->GetNowFunction() . ", QUESTIONS_FROM = 'R', QUESTIONS_FROM_ID = " . ($id_of_new_lesson + 0) . " WHERE QUESTIONS_FROM = 'H' AND QUESTIONS_FROM_ID = '" . ($arChapter['ID'] + 0) . "'", $ignore_errors = true ); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); // Mark this chapter as processed $rc = $DB->Update('b_learn_chapter', array ('JOURNAL_STATUS' => self::JOURNAL_STATUS_CHAPTER_COPIED), "WHERE ID = '" . ($arChapter['ID'] + 0) . "'", $error_position = "", $debug = false, $ignore_errors = true ); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); ++$items_processed; // This function throws exception CLearnInstall201203ConvertDBTimeOut, if it's low time left. self::avoidTimeout(); } return ($items_processed); } /** * Inserts new lesson to `b_learn_lesson`. Before insert, drop * exists lessons with such `WAS_CHAPTER_ID` (if not NULL) * or with such `WAS_COURSE_ID` (if not NULL) */ public static function _UnrepeatableCreateLesson ($arFields) { global $DB; if ( ! is_array($arFields) ) throw new CLearnInstall201203ConvertDBException('EA_PARAMS'); if ( ! isset($arFields['COURSE_ID']) ) $arFields['COURSE_ID'] = 0; // Determine, from what source import doing (Chapter or Course) if (array_key_exists('WAS_CHAPTER_ID', $arFields) && ($arFields['WAS_CHAPTER_ID'] !== false) ) { // new lesson will be created from chapter $sqlWhere = "WAS_CHAPTER_ID = '" . ($arFields['WAS_CHAPTER_ID'] + 0) . "'"; } elseif (array_key_exists('WAS_COURSE_ID', $arFields) && ($arFields['WAS_COURSE_ID'] !== false) ) { // new lesson will be created from chapter $sqlWhere = "WAS_COURSE_ID = '" . ($arFields['WAS_COURSE_ID'] + 0) . "'"; } else { throw new CLearnInstall201203ConvertDBException('EA_PARAMS'); } // Firstly, remove such imported lesson, if exists $rc = $DB->Query ("DELETE FROM b_learn_lesson WHERE " . $sqlWhere, $ignore_errors = true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $arInsert = $DB->PrepareInsert('b_learn_lesson', $arFields); $strSql = "INSERT INTO b_learn_lesson (" . $arInsert[0] . ", TIMESTAMP_X, DATE_CREATE, CREATED_BY) " . "VALUES (" . $arInsert[1] . ", " . $DB->GetNowFunction() . ", " . $DB->GetNowFunction() . ", 1)"; $rc = $DB->Query($strSql, true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('EA_SQLERROR'); $newLessonId = intval($DB->LastID()); return ($newLessonId); } /** * We are converted if option with name self::OPTION_ID is set to self::STATUS_INSTALL_COMPLETE * * !!! But, if: * 1) this option is set to self::STATUS_INSTALL_NEVER_START * AND * 2) there is tables b_learn_lesson_edges exists & b_learn_rights_all * and b_learn_course_permission doesn't exists * it means that options is incorrectly set (or was reseted by somebody else), so we returns that DB is already converted * */ public static function _IsAlreadyConverted() { $rc = COption::GetOptionString(self::MODULE_ID, self::OPTION_ID, self::STATUS_INSTALL_NEVER_START, $site = ''); if ($rc === self::STATUS_INSTALL_NEVER_START) { global $DB; if ($DB->TableExists('b_learn_lesson_edges') && $DB->TableExists('b_learn_rights_all') && ( ! $DB->TableExists('b_learn_course_permission') ) ) { return (true); } else return (false); } elseif ($rc === self::STATUS_INSTALL_COMPLETE) return (true); elseif ($rc === self::STATUS_INSTALL_INCOMPLETE) return (false); else self::_GiveUp(__LINE__); } protected static function _RemoveOrphanedTables() { global $DB; if ( ! $DB->TableExists('b_learn_course_permission') ) return; $rc = $DB->Query ("DROP TABLE b_learn_course_permission", true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('Can\'t DROP `b_learn_course_permission` under database engine: mysql'); } public static function _CreateEdgesTbl() { global $DB; if ($DB->TableExists('b_learn_lesson_edges')) return; $sql = "CREATE TABLE b_learn_lesson_edges ( SOURCE_NODE INT NOT NULL , TARGET_NODE INT NOT NULL , SORT INT NOT NULL DEFAULT '500', PRIMARY KEY ( SOURCE_NODE , TARGET_NODE ) )"; $rc = $DB->Query ($sql, $ignore_errors = true); if ($rc === false) throw new CLearnInstall201203ConvertDBException('Can\'t create `b_learn_lesson_edges` under database engine: mysql'); } public static function _CreateFieldsInTbls() { global $DB; $arTableFields = array( 'b_learn_lesson' => $DB->GetTableFieldsList ('b_learn_lesson'), 'b_learn_chapter' => $DB->GetTableFieldsList ('b_learn_chapter'), 'b_learn_course' => $DB->GetTableFieldsList ('b_learn_course') ); $sql_add = array(); $other_sql_skip_errors = array(); $other_sql = array(); // Prepare sql code for adding fields $sql_add['b_learn_lesson'] = array ( 'KEYWORDS' => "ALTER TABLE b_learn_lesson ADD KEYWORDS TEXT NOT NULL DEFAULT ''", 'CODE' => "ALTER TABLE b_learn_lesson ADD CODE VARCHAR( 50 ) NULL DEFAULT NULL", 'WAS_CHAPTER_ID' => "ALTER TABLE b_learn_lesson ADD WAS_CHAPTER_ID INT NULL DEFAULT NULL", 'WAS_PARENT_CHAPTER_ID' => "ALTER TABLE b_learn_lesson ADD WAS_PARENT_CHAPTER_ID INT NULL DEFAULT NULL", 'WAS_PARENT_COURSE_ID' => "ALTER TABLE b_learn_lesson ADD WAS_PARENT_COURSE_ID INT NULL DEFAULT NULL", 'WAS_COURSE_ID' => "ALTER TABLE b_learn_lesson ADD WAS_COURSE_ID INT NULL DEFAULT NULL", 'JOURNAL_STATUS' => "ALTER TABLE b_learn_lesson ADD JOURNAL_STATUS INT NOT NULL DEFAULT '0'" ); $sql_add['b_learn_chapter'] = array ( 'JOURNAL_STATUS' => "ALTER TABLE b_learn_chapter ADD JOURNAL_STATUS INT NOT NULL DEFAULT '0'" ); $sql_add['b_learn_course'] = array ( 'LINKED_LESSON_ID' => "ALTER TABLE b_learn_course ADD LINKED_LESSON_ID INT NULL DEFAULT NULL", 'JOURNAL_STATUS' => "ALTER TABLE b_learn_course ADD JOURNAL_STATUS INT NOT NULL DEFAULT '0'" ); $sql_tbl_b_learn_rights = " CREATE TABLE b_learn_rights ( LESSON_ID INT UNSIGNED NOT NULL , SUBJECT_ID VARCHAR( 100 ) NOT NULL , TASK_ID INT NOT NULL , PRIMARY KEY ( LESSON_ID , SUBJECT_ID ) )"; $other_sql_skip_errors[] = "ALTER TABLE b_learn_course ALTER COLUMN NAME SET DEFAULT 'name'"; $other_sql_skip_errors[] = "ALTER TABLE b_learn_lesson ALTER COLUMN NAME SET DEFAULT 'name'"; $other_sql_skip_errors[] = "ALTER TABLE b_learn_lesson ALTER COLUMN COURSE_ID SET DEFAULT '0'"; $other_sql_skip_errors[] = " CREATE TABLE b_learn_publish_prohibition ( COURSE_LESSON_ID INT UNSIGNED NOT NULL , PROHIBITED_LESSON_ID INT UNSIGNED NOT NULL , PRIMARY KEY ( COURSE_LESSON_ID , PROHIBITED_LESSON_ID ) )"; $other_sql_skip_errors[] = " CREATE TABLE b_learn_exceptions_log ( DATE_REGISTERED datetime NOT NULL, CODE int(11) NOT NULL, MESSAGE text NOT NULL, FFILE text NOT NULL, LINE int(11) NOT NULL, BACKTRACE text NOT NULL )"; if ( ! $DB->TableExists('b_learn_rights')) { $rc = $DB->Query($sql_tbl_b_learn_rights); if ($rc === false) throw new CLearnInstall201203ConvertDBException(__LINE__ . '/tbl: sql_tbl_b_learn_rights'); } foreach ($sql_add as $tableName => $sql_for_table) { // Add every field (if not exists yet) to table $tableName foreach ($sql_for_table as $fieldName => $sql) { if ( ! in_array($fieldName, $arTableFields[$tableName], true) ) { $rc = $DB->Query($sql, $ignore_erros = true); if ($rc === false) throw new CLearnInstall201203ConvertDBException(__LINE__.'/tbl:'.mb_strlen($fieldName)); } } /* !!! This does not work correctly (table's fields cache in Database class issue?) // Now, ensure, that fields was added really $arTableFields_after = $DB->GetTableFieldsList ($tableName); foreach ($sql_for_table as $fieldName => $sql) { if ( ! in_array($fieldName, $arTableFields_after, true) ) self::_GiveUp(__LINE__ . '/tbl:' . strlen($fieldName)); } */ } foreach ($other_sql_skip_errors as $sql) $rc = $DB->Query($sql, $ignore_erros = true); foreach ($other_sql as $sql) { $rc = $DB->Query($sql, $ignore_erros = true); if ($rc === false) throw new CLearnInstall201203ConvertDBException(__LINE__ . '/sql:' . htmlspecialcharsbx($sql)); } // Drop cache $rc = $DB->DDL("SELECT * FROM b_learn_lesson WHERE 1=1", true); if ($rc === false) throw new CLearnInstall201203ConvertDBException(__LINE__ . ', on DDL\'s cache drop'); } /* public static function _RemoveFieldsFromLesson() { // ORACLE: // ALTER TABLE `b_learn_lesson` DROP COLUMN `JOURNAL_ID` ??? global $DB, $DBType; $arTableFields = $DB->GetTableFieldsList (`b_learn_lesson`); // Prepare sql code for removing fields $sql_add = array(); if ($DBType === 'mysql') { $sql_add['JOURNAL_ID'] = "ALTER TABLE `b_learn_lesson` DROP `JOURNAL_ID`"; } else { // TODO: do sql code for MSSQL and Oracle self::_GiveUp ('SQL code not ready for: ' . $DBType); } // Remove every field (if exists) from table b_learn_lesson foreach ($sql_add as $fieldName => $sql) { if ( in_array($fieldName, $arTableFields, true) ) { $rc = $DB->Query($sql, $ignore_erros = true); if ($rc === false) self::_GiveUp(__LINE__); } } // Don't ensure, that fields was really removed, because it's not critical for us } */ public static function _GiveUp($msg = false) { if ($msg !== false) throw new Exception ('FATAL: ' . $msg); else throw new Exception ('Shit happens.'); } } $rc = CLearnInstall201203ConvertDB::run(); if ($rc <> '') $errorMessage = $rc; }