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/>.
16
17/**
18 * Content 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 */
24
25namespace core_contentbank;
26
27use core_text;
28use stored_file;
29use stdClass;
30use coding_exception;
31use context;
32use moodle_url;
33use core\event\contentbank_content_updated;
34
35/**
36 * Content manager class
37 *
38 * @package    core_contentbank
39 * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
40 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 */
42abstract class content {
43    /**
44     * @var int Visibility value. Public content is visible to all users with access to the content bank of the
45     * appropriate context.
46     */
47    public const VISIBILITY_PUBLIC = 1;
48
49    /**
50     * @var int Visibility value. Unlisted content is only visible to the author and to users with
51     * moodle/contentbank:viewunlistedcontent capability.
52     */
53    public const VISIBILITY_UNLISTED = 2;
54
55    /** @var stdClass $content The content of the current instance. **/
56    protected $content  = null;
57
58    /**
59     * Content bank constructor
60     *
61     * @param stdClass $record A contentbank_content record.
62     * @throws coding_exception If content type is not right.
63     */
64    public function __construct(stdClass $record) {
65        // Content type should exist and be linked to plugin classname.
66        $classname = $record->contenttype.'\\content';
67        if (get_class($this) != $classname) {
68            throw new coding_exception(get_string('contenttypenotfound', 'error', $record->contenttype));
69        }
70        $typeclass = $record->contenttype.'\\contenttype';
71        if (!class_exists($typeclass)) {
72            throw new coding_exception(get_string('contenttypenotfound', 'error', $record->contenttype));
73        }
74        // A record with the id must exist in 'contentbank_content' table.
75        // To improve performance, we are only checking the id is set, but no querying the database.
76        if (!isset($record->id)) {
77            throw new coding_exception(get_string('invalidcontentid', 'error'));
78        }
79        $this->content = $record;
80    }
81
82    /**
83     * Returns $this->content.
84     *
85     * @return stdClass  $this->content.
86     */
87    public function get_content(): stdClass {
88        return $this->content;
89    }
90
91    /**
92     * Returns $this->content->contenttype.
93     *
94     * @return string  $this->content->contenttype.
95     */
96    public function get_content_type(): string {
97        return $this->content->contenttype;
98    }
99
100    /**
101     * Return the contenttype instance of this content.
102     *
103     * @return contenttype The content type instance
104     */
105    public function get_content_type_instance(): contenttype {
106        $context = context::instance_by_id($this->content->contextid);
107        $contenttypeclass = "\\{$this->content->contenttype}\\contenttype";
108        return new $contenttypeclass($context);
109    }
110
111    /**
112     * Returns $this->content->timemodified.
113     *
114     * @return int  $this->content->timemodified.
115     */
116    public function get_timemodified(): int {
117        return $this->content->timemodified;
118    }
119
120    /**
121     * Updates content_bank table with information in $this->content.
122     *
123     * @return boolean  True if the content has been succesfully updated. False otherwise.
124     * @throws \coding_exception if not loaded.
125     */
126    public function update_content(): bool {
127        global $USER, $DB;
128
129        // A record with the id must exist in 'contentbank_content' table.
130        // To improve performance, we are only checking the id is set, but no querying the database.
131        if (!isset($this->content->id)) {
132            throw new coding_exception(get_string('invalidcontentid', 'error'));
133        }
134        $this->content->usermodified = $USER->id;
135        $this->content->timemodified = time();
136        $result = $DB->update_record('contentbank_content', $this->content);
137        if ($result) {
138            // Trigger an event for updating this content.
139            $event = contentbank_content_updated::create_from_record($this->content);
140            $event->trigger();
141        }
142        return $result;
143    }
144
145    /**
146     * Set a new name to the content.
147     *
148     * @param string $name  The name of the content.
149     * @return bool  True if the content has been succesfully updated. False otherwise.
150     * @throws \coding_exception if not loaded.
151     */
152    public function set_name(string $name): bool {
153        $name = trim($name);
154        if ($name === '') {
155            return false;
156        }
157
158        // Clean name.
159        $name = clean_param($name, PARAM_TEXT);
160        if (core_text::strlen($name) > 255) {
161            $name = core_text::substr($name, 0, 255);
162        }
163
164        $oldname = $this->content->name;
165        $this->content->name = $name;
166        $updated = $this->update_content();
167        if (!$updated) {
168            $this->content->name = $oldname;
169        }
170        return $updated;
171    }
172
173    /**
174     * Returns the name of the content.
175     *
176     * @return string   The name of the content.
177     */
178    public function get_name(): string {
179        return $this->content->name;
180    }
181
182    /**
183     * Set a new contextid to the content.
184     *
185     * @param int $contextid  The new contextid of the content.
186     * @return bool  True if the content has been succesfully updated. False otherwise.
187     */
188    public function set_contextid(int $contextid): bool {
189        if ($this->content->contextid == $contextid) {
190            return true;
191        }
192
193        $oldcontextid = $this->content->contextid;
194        $this->content->contextid = $contextid;
195        $updated = $this->update_content();
196        if ($updated) {
197            // Move files to new context
198            $fs = get_file_storage();
199            $fs->move_area_files_to_new_context($oldcontextid, $contextid, 'contentbank', 'public', $this->content->id);
200        } else {
201            $this->content->contextid = $oldcontextid;
202        }
203        return $updated;
204    }
205
206    /**
207     * Returns the contextid of the content.
208     *
209     * @return int   The id of the content context.
210     */
211    public function get_contextid(): string {
212        return $this->content->contextid;
213    }
214
215    /**
216     * Returns the content ID.
217     *
218     * @return int   The content ID.
219     */
220    public function get_id(): int {
221        return $this->content->id;
222    }
223
224    /**
225     * Change the content instanceid value.
226     *
227     * @param int $instanceid    New instanceid for this content
228     * @return boolean           True if the instanceid has been succesfully updated. False otherwise.
229     */
230    public function set_instanceid(int $instanceid): bool {
231        $this->content->instanceid = $instanceid;
232        return $this->update_content();
233    }
234
235    /**
236     * Returns the $instanceid of this content.
237     *
238     * @return int   contentbank instanceid
239     */
240    public function get_instanceid(): int {
241        return $this->content->instanceid;
242    }
243
244    /**
245     * Change the content config values.
246     *
247     * @param string $configdata    New config information for this content
248     * @return boolean              True if the configdata has been succesfully updated. False otherwise.
249     */
250    public function set_configdata(string $configdata): bool {
251        $this->content->configdata = $configdata;
252        return $this->update_content();
253    }
254
255    /**
256     * Return the content config values.
257     *
258     * @return mixed   Config information for this content (json decoded)
259     */
260    public function get_configdata() {
261        return $this->content->configdata;
262    }
263
264    /**
265     * Sets a new content visibility and saves it to database.
266     *
267     * @param int $visibility Must be self::PUBLIC or self::UNLISTED
268     * @return bool
269     * @throws coding_exception
270     */
271    public function set_visibility(int $visibility): bool {
272        if (!in_array($visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED])) {
273            return false;
274        }
275        $this->content->visibility = $visibility;
276        return $this->update_content();
277    }
278
279    /**
280     * Return true if the content may be shown to other users in the content bank.
281     *
282     * @return boolean
283     */
284    public function get_visibility(): int {
285        return $this->content->visibility;
286    }
287
288    /**
289     * Import a file as a valid content.
290     *
291     * By default, all content has a public file area to interact with the content bank
292     * repository. This method should be overridden by contentypes which does not simply
293     * upload to the public file area.
294     *
295     * If any, the method will return the final stored_file. This way it can be invoked
296     * as parent::import_file in case any plugin want to store the file in the public area
297     * and also parse it.
298     *
299     * @throws file_exception If file operations fail
300     * @param stored_file $file File to store in the content file area.
301     * @return stored_file|null the stored content file or null if the file is discarted.
302     */
303    public function import_file(stored_file $file): ?stored_file {
304        $originalfile = $this->get_file();
305        if ($originalfile) {
306            $originalfile->replace_file_with($file);
307            return $originalfile;
308        } else {
309            $itemid = $this->get_id();
310            $fs = get_file_storage();
311            $filerecord = [
312                'contextid' => $this->get_contextid(),
313                'component' => 'contentbank',
314                'filearea' => 'public',
315                'itemid' => $this->get_id(),
316                'filepath' => '/',
317                'filename' => $file->get_filename(),
318                'timecreated' => time(),
319            ];
320            return $fs->create_file_from_storedfile($filerecord, $file);
321        }
322    }
323
324    /**
325     * Returns the $file related to this content.
326     *
327     * @return stored_file  File stored in content bank area related to the given itemid.
328     * @throws \coding_exception if not loaded.
329     */
330    public function get_file(): ?stored_file {
331        $itemid = $this->get_id();
332        $fs = get_file_storage();
333        $files = $fs->get_area_files(
334            $this->content->contextid,
335            'contentbank',
336            'public',
337            $itemid,
338            'itemid, filepath, filename',
339            false
340        );
341        if (!empty($files)) {
342            $file = reset($files);
343            return $file;
344        }
345        return null;
346    }
347
348    /**
349     * Returns the places where the file associated to this content is used or an empty array if the content has no file.
350     *
351     * @return array of stored_file where current file content is used or empty array if it hasn't any file.
352     * @since 3.11
353     */
354    public function get_uses(): ?array {
355        $references = [];
356
357        $file = $this->get_file();
358        if ($file != null) {
359            $fs = get_file_storage();
360            $references = $fs->get_references_by_storedfile($file);
361        }
362
363        return $references;
364    }
365
366    /**
367     * Returns the file url related to this content.
368     *
369     * @return string       URL of the file stored in content bank area related to the given itemid.
370     * @throws \coding_exception if not loaded.
371     */
372    public function get_file_url(): string {
373        if (!$file = $this->get_file()) {
374            return '';
375        }
376        $fileurl = moodle_url::make_pluginfile_url(
377            $this->content->contextid,
378            'contentbank',
379            'public',
380            $file->get_itemid(),
381            $file->get_filepath(),
382            $file->get_filename()
383        );
384
385        return $fileurl;
386    }
387
388    /**
389     * Returns user has access permission for the content itself (based on what plugin needs).
390     *
391     * @return bool     True if content could be accessed. False otherwise.
392     */
393    public function is_view_allowed(): bool {
394        // Plugins can overwrite this method in case they want to check something related to content properties.
395        global $USER;
396        $context = \context::instance_by_id($this->get_contextid());
397
398        return $USER->id == $this->content->usercreated ||
399            $this->get_visibility() == self::VISIBILITY_PUBLIC ||
400            has_capability('moodle/contentbank:viewunlistedcontent', $context);
401    }
402}
403