moodle-mod_hvp/classes/file_storage.php
2020-08-05 12:54:36 +02:00

885 lines
28 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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/>.
/**
* The mod_hvp file storage
*
* @package mod_hvp
* @copyright 2016 Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_hvp;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/hvp/library/h5p-file-storage.interface.php');
/**
* The mod_hvp file storage class.
*
* @package mod_hvp
* @since Moodle 2.7
* @copyright 2016 Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
class file_storage implements \H5PFileStorage {
/**
* Store the library folder.
*
* @param array $library
* Library properties
*/
// @codingStandardsIgnoreLine
public function saveLibrary($library) {
// Libraries are stored in a system context.
$context = \context_system::instance();
$options = array(
'contextid' => $context->id,
'component' => 'mod_hvp',
'filearea' => 'libraries',
'itemid' => 0,
'filepath' => '/' . \H5PCore::libraryToString($library, true) . '/',
);
// Remove any old existing library files.
self::deleteFileTree($context->id, $options['filearea'], $options['filepath']);
// Move library folder.
self::readFileTree($library['uploadDirectory'], $options);
}
/**
* Store the content folder.
*
* @param string $source
* Path on file system to content directory.
* @param array $content
* Content properties
*/
// @codingStandardsIgnoreLine
public function saveContent($source, $content) {
// Remove any old content.
$this->deleteContent($content);
// Contents are stored in a course context.
$context = \context_module::instance($content['coursemodule']);
$options = array(
'contextid' => $context->id,
'component' => 'mod_hvp',
'filearea' => 'content',
'itemid' => $content['id'],
'filepath' => '/',
);
// Move content folder.
self::readFileTree($source, $options);
}
/**
* Remove content folder.
*
* @param array $content
* Content properties
*/
// @codingStandardsIgnoreLine
public function deleteContent($content) {
$context = \context_module::instance($content['coursemodule']);
self::deleteFileTree($context->id, 'content', '/', $content['id']);
}
/**
* @inheritdoc
*/
// @codingStandardsIgnoreLine
public function cloneContent($id, $newid) {
// Not implemented in Moodle.
}
/**
* Get path to a new unique tmp folder.
*
* @return string Path
*/
// @codingStandardsIgnoreLine
public function getTmpPath() {
global $CFG;
return $CFG->tempdir . uniqid('/hvp-');
}
/**
* Fetch content folder and save in target directory.
*
* @param int $id
* Content identifier
* @param string $target
* Where the content folder will be saved
*/
// @codingStandardsIgnoreLine
public function exportContent($id, $target) {
$cm = \get_coursemodule_from_instance('hvp', $id);
$context = \context_module::instance($cm->id);
self::exportFileTree($target, $context->id, 'content', '/', $id);
}
/**
* Fetch library folder and save in target directory.
*
* @param array $library
* Library properties
* @param string $target
* Where the library folder will be saved
*/
// @codingStandardsIgnoreLine
public function exportLibrary($library, $target) {
$folder = \H5PCore::libraryToString($library, true);
$context = \context_system::instance();
self::exportFileTree("{$target}/{$folder}", $context->id, 'libraries', "/{$folder}/");
}
/**
* Save export in file system
*
* @param string $source
* Path on file system to temporary export file.
* @param string $filename
* Name of export file.
*/
// @codingStandardsIgnoreLine
public function saveExport($source, $filename) {
global $COURSE;
// Remove old export.
$this->deleteExport($filename);
// Create record.
$context = \context_course::instance($COURSE->id);
$record = array(
'contextid' => $context->id,
'component' => 'mod_hvp',
'filearea' => 'exports',
'itemid' => 0,
'filepath' => '/',
'filename' => $filename
);
// Store new export.
$fs = get_file_storage();
$fs->create_file_from_pathname($record, $source);
}
/**
* Get file object for given export file.
*
* @param string $filename
* @return stdClass Moodle file object
*/
// @codingStandardsIgnoreLine
private function getExportFile($filename) {
global $COURSE;
$context = \context_course::instance($COURSE->id);
// Check if file exists.
$fs = get_file_storage();
return $fs->get_file($context->id, 'mod_hvp', 'exports', 0, '/', $filename);
}
/**
* Removes given export file
*
* @param string $filename
*/
// @codingStandardsIgnoreLine
public function deleteExport($filename) {
$file = $this->getExportFile($filename);
if ($file) {
// Remove old export.
$file->delete();
}
}
/**
* Check if the given export file exists
*
* @param string $filename
* @return boolean
*/
// @codingStandardsIgnoreLine
public function hasExport($filename) {
return !!$this->getExportFile($filename);
}
/**
* Will concatenate all JavaScrips and Stylesheets into two files in order
* to improve page performance.
*
* @param array $files
* A set of all the assets required for content to display
* @param string $key
* Hashed key for cached asset
*/
// @codingStandardsIgnoreLine
public function cacheAssets(&$files, $key) {
$context = \context_system::instance();
$fs = get_file_storage();
foreach ($files as $type => $assets) {
if (empty($assets)) {
continue;
}
$content = '';
foreach ($assets as $asset) {
// Find location of asset.
$location = array();
preg_match('/^\/(libraries|development)(.+\/)([^\/]+)$/', $asset->path, $location);
// Locate file.
$file = $fs->get_file($context->id, 'mod_hvp', $location[1], 0, $location[2], $location[3]);
// Get file content and concatenate.
if ($type === 'scripts') {
$content .= $file->get_content() . ";\n";
} else {
// Rewrite relative URLs used inside stylesheets.
$content .= preg_replace_callback(
'/url\([\'"]?([^"\')]+)[\'"]?\)/i',
function ($matches) use ($location) {
if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) {
return $matches[0]; // Not relative, skip.
}
return 'url("../' . $location[1] . $location[2] . $matches[1] . '")';
},
$file->get_content()) . "\n";
}
}
// Create new file for cached assets.
$ext = ($type === 'scripts' ? 'js' : 'css');
$fileinfo = array(
'contextid' => $context->id,
'component' => 'mod_hvp',
'filearea' => 'cachedassets',
'itemid' => 0,
'filepath' => '/',
'filename' => "{$key}.{$ext}"
);
// Store concatenated content.
$fs->create_file_from_string($fileinfo, $content);
$files[$type] = array((object) array(
'path' => "/cachedassets/{$key}.{$ext}",
'version' => ''
));
}
}
/**
* Will check if there are cache assets available for content.
*
* @param string $key
* Hashed key for cached asset
* @return array
*/
// @codingStandardsIgnoreLine
public function getCachedAssets($key) {
$context = \context_system::instance();
$fs = get_file_storage();
$files = array();
$js = $fs->get_file($context->id, 'mod_hvp', 'cachedassets', 0, '/', "{$key}.js");
if ($js) {
$files['scripts'] = array((object) array(
'path' => "/cachedassets/{$key}.js",
'version' => ''
));
}
$css = $fs->get_file($context->id, 'mod_hvp', 'cachedassets', 0, '/', "{$key}.css");
if ($css) {
$files['styles'] = array((object) array(
'path' => "/cachedassets/{$key}.css",
'version' => ''
));
}
return empty($files) ? null : $files;
}
/**
* Remove the aggregated cache files.
*
* @param array $keys
* The hash keys of removed files
*/
// @codingStandardsIgnoreLine
public function deleteCachedAssets($keys) {
$context = \context_system::instance();
$fs = get_file_storage();
foreach ($keys as $hash) {
foreach (array('js', 'css') as $type) {
$cachedasset = $fs->get_file($context->id, 'mod_hvp', 'cachedassets', 0, '/', "{$hash}.{$type}");
if ($cachedasset) {
$cachedasset->delete();
}
}
}
}
/**
* @inheritdoc
*/
// @codingStandardsIgnoreLine
public function getContent($filepath) {
// Grab context and file storage.
$context = \context_system::instance();
$fs = get_file_storage();
// Find location of file.
$location = [];
preg_match('/^\/(libraries|development|cachedassets)(.*\/)([^\/]+)$/', $filepath, $location);
// Locate file.
$file = $fs->get_file($context->id, 'mod_hvp', $location[1], 0, $location[2], $location[3]);
if (!$file) {
throw new \file_serving_exception(
'Could not retrieve the requested file, check your file permissions.'
);
}
// Return content.
return $file->get_content();
}
/**
* Save files uploaded through the editor.
*
* @param \H5peditorFile $file
* @param int $contentid
* @param \stdClass $contextid Course Context ID
*
* @return int
*/
// @codingStandardsIgnoreLine
public function saveFile($file, $contentid, $contextid = null) {
if ($contentid !== 0) {
// Grab cm context.
$cm = \get_coursemodule_from_instance('hvp', $contentid);
$context = \context_module::instance($cm->id);
$contextid = $context->id;
} else if ($contextid === null) {
// Check for context id in params.
$contextid = optional_param('contextId', null, PARAM_INT);
}
// Files not yet related to any activities are stored in a course context
// These are temporary files and should not be part of backups.
$record = array(
'contextid' => $contextid,
'component' => 'mod_hvp',
'filearea' => $contentid === 0 ? 'editor' : 'content',
'itemid' => $contentid,
'filepath' => '/' . $file->getType() . 's/',
'filename' => $file->getName()
);
$fs = get_file_storage();
$storedfile = $fs->create_file_from_pathname($record, $_FILES['file']['tmp_name']);
return $storedfile->get_id();
}
/**
* Copy a file from another content or editor tmp dir.
* Used when copy pasting content in H5P.
*
* @param string $file path + name
* @param string|int $fromid Content ID or 'editor' string
* @param stdClass $tocontent Target Content
*/
// @codingStandardsIgnoreLine
public function cloneContentFile($file, $fromid, $tocontent) {
// Determine source file area and item id.
if ($fromid === 'editor') {
$sourcefilearea = 'editor';
if (empty($tocontent->instance)) {
$sourceitemid = \context_course::instance($tocontent->course);
} else {
$sourceitemid = \context_module::instance($tocontent->coursemodule);
}
} else {
$sourcefilearea = 'content';
$sourceitemid = $fromid;
};
// Check to see if source exist.
$sourcefile = $this->getFile($sourcefilearea, $sourceitemid, $file);
if ($sourcefile === false) {
return; // Nothing to copy from.
}
// Check to make sure source doesn't exist already.
if ($this->getFile('content', $tocontent, $file) !== false) {
return; // File exists, no need to copy.
}
// Grab context for CM.
$context = \context_module::instance($tocontent->coursemodule);
// Create new file record.
$record = [
'contextid' => $context->id,
'component' => 'mod_hvp',
'filearea' => 'content',
'itemid' => $tocontent->id,
'filepath' => $this->getFilepath($file),
'filename' => $this->getFilename($file),
];
$fs = get_file_storage();
$fs->create_file_from_storedfile($record, $sourcefile);
}
/**
* Checks to see if content has the given file.
* Used when saving content.
*
* @param string $file path + name
* @param stdClass $content
* @return string|int File ID or null if not found
*/
// @codingStandardsIgnoreLine
public function getContentFile($file, $content) {
$file = $this->getFile('content', $content, $file);
return ($file === false ? null : $file->get_id());
}
/**
* Remove content files that are no longer used.
* Used when saving content.
*
* @param string $file path + name
* @param stdClass $content
*/
// @codingStandardsIgnoreLine
public function removeContentFile($file, $content) {
$file = $this->getFile('content', $content, $file);
if ($file !== false) {
$file->delete();
}
}
/**
* Copies files from tmp folder to Moodle storage.
*
* @param string $source
* Path to source directory
* @param array $options
* For Moodle's file record
* @throws \Exception Unable to copy
*/
// @codingStandardsIgnoreLine
private static function readFileTree($source, $options) {
$dir = opendir($source);
if ($dir === false) {
trigger_error('Unable to open directory ' . $source, E_USER_WARNING);
throw new \Exception('unabletocopy');
}
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore') {
if (is_dir($source . DIRECTORY_SEPARATOR . $file)) {
$suboptions = $options;
$suboptions['filepath'] .= $file . '/';
self::readFileTree($source . '/' . $file, $suboptions);
} else {
$record = $options;
$record['filename'] = $file;
$fs = get_file_storage();
$fs->create_file_from_pathname($record, $source . '/' . $file);
}
}
}
closedir($dir);
}
/**
* Copies files from Moodle storage to temporary folder.
*
* @param string $target
* Path to temporary folder
* @param int $contextid
* Moodle context where the files are found
* @param string $filearea
* Moodle file area
* @param string $filepath
* Moodle file path
* @param int $itemid
* Optional Moodle item ID
*/
// @codingStandardsIgnoreLine
private static function exportFileTree($target, $contextid, $filearea, $filepath, $itemid = 0) {
// Make sure target folder exists.
if (!file_exists($target)) {
mkdir($target, 0777, true);
}
// Read source files.
$fs = get_file_storage();
$files = $fs->get_directory_files($contextid, 'mod_hvp', $filearea, $itemid, $filepath, true);
foreach ($files as $file) {
// Correct target path for file.
$path = $target . str_replace($filepath, '/', $file->get_filepath());
if ($file->is_directory()) {
// Create directory.
$path = rtrim($path, '/');
if (!file_exists($path)) {
mkdir($path, 0777, true);
}
} else {
// Copy file.
$file->copy_content_to($path . $file->get_filename());
}
}
}
/**
* Recursive removal of given filepath.
*
* @param int $contextid
* @param string $filearea
* @param string $filepath
* @param int $itemid
*/
// @codingStandardsIgnoreLine
private static function deleteFileTree($contextid, $filearea, $filepath, $itemid = 0) {
$fs = get_file_storage();
if ($filepath === '/') {
// Remove complete file area.
$fs->delete_area_files($contextid, 'mod_hvp', $filearea, $itemid);
return;
}
// Look up files and remove.
$files = $fs->get_directory_files($contextid, 'mod_hvp', $filearea, $itemid, $filepath, true);
foreach ($files as $file) {
$file->delete();
}
// Remove root dir.
$file = $fs->get_file($contextid, 'mod_hvp', $filearea, $itemid, $filepath, '.');
if ($file) {
$file->delete();
}
}
/**
* Help make it easy to load content files.
*
* @param string $filearea
* @param int|object $itemid
* @param string $file path + name
*
* @return \stored_file|bool
*/
// @codingStandardsIgnoreLine
private function getFile($filearea, $itemid, $file) {
if ($filearea === 'editor') {
// Itemid is actually cm or course context.
$context = $itemid;
$itemid = 0;
} else if (is_object($itemid)) {
// Grab CM context from item.
$context = \context_module::instance($itemid->coursemodule);
$itemid = $itemid->id;
} else {
// Use item ID to find CM context.
$cm = \get_coursemodule_from_instance('hvp', $itemid);
$context = \context_module::instance($cm->id);
}
// Load file.
$fs = get_file_storage();
return $fs->get_file($context->id, 'mod_hvp', $filearea, $itemid, $this->getFilepath($file), $this->getFilename($file));
}
/**
* Extract Moodle compatible filepath
*
* @param string $file
* @return string With slashes
*/
// @codingStandardsIgnoreLine
private function getFilepath($file) {
return '/' . dirname($file) . '/';
}
/**
* Extract filename from filepath string
*
* @param string $file
* @return string Without slashes
*/
// @codingStandardsIgnoreLine
private function getFilename($file) {
return basename($file);
}
/**
* Checks if a file exists
*
* @method fileExists
* @param string $filearea [description]
* @param string $filepath [description]
* @param string $filename [description]
* @return boolean
*/
// @codingStandardsIgnoreLine
public static function fileExists($contextid, $filearea, $filepath, $filename) {
// Check if file exists.
$fs = get_file_storage();
return ($fs->get_file($contextid, 'mod_hvp', $filearea, 0, $filepath, $filename) !== false);
}
/**
* Check if server setup has write permission to
* the required folders
*
* @return bool true if server has the proper write access
*/
// @codingStandardsIgnoreLine
public function hasWriteAccess() {
global $CFG;
if (!is_dir($CFG->dataroot)) {
trigger_error('Path is not a directory ' . $CFG->dataroot, E_USER_WARNING);
return false;
}
if (!is_writable($CFG->dataroot)) {
trigger_error('Unable to write to ' . $CFG->dataroot . ' check directory permissions ', E_USER_WARNING);
return false;
}
return true;
}
/**
* Copy a content from one directory to another. Defaults to cloning
* content from the current temporary upload folder to the editor path.
*
* @param string $source path to source directory
* @param string $contentid path of target directory. Defaults to editor path
*
* @return object|null Object containing h5p json and content json data
*/
// @codingStandardsIgnoreLine
public function moveContentDirectory($source, $contentid = null) {
if ($source === null) {
return null;
}
// Default to 0 (editor).
if (!isset($contentid)) {
$contentid = 0;
}
// Find content context.
if ($contentid > 0) {
// Grab cm context.
$cm = \get_coursemodule_from_instance('hvp', $contentid);
$context = \context_module::instance($cm->id);
$contextid = $context->id;
}
// Get context from parameters.
if (!isset($contextid)) {
$contextid = required_param('contextId', PARAM_INT);
}
// Get h5p and content json.
$contentsource = $source . DIRECTORY_SEPARATOR . 'content';
// Move all temporary content files to editor.
$contentfiles = array_diff(scandir($contentsource), array('.', '..', 'content.json'));
foreach ($contentfiles as $file) {
if (is_dir("{$contentsource}/{$file}")) {
self::moveFileTree("{$contentsource}/{$file}", $contextid, $contentid);
} else {
self::moveFile("{$contentsource}/{$file}", $contextid, $contentid);
}
}
// TODO: Return list of all files so they can be marked as temporary. JI-366.
}
/**
* Move a single file to editor
*
* @param string $sourcefile Path to source fil
* @param int $contextid Id of context
* @param int $contentid Id of content, 0 if editor
*/
// @codingStandardsIgnoreLine
private static function moveFile($sourcefile, $contextid, $contentid) {
$fs = get_file_storage();
$pathparts = pathinfo($sourcefile);
$filename = $pathparts['basename'];
$filepath = $pathparts['dirname'];
$foldername = basename($filepath);
if ($contentid > 0) {
// Create file record for content.
$record = array(
'contextid' => $contextid,
'component' => 'mod_hvp',
'filearea' => $contentid > 0 ? 'content' : 'editor',
'itemid' => $contentid,
'filepath' => '/' . $foldername . '/',
'filename' => $filename
);
} else {
// Create file record for editor.
$record = array(
'contextid' => $contextid,
'component' => 'mod_hvp',
'filearea' => 'editor',
'itemid' => 0,
'filepath' => '/' . $foldername . '/',
'filename' => $filename
);
}
$sourcedata = file_get_contents($sourcefile);
// Check if file already exists.
$fileexists = $fs->file_exists($record['contextid'], 'mod_hvp',
$record['filearea'], $record['itemid'], $record['filepath'],
$record['filename']
);
if ($fileexists) {
// Delete it to make sure that it is replaced with correct content.
$file = $fs->get_file($record['contextid'], 'mod_hvp',
$record['filearea'], $record['itemid'], $record['filepath'],
$record['filename']
);
if ($file) {
$file->delete();
}
}
$fs->create_file_from_string($record, $sourcedata);
}
/**
* Move a complete file tree to the editor
*
* @param string $sourcefiletree Path of file tree that should be moved
* @param int $contextid Id of context
* @param int $contentid Id of content, 0 for editor
*
* @throws \Exception
*/
// @codingStandardsIgnoreLine
private static function moveFileTree($sourcefiletree, $contextid, $contentid) {
$dir = opendir($sourcefiletree);
if ($dir === false) {
trigger_error('Unable to open directory ' . $sourcefiletree, E_USER_WARNING);
throw new \Exception('unabletocopy');
}
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore') {
if (is_dir("{$sourcefiletree}/{$file}")) {
self::moveFileTree("{$sourcefiletree}/{$file}", $contextid, $contentid);
} else {
self::moveFile("{$sourcefiletree}/{$file}", $contextid, $contentid);
}
}
}
closedir($dir);
}
/**
* Check if the library has a presave.js in the root folder
*
* @param string $libraryname
* @param string $developmentpath
*
* @return bool
*/
// @codingStandardsIgnoreLine
public function hasPresave($libraryname, $developmentpath = null) {
// TODO: Implement.
return false;
}
/**
* Check if upgrades script exist for library.
*
* @param string $machineName
* @param int $majorVersion
* @param int $minorVersion
* @return string Relative path
*/
// @codingStandardsIgnoreLine
public function getUpgradeScript($machinename, $majorversion, $minorversion) {
$context = \context_system::instance();
$fs = get_file_storage();
$area = 'libraries';
$path = "/{$machinename}-{$majorversion}.{$minorversion}/";
$file = 'upgrades.js';
if ($fs->get_file($context->id, 'mod_hvp', $area, 0, $path, $file)) {
return "/{$area}{$path}{$file}";
} else {
return null;
}
}
/**
* Store the given stream into the given file.
*
* @param string $path
* @param string $file
* @param resource $stream
*
* @return bool
*/
// @codingStandardsIgnoreLine
public function saveFileFromZip($path, $file, $stream) {
$filepath = $path . '/' . $file;
// Make sure the directory exists first.
$matches = array();
preg_match('/(.+)\/[^\/]*$/', $filepath, $matches);
// Recursively make directories.
if (!file_exists($matches[1])) {
mkdir($matches[1], 0777, true);
}
// Store in local storage folder.
return file_put_contents($filepath, $stream);
}
}