Merge branch 'MDL-69270-master' of git://github.com/ferranrecio/moodle
[moodle.git] / contentbank / classes / contenttype.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Content type manager class
19  *
20  * @package    core_contentbank
21  * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace core_contentbank;
27 use core\event\contentbank_content_created;
28 use core\event\contentbank_content_deleted;
29 use core\event\contentbank_content_viewed;
30 use stored_file;
31 use Exception;
32 use moodle_url;
34 /**
35  * Content type manager class
36  *
37  * @package    core_contentbank
38  * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 abstract class contenttype {
43     /** @var string Constant representing whether the plugin implements uploading feature */
44     const CAN_UPLOAD = 'upload';
46     /** @var string Constant representing whether the plugin implements edition feature */
47     const CAN_EDIT = 'edit';
49     /**
50      * @var string Constant representing whether the plugin implements download feature
51      * @since  Moodle 3.10
52      */
53     const CAN_DOWNLOAD = 'download';
55     /** @var \context This contenttype's context. **/
56     protected $context = null;
58     /**
59      * Content type constructor
60      *
61      * @param \context $context Optional context to check (default null)
62      */
63     public function __construct(\context $context = null) {
64         if (empty($context)) {
65             $context = \context_system::instance();
66         }
67         $this->context = $context;
68     }
70     /**
71      * Fills content_bank table with appropiate information.
72      *
73      * @throws dml_exception A DML specific exception is thrown for any creation error.
74      * @param \stdClass $record An optional content record compatible object (default null)
75      * @return content  Object with content bank information.
76      */
77     public function create_content(\stdClass $record = null): content {
78         global $USER, $DB;
80         $entry = new \stdClass();
81         $entry->contenttype = $this->get_contenttype_name();
82         $entry->contextid = $this->context->id;
83         $entry->name = $record->name ?? '';
84         $entry->usercreated = $record->usercreated ?? $USER->id;
85         $entry->timecreated = time();
86         $entry->usermodified = $entry->usercreated;
87         $entry->timemodified = $entry->timecreated;
88         $entry->configdata = $record->configdata ?? '';
89         $entry->instanceid = $record->instanceid ?? 0;
90         $entry->id = $DB->insert_record('contentbank_content', $entry);
91         $classname = '\\'.$entry->contenttype.'\\content';
92         $content = new $classname($entry);
93         // Trigger an event for creating the content.
94         $event = contentbank_content_created::create_from_record($content->get_content());
95         $event->trigger();
96         return $content;
97     }
99     /**
100      * Create a new content from an uploaded file.
101      *
102      * @throws file_exception If file operations fail
103      * @throws dml_exception if the content creation fails
104      * @param stored_file $file the uploaded file
105      * @param \stdClass|null $record an optional content record
106      * @return content  Object with content bank information.
107      */
108     public function upload_content(stored_file $file, \stdClass $record = null): content {
109         if (empty($record)) {
110             $record = new \stdClass();
111             $record->name = $file->get_filename();
112         }
113         $content = $this->create_content($record);
114         try {
115             $content->import_file($file);
116         } catch (Exception $e) {
117             $this->delete_content($content);
118             throw $e;
119         }
121         return $content;
122     }
124     /**
125      * Replace a content using an uploaded file.
126      *
127      * @throws file_exception If file operations fail
128      * @throws dml_exception if the content creation fails
129      * @param stored_file $file the uploaded file
130      * @param content $content the original content record
131      * @return content Object with the updated content bank information.
132      */
133     public function replace_content(stored_file $file, content $content): content {
134         $content->import_file($file);
135         $content->update_content();
136         return $content;
137     }
139     /**
140      * Delete this content from the content_bank.
141      * This method can be overwritten by the plugins if they need to delete specific information.
142      *
143      * @param  content $content The content to delete.
144      * @return boolean true if the content has been deleted; false otherwise.
145      */
146     public function delete_content(content $content): bool {
147         global $DB;
149         // Delete the file if it exists.
150         if ($file = $content->get_file()) {
151             $file->delete();
152         }
154         // Delete the contentbank DB entry.
155         $result = $DB->delete_records('contentbank_content', ['id' => $content->get_id()]);
156         if ($result) {
157             // Trigger an event for deleting this content.
158             $record = $content->get_content();
159             $event = contentbank_content_deleted::create([
160                 'objectid' => $content->get_id(),
161                 'relateduserid' => $record->usercreated,
162                 'context' => \context::instance_by_id($record->contextid),
163                 'other' => [
164                     'contenttype' => $content->get_content_type(),
165                     'name' => $content->get_name()
166                 ]
167             ]);
168             $event->add_record_snapshot('contentbank_content', $record);
169             $event->trigger();
170         }
171         return $result;
172     }
174     /**
175      * Rename this content from the content_bank.
176      * This method can be overwritten by the plugins if they need to change some other specific information.
177      *
178      * @param  content $content The content to rename.
179      * @param  string $name  The name of the content.
180      * @return boolean true if the content has been renamed; false otherwise.
181      */
182     public function rename_content(content $content, string $name): bool {
183         return $content->set_name($name);
184     }
186     /**
187      * Move content to another context.
188      * This method can be overwritten by the plugins if they need to change some other specific information.
189      *
190      * @param  content $content The content to rename.
191      * @param  \context $context  The new context.
192      * @return boolean true if the content has been renamed; false otherwise.
193      */
194     public function move_content(content $content, \context $context): bool {
195         return $content->set_contextid($context->id);
196     }
198     /**
199      * Returns the contenttype name of this content.
200      *
201      * @return string   Content type of the current instance
202      */
203     public function get_contenttype_name(): string {
204         $classname = get_class($this);
205         $contenttype = explode('\\', $classname);
206         return array_shift($contenttype);
207     }
209     /**
210      * Returns the plugin name of the current instance.
211      *
212      * @return string   Plugin name of the current instance
213      */
214     public function get_plugin_name(): string {
215         $contenttype = $this->get_contenttype_name();
216         $plugin = explode('_', $contenttype);
217         return array_pop($plugin);
218     }
220     /**
221      * Returns the URL where the content will be visualized.
222      *
223      * @param  content $content The content to be displayed.
224      * @return string           URL where to visualize the given content.
225      */
226     public function get_view_url(content $content): string {
227         return new moodle_url('/contentbank/view.php', ['id' => $content->get_id()]);
228     }
230     /**
231      * Returns the HTML content to add to view.php visualizer.
232      *
233      * @param  content $content The content to be displayed.
234      * @return string           HTML code to include in view.php.
235      */
236     public function get_view_content(content $content): string {
237         // Trigger an event for viewing this content.
238         $event = contentbank_content_viewed::create_from_record($content->get_content());
239         $event->trigger();
241         return '';
242     }
244     /**
245      * Returns the URL to download the content.
246      *
247      * @since  Moodle 3.10
248      * @param  content $content The content to be downloaded.
249      * @return string           URL with the content to download.
250      */
251     public function get_download_url(content $content): string {
252         $downloadurl = '';
253         $file = $content->get_file();
254         if (!empty($file)) {
255             $url = \moodle_url::make_pluginfile_url(
256                 $file->get_contextid(),
257                 $file->get_component(),
258                 $file->get_filearea(),
259                 $file->get_itemid(),
260                 $file->get_filepath(),
261                 $file->get_filename()
262             );
263             $downloadurl = $url->out(false);
264         }
266         return $downloadurl;
267     }
269     /**
270      * Returns the HTML code to render the icon for content bank contents.
271      *
272      * @param  content $content The content to be displayed.
273      * @return string               HTML code to render the icon
274      */
275     public function get_icon(content $content): string {
276         global $OUTPUT;
277         return $OUTPUT->image_url('f/unknown-64', 'moodle')->out(false);
278     }
280     /**
281      * Returns user has access capability for the main content bank and the content itself (base on is_access_allowed from plugin).
282      *
283      * @return bool     True if content could be accessed. False otherwise.
284      */
285     final public function can_access(): bool {
286         $classname = 'contenttype/'.$this->get_plugin_name();
287         $capability = $classname.":access";
288         $hascapabilities = has_capability('moodle/contentbank:access', $this->context)
289             && has_capability($capability, $this->context);
290         return $hascapabilities && $this->is_access_allowed();
291     }
293     /**
294      * Returns user has access capability for the content itself.
295      *
296      * @return bool     True if content could be accessed. False otherwise.
297      */
298     protected function is_access_allowed(): bool {
299         // Plugins can overwrite this function to add any check they need.
300         return true;
301     }
303     /**
304      * Returns the user has permission to upload new content.
305      *
306      * @return bool     True if content could be uploaded. False otherwise.
307      */
308     final public function can_upload(): bool {
309         if (!$this->is_feature_supported(self::CAN_UPLOAD)) {
310             return false;
311         }
312         if (!$this->can_access()) {
313             return false;
314         }
316         $classname = 'contenttype/'.$this->get_plugin_name();
317         $uploadcap = $classname.':upload';
318         $hascapabilities = has_capability('moodle/contentbank:upload', $this->context)
319             && has_capability($uploadcap, $this->context);
320         return $hascapabilities && $this->is_upload_allowed();
321     }
323     /**
324      * Returns plugin allows uploading.
325      *
326      * @return bool     True if plugin allows uploading. False otherwise.
327      */
328     protected function is_upload_allowed(): bool {
329         // Plugins can overwrite this function to add any check they need.
330         return true;
331     }
333     /**
334      * Check if the user can delete this content.
335      *
336      * @param  content $content The content to be deleted.
337      * @return bool True if content could be uploaded. False otherwise.
338      */
339     final public function can_delete(content $content): bool {
340         global $USER;
342         if ($this->context->id != $content->get_content()->contextid) {
343             // The content has to have exactly the same context as this contenttype.
344             return false;
345         }
347         $hascapability = has_capability('moodle/contentbank:deleteanycontent', $this->context);
348         if ($content->get_content()->usercreated == $USER->id) {
349             // This content has been created by the current user; check if she can delete her content.
350             $hascapability = $hascapability || has_capability('moodle/contentbank:deleteowncontent', $this->context);
351         }
353         return $hascapability && $this->is_delete_allowed($content);
354     }
356     /**
357      * Returns if content allows deleting.
358      *
359      * @param  content $content The content to be deleted.
360      * @return bool True if content allows uploading. False otherwise.
361      */
362     protected function is_delete_allowed(content $content): bool {
363         // Plugins can overwrite this function to add any check they need.
364         return true;
365     }
367     /**
368      * Check if the user can managed this content.
369      *
370      * @param  content $content The content to be managed.
371      * @return bool     True if content could be managed. False otherwise.
372      */
373     public final function can_manage(content $content): bool {
374         global $USER;
376         if ($this->context->id != $content->get_content()->contextid) {
377             // The content has to have exactly the same context as this contenttype.
378             return false;
379         }
381         // Check main contentbank management permission.
382         $hascapability = has_capability('moodle/contentbank:manageanycontent', $this->context);
383         if ($content->get_content()->usercreated == $USER->id) {
384             // This content has been created by the current user; check if they can manage their content.
385             $hascapability = $hascapability || has_capability('moodle/contentbank:manageowncontent', $this->context);
386         }
388         return $hascapability && $this->is_manage_allowed($content);
389     }
391     /**
392      * Returns if content allows managing.
393      *
394      * @param  content $content The content to be managed.
395      * @return bool True if content allows uploading. False otherwise.
396      */
397     protected function is_manage_allowed(content $content): bool {
398         // Plugins can overwrite this function to add any check they need.
399         return true;
400     }
402     /**
403      * Returns whether or not the user has permission to use the editor.
404      * This function will be called with the content to be edited as parameter,
405      * or null when is checking permission to create a new content using the editor.
406      *
407      * @param  content $content The content to be edited or null when creating a new content.
408      * @return bool     True if the user can edit content. False otherwise.
409      */
410     final public function can_edit(?content $content = null): bool {
411         if (!$this->is_feature_supported(self::CAN_EDIT)) {
412             return false;
413         }
415         if (!$this->can_access()) {
416             return false;
417         }
419         if (!is_null($content) && !$this->can_manage($content)) {
420             return false;
421         }
423         $classname = 'contenttype/'.$this->get_plugin_name();
425         $editioncap = $classname.':useeditor';
426         $hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context);
427         return $hascapabilities && $this->is_edit_allowed($content);
428     }
430     /**
431      * Returns plugin allows edition.
432      *
433      * @param  content $content The content to be edited.
434      * @return bool     True if plugin allows edition. False otherwise.
435      */
436     protected function is_edit_allowed(?content $content): bool {
437         // Plugins can overwrite this function to add any check they need.
438         return true;
439     }
441     /**
442      * Returns whether or not the user has permission to download the content.
443      *
444      * @since  Moodle 3.10
445      * @param  content $content The content to be downloaded.
446      * @return bool    True if the user can download the content. False otherwise.
447      */
448     final public function can_download(content $content): bool {
449         if (!$this->is_feature_supported(self::CAN_DOWNLOAD)) {
450             return false;
451         }
453         if (!$this->can_access()) {
454             return false;
455         }
457         $hascapability = has_capability('moodle/contentbank:downloadcontent', $this->context);
458         return $hascapability && $this->is_download_allowed($content);
459     }
461     /**
462      * Returns plugin allows downloading.
463      *
464      * @since  Moodle 3.10
465      * @param  content $content The content to be downloaed.
466      * @return bool    True if plugin allows downloading. False otherwise.
467      */
468     protected function is_download_allowed(content $content): bool {
469         // Plugins can overwrite this function to add any check they need.
470         return true;
471     }
473     /**
474      * Returns the plugin supports the feature.
475      *
476      * @param string $feature Feature code e.g CAN_UPLOAD
477      * @return bool     True if content could be uploaded. False otherwise.
478      */
479     final public function is_feature_supported(string $feature): bool {
480         return in_array($feature, $this->get_implemented_features());
481     }
483     /**
484      * Return an array of implemented features by the plugins.
485      *
486      * @return array
487      */
488     abstract protected function get_implemented_features(): array;
490     /**
491      * Return an array of extensions the plugins could manage.
492      *
493      * @return array
494      */
495     abstract public function get_manageable_extensions(): array;
497     /**
498      * Returns the list of different types of the given content type.
499      *
500      * A content type can have one or more options for creating content. This method will report all of them or only the content
501      * type itself if it has no other options.
502      *
503      * @return array An object for each type:
504      *     - string typename: descriptive name of the type.
505      *     - string typeeditorparams: params required by this content type editor.
506      *     - url typeicon: this type icon.
507      */
508     abstract public function get_contenttype_types(): array;