Commit 5702a828 authored by Dan Poltawski's avatar Dan Poltawski
Browse files

Merge branch 'MDL-52954-master' of git://github.com/damyon/moodle

parents 9bfb76c4 6853cd5e
......@@ -12,6 +12,7 @@ $temp->add(new admin_setting_configexecutable('pathtodu', new lang_string('patht
$temp->add(new admin_setting_configexecutable('aspellpath', new lang_string('aspellpath', 'admin'), new lang_string('edhelpaspellpath'), ''));
$temp->add(new admin_setting_configexecutable('pathtodot', new lang_string('pathtodot', 'admin'), new lang_string('pathtodot_help', 'admin'), ''));
$temp->add(new admin_setting_configexecutable('pathtogs', new lang_string('pathtogs', 'admin'), new lang_string('pathtogs_help', 'admin'), '/usr/bin/gs'));
$temp->add(new admin_setting_configexecutable('pathtounoconv', new lang_string('pathtounoconv', 'admin'), new lang_string('pathtounoconv_help', 'admin'), '/usr/bin/unoconv'));
$ADMIN->add('server', $temp);
......
......@@ -840,6 +840,12 @@ $CFG->admin = 'admin';
// Note that, for now, this only used by the profiling features
// (Development->Profiling) built into Moodle.
// $CFG->pathtodot = '';
//
// Path to unoconv.
// Probably something like /usr/bin/unoconv. Used as a fallback to convert between document formats.
// Unoconv is used convert between file formats supported by LibreOffice.
// Use a recent version of unoconv ( >= 0.7 ), older versions have trouble running from a webserver.
// $CFG->pathtounoconv = '';
//=========================================================================
// ALL DONE! To continue installation, visit your main page with a browser
......
......@@ -647,21 +647,25 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$checked_s2 = $checked;
}
$radio = html_writer::tag('input', get_string('showmarkerdesc', 'gradingform_guide'), array('type' => 'radio',
$radio1 = html_writer::tag('input', get_string('showmarkerdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showmarkerdesc',
'value' => "true")+$checked1);
$radio .= html_writer::tag('input', get_string('hidemarkerdesc', 'gradingform_guide'), array('type' => 'radio',
$radio1 = html_writer::tag('label', $radio1);
$radio2 = html_writer::tag('input', get_string('hidemarkerdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showmarkerdesc',
'value' => "false")+$checked2);
$output .= html_writer::tag('div', $radio, array('class' => 'showmarkerdesc'));
$radio2 = html_writer::tag('label', $radio2);
$output .= html_writer::tag('div', $radio1 . $radio2, array('class' => 'showmarkerdesc'));
$radio = html_writer::tag('input', get_string('showstudentdesc', 'gradingform_guide'), array('type' => 'radio',
$radio1 = html_writer::tag('input', get_string('showstudentdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showstudentdesc',
'value' => "true")+$checked_s1);
$radio .= html_writer::tag('input', get_string('hidestudentdesc', 'gradingform_guide'), array('type' => 'radio',
$radio1 = html_writer::tag('label', $radio1);
$radio2 = html_writer::tag('input', get_string('hidestudentdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showstudentdesc',
'value' => "false")+$checked_s2);
$output .= html_writer::tag('div', $radio, array('class' => 'showstudentdesc'));
$radio2 = html_writer::tag('label', $radio2);
$output .= html_writer::tag('div', $radio1 . $radio2, array('class' => 'showstudentdesc'));
}
return $output;
}
......@@ -775,4 +779,4 @@ class gradingform_guide_renderer extends plugin_renderer_base {
return $html;
}
}
\ No newline at end of file
}
......@@ -790,6 +790,8 @@ $string['pathtopgdumpinvalid'] = 'Invalid path to pg_dump - either wrong path or
$string['pathtopsql'] = 'Path to psql';
$string['pathtopsqldesc'] = 'This is only necessary to enter if you have more than one psql on your system (for example if you have more than one version of postgresql installed)';
$string['pathtopsqlinvalid'] = 'Invalid path to psql - either wrong path or not executable';
$string['pathtounoconv'] = 'Path to unoconv document converter';
$string['pathtounoconv_help'] = 'Path to unoconv document converter. This is an executable that is capable of converting between document formats supported by LibreOffice. This is optional, but if specified, Moodle will use it to automatically convert between document formats. This is used to support a wider range of input files for the assignment annotate PDF feature.';
$string['pcreunicodewarning'] = 'It is strongly recommended to use PCRE PHP extension that is compatible with Unicode characters.';
$string['perfdebug'] = 'Performance info';
$string['performance'] = 'Performance';
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -778,8 +778,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
};
// Trigger an ajax update after the text field value changes.
inputElement.on("input keypress", throttledHandler);
inputElement.on("input", throttledHandler);
var arrowElement = $(document.getElementById(state.downArrowId));
arrowElement.on("click", handler);
});
......
define(['jquery'], function($) {
/**
* Tooltip class.
*
* @param {String} selector The css selector for the node(s) to enhance with tooltips.
*/
var Tooltip = function(selector) {
// Tooltip code matches: http://www.w3.org/WAI/PF/aria-practices/#tooltip
this._regionSelector = selector;
// For each node matching the selector - find an aria-describedby attribute pointing to an role="tooltip" element.
$(this._regionSelector).each(function(index, element) {
var tooltipId = $(element).attr('aria-describedby');
if (tooltipId) {
var tooltipele = document.getElementById(tooltipId);
if (tooltipele) {
var correctRole = $(tooltipele).attr('role') == 'tooltip';
if (correctRole) {
$(tooltipele).hide();
// Ensure the trigger for the tooltip is keyboard focusable.
$(element).attr('tabindex', '0');
}
// Attach listeners.
$(element).on('focus', this._handleFocus.bind(this));
$(element).on('mouseover', this._handleMouseOver.bind(this));
$(element).on('mouseout', this._handleMouseOut.bind(this));
$(element).on('blur', this._handleBlur.bind(this));
$(element).on('keydown', this._handleKeyDown.bind(this));
}
}
}.bind(this));
};
/** @type {String} Selector for the page region containing the user navigation. */
Tooltip.prototype._regionSelector = null;
/**
* Find the tooltip referred to by this element and show it.
*
* @param {Event} e
*/
Tooltip.prototype._showTooltip = function(e) {
var triggerElement = $(e.target);
var tooltipId = triggerElement.attr('aria-describedby');
if (tooltipId) {
var tooltipele = $(document.getElementById(tooltipId));
tooltipele.show();
tooltipele.attr('aria-hidden', 'false');
if (!tooltipele.is('.tooltip')) {
// Change the markup to a bootstrap tooltip.
var inner = $('<div class="tooltip-inner"></div>');
inner.append(tooltipele.contents());
tooltipele.append(inner);
tooltipele.addClass('tooltip');
tooltipele.addClass('bottom');
tooltipele.append('<div class="tooltip-arrow"></div>');
}
var pos = triggerElement.offset();
pos.top += triggerElement.height() + 10;
$(tooltipele).offset(pos);
}
};
/**
* Find the tooltip referred to by this element and hide it.
*
* @param {Event} e
*/
Tooltip.prototype._hideTooltip = function(e) {
var triggerElement = $(e.target);
var tooltipId = triggerElement.attr('aria-describedby');
if (tooltipId) {
var tooltipele = document.getElementById(tooltipId);
$(tooltipele).hide();
$(tooltipele).attr('aria-hidden', 'true');
}
};
/**
* Listener for focus events.
* @param {Event} e
*/
Tooltip.prototype._handleFocus = function(e) {
this._showTooltip(e);
};
/**
* Listener for keydown events.
* @param {Event} e
*/
Tooltip.prototype._handleKeyDown = function(e) {
if (e.which == 27) {
this._hideTooltip(e);
}
};
/**
* Listener for mouseover events.
* @param {Event} e
*/
Tooltip.prototype._handleMouseOver = function(e) {
this._showTooltip(e);
};
/**
* Listener for mouseout events.
* @param {Event} e
*/
Tooltip.prototype._handleMouseOut = function(e) {
var triggerElement = $(e.target);
if (!triggerElement.is(":focus")) {
this._hideTooltip(e);
}
};
/**
* Listener for blur events.
* @param {Event} e
*/
Tooltip.prototype._handleBlur = function(e) {
this._hideTooltip(e);
};
return Tooltip;
});
......@@ -166,7 +166,7 @@ function behat_clean_init_config() {
'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
'proxybypass', 'theme', 'pathtogs', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
'altcacheconfigpath'
'altcacheconfigpath', 'pathtounoconv'
));
// Add extra allowed settings.
......
......@@ -428,6 +428,7 @@ $functions = array(
'classpath' => 'user/externallib.php',
'description' => 'Retrieve users information for a specified unique field - If you want to do a user search, use core_user_get_users()',
'type' => 'read',
'ajax' => true,
'capabilities'=> 'moodle/user:viewdetails, moodle/user:viewhiddendetails, moodle/course:useremail, moodle/user:update',
),
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -99,6 +99,14 @@ EditorAutosave.prototype = {
*/
autosaveInstance: null,
/**
* Autosave Timer.
*
* @property autosaveTimer
* @type object
*/
autosaveTimer: null,
/**
* Initialize the autosave process
*
......@@ -183,7 +191,7 @@ EditorAutosave.prototype = {
// Now setup the timer for periodic saves.
var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
Y.later(delay, this, this.saveDraft, false, true);
this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
// Now setup the listener for form submission.
form = this.textarea.ancestor('form');
......@@ -247,6 +255,12 @@ EditorAutosave.prototype = {
*/
saveDraft: function() {
var url, params;
if (!this.editor.getDOMNode()) {
// Stop autosaving if the editor was removed from the page.
this.autosaveTimer.cancel();
return;
}
// Only copy the text from the div to the textarea if the textarea is not currently visible.
if (!this.editor.get('hidden')) {
this.updateOriginal();
......
......@@ -53,6 +53,9 @@ class file_storage {
private $dirpermissions;
/** @var int Permissions for new files */
private $filepermissions;
/** @var array List of formats supported by unoconv */
private $unoconvformats;
/**
* Constructor - do not use directly use {@link get_file_storage()} call instead.
......@@ -156,6 +159,129 @@ class file_storage {
return $storedfile;
}
/**
* Get converted document.
*
* Get an alternate version of the specified document, if it is possible to convert.
*
* @param stored_file $file the file we want to preview
* @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
* @return stored_file|bool false if unable to create the conversion, stored file otherwise
*/
public function get_converted_document(stored_file $file, $format) {
$context = context_system::instance();
$path = '/' . $format . '/';
$conversion = $this->get_file($context->id, 'core', 'documentconversion', 0, $path, $file->get_contenthash());
if (!$conversion) {
$conversion = $this->create_converted_document($file, $format);
if (!$conversion) {
return false;
}
}
return $conversion;
}
/**
* Verify the format is supported.
*
* @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
* @return bool - True if the format is supported for input.
*/
protected function is_format_supported_by_unoconv($format) {
global $CFG;
if (!isset($this->unoconvformats)) {
// Ask unoconv for it's list of supported document formats.
$cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' --show';
$pipes = array();
$pipesspec = array(2 => array('pipe', 'w'));
$proc = proc_open($cmd, $pipesspec, $pipes);
$programoutput = stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($proc);
$matches = array();
preg_match_all('/\[\.(.*)\]/', $programoutput, $matches);
$this->unoconvformats = $matches[1];
$this->unoconvformats = array_unique($this->unoconvformats);
}
$sanitized = trim(core_text::strtolower($format));
return in_array($sanitized, $this->unoconvformats);
}
/**
* Perform a file format conversion on the specified document.
*
* @param stored_file $file the file we want to preview
* @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension.
* @return stored_file|bool false if unable to create the conversion, stored file otherwise
*/
protected function create_converted_document(stored_file $file, $format) {
global $CFG;
if (empty($CFG->pathtounoconv) || !is_executable(trim($CFG->pathtounoconv))) {
// No conversions are possible, sorry.
return false;
}
$fileextension = core_text::strtolower(pathinfo($file->get_filename(), PATHINFO_EXTENSION));
if (!self::is_format_supported_by_unoconv($fileextension)) {
return false;
}
if (!self::is_format_supported_by_unoconv($format)) {
return false;
}
// Copy the file to the local tmp dir.
$tmp = make_request_directory();
$localfilename = $file->get_filename();
// Safety.
$localfilename = clean_param($localfilename, PARAM_FILE);
$filename = $tmp . '/' . $localfilename;
$file->copy_content_to($filename);
$newtmpfile = pathinfo($filename, PATHINFO_FILENAME) . '.' . $format;
// Safety.
$newtmpfile = $tmp . '/' . clean_param($newtmpfile, PARAM_FILE);
$cmd = escapeshellcmd(trim($CFG->pathtounoconv)) . ' ' .
escapeshellarg('-f') . ' ' .
escapeshellarg($format) . ' ' .
escapeshellarg('-o') . ' ' .
escapeshellarg($newtmpfile) . ' ' .
escapeshellarg($filename);
$e = file_exists($filename);
$output = null;
$currentdir = getcwd();
chdir($tmp);
$result = exec($cmd, $output);
chdir($currentdir);
if (!file_exists($newtmpfile)) {
return false;
}
$context = context_system::instance();
$record = array(
'contextid' => $context->id,
'component' => 'core',
'filearea' => 'documentconversion',
'itemid' => 0,
'filepath' => '/' . $format . '/',
'filename' => $file->get_contenthash(),
);
return $this->create_file_from_pathname($record, $newtmpfile);
}
/**
* Returns an image file that represent the given stored file as a preview
*
......@@ -2282,6 +2408,26 @@ class file_storage {
$rs->close();
mtrace('done.');
// Remove orphaned converted files (that is files in the core documentconversion filearea without
// the existing original file).
mtrace('Deleting orphaned document conversion files... ', '');
cron_trace_time_and_memory();
$sql = "SELECT p.*
FROM {files} p
LEFT JOIN {files} o ON (p.filename = o.contenthash)
WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'documentconversion' AND p.itemid = 0
AND o.id IS NULL";
$syscontext = context_system::instance();
$rs = $DB->get_recordset_sql($sql, array($syscontext->id));
foreach ($rs as $orphan) {
$file = $this->get_file_instance($orphan);
if (!$file->is_directory()) {
$file->delete();
}
}
$rs->close();
mtrace('done.');
// remove trash pool files once a day
// if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
......
......@@ -131,6 +131,9 @@ abstract class moodleform {
/** @var array globals workaround */
protected $_customdata;
/** @var array submitted form data when using mforms with ajax */
protected $_ajaxformdata;
/** @var object definition_after_data executed flag */
protected $_definition_finalized = false;
......@@ -156,8 +159,10 @@ abstract class moodleform {
* it if you don't need to as the target attribute is deprecated in xhtml strict.
* @param mixed $attributes you can pass a string of html attributes here or an array.
* @param bool $editable
* @param array $ajaxformdata Forms submitted via ajax, must pass their data here, instead of relying on _GET and _POST.
*/
public function __construct($action=null, $customdata=null, $method='post', $target='', $attributes=null, $editable=true) {
public function __construct($action=null, $customdata=null, $method='post', $target='', $attributes=null, $editable=true,
$ajaxformdata=null) {
global $CFG, $FULLME;
// no standard mform in moodle should allow autocomplete with the exception of user signup
if (empty($attributes)) {
......@@ -170,6 +175,7 @@ abstract class moodleform {
}
}
if (empty($action)){
// do not rely on PAGE->url here because dev often do not setup $actualurl properly in admin_externalpage_setup()
$action = strip_querystring($FULLME);
......@@ -183,6 +189,7 @@ abstract class moodleform {
// Assign custom data first, so that get_form_identifier can use it.
$this->_customdata = $customdata;
$this->_formname = $this->get_form_identifier();
$this->_ajaxformdata = $ajaxformdata;
$this->_form = new MoodleQuickForm($this->_formname, $method, $action, $target, $attributes);
if (!$editable){
......@@ -272,7 +279,9 @@ abstract class moodleform {
*/
function _process_submission($method) {
$submission = array();
if ($method == 'post') {
if (!empty($this->_ajaxformdata)) {
$submission = $this->_ajaxformdata;
} else if ($method == 'post') {
if (!empty($_POST)) {
$submission = $_POST;
}
......
......@@ -185,7 +185,8 @@ $CFG->dboptions = isset($CFG->phpunit_dboptions) ? $CFG->phpunit_dboptions : $CF
$allowed = array('wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions',
'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass', // keep proxy settings from config.php
'altcacheconfigpath', 'pathtogs', 'pathtodu', 'aspellpath', 'pathtodot'
'altcacheconfigpath', 'pathtogs', 'pathtodu', 'aspellpath', 'pathtodot',
'pathtounoconv'
);
$productioncfg = (array)$CFG;
$CFG = new stdClass();
......
<!DOCTYPE html>
<html>
<body>
<h1>My First Heading</h1>
<p>My first paragraph.</p>
</body>
</html>
<?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/>.
/**
* Test unoconv functionality.
*
* @package core
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* A set of tests for some of the unoconv functionality within Moodle.
*
* @package core
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_unoconv_testcase extends advanced_testcase {
/** @var $testfile1 */
private $testfile1 = null;
/** @var $testfile2 */
private $testfile2 = null;
public function setUp() {
$this->fixturepath = __DIR__ . DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR;
$fs = get_file_storage();
$filerecord = array(
'contextid' => context_system::instance()->id,
'component' => 'test',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'test.html'
);
$teststring = file_get_contents($this->fixturepath . DIRECTORY_SEPARATOR . 'unoconv-source.html');
$this->testfile1 = $fs->create_file_from_string($filerecord, $teststring);
$filerecord = array(
'contextid' => context_system::instance()->id,
'component' => 'test',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => 'test.docx'
);
$teststring = file_get_contents($this->fixturepath . DIRECTORY_SEPARATOR . 'unoconv-source.docx');
$this->testfile2 = $fs->create_file_from_string($filerecord, $teststring);
$this->resetAfterTest();
}
public function test_generate_pdf() {
global $CFG;
if (empty($CFG->pathtounoconv) || !is_executable(trim($CFG->pathtounoconv))) {
// No conversions are possible, sorry.
return $this->markTestSkipped();
}
$fs = get_file_storage();
$result = $fs->get_converted_document($this->testfile1, 'pdf');
$this->assertNotFalse($result);
$this->assertSame($result->get_mimetype(), 'application/pdf');
$this->assertGreaterThan(0, $result->get_filesize());
$result = $fs->get_converted_document($this->testfile2, 'pdf');
$this->assertNotFalse($result);
$this->assertSame($result->get_mimetype(), 'application/pdf'