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.

769 lines
30 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/>.
/**
* Format tiles external API
*
* @package format_tiles
* @copyright 2018 David Watson {@link http://evolutioncode.uk}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use format_tiles\tile_photo;
defined('MOODLE_INTERNAL') || die;
global $CFG;
require_once("$CFG->libdir/externallib.php");
require_once($CFG->dirroot . '/course/format/tiles/locallib.php');
/**
* Format tiles external functions
*
* @package format_tiles
* @category external
* @copyright 2018 David Watson {@link http://evolutioncode.uk}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.3
*/
class format_tiles_external extends external_api
{
/**
* Teacher is changing the icon for a course section or whole course using AJAX
* @param Integer $courseid the id of this course
* @param Integer $sectionid the number of the section in this course - zero if whole course
* @param String $filename the icon filename or photo filename for this tile.
* @param string $imagetype whether it's a tile icon or a background photo.
* @param int $sourcecontextid the context id of the source photo or icon.
* @param int $sourceitemid the item id of the course photo or icon.
* @return [] status and image URL if applicable.
* @throws dml_exception
* @throws invalid_parameter_exception
* @throws moodle_exception
* @throws required_capability_exception
* @throws restricted_context_exception
*/
public static function set_image(
$courseid, $sectionid, $filename, $imagetype = 'tileicon', $sourcecontextid = 0, $sourceitemid = 0
) {
global $DB;
$data = self::validate_parameters(self::set_image_parameters(),
array(
'courseid' => $courseid,
'sectionid' => $sectionid,
'image' => $filename,
'sourcecontextid' => $sourcecontextid,
'sourceitemid' => $sourceitemid,
'imagetype' => $imagetype
)
);
// Section id of zero means we are changing the course icon. Otherwise check sec id is valid.
if ($data['sectionid'] !== 0 && $DB->get_record('course_sections',
array('course' => $data['courseid'], 'id' => $data['sectionid'])) === false) {
throw new invalid_parameter_exception('Invalid course and section id combination');
}
$context = context_course::instance($data['courseid']);
self::validate_context($context);
require_capability('moodle/course:viewhiddenactivities', $context); // This allows non-editing teachers for the course.
switch ($data['imagetype']) {
case 'tileicon':
$result = self::set_tile_icon($data);
break;
case 'tilephoto':
if (!get_config('format_tiles', 'allowphototiles')) {
throw new invalid_parameter_exception("Photo tiles are disabled by site admin");
}
$result = self::set_tile_photo($data);
break;
case 'draftfile':
$result = self::set_tile_photo_from_draftfile($data);
break;
default:
throw new invalid_parameter_exception('Image type is invalid ' . $data['imagetype']);
}
return $result;
}
/**
* Given a draft file uploaded by user, save top this plugin's file area.
* @param [] $data
* @return array
* @throws dml_exception
* @throws file_exception
* @throws invalid_parameter_exception
* @throws moodle_exception
* @throws required_capability_exception
* @throws stored_file_creation_exception
*/
private static function set_tile_photo_from_draftfile($data) {
if (!$data['sourcecontextid'] || !$data['sourceitemid']) {
throw new invalid_parameter_exception("Invalid source context id or source item id");
}
$tilephoto = new tile_photo($data['courseid'], $data['sectionid']);
$fs = get_file_storage();
$sourcefile = $fs->get_file(
$data['sourcecontextid'],
'user',
'draft',
$data['sourceitemid'],
'/',
$data['image']
);
$newfile = $tilephoto->set_file_from_stored_file($sourcefile, $data['image']);
if ($newfile) {
return array(
'status' => true,
'imageurl' => $tilephoto->get_image_url()
);
} else {
return array(
'status' => false,
'imageurl' => ''
);
}
}
/**
* Given the data describing the photo we want and the tile to apply it to, set the tile to use that photo.
* @param [] $data
* @return array
* @throws coding_exception
* @throws dml_exception
* @throws file_exception
* @throws invalid_parameter_exception
* @throws moodle_exception
* @throws required_capability_exception
* @throws stored_file_creation_exception
*/
private static function set_tile_photo($data) {
$sourcecontext = context::instance_by_id($data['sourcecontextid']);
$issettingsampleimage =
$sourcecontext->contextlevel == CONTEXT_SYSTEM && $data['sourceitemid'] == 0 & $data['image'] == 'sample_image.jpg';
if (!$data['sourcecontextid'] || (!$data['sourceitemid'] && !$issettingsampleimage)) {
throw new invalid_parameter_exception("Invalid source context id or source item id");
}
if ($sourcecontext->contextlevel !== CONTEXT_COURSE && !$issettingsampleimage) {
throw new InvalidArgumentException("Invalid context level");
}
if ($data['sourcecontextid'] &&!$issettingsampleimage) {
// Arguably we don't need to do this as the only files the user will see are those they posted themselves.
// This is thanks to the database query which generates the files list. So they could see them once.
require_capability('moodle/course:viewhiddenactivities', $sourcecontext);
}
$courseid = $sourcecontext->instanceid;
if ($issettingsampleimage) {
$sourcefile = tile_photo::get_sample_image_file();
} else {
$sourcephoto = new tile_photo($courseid, $data['sourceitemid']);
$sourcefile = $sourcephoto->get_file();
}
$tilephoto = new tile_photo($data['courseid'], $data['sectionid']);
$file = $tilephoto->set_file_from_stored_file($sourcefile, $data['image']);
if ($file) {
return array(
'status' => true,
'imageurl' => $tilephoto->get_image_url()
);
} else {
return array(
'status' => false,
'imageurl' => ''
);
}
}
/**
* Given the data describing the icon we want and the tile to apply it to, set the tile to use that icon
* @param [] $data
* @return array
* @throws coding_exception
* @throws dml_exception
* @throws invalid_parameter_exception
*/
private static function set_tile_icon($data) {
global $DB;
$availableicons = (new \format_tiles\icon_set)->available_tile_icons($data['courseid']);
if (!isset($availableicons[$data['image']])) {
throw new invalid_parameter_exception('Icon is invalid');
}
if ($data['sectionid'] === 0) {
$optionname = 'defaulttileicon'; // All default icon for whole course.
} else {
$optionname = 'tileicon'; // Icon for just this tile.
}
$existingicon = $DB->get_record(
'course_format_options',
['format' => 'tiles', 'name' => $optionname, 'courseid' => $data['courseid'], 'sectionid' => $data['sectionid']]
);
if (!isset($existingicon->value)) {
// No icon is presently stored for this so we need to insert new record.
$record = new stdClass();
$record->format = 'tiles';
$record->courseid = $data['courseid'];
$record->sectionid = $data['sectionid'];
$record->name = $optionname;
$record->value = $data['image'];
$result = $DB->insert_record('course_format_options', $record);
} else if ($data['sectionid'] != 0) {
// We are dealing with a tile icon for one particular section, so check if user has picked the course default.
$defaulticonthiscourse = $DB->get_record(
'course_format_options',
['format' => 'tiles', 'name' => 'defaulttileicon', 'courseid' => $data['courseid'], 'sectionid' => 0]
)->value;
if ($data['image'] == $defaulticonthiscourse) {
// Using default icon for a tile do don't store anything in database = default.
$result = $DB->delete_records(
'course_format_options',
['format' => 'tiles', 'name' => 'tileicon', 'courseid' => $data['courseid'], 'sectionid' => $data['sectionid']]
);
} else {
// User has not picked default and there is an existing record so update it.
$existingicon->value = $data['image'];
$result = $DB->update_record('course_format_options', $existingicon);
}
} else {
// Updating existing course icon record.
$existingicon->value = $data['image'];
$result = $DB->update_record('course_format_options', $existingicon);
}
if ($data['sectionid'] !== 0) {
// If there is a tile photo attached to this tile, clear it.
$tilephoto = new tile_photo($data['courseid'], $data['sectionid']);
$tilephoto->clear();
}
return array(
'status' => $result ? true : false,
'imageurl' => ''
);
}
/**
* Returns description of get_instance_info() parameters.
*
* @return external_function_parameters
*/
public static function set_image_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course id whose icon/image we are setting'),
'sectionid' => new external_value(
PARAM_INT,
'Section id whose icon/imasge we are setting (zero means whole course not just one section)'
),
'image' => new external_value(PARAM_RAW, 'File name for the image picked'),
'imagetype' => new external_value(PARAM_RAW, 'Image type for image picked (tileicon, tilephoto, draftfile)'),
'sourcecontextid' => new external_value(
PARAM_INT, 'File table context id for the photo file picked (0 if unused)', VALUE_DEFAULT, 0
),
'sourceitemid' => new external_value(
PARAM_INT, 'File table item id for the photo file picked (0 if unused)', VALUE_DEFAULT, 0
)
)
);
}
/**
* Returns description of method result value
* @return external_description
*/
public static function set_image_returns() {
return new external_single_structure(array(
'status' => new external_value(PARAM_BOOL, 'Whether the image was set'),
'imageurl' => new external_value(PARAM_RAW, 'Image URL if background photo set (not used for icons)'),
));
}
/**
* Get the HTML for a single section page for a course
* (i.e. the list of activities and resources comprising the contents of a tile)
* Intended to be called from AJAX so that the result can be added to the multi
* tiles page by JS
*
* The method returns the HTML rather than the underlying course data to save making
* another round trip to the server to render the HTML from the data, via the mustache
* template. This would have been another way of doing it, and would be easy to achieve
* by calling the template from JS.
*
* @param int $courseid
* @param int $sectionid we want to display
* @param boolean $setjsusedsession whether to set the session jsenabled flag to true
* @return array of warnings and status result
* @since Moodle 3.0
* @throws moodle_exception
*/
public static function get_single_section_page_html($courseid, $sectionid, $setjsusedsession = false) {
global $PAGE, $SESSION;
$params = self::validate_parameters(
self::get_single_section_page_html_parameters(),
array(
'courseid' => $courseid,
'sectionid' => $sectionid,
'setjsusedsession' => $setjsusedsession
)
);
// Request and permission validation.
// Ensure user has access to course context.
// validate_context() below ends up calling require_login($courseid).
$context = context_course::instance($params['courseid']);
self::validate_context($context);
$course = get_course($params['courseid']);
$renderer = $PAGE->get_renderer('format_tiles');
$templateable = new \format_tiles\output\course_output($course, true, $params['sectionid']);
$data = $templateable->export_for_template($renderer);
$result = array(
'html' => $renderer->render_from_template('format_tiles/single_section', $data)
);
// This session var is used later, when user revisits main course page, or a single section, for a course using this format.
// If set to true, the page can safely be rendered from PHP in the javascript friendly format.
// (A <noscript> box will be displayed only to users who have JS disabled with a link to switch to non JS format).
if ($params['setjsusedsession']) {
$SESSION->format_tiles_jssuccessfullyused = 1;
}
return $result;
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.0
*/
public static function get_single_section_page_html_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course id'),
'sectionid' => new external_value(PARAM_INT, 'Section id'),
'setjsusedsession' => new external_value(
PARAM_BOOL,
'Whether to set the session flag for JS successfully used',
VALUE_DEFAULT,
0,
true
)
)
);
}
/**
*
* Returns description of method result value
*
* @return external_description
* @since Moodle 3.0
*/
public static function get_single_section_page_html_returns () {
return new external_single_structure(
array(
'html' => new external_value(PARAM_RAW, 'HTML for the single section (tile contents)')
)
);
}
/**
* Get the HTML for a single page for display in a modal window
* @param int $courseid
* @param int $cmid we want to display
* @return array of warnings and status result
* @since Moodle 3.0
* @throws moodle_exception
*/
public static function get_mod_page_html($courseid, $cmid) {
global $DB, $PAGE;
$params = self::validate_parameters(
self::get_mod_page_html_parameters(),
array('courseid' => $courseid, 'cmid' => $cmid)
);
// Request and permission validation.
$modcontext = context_module::instance($params['cmid']);
self::validate_context($modcontext);
$result = array('status' => false, 'warnings' => [], 'html' => '');
$mod = get_fast_modinfo($params['courseid'])->get_cm($params['cmid']);
require_capability('mod/' . $mod->modname . ':view', $modcontext);
if ($mod && $mod->available) {
if (array_search($mod->modname, explode(",", get_config('format_tiles', 'modalmodules'))) === false) {
throw new invalid_parameter_exception('Not allowed to call this mod type - disabled by site admin');
}
if ($mod->modname == 'page') {
// Record from the page table.
$record = $DB->get_record($mod->modname, array('id' => $mod->instance), 'intro, content, revision, contentformat');
$renderer = $PAGE->get_renderer('format_tiles');
$content = $renderer->format_cm_content_text($mod, $record, $modcontext);
$result['status'] = true;
$result['html'] = $content;
return $result;
} else {
throw new invalid_parameter_exception('Only page modules allowed through this service');
}
} else {
$result['status'] = false;
$result['html'] = '';
$result['warnings'][] = 'Course module is not available';
}
return $result;
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.0
*/
public static function get_mod_page_html_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course id'),
'cmid' => new external_value(PARAM_INT, 'Course module id'),
)
);
}
/**
*
* Returns description of method result value
*
* @return external_description
* @since Moodle 3.0
*/
public static function get_mod_page_html_returns () {
return new external_single_structure(
array(
'html' => new external_value(PARAM_RAW, 'HTML for the course module')
)
);
}
/**
* Log that fact that the user clicked a tile
* @param int $courseid
* @param int $sectionid we are viewing
* @return array of warnings and status result
* @since Moodle 3.0
* @throws moodle_exception
*/
public static function log_tile_click($courseid, $sectionid) {
$params = self::validate_parameters(
self::log_tile_click_parameters(),
array('courseid' => $courseid, 'sectionid' => $sectionid)
);
// Request and permission validation.
$coursecontext = context_course::instance($params['courseid']);
self::validate_context($coursecontext);
course_view(context_course::instance($courseid), $sectionid);
$result = array('status' => true, 'warnings' => []);
return $result;
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.0
*/
public static function log_tile_click_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course id'),
'sectionid' => new external_value(PARAM_INT, 'Section id viewed', VALUE_DEFAULT, 0, true),
)
);
}
/**
*
* Returns description of method result value
*
* @return external_description
* @since Moodle 3.0
*/
public static function log_tile_click_returns () {
return new external_single_structure(
array(
'status' => new external_value(PARAM_BOOL, 'status: true if success')
)
);
}
/**
* Simulate the resource/view.php and page/view.php etc logging when caleld from AJAX
*
* This is a re-implementation of the core service only required because the core
* version is not callable from AJAX
* @see mod_resource_external::log_resource_view() for example
* @param int $courseid the course id where the module is
* @param int $cmid the resource module instance id
* @return array of warnings and status result
* @since Moodle 3.0
* @throws moodle_exception
*/
public static function log_mod_view($courseid, $cmid) {
global $DB, $USER;
$params = self::validate_parameters(
self::log_mod_view_parameters(),
array(
'courseid' => $courseid,
'cmid' => $cmid
)
);
list($course, $cm) = get_course_and_cm_from_cmid($params['cmid'], '', $params['courseid']);
// Request and permission validation.
$context = context_module::instance($cm->id);
self::validate_context($context);
require_capability('mod/' . $cm->modname . ':view', $context);
$allowedmodalmodules = format_tiles_allowed_modal_modules();
if (array_search($cm->modname, $allowedmodalmodules['modules']) === false
&& count($allowedmodalmodules['resources']) == 0) {
throw new invalid_parameter_exception(
'Not allowed to log views of this mod type - disabled by site admin or incorrect device type.'
. ' If you are testing this may be because you have not refreshed since switching device types');
}
$modobject = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
// Trigger course_module_viewed event.
switch ($cm->modname) {
case 'page':
page_view($modobject, $course, $cm, $context);
break;
case 'resource':
resource_view($modobject, $course, $cm, $context);
break;
case 'url':
url_view($modobject, $course, $cm, $context);
break;
default:
throw new invalid_parameter_exception('No logging method provided for type |' . $cm->modname . '|');
// TODO add more to these if more modules added.
}
// If this item is using automatic completion, mark the item as complete.
$completion = new completion_info($course);
if ($completion->is_enabled() && $cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
$completion->update_state($cm, COMPLETION_COMPLETE, $USER->id);
}
$result = array();
$result['status'] = true;
return $result;
}
/**
* Simulate the resource/view.php web interface page: trigger events, completion, etc...
*
* This is a re-implementation of the core service, only required because the core
* version is not callable from AJAX
* @see mod_resource_external::log_resource_view_parameters()
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.0
*/
public static function log_mod_view_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'course id'),
'cmid' => new external_value(PARAM_INT, 'course module id')
)
);
}
/**
*
* Returns description of method result value
*
* This is a re-implementation of the core service only required because the core
* version is not callable from AJAX
* @see mod_resource_external::log_resource_view_returns()
* @return external_description
* @since Moodle 3.0
*/
public static function log_mod_view_returns () {
return new external_single_structure(
array(
'status' => new external_value(PARAM_BOOL, 'status: true if success')
)
);
}
/**
* Get the available icon set
* @param int $courseid
* @return array of warnings and status result
* @since Moodle 3.3
* @throws moodle_exception
*/
public static function get_icon_set($courseid) {
$params = self::validate_parameters(
self::get_icon_set_parameters(),
array('courseid' => $courseid)
);
// Request and permission validation.
// Note course id could be zero if creating new course.
if ($params['courseid'] != 0) {
$context = context_course::instance($params['courseid']);
} else {
$context = context_coursecat::instance(optional_param('category', 0, PARAM_INT));
}
self::validate_context($context);
if (!has_capability('moodle/course:update', $context) && !has_capability('moodle/course:create', $context)) {
if (!has_capability('moodle/course:update', $context)) {
throw new required_capability_exception(
$context,
'moodle/course:update',
"nopermissions",
""
);
} else {
throw new required_capability_exception(
$context,
'moodle/course:create',
"nopermissions",
""
);
}
};
$data = array(
'status' => true,
'warnings' => [],
'icons' => json_encode((new \format_tiles\icon_set)->available_tile_icons($courseid)),
'photos' => ''
);
if (get_config('format_tiles', 'allowphototiles')) {
$data['photos'] = json_encode(tile_photo::get_photo_library_photos($context->id));
}
return $data;
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.3
*/
public static function get_icon_set_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course id'),
)
);
}
/**
*
* Returns description of method result value
*
* @return external_description
* @since Moodle 3.3
*/
public static function get_icon_set_returns () {
return new external_single_structure(
array(
'icons' => new external_value(PARAM_RAW, 'Icon set available for use on tile icons (JSON array)'),
'photos' => new external_value(PARAM_RAW, 'Recent photos set for teacher photo library (JSON array)'),
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'warnings' => new external_warnings()
)
);
}
/**
* Set the result of the JS calculation of the optimal width of the main tiles window for a course.
* This has to be by course as they have different numbers of tiles.
* We can then use this to render the page from PHP at the correct width initially next time.
* @param int $courseid the course id we are in
* @param int $width the JS calculated width
* @see format_tiles_width_template_data() for where this is used.
* @return array of warnings and status result
* @since Moodle 3.0
* @throws moodle_exception
*/
public static function set_session_width($courseid, $width) {
global $SESSION;
$params = self::validate_parameters(
self::set_session_width_parameters(),
array('courseid' => $courseid, 'width' => $width)
);
// Request and permission validation - validate_context() includes require_login() check.
$coursecontext = context_course::instance($params['courseid']);
self::validate_context($coursecontext);
$sessionvar = 'format_tiles_width_' . $params['courseid'];
if (!get_config('format_tiles', 'fittilestowidth')) {
throw new invalid_parameter_exception("Setting tiles width is disabled by site admin");
}
if ($params['width'] < 300 || $params['width'] > 3000) {
// Value passed is out of bounds, so unset as something has gone wrong.
unset($SESSION->$sessionvar);
return array('status' => false, 'warnings' => ['Session width out bounds']);
}
$SESSION->$sessionvar = $params['width'];
return array('status' => true, 'warnings' => []);
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.0
*/
public static function set_session_width_parameters() {
return new external_function_parameters(
array(
'courseid' => new external_value(PARAM_INT, 'Course id'),
'width' => new external_value(
PARAM_INT,
'The JS calculated width optimal width for tiles window (used to render from PHP next time)',
VALUE_DEFAULT,
0,
true
),
)
);
}
/**
*
* Returns description of method result value
*
* @return external_description
* @since Moodle 3.0
*/
public static function set_session_width_returns () {
return new external_single_structure(
array(
'status' => new external_value(PARAM_BOOL, 'status: true if success')
)
);
}
}