You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
536 lines
19 KiB
536 lines
19 KiB
<?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/>.
|
|
|
|
/**
|
|
* Tile photo class for format tiles.
|
|
* @package format_tiles
|
|
* @copyright 2019 David Watson {@link http://evolutioncode.uk}
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace format_tiles;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
/**
|
|
* Tile photo class for format tiles.
|
|
* @package format_tiles
|
|
* @copyright 2019 David Watson {@link http://evolutioncode.uk}
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
class tile_photo {
|
|
|
|
/**
|
|
* Course id for this course.
|
|
* @var int
|
|
*/
|
|
private $courseid;
|
|
|
|
/**
|
|
* Section id we are concerned with.
|
|
* @var int
|
|
*/
|
|
private $sectionid;
|
|
|
|
/**
|
|
* Context we are concerned with (will be course context).
|
|
* @var \context_course
|
|
*/
|
|
private $context;
|
|
|
|
/**
|
|
* The course format option reflecting this tile_photo object (from course_format_options table).
|
|
* @var mixed
|
|
*/
|
|
private $courseformatoption;
|
|
|
|
/**
|
|
* The filename relating to this tile_photo object.
|
|
* @var
|
|
*/
|
|
private $filename;
|
|
|
|
/**
|
|
* The file object to which this tile_photo object relates.
|
|
* @var
|
|
*/
|
|
private $file;
|
|
|
|
/**
|
|
* Creates a new instance of class
|
|
*
|
|
* @param int $courseid
|
|
* @param int $sectionid
|
|
* @throws \dml_exception
|
|
*/
|
|
public function __construct($courseid, $sectionid) {
|
|
global $DB;
|
|
$this->courseid = $courseid;
|
|
$this->sectionid = $sectionid;
|
|
$this->context = \context_course::instance($courseid);
|
|
$this->courseformatoption = $this->get_course_format_option();
|
|
if (isset($this->courseformatoption->value)) {
|
|
$this->filename = $this->courseformatoption->value;
|
|
} else {
|
|
// As no course format option is set, we should not have any files set either. Make sure.
|
|
self::delete_file_from_ids($courseid, $sectionid);
|
|
}
|
|
// Ensure that this section really exists in this course.
|
|
$DB->get_record('course_sections', array('course' => $this->courseid, 'id' => $this->sectionid), "id", MUST_EXIST);
|
|
}
|
|
|
|
/**
|
|
* Get the data from the course_format_options table for this tile_photo object.
|
|
* @return mixed
|
|
* @throws \dml_exception
|
|
*/
|
|
private function get_course_format_option() {
|
|
global $DB;
|
|
return $DB->get_record('course_format_options', array(
|
|
'format' => 'tiles',
|
|
'courseid' => $this->courseid,
|
|
'sectionid' => $this->sectionid,
|
|
'name' => 'tilephoto'
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the data from the course_format_options table for this tile_photo object.
|
|
* @param int $sectionid the section if that the format option relates to.
|
|
* @return mixed
|
|
* @throws \dml_exception
|
|
*/
|
|
public static function get_course_format_option_value($sectionid) {
|
|
global $DB;
|
|
$field = $DB->get_field('course_format_options', 'value', array(
|
|
'format' => 'tiles',
|
|
'sectionid' => $sectionid,
|
|
'name' => 'tilephoto'
|
|
)
|
|
);
|
|
return($field);
|
|
}
|
|
|
|
/**
|
|
* Set the data in the course_format_options table for this tile_photo object.
|
|
* @throws \dml_exception
|
|
* @throws \required_capability_exception
|
|
*/
|
|
public function set_course_format_option() {
|
|
global $DB;
|
|
require_capability('moodle/course:update', \context_course::instance($this->courseid));
|
|
|
|
$record = $this->get_course_format_option();
|
|
if (!$record) {
|
|
$record = new \stdClass();
|
|
$record->format = 'tiles';
|
|
$record->name = 'tilephoto';
|
|
$record->courseid = $this->courseid;
|
|
$record->sectionid = $this->sectionid;
|
|
$record->value = $this->filename;
|
|
$record->id = $DB->insert_record('course_format_options', $record, true);
|
|
} else {
|
|
$record->value = $this->filename;
|
|
$DB->update_record('course_format_options', $record);
|
|
}
|
|
$this->courseformatoption = $record;
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Get the image url associated with this tile_photo object.
|
|
* @return bool|\moodle_url
|
|
*/
|
|
public function get_image_url () {
|
|
$config = self::file_api_params();
|
|
if (!$this->filename) {
|
|
return false;
|
|
} else {
|
|
return \moodle_url::make_pluginfile_url(
|
|
$this->context->id,
|
|
$config['component'],
|
|
$config['filearea'],
|
|
$this->sectionid,
|
|
$config['filepath'],
|
|
$this->filename,
|
|
false
|
|
)->out();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a course context id, section id and a filename, get the related photo file.
|
|
* @param int $contextid the context id.
|
|
* @param int $sectionid the section id.
|
|
* @param string $filename the file name.
|
|
* @return bool|\stored_file
|
|
*/
|
|
public static function get_file_from_ids($contextid, $sectionid, $filename) {
|
|
$fs = get_file_storage();
|
|
$config = self::file_api_params();
|
|
return $fs->get_file(
|
|
$contextid,
|
|
$config['component'],
|
|
$config['filearea'],
|
|
$sectionid,
|
|
$config['filepath'],
|
|
$filename
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the image file associated with this tile_photo object.
|
|
* @return bool|\stored_file
|
|
*/
|
|
public function get_file() {
|
|
if (!isset($this->file)) {
|
|
$this->file = self::get_file_from_ids($this->context->id, $this->sectionid, $this->filename);
|
|
}
|
|
return $this->file;
|
|
}
|
|
|
|
/**
|
|
* When course_section_deleted is trigger we remove related files.
|
|
* @param int $courseid the course id.
|
|
* @param int $sectionid the section id.
|
|
* @return bool
|
|
*/
|
|
public static function delete_file_from_ids($courseid, $sectionid) {
|
|
$params = self::file_api_params();
|
|
$fs = get_file_storage();
|
|
$contextid = \context_course::instance($courseid)->id;
|
|
if ($contextid) {
|
|
return $fs->delete_area_files(
|
|
$contextid,
|
|
$params['component'],
|
|
$params['filearea'],
|
|
$sectionid
|
|
);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used if we already have a stored file that we want to set as the file for this object.
|
|
* E.g. we are converting from Grid format and the file is already saved.
|
|
* @param \stored_file $file
|
|
* @throws \dml_exception
|
|
* @throws \required_capability_exception
|
|
*/
|
|
public function set_file($file) {
|
|
$this->file = $file;
|
|
$this->filename = $file->get_filename();
|
|
$this->set_course_format_option();
|
|
}
|
|
|
|
/**
|
|
* Handle an existing stored file (e.g. a user draft file or a file used in another course).
|
|
* Scale the image to suit this plugin and then save it and update this object.
|
|
* @param \stored_file $sourcefile
|
|
* @param string $newfilename
|
|
* @return bool|\stored_file
|
|
* @throws \dml_exception
|
|
* @throws \file_exception
|
|
* @throws \moodle_exception
|
|
* @throws \required_capability_exception
|
|
* @throws \stored_file_creation_exception
|
|
*/
|
|
public function set_file_from_stored_file($sourcefile, $newfilename) {
|
|
if ($sourcefile) {
|
|
if ($sourcefile->get_itemid() == $this->sectionid
|
|
&& $sourcefile->get_contextid() == $this->context->id
|
|
&& $sourcefile->get_filename() == $this->filename
|
|
&& $sourcefile->get_filepath() == self::file_api_params()['filepath']) {
|
|
debugging("File is already set for this section");
|
|
return false;
|
|
}
|
|
$sourceimageinfo = $sourcefile->get_imageinfo();
|
|
$newwidth = self::get_max_image_width();
|
|
|
|
// In case the new file has the same name as the old one, delete it early.
|
|
// Otherwise we do it in a few lines' time when we know we have the new one.
|
|
if ($this->filename == $sourcefile->get_filename()) {
|
|
$this->delete_stored_file();
|
|
}
|
|
|
|
$newfile = image_processor::adjust_and_copy_file(
|
|
$sourcefile,
|
|
$newfilename,
|
|
$this->context,
|
|
$this->sectionid,
|
|
$newwidth,
|
|
$sourceimageinfo['height'] * $newwidth / $sourceimageinfo['width']
|
|
);
|
|
if ($newfile) {
|
|
if ($this->filename != $sourcefile->get_filename()) {
|
|
// We didn't delete the file a few lines ago so do it now.
|
|
$this->delete_stored_file();
|
|
}
|
|
$this->set_file($newfile);
|
|
return $newfile;
|
|
} else {
|
|
debugging('Failed to set file from details - filename ' . $newfilename);
|
|
|
|
// Restore the original file name of the original file.
|
|
debugging("New file could not be created");
|
|
debugging($newfile);
|
|
$this->get_file()->rename(self::file_api_params()['filepath'], $this->filename);
|
|
return false;
|
|
}
|
|
} else {
|
|
debugging('Failed to set file from details - filename ' . $newfilename);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the aspect ratio is a normal landscape one or not.
|
|
* @return array message as to whether it is or not.
|
|
* @throws \coding_exception
|
|
* @throws \dml_exception
|
|
*/
|
|
public function verify_aspect_ratio() {
|
|
$file = $this->get_file();
|
|
if (!$file) {
|
|
debugging("No stored file found");
|
|
$this->clear();
|
|
return array('status' => false);
|
|
}
|
|
|
|
$requiredratio = 0.666; // Landscape is 2:3 ratio height:width.
|
|
// We allow 5% error without warning.
|
|
// Beyond 5% we accept incorrect aspect ratios but warn the user.
|
|
$tolerance = 0.05;
|
|
|
|
$imageinfo = $file->get_imageinfo();
|
|
$ratio = $imageinfo['height'] / $imageinfo['width'];
|
|
$messageshort = get_string('imagesize', 'format_tiles') . ": ";
|
|
if (abs($ratio - $requiredratio) > $tolerance) {
|
|
if ($ratio > $requiredratio) {
|
|
$tallorwide = array(
|
|
'tallorwide' => get_string('tootall', 'format_tiles')
|
|
);
|
|
$messageshort .= get_string('tootall', 'format_tiles');
|
|
} else {
|
|
$tallorwide = array(
|
|
'tallorwide' => get_string('toowide', 'format_tiles'),
|
|
);
|
|
$messageshort .= get_string('toowide', 'format_tiles');
|
|
}
|
|
return array(
|
|
'status' => false,
|
|
'message' => get_string(
|
|
'aspectratiotootallorwide',
|
|
'format_tiles',
|
|
$tallorwide
|
|
),
|
|
'messageshort' => $messageshort
|
|
);
|
|
}
|
|
$messageshort .= get_string('ok', 'format_tiles');
|
|
return array('status' => true, 'message' => $messageshort, 'messageshort' => $messageshort);
|
|
}
|
|
|
|
/**
|
|
* Clear the data associated with this tile_photo object.
|
|
* @throws \dml_exception
|
|
*/
|
|
public function clear() {
|
|
global $DB;
|
|
if (isset($this->courseid) && isset($this->sectionid)) {
|
|
$DB->delete_records(
|
|
'course_format_options',
|
|
array(
|
|
'format' => 'tiles',
|
|
'name' => 'tilephoto',
|
|
'courseid' => $this->courseid,
|
|
'sectionid' => $this->sectionid,
|
|
)
|
|
);
|
|
}
|
|
$this->courseformatoption = null;
|
|
$this->delete_stored_file();
|
|
}
|
|
|
|
/**
|
|
* When a course is switched in to "Tiles" we may have Tiles images sitting in the database.
|
|
* This would happen if the course was once in tiles but was switched to something else.
|
|
* We delete them so that we can start again.
|
|
* Really we should run this when we switch *out* of tiles too, as a clean up exercise (later release).
|
|
* @param int $courseid the id for this course.
|
|
* @return bool whether successful.
|
|
*/
|
|
public static function delete_all_tile_photos_course($courseid) {
|
|
$fs = get_file_storage();
|
|
$fileapiparams = self::file_api_params();
|
|
return $fs->delete_area_files(
|
|
\context_course::instance($courseid)->id,
|
|
$fileapiparams['component'],
|
|
$fileapiparams['filearea']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete the file stored for this object from file storage, and from this object.
|
|
* @return bool
|
|
*/
|
|
private function delete_stored_file() {
|
|
$file = $this->get_file();
|
|
if ($file) {
|
|
return $file->delete();
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a given course, find out the IDs of all tiles which have photos instead of icons.
|
|
* @param int $courseid the course we are interested in.
|
|
* @return array the array of relevant tile ids.
|
|
* @throws \dml_exception
|
|
*/
|
|
public static function get_photo_tile_ids($courseid) {
|
|
global $DB;
|
|
$records = $DB->get_records(
|
|
'course_format_options',
|
|
array('format' => 'tiles', 'courseid' => $courseid, 'name' => 'tilephoto'),
|
|
'sectionid',
|
|
'sectionid'
|
|
);
|
|
return(array_keys($records));
|
|
}
|
|
|
|
/**
|
|
* Types of files that we allow to be uploaded as tile backgrounds.
|
|
* @return array
|
|
*/
|
|
public static function allowed_file_types() {
|
|
return array('image/gif', 'image/jpeg', 'image/png');
|
|
}
|
|
|
|
/**
|
|
* Verify a particular file against allowed types.
|
|
* @param \stored_file $file the file to check
|
|
* @return bool whether file type is allowed.
|
|
*/
|
|
public static function verify_file_type($file) {
|
|
$mime = $file->get_mimetype();
|
|
if (array_search($mime, self::allowed_file_types()) === false) {
|
|
debugging("File type not allowed " . $mime);
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the most recent x number of photos ($maxnumberphotos) that I uploaded.
|
|
* Or that exist in this course (even if someone else uploaded).
|
|
* Ignore any more than a certain time old. Used to populate my photo library.
|
|
* @param int $contextid the id for this context.
|
|
* @param int $maxnumberphotos how many to return maximum.
|
|
* @return array details of photos incl filename and details for path to make URL.
|
|
* @throws \dml_exception
|
|
*/
|
|
public static function get_photo_library_photos($contextid, $maxnumberphotos = 20) {
|
|
// Did not use (new \file_storage())->get_area_files() for this as it requires context id.
|
|
// We want to filter by user id instead.
|
|
global $DB, $USER;
|
|
$params['contextid'] = $contextid;
|
|
$params['userid'] = $USER->id;
|
|
$params['cutofftime'] = strtotime("-12 months");
|
|
$params['filesizecutoff'] = get_real_size("700K"); // Don't want to try to display really large draft files in library.
|
|
$fileapiparams = self::file_api_params();
|
|
$params['component'] = $fileapiparams['component'];
|
|
$params['filearea'] = $fileapiparams['filearea'];
|
|
$params['filepath'] = $fileapiparams['filepath'];
|
|
|
|
$sql = "SELECT id, component, filearea, contextid, itemid, filepath, filename, filesize, mimetype
|
|
FROM {files}
|
|
WHERE component = :component AND filearea = :filearea AND (contextid = :contextid OR userid = :userid)
|
|
AND filename != '.' AND filepath = :filepath
|
|
AND timemodified > :cutofftime
|
|
AND filesize < :filesizecutoff AND filesize > 0";
|
|
|
|
try {
|
|
$records = $DB->get_records_sql($sql, $params, 0, $maxnumberphotos);
|
|
} catch (\Exception $ex) {
|
|
debugging('Failed to run query to get files for library. ' . $ex->getMessage());
|
|
$records = [];
|
|
}
|
|
|
|
// If the teacher has nothing in their library, add a sample image.
|
|
if (count($records) == 0) {
|
|
$params['contextid'] = \context_system::instance()->id;
|
|
$sql = "SELECT id, component, filearea, contextid, itemid, filepath, filename, filesize, mimetype
|
|
FROM {files}
|
|
WHERE component = :component AND filearea = :filearea AND contextid = :contextid
|
|
AND filename = 'sample_image.jpg'
|
|
AND filepath = :filepath
|
|
AND filesize > 0";
|
|
$records = $DB->get_records_sql($sql, $params, 0, 1);
|
|
}
|
|
|
|
// Reduce to a set (ignore items with same filename/type and *roughly* same size as already existing).
|
|
$set = [];
|
|
$filesizetolerance = 2000; // If file size is within 2kb of another file, we treat that as same size.
|
|
|
|
foreach ($records as $record) {
|
|
$setkey = $record->filename . '|' . $record->mimetype;
|
|
if (!isset($set[$setkey]) || abs($set[$setkey]->filesize - $record->filesize) > $filesizetolerance) {
|
|
// Seems like we don't already have this file in the set - don't have to be precise here given purpose.
|
|
unset($record->mimetype); // Don't need to keep this.
|
|
$set[$setkey] = $record;
|
|
}
|
|
}
|
|
return array_values($set);
|
|
}
|
|
|
|
/**
|
|
* When we store a new tile photo as a file, the config should we use for the Moodle File API.
|
|
* @return array the config data.
|
|
*/
|
|
public static function file_api_params() {
|
|
return array(
|
|
'component' => 'format_tiles',
|
|
'filearea' => 'tilephoto',
|
|
'filepath' => '/tilephoto/',
|
|
'tempfilearea' => 'temptilephoto'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The maximum width of photos that we want to save (somewhat larger than tile size).
|
|
* @return int
|
|
*/
|
|
public static function get_max_image_width() {
|
|
return 360;
|
|
}
|
|
|
|
/**
|
|
* The sample image file in the database for this Moodle instance.
|
|
* There is only one and it is shown to teacher as a sample if their library is empty.
|
|
* @return bool|\stored_file
|
|
* @throws \dml_exception
|
|
*/
|
|
public static function get_sample_image_file() {
|
|
return self::get_file_from_ids(\context_system::instance()->id, 0, 'sample_image.jpg');
|
|
}
|
|
}
|
|
|
|
|