= 4.1.0 and the presence of the IMagick extension. * * @package sloodle * @copyright Copyright (c) 2009 Sloodle (various contributors) * @license http://www.gnu.org/licenses/gpl-3.0.html GNU GPL v3 * @since Sloodle 0.4.1 * * @contributor Jordan Guinaud * @contributor Peter R. Bloomfield * */ // Path to the ImageMagick "convert" program. Leave it blank to DISABLE this feature for security. // On Linux, it is likely to be '/usr/bin/convert' or '/usr/local/bin/convert' // On Windows, you can probably just use 'convert' // Note: the PDF Importer plugin will attempt to auto-detect the location. You only need to modify this if the plugin can't find it. global $IMAGICK_CONVERT_PATH; $IMAGICK_CONVERT_PATH = '/usr/bin/convert'; /** * Presenter plugin for importing a PDF file as a series of image slides. * * @package sloodle */ class SloodlePluginPresenterPDFImporter extends SloodlePluginBasePresenterImporter { /** * This string will contain a description of the plugin's compatibility after the compatibility check function is called. * It will be stored in the current language when the compatibility check function is called, assuming there is a suitable SLOODLE language file installed. * @var string * @access public */ var $compatibility_summary = ''; /** * Gets the identifier of this plugin. * This function MUST be overridden by sub-classes to return an ID that is unique to the category. * It is possible to have different plugins of the same ID in different categories. * This function is given a very explicitly sloodley name as it lets us ignore any classes which don't declare it. * @access public * @return string|bool The ID of this plugin, or boolean false if this is a base class and should not be instantiated as a plugin. */ function sloodle_get_plugin_id() { return 'pdf-imagemagick'; } /** * Render this importer on a web page. * All importing functionality should be handled by this function as well. * @param string $url A URL to this Presenter, without a "mode" parameter. * @param SloodleModulePresenter $presenter An object representing this Presenter. */ function render($url, $presenter) { // Get translation strings $struploadfile = get_string('upload:file', 'sloodle'); $strselectuploadfile = get_string('upload:selectfile', 'sloodle'); $strimportposition = get_string('presenter:importposition', 'sloodle'); $strimportfromcomputer = get_string('presenter:importfrommycomputer', 'sloodle'); $strimportname = get_string('presenter:importname', 'sloodle'); $strimportnamecaption = get_string('presenter:importnamecaption', 'sloodle'); // Get expected form data $selectfile = optional_param('selectfile', '', PARAM_CLEAN); $uploadfile = optional_param('uploadfile', '', PARAM_CLEAN); $importname = optional_param('importname', '', PARAM_CLEAN); $position = (int)optional_param('sloodleentryposition', -1, PARAM_INT); // Has a file been selected? if (!empty($selectfile)) { // Has a local file name been specified to import from? $localfile = optional_param('sloodleimportfile', '', PARAM_CLEAN); if (!empty($localfile)) return $this->import_file($presenter, $localfile, $importname, $position); } // Has a file been uploaded? if (!empty($uploadfile)) { // Has an upload been made which we can import from? $localfile = ''; $clientfile = ''; $res = $this->process_upload($localfile, $clientfile); if ($res === true) { sloodle_debug("Upload successful
\n"); return $this->import_file($presenter, $localfile, $importname, $position, $clientfile); } if (is_string($res)) error($res, $url.'&mode=edit'); } // No file specified - display forms to let the user select or upload the file. // Determine our maximum upload size $maxsize = sloodle_get_max_post_upload(); $maxsizedesc = sloodle_get_size_description($maxsize); // Open the form echo '
'; echo ''; echo ''; echo ''; echo '

'; // Let the user specify a name for the imported files echo ''; echo ''; echo "

\n"; // Let the user select the position to upload to in the Presentation $this->print_slide_position_menu($presenter, $position); echo "

\n"; // Display our upload form echo '
'; echo '

'.$strimportfromcomputer."

\n"; echo ''; echo ''; echo ''; echo '

['.get_string('upload:maxsize', 'sloodle', $maxsizedesc)."]


\n"; echo '
'."\n"; echo '

'; // TODO: add a separate section allowing the import of a file elsewhere on the web // TODO: add a separate section allowing the import of a file already in the course/site files // Close the form echo "

\n"; } /** * Display a drop-down menu of slides in the current presentation. * @param SloodleModulePresenter $presenter An object representing the Presenter to work from. * @param integer $position Specifies the intially selected position, if known. Defaults to end. */ function print_slide_position_menu($presenter, $position = -1) { // Get translation strings $strimportposition = get_string('presenter:importposition', 'sloodle'); $strimportpositioncaption = get_string('presenter:importpositioncaption', 'sloodle'); // Get a list of slides $slides = $presenter->get_slides(); // Display the form element echo ''; echo '\n"; } /** * Attempt to process a file upload. * @param string $path [out] This reference parameter will contain the path of the uploaded file if an upload has been performed. It will be empty if no upload has occurred. * @param string $name [out] This reference parameter will contain the original name of the uploaded file if an upload has been performed. * @return bool|string Returns true if an upload occurred succesfully. Returns false if no upload has happened. Returns a string containing an error message if an error occurred. */ function process_upload(&$path, &$name) { // If no file has been uploaded, then there is nothing to do if (empty($_FILES['userfile']['name'])) return false; // Is the file empty? if ((int)$_FILES['userfile']['size'] == 0) return get_string('upload:emptyfile', 'sloodle'); // Was an error code specified? if (isset($_FILES['userfile']['error'])) { switch ($_FILES['userfile']['error']) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: return get_string('upload:toobig', 'sloodle'); case UPLOAD_ERR_PARTIAL: return get_string('upload:partial', 'sloodle'); } if ($_FILES['userfile']['error'] != UPLOAD_ERR_OK) return get_string('upload:error', 'sloodle'); } // Store the file path and name $name = $_FILES['userfile']['name']; $path = $_FILES['userfile']['tmp_name']; return true; } /** * Import a file to this Presenter. * @param SloodleModulePresenter $presenter An object representing the Presenter we are importing into. * @param string $path The path of the file to import (must be local... i.e. on disk!) * @param string $name Optional -- a name for the import. If omitted, it will be taken from the clientpath or path. * @param integer $position The position at which the slides should be imported. Optional. Defaults to import at the end. * @param string $clientpath (Optional) Specifies the original client path from which a file name can be extrapolated. * @return bool True if successful or false if not. */ function import_file($presenter, $path, $name = '', $position = -1, $clientpath = '') { global $CFG; $cmid = $presenter->cm->id; $context = get_context_instance(CONTEXT_MODULE, $cmid); $contextid = $context->id; // In Moodle 2, we make an itemid for the file api $itemid = time(); if (!file_exists($path)) error("Import file doesn't exist."); // Start by running a compatibility check -- this just makes sure the extensions are loaded. $this->check_compatibility(); // PHP 4 doesn't support recursive creation of folders, so we need to do this the manual way // For Moodle 2, we upload to a temporary directory, then use the file api to move to the relevant place. $dir_sitefiles = SLOODLE_IS_ENVIRONMENT_MOODLE_2 ? $CFG->dataroot.'/temp/sloodle' : $CFG->dataroot.'/'.SITEID; //$dir_sitefiles = $CFG->dataroot.'/'.SITEID; $dir_presenter = $dir_sitefiles.'/presenter'; $dir_import = $dir_presenter.'/'.$contextid; if (!file_exists($dir_sitefiles)) mkdir($dir_sitefiles); if (!file_exists($dir_presenter)) mkdir($dir_presenter); if (!file_exists($dir_import)) mkdir($dir_import); // Now check on last time that the import folder exists if (!file_exists($dir_import)) { error("Failed to create directory for imported images. Please check the file permissions for your MoodleData folder.

Attempted to create: {$dir_import}"); } // Construct the URL of the folder for viewing the files $dir_view = $CFG->wwwroot; if (SLOODLE_IS_ENVIRONMENT_MOODLE_2) { $dir_view .= '/pluginfile.php/'.intval($contextid).'/mod_sloodle/presenter/'.intval($itemid); } else { $dir_view .= '/file.php/'.SITEID.'/presenter/'.$presenter->cm->id; } // In Moodle 2, make a file path, as distinct to the temporary file saving location $fileapipath = ''; if (SLOODLE_IS_ENVIRONMENT_MOODLE_2) { $fileapipath = '/'.intval($contextid).'/mod_sloodle/presenter/'.intval($itemid).'/'; } // Use the file name from the path if necessary if (empty($name)) { if (!empty($clientpath)) $name = basename($clientpath, '.pdf'); else $name = basename($path); } // Construct a basic identifier for the files which will be imported. // It will consist of a timestamp and the import name $filebase = gmdate('U').'_'.str_replace(" ", "_", $name); // We'll use JPG files as standard just now. We could make this customizable in future? $ext = 'jpg'; // Attempt each importing method in turn $result = $this->_import_MagickWand($presenter, $path, $dir_import, $dir_view, $filebase, $ext, $name, $position, $itemid, $contextid, $fileapipath); if ($result === false) $result = $this->_import_ImageMagick($presenter, $path, $dir_import, $dir_view, $filebase, $ext, $name, $position, $itemid, $contextid, $fileapipath); // Prepare a "Continue" link which takes us to edit mode $continueURL = $CFG->wwwroot."/mod/sloodle/view.php?id={$presenter->cm->id}&mode=edit"; // Display the results (in future, it might be good to show a list of slides, and let the user rename or delete them before addition to the presentation) if ($result === false) { echo "

",get_string('presenter:importfailed', 'sloodle'),"

\n"; echo "

",get_string('presenter:importneedimagick', 'sloodle'),"

\n"; $strcontinue = get_string('continue'); echo "

( {$strcontinue} )

"; return false; } echo "

",get_string('presenter:importsuccessful', 'sloodle', $result),"

\n"; redirect($continueURL, '', 5); return true; } /** * Imports the given file using the MagickWand extension if possible. (Internal only) * @param SloodleModulePresenter $presenter An object representing the Presenter we are importing into. * @param string $srcfile Full path of the PDF file we are importing * @param string $destpath Folder path to which the imported files will be added. * @param string $viewurl URL of the folder in which the imported files will be viewed * @param string $destfile Name for the output files (excluding extension, such as .jpg). The page numbers will be appended automatically, before the extension * @param string $destfileext Extension for destination files, not including the dot. (e.g. "jpg" or "png"). * @param string $destname Basic name to use for each imported slide. The page numbers will be appended automatically. * @param integer $position The position within the Presentation to add the new slides. Optional. Default is to put them at the end. * @return integer|bool If successful, an integer indicating the number of slides loaded is displayed. If the import does not (or cannot) work, then boolean false is returned. * @access private */ function _import_MagickWand($presenter, $srcfile, $destpath, $viewurl, $destfile, $destfileext, $destname, $position = -1, $itemid = '', $contextid= '', $fileapipath = '') { global $CFG; // Only continue if the MagickWand extension is loaded (this is done by the check_compatibility function) if (!extension_loaded('magickwand')) return false; // Load the PDF file sloodle_debug('Loading PDF file... '); $mwand = NewMagickWand(); if (!MagickReadImage($mwand, $srcfile)) { sloodle_debug('failed.
'); return false; } sloodle_debug('OK.
'); // Quick validation - position should start at 1. (-ve numbers mean "at the end") if ($position == 0) $position = 1; // Go through each page sloodle_debug('Preparing to iterate through pages of document...
'); MagickSetFirstIterator($mwand); $pagenum = 0; $page_position = -1; do { // Determine this page's position in the Presentation if ($position > 0) $page_position = $position + $pagenum; $pagenum++; // Construct the file and slide names for this page $page_filename = "{$destpath}/{$destfile}-{$pagenum}.{$destfileext}"; // Where it gets uploaded to $page_slidesource = "{$viewurl}/{$destfile}-{$pagenum}.{$destfileext}"; // The URL to access it publicly $page_slidename = "{$destname} ({$pagenum})"; // Output the file sloodle_debug(" Writing page {$pagenum} to file..."); if (!MagickWriteImage($mwand, $page_filename)) { sloodle_debug('failed.
'); } else { sloodle_debug('OK.
'); } if (SLOODLE_IS_ENVIRONMENT_MOODLE_2) { $registered = $this->_register_moodle_api_file( $page_filename, $itemid, $contextid, $fileapipath, "{$destfile}-{$pagenum}.{$destfileext}"); @unlink($page_filename); if (!$registered) { return false; } } // Add the entry to the Presenter sloodle_debug(" Adding slide \"{$page_slidename}\" to presentation at position {$page_position}... "); if (!$presenter->add_entry($page_slidesource, 'image', $page_slidename, $page_position)) { sloodle_debug('failed.
'); } else { sloodle_debug('OK.
'); } } while (MagickNextImage($mwand)); sloodle_debug('Finished.
'); DestroyMagickWand($mwand); return $pagenum; } /** * Imports the given file using the ImageMagick command line programs if possible. (Internal only) * @param SloodleModulePresenter $presenter An object representing the Presenter we are importing into. * @param string $srcfile Full path of the PDF file we are importing * @param string $destpath Folder path to which the imported files will be added. * @param string $viewurl URl of the folder in which the files can be viewed * @param string $destfile Name for the output files (excluding extension, such as .jpg). The page numbers will be appended automatically, before the extension * @param string $destfileext Extension for destination files, not including the dot. (e.g. "jpg" or "png"). * @param string $destname Basic name to use for each imported slide. The page numbers will be appended automatically. * @param integer $position The position within the Presentation to add the new slides. Optional. Default is to put them at the end. * @param string $itemid The item id - used to make a unique directory name to avoid naming clashes. * @return integer|bool If successful, an integer indicating the number of slides loaded is displayed. If the import does not (or cannot) work, then boolean false is returned. * @access private */ function _import_ImageMagick($presenter, $srcfile, $destpath, $viewurl, $destfile, $destfileext, $destname, $position = -1, $itemid = '', $contextid = '', $fileapipath = '') { global $IMAGICK_CONVERT_PATH; // Do a security check -- has command-line execution of IMagick been disabled by the admin? sloodle_debug("
Attempting to use ImageMagick by command-line.
"); if (empty($IMAGICK_CONVERT_PATH)) { sloodle_debug(" ERROR: path to ImageMagick \'convert\' program is blank."); return false; } // Now make sure there are no quotation marks in the source/destination file and path names // (these could be used to execute malicious commands on the server) if (strpos($srcfile, "\"") !== false || strpos($destpath, "\"") !== false || strpos($destfile, "\"") !== false || strpos($destfileext, "\"") != false) error("Invalid file name -- please remove quotation marks from file names."); // Construct the conversion command $srcfile_shell_clean = escapeshellarg($srcfile); $destpath_shell_clean = escapeshellarg($destpath.'/'.$destfile.'-'); $destfileext_shell_clean = escapeshellarg('.'.$destfileext); //$cmd = "\"{$IMAGICK_CONVERT_PATH}\" -verbose \"{$srcfile_shell_clean}\" \"{$destpath_shell_clean}/{$destfile_shell_clean}-%d.{$destfileext_shell_clean}\""; $cmd = "{$IMAGICK_CONVERT_PATH} -verbose {$srcfile_shell_clean} {$destpath_shell_clean}%d{$destfileext_shell_clean}"; if (substr(php_uname(), 0, 7) == "Windows") $cmd = 'start /B "" '.$cmd; // Windows compatibility sloodle_debug(" Executing shell command: {$cmd}
"); $output = array(); $result = @exec($cmd, $output); // If all the output is empty, then execution failed if (empty($result) && empty($output)) { sloodle_debug(" ERROR: execution of the shell command failed.
"); //echo "
"; print_r($output); echo "

"; return false; } // Quick validation - position should start at 1. (-ve numbers mean "at the end") if ($position == 0) $position = 1; // Go through each page which was created. // Stop when we encounter a file which wasn't created -- that will be the end of the document. $pagenum = 0; $page_position = -1; $stop = false; while ($stop == false && $pagenum < 10000) { // Determine this page's position in the Presentation if ($position > 0) $page_position = $position + $pagenum; // Construct the file and slide names for this page $page_filename = "{$destpath}/{$destfile}-{$pagenum}.{$destfileext}"; // Where it gets uploaded to $page_slidesource = "{$viewurl}/{$destfile}-{$pagenum}.{$destfileext}"; // The URL to access it publicly $page_slidename = "{$destname} (".($pagenum + 1).")"; // Was this file created? if (file_exists($page_filename)) { if (SLOODLE_IS_ENVIRONMENT_MOODLE_2) { $registered = $this->_register_moodle_api_file( $page_filename, $itemid, $contextid, $fileapipath, "{$destfile}-{$pagenum}.{$destfileext}"); @unlink($page_filename); if (!$registered) { return false; } } // Add it to the Presenter $presenter->add_entry($page_slidesource, 'image', $page_slidename, $page_position); $pagenum++; } else { $stop = true; } } return $pagenum; } function _register_moodle_api_file( $tempfile, $itemid, $contextid, $fileapipath, $filename ) { $fs = get_file_storage(); $fileinfo = array( 'contextid' => $contextid, // ID of context 'component' => 'mod_sloodle', // usually = table name 'filearea' => 'presenter', // usually = table name 'itemid' => $itemid, // usually = ID of row in table 'filepath' => $fileapipath, 'filename' => $filename ); $fs->create_file_from_pathname( $fileinfo, $tempfile); return true; } /** * Gets the human-readable name of this plugin. * @param string $lang Optional -- can specify the language we want the plugin name in, as an identifier like "en_utf8". If unspecified, then the current Moodle language should be used. * @access public * @return string The human-readable name of this plugin */ function get_plugin_name($lang = null) { return 'PDF Importer'; } /** * Gets the human-readable description of this plugin. */ function get_plugin_description($lang = null) { return 'Imports an Adbobe Acrobat (PDF) file into your Presentation. Each page becomes a single image slide.'; } /** * Gets the internal version number of this plugin. * This should be a number like the internal version number for Moodle modules, containing the date and release number. * Format is: YYYYMMDD##. * For example, "2009012302" would be the 3rd release on the 23rd January 2009. * @return int The version number of this module. */ function get_version() { return 2010020400; } /** * Checks the compatibility of this plugin with the current installation. * Override this for any plugin which has non-standard requirements, such as relying on particular PHP extensions. * Note that the default (base class) implementation of this function returns true. * @todo It would be useful if this function performed the auto-detection of the ImageMagick 'convert' program whether MagickWand is available or not. * @return bool True if plugin is compatible, or false if not. */ function check_compatibility() { global $IMAGICK_CONVERT_PATH; $this->compatibility_summary = ''; // Check to see if the MagickWand extension is already loaded.. if (extension_loaded('magickwand')) { $this->compatibility_summary .= get_string('presenter:usingmagickwand', 'sloodle'); return true; } // Attempt to load the extension, depending on OS. if ( (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') && function_exists('dl')) @dl('php_magickwand.dll'); else if (function_exists('dl')) @dl('magickwand.so'); // Check if MagickWand has been successfully loaded now. if (extension_loaded('magickwand')) { $this->compatibility_summary .= get_string('presenter:usingmagickwand', 'sloodle'); return true; } $this->compatibility_summary .= get_string('presenter:magickwandnotinstalled', 'sloodle').' '; // Only do this if the use of ImageMagick via shell commands has not been disabled if (!empty($IMAGICK_CONVERT_PATH)) { // Build a list of locations to check for the ImageMagick program $checkLocs = array(); $checkLocs[] = $IMAGICK_CONVERT_PATH; $checkLocs[] = '/usr/bin/convert'; $checkLocs[] = '/usr/local/bin/convert'; // Edmund Edgar, 2011-10-30: // Disabling this - I'm not happy with the security implications of trusting any file called "convert" or "convert.exe" in the web user's path. // Uncomment if you're really sure you want to do this. // $checkLocs[] = 'convert'; // $checkLocs[] = 'convert.exe'; // Check for the presence of the ImageMagick convert program at each location foreach ($checkLocs as $loc) { // Make sure it's a safe command $cmd = $loc.' -version'; // Attempt to execute it $output = array(); @exec($cmd, $output); // Do we have any output? And does it look like ImageMagick output? if (!empty($output) && strpos($output[0], 'ImageMagick') !== false) { // Store this path $this->compatibility_summary .= get_string('presenter:usingexecutable', 'sloodle'); if ($loc != $IMAGICK_CONVERT_PATH) $IMAGICK_CONVERT_PATH = $loc; return true; } } $this->compatibility_summary .= get_string('presenter:convertnotfound', 'sloodle'); } else { $this->compatibility_summary .= get_string('presenter:convertdisabled', 'sloodle'); } return false; } /** * After check_compatibility() has been called, this function will return a string summarising the compatibility of the plugin. * For example, it may explain that a particular extension is being used, or that it could not be loaded. * @return string A summary of the compatibility of the plugin. */ function get_compatibility_summary() { return $this->compatibility_summary; } /** * Run a full compatibility test and output the results to the webpage. * @return bool True if plugin is compatible, or false otherwise. */ function run_compatibility_test() { global $IMAGICK_CONVERT_PATH; // Check to see if the MagickWand extension is already loaded echo "

MagickWand

\n"; echo "Checking to see if MagickWand extension is loaded.
"; if (extension_loaded('magickwand')) { echo "Success. MagickWand extension was already loaded.
"; return true; } echo "MagickWand extension not already loaded.
"; // Attempt to load the extension, depending on OS. if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { echo "Windows OS detected. Attempting to load 'php_magickwand.dll' extension dynamically.
"; if(function_exists('dl')) { @dl('php_magickwand.dll'); } } else { echo "Attempting to load 'php_magickwand.so' extension dynamically.
"; if(function_exists('dl')) { @dl('magickwand.so'); } } // Check if MagickWand has been successfully loaded now. if (extension_loaded('magickwand')) { echo "Success. MagickWand extension can be loaded dynamically.
"; return true; } echo "MagickWand extension could not be loaded. This plugin will attempt to use ImageMagick directly.
"; echo "

ImageMagick

\n"; // Only do this if the use of ImageMagick via shell commands has not been disabled if (!empty($IMAGICK_CONVERT_PATH)) { echo "Attempting to auto-detect location of the ImageMagick 'convert' program.
"; // Build a list of locations to check for the ImageMagick program $checkLocs = array(); $checkLocs[] = $IMAGICK_CONVERT_PATH; $checkLocs[] = '/usr/bin/convert'; $checkLocs[] = '/usr/local/bin/convert'; // Edmund Edgar, 2011-10-30: // Disabling this - I'm not happy with the security implications of trusting any file called "convert" or "convert.exe" in the web user's path. // Uncomment if you're really sure you want to do this. // $checkLocs[] = 'convert'; // $checkLocs[] = 'convert.exe'; // Check for the presence of the ImageMagick convert program at each location foreach ($checkLocs as $loc) { echo "
Checking for program at location: \"{$loc}\"...
"; // Make sure it's a safe command $cmd = $loc.' -version'; // Attempt to execute it $output = array(); @exec($cmd, $output); if (empty($output)) { echo " - path is not executable.
"; } else if (strpos($output[0], 'ImageMagick') === false) { echo " - executable does not appear to belong to Imagemagick.
"; } else { echo "- success. This appears to be the ImageMagick 'convert' program.
"; return true; } } echo "
Unable to locate the ImageMagick 'convert' program.
"; return false; } echo "The use of ImageMagick by shell execution has been disabled.
"; return false; } } ?>