%PDF- %PDF-
Direktori : /proc/self/root/home/bitrix/www/bitrix/modules/learning/classes/general/ |
Current File : //proc/self/root/home/bitrix/www/bitrix/modules/learning/classes/general/clearnlesson.php |
<?php /** * Note: usually in phpDoc blocks for methods listed not all exceptions, * that can be throwed by them. * * @access public */ interface ILearnLesson { /** * WARNING: second param ($isCourse) must be always set to FALSE, because * it's is for internal use only. If you want to create course, use * CCourse::Add instead. * * Creates new lesson * * WARNING: this method terminates (by die()/exit()) current execution flow * when SQL server error occured. It's due to bug in CDatabase::Insert() in main * module (version info: * define("SM_VERSION","11.0.12"); * define("SM_VERSION_DATE","2012-02-21 17:00:00"); // YYYY-MM-DD HH:MI:SS * ) * * @param array of pairs field => value for new lesson. Allowed fields are ACTIVE, * ACTIVE, true by default, available values are: true/false * NAME, mustn't be omitted * CODE, NULL by default * PREVIEW_PICTURE, NULL by default, available value is array ('name' => ..., * 'size' => ..., 'tmp_name' => ..., 'type' => ..., 'del' => ...) * PREVIEW_TEXT, NULL by default * PREVIEW_TEXT_TYPE, 'text' by default, available values are: 'text', 'html' * DETAIL_PICTURE, NULL by default, available value is array ('name' => ..., * 'size' => ..., 'tmp_name' => ..., 'type' => ..., 'del' => ...) * DETAIL_TEXT, NULL by default * DETAIL_TEXT_TYPE, 'text' by default, available values are: 'text', 'html', 'file' (filename in LAUNCH) * LAUNCH, NULL by default * * @param bool flag of course. If FALSE (default) lesson is not course, * if TRUE created lesson will be the course (in this case arguments * $parentLessonId and $arProperties are ignored, and $arFields * can contain additional fields (any can be omitted): * - SORT or COURSE_SORT: integer, 500 by default, sort order of courses. CORSE_SORT overrides SORT * - ACTIVE_FROM: datetime, NULL by default * - ACTIVE_TO: datetime, NULL by default * - RATING: string (1 char): 'Y' / 'N' / NULL, 'N' by default * - RATING_TYPE: string, allowed values: NULL, "like", "standart_text", * "like_graphic", "standart", NULL by default * - SCORM: string (1 char), 'N' by default * ). * @param integer/bool id of immediate parent lesson. Default value is TRUE, * what means "no immediate parent". * @param array of properties in relation to * parent lesson: array ('SORT' => sort_order) * * @throws LearnException with errcodes bit set (one of): * - LearnException::EXC_ERR_GN_CREATE, * - LearnException::EXC_ERR_GN_CHECK_PARAMS, * - LearnException::EXC_ERR_GN_FILE_UPLOAD * Also can throws other exceptions or exceptions' codes. * * @return integer id of created lesson (if course was created, * for get id of course method GetLinkedCourse() must be used) */ public static function Add ($arFields, $isCourse = false, $parentLessonId = true, $arProperties = array('SORT' => 500)); /** * Changes lesson's data * * WARNING: this method terminates (by die()/exit()) current execution flow * when SQL server error occured. It's due to bug in CDatabase::Update() in main * module (version info: * define("SM_VERSION","11.0.12"); * define("SM_VERSION_DATE","2012-02-21 17:00:00"); // YYYY-MM-DD HH:MI:SS * ) * * @param integer id of node to be updated * @param array of pairs field => value for lesson * If lesson is the course, additional fields maybe set: * - SORT or COURSE_SORT: integer, sort order of courses. CORSE_SORT overrides SORT * - ACTIVE_FROM: datetime * - ACTIVE_TO: datetime * - RATING: string (1 char): 'Y' / 'N' / NULL * - RATING_TYPE: string, allowed values: NULL, "like", "standart_text", * "like_graphic", "standart" * - SCORM: string (1 char) * ). * * @throws LearnException with errcodes bit set (one of): * - LearnException::EXC_ERR_GN_UPDATE, * - LearnException::EXC_ERR_GN_CHECK_PARAMS, * - LearnException::EXC_ERR_GN_FILE_UPLOAD * Also can throws other exceptions or exceptions' codes */ public static function Update ($id, $arFields); /** * Removes lesson and all relations from/to it. * * @param integer/array. If not array => param interpreted as id of lesson to be removed. * If array => param interpreted as array of params. Available params are: * - 'lesson_id' - integer. Id of lesson to be removed. * - 'simulate' - boolean, false by default. If true => nothing will be writed to DB. * - 'check_permissions' - boolean, true by default * - 'user_id' - integer. User_id for which permissions will be checked, -1 by * default, which means 'current logged user' * (it's means 'current user') * * @throws LearnException with errcode bit set LearnException::EXC_ERR_GN_REMOVE, * also errmsg === 'EA_NOT_EXISTS' if there is wasn't node with this id. */ public static function Delete ($id); /** * Detach the given lesson from all parents and recursively remove descendants, excepts * descendants, that have ancestors outside of descendants of the given lesson. Such * lessons will not be removed, but will be unlinked from lessons, which will be really * removed. * * @param integer/array. If not array => param interpreted as id of lesson to be removed. * If array => param interpreted as array of params. Available params are: * - 'lesson_id' - integer. Id of lesson to be removed. * - 'simulate' - boolean, false by default. If true => nothing will be writed to DB. * - 'check_permissions' - boolean, true by default * - 'user_id' - integer. User_id for which permissions will be checked, -1 by default * (it's means 'current user') */ public static function DeleteRecursiveLikeHardlinks ($id); /** * WARNING: don't use this function, it's for internal use only. * * @param integer id of node to be getted * * @throws LearnException with errcode bit set LearnException::EXC_ERR_GN_GETBYID. * Messages can be: 'EA_PARAMS', 'EA_ACCESS_DENIED', * 'EA_SQLERROR', 'EA_NOT_EXISTS'. * * @access private * * @return array of properties for node with $id */ public static function GetByIDAsArr($id); /** * @throws LearnException with error bit set EXC_ERR_ALL_PARAMS * @return CDBResult */ public static function GetByID($id); /** * @param array order in format: array('FIELD_NAME' => '#sort_order#' [, ...]), * where #sort_order# is 'ASC' or 'DESC' * * @param array filter in format: array('???FIELD_NAME' => 'value' [, ...]), * where ??? can be one of (without double quotes): * "!" - not equals * "<" - less than value * "<=" - less than or equal to value * ">" - greater than value * ">=" - greater than or equal to value * * Additionally available fields (not presented in data, but * can be used for filter): DATE_ACTIVE_TO, DATE_ACTIVE_FROM, ACTIVE_DATE. * * @example shows lessons with LESSON_ID >= 100, and DETAIL_TEXT_TYPE != 'html', * sorted by NAME ascending, than by LESSON_ID descending. * * <?php * $arOrder = array ('NAME' => 'ASC', 'LESSON_ID' => 'DESC'); * $arFilter = array ('!DETAIL_TEXT_TYPE' => 'html', '>=LESSON_ID' => 100); * * $rc = ClassName::GetList($arOrder, $arFilter); * while (($data = $rc->Fetch()) !== false) * var _dump ($data); * * @throws LearnException with error bit set EXC_ERR_ALL_PARAMS * @return CDBResult each element of which can contains * COURSE_SORT (only if lesson is course) */ public static function GetList($arOrder = array(), $arFilter = array()); /** * @param int id of child lesson * @param array order (see format in comment for ThisClass::GetList()) * @param array filter (see format in comment for ThisClass::GetList()) * @param array list of fields to be selected. If empty => selects all fields. * * @throws LearnException with error bit set EXC_ERR_ALL_PARAMS * @return CDBResult */ public static function GetListOfImmediateParents($lessonId, $arOrder = array(), $arFilter = array(), $arSelectFields = array()); /** * @param int id of parent lesson * @param array order (see format in comment for ThisClass::GetList()) * @param array filter (see format in comment for ThisClass::GetList()) * @param array list of fields to be selected. If empty => selects all fields. * * @throws LearnException with error bit set EXC_ERR_ALL_PARAMS * @return CDBResult each element contains EDGE_SORT - the sort index * of LESSON_ID in relation to parent lesson given in first argument * to this method. */ public static function GetListOfImmediateChilds($lessonId, $arOrder = array(), $arFilter = array(), $arSelectFields = array()); /** * Lists immediate parents. * * @param integer id of lesson * * @return array of immediate parents (empty array if there is no parents) * * @example * <?php * $arParents = ThisClass::ListImmediateNeighbours (1); * var _dump ($arParents); * ?> * * output: * array(2) { * [0]=> * array(4) { * ["PARENT_LESSON"]=> * int(1) * ["CHILD_LESSON"]=> * int(2) * ["SORT"]=> * int(500) * } * [1]=> * array(4) { * ["PARENT_LESSON"]=> * int(4) * ["CHILD_LESSON"]=> * int(1) * ["SORT"]=> * int(500) * } * } * */ public static function ListImmediateParents($lessonId); /** * Lists immediate childs. * * @param integer id of lesson * * @return array of immediate childs (empty array if there is no childs) * * @see example for ListImmediateParents() */ public static function ListImmediateChilds($lessonId); /** * Lists immediate neighbours. * * @param integer id of lesson * * @return array of immediate neighbours (empty array if there is no neighbours) * * @see example for ListImmediateParents() */ public static function ListImmediateNeighbours($lessonId); /** * Gets id of course corresponded to given lesson * @param integer id of lesson * @throws LearnException with error bit set (one of): * - LearnException::EXC_ERR_ALL_GIVEUP * - LearnException::EXC_ERR_ALL_LOGIC * @return integer/bool id of linked (corresponded) course or * FALSE if there is no course corresponded to the lesson. */ public static function GetLinkedCourse ($lessonId); /** * Build tree of lessons with the given root. * WARNING: tree build algorithm skips duplicated lessons, so * if there is some duplicates lessons, only one of them * will be in resulted tree. * * @param integer id of root lesson * @param array order (by default: array('EDGE_SORT' => 'asc')) * @param array Filter for lessons * @param bool public prohibition mode flag. If set to TRUE, than all * lessons (and they descendants) that are public prohibited in context * of a course with lesson_id == $lessonId will be skipped during * tree building. * @return object of type CLearnLessonTree */ public static function GetTree ( $lessonId, $arOrder = array ('EDGE_SORT' => 'asc'), $arFilter = array(), $publishProhibitionMode = true ); /** * Link two lessons from $parentLessonId to $childLessonId * * @param int $parentLessonId * @param int $childLessonId * @param array of properties of the link. Currently available properties: * - 'SORT', integer * All properties must be set. * * @throws Exception LearnException with code error bit set LearnException::EXC_ERR_GR_LINK */ public static function RelationAdd ($parentLessonId, $childLessonId, $arProperties); /** * Update parametres of relation between two lessons * * @param int $parentLessonId * @param int $childLessonId * @param array of properties of the link. Currently available properties: * - 'SORT', integer * * @throws Exception LearnException with code error bit set LearnException::EXC_ERR_GR_SET_PROPERTY */ public static function RelationUpdate ($parentLessonId, $childLessonId, $arProperties); /** * Get parametres of relation between two lessons * * @param int $parentLessonId * @param int $childLessonId * @return array of properties of the link. Currently available properties: * - 'SORT', integer * * @throws Exception LearnException with code error bit set LearnException::EXC_ERR_GR_GET_PROPERTY */ public static function RelationGet ($parentLessonId, $childLessonId); /** * Remove relation from $parentLessonId to $childLessonId * * @param int $parentLessonId * @param int $childLessonId * * @throws Exception LearnException with code error bit set LearnException::EXC_ERR_GR_UNLINK * if relation isn't exists => message of exception === 'EA_NOT_EXISTS' */ public static function RelationRemove ($parentLessonId, $childLessonId); /** * Counts how much immediate childs given lesson has. * * @param int id of lesson * * @return int count of immediate childs for given lesson id. */ public static function CountImmediateChilds ($lessonId); /** * Lists all pathes to given lesson. Given lesson not included in pathes. * * @param int lesson id to be started from * @param int/bool id of breakpoint-lesson (root lesson). * It means, this lesson will be interpreted as parentless lesson. * If param is false (it's by default) - than this argument will be ignored. * @param int/bool id of pre-breakpoint-lesson. * It means, this lesson will not be included in pathes (all childs of this * lesson will be interpreted as parentless lessons). * If param is false (it's by default) - than this argument will be ignored. * @param array of edges to be ignored (interpreted as non-existing). * array must be array of such arrays: ('PARENT_LESSON' => #id#, 'CHILD_LESSON' => #id#) * * @return array of objects CLearnPath */ public static function GetListOfParentPathes ($lessonId, $breakOnLessonId = false, $breakBeforeLessonId = false, $arIgnoreEdges = array()); /** * Checks for probition of publishing for given lesson in context of given course. * * @param int lesson id to be checked for publish prohibition * @param int lesson id in context of which check. Must corresponds to course. * * @return bool true - if lesson is prohibited to be published in this course, otherwise - false. */ public static function IsPublishProhibited ($lessonId, $contextCourseLessonId); /** * * * @param int lesson id for publish (un)prohibition * @param int lesson id in context of which publish (un)prohibition will be done * @param bool if true - lesson will be prohibited for publish. If false - prohibition will be removed. * * @return bool if true - prohibition status changed, false - otherwise. If status not changed - it isn't error, * it means that status to be setted === status, that already set for lesson. */ public static function PublishProhibitionSetTo ($lessonId, $contextCourseLessonId, $isProhibited); } class CLearnLesson implements ILearnLesson { const GET_LIST_ALL = 0x0; // List any lessons const GET_LIST_IMMEDIATE_CHILDS_OF = 0x1; // List only immediate childs of requested parent_lesson_id const GET_LIST_IMMEDIATE_PARENTS_OF = 0x2; // List only immediate parents of requested parent_lesson_id // PUBLISH_PROHIBITION_PURGE_* constants can be ORed // Purge all prohibitions where given lessonId is contextCourse const PUBLISH_PROHIBITION_PURGE_ALL_LESSONS_IN_COURSE_CONTEXT = 0x1; // Purge all prohibitions for lessonId in all contextCourses const PUBLISH_PROHIBITION_PURGE_LESSON_IN_ALL_COURSE_CONTEXT = 0x2; // Purge all prohibitions for given lessonId (as course, and as prohibited lesson) const PUBLISH_PROHIBITION_PURGE_BOTH = 0x3; final public static function Add ($arFields, $isCourse = false, $parentLessonId = true, $arProperties = array('SORT' => 500), $isCheckPermissions = true, $checkPermissionsForUserId = -1 // -1 means - for current logged user ) { global $USER_FIELD_MANAGER; $isAccessGranted = false; if ($isCheckPermissions) { if (CLearnAccessMacroses::CanUserAddLessonWithoutParentLesson( array('user_id' => $checkPermissionsForUserId) ) ) { if ($parentLessonId === true) { // we don't need to link lesson to parent, // so permissions check is complete $isAccessGranted = true; } else { // We must check, is user have access to link lesson to some parent if (CLearnAccessMacroses::CanUserAddLessonToParentLesson ( array( 'parent_lesson_id' => $parentLessonId, 'user_id' => $checkPermissionsForUserId ) ) ) { $isAccessGranted = true; } } } } else $isAccessGranted = true; // don't check permissions if ( ! $isAccessGranted ) { throw new LearnException( 'EA_ACCESS_DENIED', LearnException::EXC_ERR_ALL_ACCESS_DENIED); } // If lesson is course, there is can be additional params, which must be extracted if ($isCourse) { // Additional fields will be removed from $arFields by this method $arCourseFields = self::_ExtractAdditionalCourseFields ($arFields); } if ( ! $USER_FIELD_MANAGER->CheckFields('LEARNING_LESSONS', 0, $arFields) ) return (false); foreach(GetModuleEvents('learning', 'OnBeforeLessonAdd', true) as $arEvent) ExecuteModuleEventEx($arEvent, array(&$arFields)); if ( ( ! isset($arFields['NAME']) ) || ($arFields['NAME'] == '') ) { $lessonId = false; $arMsg = array(array("id"=>"NAME", "text"=> GetMessage("LEARNING_BAD_NAME"))); $e = new CAdminException($arMsg); $GLOBALS["APPLICATION"]->ThrowException($e); } else $lessonId = CLearnGraphNode::Create ($arFields); if ($lessonId) { $USER_FIELD_MANAGER->Update('LEARNING_LESSONS', $lessonId, $arFields); if ($isCourse) { // Convert lesson to course self::BecomeCourse ($lessonId, $arCourseFields); } else { // Link to parent lesson, if need if ($parentLessonId !== true) self::RelationAdd ($parentLessonId, $lessonId, $arProperties); } CLearnCacheOfLessonTreeComponent::MarkAsDirty(); } $arFields['LESSON_ID'] = $lessonId; foreach(GetModuleEvents('learning', 'OnAfterLessonAdd', true) as $arEvent) ExecuteModuleEventEx($arEvent, array(&$arFields)); if (!$isCourse) { \Bitrix\Learning\Integration\Search::indexLesson($lessonId); } return ($lessonId); } protected static function _ExtractAdditionalCourseFields (&$arFields) { $arCourseFields = array(); if (array_key_exists('SORT', $arFields) && ( ! array_key_exists('COURSE_SORT', $arFields)) ) { // If SORT given, but COURSE_SORT not given => COURSE_SORT = SORT $arFields['COURSE_SORT'] = $arFields['SORT']; // So, if both SORT and COURSE_SORT are exists => SORT ignored. } // We must unset course-related fields if (array_key_exists('SORT', $arFields)) unset ($arFields['SORT']); $additionalParams = array ('COURSE_SORT', 'ACTIVE_FROM', 'ACTIVE_TO', 'RATING', 'RATING_TYPE', 'SCORM'); foreach ($additionalParams as $paramName) { if (array_key_exists($paramName, $arFields)) { $arCourseFields[$paramName] = $arFields[$paramName]; unset ($arFields[$paramName]); // We must unset course-related fields } } return ($arCourseFields); } /** * Canonize and checks additional params when adding course * @throws LearnException with error bit set LearnException::EXC_ERR_ALL_PARAMS * @return array of canonized params */ protected static function _CanonizeAndCheckAdditionalParamsForAddCourse ($arFields, $forUpdate = false) { if ( ! is_array($arFields) ) throw new LearnException ('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); $arAllowedFields = array ('COURSE_SORT', 'ACTIVE_FROM', 'ACTIVE_TO', 'RATING', 'RATING_TYPE', 'SCORM'); if ( ! $forUpdate ) { $defaultsValues = array ( 'COURSE_SORT' => 500, 'ACTIVE_FROM' => NULL, 'ACTIVE_TO' => NULL, 'RATING' => 'N', 'RATING_TYPE' => NULL, 'SCORM' => 'N' ); // set defaults values, if need foreach ($defaultsValues as $fieldName => $defaultValue) { if ( ! array_key_exists($fieldName, $arFields) ) $arFields[$fieldName] = $defaultValue; } } // check for admitted regions (do all checks only if not forUpdate mode OR in forUpdate mode and field given): // COURSE_SORT if ( ( ! $forUpdate) || array_key_exists('COURSE_SORT', $arFields) ) self::_EnsureArgsStrictlyCastableToIntegers ($arFields['COURSE_SORT']); // ACTIVE_FROM if ( ( ! $forUpdate) || isset($arFields['ACTIVE_FROM']) ) { if ( ($arFields['ACTIVE_FROM'] !== NULL) && ( ! is_string($arFields['ACTIVE_FROM']) ) ) { throw new LearnException ('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); } } // ACTIVE_TO if ( ( ! $forUpdate) || isset($arFields['ACTIVE_TO']) ) { if ( ($arFields['ACTIVE_TO'] !== NULL) && ( ! is_string($arFields['ACTIVE_TO']) ) ) { throw new LearnException ('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); } } // RATING if ( ( ! $forUpdate) || array_key_exists('RATING', $arFields) ) { if ($arFields['RATING'] === '') $arFields['RATING'] = NULL; if ( ! in_array ($arFields['RATING'], array ('Y', 'N', NULL), true) ) throw new LearnException ('EA_PARAMS: RATING is ' . $arFields['RATING'], LearnException::EXC_ERR_ALL_PARAMS); } // RATING_TYPE if ( ( ! $forUpdate) || array_key_exists('RATING_TYPE', $arFields) ) { if ( ($arFields['RATING_TYPE'] !== NULL) && ( ! in_array ( $arFields['RATING_TYPE'], array ('like', 'standart_text', 'like_graphic', 'standart'), true) ) ) { throw new LearnException ('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); } } // SCORM if ( ( ! $forUpdate) || array_key_exists('SCORM', $arFields) ) { if ( ! in_array ($arFields['SCORM'], array ('Y', 'N'), true) ) throw new LearnException ('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); } // Return only exists fields (some fields may be omitted in case $forUpdate = true) $rc = array(); foreach ($arAllowedFields as $fieldName) { if (array_key_exists($fieldName, $arFields)) $rc[$fieldName] = $arFields[$fieldName]; } return ($rc); } final public static function Update ($id, $arFields) { global $DB, $USER_FIELD_MANAGER; if ( isset($arFields['ACTIVE']) && ( ! is_bool($arFields['ACTIVE']) ) ) { if ($arFields['ACTIVE'] === 'Y') $arFields['ACTIVE'] = true; else $arFields['ACTIVE'] = false; } if ( ! $USER_FIELD_MANAGER->CheckFields('LEARNING_LESSONS', $id, $arFields) ) return (false); $courseId = self::GetLinkedCourse ($id); // if lesson is course, extract additional fields of course if ($courseId !== false) { // Additional fields will be removed from $arFields by this method $arCourseFields = self::_ExtractAdditionalCourseFields ($arFields); } foreach(GetModuleEvents('learning', 'OnBeforeLessonUpdate', true) as $arEvent) ExecuteModuleEventEx($arEvent, array(&$arFields)); if ( array_key_exists('NAME', $arFields) && ($arFields['NAME'] == '') ) { $lessonId = false; $arMsg = array(array("id"=>"NAME", "text"=> GetMessage("LEARNING_BAD_NAME"))); $e = new CAdminException($arMsg); $GLOBALS["APPLICATION"]->ThrowException($e); return(false); } $USER_FIELD_MANAGER->Update('LEARNING_LESSONS', $id, $arFields); // Update main lesson data CLearnGraphNode::Update ($id, $arFields); // If lesson is course, update course-specific data if ($courseId !== false) { // LearnException will be throwed on invalid params $arCourseFields = self::_CanonizeAndCheckAdditionalParamsForAddCourse ($arCourseFields, true); // forUpdate = true $arFieldsToDb = array(); if (array_key_exists('COURSE_SORT', $arCourseFields)) $arFieldsToDb['SORT'] = "'" . (int) ($arCourseFields['COURSE_SORT'] + 0) . "'"; if (array_key_exists('ACTIVE_FROM', $arCourseFields)) { if (($arCourseFields['ACTIVE_FROM'] === NULL) || ($arCourseFields['ACTIVE_FROM'] === '')) $arFieldsToDb['ACTIVE_FROM'] = 'NULL'; else $arFieldsToDb['ACTIVE_FROM'] = $DB->CharToDateFunction($arCourseFields['ACTIVE_FROM']); } if (array_key_exists('ACTIVE_TO', $arCourseFields)) { if (($arCourseFields['ACTIVE_TO'] === NULL) || ($arCourseFields['ACTIVE_TO'] === '')) $arFieldsToDb['ACTIVE_TO'] = 'NULL'; else $arFieldsToDb['ACTIVE_TO'] = $DB->CharToDateFunction($arCourseFields['ACTIVE_TO']); } if (array_key_exists('RATING', $arCourseFields)) $arFieldsToDb['RATING'] = "'" . $DB->ForSql($arCourseFields['RATING']) . "'"; if (array_key_exists('RATING_TYPE', $arCourseFields)) { if ($arCourseFields['RATING_TYPE'] === NULL) $arFieldsToDb['RATING_TYPE'] = 'NULL'; else $arFieldsToDb['RATING_TYPE'] = "'" . $DB->ForSql($arCourseFields['RATING_TYPE']) . "'"; } if (array_key_exists('SCORM', $arCourseFields)) $arFieldsToDb['SCORM'] = "'" . $DB->ForSql($arCourseFields['SCORM']) . "'"; // Does need update for some fields? if (count($arFieldsToDb) > 0) { $rc = $DB->Update ('b_learn_course', $arFieldsToDb, "WHERE ID='" . (int) ($courseId + 0) . "'", __LINE__, false, false); // we must halt on errors due to bug in CDatabase::Update(); // reload cache of LINKED_LESSON_ID -> COURSE_ID self::GetCourseToLessonMap_ReloadCache(); /** * This code will be useful after bug in CDatabase::Update() * and CDatabase::Insert() will be solved and $ignore_errors setted * to true in Insert()/Update() call above. */ if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); } } CLearnCacheOfLessonTreeComponent::MarkAsDirty(); foreach(GetModuleEvents('learning', 'OnAfterLessonUpdate', true) as $arEvent) ExecuteModuleEventEx($arEvent, array(&$arFields, $id)); \Bitrix\Learning\Integration\Search::indexLesson($id); return true; } protected static function _funcDelete_ParseOptions($lesson_id) { $simulate = false; // don't simulate by default $check_permissions = true; // check rights by default $user_id = -1; // -1 means 'current logged user' if (is_array($lesson_id)) { // Parse options $options = CLearnSharedArgManager::StaticParser( $lesson_id, array( 'lesson_id' => array( 'type' => 'strictly_castable_to_integer', 'mandatory' => true ), 'simulate' => array( 'type' => 'boolean', 'mandatory' => false, 'default_value' => $simulate ), 'check_permissions' => array( 'type' => 'boolean', 'mandatory' => false, 'default_value' => $check_permissions ), 'user_id' => array( 'type' => 'strictly_castable_to_integer', 'mandatory' => false, 'default_value' => $user_id ) ) ); $lesson_id = $options['lesson_id']; $simulate = $options['simulate']; $check_permissions = $options['check_permissions']; $user_id = $options['user_id']; } else $lesson_id = (int) $lesson_id; if ($check_permissions) { if ($user_id === -1) { global $USER; if ( ! (is_object($USER) && method_exists($USER, 'GetID')) ) { throw new LearnException( 'EA_OTHER: $USER isn\'t available.', LearnException::EXC_ERR_ALL_GIVEUP | LearnException::EXC_ERR_ALL_LOGIC); } $user_id = (int) $USER->GetID(); } } return (array($lesson_id, $simulate, $check_permissions, $user_id)); } final public static function DeleteRecursiveLikeHardlinks ($in_data) { list ($root_lesson_id, $simulate, $check_permissions, $user_id) = self::_funcDelete_ParseOptions($in_data); // list of lessons, which are candidates to be removed $arCandidatesToRemove = array(); // Build list of all descendants (excluded duplicated) $oTree = self::GetTree($root_lesson_id); $arDescendantsList = $oTree->GetLessonsIdListInTree(); // Transform list: add list of immediate parents to every candidate foreach ($arDescendantsList as $lesson_id) { $arParents = array(); $arEdges = self::ListImmediateParents($lesson_id); foreach ($arEdges as $arEdgeData) $arParents[] = (int) $arEdgeData['PARENT_LESSON']; $arCandidatesToRemove[(int) $lesson_id] = $arParents; } // Now, move out parents of root lesson, because they mustn't be checked below. $arCandidatesToRemove[$root_lesson_id] = array(); // Withdraw lessons, which has ancestors not among candidates to be removed do { $lessonsWithdrawn = 0; // count of withdrawn lessons foreach ($arCandidatesToRemove as $lesson_id => $arParents) { // Check that all parents are from candidates to be removed; // otherwise $lesson_id must be withdrew from candidates foreach ($arParents as $parent_lesson_id) { if ( ! array_key_exists((int) $parent_lesson_id, $arCandidatesToRemove) ) { unset($arCandidatesToRemove[(int) $lesson_id]); $lessonsWithdrawn++; break; // we don't need to check other parents for this lesson anymore } } } } while ($lessonsWithdrawn > 0); // Now, broke edges and remove lessons. // Broke edges to lessons in $arCandidatesToRemove list only. foreach ($arCandidatesToRemove as $lesson_id => $arParents) { try { self::Delete( array( 'lesson_id' => $lesson_id, 'simulate' => $simulate, 'check_permissions' => $check_permissions, 'user_id' => $user_id ) ); } catch (LearnException $e) { if ($e->GetCode() === LearnException::EXC_ERR_LL_UNREMOVABLE_CL) ; // course cannot be removed - ignore this error elseif ($e->GetCode() === LearnException::EXC_ERR_ALL_ACCESS_DENIED) { // if lesson not exists - ignore error (lesson to be deleted is already removed) $rsLesson = self::GetListUni( array(), array('LESSON_ID' => $lesson_id, 'CHECK_PERMISSIONS' => 'N'), array('LESSON_ID'), self::GET_LIST_ALL ); if ( ! $rsLesson->fetch() ) { ; // ignore this situation, it's OK } else { // bubble exception throw new LearnException ($e->GetMessage(), $e->GetCode()); } } else { // bubble exception throw new LearnException ($e->GetMessage(), $e->GetCode()); } } } } final public static function Delete ($lesson_id) { global $USER_FIELD_MANAGER; list ($lesson_id, $simulate, $check_permissions, $user_id) = self::_funcDelete_ParseOptions($lesson_id); if ($check_permissions) { $oAccess = CLearnAccess::GetInstance($user_id); if ( ! $oAccess->IsLessonAccessible($lesson_id, CLearnAccess::OP_LESSON_REMOVE) ) { throw new LearnException( 'EA_ACCESS_DENIED', LearnException::EXC_ERR_ALL_ACCESS_DENIED); } } // Parents and childs of the lesson $arNeighboursEdges = self::ListImmediateNeighbours ($lesson_id); // precache rights for lesson if ($check_permissions) { $IsLessonAccessibleFor_OP_LESSON_UNLINK_DESCENDANTS = $oAccess->IsLessonAccessible($lesson_id, CLearnAccess::OP_LESSON_UNLINK_DESCENDANTS); $IsLessonAccessibleFor_OP_LESSON_UNLINK_FROM_PARENTS = $oAccess->IsLessonAccessible($lesson_id, CLearnAccess::OP_LESSON_UNLINK_FROM_PARENTS); } foreach(GetModuleEvents('learning', 'OnBeforeLessonDelete', true) as $arEvent) ExecuteModuleEventEx($arEvent, array($lesson_id)); foreach ($arNeighboursEdges as $arEdge) { $child_lesson_id = (int) $arEdge['CHILD_LESSON']; $parent_lesson_id = (int) $arEdge['PARENT_LESSON']; if ($check_permissions) { $IsLessonAccessible = false; if ($child_lesson_id === $lesson_id) { // if we will be remove edge to parent - use precached rights for OP_LESSON_UNLINK_FROM_PARENTS $IsLessonAccessible = $IsLessonAccessibleFor_OP_LESSON_UNLINK_FROM_PARENTS && $oAccess->IsLessonAccessible($parent_lesson_id, CLearnAccess::OP_LESSON_UNLINK_DESCENDANTS); } elseif ($parent_lesson_id === $lesson_id) { // if we will be remove edge to child - use precached rights for OP_LESSON_UNLINK_DESCENDANTS $IsLessonAccessible = $IsLessonAccessibleFor_OP_LESSON_UNLINK_DESCENDANTS && $oAccess->IsLessonAccessible($child_lesson_id, CLearnAccess::OP_LESSON_UNLINK_FROM_PARENTS); } else { throw new LearnException( 'EA_FATAL: $lesson_id (' . $lesson_id . ') not equal to one of: $child_lesson_id (' . $child_lesson_id . '), $parent_lesson_id (' . $parent_lesson_id . ')', LearnException::EXC_ERR_ALL_LOGIC | LearnException::EXC_ERR_ALL_GIVEUP); } if ( ! $IsLessonAccessible ) { throw new LearnException( 'EA_ACCESS_DENIED', LearnException::EXC_ERR_ALL_ACCESS_DENIED); } if ($simulate === false) self::RelationRemove ($parent_lesson_id, $child_lesson_id); } } $linkedCourseId = self::GetLinkedCourse ($lesson_id); // If lesson is course, remove course if ($linkedCourseId !== false) { global $DB; if ($simulate === false) { if ( ! $DB->Query("DELETE FROM b_learn_course_site WHERE COURSE_ID = " . (int) $linkedCourseId, true) ) throw new LearnException ( 'EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); $rc = self::CourseBecomeLesson ($linkedCourseId); // if course cannot be converted to lesson - don't remove lesson if ($rc === false) { throw new LearnException ( 'EA_OTHER: lesson is unremovable because linked course is in use.', LearnException::EXC_ERR_LL_UNREMOVABLE_CL); } // reload cache of LINKED_LESSON_ID -> COURSE_ID self::GetCourseToLessonMap_ReloadCache(); } } // And remove lesson if ($simulate === false) { global $DB; $r = $DB->Query( "SELECT PREVIEW_PICTURE, DETAIL_PICTURE FROM b_learn_lesson WHERE ID = " . (int) $lesson_id, true); if ($r === false) { throw new LearnException( 'EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); } $arRes = $r->Fetch(); if ( ! $arRes ) { throw new LearnException( 'EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); } CFile::Delete($arRes['PREVIEW_PICTURE']); CFile::Delete($arRes['DETAIL_PICTURE']); // Remove questions $q = CLQuestion::GetList( array(), array('LESSON_ID' => $lesson_id) ); while($arQ = $q->Fetch()) { if ( ! CLQuestion::Delete($arQ['ID']) ) { throw new LearnException( 'EA_QUESTION_NOT_REMOVED', LearnException::EXC_ERR_ALL_GIVEUP); } } CLearnGraphNode::Remove($lesson_id); $USER_FIELD_MANAGER->delete('LEARNING_LESSONS', $lesson_id); CLearnCacheOfLessonTreeComponent::MarkAsDirty(); CEventLog::add(array( 'AUDIT_TYPE_ID' => 'LEARNING_REMOVE_ITEM', 'MODULE_ID' => 'learning', 'ITEM_ID' => 'L #' . $lesson_id, 'DESCRIPTION' => 'lesson removed' )); if (CModule::IncludeModule('search')) { CSearch::deleteIndex("learning", "U\\_%", "L".$lesson_id, null); } } if ($simulate === false) { foreach(GetModuleEvents('learning', 'OnAfterLessonDelete', true) as $arEvent) ExecuteModuleEventEx($arEvent, array($lesson_id)); } } final public static function GetByID($id) { return (self::GetList(array(), array('LESSON_ID' => $id))); } final public static function GetByIDAsArr($id) { global $DB; $arData = CLearnGraphNode::GetByID($id); // If lesson is course - get additional data $courseId = self::GetLinkedCourse ($id); if ($courseId !== false) { $rc = $DB->Query ( "SELECT SORT, ACTIVE_FROM, ACTIVE_TO, RATING, RATING_TYPE, SCORM FROM b_learn_course WHERE ID = '" . (int) ($courseId + 0) . "'", true // ignore errors ); if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); $arCourseData = $rc->Fetch(); if ( ($arCourseData === false) || ( ! isset($arCourseData['SORT']) ) ) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); $arData = array_merge($arData, $arCourseData); } // convert return data to expected form if ( isset($arData['ACTIVE']) && is_bool($arData['ACTIVE']) ) { if ($arData['ACTIVE']) $arData['ACTIVE'] = 'Y'; else $arData['ACTIVE'] = 'N'; } $arData['LESSON_ID'] = $arData['ID']; return ($arData); } final public static function GetLinkedCourse ($lessonId) { $arMap = self::GetCourseToLessonMap(); if ( ! isset($arMap['L' . $lessonId]) ) return (false); // no corresponded course // return id of corresponded course return ($arMap['L' . $lessonId]); } /** * This function is for internal use only. It's not a part of public API. * * @access private */ final public static function GetCourseToLessonMap($bRefreshCache = false) { static $arMap = array(); $bCacheHit = false; static $ttl = 1800; // seconds static $cacheId = 'fixed_cache_id'; static $cachePath = '/learning/coursetolessonmap/'; $oCache = new CPHPCache(); // Try to load from cache only if cache isn't dirty if ( ! $bRefreshCache ) { if ($oCache->InitCache($ttl, $cacheId, $cachePath)) { $arCached = $oCache->GetVars(); if (isset($arCached['arMap']) && is_array($arCached['arMap'])) { $arMap = $arCached['arMap']; $bCacheHit = true; } } } // Reload map from DB on cache miss or when cache is dirty if (( ! $bCacheHit ) || $bRefreshCache) { $oCache->CleanDir($cachePath); $arMap = self::GetCourseToLessonMap_LoadFromDB(); $oCache->InitCache($ttl, $cacheId, $cachePath); $oCache->StartDataCache($ttl, $cacheId, $cachePath); $oCache->EndDataCache(array('arMap' => $arMap)); } return ($arMap); } protected static function GetCourseToLessonMap_ReloadCache() { $bRefreshCache = true; self::GetCourseToLessonMap($bRefreshCache); } protected static function GetCourseToLessonMap_LoadFromDB() { global $DB; $arMap = array(); $rc = $DB->Query ( "SELECT ID, LINKED_LESSON_ID FROM b_learn_course WHERE 1 = 1", true // ignore errors ); if ($rc === false) { throw new LearnException ( 'EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); } while ($arData = $rc->Fetch()) { // skip invalid elements if ( ($arData['ID'] <= 0) || ($arData['LINKED_LESSON_ID'] <= 0) ) continue; $arMap['C' . $arData['ID']] = (int) $arData['LINKED_LESSON_ID']; $arMap['L' . $arData['LINKED_LESSON_ID']] = (int) $arData['ID']; } return ($arMap); } /** * WARNING: don't use this method, it's for internal use only * * Convert lesson to course (lesson will stay exists, but new course * binded to lesson will be created) * * WARNING: this method terminates (by die()/exit()) current execution flow * when SQL server error occured. It's due to bug in CDatabase::Update() in main * module (version info: * define("SM_VERSION","11.0.12"); * define("SM_VERSION_DATE","2012-02-21 17:00:00"); // YYYY-MM-DD HH:MI:SS * ) * * @param int $lessonId * @param array of pairs field => value for course additional params. Allowed are: * - SORT or COURSE_SORT: integer, 500 by default, sort order of courses. CORSE_SORT overrides SORT * - ACTIVE_FROM: datetime, NULL by default * - ACTIVE_TO: datetime, NULL by default * - RATING: string (1 char), 'N' by default * - RATING_TYPE: string, allowed values: NULL, "like", "standart_text", * "like_graphic", "standart", NULL by default * - SCORM: string (1 char), 'N' by default * * @return int course id * * @access private */ protected static function BecomeCourse ($lessonId, $arFields) { global $DB; self::_EnsureArgsStrictlyCastableToIntegers ($lessonId); // LearnException will be throwed on invalid params $arCourseFields = self::_CanonizeAndCheckAdditionalParamsForAddCourse ($arFields); $ACTIVE_FROM = $ACTIVE_TO = 'NULL'; if (($arCourseFields['ACTIVE_FROM'] !== NULL) && ($arCourseFields['ACTIVE_FROM'] !== '')) $ACTIVE_FROM = $DB->CharToDateFunction($arCourseFields['ACTIVE_FROM']); if (($arCourseFields['ACTIVE_TO'] !== NULL) && ($arCourseFields['ACTIVE_TO'] !== '')) $ACTIVE_TO = $DB->CharToDateFunction($arCourseFields['ACTIVE_TO']); $arFieldsToDb = array( 'LINKED_LESSON_ID' => "'" . (int) ($lessonId + 0) . "'", 'SORT' => "'" . (int) ($arCourseFields['COURSE_SORT'] + 0) . "'", 'ACTIVE_FROM' => $ACTIVE_FROM, 'ACTIVE_TO' => $ACTIVE_TO, 'RATING' => "'" . $DB->ForSql($arCourseFields['RATING']) . "'", 'RATING_TYPE' => ( ($arCourseFields['RATING_TYPE'] === NULL) ? 'NULL' : ("'" . $DB->ForSql($arCourseFields['RATING_TYPE']) . "'") ), 'SCORM' => "'" . $DB->ForSql($arCourseFields['SCORM']) . "'" ); $rc = $DB->Insert ('b_learn_course', $arFieldsToDb, __LINE__, // $error_position false, // $debug "", // $exist_id false // $ignore_errors, we must halt on errors due to bug in CDatabase::Insert(); ); // reload cache of LINKED_LESSON_ID -> COURSE_ID self::GetCourseToLessonMap_ReloadCache(); CLearnCacheOfLessonTreeComponent::MarkAsDirty(); /** * This code will be useful after bug in CDatabase::Update() * and CDatabase::Insert() will be solved and $ignore_errors setted * to true in Insert()/Update() call above. */ if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); return ( (int) $rc); // returns course_id } /** * WARNING: don't use this method, it's for internal use only * * Convert course to non-course lesson (course will be removed, * but lesson will stay exists) * * WARNING: this method terminates (by die()/exit()) current execution flow * when SQL server error occured. It's due to bug in CDatabase::Update() in main * module (version info: * define("SM_VERSION","11.0.12"); * define("SM_VERSION_DATE","2012-02-21 17:00:00"); // YYYY-MM-DD HH:MI:SS * ) * * @param int $courseId (returned by GetLinkedCourse($lessonId) ) * * @access private */ protected static function CourseBecomeLesson ($courseId) { global $DB; self::_EnsureArgsStrictlyCastableToIntegers ($courseId); $linkedLessonId = CCourse::CourseGetLinkedLesson ($courseId); if ($linkedLessonId === false) { return false; } // Check certificates (if exists => forbid removing course) $certificate = CCertification::GetList(Array(), Array("COURSE_ID" => $courseId, 'CHECK_PERMISSIONS' => 'N')); if ( ($certificate === false) || ($certificate->GetNext()) ) return false; // Remove tests $tests = CTest::GetList(Array(), Array("COURSE_ID" => $courseId)); if ($tests === false) return (false); while ($arTest = $tests->Fetch()) { if ( ! CTest::Delete($arTest["ID"]) ) return false; } // Remove all prohibitions for lessons in context of course to be removed // and remove prohibitions for course to be removed in context of all other courses self::PublishProhibitionPurge( $linkedLessonId, self::PUBLISH_PROHIBITION_PURGE_ALL_LESSONS_IN_COURSE_CONTEXT | self::PUBLISH_PROHIBITION_PURGE_LESSON_IN_ALL_COURSE_CONTEXT ); $rc = $DB->Query ( "DELETE FROM b_learn_course WHERE ID=" . (string) ((int) $courseId), true // $ignore_errors ); // reload cache of LINKED_LESSON_ID -> COURSE_ID self::GetCourseToLessonMap_ReloadCache(); CLearnCacheOfLessonTreeComponent::MarkAsDirty(); /** * This code will be useful after bug in CDatabase::Update() * and CDatabase::Insert() will be solved and $ignore_errors setted * to true in Insert()/Update() call above. */ if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); // If data not updated if ($rc === 0) throw new LearnException ('EA_OTHER: data not updated', LearnException::EXC_ERR_ALL_GIVEUP); } /** * @throws LearnException with error bit set LearnException::EXC_ERR_ALL_LOGIC * and errmessage "EA_PARAMS", if any of args isn't of integer type * or can't be strictly casted to integer. */ protected static function _EnsureArgsStrictlyCastableToIntegers () { $args = func_get_args(); foreach ($args as $arg) { if ( ( ! is_numeric($arg) ) || ( ! is_int($arg + 0) ) ) { throw new LearnException ('EA_PARAMS', LearnException::EXC_ERR_ALL_LOGIC | LearnException::EXC_ERR_ALL_PARAMS); } } return (true); } final public static function RelationAdd ($parentLessonId, $childLessonId, $arProperties) { CLearnGraphRelation::Link ($parentLessonId, $childLessonId, $arProperties); CLearnCacheOfLessonTreeComponent::MarkAsDirty(); Bitrix\Learning\Integration\Search::indexLesson($parentLessonId); Bitrix\Learning\Integration\Search::indexLesson($childLessonId); } final public static function RelationUpdate ($parentLessonId, $childLessonId, $arProperties) { foreach ($arProperties as $propertyName => $value) CLearnGraphRelation::SetProperty ($parentLessonId, $childLessonId, $propertyName, $value); CLearnCacheOfLessonTreeComponent::MarkAsDirty(); } final public static function RelationGet ($parentLessonId, $childLessonId) { $rc = array(); $rc['SORT'] = CLearnGraphRelation::GetProperty ($parentLessonId, $childLessonId, 'SORT'); return ($rc); } final public static function RelationRemove ($parentLessonId, $childLessonId) { self::PublishProhibitionPurge_OnBeforeRelationRemove ($parentLessonId, $parentLessonId); CLearnGraphRelation::Unlink ($parentLessonId, $childLessonId); CLearnCacheOfLessonTreeComponent::MarkAsDirty(); Bitrix\Learning\Integration\Search::indexLesson($parentLessonId); Bitrix\Learning\Integration\Search::indexLesson($childLessonId); } final public static function ListImmediateParents($lessonId) { return (CLearnGraphRelation::ListImmediateParents ($lessonId)); } final public static function ListImmediateChilds($lessonId) { return (CLearnGraphRelation::ListImmediateChilds ($lessonId)); } final public static function ListImmediateNeighbours($lessonId) { return (CLearnGraphRelation::ListImmediateNeighbours ($lessonId)); } protected static function GetListUni ($arOrder = array(), $arFilter = array(), $arSelectFields = array(), $mode = self::GET_LIST_ALL, $lessonId = -1, $arNavParams = array()) { global $DB, $USER_FIELD_MANAGER; $obUserFieldsSql = new CUserTypeSQL(); $obUserFieldsSql->SetEntity('LEARNING_LESSONS', 'TL.ID'); $obUserFieldsSql->SetSelect($arSelectFields); $obUserFieldsSql->SetFilter($arFilter); $obUserFieldsSql->SetOrder($arOrder); $bReplaceCourseId = false; if (isset($arFilter['#REPLACE_COURSE_ID_TO_ID'])) { $bReplaceCourseId = true; unset($arFilter['#REPLACE_COURSE_ID_TO_ID']); } $oPermParser = new CLearnParsePermissionsFromFilter ($arFilter); // For ordering $arMap = array( 'lesson_id' => 'TL.ID', 'site_id' => 'TL.ID', // hack for compatibility with courses in shared lists 'name' => 'TL.NAME', 'code' => 'TL.CODE', 'active' => 'TL.ACTIVE', 'created' => 'TL.DATE_CREATE', // 'created' was in previous code, perhaps for back compatibility 'date_create' => 'TL.DATE_CREATE', 'created_by' => 'TL.CREATED_BY', 'timestamp_x' => 'TL.TIMESTAMP_X', 'course_id' => 'TC.ID', 'course_sort' => 'TC.SORT', 'active_from' => 'TC.ACTIVE_FROM', // ! This will be overrided below to TLE.SORT in case of self::GET_LIST_IMMEDIATE_CHILDS_OF 'sort' => 'TC.SORT', 'linked_lesson_id' => 'TC.LINKED_LESSON_ID' // This element is dynamically added below for case of self::GET_LIST_IMMEDIATE_CHILDS_OF // 'edge_sort' => 'TLE.SORT' ); $allowedModes = array( self::GET_LIST_ALL, self::GET_LIST_IMMEDIATE_CHILDS_OF, self::GET_LIST_IMMEDIATE_PARENTS_OF, self::GET_LIST_IMMEDIATE_CHILDS_OF | self::GET_LIST_IMMEDIATE_PARENTS_OF ); $argsCheck = is_array($arOrder) && is_array($arSelectFields) && in_array($mode, $allowedModes, true) && self::_EnsureArgsStrictlyCastableToIntegers ($lessonId); if ( ! $argsCheck ) throw new LearnException('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); $arFieldsMap = array( 'LESSON_ID' => 'TL.ID', 'SITE_ID' => 'CASE WHEN (1 > 0) THEN \'no site\' ELSE \'0\' END', // hack for compatibility with courses in shared lists 'WAS_CHAPTER_ID' => 'TL.WAS_CHAPTER_ID', 'KEYWORDS' => 'TL.KEYWORDS', 'CHILDS_CNT' => '(SELECT COUNT(*) FROM b_learn_lesson_edges TLES WHERE TLES.SOURCE_NODE = TL.ID)', 'IS_CHILDS' => 'CASE WHEN (SELECT COUNT(*) FROM b_learn_lesson_edges TLES WHERE TLES.SOURCE_NODE = TL.ID) > 0 THEN \'1\' ELSE \'0\' END', 'SORT' => 'TC.SORT', 'TIMESTAMP_X' => $DB->DateToCharFunction('TL.TIMESTAMP_X'), 'DATE_CREATE' => $DB->DateToCharFunction('TL.DATE_CREATE'), 'CREATED_USER_NAME' => $DB->Concat("'('", 'TU.LOGIN', "') '", 'TU.NAME', "' '", 'TU.LAST_NAME'), 'CREATED_BY' => 'TL.CREATED_BY', 'ACTIVE' => 'TL.ACTIVE', 'NAME' => 'TL.NAME', 'PREVIEW_PICTURE' => 'TL.PREVIEW_PICTURE', 'PREVIEW_TEXT' => 'TL.PREVIEW_TEXT', 'PREVIEW_TEXT_TYPE' => 'TL.PREVIEW_TEXT_TYPE', 'DETAIL_TEXT' => 'TL.DETAIL_TEXT', 'DETAIL_PICTURE' => 'TL.DETAIL_PICTURE', 'DETAIL_TEXT_TYPE' => 'TL.DETAIL_TEXT_TYPE', 'LAUNCH' => 'TL.LAUNCH', 'CODE' => 'TL.CODE', 'ACTIVE_FROM' => $DB->DateToCharFunction('TC.ACTIVE_FROM'), 'ACTIVE_TO' => $DB->DateToCharFunction('TC.ACTIVE_TO'), 'RATING' => 'TC.RATING', 'RATING_TYPE' => 'TC.RATING_TYPE', 'SCORM' => 'TC.SCORM', 'LINKED_LESSON_ID' => 'TC.LINKED_LESSON_ID', 'COURSE_ID' => 'TC.ID', 'COURSE_SORT' => 'TC.SORT' ); // filter by TIMESTAMP_X by default if (count($arOrder) == 0) $arOrder['TIMESTAMP_X'] = 'DESC'; $arSqlSearch = self::GetFilter($arFilter, $mode); if (isset($arFilter['SITE_ID'])) { $arLID = array(); if (is_array($arFilter['SITE_ID'])) $arLID = $arFilter['SITE_ID']; else { if (strlen($arFilter['SITE_ID']) > 0) $arLID[] = $arFilter['SITE_ID']; } $SqlSearchLang = "''"; foreach ($arLID as $v) $SqlSearchLang .= ", '" . $DB->ForSql($v) . "'"; } $r = $obUserFieldsSql->GetFilter(); if (strlen($r) > 0) $arSqlSearch[] = "(".$r.")"; $sqlSearch = ''; foreach ($arSqlSearch as $value) { if (strlen($value) > 0) $sqlSearch .= ' AND ' . $value; } $modeSQL_join = $modeSQL_where = ''; $modeSQL_defaultSortField = "TC.SORT"; // as SORT // Prepare SQL's joins, if $mode need it if ($mode & self::GET_LIST_IMMEDIATE_PARENTS_OF) { $modeSQL_join .= "\nINNER JOIN b_learn_lesson_edges TLE ON TLE.SOURCE_NODE = TL.ID\n"; $modeSQL_where .= "\nAND TLE.TARGET_NODE = " . ($lessonId + 0) . "\n"; $arFieldsMap['EDGE_SORT'] = 'TLE.SORT'; $arFieldsMap['SORT'] = 'TLE.SORT'; } if ($mode & self::GET_LIST_IMMEDIATE_CHILDS_OF) { /** * GROUP BY works for MySQL, MSSQL, Oracle * select a.id, a.NAME, count(b.USER_ID) as C * from b_group a, b_user_group b * where a.id = b.GROUP_ID * group by a.id, a.NAME * order by C */ $modeSQL_join .= "\nINNER JOIN b_learn_lesson_edges TLE ON TLE.TARGET_NODE = TL.ID\n"; $modeSQL_where .= "\nAND TLE.SOURCE_NODE = " . ($lessonId + 0) . "\n"; $arMap['childs_cnt'] = 'CHILDS_CNT'; $arMap['is_childs'] = 'IS_CHILDS'; $arMap['edge_sort'] = 'TLE.SORT'; // Override default sort $arMap['sort'] = $arMap['edge_sort']; $modeSQL_defaultSortField = "TLE.SORT"; // as SORT $arFieldsMap['EDGE_SORT'] = 'TLE.SORT'; $arFieldsMap['SORT'] = 'TLE.SORT'; } if ($bReplaceCourseId) $arFieldsMap['ID'] = $arFieldsMap['COURSE_ID']; // Select all fields by default if (count($arSelectFields) == 0) $arSelectFields = array_keys($arFieldsMap); // Ensure that all order fields will be selected foreach ($arOrder as $by => $order) { $fieldName = strtoupper($by); if ( ! in_array($fieldName, $arSelectFields) ) $arSelectFields[] = $fieldName; } // Build list of fields to be selected $strSqlSelect = ''; $bFirstPass = true; $bDefaultSortFieldSelected = false; foreach ($arSelectFields as $selectFieldName) { if (substr($selectFieldName, 0, 3) === 'UF_') continue; if (!$bFirstPass) $strSqlSelect .= ', '; else $bFirstPass = false; if (!isset($arFieldsMap[$selectFieldName])) { throw new LearnException( 'EA_OTHER: UNKNOWN FIELD: ' . $selectFieldName, LearnException::EXC_ERR_ALL_GIVEUP); } $strSqlSelect .= $arFieldsMap[$selectFieldName] . ' AS ' . $selectFieldName; if ( ($selectFieldName === 'SORT') && ($arFieldsMap[$selectFieldName] === $modeSQL_defaultSortField) ) { $bDefaultSortFieldSelected = true; } } if ( ! $bDefaultSortFieldSelected ) { if ($strSqlSelect !== '') $strSqlSelect .= ', '; $strSqlSelect .= $modeSQL_defaultSortField . ' AS SORT'; } $strSqlSelect .= $obUserFieldsSql->GetSelect(); $sqlLangConstraint = ''; if (strlen($SqlSearchLang) > 2) { $sqlLangConstraint = " AND EXISTS ( SELECT 'x' FROM b_learn_course_site TCS WHERE TC.ID = TCS.COURSE_ID AND TCS.SITE_ID IN (" . $SqlSearchLang . ") ) "; } $strSqlFrom = "FROM b_learn_lesson TL LEFT JOIN b_learn_course TC ON TC.LINKED_LESSON_ID = TL.ID LEFT JOIN b_user TU ON TU.ID = TL.CREATED_BY " . $modeSQL_join // for getting only parents/childs, if need . $obUserFieldsSql->GetJoin("TL.ID") . " WHERE 1 = 1 " . $sqlLangConstraint // filter by site IDs . $modeSQL_where; // for getting only parents/childs, if need if ($oPermParser->IsNeedCheckPerm()) $strSqlFrom .= " AND TL.ID IN (" . $oPermParser->SQLForAccessibleLessons() . ") "; $strSqlFrom .= $sqlSearch; $sql = "SELECT " . $strSqlSelect . " " . $strSqlFrom; $arSqlOrder = array(); foreach($arOrder as $by => $order) { $by = strtolower($by); $order = strtolower($order); if ($order !== 'asc') $order = 'desc'; if ($s = $obUserFieldsSql->getOrder(strtolower($by))) $arSqlOrder[] = ' ' . $s . ' ' . $order . ' '; if (substr($by, 0, 3) !== 'uf_') { if ( ! isset($arMap[$by]) ) { throw new LearnException( 'EA_PARAMS: unknown order by field: "' . $by . '"', LearnException::EXC_ERR_ALL_PARAMS ); } } $arSqlOrder[] = ' ' . $arMap[$by] . ' ' . $order . ' '; } // on duplicate first occured FIELD will be used according to function description DelDuplicateSort($arSqlOrder); $sql .= ' ORDER BY ' . implode(', ', $arSqlOrder); if (is_array($arNavParams) && ( ! empty($arNavParams) ) ) { if (isset($arNavParams['nTopCount']) && ((int) $arNavParams['nTopCount'] > 0)) { $sql = $DB->TopSql($sql, (int) $arNavParams['nTopCount']); $res = $DB->Query($sql, true); } else { $res_cnt = $DB->Query("SELECT COUNT(TL.ID) as C " . $strSqlFrom); $res_cnt = $res_cnt->fetch(); $res = new CDBResult(); $rc = $res->NavQuery($sql, $res_cnt['C'], $arNavParams, true); if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); } } else $res = $DB->Query($sql, true); if ($res === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); $res->SetUserFields($USER_FIELD_MANAGER->GetUserFields('LEARNING_LESSONS')); return ($res); } final public static function GetList ($arOrder = array(), $arFilter = array(), $arSelectFields = array(), $arNavParams = array()) { return (self::GetListUni($arOrder, $arFilter, $arSelectFields, self::GET_LIST_ALL, -1, $arNavParams)); } final public static function GetListOfImmediateChilds ($lessonId, $arOrder = array(), $arFilter = array(), $arSelectFields = array(), $arNavParams = array()) { return (self::GetListUni($arOrder, $arFilter, $arSelectFields, self::GET_LIST_IMMEDIATE_CHILDS_OF, $lessonId, $arNavParams)); } final public static function GetListOfImmediateParents ($lessonId, $arOrder = array(), $arFilter = array(), $arSelectFields = array(), $arNavParams = array()) { return (self::GetListUni($arOrder, $arFilter, $arSelectFields, self::GET_LIST_IMMEDIATE_PARENTS_OF, $lessonId, $arNavParams)); } final public static function GetTree ( $lessonId, $arOrder = array ('EDGE_SORT' => 'asc'), $arFilter = array(), $publishProhibitionMode = true, $arSelectFields = array() ) { return (new CLearnLessonTree ($lessonId, $arOrder, $arFilter, $publishProhibitionMode, $arSelectFields)); } /** * @access protected * @throws LearnException with error bit set EXC_ERR_ALL_PARAMS */ protected static function GetFilter($arFilter = array(), $mode) { global $DB; if ( ! is_array($arFilter) ) throw new LearnException ('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); $arSqlSearch = array(); foreach ($arFilter as $key => $val) { $res = CLearnHelper::MkOperationFilter($key); $key = $res["FIELD"]; $cOperationType = $res["OPERATION"]; $key = strtoupper($key); switch ($key) { // for courses only case 'COURSE_ID': $arSqlSearch[] = CLearnHelper::FilterCreate('TC.ID', $val, 'number', $bFullJoin, $cOperationType); break; case 'COURSE_SORT': $arSqlSearch[] = CLearnHelper::FilterCreate('TC.SORT', $val, 'number', $bFullJoin, $cOperationType); break; case 'EDGE_SORT': // edges table (TLE) available only if requested immediate childs of some parent lesson if ($mode & self::GET_LIST_IMMEDIATE_CHILDS_OF) $arSqlSearch[] = CLearnHelper::FilterCreate('TLE.SORT', $val, 'number', $bFullJoin, $cOperationType); else throw new LearnException ('EA_PARAMS: unknown field ' . $key, LearnException::EXC_ERR_ALL_PARAMS); break; case 'SORT': if ($mode & self::GET_LIST_IMMEDIATE_CHILDS_OF) { // edges table (TLE) available only if requested immediate childs of some parent lesson $arSqlSearch[] = CLearnHelper::FilterCreate('TLE.SORT', $val, 'number', $bFullJoin, $cOperationType); } else { // so, by default sort by b_learn_course.SORT (for partially backward compatibility) $arSqlSearch[] = CLearnHelper::FilterCreate('TC.SORT', $val, 'number', $bFullJoin, $cOperationType); } break; case 'LINKED_LESSON_ID': $arSqlSearch[] = CLearnHelper::FilterCreate('TC.' . $key, $val, 'number', $bFullJoin, $cOperationType, false); break; case 'CHILDS_CNT': $arSqlSearch[] = CLearnHelper::FilterCreate('(SELECT COUNT(*) FROM b_learn_lesson_edges TLES WHERE TLES.SOURCE_NODE = TL.ID)', $val, 'number', $bFullJoin, $cOperationType); break; case 'ACTIVE_FROM': case 'ACTIVE_TO': if (strlen($val) > 0) { $arSqlSearch[] = "(TC." . $key . " " . ($cOperationType == "N" ? "<" : ">=") . $DB->CharToDateFunction($DB->ForSql($val), "FULL") . ($cOperationType == "N" ? "" : " OR TC.ACTIVE_FROM IS NULL") . ")"; } break; case "ACTIVE_DATE": if(strlen($val) > 0) { $arSqlSearch[] = ($cOperationType == "N" ? " NOT" : "") . "((TC.ACTIVE_TO >= " . $DB->GetNowFunction() ." OR TC.ACTIVE_TO IS NULL) AND (TC.ACTIVE_FROM <= " . $DB->GetNowFunction() . " OR TC.ACTIVE_FROM IS NULL))"; } break; case "DATE_ACTIVE_TO": case "DATE_ACTIVE_FROM": $arSqlSearch[] = CLearnHelper::FilterCreate("TC." . $key, $val, 'date', $bFullJoin, $cOperationType); break; case 'RATING_TYPE': $arSqlSearch[] = CLearnHelper::FilterCreate('TC.' . $key, $val, 'string', $bFullJoin, $cOperationType); break; case 'RATING': case 'SCORM': $arSqlSearch[] = CLearnHelper::FilterCreate('TC.' . $key, $val, 'string_equal', $bFullJoin, $cOperationType); break; // for all lessons case 'WAS_CHAPTER_ID': $arSqlSearch[] = CLearnHelper::FilterCreate('TL.WAS_CHAPTER_ID', $val, 'number', $bFullJoin, $cOperationType); break; case 'LESSON_ID': $arSqlSearch[] = CLearnHelper::FilterCreate('TL.ID', $val, 'number', $bFullJoin, $cOperationType); break; case 'CREATED_BY': $arSqlSearch[] = CLearnHelper::FilterCreate('TL.' . $key, $val, 'number', $bFullJoin, $cOperationType); break; case 'NAME': case 'CODE': case 'LAUNCH': case 'DETAIL_TEXT': case 'DETAIL_TEXT_TYPE': case 'PREVIEW_TEXT': case 'PREVIEW_TEXT_TYPE': $arSqlSearch[] = CLearnHelper::FilterCreate('TL.' . $key, $val, 'string', $bFullJoin, $cOperationType); break; case 'CREATED_USER_NAME': $arSqlSearch[] = CLearnHelper::FilterCreate($DB->Concat("'('", 'TU.LOGIN', "') '", 'TU.NAME', "' '", 'TU.LAST_NAME'), $val, 'string', $bFullJoin, $cOperationType); break; case 'KEYWORDS': $arSqlSearch[] = CLearnHelper::FilterCreate('TL.' . $key, $val, 'string', $bFullJoin, $cOperationType); break; case 'ACTIVE': $arSqlSearch[] = CLearnHelper::FilterCreate('TL.' . $key, $val, 'string_equal', $bFullJoin, $cOperationType); break; case 'TIMESTAMP_X': case 'DATE_CREATE': $arSqlSearch[] = CLearnHelper::FilterCreate('TL.' . $key, $val, 'date', $bFullJoin, $cOperationType); break; case 'SITE_ID': break; case 'CHECK_PERMISSIONS': case 'CHECK_PERMISSIONS_FOR_USER_ID': case 'ACCESS_OPERATIONS': // this is meta-fields, nothing to do with them here break; default: if (substr($key, 0, 3) !== 'UF_') throw new LearnException ('EA_PARAMS: unknown field ' . $key, LearnException::EXC_ERR_ALL_PARAMS); break; } } return $arSqlSearch; } final public static function CountImmediateChilds ($lessonId) { if ( ! self::_EnsureArgsStrictlyCastableToIntegers ($lessonId) ) throw new LearnException('EA_PARAMS', LearnException::EXC_ERR_ALL_PARAMS); global $DB; $rc = $DB->Query ( "SELECT COUNT(TARGET_NODE) AS CHILDS_COUNT FROM b_learn_lesson_edges WHERE SOURCE_NODE = '" . (int) ($lessonId + 0) . "'", true // ignore errors ); if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); $arData = $rc->Fetch(); if ( ($arData === false) || ( ! isset($arData['CHILDS_COUNT']) ) ) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); return ( (int) ($arData['CHILDS_COUNT'] + 0) ); } /** * This function is DEPRECATED * * Get lesson id of lesson, previously was chapter (before convertion to new data model. * * WARNING: This function is for backward compatibility of old-style * links to courses, chapters, lessons resolving in components. * * Don't use it in new projects, when there is no old-style links. * * @access public */ final public static function LessonIdByChapterId ($chapterId) { $rc = self::GetListUni( array(), array( 'WAS_CHAPTER_ID' => $chapterId, 'CHECK_PERMISSIONS' => 'N' ), array('LESSON_ID'), self::GET_LIST_ALL ); if ($rc === false) throw new LearnException ('EA_UNKNOWN_ERROR', LearnException::EXC_ERR_ALL_GIVEUP); $row = $rc->Fetch(); if ( ! isset($row['LESSON_ID']) ) return (false); else return ( (int) $row['LESSON_ID'] ); } /** * @access private */ final public static function GetListOfAncestors ($lessonId, $stopAtLessonId = false, $stopBeforeLessonId = false, $arIgnoreEdges = array()) { $arAncestors = array(); $arOPathes = self::GetListOfParentPathes ($lessonId, $stopAtLessonId, $stopBeforeLessonId, $arIgnoreEdges); foreach ($arOPathes as $oPath) $arAncestors = array_merge($arAncestors, array_map('intval', $oPath->GetPathAsArray())); array_unique($arAncestors); return ($arAncestors); } /** * @access public */ final public static function GetListOfParentPathes ($lessonId, $breakOnLessonId = false, $breakBeforeLesson = false, $arIgnoreEdges = array()) { $arPathes = array( array($lessonId) ); $arAlreadyProcessedLessons = array($lessonId); if ($breakOnLessonId !== false) { // This lesson must be interpreted as parentless. // This behaviour can be emulated by adding to // $arAlreadyProcessedLessons all immediate parents // of this lesson. $arEdges = self::ListImmediateParents($breakOnLessonId); foreach ($arEdges as $arEdge) $arAlreadyProcessedLessons[] = (int) $arEdge['PARENT_LESSON']; } if ($breakBeforeLesson !== false) $arAlreadyProcessedLessons[] = (int) $breakBeforeLesson; $arAllPathes = self::GetListOfParentPathesRecursive ($arPathes, $arAlreadyProcessedLessons, $arIgnoreEdges); $arObjPathes = array(); foreach ($arAllPathes as $arPathBackward) { $arPath = array_reverse($arPathBackward); $o = new CLearnPath ($arPath); $o->PopBottom(); // remove $lessonId // skip empty pathes if ($o->Count() > 0) $arObjPathes[] = $o; } return ($arObjPathes); } protected static function GetListOfParentPathesRecursive ($arPathes, &$arAlreadyProcessedLessons, $arIgnoreEdges = array()) { $arPathesNew = $arPathes; $must_be_stopped = 0x1; // stop if no more parents available or finally cycled foreach ($arPathes as $key => $arPath) { $lessonId = $arPath[count($arPath) - 1]; $arEdges = self::ListImmediateParents($lessonId); $arParents = array(); foreach ($arEdges as $arEdge) { $parentLessonId = (int) $arEdge['PARENT_LESSON']; if ( ! in_array($parentLessonId, $arAlreadyProcessedLessons, true) ) { $isEdgeIgnored = false; foreach ($arIgnoreEdges as $arIgnoreEdge) { if ( ($arIgnoreEdge['PARENT_LESSON'] == $arEdge['PARENT_LESSON']) && ($arIgnoreEdge['CHILD_LESSON'] == $arEdge['CHILD_LESSON']) ) { $isEdgeIgnored = true; break; } } if ( ! $isEdgeIgnored ) { $arParents[] = $parentLessonId; // Precache already processed lesson (for prevent cycling) $arAlreadyProcessedLessons[] = $parentLessonId; } } } $must_be_stopped &= (int) (count($arParents) === 0); // true evaluted to 1 $i = 0; foreach ($arParents as $parentLessonId) { $parentLessonId = (int) $parentLessonId; if ( $i || $i++ ) // executed only for all except first lesson in $arParents { $arPathTmp = $arPath; $arPathTmp[] = $parentLessonId; $arPathesNew[] = $arPathTmp; } else { $arPathesNew[$key][] = $parentLessonId; } } } if ($must_be_stopped) return ($arPathesNew); return (self::GetListOfParentPathesRecursive($arPathesNew, $arAlreadyProcessedLessons, $arIgnoreEdges)); } final public static function IsPublishProhibited ($in_lessonId, $in_contextCourseLessonId) { global $DB; self::_EnsureArgsStrictlyCastableToIntegers ($in_lessonId, $in_contextCourseLessonId); $lessonId = (int) $in_lessonId; $contextCourseLessonId = (int) $in_contextCourseLessonId; $rc = $DB->Query ( "SELECT COURSE_LESSON_ID FROM b_learn_publish_prohibition WHERE COURSE_LESSON_ID = $contextCourseLessonId AND PROHIBITED_LESSON_ID = $lessonId ", true // ignore errors ); if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); $arData = $rc->Fetch(); if ($arData === false) return (false); // lesson isn't prohibited for publish under given course return (true); // lesson is prohibited for publish } final public static function PublishProhibitionSetTo ($in_lessonId, $in_contextCourseLessonId, $in_isProhibited) { global $DB; self::_EnsureArgsStrictlyCastableToIntegers ($in_lessonId, $in_contextCourseLessonId); if ( ! is_bool($in_isProhibited) ) { throw new LearnException ('EA_PARAMS: isProhibited', LearnException::EXC_ERR_ALL_LOGIC | LearnException::EXC_ERR_ALL_PARAMS); } $lessonId = (int) $in_lessonId; $contextCourseLessonId = (int) $in_contextCourseLessonId; $isProhibitedNow = self::IsPublishProhibited ($lessonId, $contextCourseLessonId); // Update status only if it was changed if ($isProhibitedNow !== $in_isProhibited) { if ($in_isProhibited) { $sql = "INSERT INTO b_learn_publish_prohibition (COURSE_LESSON_ID, PROHIBITED_LESSON_ID) VALUES ($contextCourseLessonId, $lessonId)"; } else { $sql = "DELETE FROM b_learn_publish_prohibition WHERE COURSE_LESSON_ID = $contextCourseLessonId AND PROHIBITED_LESSON_ID = $lessonId"; } $rc = $DB->Query($sql, true); if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); CLearnCacheOfLessonTreeComponent::MarkAsDirty(); Bitrix\Learning\Integration\Search::indexLesson($lessonId); return (true); // prohibition status changed } return (false); // prohibition status not changed } /** * * @param int lesson id * @param int purge mode (PUBLISH_PROHIBITION_PURGE_ALL_LESSONS_IN_COURSE_CONTEXT, * PUBLISH_PROHIBITION_PURGE_LESSON_IN_ALL_COURSE_CONTEXT, * PUBLISH_PROHIBITION_PURGE_BOTH) */ protected static function PublishProhibitionPurge ($in_lessonId, $in_purgeMode) { global $DB; self::_EnsureArgsStrictlyCastableToIntegers ($in_lessonId, $in_purgeMode); $lessonId = (int) $in_lessonId; $purgeMode = (int) $in_purgeMode; if ( ! in_array( $purgeMode, array( self::PUBLISH_PROHIBITION_PURGE_ALL_LESSONS_IN_COURSE_CONTEXT, self::PUBLISH_PROHIBITION_PURGE_LESSON_IN_ALL_COURSE_CONTEXT, self::PUBLISH_PROHIBITION_PURGE_BOTH // ORed previous two elements ), true ) ) { throw new LearnException ('EA_PARAMS: purgeMode', LearnException::EXC_ERR_ALL_LOGIC | LearnException::EXC_ERR_ALL_PARAMS); } $arSqlCondition = array(); if ($purgeMode & self::PUBLISH_PROHIBITION_PURGE_ALL_LESSONS_IN_COURSE_CONTEXT) $arSqlCondition[] = 'COURSE_LESSON_ID = ' . $lessonId; if ($purgeMode & self::PUBLISH_PROHIBITION_PURGE_LESSON_IN_ALL_COURSE_CONTEXT) $arSqlCondition[] = 'PROHIBITED_LESSON_ID = ' . $lessonId; if (count($arSqlCondition) > 0) { $sqlCondition = implode(' OR ', $arSqlCondition); $rc = $DB->Query( "DELETE FROM b_learn_publish_prohibition WHERE " . $sqlCondition, true); if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); } CLearnCacheOfLessonTreeComponent::MarkAsDirty(); } /** * Cleanup publish prohibitions to be orphaned on relation remove. * * @param int $parentLessonId of relation to be removed * @param int $childLessonId of relation to be removed */ protected static function PublishProhibitionPurge_OnBeforeRelationRemove ($in_parentLessonId, $in_childLessonId) { global $DB; /* We must remove publish prohibition for all lessons-descendants of $in_childLessonId, in context of courses-ancestors of $in_parentLessonId, that are will lost link (path). General version of algorithm: 1) Get list of all descendants of $in_childLessonId (include $in_childLessonId itself). 2) Get list of publish prohibitions for lessons from step 1. 3) Checks every prohibition, that prohibited lesson still have path to courseLessonId in context of which lesson is prohibited. Remove prohibition, when check failed. Optimized version of algorithm: 1) Get list of all ancectors (that are courses) of $in_parentLessonId (include $in_parentLessonId itself). EXPLAINATION: when DeleteRecursiveLikeHardlinks() function will work, relations will be removed from top to bottom mainly. It means, that if we will get list descendants on each step - it will be too many lessons. So, we get ancestors instead. 2) Get list of publish prohibitions in context of courses from step 1. 3) Checks every prohibition, that prohibited lesson still have path to courseLessonId in context of which lesson is prohibited. Remove prohibition, when check failed. One more optimization: In optimized algorithm, we shouldn't exclude non-courses from ancestors list on step 1, because, there is no non-courses can be in table b_learn_publish_prohibition. So, if we can do "SELECT * FROM b_learn_publish_prohibition WHERE COURSE_LESSON_ID IN (...list of all ancestors...)" and result will be as expected when ancesotrs list includes only courses. I'm sure, DB engine will do this job more fast, than my PHP-script excludes non-courses. And one more optimization: In step 1 of optimized algorithm we can limit tree of ancestors at $in_childLessonId (in case, when tree of ancestors are cycled). EXPLAINATION: $in_childLessonId will lost relation to parent lesson ($in_parentLessonId) only. It means, that all descendants of $in_childLessonId (include $in_childLessonId itself) will not lost link (path) to other immediate parents of $in_childLessonId and to $in_childLessonId itself. So we don't need to check descendsnts in context of $in_childLessonId or it's ancestors (except $in_parentLessonId and it's ancestros). About checking that lesson after relation remove still have path (link) to some course: 1) Get all ancestors of lesson with method self::GetListOfAncestors($lessonId, false, false, $arIgnoreEdges). It will return ancestors in case, when all edges from $arIgnoreEdges is interpreted as non-existing. 2) If course-lesson among this ancestors, that link will be still exists after relation removing. This steps will be perfomed for every pair of finded prohibitions. There is probability that prohibited lesson will be in few courses. We can optimize steps by caching ancestors for prohibited lessons. In spite of that probability is not good in general case, we should use cache, because cache hit can save very-very much time. And caching itself don't gives overhead for processor, it's only overheads RAM, but a little. So, final algorithm: 1) Get list of all ancectors of $in_parentLessonId (include $in_parentLessonId itself). Stop cycling BEFORE $in_childLessonId. 2) Get list of publish prohibitions in context of courses from step 1. 3) Checks every prohibition, that prohibited lesson still have path to courseLessonId in context of which lesson is prohibited. Remove prohibition, when check failed. */ // 1) Get list of all ancectors of $in_parentLessonId (include $in_parentLessonId itself). // Stop cycling BEFORE $in_childLessonId. $arAncestors = self::GetListOfAncestors ($in_parentLessonId, false, $in_childLessonId); $arAncestors[] = (int) $in_parentLessonId; // include $in_parentLessonId itself // convert ids to int $arAncestorsInt = array(); foreach ($arAncestors as $ancestroId) $arAncestorsInt[] = (int) $ancestroId; // 2) Get list of publish prohibitions in context of courses from step 1. $rc = $DB->Query( "SELECT COURSE_LESSON_ID, PROHIBITED_LESSON_ID FROM b_learn_publish_prohibition WHERE COURSE_LESSON_ID IN (" . implode (',', $arAncestorsInt) . ")" , true); if ($rc === false) throw new LearnException ('EA_SQLERROR', LearnException::EXC_ERR_ALL_GIVEUP); // This relation will be removed, so must be ignoredm when determine // future ancestors (after relation removing) $arIgnoreEdges = array( array( 'PARENT_LESSON' => (int) $in_parentLessonId, 'CHILD_LESSON' => (int) $in_childLessonId ) ); $arCache_ancestorsOfLesson = array(); while ($arData = $rc->Fetch()) { $prohibitedLessonId = (int) $arData['PROHIBITED_LESSON_ID']; $contextLessonId = (int) $arData['COURSE_LESSON_ID']; // Precache future ancestors (after relation removing) // for lesson, if they are not precached yet. if ( ! isset($arCache_ancestorsOfLesson[$prohibitedLessonId]) ) { $arCache_ancestorsOfLesson[$prohibitedLessonId] = self::GetListOfAncestors( $prohibitedLessonId, false, // $stopAtLessonId false, // $stopBeforeLessonId $arIgnoreEdges ); } // Will prohibited lesson lost link to course $contextLessonId? if ( ! in_array($contextLessonId, $arCache_ancestorsOfLesson[$prohibitedLessonId], true) ) { // Yes, this lesson will not in subpathes of $contextLessonId, // so accorded publish prohibition must be removed. self::PublishProhibitionSetTo ($prohibitedLessonId, $contextLessonId, false); } } CLearnCacheOfLessonTreeComponent::MarkAsDirty(); } }