Merge branch 'MDL-68569-310' of https://github.com/paulholden/moodle into MOODLE_310_...
[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     /** Plugin implements uploading feature */
44     const CAN_UPLOAD = 'upload';
46     /** Plugin implements edition feature */
47     const CAN_EDIT = 'edit';
49     /** @var \context This contenttype's context. **/
50     protected $context = null;
52     /**
53      * Content type constructor
54      *
55      * @param \context $context Optional context to check (default null)
56      */
57     public function __construct(\context $context = null) {
58         if (empty($context)) {
59             $context = \context_system::instance();
60         }
61         $this->context = $context;
62     }
64     /**
65      * Fills content_bank table with appropiate information.
66      *
67      * @throws dml_exception A DML specific exception is thrown for any creation error.
68      * @param \stdClass $record An optional content record compatible object (default null)
69      * @return content  Object with content bank information.
70      */
71     public function create_content(\stdClass $record = null): content {
72         global $USER, $DB;
74         $entry = new \stdClass();
75         $entry->contenttype = $this->get_contenttype_name();
76         $entry->contextid = $this->context->id;
77         $entry->name = $record->name ?? '';
78         $entry->usercreated = $record->usercreated ?? $USER->id;
79         $entry->timecreated = time();
80         $entry->usermodified = $entry->usercreated;
81         $entry->timemodified = $entry->timecreated;
82         $entry->configdata = $record->configdata ?? '';
83         $entry->instanceid = $record->instanceid ?? 0;
84         $entry->id = $DB->insert_record('contentbank_content', $entry);
85         $classname = '\\'.$entry->contenttype.'\\content';
86         $content = new $classname($entry);
87         // Trigger an event for creating the content.
88         $event = contentbank_content_created::create_from_record($content->get_content());
89         $event->trigger();
90         return $content;
91     }
93     /**
94      * Create a new content from an uploaded file.
95      *
96      * @throws file_exception If file operations fail
97      * @throws dml_exception if the content creation fails
98      * @param stored_file $file the uploaded file
99      * @param \stdClass|null $record an optional content record
100      * @return content  Object with content bank information.
101      */
102     public function upload_content(stored_file $file, \stdClass $record = null): content {
103         if (empty($record)) {
104             $record = new \stdClass();
105             $record->name = $file->get_filename();
106         }
107         $content = $this->create_content($record);
108         try {
109             $content->import_file($file);
110         } catch (Exception $e) {
111             $this->delete_content($content);
112             throw $e;
113         }
115         return $content;
116     }
118     /**
119      * Delete this content from the content_bank.
120      * This method can be overwritten by the plugins if they need to delete specific information.
121      *
122      * @param  content $content The content to delete.
123      * @return boolean true if the content has been deleted; false otherwise.
124      */
125     public function delete_content(content $content): bool {
126         global $DB;
128         // Delete the file if it exists.
129         if ($file = $content->get_file()) {
130             $file->delete();
131         }
133         // Delete the contentbank DB entry.
134         $result = $DB->delete_records('contentbank_content', ['id' => $content->get_id()]);
135         if ($result) {
136             // Trigger an event for deleting this content.
137             $record = $content->get_content();
138             $event = contentbank_content_deleted::create([
139                 'objectid' => $content->get_id(),
140                 'relateduserid' => $record->usercreated,
141                 'context' => \context::instance_by_id($record->contextid),
142                 'other' => [
143                     'contenttype' => $content->get_content_type(),
144                     'name' => $content->get_name()
145                 ]
146             ]);
147             $event->add_record_snapshot('contentbank_content', $record);
148             $event->trigger();
149         }
150         return $result;
151     }
153     /**
154      * Rename this content from the content_bank.
155      * This method can be overwritten by the plugins if they need to change some other specific information.
156      *
157      * @param  content $content The content to rename.
158      * @param  string $name  The name of the content.
159      * @return boolean true if the content has been renamed; false otherwise.
160      */
161     public function rename_content(content $content, string $name): bool {
162         return $content->set_name($name);
163     }
165     /**
166      * Move content to another context.
167      * This method can be overwritten by the plugins if they need to change some other specific information.
168      *
169      * @param  content $content The content to rename.
170      * @param  \context $context  The new context.
171      * @return boolean true if the content has been renamed; false otherwise.
172      */
173     public function move_content(content $content, \context $context): bool {
174         return $content->set_contextid($context->id);
175     }
177     /**
178      * Returns the contenttype name of this content.
179      *
180      * @return string   Content type of the current instance
181      */
182     public function get_contenttype_name(): string {
183         $classname = get_class($this);
184         $contenttype = explode('\\', $classname);
185         return array_shift($contenttype);
186     }
188     /**
189      * Returns the plugin name of the current instance.
190      *
191      * @return string   Plugin name of the current instance
192      */
193     public function get_plugin_name(): string {
194         $contenttype = $this->get_contenttype_name();
195         $plugin = explode('_', $contenttype);
196         return array_pop($plugin);
197     }
199     /**
200      * Returns the URL where the content will be visualized.
201      *
202      * @param  content $content The content to be displayed.
203      * @return string           URL where to visualize the given content.
204      */
205     public function get_view_url(content $content): string {
206         return new moodle_url('/contentbank/view.php', ['id' => $content->get_id()]);
207     }
209     /**
210      * Returns the HTML content to add to view.php visualizer.
211      *
212      * @param  content $content The content to be displayed.
213      * @return string           HTML code to include in view.php.
214      */
215     public function get_view_content(content $content): string {
216         // Trigger an event for viewing this content.
217         $event = contentbank_content_viewed::create_from_record($content->get_content());
218         $event->trigger();
220         return '';
221     }
223     /**
224      * Returns the HTML code to render the icon for content bank contents.
225      *
226      * @param  content $content The content to be displayed.
227      * @return string               HTML code to render the icon
228      */
229     public function get_icon(content $content): string {
230         global $OUTPUT;
231         return $OUTPUT->image_url('f/unknown-64', 'moodle')->out(false);
232     }
234     /**
235      * Returns user has access capability for the main content bank and the content itself (base on is_access_allowed from plugin).
236      *
237      * @return bool     True if content could be accessed. False otherwise.
238      */
239     final public function can_access(): bool {
240         $classname = 'contenttype/'.$this->get_plugin_name();
241         $capability = $classname.":access";
242         $hascapabilities = has_capability('moodle/contentbank:access', $this->context)
243             && has_capability($capability, $this->context);
244         return $hascapabilities && $this->is_access_allowed();
245     }
247     /**
248      * Returns user has access capability for the content itself.
249      *
250      * @return bool     True if content could be accessed. False otherwise.
251      */
252     protected function is_access_allowed(): bool {
253         // Plugins can overwrite this function to add any check they need.
254         return true;
255     }
257     /**
258      * Returns the user has permission to upload new content.
259      *
260      * @return bool     True if content could be uploaded. False otherwise.
261      */
262     final public function can_upload(): bool {
263         if (!$this->is_feature_supported(self::CAN_UPLOAD)) {
264             return false;
265         }
266         if (!$this->can_access()) {
267             return false;
268         }
270         $classname = 'contenttype/'.$this->get_plugin_name();
271         $uploadcap = $classname.':upload';
272         $hascapabilities = has_capability('moodle/contentbank:upload', $this->context)
273             && has_capability($uploadcap, $this->context);
274         return $hascapabilities && $this->is_upload_allowed();
275     }
277     /**
278      * Returns plugin allows uploading.
279      *
280      * @return bool     True if plugin allows uploading. False otherwise.
281      */
282     protected function is_upload_allowed(): bool {
283         // Plugins can overwrite this function to add any check they need.
284         return true;
285     }
287     /**
288      * Check if the user can delete this content.
289      *
290      * @param  content $content The content to be deleted.
291      * @return bool True if content could be uploaded. False otherwise.
292      */
293     final public function can_delete(content $content): bool {
294         global $USER;
296         if ($this->context->id != $content->get_content()->contextid) {
297             // The content has to have exactly the same context as this contenttype.
298             return false;
299         }
301         $hascapability = has_capability('moodle/contentbank:deleteanycontent', $this->context);
302         if ($content->get_content()->usercreated == $USER->id) {
303             // This content has been created by the current user; check if she can delete her content.
304             $hascapability = $hascapability || has_capability('moodle/contentbank:deleteowncontent', $this->context);
305         }
307         return $hascapability && $this->is_delete_allowed($content);
308     }
310     /**
311      * Returns if content allows deleting.
312      *
313      * @param  content $content The content to be deleted.
314      * @return bool True if content allows uploading. False otherwise.
315      */
316     protected function is_delete_allowed(content $content): bool {
317         // Plugins can overwrite this function to add any check they need.
318         return true;
319     }
321     /**
322      * Check if the user can managed this content.
323      *
324      * @param  content $content The content to be managed.
325      * @return bool     True if content could be managed. False otherwise.
326      */
327     public final function can_manage(content $content): bool {
328         global $USER;
330         if ($this->context->id != $content->get_content()->contextid) {
331             // The content has to have exactly the same context as this contenttype.
332             return false;
333         }
335         // Check main contentbank management permission.
336         $hascapability = has_capability('moodle/contentbank:manageanycontent', $this->context);
337         if ($content->get_content()->usercreated == $USER->id) {
338             // This content has been created by the current user; check if they can manage their content.
339             $hascapability = $hascapability || has_capability('moodle/contentbank:manageowncontent', $this->context);
340         }
342         return $hascapability && $this->is_manage_allowed($content);
343     }
345     /**
346      * Returns if content allows managing.
347      *
348      * @param  content $content The content to be managed.
349      * @return bool True if content allows uploading. False otherwise.
350      */
351     protected function is_manage_allowed(content $content): bool {
352         // Plugins can overwrite this function to add any check they need.
353         return true;
354     }
356     /**
357      * Returns whether or not the user has permission to use the editor.
358      * This function will be called with the content to be edited as parameter,
359      * or null when is checking permission to create a new content using the editor.
360      *
361      * @param  content $content The content to be edited or null when creating a new content.
362      * @return bool     True if the user can edit content. False otherwise.
363      */
364     final public function can_edit(?content $content = null): bool {
365         if (!$this->is_feature_supported(self::CAN_EDIT)) {
366             return false;
367         }
369         if (!$this->can_access()) {
370             return false;
371         }
373         if (!is_null($content) && !$this->can_manage($content)) {
374             return false;
375         }
377         $classname = 'contenttype/'.$this->get_plugin_name();
379         $editioncap = $classname.':useeditor';
380         $hascapabilities = has_all_capabilities(['moodle/contentbank:useeditor', $editioncap], $this->context);
381         return $hascapabilities && $this->is_edit_allowed($content);
382     }
384     /**
385      * Returns plugin allows edition.
386      *
387      * @param  content $content The content to be edited.
388      * @return bool     True if plugin allows edition. False otherwise.
389      */
390     protected function is_edit_allowed(?content $content): bool {
391         // Plugins can overwrite this function to add any check they need.
392         return true;
393     }
395     /**
396      * Returns the plugin supports the feature.
397      *
398      * @param string $feature Feature code e.g CAN_UPLOAD
399      * @return bool     True if content could be uploaded. False otherwise.
400      */
401     final public function is_feature_supported(string $feature): bool {
402         return in_array($feature, $this->get_implemented_features());
403     }
405     /**
406      * Return an array of implemented features by the plugins.
407      *
408      * @return array
409      */
410     abstract protected function get_implemented_features(): array;
412     /**
413      * Return an array of extensions the plugins could manage.
414      *
415      * @return array
416      */
417     abstract public function get_manageable_extensions(): array;
419     /**
420      * Returns the list of different types of the given content type.
421      *
422      * A content type can have one or more options for creating content. This method will report all of them or only the content
423      * type itself if it has no other options.
424      *
425      * @return array An object for each type:
426      *     - string typename: descriptive name of the type.
427      *     - string typeeditorparams: params required by this content type editor.
428      *     - url typeicon: this type icon.
429      */
430     abstract public function get_contenttype_types(): array;