diff --git a/main/exercise/export/aiken/aiken_classes.php b/main/exercise/export/aiken/aiken_classes.php index 02612cd481..d0d74e2aae 100755 --- a/main/exercise/export/aiken/aiken_classes.php +++ b/main/exercise/export/aiken/aiken_classes.php @@ -13,6 +13,7 @@ if (!function_exists('mime_content_type')) { /** * @param string $filename + * @return string */ function mime_content_type($filename) { return DocumentManager::file_get_mime_type((string)$filename); diff --git a/main/exercise/export/exercise_import.inc.php b/main/exercise/export/exercise_import.inc.php index 2843e86b3d..3483623808 100755 --- a/main/exercise/export/exercise_import.inc.php +++ b/main/exercise/export/exercise_import.inc.php @@ -1,32 +1,13 @@ * @author Guillaume Lederer + * @author Yannick Warnier */ -/** - * function to create a temporary directory (SAME AS IN MODULE ADMIN) - */ -function tempdir($dir, $prefix = 'tmp', $mode = 0777) -{ - if (substr($dir, -1) != '/') { - $dir .= '/'; - } - - do { - $path = $dir.$prefix.mt_rand(0, 9999999); - } while (!mkdir($path, $mode)); - - return $path; -} - /** * Unzip the exercise in the temp folder * @param string The path of the temporary directory where the exercise was uploaded and unzipped @@ -76,9 +57,9 @@ function import_exercise($file) global $record_item_body; // used to specify the question directory where files could be found in relation in any question global $questionTempDir; + global $resourcesLinks; - $archive_path = api_get_path(SYS_ARCHIVE_PATH) . 'qti2'; - $baseWorkDir = $archive_path; + $baseWorkDir = api_get_path(SYS_ARCHIVE_PATH) . 'qti2'; if (!is_dir($baseWorkDir)) { mkdir($baseWorkDir, api_get_permissions_for_new_directories(), true); @@ -110,32 +91,50 @@ function import_exercise($file) } // find the different manifests for each question and parse them. - $exerciseHandle = opendir($baseWorkDir); //$question_number = 0; $file_found = false; $operation = false; $result = false; $filePath = null; + $resourcesLinks = array(); - // parse every subdirectory to search xml question files + // parse every subdirectory to search xml question files and other assets to be imported + // The assets-related code is a bit fragile as it has to deal with files renamed by Chamilo and it only works if + // the imsmanifest.xml file is read. while (false !== ($file = readdir($exerciseHandle))) { if (is_dir($baseWorkDir . '/' . $file) && $file != "." && $file != "..") { // Find each manifest for each question repository found $questionHandle = opendir($baseWorkDir . '/' . $file); + // Only analyse one level of subdirectory - no recursivity here while (false !== ($questionFile = readdir($questionHandle))) { if (preg_match('/.xml$/i', $questionFile)) { - $result = parse_file($baseWorkDir, $file, $questionFile); - $filePath = $baseWorkDir . $file; - $file_found = true; + $isQti = isQtiQuestionBank($baseWorkDir . '/' . $file . '/' . $questionFile); + if ($isQti) { + $result = qti_parse_file($baseWorkDir, $file, $questionFile); + $filePath = $baseWorkDir . $file; + $file_found = true; + } else { + $isManifest = isQtiManifest($baseWorkDir . '/' . $file . '/' . $questionFile); + if ($isManifest) { + $resourcesLinks = qtiProcessManifest($baseWorkDir . '/' . $file . '/' . $questionFile); + } + } } } } elseif (preg_match('/.xml$/i', $file)) { + $isQti = isQtiQuestionBank($baseWorkDir . '/' . $file); + if ($isQti) { + $result = qti_parse_file($baseWorkDir, '', $file); + $filePath = $baseWorkDir . '/' . $file; + $file_found = true; + } else { + $isManifest = isQtiManifest($baseWorkDir . '/' . $file); + if ($isManifest) { + $resourcesLinks = qtiProcessManifest($baseWorkDir . '/' . $file); + } + } - // Else ignore file - $result = parse_file($baseWorkDir, '', $file); - $filePath = $baseWorkDir . '/' . $file; - $file_found = true; } } @@ -234,7 +233,7 @@ function formatText($text) * @param string $questionFile * @return bool */ -function parse_file($exercisePath, $file, $questionFile) +function qti_parse_file($exercisePath, $file, $questionFile) { global $non_HTML_tag_to_avoid; global $record_item_body; @@ -252,7 +251,8 @@ function parse_file($exercisePath, $file, $questionFile) } //parse XML question file - $data = str_replace(array('

', '

', '', ''), '', $data); + //$data = str_replace(array('

', '

', '', ''), '', $data); + $data = stripGivenTags($data, array('p', 'front')); $qtiVersion = array(); $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data, $qtiVersion); $qtiMainVersion = 2; //by default, assume QTI version 2 @@ -292,7 +292,14 @@ function parse_file($exercisePath, $file, $questionFile) if (!xml_parse($xml_parser, $data, feof($fp))) { // if reading of the xml file in not successful : // set errorFound, set error msg, break while statement - Display:: display_error_message(get_lang('Error reading XML file')); + $error = xml_get_error_code(); + Display::addFlash( + Display::return_message( + get_lang('Error reading XML file') . sprintf('[%d:%d]', xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser)), + 'error' + ) + ); + return false; } @@ -310,6 +317,7 @@ function parse_file($exercisePath, $file, $questionFile) 'error' ) ); + return false; } return true; @@ -517,6 +525,7 @@ function elementDataQti2($parser, $data) global $non_HTML_tag_to_avoid; global $current_inlinechoice_id; global $cardinality; + global $resourcesLinks; $current_element = end($element_pile); if (sizeof($element_pile) >= 2) { @@ -564,6 +573,13 @@ function elementDataQti2($parser, $data) } break; case 'ITEMBODY': + // Replace relative links by links to the documents in the course + // $resourcesLinks is only defined by qtiProcessManifest() + if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) { + foreach ($resourcesLinks['manifest'] as $key => $value) { + $data = preg_replace('|' . $value . '|', $resourcesLinks['web'][$key], $data); + } + } $current_question_item_body .= $data; break; case 'INLINECHOICE': @@ -667,14 +683,9 @@ function startElementQti1($parser, $name, $attributes) $exercise_info['question'][$current_question_ident] = array(); $exercise_info['question'][$current_question_ident]['answer'] = array(); $exercise_info['question'][$current_question_ident]['correct_answers'] = array(); - //$exercise_info['question'][$current_question_ident]['title'] = $attributes['TITLE']; $exercise_info['question'][$current_question_ident]['tempdir'] = $questionTempDir; break; case 'SECTION': - //retrieve exercise name - //if (isset($attributes['TITLE']) && !empty($attributes['TITLE'])) { - // $exercise_info['name'] = $attributes['TITLE']; - //} break; case 'RESPONSE_LID': // Retrieve question type @@ -719,7 +730,6 @@ function startElementQti1($parser, $name, $attributes) } break; case 'IMG': - //$exercise_info['question'][$current_question_ident]['attached_file_url'] = $attributes['SRC']; break; case 'MATTEXT': if ($parent_element == 'MATERIAL') { @@ -748,6 +758,7 @@ function endElementQti1($parser, $name, $attributes) global $cardinality; global $lastLabelFieldName; global $lastLabelFieldValue; + global $resourcesLinks; $current_element = end($element_pile); if (sizeof($element_pile) >= 2) { @@ -775,7 +786,9 @@ function endElementQti1($parser, $name, $attributes) switch ($name) { case 'MATTEXT': if ($parent_element == 'MATERIAL') { - if ($grand_parent_element == 'PRESENTATION') { + // For some reason an item in a hierarchy doesn't seem to + // catch the grandfather 'presentation', so we check for 'item' as a patch (great-grand-father) + if ($grand_parent_element == 'PRESENTATION' OR $grand_parent_element == 'ITEM') { $exercise_info['question'][$current_question_ident]['title'] = $current_question_item_body; $current_question_item_body = ''; } elseif ($grand_parent_element == 'RESPONSE_LABEL') { @@ -828,6 +841,7 @@ function elementDataQti1($parser, $data) global $cardinality; global $lastLabelFieldName; global $lastLabelFieldValue; + global $resourcesLinks; $current_element = end($element_pile); if (sizeof($element_pile) >= 2) { @@ -879,6 +893,13 @@ function elementDataQti1($parser, $data) } break; case 'MATTEXT': + // Replace relative links by links to the documents in the course + // $resourcesLinks is only defined by qtiProcessManifest() + if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) { + foreach ($resourcesLinks['manifest'] as $key=>$value) { + $data = preg_replace('|' . $value . '|', $resourcesLinks['web'][$key], $data); + } + } if (!empty($current_question_item_body)) { $current_question_item_body .= $data; } else { @@ -900,3 +921,90 @@ function elementDataQti1($parser, $data) } } + +/** + * Check if a given file is an IMS/QTI question bank file + * @param string $filePath The absolute filepath + * @return bool Whether it is an IMS/QTI question bank or not + */ +function isQtiQuestionBank($filePath) +{ + $data = file_get_contents($filePath); + if (!empty($data)) { + $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data); + if ($match) { + return true; + } + } + + return false; +} + +/** + * Check if a given file is an IMS/QTI manifest file (listing of extra files) + * @param string $filePath The absolute filepath + * @return bool Whether it is an IMS/QTI manifest file or not + */ +function isQtiManifest($filePath) +{ + $data = file_get_contents($filePath); + if (!empty($data)) { + $match = preg_match('/imsccv(\d)p(\d)/', $data); + if ($match) { + return true; + } + } + + return false; +} + +/** + * Processes an IMS/QTI manifest file: store links to new files to be able to transform them into the questions text + * @param string $filePath The absolute filepath + * @param array $links List of filepaths changes + * @return bool + */ +function qtiProcessManifest($filePath) +{ + $xml = simplexml_load_file($filePath); + $course = api_get_course_info(); + $sessionId = api_get_session_id(); + $courseDir = $course['path']; + $sysPath = api_get_path(SYS_COURSE_PATH); + $exercisesSysPath = $sysPath . $courseDir . '/document/'; + $webPath = api_get_path(WEB_CODE_PATH); + $exercisesWebPath = $webPath . 'document/document.php?' . api_get_cidreq() . '&action=download&id='; + $links = array( + 'manifest' => array(), + 'system' => array(), + 'web' => array(), + ); + $tableDocuments = Database::get_course_table(TABLE_DOCUMENT); + $countResources = count($xml->resources->resource->file); + for ($i=0; $i < $countResources; $i++) { + $file = $xml->resources->resource->file[$i]; + $href = ''; + foreach ($file->attributes() as $key => $value) { + if ($key == 'href') { + if (substr($value, -3, 3) != 'xml') { + $href = $value; + } + } + } + if (!empty($href)) { + $links['manifest'][] = (string) $href; + $links['system'][] = $exercisesSysPath . strtolower($href); + $specialHref = Database::escape_string(preg_replace('/_/', '-', strtolower($href))); + $specialHref = preg_replace('/(-){2,8}/', '-', $specialHref); + + $sql = "SELECT iid FROM " . $tableDocuments . " WHERE c_id = " . $course['real_id'] . " AND session_id = $sessionId AND path = '/" . $specialHref . "'"; + $result = Database::query($sql); + $documentId = 0; + while ($row = Database::fetch_assoc($result)) { + $documentId = $row['iid']; + } + $links['web'][] = $exercisesWebPath . $documentId; + } + } + return $links; +} diff --git a/main/exercise/export/qti2/qti2_export.php b/main/exercise/export/qti2/qti2_export.php index 6753232800..ce4999b191 100755 --- a/main/exercise/export/qti2/qti2_export.php +++ b/main/exercise/export/qti2/qti2_export.php @@ -34,7 +34,7 @@ class ImsAssessmentItem * * @param Ims2Question $question Ims2Question object we want to export. */ - function ImsAssessmentItem($question) + function __construct($question) { $this->question = $question; $this->answer = $this->question->setAnswer(); @@ -155,9 +155,10 @@ class ImsSection /** * Constructor. * @param Exercise $exe The Exercise instance to export + * @return ImsSection * @author Amand Tihon */ - function ImsSection($exe) + function __construct($exe) { $this->exercise = $exe; } @@ -302,9 +303,10 @@ class ImsItem * Constructor. * * @param $question The Question object we want to export. + * @return ImsItem * @author Anamd Tihon */ - function ImsItem($question) + function __construct($question) { $this->question = $question; $this->answer = $question->answer; @@ -435,6 +437,7 @@ function export_exercise_to_qti($exerciseId, $standalone = true) * * @param int $questionId * @param bool $standalone (ie including XML tag, DTD declaration, etc) + * @return string */ function export_question_qti($questionId, $standalone = true) { diff --git a/main/exercise/qti2.php b/main/exercise/qti2.php index b0eb6d38c2..cfcf3ba835 100755 --- a/main/exercise/qti2.php +++ b/main/exercise/qti2.php @@ -26,7 +26,7 @@ $interbreadcrumb[]= array ( $is_allowedToEdit = api_is_allowed_to_edit(null, true); /** - * This function displays the form for import of the zip file with qti2 + * This function displays the form to import the zip file with qti2 */ function ch_qti2_display_form() { @@ -52,6 +52,7 @@ function ch_qti2_display_form() /** * This function will import the zip file with the respective qti2 * @param array $array_file ($_FILES) + * @return string|array */ function ch_qti2_import_file($array_file) { diff --git a/main/inc/ajax/work.ajax.php b/main/inc/ajax/work.ajax.php index a77d99e583..a16715b45b 100755 --- a/main/inc/ajax/work.ajax.php +++ b/main/inc/ajax/work.ajax.php @@ -41,7 +41,7 @@ switch ($action) { 'title' => $file['name'], 'description' => '' ]; - $result = processWorkForm($workInfo, $values, $courseInfo, $sessionId, 0, $userId, $file); + $result = processWorkForm($workInfo, $values, $courseInfo, $sessionId, 0, $userId, $file, true); $json = array(); if (!empty($result) && is_array($result) && empty($result['error'])) { @@ -60,7 +60,7 @@ switch ($action) { ); } else { $json['url'] = ''; - $json['error'] = get_lang('Error'); + $json['error'] = isset($result['error']) ? $result['error'] : get_lang('Error'); } $resultList[] = $json; } diff --git a/main/inc/lib/api.lib.php b/main/inc/lib/api.lib.php index 58b6ce86e0..16b7214408 100644 --- a/main/inc/lib/api.lib.php +++ b/main/inc/lib/api.lib.php @@ -8029,3 +8029,19 @@ function api_protect_limit_for_session_admin() function api_is_student_view_active() { return (isset($_SESSION['studentview']) && $_SESSION['studentview'] == "studentview"); } + +/** + * Like strip_tags(), but leaves an additional space and removes only the given tags + * @param string $string + * @param array $tags Tags to be removed + * @return string The original string without the given tags + */ +function stripGivenTags($string, $tags) { + foreach ($tags as $tag) { + $string2 = preg_replace('#]*>#i', ' ', $string); + if ($string2 != $string) { + $string = preg_replace('/<' . $tag . '[^>]*>/i', ' ', $string2); + } + } + return $string; +} \ No newline at end of file diff --git a/main/inc/lib/formvalidator/FormValidator.class.php b/main/inc/lib/formvalidator/FormValidator.class.php index a6857faa3f..420f68ac62 100755 --- a/main/inc/lib/formvalidator/FormValidator.class.php +++ b/main/inc/lib/formvalidator/FormValidator.class.php @@ -1,6 +1,8 @@ ') .prepend(file.preview); + node + .append('
') + .append($('').text('" . get_lang('UplUploadSucceeded') . "')); } if (file.error) { node @@ -1460,7 +1465,6 @@ EOT; progress + '%' ); }).on('fileuploaddone', function (e, data) { - $.each(data.result.files, function (index, file) { if (file.url) { var link = $('') @@ -1477,7 +1481,8 @@ EOT; }); }).on('fileuploadfail', function (e, data) { $.each(data.files, function (index) { - var error = $('').text('".get_lang('UploadError')."'); + var failedMessage = '" . get_lang('UplUploadFailed') . "'; + var error = $('').text(failedMessage); $(data.context.children()[index]) .append('
') .append(error); diff --git a/main/inc/lib/system/session.class.php b/main/inc/lib/system/session.class.php index 2b9289238b..942ce0260e 100755 --- a/main/inc/lib/system/session.class.php +++ b/main/inc/lib/system/session.class.php @@ -39,6 +39,7 @@ class Session implements \ArrayAccess * Returns true if session has variable set up, false otherwise. * * @param string $variable + * @return mixed value */ static function has($variable) { diff --git a/main/template/default/learnpath/view.tpl b/main/template/default/learnpath/view.tpl index 726285e143..ed7ef8393b 100644 --- a/main/template/default/learnpath/view.tpl +++ b/main/template/default/learnpath/view.tpl @@ -135,9 +135,9 @@
{% if lp_mode == 'fullscreen' %} - + {% else %} - + {% endif %}
@@ -171,7 +171,7 @@ '-webkit-overflow-scrolling': 'touch' }); } - + {% if lp_mode == 'embedframe' %} //$('#learning_path_main').addClass('lp-view-collapsed'); $('#lp-view-expand-button, #lp-view-expand-toggle').on('click', function (e) { diff --git a/main/work/work.lib.php b/main/work/work.lib.php index abddfaef5b..3e9b1ab48c 100755 --- a/main/work/work.lib.php +++ b/main/work/work.lib.php @@ -1370,7 +1370,7 @@ function getWorkListStudent( $lastWork = getLastWorkStudentFromParentByUser($userId, $work['id'], $courseInfo); if (!empty($lastWork)) { - $work['last_upload'] = Display::label($lastWork['qualification'], 'warning').' - '; + $work['last_upload'] = (!empty($lastWork['qualification'])) ? Display::label($lastWork['qualification'], 'warning').' - ' : ''; $work['last_upload'] .= api_get_local_time($lastWork['sent_date']); } @@ -3509,6 +3509,23 @@ function sendAlertToUsers($workId, $courseInfo, $session_id) } } +/** + * Check if the current uploaded work filename already exists in the current assement + * + * @param $filename + * @param $workId + * @return mixed + */ +function checkExistingWorkFileName($filename, $workId) +{ + $work_table = Database :: get_course_table(TABLE_STUDENT_PUBLICATION); + $filename = Database::escape_string($filename); + $sql = "SELECT title FROM $work_table + WHERE parent_id = $workId AND title = '$filename'"; + $result = Database::query($sql); + return Database::fetch_assoc($result); +} + /** * @param array $workInfo * @param array $values @@ -3517,10 +3534,11 @@ function sendAlertToUsers($workId, $courseInfo, $session_id) * @param int $groupId * @param int $userId * @param array $file + * @param bool $checkDuplicated * * @return null|string */ -function processWorkForm($workInfo, $values, $courseInfo, $sessionId, $groupId, $userId, $file = []) +function processWorkForm($workInfo, $values, $courseInfo, $sessionId, $groupId, $userId, $file = [], $checkDuplicated = false) { $work_table = Database :: get_course_table(TABLE_STUDENT_PUBLICATION); @@ -3537,12 +3555,20 @@ function processWorkForm($workInfo, $values, $courseInfo, $sessionId, $groupId, $filename = null; $url = null; $filesize = null; + $workData = []; if ($values['contains_file']) { - $result = uploadWork($workInfo, $courseInfo, false, [], $file); - if (!$result) { - $saveWork = false; + if ($checkDuplicated) { + if (checkExistingWorkFileName($file['name'], $workInfo['id'])) { + $saveWork = false; + $workData['error'] = get_lang('YouAlreadySentThisFile'); + } else { + $result = uploadWork($workInfo, $courseInfo, false, [], $file); + } + } else { + $result = uploadWork($workInfo, $courseInfo, false, [], $file); } + if (isset($result['error'])) { $message = $result['error']; Display::addFlash($message); @@ -3559,12 +3585,14 @@ function processWorkForm($workInfo, $values, $courseInfo, $sessionId, $groupId, $title = isset($result['title']) && !empty($result['title']) ? $result['title'] : get_lang('Untitled'); } $filesize = isset($result['filesize']) ? $result['filesize'] : null; - $url = $result['url']; + $url = isset($result['url']) ? $result['url'] : null; + } - if (empty($title)) { - $title = get_lang('Untitled'); - } + if (empty($title)) { + $title = get_lang('Untitled'); + } + if ($saveWork) { $active = '1'; $params = [ 'c_id' => $courseId, diff --git a/tests/main/exercice/export/exercise_import.inc.test.php b/tests/main/exercice/export/exercise_import.inc.test.php index fb2402337a..2f2c1b13a7 100755 --- a/tests/main/exercice/export/exercise_import.inc.test.php +++ b/tests/main/exercice/export/exercise_import.inc.test.php @@ -67,7 +67,7 @@ class TestExerciseImport extends UnitTestCase { $file = ''; $exercisePath = ''; $questionFile = ''; - $res = parse_file($exercisePath, $file, $questionFile); + $res = qti_parse_file($exercisePath, $file, $questionFile); $this->assertTrue(is_array($res)); if(!is_null){ $this->assertTrue($res); @@ -86,15 +86,5 @@ class TestExerciseImport extends UnitTestCase { } //var_dump($res); } - - function testtempdir() { - $dir = '/tmp'; - $res = tempdir($dir, $prefix='tmp', $mode=0777); - $this->assertFalse(is_array($res)); - if(!is_null){ - $this->assertTrue(is_string($res)); - } - //var_dump($res); - } } ?>