diff --git a/files/converter/googledrive/classes/converter.php b/files/converter/googledrive/classes/converter.php new file mode 100644 index 00000000000..13c3605f4b1 --- /dev/null +++ b/files/converter/googledrive/classes/converter.php @@ -0,0 +1,265 @@ +. + +/** + * Class for converting files between different file formats using google drive. + * + * @package fileconverter_googledrive + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace fileconverter_googledrive; + +defined('MOODLE_INTERNAL') || die(); + +use stored_file; +use moodle_exception; +use moodle_url; +use \core_files\conversion; + +/** + * Class for converting files between different formats using unoconv. + * + * @package fileconverter_googledrive + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class converter implements \core_files\converter_interface { + + private static $imports = [ + 'doc' => 'application/vnd.google-apps.document', + 'docx' => 'application/vnd.google-apps.document', + 'rtf' => 'application/vnd.google-apps.document', + 'xls' => 'application/vnd.google-apps.spreadsheet', + 'xlsx' => 'application/vnd.google-apps.spreadsheet', + 'ppt' => 'application/vnd.google-apps.presentation', + 'pptx' => 'application/vnd.google-apps.presentation' + ]; + + private static $exports = [ + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'rtf' => 'application/rtf', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pdf' => 'application/pdf', + 'txt' => 'text/plain' + ]; + + /** + * Convert a document to a new format and return a conversion object relating to the conversion in progress. + * + * @param conversion $conversion The file to be converted + * @param string $format The target file format + * @return this + */ + public function start_document_conversion(\core_files\conversion $conversion) { + global $CFG; + + $file = $conversion->get_sourcefile(); + $format = $conversion->get('targetformat'); + + $issuerid = get_config('fileconverter_googledrive', 'issuerid'); + if (empty($issuerid)) { + $conversion->set('status', conversion::STATUS_FAILED); + return $this; + } + + $issuer = \core\oauth2\api::get_issuer($issuerid); + if (empty($issuer)) { + $conversion->set('status', conversion::STATUS_FAILED); + return $this; + } + $client = \core\oauth2\api::get_system_oauth_client($issuer); + + $service = new \fileconverter_googledrive\rest($client); + + $contenthash = $file->get_contenthash(); + + $originalname = $file->get_filename(); + if (strpos($originalname, '.') === false) { + $conversion->set('status', conversion::STATUS_FAILED); + return $this; + } + $importextension = substr($originalname, strrpos($originalname, '.') + 1); + + $importformat = self::$imports[$importextension]; + $exportformat = self::$exports[$format]; + + $metadata = [ + 'name' => $contenthash, + 'mimeType' => $importformat + ]; + + $filecontent = $file->get_content(); + $filesize = $file->get_filesize(); + $filemimetype = $file->get_mimetype(); + + // Start resumable upload. + // First create empty file. + $params = [ + 'uploadType' => 'resumable', + 'fields' => 'id,name' + ]; + + $client->setHeader('X-Upload-Content-Type: ' . $filemimetype); + $client->setHeader('X-Upload-Content-Length: ' . $filesize); + + $headers = $service->call('upload', $params, json_encode($metadata)); + + $uploadurl; + // Google returns a location header with the location for the upload. + foreach ($headers as $header) { + if (strpos($header, 'Location:') === 0) { + $uploadurl = trim(substr($header, strpos($header, ':') + 1)); + } + } + + if (empty($uploadurl)) { + $conversion->set('status', conversion::STATUS_FAILED); + return $this; + } + + $params = [ + 'uploadurl' => $uploadurl + ]; + $result = $service->call('upload_content', $params, $filecontent, $filemimetype); + + $fileid = $result->id; + // Now export it again. + $params = ['mimeType' => $exportformat]; + $sourceurl = new moodle_url('https://www.googleapis.com/drive/v3/files/' . $fileid . '/export', $params); + $source = $sourceurl->out(false); + + $tmp = make_request_directory(); + $downloadto = $tmp . '/' . $fileid . '.' . $format; + + $options = ['filepath' => $downloadto, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5]; + $success = $client->download_one($source, null, $options); + + if ($success) { + $conversion->store_destfile_from_path($downloadto); + $conversion->set('status', conversion::STATUS_COMPLETE); + $conversion->update(); + } else { + $conversion->set('status', conversion::STATUS_FAILED); + } + // Cleanup. + $params = [ + 'fileid' => $fileid + ]; + $service->call('delete', $params); + + return $this; + } + + /** + * Generate and serve the test document. + * + * @return stored_file + */ + public function serve_test_document() { + global $CFG; + require_once($CFG->libdir . '/filelib.php'); + + $filerecord = [ + 'contextid' => \context_system::instance()->id, + 'component' => 'test', + 'filearea' => 'fileconverter_googledrive', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => 'conversion_test.docx' + ]; + + // Get the fixture doc file content and generate and stored_file object. + $fs = get_file_storage(); + $testdocx = $fs->get_file($filerecord['contextid'], $filerecord['component'], $filerecord['filearea'], + $filerecord['itemid'], $filerecord['filepath'], $filerecord['filename']); + + if (!$testdocx) { + $fixturefile = dirname(__DIR__) . '/tests/fixtures/source.docx'; + $testdocx = $fs->create_file_from_pathname($filerecord, $fixturefile); + } + + $conversion = new \core_files\conversion(0, (object) [ + 'targetformat' => 'pdf', + ]); + + $conversion->set_sourcefile($testdocx); + $conversion->create(); + + // Convert the doc file to pdf and send it direct to the browser. + $this->start_document_conversion($conversion); + + $testfile = $conversion->get_destfile(); + readfile_accel($testfile, 'application/pdf', true); + } + + /** + * Poll an existing conversion for status update. + * + * @param conversion $conversion The file to be converted + * @return $this; + */ + public function poll_conversion_status(conversion $conversion) { + return $this; + } + + /** + * Whether the plugin is configured and requirements are met. + * + * @return bool + */ + public static function are_requirements_met() { + $issuerid = get_config('fileconverter_googledrive', 'issuerid'); + if (empty($issuerid)) { + return false; + } + + $issuer = \core\oauth2\api::get_issuer($issuerid); + if (empty($issuer)) { + return false; + } + + if (!$issuer->is_system_account_connected()) { + return false; + } + + return true; + } + + /** + * Whether a file conversion can be completed using this converter. + * + * @param string $from The source type + * @param string $to The destination type + * @return bool + */ + public static function supports($from, $to) { + // This is not a one-liner because of php 5.6. + $imports = self::$imports; + $exports = self::$exports; + return isset($imports[$from]) && isset($exports[$to]); + } + + /** + * A list of the supported conversions. + * + * @return string + */ + public function get_supported_conversions() { + return implode(', ', ['rtf', 'doc', 'xls', 'docx', 'xlsx', 'ppt', 'pptx', 'pdf']); + } +} diff --git a/files/converter/googledrive/classes/rest.php b/files/converter/googledrive/classes/rest.php new file mode 100644 index 00000000000..2be69342d20 --- /dev/null +++ b/files/converter/googledrive/classes/rest.php @@ -0,0 +1,79 @@ +. + +/** + * Google Drive Rest API. + * + * @package fileconverter_googledrive + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace fileconverter_googledrive; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Google Drive Rest API. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class rest extends \core\oauth2\rest { + + /** + * Define the functions of the rest API. + * + * @return array Example: + * [ 'listFiles' => [ 'method' => 'get', 'endpoint' => 'http://...', 'args' => [ 'folder' => PARAM_STRING ] ] ] + */ + public function get_api_functions() { + return [ + 'upload' => [ + 'endpoint' => 'https://www.googleapis.com/upload/drive/v3/files', + 'method' => 'post', + 'args' => [ + 'uploadType' => PARAM_RAW, + 'fields' => PARAM_RAW + ], + 'response' => 'headers' + ], + 'upload_content' => [ + 'endpoint' => '{uploadurl}', + 'method' => 'put', + 'args' => [ + 'uploadurl' => PARAM_URL + ], + 'response' => 'json' + ], + 'create' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files', + 'method' => 'post', + 'args' => [ + 'fields' => PARAM_RAW + ], + 'response' => 'json' + ], + 'delete' => [ + 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', + 'method' => 'delete', + 'args' => [ + 'fileid' => PARAM_RAW + ], + 'response' => 'json' + ], + ]; + } +} diff --git a/files/converter/googledrive/lang/en/fileconverter_googledrive.php b/files/converter/googledrive/lang/en/fileconverter_googledrive.php new file mode 100644 index 00000000000..b796bba4f02 --- /dev/null +++ b/files/converter/googledrive/lang/en/fileconverter_googledrive.php @@ -0,0 +1,34 @@ +. + +/** + * Strings for plugin 'fileconverter_example' + * + * @package fileconverter_example + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['pluginname'] = 'Google Drive'; +$string['disabled'] = 'Disabled'; +$string['issuer'] = 'OAuth 2 Service'; +$string['issuer_help'] = 'The OAuth 2 service used to access google drive.'; +$string['test_converter'] = 'Test this converter is working properly.'; +$string['test_conversion'] = 'Test document conversion'; +$string['test_conversionready'] = 'This document converter is configured properly.'; +$string['test_conversionnotready'] = 'This document converter is not configured properly.'; diff --git a/files/converter/googledrive/lib.php b/files/converter/googledrive/lib.php new file mode 100644 index 00000000000..72b3a746119 --- /dev/null +++ b/files/converter/googledrive/lib.php @@ -0,0 +1,38 @@ +. + +/** + * This plugin is used to convert documents with google drive. + * + * @package fileconverter_googledrive + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Callback to get the required scopes for system account. + * + * @param \core\oauth2\issuer $issuer + * @return string + */ +function fileconverter_googledrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) { + if ($issuer->get('id') == get_config('fileconverter_googledrive', 'issuerid')) { + return 'https://www.googleapis.com/auth/drive'; + } + return ''; +} diff --git a/files/converter/googledrive/settings.php b/files/converter/googledrive/settings.php new file mode 100644 index 00000000000..bcedc58afad --- /dev/null +++ b/files/converter/googledrive/settings.php @@ -0,0 +1,45 @@ +. + +/** + * Link to the OAuth 2 service we will use. + * + * @package fileconverter_googledrive + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + $options = []; + $issuers = \core\oauth2\api::get_all_issuers(); + + $options[''] = get_string('disabled', 'fileconverter_googledrive'); + foreach ($issuers as $issuer) { + $options[$issuer->get('id')] = s($issuer->get('name')); + } + + $settings->add(new admin_setting_configselect('fileconverter_googledrive/issuerid', + get_string('issuer', 'fileconverter_googledrive'), + get_string('issuer_help', 'fileconverter_googledrive'), + '', + $options)); + + $url = new moodle_url('/files/converter/googledrive/test.php'); + $link = html_writer::link($url, get_string('test_converter', 'fileconverter_googledrive')); + $settings->add(new admin_setting_heading('test_converter', '', $link)); +} diff --git a/files/converter/googledrive/test.php b/files/converter/googledrive/test.php new file mode 100644 index 00000000000..426d41ab422 --- /dev/null +++ b/files/converter/googledrive/test.php @@ -0,0 +1,67 @@ +. + +/** + * Test that unoconv is configured correctly + * + * @package fileconverter_unoconv + * @copyright 2017 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +require(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/filelib.php'); + +$sendpdf = optional_param('sendpdf', 0, PARAM_BOOL); + +$PAGE->set_url(new moodle_url('/files/converter/googledrive/test.php')); +$PAGE->set_context(context_system::instance()); + +require_login(); +require_capability('moodle/site:config', context_system::instance()); + +$strheading = get_string('test_conversion', 'fileconverter_googledrive'); +$PAGE->navbar->add(get_string('administrationsite')); +$PAGE->navbar->add(get_string('plugins', 'admin')); +$PAGE->navbar->add(get_string('pluginname', 'fileconverter_googledrive'), + new moodle_url('/admin/settings.php', array('section' => 'fileconvertergoogledrive'))); +$PAGE->navbar->add($strheading); +$PAGE->set_heading($strheading); +$PAGE->set_title($strheading); + +$converter = new \fileconverter_googledrive\converter(); + +if ($sendpdf) { + require_sesskey(); + + $converter->serve_test_document(); + die(); +} + +$result = $converter->are_requirements_met(); +if ($result) { + $msg = $OUTPUT->notification(get_string('test_conversionready', 'fileconverter_googledrive'), 'success'); + $pdflink = new moodle_url($PAGE->url, array('sendpdf' => 1, 'sesskey' => sesskey())); + $msg .= html_writer::link($pdflink, get_string('test_conversion', 'fileconverter_googledrive')); + $msg .= html_writer::empty_tag('br'); +} else { + $msg = $OUTPUT->notification(get_string('test_conversionnotready', 'fileconverter_googledrive'), 'warning'); +} +$returl = new moodle_url('/admin/settings.php', array('section' => 'fileconvertergoogledrive')); +$msg .= $OUTPUT->continue_button($returl); + +echo $OUTPUT->header(); +echo $OUTPUT->box($msg, 'generalbox'); +echo $OUTPUT->footer(); diff --git a/files/converter/googledrive/tests/fixtures/source.docx b/files/converter/googledrive/tests/fixtures/source.docx new file mode 100644 index 00000000000..286c58a8926 Binary files /dev/null and b/files/converter/googledrive/tests/fixtures/source.docx differ diff --git a/files/converter/googledrive/version.php b/files/converter/googledrive/version.php new file mode 100644 index 00000000000..78383902221 --- /dev/null +++ b/files/converter/googledrive/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details + * + * @package fileconverter_googledrive + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2017020600; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2016112900; // Requires this Moodle version +$plugin->component = 'fileconverter_googledrive'; // Full name of the plugin (used for diagnostics). diff --git a/lib/classes/oauth2/rest.php b/lib/classes/oauth2/rest.php index b4a78732999..ca48a3590e1 100644 --- a/lib/classes/oauth2/rest.php +++ b/lib/classes/oauth2/rest.php @@ -39,7 +39,7 @@ require_once($CFG->libdir . '/filelib.php'); abstract class rest { /** @var curl $curl */ - private $curl; + protected $curl; /** * Constructor. @@ -66,7 +66,7 @@ abstract class rest { * @param string $rawpost Optional param to include in the body of a post. * @return string|object */ - public function call($functionname, $functionargs, $rawpost = false) { + public function call($functionname, $functionargs, $rawpost = false, $contenttype = false) { $functions = $this->get_api_functions(); $supportedmethods = [ 'get', 'put', 'post', 'patch', 'head', 'delete' ]; if (empty($functions[$functionname])) { @@ -106,7 +106,11 @@ abstract class rest { $callargs = $rawpost; } - $this->curl->setHeader('Content-type: application/json'); + if (empty($contenttype)) { + $this->curl->setHeader('Content-type: application/json'); + } else { + $this->curl->setHeader('Content-type: ' . $contenttype); + } $response = $this->curl->$method($endpoint, $callargs); if ($this->curl->errno == 0) { @@ -117,6 +121,8 @@ abstract class rest { throw new rest_exception($json->error->code . ': ' . $json->error->message); } return $json; + } else if ($responsetype == 'headers') { + $response = $this->curl->get_raw_response(); } return $response; } else { diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 04a010541b3..22da707d965 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1757,7 +1757,7 @@ class core_plugin_manager { ), 'fileconverter' => array( - 'unoconv' + 'unoconv', 'googledrive' ), 'editor' => array( diff --git a/lib/filelib.php b/lib/filelib.php index 0c314f547ba..34434a0a90b 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -3603,20 +3603,31 @@ class curl { * @return bool */ public function put($url, $params = array(), $options = array()) { - $file = $params['file']; - if (!is_file($file)) { - return null; - } - $fp = fopen($file, 'r'); - $size = filesize($file); - $options['CURLOPT_PUT'] = 1; - $options['CURLOPT_INFILESIZE'] = $size; - $options['CURLOPT_INFILE'] = $fp; - if (!isset($this->options['CURLOPT_USERPWD'])) { - $this->setopt(array('CURLOPT_USERPWD'=>'anonymous: noreply@moodle.org')); + $file = ''; + $fp = false; + if (isset($params['file'])) { + $file = $params['file']; + if(is_file($file)) { + $fp = fopen($file, 'r'); + $size = filesize($file); + $options['CURLOPT_PUT'] = 1; + $options['CURLOPT_INFILESIZE'] = $size; + $options['CURLOPT_INFILE'] = $fp; + } else { + return null; + } + if (!isset($this->options['CURLOPT_USERPWD'])) { + $this->setopt(array('CURLOPT_USERPWD'=>'anonymous: noreply@moodle.org')); + } + } else { + $options['CURLOPT_CUSTOMREQUEST'] = 'PUT'; + $options['CURLOPT_POSTFIELDS'] = $params; } + $ret = $this->request($url, $options); - fclose($fp); + if ($fp !== false) { + fclose($fp); + } return $ret; }