Skip to content
Snippets Groups Projects
locallib.php 94.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • <?php
    // This file is part of Moodle - http://moodle.org/
    //
    // Moodle is free software: you can redistribute it and/or modify
    // it under the terms of the GNU General Public License as published by
    // the Free Software Foundation, either version 3 of the License, or
    // (at your option) any later version.
    //
    // Moodle is distributed in the hope that it will be useful,
    // but WITHOUT ANY WARRANTY; without even the implied warranty of
    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    // GNU General Public License for more details.
    //
    // You should have received a copy of the GNU General Public License
    // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
    /**
     * @package   mod_pdfannotator
     * @copyright 2018 RWTH Aachen (see README)
     * @author    Rabea de Groot, Anna Heynkes, Friederike Schwager
     * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
     */
    
    use mod_pdfannotator\output\answermenu;
    use mod_pdfannotator\output\questionmenu;
    use mod_pdfannotator\output\reportmenu;
    use mod_pdfannotator\output\index;
    
    defined('MOODLE_INTERNAL') || die;
    
    require_once("$CFG->libdir/filelib.php");
    require_once("$CFG->libdir/resourcelib.php");
    require_once("$CFG->dirroot/mod/pdfannotator/lib.php");
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
    require_once($CFG->dirroot . '/repository/lib.php');
    
    
    /**
     * Display embedded pdfannotator file.
     * @param object $pdfannotator
     * @param object $cm
     * @param object $course
     * @param stored_file $file main file
     * @return does not return
     */
    function pdfannotator_display_embed($pdfannotator, $cm, $course, $file, $page = 1, $annoid = null, $commid = null) {
        global $CFG, $PAGE, $OUTPUT, $USER;
    
        // The revision attribute's existance is demanded by moodle for versioning and could be saved in the pdfannotator table in the future.
        // Note, however, that we forbid file replacement in order to prevent a change of meaning in other people's comments.
        $pdfannotator->revision = 1;
    
        $context = context_module::instance($cm->id);
        $path = '/' . $context->id . '/mod_pdfannotator/content/' . $pdfannotator->revision . $file->get_filepath() . $file->get_filename();
        $fullurl = file_encode_url($CFG->wwwroot . '/pluginfile.php', $path, false);
    
        $documentobject = new stdClass();
        $documentobject->annotatorid = $pdfannotator->id;
        $documentobject->fullurl = $fullurl;
    
        $stringman = get_string_manager();
        // With this method you get the strings of the language-Files.
        $strings = $stringman->load_component_strings('pdfannotator', 'en');
        // Method to use the language-strings in javascript.
        $PAGE->requires->strings_for_js(array_keys($strings), 'pdfannotator');
        // Load and execute the javascript files.
    
    Tim Schroeder's avatar
    Tim Schroeder committed
        $PAGE->requires->js(new moodle_url("/mod/pdfannotator/shared/pdf.js?ver=00002"));
    
        $PAGE->requires->js(new moodle_url("/mod/pdfannotator/shared/textclipper.js"));
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
        $PAGE->requires->js(new moodle_url("/mod/pdfannotator/shared/index.js?ver=00025"));
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
        $PAGE->requires->js(new moodle_url("/mod/pdfannotator/shared/locallib.js?ver=00005"));
    
    
        // Pass parameters from PHP to JavaScript.
    
        // 1. Toolbar settings.
        $toolbarsettings = new stdClass();
        $toolbarsettings->use_studenttextbox = $pdfannotator->use_studenttextbox;
        $toolbarsettings->use_studentdrawing = $pdfannotator->use_studentdrawing;
        $toolbarsettings->useprint = $pdfannotator->useprint;
        $toolbarsettings->useprintcomments = $pdfannotator->useprintcomments;
        // 2. Capabilities.
        $capabilities = new stdClass();
        $capabilities->viewquestions = has_capability('mod/pdfannotator:viewquestions', $context);
        $capabilities->viewanswers = has_capability('mod/pdfannotator:viewanswers', $context);
        $capabilities->viewposts = has_capability('mod/pdfannotator:viewposts', $context);
        $capabilities->viewreports = has_capability('mod/pdfannotator:viewreports', $context);
        $capabilities->deleteany = has_capability('mod/pdfannotator:deleteany', $context);
        $capabilities->hidecomment = has_capability('mod/pdfannotator:hidecomments', $context);
        $capabilities->seehiddencomments = has_capability('mod/pdfannotator:seehiddencomments', $context);
        $capabilities->usetextbox = has_capability('mod/pdfannotator:usetextbox', $context);
        $capabilities->usedrawing = has_capability('mod/pdfannotator:usedrawing', $context);
        $capabilities->useprint = has_capability('mod/pdfannotator:printdocument', $context);
        $capabilities->useprintcomments = has_capability('mod/pdfannotator:printcomments', $context);
    
        $params = [$cm, $documentobject, $context->id, $USER->id, $capabilities, $toolbarsettings, $page, $annoid, $commid];
        $PAGE->requires->js_init_call('adjustPdfannotatorNavbar', null, true);
        $PAGE->requires->js_init_call('startIndex', $params, true);
        // The renderer renders the original index.php / takes the template and renders it.
        $myrenderer = $PAGE->get_renderer('mod_pdfannotator');
        echo $myrenderer->render_index(new index($pdfannotator, $capabilities, $file));
    
        $PAGE->requires->js_init_call('checkOnlyOneCheckbox', null, true);
        pdfannotator_data_preprocessing($context, 'id_pdfannotator_content', "editor-commentlist-inputs");
    
        $PAGE->requires->js_init_call('checkOnlyOneCheckbox', null, true);
    
        pdfannotator_print_intro($pdfannotator, $cm, $course);
    
        echo $OUTPUT->footer();
        die;
    }
    
    
    function pdfannotator_get_image_options_editor() {
        $image_options = new \stdClass();
        $image_options->maxbytes = get_config('mod_pdfannotator', 'maxbytes');
        $image_options->maxfiles = get_config('mod_pdfannotator', 'maxfiles');
        $image_options->autosave = false;
        $image_options->env = 'editor';
        $draftitemid = file_get_unused_draft_itemid();
        $image_options->itemid = $draftitemid;
        return $image_options;
    }
    
    function pdfannotator_get_editor_options($context) {
        $options = [];
        $options = [
            'atto:toolbar' => get_config('mod_pdfannotator', 'attobuttons'),
            'maxbytes' => get_config('mod_pdfannotator', 'maxbytes'),
            'maxfiles' => get_config('mod_pdfannotator', 'maxfiles'),
            'return_types' => 15,
            'enable_filemanagement' => true, 
            'removeorphaneddrafts' => false, 
            'autosave' => true,
            'noclean' => false, 
            'trusttext' => 0,
            'subdirs' => true,
            'forcehttps' => false,
            'areamaxbytes' => FILE_AREA_MAX_BYTES_UNLIMITED,
        ];
        return $options;
    }
    
    function pdfannotator_get_relativelink($content, $commentid, $context) {
        preg_match('/@@PLUGINFILE@@/', $content, $matches);
        if($matches) {
            $relativelink = file_rewrite_pluginfile_urls($content, 'pluginfile.php', $context->id, 'mod_pdfannotator', 'post', $commentid);
            return $relativelink;
        }
        return $content;
    }
    
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
    function pdfannotator_extract_images($contentarr) {
        // Remove quotes here, in case if there is no math form.
        if (gettype($contentarr) === 'string') {
            $str = preg_replace('/[\"]/', "", $contentarr);
            $contentarr = [$str];
        }
        $res = [];
        $index = 0;
        foreach ($contentarr as $content) {
            $index++;
            if (gettype($content) === "array") {
                $res[] = $content;
                continue;
            }
            $res = pdfannotator_split_content_image($content, $res);
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
        return $res;
    }
    
    function pdfannotator_split_content_image($content, $res) {
        $imgmatch = [];
        $firststr = '';
        $data = [];
        while (preg_match_all('/<img/', $content, $imgmatch)) {
            $offsetlength = strlen($content);
            
            $imgpos_start = strpos($content, '<img');                
            $imgpos_end = strpos($content, '>', $imgpos_start);
    
            $firststr = substr($content, 0, $imgpos_start);
            $imgstr = substr($content, $imgpos_start, $imgpos_end - $imgpos_start + 1);
            $laststr = substr($content, $imgpos_end + 1, $offsetlength - $imgpos_end);
    
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
            preg_match('/(https...{1,}[.]((gif)|(jpe)g*|(jpg)|(png)|(svg)|(svgz)))/i', $imgstr, $url);
            preg_match('/(gif)|(jpe)g*|(jpg)|(png)|(svg)|(svgz)/i', $url[0], $format);
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
            if (!$format) {
                throw new \moodle_exception('error:unsupportedextention', 'pdfannotator');
            }
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
            if (in_array('jpg', $format) || in_array('jpeg', $format) || in_array('jpe', $format) 
            || in_array('JPG', $format) || in_array('JPEG', $format) || in_array('JPE', $format)) {
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
                $format[0] = 'jpeg';
            }
            $data['image'] = $url[0];
            $data['format'] = strtoupper($format[0]);
            preg_match('/height=[0-9]+/', $imgstr, $height);
            $data['imageheight'] = str_replace("\"", "", explode('=', $height[0])[1]);
            preg_match('/width=[0-9]+/', $imgstr, $width);
            $data['imagewidth'] = str_replace("\"", "", explode('=', $width[0])[1]);
    
            $res[] = $firststr;
            $res[] = $data;
            $content = $laststr;      
        }
        $res[] = $content;
        return $res;
    
    }
    
    function pdfannotator_data_preprocessing($context, $textarea, $classname, $draftitemid = 0) {
        global $PAGE;
    
        $options = pdfannotator_get_editor_options($context);
    
        // Check if image button is activated.
        $attobuttons = $options['atto:toolbar'];
        $grouplines = explode("\n", $attobuttons);
        $groups = [];
        $imagebtn = false;
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
        $image_options = new stdClass();
    
        foreach ($grouplines as $groupline) {
            $line = explode('=', $groupline);
            $groups = array_map('trim', explode(',', $line[1]));
            if (in_array('image', $groups)) {
                $imagebtn = true;
                break;
            }
        }
    
        $editor = editors_get_preferred_editor(FORMAT_HTML);
    
        if(!$imagebtn) {
            $editor->use_editor($textarea, $options);
        } else {
            // inilialize Filepicker if image button is active.
            $args = new \stdClass();    
            // need these three to filter repositories list.    
            $args->accepted_types = ['web_image'];
            $args->return_types = 15;
            $args->context = $context;
            $args->env = 'filepicker';
            // advimage plugin
            $image_options = (object)initialise_filepicker($args);
            $image_options->maxbytes = get_config('mod_pdfannotator', 'maxbytes');
            $image_options->maxfiles = get_config('mod_pdfannotator', 'maxfiles');
            $image_options->autosave = false;
            $image_options->env = 'editor';
            $draftitemid = file_get_unused_draft_itemid();
            $image_options->itemid = $draftitemid;
            $editor->use_editor($textarea, $options, ['image' => $image_options]);
        }
    
        // Add draftitemid and editorformat into input-tags.
        $editorformat = editors_get_preferred_format(FORMAT_HTML);
    
        $PAGE->requires->js_init_call('inputDraftItemID', [$draftitemid, (int)$editorformat, $classname]);
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
        
        return $draftitemid;
    }
    
    function pdfannotator_file_prepare_draft_area(&$draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null) {
        global $CFG, $USER, $CFG, $DB;
    
        $options = (array)$options;
        if (!isset($options['subdirs'])) {
            $options['subdirs'] = false;
        }
        if (!isset($options['forcehttps'])) {
            $options['forcehttps'] = false;
        }
    
        $usercontext = \context_user::instance($USER->id);
        $fs = get_file_storage();
    
        if (empty($draftitemid)) {
            // create a new area and copy existing files into
            $draftitemid = file_get_unused_draft_itemid();
        }
        $file_record = array('contextid'=>$usercontext->id, 'component'=>'user', 'filearea'=>'draft', 'itemid'=>$draftitemid);
        if (!is_null($itemid) and $files = $fs->get_area_files($contextid, $component, $filearea, $itemid)) {
            foreach ($files as $file) {
                if ($file->is_directory() and $file->get_filepath() === '/') {
                    // we need a way to mark the age of each draft area,
                    // by not copying the root dir we force it to be created automatically with current timestamp
                    continue;
                }
                if (!$options['subdirs'] and ($file->is_directory() or $file->get_filepath() !== '/')) {
                    continue;
                }
    
                // We are adding to an already existing draft area so we need to make sure we don't double add draft files!
                $checkfile = array_merge($file_record, ['filename' => $file->get_filename()]);
                $draftexists = $DB->get_record('files', $checkfile);
                if ($draftexists) {
                    continue;
                }
                $draftfile = $fs->create_file_from_storedfile($file_record, $file);
                // XXX: This is a hack for file manager (MDL-28666)
                // File manager needs to know the original file information before copying
                // to draft area, so we append these information in mdl_files.source field
                // {@link file_storage::search_references()}
                // {@link file_storage::search_references_count()}
                $sourcefield = $file->get_source();
                $newsourcefield = new \stdClass;
                $newsourcefield->source = $sourcefield;
                $original = new \stdClass;
                $original->contextid = $contextid;
                $original->component = $component;
                $original->filearea  = $filearea;
                $original->itemid    = $itemid;
                $original->filename  = $file->get_filename();
                $original->filepath  = $file->get_filepath();
                $newsourcefield->original = \file_storage::pack_reference($original);
                $draftfile->set_source(serialize($newsourcefield));
                // End of file manager hack
            }
        }
        if (!is_null($text)) {
            // at this point there should not be any draftfile links yet,
            // because this is a new text from database that should still contain the @@pluginfile@@ links
            // this happens when developers forget to post process the text
            $text = str_replace("\"$CFG->httpswwwroot/draftfile.php", "\"$CFG->httpswwwroot/brokenfile.php#", $text);
        }
    
    
        if (is_null($text)) {
            return null;
        }
    
        // relink embedded files - editor can not handle @@PLUGINFILE@@ !
        return file_rewrite_pluginfile_urls($text, 'draftfile.php', $usercontext->id, 'user', 'draft', $draftitemid, $options);
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
    /**
     * file_save_draft_area_files for pdfannotator. The difference is that
     * we check the filename in the $text, instead of checking if the
     * maximal file number has been already exceeded.
     */
    function pdfannotator_file_save_draft_area_files($draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null, $forcehttps=false, $commentid=0) {
        global $USER;
    
        // 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);
            $count = 0;
            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;
                    }
                    $filename = $file->get_filename();
                    $match = strpos($text, $filename);
                    if ($options['maxfiles'] != -1 && !$match) {
                        // 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;
                    }
                }
    
                $fs->create_file_from_storedfile($file_record, $file);
            }
        }
    
        // 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;
        return $DB->get_field('pdfannotator', 'name', array('id' => $id), $strictness = MUST_EXIST);
    }
    
    function pdfannotator_get_course_name_by_id($courseid) {
        global $DB;
        return $DB->get_field('course', 'fullname', array('id' => $courseid), $strictness = MUST_EXIST);
    }
    
    function pdfannotator_get_username($userid) {
        global $DB;
        $user = $DB->get_record('user', array('id' => $userid));
        return fullname($user);
    }
    
    function pdfannotator_get_annotationtype_id($typename) {
        global $DB;
        if ($typename == 'point') {
            $typename = 'pin';
        }
        $result = $DB->get_records('pdfannotator_annotationtypes', array('name' => $typename));
        foreach ($result as $r) {
            return $r->id;
        }
    }
    
    function pdfannotator_get_annotationtype_name($typeid) {
        global $DB;
        $result = $DB->get_records('pdfannotator_annotationtypes', array('id' => $typeid));
        foreach ($result as $r) {
            return $r->name;
        }
    }
    
    function pdfannotator_handle_latex($context, string $subject) {
        global $CFG;
        require_once($CFG->dirroot . '/mod/pdfannotator/constants.php');
        $latexapi = get_config('mod_pdfannotator', 'latexapi');
    
        // Look for these formulae: $$ ... $$, \( ... \) and \[ ... \]
        // !!! keep indentation!
        $pattern = <<<'SIGN'
    ~(?:\$\$.*?\$\$)|(?:\\\(.*?\\\))|(?:\\\[.*?\\\])~
    SIGN;
    
        $matches = array();
        $hits = preg_match_all($pattern, $subject, $matches, PREG_OFFSET_CAPTURE);
    
        if ($hits == 0) {
            return $subject;
        }
    
        $textstart = 0;
        $formulalength = 0;
        $formulaoffset = 0;
        $result = [];
        $matches = $matches[0];
        foreach ($matches as $match) {
            $formulalength = strlen($match[0]);
            $formulaoffset = $match[1];
            $string = $match[0];
            $string = str_replace('\xrightarrow', '\rightarrow', $string);
            $string = str_replace('\xlefttarrow', '\leftarrow', $string);
    
            $pos = strpos($string, '\\[');
            if ($pos !== false) {
                $string = substr_replace($string, '', $pos, strlen('\\['));
            }
    
            $pos = strpos($string, '\\(');
            if ($pos !== false) {
                $string = substr_replace($string, '', $pos, strlen('\\('));
            }
    
            $string = str_replace('\\]', '', $string);
    
            $string = str_replace('\\)', '', $string);
    
            $string = str_replace('\begin{aligned}', '', $string);
            $string = str_replace('\end{aligned}', '', $string);
    
            $string = str_replace('\begin{align*}', '', $string);
            $string = str_replace('\end{align*}', '', $string);
    
            // Find any backslash preceding a ( or [ and replace it with \backslash
            $pattern = '~\\\\(?=[\\\(\\\[])~';
            $string = preg_replace($pattern, '\\backslash', $string);
            $match[0] = $string;
    
            $result[] = trim(substr($subject, $textstart, $formulaoffset - $textstart));
            if ($latexapi == LATEX_TO_PNG_GOOGLE_API) {
                $result[] = pdfannotator_process_latex_google($match[0]);
            } else {
                $result[] = pdfannotator_process_latex_moodle($context, $match[0]);
            }
            $textstart = $formulaoffset + $formulalength;
        }
        if ($textstart != strlen($subject) - 1) {
            $result[] = trim(substr($subject, $textstart, strlen($subject) - $textstart));
        }
        return $result;
    }
    
    function pdfannotator_process_latex_moodle($context, $string) {
        global $CFG;
        require_once($CFG->libdir . '/moodlelib.php');
        require_once($CFG->dirroot . '/filter/tex/latex.php');
        require_once($CFG->dirroot . '/filter/tex/lib.php');
        $result = array();
        $tex = new latex();
        $md5 = md5($string);
        $image = $tex->render($string, $md5 . 'png');
        if ($image == false) {
            return false;
        }
        $imagedata = file_get_contents($image);
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
        $result['mathform'] = IMAGE_PREFIX . base64_encode($imagedata);
    
        // Imageinfo returns an array with the info of the size of the image. In Parameter 1 there is the height, which is the only
        // thing needed here.
        $imageinfo = getimagesize($image);
    
    anisa kusumadewi's avatar
    anisa kusumadewi committed
        $result['mathformheight'] = $imageinfo[1];
        $result['extension'] = 'PNG';
    
        return $result;
    }
    /**
     * Function takes a latex code string, modifies and url encodes it for the Google Api to process,
     * and returns the resulting image along with its height
     *
     * @param type $string
     * @return type
     */
    function pdfannotator_process_latex_google(string $string) {
    
        $length = strlen($string);
        $im = null;
        if ($length <= 200) { // Google API constraint XXX find better alternative if possible.
            $latexdata = urlencode($string);
            $requesturl = LATEX_TO_PNG_REQUEST . $latexdata;
            $im = @file_get_contents($requesturl); // '@' suppresses warnings so that one failed google request doesn't prevent the pdf from being printed,
            // but just the one formula from being presented as a picture.
        }
        if ($im != null) {
            $array = [];
            try {
                list($width, $height) = getimagesize($requesturl); // XXX alternative: acess height by decoding the string (saving the extra server request)?
                if ($height != null) {
                    $imagedata = IMAGE_PREFIX . base64_encode($im); // Image.
                    $array['image'] = $imagedata;
                    $array['imageheight'] = $height;
                    return $array;
                }
            } catch (Exception $ex) {
                return $string;
            }
        } else {
            return $string;
        }
    }
    
    function pdfannotator_send_forward_message($recipients, $messageparams, $course, $cm, $context) {
        $name = 'forwardedquestion';
        $text = new stdClass();
        $module = get_string('modulename', 'pdfannotator');
        $modulename = format_string($cm->name, true);
        $text->text = pdfannotator_format_notification_message_text($course, $cm, $context, $module, $modulename, $messageparams, $name);
        $text->url = $messageparams->urltoquestion;
    
        foreach ($recipients as $recipient) {
            $text->html = pdfannotator_format_notification_message_html($course, $cm, $context, $module, $modulename, $messageparams, $name, $recipient);
            pdfannotator_notify_manager($recipient, $course, $cm, $name, $text);
        }
    }
    
    function pdfannotator_notify_manager($recipient, $course, $cm, $name, $messagetext, $anonymous = false) {
    
        global $USER;
        $userfrom = $USER;
        $modulename = format_string($cm->name, true);
        if ($anonymous) {
            $userfrom = clone($USER);
            $userfrom->firstname = get_string('pdfannotatorname', 'pdfannotator') . ':';
            $userfrom->lastname = $modulename;
        }
        $message = new \core\message\message();
        $message->component = 'mod_pdfannotator';
        $message->name = $name;
        $message->courseid = $course->id;
    
        $message->userfrom = $anonymous ? core_user::get_noreply_user() : $userfrom;
    
        $message->userto = $recipient;
        $message->subject = get_string('notificationsubject:' . $name, 'pdfannotator', $modulename);
        $message->fullmessage = $messagetext->text;
        $message->fullmessageformat = FORMAT_PLAIN;
        $message->fullmessagehtml = $messagetext->html;
        $message->smallmessage = get_string('notificationsubject:' . $name, 'pdfannotator', $modulename);
        $message->notification = 1; // For personal messages '0'. Important: the 1 without '' and 0 with ''.
        $message->contexturl = $messagetext->url;
        $message->contexturlname = 'Context name';
        $content = array('*' => array('header' => ' test ', 'footer' => ' test ')); // Extra content for specific processor.
    
        $messageid = message_send($message);
    
        return $messageid;
    }
    
    function pdfannotator_format_notification_message_text($course, $cm, $context, $modulename, $pdfannotatorname, $paramsforlanguagestring, $messagetype) {
        global $CFG;
        $formatparams = array('context' => $context->get_course_context());
        $posttext = format_string($course->shortname, true, $formatparams) .
            ' -> ' .
            $modulename .
            ' -> ' .
            format_string($pdfannotatorname, true, $formatparams) . "\n";
        $posttext .= '---------------------------------------------------------------------' . "\n";
        $posttext .= "\n";
        $posttext .= get_string($messagetype . 'text', 'pdfannotator', $paramsforlanguagestring) . "\n---------------------------------------------------------------------\n";
        return $posttext;
    }
    
    /**
     * Format a notification for HTML.
     *
     * @param string $messagetype
     * @param stdClass $info
     * @param stdClass $course
     * @param stdClass $context
     * @param string $modulename
     * @param stdClass $coursemodule
     * @param string $assignmentname
     */
    function pdfannotator_format_notification_message_html($course, $cm, $context, $modulename, $pdfannotatorname, $report, $messagetype, $recipientid) {
        global $CFG, $USER;
        $formatparams = array('context' => $context->get_course_context());
        $posthtml = '<p><font face="sans-serif">' .
            '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
            format_string($course->shortname, true, $formatparams) .
            '</a> ->' .
            '<a href="' . $CFG->wwwroot . '/mod/pdfannotator/index.php?id=' . $course->id . '">' .
            $modulename .
            '</a> ->' .
            '<a href="' . $CFG->wwwroot . '/mod/pdfannotator/view.php?id=' . $cm->id . '">' .
            format_string($pdfannotatorname, true, $formatparams) .
            '</a></font></p>';
        $posthtml .= '<hr /><font face="sans-serif">';
        $report->urltoreport = $CFG->wwwroot . '/mod/pdfannotator/view.php?id=' . $cm->id . '&action=overviewreports';
        $posthtml .= '<p>' . get_string($messagetype . 'html', 'pdfannotator', $report) . '</p>';
        $linktonotificationsettingspage = new moodle_url('/message/notificationpreferences.php', array('userid' => $recipientid));
        $linktonotificationsettingspage = $linktonotificationsettingspage->__toString();
        $posthtml .= '</font><hr />';
        $posthtml .= '<font face="sans-serif"><p>' . get_string('unsubscribe_notification', 'pdfannotator', $linktonotificationsettingspage) . '</p></font>';
        return $posthtml;
    }
    
    /**
     * Internal function - create click to open text with link.
     */
    function pdfannotator_get_clicktoopen($file, $revision, $extra = '') {
        global $CFG;
    
        $filename = $file->get_filename();
        $path = '/' . $file->get_contextid() . '/mod_pdfannotator/content/' . $revision . $file->get_filepath() . $file->get_filename();
        $fullurl = file_encode_url($CFG->wwwroot . '/pluginfile.php', $path, false);
    
        $string = get_string('clicktoopen2', 'pdfannotator', "<a href=\"$fullurl\" $extra>$filename</a>");
    
        return $string;
    }
    
    /**
     * Internal function - create click to open text with link.
     */
    function pdfannotator_get_clicktodownload($file, $revision) {
        global $CFG;
    
        $filename = $file->get_filename();
        $path = '/' . $file->get_contextid() . '/mod_pdfannotator/content/' . $revision . $file->get_filepath() . $file->get_filename();
        $fullurl = file_encode_url($CFG->wwwroot . '/pluginfile.php', $path, true);
    
        $string = get_string('clicktodownload', 'pdfannotator', "<a href=\"$fullurl\">$filename</a>");
    
        return $string;
    }
    
    /**
     * Print pdfannotator header.
     * @param object $pdfannotator
     * @param object $cm
     * @param object $course
     * @return void
     */
    function pdfannotator_print_header($pdfannotator, $cm, $course) {
        global $PAGE, $OUTPUT;
        $PAGE->set_title($course->shortname . ': ' . $pdfannotator->name);
        $PAGE->set_heading($course->fullname);
        $PAGE->set_activity_record($pdfannotator);
        echo $OUTPUT->header();
    }
    
    /**
     * Gets details of the file to cache in course cache to be displayed using {@see pdfannotator_get_optional_details()}
     *
     * @param object $pdfannotator pdfannotator table row (only property 'displayoptions' is used here)
     * @param object $cm Course-module table row
     * @return string Size and type or empty string if show options are not enabled
     */
    function pdfannotator_get_file_details($pdfannotator, $cm) {
        $filedetails = array();
    
        $context = context_module::instance($cm->id);
        $fs = get_file_storage();
        $files = $fs->get_area_files($context->id, 'mod_pdfannotator', 'content', 0, 'sortorder DESC, id ASC', false);
        // For a typical file pdfannotator, the sortorder is 1 for the main file
        // and 0 for all other files. This sort approach is used just in case
        // there are situations where the file has a different sort order.
        $mainfile = $files ? reset($files) : null;
    
        foreach ($files as $file) {
            // This will also synchronize the file size for external files if needed.
            $filedetails['size'] += $file->get_filesize();
            if ($file->get_repository_id()) {
                // If file is a reference the 'size' attribute can not be cached.
                $filedetails['isref'] = true;
            }
        }
    
        return $filedetails;
    }
    
    /**
     * Print pdfannotator introduction.
     * @param object $pdfannotator
     * @param object $cm
     * @param object $course
     * @param bool $ignoresettings print even if not specified in modedit
     * @return void
     */
    function pdfannotator_print_intro($pdfannotator, $cm, $course, $ignoresettings = false) {
        global $OUTPUT;
        if ($ignoresettings) {
            $gotintro = trim(strip_tags($pdfannotator->intro));
            if ($gotintro || $extraintro) {
                echo $OUTPUT->box_start('mod_introbox', 'pdfannotatorintro');
                if ($gotintro) {
                    echo format_module_intro('pdfannotator', $pdfannotator, $cm->id);
                }
                echo $extraintro;
                echo $OUTPUT->box_end();
            }
        }
    }
    
    /**
     * Print warning that file can not be found.
     * @param object $pdfannotator
     * @param object $cm
     * @param object $course
     * @return void, does not return
     */
    function pdfannotator_print_filenotfound($pdfannotator, $cm, $course) {
        global $DB, $OUTPUT;
    
        pdfannotator_print_header($pdfannotator, $cm, $course);
        // pdfannotator_print_heading($pdfannotator, $cm, $course);//TODO Method is not defined.
        pdfannotator_print_intro($pdfannotator, $cm, $course);
        echo $OUTPUT->notification(get_string('filenotfound', 'pdfannotator'));
    
        echo $OUTPUT->footer();
        die;
    }
    
    /**
     * Function returns the number of new comments, drawings and textboxes*
     * in this annotator. 'New' is defined here as 'no older than 24h' but
     * can easily be changed to another time span.
     * *Drawings and textboxes cannot be commented. In their case (only),
     * therefore, annotations are counted.
     *
     */
    function pdfannotator_get_number_of_new_activities($annotatorid) {
    
        global $DB;
    
        $parameters = array();
        $parameters[] = $annotatorid;
        $parameters[] = strtotime("-1 day");
    
        $sql = "SELECT c.id FROM {pdfannotator_annotations} a JOIN {pdfannotator_comments} c ON c.annotationid = a.id "
            . "WHERE a.pdfannotatorid = ? AND c.timemodified >= ?";
        $sql2 = "SELECT a.id FROM {pdfannotator_annotations} a JOIN {pdfannotator_annotationtypes} t ON a.annotationtypeid = t.id "
            . "WHERE a.pdfannotatorid = ? AND a.timecreated >= ? AND t.name IN('drawing','textbox')";
    
        return ( count($DB->get_records_sql($sql, $parameters)) + count($DB->get_records_sql($sql2, $parameters)) );
    }
    
    /**
     * Function returns the datetime of the last modification on or in the specified annotator.
     * The modification can be the creation of the annotator, a change of title or description,
     * a new annotation or a new comment. Reports are not considered.
     *
     * @param int $annotatorid
     * @return datetime $timemodified
     * The timestamp can be transformed into a readable string with this moodle method:
     * userdate($timestamp, $format = '', $timezone = 99, $fixday = true, $fixhour = true);
     */
    function pdfannotator_get_datetime_of_last_modification($annotatorid) {
    
        global $DB;
    
        // 1. When was the last time the annotator itself (i.e. its title, description or pdf) was modified?
        $timemodified = $DB->get_record('pdfannotator', array('id' => $annotatorid), 'timemodified', MUST_EXIST);
        $timemodified = $timemodified->timemodified;
    
        // 2. When was the last time an annotation or a comment was added in the specified annotator?
        $sql = "SELECT max(a.timecreated) AS last_annotation, max(c.timemodified) AS last_comment "
            . "FROM {pdfannotator_annotations} a LEFT OUTER JOIN {pdfannotator_comments} c ON a.id = c.annotationid "
            . "WHERE a.pdfannotatorid = ?";
        $newposts = $DB->get_records_sql($sql, array($annotatorid));
    
        if (!empty($newposts)) {
    
            foreach ($newposts as $entry) {
    
                // 2.a) If there is an annotation younger than the creation/modification of the annotator, set timemodified to the annotation time.
                if (!empty($entry->last_annotation) && ($entry->last_annotation > $timemodified)) {
                    $timemodified = $entry->last_annotation;
                }
                // 2.b) If there is a comment younger than the creation/modification of the annotator or its newest annotation, set timemodified to the comment time.
                if (!empty($entry->last_comment) && ($entry->last_comment > $timemodified)) {
                    $timemodified = $entry->last_comment;
                }
                return $timemodified;
            }
        }
    }
    
    /**
     * File browsing support class
     */
    class pdfannotator_content_file_info extends file_info_stored {
    
        public function get_parent() {
            if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
                return $this->browser->get_file_info($this->context);
            }
            return parent::get_parent();
        }
    
        public function get_visible_name() {
            if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
                return $this->topvisiblename;
            }
            return parent::get_visible_name();
        }
    
    }
    
    function pdfannotator_set_mainfile($data) {
        global $DB;
        $fs = get_file_storage();
        $cmid = $data->coursemodule;
        $draftitemid = $data->files; // Name from the filemanger.
    
        $context = context_module::instance($cmid);
        if ($draftitemid) {