Yannick Warnier 8 年之前
父節點
當前提交
bee2c0ca06

+ 1 - 0
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);

+ 150 - 42
main/exercise/export/exercise_import.inc.php

@@ -1,32 +1,13 @@
 <?php
+/* For licensing terms, see /license.txt */
 /**
  * @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
- *
- * @license http://www.gnu.org/copyleft/gpl.html (GPL) GENERAL PUBLIC LICENSE
- *
  * @package chamilo.exercise
- * @deprecated
- *
  * @author claro team <cvs@claroline.net>
  * @author Guillaume Lederer <guillaume@claroline.net>
+ * @author Yannick Warnier <yannick.warnier@beeznest.com>
  */
 
-/**
- * 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('<p>', '</p>', '<front>', '</front>'), '', $data);
+    //$data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $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 <item><presentation><material><mattext> 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;
+}

+ 6 - 3
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 <amand@alrj.org>
      */
-    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)
 {

+ 2 - 1
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)
 {

+ 2 - 2
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;
             }

+ 16 - 0
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('#</' . $tag . '[^>]*>#i', ' ', $string);
+        if ($string2 != $string) {
+            $string = preg_replace('/<' . $tag . '[^>]*>/i', ' ', $string2);
+        }
+    }
+    return $string;
+}

+ 7 - 2
main/inc/lib/formvalidator/FormValidator.class.php

@@ -1,6 +1,8 @@
 <?php
 /* For licensing terms, see /license.txt */
 
+use ChamiloSession as Session;
+
 /**
  * Class FormValidator
  * create/manipulate/validate user input.
@@ -1442,6 +1444,9 @@ EOT;
                     node
                         .prepend('<br>')
                         .prepend(file.preview);
+                    node
+                        .append('<br>')
+                        .append($('<span class=\"text-success\"/>').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 = $('<a>')
@@ -1477,7 +1481,8 @@ EOT;
                 });
             }).on('fileuploadfail', function (e, data) {
                 $.each(data.files, function (index) {
-                    var error = $('<span class=\"text-danger\"/>').text('".get_lang('UploadError')."');
+                    var failedMessage = '" . get_lang('UplUploadFailed') . "';
+                    var error = $('<span class=\"text-danger\"/>').text(failedMessage);
                     $(data.context.children()[index])
                         .append('<br>')
                         .append(error);

+ 1 - 0
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)
     {

+ 3 - 3
main/template/default/learnpath/view.tpl

@@ -135,9 +135,9 @@
                             <div role="tabpanel" class="tab-pane active" id="lp-view-content">
                                 <div id="wrapper-iframe" style="width:100%; height:100%">
                                 {% if lp_mode == 'fullscreen' %}
-                                    <iframe id="content_id_blank" name="content_name_blank" style="width:100%; height:100%" src="blank.php" border="0" frameborder="0" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true"></iframe>
+                                    <iframe id="content_id_blank" name="content_name_blank" src="blank.php" style="width:100%; height:100%" border="0" frameborder="0" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true"></iframe>
                                 {% else %}
-                                    <iframe id="content_id" name="content_name" style="width:100%; height:100%" src="{{ iframe_src }}" border="0" frameborder="0" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true"></iframe>
+                                    <iframe id="content_id" name="content_name" src="{{ iframe_src }}" style="width:100%; height:100%" border="0" frameborder="0" allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true"></iframe>
                                 {% endif %}
                                 </div>
                             </div>
@@ -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) {

+ 37 - 9
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,

+ 1 - 11
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);
-	}
 }
 ?>