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 * The main interface for recycle bin methods.
19 *
20 * @package    tool_recyclebin
21 * @copyright  2015 University of Kent
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace tool_recyclebin;
26
27defined('MOODLE_INTERNAL') || die();
28
29define('TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA', 'recyclebin_coursecat');
30
31/**
32 * Represents a category's recyclebin.
33 *
34 * @package    tool_recyclebin
35 * @copyright  2015 University of Kent
36 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38class category_bin extends base_bin {
39
40    /**
41     * @var int The category id.
42     */
43    protected $_categoryid;
44
45    /**
46     * Constructor.
47     *
48     * @param int $categoryid The category id.
49     */
50    public function __construct($categoryid) {
51        $this->_categoryid = $categoryid;
52    }
53
54    /**
55     * Is this recyclebin enabled?
56     *
57     * @return bool true if enabled, false if not.
58     */
59    public static function is_enabled() {
60        return get_config('tool_recyclebin', 'categorybinenable');
61    }
62
63    /**
64     * Returns an item from the recycle bin.
65     *
66     * @param int $itemid Item ID to retrieve.
67     * @return \stdClass the item.
68     */
69    public function get_item($itemid) {
70        global $DB;
71
72        $item = $DB->get_record('tool_recyclebin_category', array(
73            'id' => $itemid
74        ), '*', MUST_EXIST);
75
76        $item->name = get_course_display_name_for_list($item);
77
78        return $item;
79    }
80
81    /**
82     * Returns a list of items in the recycle bin for this course.
83     *
84     * @return array the list of items.
85     */
86    public function get_items() {
87        global $DB;
88
89        $items = $DB->get_records('tool_recyclebin_category', array(
90            'categoryid' => $this->_categoryid
91        ));
92
93        foreach ($items as $item) {
94            $item->name = get_course_display_name_for_list($item);
95        }
96
97        return $items;
98    }
99
100    /**
101     * Store a course in the recycle bin.
102     *
103     * @param \stdClass $course Course
104     * @throws \moodle_exception
105     */
106    public function store_item($course) {
107        global $CFG, $DB;
108
109        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
110
111        // As far as recycle bin is using MODE_AUTOMATED, it observes the backup_auto_storage
112        // settings (storing backups @ real location and potentially not including files).
113        // For recycle bin we want to ensure that backup files are always stored in Moodle file
114        // area and always contain the users' files. In order to achieve that, we hack the
115        // setting here via $CFG->forced_plugin_settings, so it won't interfere other operations.
116        // See MDL-65218 and MDL-35773 for more information.
117        // This hack will be removed once recycle bin switches to use its own backup mode, with
118        // own preferences and 100% separate from MOODLE_AUTOMATED.
119        // TODO: Remove this as part of MDL-65228.
120        $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0, 'backup_auto_files' => 1];
121
122        // Backup the course.
123        $user = get_admin();
124        $controller = new \backup_controller(
125            \backup::TYPE_1COURSE,
126            $course->id,
127            \backup::FORMAT_MOODLE,
128            \backup::INTERACTIVE_NO,
129            \backup::MODE_AUTOMATED,
130            $user->id
131        );
132        $controller->execute_plan();
133
134        // We don't need the forced setting anymore, hence unsetting it.
135        // TODO: Remove this as part of MDL-65228.
136        unset($CFG->forced_plugin_settings['backup']);
137
138        // Grab the result.
139        $result = $controller->get_results();
140        if (!isset($result['backup_destination'])) {
141            throw new \moodle_exception('Failed to backup activity prior to deletion.');
142        }
143
144        // Have finished with the controller, let's destroy it, freeing mem and resources.
145        $controller->destroy();
146
147        // Grab the filename.
148        $file = $result['backup_destination'];
149        if (!$file->get_contenthash()) {
150            throw new \moodle_exception('Failed to backup activity prior to deletion (invalid file).');
151        }
152
153        // Record the activity, get an ID.
154        $item = new \stdClass();
155        $item->categoryid = $course->category;
156        $item->shortname = $course->shortname;
157        $item->fullname = $course->fullname;
158        $item->timecreated = time();
159        $binid = $DB->insert_record('tool_recyclebin_category', $item);
160
161        // Create the location we want to copy this file to.
162        $filerecord = array(
163            'contextid' => \context_coursecat::instance($course->category)->id,
164            'component' => 'tool_recyclebin',
165            'filearea' => TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA,
166            'itemid' => $binid,
167            'timemodified' => time()
168        );
169
170        // Move the file to our own special little place.
171        $fs = get_file_storage();
172        if (!$fs->create_file_from_storedfile($filerecord, $file)) {
173            // Failed, cleanup first.
174            $DB->delete_records('tool_recyclebin_category', array(
175                'id' => $binid
176            ));
177
178            throw new \moodle_exception("Failed to copy backup file to recyclebin.");
179        }
180
181        // Delete the old file.
182        $file->delete();
183
184        // Fire event.
185        $event = \tool_recyclebin\event\category_bin_item_created::create(array(
186            'objectid' => $binid,
187            'context' => \context_coursecat::instance($course->category)
188        ));
189        $event->trigger();
190    }
191
192    /**
193     * Restore an item from the recycle bin.
194     *
195     * @param \stdClass $item The item database record
196     * @throws \moodle_exception
197     */
198    public function restore_item($item) {
199        global $CFG, $OUTPUT, $PAGE;
200
201        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
202        require_once($CFG->dirroot . '/course/lib.php');
203
204        $user = get_admin();
205
206        // Grab the course category context.
207        $context = \context_coursecat::instance($this->_categoryid);
208
209        // Get the backup file.
210        $fs = get_file_storage();
211        $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id,
212            'itemid, filepath, filename', false);
213
214        if (empty($files)) {
215            throw new \moodle_exception('Invalid recycle bin item!');
216        }
217
218        if (count($files) > 1) {
219            throw new \moodle_exception('Too many files found!');
220        }
221
222        // Get the backup file.
223        $file = reset($files);
224
225        // Get a backup temp directory name and create it.
226        $tempdir = \restore_controller::get_tempdir_name($context->id, $user->id);
227        $fulltempdir = make_backup_temp_directory($tempdir);
228
229        // Extract the backup to tmpdir.
230        $fb = get_file_packer('application/vnd.moodle.backup');
231        $fb->extract_to_pathname($file, $fulltempdir);
232
233        // Build a course.
234        $course = new \stdClass();
235        $course->category = $this->_categoryid;
236        $course->shortname = $item->shortname;
237        $course->fullname = $item->fullname;
238        $course->summary = '';
239
240        // Create a new course.
241        $course = create_course($course);
242        if (!$course) {
243            throw new \moodle_exception("Could not create course to restore into.");
244        }
245
246        // Define the import.
247        $controller = new \restore_controller(
248            $tempdir,
249            $course->id,
250            \backup::INTERACTIVE_NO,
251            \backup::MODE_AUTOMATED,
252            $user->id,
253            \backup::TARGET_NEW_COURSE
254        );
255
256        // Prechecks.
257        if (!$controller->execute_precheck()) {
258            $results = $controller->get_precheck_results();
259
260            // Check if errors have been found.
261            if (!empty($results['errors'])) {
262                // Delete the temporary file we created.
263                fulldelete($fulltempdir);
264
265                // Delete the course we created.
266                delete_course($course, false);
267
268                echo $OUTPUT->header();
269                $backuprenderer = $PAGE->get_renderer('core', 'backup');
270                echo $backuprenderer->precheck_notices($results);
271                echo $OUTPUT->continue_button(new \moodle_url('/course/index.php', array('categoryid' => $this->_categoryid)));
272                echo $OUTPUT->footer();
273                exit();
274            }
275        }
276
277        // Run the import.
278        $controller->execute_plan();
279
280        // Have finished with the controller, let's destroy it, freeing mem and resources.
281        $controller->destroy();
282
283        // Fire event.
284        $event = \tool_recyclebin\event\category_bin_item_restored::create(array(
285            'objectid' => $item->id,
286            'context' => $context
287        ));
288        $event->add_record_snapshot('tool_recyclebin_category', $item);
289        $event->trigger();
290
291        // Cleanup.
292        fulldelete($fulltempdir);
293        $this->delete_item($item);
294    }
295
296    /**
297     * Delete an item from the recycle bin.
298     *
299     * @param \stdClass $item The item database record
300     * @throws \coding_exception
301     */
302    public function delete_item($item) {
303        global $DB;
304
305        // Grab the course category context.
306        $context = \context_coursecat::instance($this->_categoryid, IGNORE_MISSING);
307        if (!empty($context)) {
308            // Delete the files.
309            $fs = get_file_storage();
310            $fs->delete_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id);
311        } else {
312            // Course category has been deleted. Find records using $item->id as this is unique for coursecat recylebin.
313            $files = $DB->get_recordset('files', [
314                'component' => 'tool_recyclebin',
315                'filearea' => TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA,
316                'itemid' => $item->id,
317            ]);
318            $fs = get_file_storage();
319            foreach ($files as $filer) {
320                $file = $fs->get_file_instance($filer);
321                $file->delete();
322            }
323            $files->close();
324        }
325
326        // Delete the record.
327        $DB->delete_records('tool_recyclebin_category', array(
328            'id' => $item->id
329        ));
330
331        // The coursecat might have been deleted, check we have a context before triggering event.
332        if (!$context) {
333            return;
334        }
335
336        // Fire event.
337        $event = \tool_recyclebin\event\category_bin_item_deleted::create(array(
338            'objectid' => $item->id,
339            'context' => \context_coursecat::instance($item->categoryid)
340        ));
341        $event->add_record_snapshot('tool_recyclebin_category', $item);
342        $event->trigger();
343    }
344
345    /**
346     * Can we view items in this recycle bin?
347     *
348     * @return bool returns true if they can view, false if not
349     */
350    public function can_view() {
351        $context = \context_coursecat::instance($this->_categoryid);
352        return has_capability('tool/recyclebin:viewitems', $context);
353    }
354
355    /**
356     * Can we restore items in this recycle bin?
357     *
358     * @return bool returns true if they can restore, false if not
359     */
360    public function can_restore() {
361        $context = \context_coursecat::instance($this->_categoryid);
362        return has_capability('tool/recyclebin:restoreitems', $context);
363    }
364
365    /**
366     * Can we delete items in this recycle bin?
367     *
368     * @return bool returns true if they can delete, false if not
369     */
370    public function can_delete() {
371        $context = \context_coursecat::instance($this->_categoryid);
372        return has_capability('tool/recyclebin:deleteitems', $context);
373    }
374}
375