diff --git a/db/install.xml b/db/install.xml old mode 100644 new mode 100755 index 3324d944bee9ea463f3a3d9d064af096383b7442..102667decb1d77bd9bc68cd3ef3ff5d6ae7135a8 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" ?> -<XMLDB PATH="mod/pdfannotator/db" VERSION="20181017" COMMENT="XMLDB file for Moodle mod/pdfannotator" +<XMLDB PATH="mod/pdfannotator/db" VERSION="20221101" COMMENT="XMLDB file for Moodle mod/pdfannotator" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd" > @@ -48,9 +48,7 @@ <FIELD NAME="isquestion" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="When the user creates an annotation, the comment he has to write is a question. So this column has to be true, otherwise false."/> <FIELD NAME="isdeleted" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/> <FIELD NAME="ishidden" TYPE="int" LENGTH="2" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Hidden comments can be seen by managers but not participants."/> - <FIELD NAME="solved" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="saves the userid of the user who marked the comment -question: marked as solved -answer: marked as correct answer"/> + <FIELD NAME="solved" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="saves the userid of the user who marked the comment question: marked as solved answer: marked as correct answer"/> </FIELDS> <KEYS> <KEY NAME="primary" TYPE="primary" FIELDS="id"/> diff --git a/db/upgrade.php b/db/upgrade.php old mode 100644 new mode 100755 index c623f1f6262450d4554bac0639c9431a9bb58da8..d831d7a6ebfc3b41ffeab592ee9869ed44d1ee4c --- a/db/upgrade.php +++ b/db/upgrade.php @@ -603,5 +603,46 @@ function xmldb_pdfannotator_upgrade($oldversion) { upgrade_mod_savepoint(true, 2021032201, 'pdfannotator'); } + if ($oldversion < 2022102606) { + + // Define table pdfannotator_embeddedfiles to be created. + $table = new xmldb_table('pdfannotator_embeddedfiles'); + + // Adding fields to table pdfannotator_embeddedfiles. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('fileid', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, null); + $table->add_field('commentid', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table pdfannotator_embeddedfiles. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + $table->add_key('fileid', XMLDB_KEY_FOREIGN, ['fileid'], 'files', ['id']); + $table->add_key('commentid', XMLDB_KEY_FOREIGN, ['commentid'], 'comments', ['id']); + + // Adding indexes to table pdfannotator_embeddedfiles. + $table->add_index('idandcomment', XMLDB_INDEX_NOTUNIQUE, ['id', 'commentid']); + + // Conditionally launch create table for pdfannotator_embeddedfiles. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Pdfannotator savepoint reached. + upgrade_mod_savepoint(true, 2022102606, 'pdfannotator'); + } + + if ($oldversion < 2022110200) { + + // Define table pdfannotator_embeddedfiles to be dropped. + $table = new xmldb_table('pdfannotator_embeddedfiles'); + + // Conditionally launch drop table for pdfannotator_embeddedfiles. + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + // Pdfannotator savepoint reached. + upgrade_mod_savepoint(true, 2022110200, 'pdfannotator'); + } + return true; } diff --git a/lang/en/pdfannotator.php b/lang/en/pdfannotator.php index 77a3609504c93366dea4af44c9bd6ef04d71eae1..b27d831edc5cb01919dd73bc2d6c6b3d3960dffc 100644 --- a/lang/en/pdfannotator.php +++ b/lang/en/pdfannotator.php @@ -113,8 +113,7 @@ $string['error:hideComment'] = "An error has occured while trying to hide the co $string['error:markasread'] = 'The item could not be marked as read.'; $string['error:markasunread'] = 'The item could not be marked as unread.'; $string['error:markcorrectanswer'] = 'An error has occured while marking the answer as correct.'; -$string['error:maximalsizeoffile_created'] = 'Your comment cannot be created, because it exceeds the maximum size of files. You can attach file(s) with at most {$a} to a single comment.'; -$string['error:maximalsizeoffile_edited'] = 'Your comment cannot be edited, because it exceeds the maximum size of files. You can attach file(s) with at most {$a} to a single comment.'; +$string['error:maximalsizeoffile'] = 'Your file {$a->filename}, because it exceeds {$a->filesize} as the maximum size of files. You can attach file(s) with at most {$a->maxfilesize} to a single comment.'; $string['error:missingAnnotationtype'] = 'Annotationtype does not exists. Possibly the entry in table pdfannotator_annotationtypes is missing.'; $string['error:openingPDF'] = 'An error occurred while opening the PDF file.'; $string['error:openprintview'] = 'An error has occured while trying to open the pdf in Acrobat Reader.'; diff --git a/locallib.php b/locallib.php index 48ec13f2c669af80e0cb5f1337583f8d7661065e..8210ee36f68244e5c55b1ae203719762fb74de03 100644 --- a/locallib.php +++ b/locallib.php @@ -211,20 +211,22 @@ function pdfannotator_split_content_image($content, $res, $itemid, $context=null $tempinfo = []; foreach($fileinfo as $file) { - $count = substr_count($imgstr, $file['filename']); + $count = substr_count(urldecode($url[0]), $file['filename']); if($count) { $tempinfo = $file; break; } } - $imagedata = 'data:' . $tempinfo['filemimetype'] . ';base64,' . base64_encode($tempinfo['filecontent']); - $data['image'] = $imagedata; - $data['format'] = $tempinfo['filemimetype']; - $data['fileid'] = $tempinfo['fileid']; - $data['filename'] = $tempinfo['filename']; - $data['filepath'] = $tempinfo['filepath']; - $data['filesize'] = $tempinfo['filesize']; + if($tempinfo) { + $imagedata = 'data:' . $tempinfo['filemimetype'] . ';base64,' . base64_encode($tempinfo['filecontent']); + $data['image'] = $imagedata; + $data['format'] = $tempinfo['filemimetype']; + $data['fileid'] = $tempinfo['fileid']; + $data['filename'] = $tempinfo['filename']; + $data['filepath'] = $tempinfo['filepath']; + $data['filesize'] = $tempinfo['filesize']; + } preg_match('/height=[0-9]+/', $imgstr, $height); $data['imageheight'] = str_replace("\"", "", explode('=', $height[0])[1]); @@ -263,7 +265,7 @@ function pdfannotator_data_preprocessing($context, $textarea, $draftitemid = 0) if(!$imagebtn) { $editor->use_editor($textarea, $options); } else { - // inilialize Filepicker if image button is active. + // initialize Filepicker if image button is active. $args = new \stdClass(); // need these three to filter repositories list. $args->accepted_types = ['web_image']; @@ -295,7 +297,7 @@ function pdfannotator_data_preprocessing($context, $textarea, $draftitemid = 0) /** * Same function as core, however we need to add files into the existing draft area! - * + * Copied from hsuforum. */ function pdfannotator_file_prepare_draft_area(&$draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null) { global $CFG, $USER, $CFG, $DB; @@ -366,6 +368,233 @@ function pdfannotator_file_prepare_draft_area(&$draftitemid, $contextid, $compon return file_rewrite_pluginfile_urls($text, 'draftfile.php', $usercontext->id, 'user', 'draft', $draftitemid, $options); } +/** + * Just like the file_save_draft_area_files core function. + * However, we need to store fileid and commentid in pdfannotator_embeddedfiles + * and add its id into the html code from the corresponding files. + * @return string|null if $text was passed in, the rewritten $text is returned. + * Otherwise NULL. + */ +function pdfannotator_file_save_draft_area_files($draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null, $forcehttps=false) { + global $USER, $DB; + + // Do not merge files, leave it as it was. + if ($draftitemid === IGNORE_FILE_MERGE) { + // Safely return $text, no need to rewrite pluginfile because this is mostly comming from an external client like the app. + return $text; + } + + $usercontext = context_user::instance($USER->id); + $fs = get_file_storage(); + + $options = (array)$options; + if (!isset($options['subdirs'])) { + $options['subdirs'] = false; + } + if (!isset($options['maxfiles'])) { + $options['maxfiles'] = -1; // unlimited + } + if (!isset($options['maxbytes']) || $options['maxbytes'] == USER_CAN_IGNORE_FILE_SIZE_LIMITS) { + $options['maxbytes'] = 0; // unlimited + } + if (!isset($options['areamaxbytes'])) { + $options['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED; // Unlimited. + } + $allowreferences = true; + if (isset($options['return_types']) && !($options['return_types'] & (FILE_REFERENCE | FILE_CONTROLLED_LINK))) { + // we assume that if $options['return_types'] is NOT specified, we DO allow references. + // this is not exactly right. BUT there are many places in code where filemanager options + // are not passed to file_save_draft_area_files() + $allowreferences = false; + } + + // Check if the user has copy-pasted from other draft areas. Those files will be located in different draft + // areas and need to be copied into the current draft area. + $text = file_merge_draft_areas($draftitemid, $usercontext->id, $text, $forcehttps); + + // Check if the draft area has exceeded the authorised limit. This should never happen as validation + // should have taken place before, unless the user is doing something nauthly. If so, let's just not save + // anything at all in the next area. + if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) { + return null; + } + + $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id'); + $oldfiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'id'); + + // One file in filearea means it is empty (it has only top-level directory '.'). + if (count($draftfiles) > 1 || count($oldfiles) > 1) { + // we have to merge old and new files - we want to keep file ids for files that were not changed + // we change time modified for all new and changed files, we keep time created as is + + $newhashes = array(); + $filecount = 0; + $context = context::instance_by_id($contextid, MUST_EXIST); + foreach ($draftfiles as $file) { + if (!$options['subdirs'] && $file->get_filepath() !== '/') { + continue; + } + if (!$allowreferences && $file->is_external_file()) { + continue; + } + if (!$file->is_directory()) { + // Check to see if this file was uploaded by someone who can ignore the file size limits. + $fileusermaxbytes = get_user_max_upload_file_size($context, $options['maxbytes'], 0, 0, $file->get_userid()); + if ($fileusermaxbytes != USER_CAN_IGNORE_FILE_SIZE_LIMITS + && ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize())) { + // Oversized file. + continue; + } + if ($options['maxfiles'] != -1 and $options['maxfiles'] <= $filecount) { + // more files - should not get here at all + continue; + } + $filecount++; + } + $newhash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $file->get_filepath(), $file->get_filename()); + $newhashes[$newhash] = $file; + } + + // Loop through oldfiles and decide which we need to delete and which to update. + // After this cycle the array $newhashes will only contain the files that need to be added. + foreach ($oldfiles as $oldfile) { + $oldhash = $oldfile->get_pathnamehash(); + if (!isset($newhashes[$oldhash])) { + // delete files not needed any more - deleted by user + $oldfile->delete(); + continue; + } + + $newfile = $newhashes[$oldhash]; + // Now we know that we have $oldfile and $newfile for the same path. + // Let's check if we can update this file or we need to delete and create. + if ($newfile->is_directory()) { + // Directories are always ok to just update. + } else if (($source = @unserialize($newfile->get_source())) && isset($source->original)) { + // File has the 'original' - we need to update the file (it may even have not been changed at all). + $original = file_storage::unpack_reference($source->original); + if ($original['filename'] !== $oldfile->get_filename() || $original['filepath'] !== $oldfile->get_filepath()) { + // Very odd, original points to another file. Delete and create file. + $oldfile->delete(); + continue; + } + } else { + // The same file name but absence of 'original' means that file was deteled and uploaded again. + // By deleting and creating new file we properly manage all existing references. + $oldfile->delete(); + continue; + } + + // status changed, we delete old file, and create a new one + if ($oldfile->get_status() != $newfile->get_status()) { + // file was changed, use updated with new timemodified data + $oldfile->delete(); + // This file will be added later + continue; + } + + // Updated author + if ($oldfile->get_author() != $newfile->get_author()) { + $oldfile->set_author($newfile->get_author()); + } + // Updated license + if ($oldfile->get_license() != $newfile->get_license()) { + $oldfile->set_license($newfile->get_license()); + } + + // Updated file source + // Field files.source for draftarea files contains serialised object with source and original information. + // We only store the source part of it for non-draft file area. + $newsource = $newfile->get_source(); + if ($source = @unserialize($newfile->get_source())) { + $newsource = $source->source; + } + if ($oldfile->get_source() !== $newsource) { + $oldfile->set_source($newsource); + } + + // Updated sort order + if ($oldfile->get_sortorder() != $newfile->get_sortorder()) { + $oldfile->set_sortorder($newfile->get_sortorder()); + } + + // Update file timemodified + if ($oldfile->get_timemodified() != $newfile->get_timemodified()) { + $oldfile->set_timemodified($newfile->get_timemodified()); + } + + // Replaced file content + if (!$oldfile->is_directory() && + ($oldfile->get_contenthash() != $newfile->get_contenthash() || + $oldfile->get_filesize() != $newfile->get_filesize() || + $oldfile->get_referencefileid() != $newfile->get_referencefileid() || + $oldfile->get_userid() != $newfile->get_userid())) { + $oldfile->replace_file_with($newfile); + } + + // unchanged file or directory - we keep it as is + unset($newhashes[$oldhash]); + } + + // Add fresh file or the file which has changed status + // the size and subdirectory tests are extra safety only, the UI should prevent it + foreach ($newhashes as $file) { + $file_record = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'timemodified'=>time()); + if ($source = @unserialize($file->get_source())) { + // Field files.source for draftarea files contains serialised object with source and original information. + // We only store the source part of it for non-draft file area. + $file_record['source'] = $source->source; + } + + if ($file->is_external_file()) { + $repoid = $file->get_repository_id(); + if (!empty($repoid)) { + $context = context::instance_by_id($contextid, MUST_EXIST); + $repo = repository::get_repository_by_id($repoid, $context); + if (!empty($options)) { + $repo->options = $options; + } + $file_record['repositoryid'] = $repoid; + // This hook gives the repo a place to do some house cleaning, and update the $reference before it's saved + // to the file store. E.g. transfer ownership of the file to a system account etc. + $reference = $repo->reference_file_selected($file->get_reference(), $context, $component, $filearea, $itemid); + + $file_record['reference'] = $reference; + } + } + + // Changes for PDFAnnotator. + $newentry = $fs->create_file_from_storedfile($file_record, $file); + // Checks if the file size exceeds the max size of files in the PDFAnnotator setting. + $maxfilesize = get_config('mod_pdfannotator', 'maxbytes'); + if ($maxfilesize != 0 && $maxfilesize < $newentry->get_filesize()) { + $params = new stdClass(); + $params->filename = $newentry->get_filename(); + $params->filesize = $newentry->get_filesize(); + $params->maxfilesize = $maxfilesize; + throw new Error(get_string('error:maxsizeoffiles', 'mod_pdfannotator', $params)); + } + // Changes for PDFAnnotator. + $embeddedfile_pdfannotator = new stdClass(); + $embeddedfile_pdfannotator->fileid = $newentry->get_id(); + $embeddedfile_pdfannotator->commentid = $itemid; + $embeddedfile_pdfannotator->id = $DB->insert_record('pdfannotator_embeddedfiles', $embeddedfile_pdfannotator); + // Set the sortorder for the mapping with pdfannotator_embeddedfiles table. + $newentry->set_sortorder($embeddedfile_pdfannotator->id); + } + } + + // note: do not purge the draft area - we clean up areas later in cron, + // the reason is that user might press submit twice and they would loose the files, + // also sometimes we might want to use hacks that save files into two different areas + + if (is_null($text)) { + return null; + } else { + return file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps); + } +} + function pdfannotator_get_instance_name($id) { global $DB; diff --git a/shared/index.js b/shared/index.js index 9be76a86b217128c409366d33c7e0a613bbb9ec5..014762ddb1562950cdce5404060c0f4d5a0e4299 100644 --- a/shared/index.js +++ b/shared/index.js @@ -6024,11 +6024,13 @@ function startIndex(Y,_cm,_documentObject,_contextId, _userid,_capabilities, _to var rectObj; var _svg=void 0; var rect=void 0; + /** * Get the current window selection as rects * * @return {Array} An Array of rects - */function getSelectionRects(){ + */ + function getSelectionRects(){ try{ var selection=window.getSelection(); try{ @@ -6048,11 +6050,13 @@ function startIndex(Y,_cm,_documentObject,_contextId, _userid,_capabilities, _to } return null; - }/** + } + /** * Handle document.mousedown event * * @param {Event} e The DOM event to handle - */function handleDocumentMousedown(e){ + */ + function handleDocumentMousedown(e){ if(!(_svg=(0,_utils.findSVGAtPoint)(e.clientX,e.clientY))|| _type!=='area'){ return; } @@ -6105,12 +6109,12 @@ function startIndex(Y,_cm,_documentObject,_contextId, _userid,_capabilities, _to (0,_utils.disableUserSelect)(); } - /** * Handle document.mousemove event * * @param {Event} e The DOM event to handle - */function handleDocumentMousemove(e){ + */ + function handleDocumentMousemove(e){ if(originX+(e.clientX-originX)<rect.right){ overlay.style.width=e.clientX-originX+'px'; } @@ -6143,7 +6147,8 @@ function startIndex(Y,_cm,_documentObject,_contextId, _userid,_capabilities, _to * Handle document.mouseup event * concerns area,highlight and strikeout * @param {Event} e The DOM event to handle - */function handleDocumentMouseup(e){ + */ + function handleDocumentMouseup(e){ //if the cursor is clicked nothing should happen! if((typeof e.target.getAttribute('className')!='string') && e.target.className.indexOf('cursor') === -1){ document.removeEventListener('mousemove',handleDocumentMousemove); @@ -6280,7 +6285,8 @@ function startIndex(Y,_cm,_documentObject,_contextId, _userid,_capabilities, _to * Handle document.keyup event * * @param {Event} e The DOM event to handle - */function handleDocumentKeyup(e){// Cancel rect if Esc is pressed + */ + function handleDocumentKeyup(e){// Cancel rect if Esc is pressed if(e.keyCode===27){var selection=window.getSelection();selection.removeAllRanges();if(overlay&&overlay.parentNode){overlay.parentNode.removeChild(overlay);overlay=null;document.removeEventListener('mousemove',handleDocumentMousemove);}} } @@ -6403,28 +6409,31 @@ function startIndex(Y,_cm,_documentObject,_contextId, _userid,_capabilities, _to } /** * Enable rect behavior - */function enableRect(type){ - _type=type; - if(_enabled){return;} - - if(_type === 'area'){ - document.getElementById('content-wrapper').classList.add('cursor-area'); - }else if(_type === 'highlight'){ - document.getElementById('content-wrapper').classList.add('cursor-highlight'); - }else if(_type === 'strikeout'){ - document.getElementById('content-wrapper').classList.add('cursor-strikeout'); - } - - _enabled=true; - document.addEventListener('mouseup',handleDocumentMouseup); - document.addEventListener('mousedown',handleDocumentMousedown); - document.addEventListener('keyup',handleDocumentKeyup); - - document.addEventListener('touchstart', handleDocumentTouchstart); - document.addEventListener('touchend', handleDocumentTouchend); - }/** + */ + function enableRect(type){ + _type=type; + if(_enabled){return;} + + if(_type === 'area'){ + document.getElementById('content-wrapper').classList.add('cursor-area'); + }else if(_type === 'highlight'){ + document.getElementById('content-wrapper').classList.add('cursor-highlight'); + }else if(_type === 'strikeout'){ + document.getElementById('content-wrapper').classList.add('cursor-strikeout'); + } + + _enabled=true; + document.addEventListener('mouseup',handleDocumentMouseup); + document.addEventListener('mousedown',handleDocumentMousedown); + document.addEventListener('keyup',handleDocumentKeyup); + + document.addEventListener('touchstart', handleDocumentTouchstart); + document.addEventListener('touchend', handleDocumentTouchend); + } + /** * Disable rect behavior - */function disableRect(){ + */ + function disableRect(){ if(!_enabled){return;} _enabled=false; if(_type === 'area'){ diff --git a/version.php b/version.php index 981da977622d52f7a22be2622839554f2398f859..1350aa61f7610854ec3aaf5a4fa92bb36b87c8ed 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_pdfannotator'; -$plugin->version = 2022102602; +$plugin->version = 2022110200; $plugin->release = 'PDF Annotator v1.4 release 11'; $plugin->requires = 2021051700; $plugin->maturity = MATURITY_STABLE;