Merge branch 'MDL-63241-master-take2' of https://github.com/lucaboesch/moodle
authorJun Pataleta <jun@moodle.com>
Tue, 9 Oct 2018 04:49:07 +0000 (12:49 +0800)
committerJun Pataleta <jun@moodle.com>
Tue, 9 Oct 2018 04:49:07 +0000 (12:49 +0800)
27 files changed:
blog/classes/external.php [new file with mode: 0644]
blog/classes/external/post_exporter.php [new file with mode: 0644]
blog/index.php
blog/lib.php
blog/tests/external_test.php [new file with mode: 0644]
course/classes/output/activity_navigation.php
lang/en/privacy.php
lib/amd/build/tree.min.js
lib/amd/src/tree.js
lib/db/services.php
lib/jquery/readme_moodle.txt
lib/requirejs/readme_moodle.txt
mod/assign/submission/onlinetext/classes/privacy/provider.php
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js
mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js
mod/quiz/yui/src/dragdrop/js/resource.js
privacy/classes/local/request/moodle_content_writer.php
privacy/classes/output/exported_html_page.php [new file with mode: 0644]
privacy/classes/output/exported_navigation_page.php [new file with mode: 0644]
privacy/classes/output/renderer.php [new file with mode: 0644]
privacy/export_files/general.css [new file with mode: 0644]
privacy/export_files/general.js [new file with mode: 0644]
privacy/templates/htmlpage.mustache [new file with mode: 0644]
privacy/templates/navigation.mustache [new file with mode: 0644]
privacy/tests/moodle_content_writer_test.php
version.php

diff --git a/blog/classes/external.php b/blog/classes/external.php
new file mode 100644 (file)
index 0000000..9333d2b
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the external API for blogs.
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_blog;
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir .'/externallib.php');
+require_once($CFG->dirroot .'/blog/lib.php');
+require_once($CFG->dirroot .'/blog/locallib.php');
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use external_single_structure;
+use external_multiple_structure;
+use external_warnings;
+use context_system;
+use context_course;
+use moodle_exception;
+use core_blog\external\post_exporter;
+
+/**
+ * This is the external API for blogs.
+ *
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+    /**
+     * Returns description of get_entries() parameters.
+     *
+     * @return external_function_parameters
+     * @since  Moodle 3.6
+     */
+    public static function get_entries_parameters() {
+        return new external_function_parameters(
+            array(
+                'filters' => new external_multiple_structure (
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHA,
+                                'The expected keys (value format) are:
+                                tag      PARAM_NOTAGS blog tag
+                                tagid    PARAM_INT    blog tag id
+                                userid   PARAM_INT    blog author (userid)
+                                cmid    PARAM_INT    course module id
+                                entryid  PARAM_INT    entry id
+                                groupid  PARAM_INT    group id
+                                courseid PARAM_INT    course id
+                                search   PARAM_RAW    search term
+                                '
+                            ),
+                            'value' => new external_value(PARAM_RAW, 'The value of the filter.')
+                        )
+                    ), 'Parameters to filter blog listings.', VALUE_DEFAULT, array()
+                ),
+                'page' => new external_value(PARAM_INT, 'The blog page to return.', VALUE_DEFAULT, 0),
+                'perpage' => new external_value(PARAM_INT, 'The number of posts to return per page.', VALUE_DEFAULT, 10),
+            )
+        );
+    }
+
+    /**
+     * Return blog entries.
+     *
+     * @param array $filters the parameters to filter the blog listing
+     * @param int $page the blog page to return
+     * @param int $perpage the number of posts to return per page
+     * @return array with the blog entries and warnings
+     * @since  Moodle 3.6
+     */
+    public static function get_entries($filters = array(), $page = 0, $perpage = 10) {
+        global $CFG, $DB, $PAGE;
+
+        $warnings = array();
+        $params = self::validate_parameters(self::get_entries_parameters(),
+            array('filters' => $filters, 'page' => $page, 'perpage' => $perpage));
+
+        if (empty($CFG->enableblogs)) {
+            throw new moodle_exception('blogdisable', 'blog');
+        }
+
+        // Init filters.
+        $filterstype = array('courseid' => PARAM_INT, 'groupid' => PARAM_INT, 'userid' => PARAM_INT, 'tagid' => PARAM_INT,
+            'tag' => PARAM_NOTAGS, 'cmid' => PARAM_INT, 'entryid' => PARAM_INT, 'search' => PARAM_RAW);
+        $filters = array('courseid' => null, 'groupid' => null, 'userid' => null, 'tagid' => null,
+            'tag' => null, 'cmid' => null, 'entryid' => null, 'search' => null);
+
+        foreach ($params['filters'] as $filter) {
+            $name = trim($filter['name']);
+            if (!isset($filterstype[$name])) {
+                throw new moodle_exception('errorinvalidparam', 'webservice', '', $name);
+            }
+            $filters[$name] = clean_param($filter['value'], $filterstype[$name]);
+        }
+
+        // Do not overwrite here the filters, blog_get_headers and blog_listing will take care of that.
+        list($courseid, $userid) = blog_validate_access($filters['courseid'], $filters['cmid'], $filters['groupid'],
+            $filters['entryid'], $filters['userid']);
+
+        if ($courseid && $courseid != SITEID) {
+            $context = context_course::instance($courseid);
+            self::validate_context($context);
+        } else {
+            $context = context_system::instance();
+            if ($CFG->bloglevel == BLOG_GLOBAL_LEVEL) {
+                // Everybody can see anything - no login required unless site is locked down using forcelogin.
+                if ($CFG->forcelogin) {
+                    self::validate_context($context);
+                }
+            } else {
+                self::validate_context($context);
+            }
+        }
+        $PAGE->set_context($context); // Needed by internal APIs.
+
+        // Get filters.
+        $blogheaders = blog_get_headers($filters['courseid'], $filters['groupid'], $filters['userid'], $filters['tagid'],
+            $filters['tag'], $filters['cmid'], $filters['entryid'], $filters['search']);
+        $bloglisting = new \blog_listing($blogheaders['filters']);
+
+        $page  = $params['page'];
+        $limit = empty($params['perpage']) ? get_user_preferences('blogpagesize', 10) : $params['perpage'];
+        $start = $page * $limit;
+        $entries = $bloglisting->get_entries($start, $limit);
+        $totalentries = $bloglisting->count_entries();
+
+        $exportedentries = array();
+        $output = $PAGE->get_renderer('core');
+        foreach ($entries as $entry) {
+            $exporter = new post_exporter($entry, array('context' => $context));
+            $exportedentries[] = $exporter->export($output);
+        }
+        return array(
+            'warnings' => $warnings,
+            'entries' => $exportedentries,
+            'totalentries' => $totalentries,
+        );
+    }
+
+    /**
+     * Returns description of get_entries() result value.
+     *
+     * @return external_description
+     * @since  Moodle 3.6
+     */
+    public static function get_entries_returns() {
+        return new external_single_structure(
+            array(
+                'entries' => new external_multiple_structure(
+                    post_exporter::get_read_structure()
+                ),
+                'totalentries' => new external_value(PARAM_INT, 'The total number of entries found.'),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+}
diff --git a/blog/classes/external/post_exporter.php b/blog/classes/external/post_exporter.php
new file mode 100644 (file)
index 0000000..3d69299
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for exporting a blog post (entry).
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_blog\external;
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use external_util;
+use external_files;
+use renderer_base;
+use context_system;
+
+/**
+ * Class for exporting a blog post (entry).
+ *
+ * @copyright  2018 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class post_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array list of properties
+     */
+    protected static function define_properties() {
+        return array(
+            'id' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post/entry id.',
+            ),
+            'module' => array(
+                'type' => PARAM_ALPHANUMEXT,
+                'null' => NULL_NOT_ALLOWED,
+                'description' => 'Where it was published the post (blog, blog_external...).',
+            ),
+            'userid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Post author.',
+            ),
+            'courseid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Course where the post was created.',
+            ),
+            'groupid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Group post was created for.',
+            ),
+            'moduleid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Module id where the post was created (not used anymore).',
+            ),
+            'coursemoduleid' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Course module id where the post was created.',
+            ),
+            'subject' => array(
+                'type' => PARAM_TEXT,
+                'null' => NULL_NOT_ALLOWED,
+                'description' => 'Post subject.',
+            ),
+            'summary' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post summary.',
+            ),
+            'content' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post content.',
+            ),
+            'uniquehash' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_NOT_ALLOWED,
+                'description' => 'Post unique hash.',
+            ),
+            'rating' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Post rating.',
+            ),
+            'format' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'Post content format.',
+            ),
+            'summaryformat' => array(
+                'choices' => array(FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN),
+                'type' => PARAM_INT,
+                'default' => FORMAT_MOODLE,
+                'description' => 'Format for the summary field.',
+            ),
+            'attachment' => array(
+                'type' => PARAM_RAW,
+                'null' => NULL_ALLOWED,
+                'description' => 'Post atachment.',
+            ),
+            'publishstate' => array(
+                'type' => PARAM_ALPHA,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 'draft',
+                'description' => 'Post publish state.',
+            ),
+            'lastmodified' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'When it was last modified.',
+            ),
+            'created' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_NOT_ALLOWED,
+                'default' => 0,
+                'description' => 'When it was created.',
+            ),
+            'usermodified' => array(
+                'type' => PARAM_INT,
+                'null' => NULL_ALLOWED,
+                'description' => 'User that updated the post.',
+            ),
+        );
+    }
+
+    protected static function define_related() {
+        return array(
+            'context' => 'context'
+        );
+    }
+
+    protected static function define_other_properties() {
+        return array(
+            'summaryfiles' => array(
+                'type' => external_files::get_properties_for_exporter(),
+                'multiple' => true
+            ),
+            'attachmentfiles' => array(
+                'type' => external_files::get_properties_for_exporter(),
+                'multiple' => true,
+                'optional' => true
+            ),
+        );
+    }
+
+    protected function get_other_values(renderer_base $output) {
+        $context = context_system::instance(); // Files always on site context.
+
+        $values['summaryfiles'] = external_util::get_area_files($context->id, 'blog', 'post', $this->data->id);
+        $values['attachmentfiles'] = external_util::get_area_files($context->id, 'blog', 'attachment', $this->data->id);
+
+        return $values;
+    }
+}
index 664d70a..a7b8c97 100644 (file)
@@ -71,8 +71,6 @@ if (isset($userid) && empty($courseid) && empty($modid)) {
 }
 $PAGE->set_context($context);
 
-$sitecontext = context_system::instance();
-
 if (isset($userid) && $USER->id == $userid) {
     $blognode = $PAGE->navigation->find('siteblog', null);
     if ($blognode) {
@@ -108,125 +106,20 @@ if (empty($CFG->enableblogs)) {
     print_error('blogdisable', 'blog');
 }
 
-// Add courseid if modid or groupid is specified: This is used for navigation and title.
-if (!empty($modid) && empty($courseid)) {
-    $courseid = $DB->get_field('course_modules', 'course', array('id' => $modid));
-}
-
-if (!empty($groupid) && empty($courseid)) {
-    $courseid = $DB->get_field('groups', 'courseid', array('id' => $groupid));
-}
-
-
-if (!$userid && has_capability('moodle/blog:view', $sitecontext) && $CFG->bloglevel > BLOG_USER_LEVEL) {
-    if ($entryid) {
-        if (!$entryobject = $DB->get_record('post', array('id' => $entryid))) {
-            print_error('nosuchentry', 'blog');
-        }
-        $userid = $entryobject->userid;
-    }
-} else if (!$userid) {
-    $userid = $USER->id;
-}
-
-if (!empty($modid)) {
-    if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
-        print_error(get_string('nocourseblogs', 'blog'));
-    }
-    if (!$mod = $DB->get_record('course_modules', array('id' => $modid))) {
-        print_error(get_string('invalidmodid', 'blog'));
-    }
-    $courseid = $mod->course;
-}
-
-if ((empty($courseid) ? true : $courseid == SITEID) && empty($userid)) {
-    if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
-        print_error('siteblogdisable', 'blog');
-    }
-    if (!has_capability('moodle/blog:view', $sitecontext)) {
-        print_error('cannotviewsiteblog', 'blog');
-    }
-
-    $COURSE = $DB->get_record('course', array('format' => 'site'));
-    $courseid = $COURSE->id;
-}
-
-if (!empty($courseid)) {
-    if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('invalidcourseid');
-    }
-
-    $courseid = $course->id;
-    require_login($course);
-
-    if (!has_capability('moodle/blog:view', $sitecontext)) {
-        print_error('cannotviewcourseblog', 'blog');
-    }
-} else {
-    $coursecontext = context_course::instance(SITEID);
-}
-
-if (!empty($groupid)) {
-    if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
-        print_error('groupblogdisable', 'blog');
-    }
-
-    if (! $group = groups_get_group($groupid)) {
-        print_error(get_string('invalidgroupid', 'blog'));
-    }
+list($courseid, $userid) = blog_validate_access($courseid, $modid, $groupid, $entryid, $userid);
 
-    if (!$course = $DB->get_record('course', array('id' => $group->courseid))) {
-        print_error('invalidcourseid');
-    }
+$courseid = (empty($courseid)) ? SITEID : $courseid;
 
-    $coursecontext = context_course::instance($course->id);
-    $courseid = $course->id;
+if ($courseid != SITEID) {
+    $course = get_course($courseid);
     require_login($course);
-
-    if (!has_capability('moodle/blog:view', $sitecontext)) {
-        print_error(get_string('cannotviewcourseorgroupblog', 'blog'));
-    }
-
-    if (groups_get_course_groupmode($course) == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) {
-        if (!groups_is_member($groupid)) {
-            print_error('notmemberofgroup');
-        }
-    }
 }
 
 if (!empty($userid)) {
-    if ($CFG->bloglevel < BLOG_USER_LEVEL) {
-        print_error('blogdisable', 'blog');
-    }
-
-    if (!$user = $DB->get_record('user', array('id' => $userid))) {
-        print_error('invaliduserid');
-    }
-
-    if ($user->deleted) {
-        echo $OUTPUT->header();
-        echo $OUTPUT->heading(get_string('userdeleted'));
-        echo $OUTPUT->footer();
-        die;
-    }
-
-    if ($USER->id == $userid) {
-        if (!has_capability('moodle/blog:create', $sitecontext)
-          && !has_capability('moodle/blog:view', $sitecontext)) {
-            print_error('donothaveblog', 'blog');
-        }
-    } else {
-        if (!has_capability('moodle/blog:view', $sitecontext) || !blog_user_can_view_user_entry($userid)) {
-            print_error('cannotviewcourseblog', 'blog');
-        }
-
-        $PAGE->navigation->extend_for_user($user);
-    }
+    $user = core_user::get_user($userid);
+    $PAGE->navigation->extend_for_user($user);
 }
 
-$courseid = (empty($courseid)) ? SITEID : $courseid;
-
-
 $blogheaders = blog_get_headers();
 
 $rsscontext = null;
index b81c645..f575ff7 100644 (file)
@@ -628,20 +628,29 @@ function blog_get_options_for_module($module, $user=null) {
  * It uses the current URL to build these variables.
  * A number of mutually exclusive use cases are used to structure this function.
  *
+ * @param  int $courseid   course id the the blog is associated to (can be null).
+ * @param  int $groupid    group id to filter blogs I can see (can be null)
+ * @param  int $userid     blog author id (can be null)
+ * @param  int $tagid      tag id to filter (can be null)
+ * @param  string $tag     tag name to filter (can be null)
+ * @param  int $modid      module id the blog is associated to (can be null).
+ * @param  int $entryid    blog entry id to filter(can be null)
+ * @param  string $search  string to search (can be null)
  * @return array
  */
-function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=null) {
+function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=null, $tag=null, $modid=null, $entryid=null,
+        $search = null) {
     global $CFG, $PAGE, $DB, $USER;
 
     $id       = optional_param('id', null, PARAM_INT);
-    $tag      = optional_param('tag', null, PARAM_NOTAGS);
+    $tag      = optional_param('tag', $tag, PARAM_NOTAGS);
     $tagid    = optional_param('tagid', $tagid, PARAM_INT);
     $userid   = optional_param('userid', $userid, PARAM_INT);
-    $modid    = optional_param('modid', null, PARAM_INT);
-    $entryid  = optional_param('entryid', null, PARAM_INT);
+    $modid    = optional_param('modid', $modid, PARAM_INT);
+    $entryid  = optional_param('entryid', $entryid, PARAM_INT);
     $groupid  = optional_param('groupid', $groupid, PARAM_INT);
     $courseid = optional_param('courseid', $courseid, PARAM_INT);
-    $search   = optional_param('search', null, PARAM_RAW);
+    $search   = optional_param('search', $search, PARAM_RAW);
     $action   = optional_param('action', null, PARAM_ALPHA);
     $confirm  = optional_param('confirm', false, PARAM_BOOL);
 
@@ -1166,3 +1175,131 @@ function blog_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx
     $rv->exclusiveurl = null;
     return $rv;
 }
+
+/**
+ * Validate the access to a blog.
+ *
+ * @param  int $courseid course id the the blog is associated to (can be null).
+ * @param  int $modid    module id the blog is associated to (can be null).
+ * @param  int $groupid  group id to filter blogs I can see (can be null)
+ * @param  int $entryid  blog entry id (can be null)
+ * @param  int $userid   blog author id (can be null)
+ * @return array with the calculated course and id
+ * @since  Moodle 3.6
+ */
+function blog_validate_access($courseid, $modid, $groupid, $entryid, $userid) {
+    global $CFG, $DB, $USER, $COURSE;
+
+    $sitecontext = context_system::instance();
+
+    // Add courseid if modid or groupid is specified: This is used for navigation and title.
+    if (!empty($modid) && empty($courseid)) {
+        $courseid = $DB->get_field('course_modules', 'course', array('id' => $modid));
+    }
+
+    if (!empty($groupid) && empty($courseid)) {
+        $courseid = $DB->get_field('groups', 'courseid', array('id' => $groupid));
+    }
+
+    if (!$userid && has_capability('moodle/blog:view', $sitecontext) && $CFG->bloglevel > BLOG_USER_LEVEL) {
+        if ($entryid) {
+            if (!$entryobject = $DB->get_record('post', array('id' => $entryid))) {
+                print_error('nosuchentry', 'blog');
+            }
+            $userid = $entryobject->userid;
+        }
+    } else if (!$userid) {
+        $userid = $USER->id;
+    }
+
+    if (!empty($modid)) {
+        if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
+            print_error(get_string('nocourseblogs', 'blog'));
+        }
+        if (!$mod = $DB->get_record('course_modules', array('id' => $modid))) {
+            print_error(get_string('invalidmodid', 'blog'));
+        }
+        $courseid = $mod->course;
+    }
+
+    if ((empty($courseid) ? true : $courseid == SITEID) && empty($userid)) {
+        if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
+            print_error('siteblogdisable', 'blog');
+        }
+        if (!has_capability('moodle/blog:view', $sitecontext)) {
+            print_error('cannotviewsiteblog', 'blog');
+        }
+
+        $COURSE = $DB->get_record('course', array('format' => 'site'));
+        $courseid = $COURSE->id;
+    }
+
+    if (!empty($courseid)) {
+        if (!$course = $DB->get_record('course', array('id' => $courseid))) {
+            print_error('invalidcourseid');
+        }
+
+        $courseid = $course->id;
+
+        if (!has_capability('moodle/blog:view', $sitecontext)) {
+            print_error('cannotviewcourseblog', 'blog');
+        }
+    } else {
+        $coursecontext = context_course::instance(SITEID);
+    }
+
+    if (!empty($groupid)) {
+        if ($CFG->bloglevel < BLOG_SITE_LEVEL) {
+            print_error('groupblogdisable', 'blog');
+        }
+
+        if (! $group = groups_get_group($groupid)) {
+            print_error(get_string('invalidgroupid', 'blog'));
+        }
+
+        if (!$course = $DB->get_record('course', array('id' => $group->courseid))) {
+            print_error('invalidcourseid');
+        }
+
+        $coursecontext = context_course::instance($course->id);
+        $courseid = $course->id;
+
+        if (!has_capability('moodle/blog:view', $sitecontext)) {
+            print_error(get_string('cannotviewcourseorgroupblog', 'blog'));
+        }
+
+        if (groups_get_course_groupmode($course) == SEPARATEGROUPS &&
+                !has_capability('moodle/site:accessallgroups', $coursecontext)) {
+
+            if (!groups_is_member($groupid)) {
+                print_error('notmemberofgroup');
+            }
+        }
+    }
+
+    if (!empty($userid)) {
+        if ($CFG->bloglevel < BLOG_USER_LEVEL) {
+            print_error('blogdisable', 'blog');
+        }
+
+        if (!$user = $DB->get_record('user', array('id' => $userid))) {
+            print_error('invaliduserid');
+        }
+
+        if ($user->deleted) {
+            print_error('userdeleted');
+        }
+
+        if ($USER->id == $userid) {
+            if (!has_capability('moodle/blog:create', $sitecontext)
+              && !has_capability('moodle/blog:view', $sitecontext)) {
+                print_error('donothaveblog', 'blog');
+            }
+        } else {
+            if (!has_capability('moodle/blog:view', $sitecontext) || !blog_user_can_view_user_entry($userid)) {
+                print_error('cannotviewcourseblog', 'blog');
+            }
+        }
+    }
+    return array($courseid, $userid);
+}
diff --git a/blog/tests/external_test.php b/blog/tests/external_test.php
new file mode 100644 (file)
index 0000000..016865c
--- /dev/null
@@ -0,0 +1,565 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for blog external API.
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/blog/locallib.php');
+require_once($CFG->dirroot . '/blog/lib.php');
+
+/**
+ * Unit tests for blog external API.
+ *
+ * @package    core_blog
+ * @copyright  2018 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_blog_external_testcase extends advanced_testcase {
+
+    private $courseid;
+    private $cmid;
+    private $userid;
+    private $groupid;
+    private $tagid;
+    private $postid;
+
+    protected function setUp() {
+        global $DB, $CFG;
+        parent::setUp();
+
+        $this->resetAfterTest();
+
+        // Create default course.
+        $course = $this->getDataGenerator()->create_course(array('category' => 1, 'shortname' => 'ANON'));
+        $this->assertNotEmpty($course);
+        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
+        $this->assertNotEmpty($page);
+
+        // Create default user.
+        $user = $this->getDataGenerator()->create_user(array(
+                'username' => 'testuser',
+                'firstname' => 'Jimmy',
+                'lastname' => 'Kinnon'
+        ));
+        // Enrol user.
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group->id, 'userid' => $user->id));
+
+        // Create default post.
+        $post = new stdClass();
+        $post->userid = $user->id;
+        $post->courseid = $course->id;
+        $post->groupid = $group->id;
+        $post->content = 'test post content text';
+        $post->module = 'blog';
+        $post->id = $DB->insert_record('post', $post);
+
+        core_tag_tag::set_item_tags('core', 'post', $post->id, context_user::instance($user->id), array('tag1'));
+        $tagid = $DB->get_field('tag', 'id', array('name' => 'tag1'));
+
+        // Grab important ids.
+        $this->courseid = $course->id;
+        $this->cmid = $page->cmid;
+        $this->userid  = $user->id;
+        $this->groupid  = $group->id;
+        $this->tagid  = $tagid;
+        $this->postid = $post->id;
+        $this->publishstate = 'site';   // To be override in tests.
+
+        // Set default blog level.
+        $CFG->bloglevel = BLOG_SITE_LEVEL;
+    }
+
+    /**
+     * Get global public entries even for not authenticated users.
+     * We get the entry since is public.
+     */
+    public function test_get_public_entries_global_level_by_non_logged_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+        $CFG->forcelogin = 0;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'public', array('id' => $this->postid));
+
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get global public entries even for not authenticated users in closed site.
+     */
+    public function test_get_public_entries_global_level_by_non_logged_users_closed_site() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+        $CFG->forcelogin = 1;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'public', array('id' => $this->postid));
+
+        $this->expectException('moodle_exception');
+        core_blog\external::get_entries();
+    }
+
+    /**
+     * Get global public entries for guest users.
+     * We get the entry since is public.
+     */
+    public function test_get_public_entries_global_level_by_guest_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'public', array('id' => $this->postid));
+
+        $this->setGuestUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get global not public entries even for not authenticated users withouth being authenticated.
+     * We don't get any because they are not public (restricted to site users).
+     */
+    public function test_get_not_public_entries_global_level_by_non_logged_users() {
+        global $CFG;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get global not public entries users being guest.
+     * We don't get any because they are not public (restricted to real site users).
+     */
+    public function test_get_not_public_entries_global_level_by_guest_user() {
+        global $CFG;
+
+        $CFG->bloglevel = BLOG_GLOBAL_LEVEL;
+
+        $this->setGuestUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get site not public entries for not authenticated users.
+     * We don't get any because they are not public (restricted to real site users).
+     */
+    public function test_get_not_public_entries_site_level_by_non_logged_users() {
+        $this->expectException('require_login_exception'); // In this case we get a security exception.
+        $result = core_blog\external::get_entries();
+    }
+
+    /**
+     * Get site not public entries for guest users.
+     * We don't get any because they are not public (restricted to real site users).
+     */
+    public function test_get_not_public_entries_site_level_by_guest_users() {
+
+        $this->setGuestUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get site entries at site level by system users.
+     */
+    public function test_get_site_entries_site_level_by_normal_users() {
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft site entries by authors.
+     */
+    public function test_get_draft_entries_site_level_by_author_users() {
+        global $DB;
+
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft site entries by not authors.
+     */
+    public function test_get_draft_entries_site_level_by_not_author_users() {
+        global $DB;
+
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setUser($user);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get draft site entries by admin.
+     */
+    public function test_get_draft_entries_site_level_by_admin_users() {
+        global $DB;
+
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setAdminUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft user entries by authors.
+     */
+    public function test_get_draft_entries_user_level_by_author_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_USER_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Get draft user entries by not authors.
+     */
+    public function test_get_draft_entries_user_level_by_not_author_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_USER_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setUser($user);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Get draft user entries by admin.
+     */
+    public function test_get_draft_entries_user_level_by_admin_users() {
+        global $CFG, $DB;
+
+        $CFG->bloglevel = BLOG_USER_LEVEL;
+        // Set current entry global.
+        $DB->set_field('post', 'publishstate', 'draft', array('id' => $this->postid));
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->setAdminUser();
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Test get all entries including testing pagination.
+     */
+    public function test_get_all_entries_including_pagination() {
+        global $DB, $USER;
+
+        $DB->set_field('post', 'publishstate', 'site', array('id' => $this->postid));
+
+        // Create another entry.
+        $this->setAdminUser();
+        $newpost = new stdClass();
+        $newpost->userid = $USER->id;
+        $newpost->content = 'test post content text';
+        $newpost->module = 'blog';
+        $newpost->publishstate = 'site';
+        $newpost->created = time() + HOURSECS;
+        $newpost->lastmodified = time() + HOURSECS;
+        $newpost->id = $DB->insert_record('post', $newpost);
+
+        $this->setUser($this->userid);
+        $result = core_blog\external::get_entries();
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(2, $result['entries']);
+        $this->assertEquals(2, $result['totalentries']);
+
+        $result = core_blog\external::get_entries(array(), 0, 1);
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(2, $result['totalentries']);
+        $this->assertEquals($newpost->id, $result['entries'][0]['id']);
+
+        $result = core_blog\external::get_entries(array(), 1, 1);
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        $this->assertEquals(2, $result['totalentries']);
+        $this->assertEquals($this->postid, $result['entries'][0]['id']);
+    }
+
+    /**
+     * Test get entries filtering by course.
+     */
+    public function test_get_entries_filtering_by_course() {
+        global $CFG, $DB;
+
+        $DB->set_field('post', 'publishstate', 'site', array('id' => $this->postid));
+
+        $this->setAdminUser();
+        $coursecontext = context_course::instance($this->courseid);
+        $anothercourse = $this->getDataGenerator()->create_course();
+
+        // Add blog associations with a course.
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($coursecontext->id);
+
+        // There is one entry associated with a course.
+        $result = core_blog\external::get_entries(array(array('name' => 'courseid', 'value' => $this->courseid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+
+        // There is no entry associated with a wrong course.
+        $result = core_blog\external::get_entries(array(array('name' => 'courseid', 'value' => $anothercourse->id)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+
+        // There is no entry associated with a module.
+        $result = core_blog\external::get_entries(array(array('name' => 'cmid', 'value' => $this->cmid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by module.
+     */
+    public function test_get_entries_filtering_by_module() {
+        global $CFG, $DB;
+
+        $DB->set_field('post', 'publishstate', 'site', array('id' => $this->postid));
+
+        $this->setAdminUser();
+        $coursecontext = context_course::instance($this->courseid);
+        $contextmodule = context_module::instance($this->cmid);
+        $anothermodule = $this->getDataGenerator()->create_module('page', array('course' => $this->courseid));
+
+        // Add blog associations with a module.
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($contextmodule->id);
+
+        // There is no entry associated with a course.
+        $result = core_blog\external::get_entries(array(array('name' => 'courseid', 'value' => $this->courseid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+
+        // There is one entry associated with a module.
+        $result = core_blog\external::get_entries(array(array('name' => 'cmid', 'value' => $this->cmid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+
+        // There is no entry associated with a wrong module.
+        $result = core_blog\external::get_entries(array(array('name' => 'cmid', 'value' => $anothermodule->cmid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by author.
+     */
+    public function test_get_entries_filtering_by_author() {
+        $this->setAdminUser();
+        // Filter by author.
+        $result = core_blog\external::get_entries(array(array('name' => 'userid', 'value' => $this->userid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // No author.
+        $anotheruser = $this->getDataGenerator()->create_user();
+        $result = core_blog\external::get_entries(array(array('name' => 'userid', 'value' => $anotheruser->id)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by entry.
+     */
+    public function test_get_entries_filtering_by_entry() {
+        $this->setAdminUser();
+        // Filter by correct entry.
+        $result = core_blog\external::get_entries(array(array('name' => 'entryid', 'value' => $this->postid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent entry.
+        $this->expectException('moodle_exception');
+        $result = core_blog\external::get_entries(array(array('name' => 'entryid', 'value' => -1)));
+    }
+
+    /**
+     * Test get entries filtering by search.
+     */
+    public function test_get_entries_filtering_by_search() {
+        $this->setAdminUser();
+        // Filter by correct search.
+        $result = core_blog\external::get_entries(array(array('name' => 'search', 'value' => 'test')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent search.
+        $result = core_blog\external::get_entries(array(array('name' => 'search', 'value' => 'abc')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by tag.
+     */
+    public function test_get_entries_filtering_by_tag() {
+        $this->setAdminUser();
+        // Filter by correct tag.
+        $result = core_blog\external::get_entries(array(array('name' => 'tag', 'value' => 'tag1')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Create tag.
+        $tag = $this->getDataGenerator()->create_tag(array('userid' => $this->userid, 'name' => 'tag2',
+            'isstandard' => 1));
+
+        $result = core_blog\external::get_entries(array(array('name' => 'tag', 'value' => 'tag2')));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by tag id.
+     */
+    public function test_get_entries_filtering_by_tagid() {
+        $this->setAdminUser();
+        // Filter by correct tag.
+        $result = core_blog\external::get_entries(array(array('name' => 'tagid', 'value' => $this->tagid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent tag.
+
+        // Create tag.
+        $tag = $this->getDataGenerator()->create_tag(array('userid' => $this->userid, 'name' => 'tag2',
+            'isstandard' => 1));
+
+        $result = core_blog\external::get_entries(array(array('name' => 'tagid', 'value' => $tag->id)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by group.
+     */
+    public function test_get_entries_filtering_by_group() {
+        $this->setAdminUser();
+        // Add blog associations with a course.
+        $coursecontext = context_course::instance($this->courseid);
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($coursecontext->id);
+
+        // Filter by correct group.
+        $result = core_blog\external::get_entries(array(array('name' => 'groupid', 'value' => $this->groupid)));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+        // Non-existent group.
+        $anotheruser = $this->getDataGenerator()->create_user();
+        $this->expectException('moodle_exception');
+        core_blog\external::get_entries(array(array('name' => 'groupid', 'value' => -1)));
+    }
+
+    /**
+     * Test get entries multiple filter.
+     */
+    public function test_get_entries_multiple_filter() {
+        $this->setAdminUser();
+        // Add blog associations with a course.
+        $coursecontext = context_course::instance($this->courseid);
+        $blog = new blog_entry($this->postid);
+        $blog->add_association($coursecontext->id);
+
+        $result = core_blog\external::get_entries(array(
+            array('name' => 'tagid', 'value' => $this->tagid),
+            array('name' => 'userid', 'value' => $this->userid),
+        ));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(1, $result['entries']);
+
+        // Non-existent multiple filter.
+        $result = core_blog\external::get_entries(array(
+            array('name' => 'search', 'value' => 'www'),
+            array('name' => 'userid', 'value' => $this->userid),
+        ));
+        $result = external_api::clean_returnvalue(core_blog\external::get_entries_returns(), $result);
+        $this->assertCount(0, $result['entries']);
+    }
+
+    /**
+     * Test get entries filtering by invalid_filter.
+     */
+    public function test_get_entries_filtering_by_invalid_filter() {
+        $this->setAdminUser();
+        // Filter by incorrect filter.
+        $this->expectException('moodle_exception');
+        $result = core_blog\external::get_entries(array(array('name' => 'zzZZzz', 'value' => 'wwWWww')));
+    }
+
+    /**
+     * Test get entries when blog is disabled.
+     */
+    public function test_get_entries_blog_disabled() {
+        global $CFG;
+
+        $this->setAdminUser();
+        $CFG->enableblogs = 0;
+        // Filter by incorrect filter.
+        $this->expectException('moodle_exception');
+        $result = core_blog\external::get_entries(array(array('name' => 'zzZZzz', 'value' => 'wwWWww')));
+    }
+}
+
index 3fbf4e9..8446142 100644 (file)
@@ -72,7 +72,7 @@ class activity_navigation implements renderable, templatable {
             }
 
             $attributes = [
-                'classes' => 'btn btn-link',
+                'class' => 'btn btn-link',
                 'id' => 'prev-activity-link',
                 'title' => $linkname,
             ];
@@ -88,7 +88,7 @@ class activity_navigation implements renderable, templatable {
             }
 
             $attributes = [
-                'classes' => 'btn btn-link',
+                'class' => 'btn btn-link',
                 'id' => 'next-activity-link',
                 'title' => $linkname,
             ];
index 95c19a8..0045cb6 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['broughtbymoodle'] = 'This data export provided by Moodle';
+$string['exportfrom'] = 'Exported from {$a}';
+$string['exporttime'] = 'Exported on {$a}';
+$string['exportuser'] = 'Data for {$a}';
 $string['privacy:metadata'] = 'The privacy subsystem does not store any data of its own and is designed to act as a channel between components and the interface used to describe, export, and remove their data.';
 $string['trace:done'] = 'Complete';
 $string['trace:exportcomplete'] = 'Export complete';
@@ -32,4 +36,6 @@ $string['trace:processingcomponent'] = 'Processing {$a->component} ({$a->progres
 $string['trace:fetchcomponents'] = 'Fetching {$a->total} components ({$a->datetime})';
 $string['trace:deletingapproved'] = 'Performing removal of approved {$a->total} contexts ({$a->datetime})';
 $string['trace:deletingcontext'] = 'Performing removal of context from {$a->total} components ({$a->datetime})';
+$string['navigation'] = 'Navigation';
 $string['privacy:subsystem:empty'] = 'This subsystem does not store any data.';
+$string['viewdata'] = 'Click on a link in the navigation to view data.';
index 8d52c61..077b92c 100644 (file)
Binary files a/lib/amd/build/tree.min.js and b/lib/amd/build/tree.min.js differ
index 0938056..24fd7da 100644 (file)
@@ -70,6 +70,10 @@ define(['jquery'], function($) {
         this.bindEventHandlers();
     };
 
+    Tree.prototype.registerEnterCallback = function(callback) {
+        this.enterCallback = callback;
+    };
+
     /**
      * Find all visible tree items and save a cache of them on the tree object.
      *
@@ -136,7 +140,13 @@ define(['jquery'], function($) {
      * @returns {bool}
      */
     Tree.prototype.getGroupFromItem = function(item) {
-        return this.treeRoot.find('#' + item.attr('aria-owns')) || item.children('[role=group]');
+        var ariaowns = this.treeRoot.find('#' + item.attr('aria-owns'));
+        var plain = item.children('[role=group]');
+        if (ariaowns.length > plain.length) {
+            return ariaowns;
+        } else {
+            return plain;
+        }
     };
 
     /**
@@ -371,7 +381,12 @@ define(['jquery'], function($) {
             case this.keys.enter: {
                 var links = item.children('a').length ? item.children('a') : item.children().not(SELECTORS.GROUP).find('a');
                 if (links.length) {
-                    window.location.href = links.first().attr('href');
+                    // See if we have a callback.
+                    if (typeof this.enterCallback === 'function') {
+                        this.enterCallback(item);
+                    } else {
+                        window.location.href = links.first().attr('href');
+                    }
                 } else if (this.isGroupItem(item)) {
                     this.toggleGroup(item, true);
                 }
index 2334240..4893b41 100644 (file)
@@ -74,6 +74,15 @@ $functions = array(
         'capabilities'  => 'moodle/badges:viewotherbadges',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_blog_get_entries' => array(
+        'classname'   => 'core_blog\external',
+        'methodname'  => 'get_entries',
+        'description' => 'Returns blog entries.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax'          => true,
+        'loginrequired' => false,
+    ),
     'core_calendar_get_calendar_monthly_view' => array(
         'classname' => 'core_calendar_external',
         'methodname' => 'get_calendar_monthly_view',
index d0840bd..e339c5c 100644 (file)
@@ -13,4 +13,6 @@ Description of import of various jQuery libraries into Moodle:
 
 5/ open http://127.0.0.1/lib/tests/other/jquerypage.php
 
+6/ Update the version of jquery in core_privacy\local\request\moodle_content_writer::write_html_data()
+
 Petr Skoda
index 17ce5e7..8d2a465 100644 (file)
@@ -2,3 +2,4 @@ Description of import into Moodle:
 // Download from https://requirejs.org/docs/download.html
 // Put the require.js and require.min.js and LICENSE file in this folder.
 // Check if MDL-60458 workaround can be removed.
+// Check that core_privacy\local\request\moodle_content_writer::write_html_data() does not need to be updated.
index 27a7d56..fc5e274 100644 (file)
@@ -98,9 +98,10 @@ class provider implements metadataprovider, \mod_assign\privacy\assignsubmission
         $context = $exportdata->get_context();
         if (!empty($editortext)) {
             $submissiontext = new \stdClass();
-            $submissiontext->text = writer::with_context($context)->rewrite_pluginfile_urls([], '', '', '', $editortext);
             $currentpath = $exportdata->get_subcontext();
             $currentpath[] = get_string('privacy:path', 'assignsubmission_onlinetext');
+            $submissiontext->text = writer::with_context($context)->rewrite_pluginfile_urls($currentpath,
+                    'assignsubmission_onlinetext', 'submissions_onlinetext', $submission->id, $editortext);
             writer::with_context($context)
                     ->export_area_files($currentpath, 'assignsubmission_onlinetext', 'submissions_onlinetext', $submission->id)
                     // Add the text to the exporter.
index 69bf314..04a5f30 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-debug.js differ
index d7e49f0..43c3535 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js differ
index 4c81466..92803c4 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js and b/mod/quiz/yui/build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js differ
index f9ef84b..03c4a30 100644 (file)
@@ -109,7 +109,6 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
         var drag = e.drag;
         // Get a reference to our drag node
         var dragnode = drag.get('node');
-        var dropnode = e.drop.get('node');
 
         // Add spinner if it not there
         var actionarea = dragnode.one(CSS.ACTIONAREA);
@@ -131,7 +130,7 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
         params['class'] = 'resource';
         params.field = 'move';
         params.id = Number(Y.Moodle.mod_quiz.util.slot.getId(dragnode));
-        params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor('li.section', true));
+        params.sectionId = Y.Moodle.core_course.util.section.getId(dragnode.ancestor('li.section', true));
 
         var previousslot = dragnode.previous(SELECTOR.SLOT);
         if (previousslot) {
index bf25a06..21504fe 100644 (file)
@@ -52,6 +52,11 @@ class moodle_content_writer implements content_writer {
      */
     protected $files = [];
 
+    /**
+     * @var array The list of plugins that have been checked to see if they are installed.
+     */
+    protected $checkedplugins = [];
+
     /**
      * Constructor for the content writer.
      *
@@ -162,7 +167,17 @@ class moodle_content_writer implements content_writer {
      * @return  string                      The processed string
      */
     public function rewrite_pluginfile_urls(array $subcontext, $component, $filearea, $itemid, $text) : string {
-        return str_replace('@@PLUGINFILE@@/', $this->get_files_target_url($component, $filearea, $itemid).'/', $text);
+        // Need to take into consideration the subcontext to provide the full path to this file.
+        $subcontextpath = '';
+        if (!empty($subcontext)) {
+            $subcontextpath = DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $subcontext);
+        }
+        $path = $this->get_context_path();
+        $path = implode(DIRECTORY_SEPARATOR, $path) . $subcontextpath;
+        $returnstring = $path . DIRECTORY_SEPARATOR . $this->get_files_target_url($component, $filearea, $itemid) . '/';
+        $returnstring = clean_param($returnstring, PARAM_PATH);
+
+        return str_replace('@@PLUGINFILE@@/', $returnstring, $text);
     }
 
     /**
@@ -245,7 +260,7 @@ class moodle_content_writer implements content_writer {
         $contexts = array_reverse($this->context->get_parent_contexts(true));
         foreach ($contexts as $context) {
             $name = $context->get_context_name();
-            $id = $context->id;
+            $id = '_.' . $context->id;
             $path[] = shorten_filename(clean_param("{$name} {$id}", PARAM_FILE), MAX_FILENAME_SIZE, true);
         }
 
@@ -263,6 +278,24 @@ class moodle_content_writer implements content_writer {
         $subcontext = shorten_filenames($subcontext, MAX_FILENAME_SIZE, true);
         $name = shorten_filename($name, MAX_FILENAME_SIZE, true);
 
+        // This weird code is to look for a subcontext that contains a number and append an '_' to the front.
+        // This is because there seems to be some weird problem with array_merge_recursive used in finalise_content().
+        $subcontext = array_map(function($data) {
+            if (stripos($data, DIRECTORY_SEPARATOR) !== false) {
+                $newpath = explode(DIRECTORY_SEPARATOR, $data);
+                $newpath = array_map(function($value) {
+                    if (is_numeric($value)) {
+                        return '_' . $value;
+                    }
+                    return $value;
+                }, $newpath);
+                return implode(DIRECTORY_SEPARATOR, $newpath);
+            } else if (is_numeric($data)) {
+                $data = '_' . $data;
+            }
+            return $data;
+        }, $subcontext);
+
         // Combine the context path, and the subcontext data.
         $path = array_merge(
             $this->get_context_path(),
@@ -331,7 +364,7 @@ class moodle_content_writer implements content_writer {
         $parts = ['_files', $filearea];
 
         if (!empty($itemid)) {
-            $parts[] = $itemid;
+            $parts[] = '_' . $itemid;
         }
 
         return implode('/', $parts);
@@ -350,12 +383,308 @@ class moodle_content_writer implements content_writer {
         $this->files[$path] = $targetpath;
     }
 
+    /**
+     * Copy a file to the specified path.
+     *
+     * @param  array  $path        Current location of the file.
+     * @param  array  $destination Destination path to copy the file to.
+     */
+    protected function copy_data(array $path, array $destination) {
+        // Do we not have a moodle function to do something like this?
+        $systempath = getcwd();
+        // This is likely to be running from admin/cli.
+        if (stripos($systempath, 'admin' . DIRECTORY_SEPARATOR . 'cli') !== false) {
+            $bits = explode('admin' . DIRECTORY_SEPARATOR . 'cli', $systempath);
+            $systempath = implode('', $bits);
+        }
+        $filename = array_pop($destination);
+        $destdirectory = implode(DIRECTORY_SEPARATOR, $destination);
+        $fulldestination = $this->path . DIRECTORY_SEPARATOR . $destdirectory;
+        check_dir_exists($fulldestination, true, true);
+        $fulldestination .= $filename;
+        $currentpath = $systempath . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $path);
+        copy($currentpath, $fulldestination);
+        $this->files[$destdirectory . DIRECTORY_SEPARATOR . $filename] = $fulldestination;
+    }
+
+    /**
+     * This creates three different bits of data from all of the files that will be
+     * exported.
+     * $tree - A multidimensional array of the navigation tree structure.
+     * $treekey - An array with the short path of the file and element data for
+     *            html (data_file_{number} or 'No var')
+     * $allfiles - All *.json files that need to be added as an index to be referenced
+     *             by the js files to display the user data.
+     *
+     * @return array returns a tree, tree key, and a list of all files.
+     */
+    protected function prepare_for_export() : Array {
+        $tree = [];
+        $treekey = [];
+        $allfiles = [];
+        $i = 1;
+        foreach ($this->files as $shortpath => $fullfile) {
+
+            // Generate directory tree as an associative array.
+            $items = explode(DIRECTORY_SEPARATOR, $shortpath);
+            $newitems = $this->condense_array($items);
+            $tree = array_merge_recursive($tree, $newitems);
+
+            if (is_string($fullfile)) {
+                $filearray = explode(DIRECTORY_SEPARATOR, $shortpath);
+                $filename = array_pop($filearray);
+                $filenamearray = explode('.', $filename);
+                // Don't process files that are not json files.
+                if (end($filenamearray) !== 'json') {
+                    continue;
+                }
+                // Chop the last two characters of the extension. json => js.
+                $filename = substr($filename, 0, -2);
+                array_push($filearray, $filename);
+                $newshortpath = implode(DIRECTORY_SEPARATOR, $filearray);
+
+                $varname = 'data_file_' . $i;
+                $i++;
+
+                $quicktemp = clean_param($shortpath, PARAM_PATH);
+                $treekey[$quicktemp] = $varname;
+                $allfiles[$varname] = clean_param($newshortpath, PARAM_PATH);
+
+                // Need to load up the current json file and add a variable (varname mentioned above) at the start.
+                // Then save it as a js file.
+                $content = $this->get_file_content($fullfile);
+                $jsondecodedcontent = json_decode($content);
+                $jsonencodedcontent = json_encode($jsondecodedcontent, JSON_PRETTY_PRINT);
+                $variablecontent = 'var ' . $varname . ' = ' . $jsonencodedcontent;
+
+                $this->write_data($newshortpath, $variablecontent);
+            } else {
+                $treekey[$shortpath] = 'No var';
+            }
+        }
+        return [$tree, $treekey, $allfiles];
+    }
+
+    /**
+     * Add more detail to the tree to help with sorting and display in the renderer.
+     *
+     * @param  array  $tree       The file structure currently as a multidimensional array.
+     * @param  array  $treekey    An array of the current file paths.
+     * @param  array  $currentkey The current short path of the tree.
+     * @return array An array of objects that has additional data.
+     */
+    protected function make_tree_object(array $tree, array $treekey, array $currentkey = []) : Array {
+        $newtree = [];
+        // Try to extract the context id and then add the context object.
+        $addcontext = function($index, $object) {
+            if (stripos($index, '_.') !== false) {
+                $namearray = explode('_.', $index);
+                $contextid = array_pop($namearray);
+                if (is_numeric($contextid)) {
+                    $object[$index]->name = implode('_.', $namearray);
+                    $object[$index]->context = \context::instance_by_id($contextid);
+                }
+            } else {
+                $object[$index]->name = $index;
+            }
+        };
+        // Just add the final data to the tree object.
+        $addfinalfile = function($directory, $treeleaf, $file) use ($treekey) {
+            $url = implode(DIRECTORY_SEPARATOR, $directory);
+            $url = clean_param($url, PARAM_PATH);
+            $treeleaf->name = $file;
+            $treeleaf->itemtype = 'item';
+            $gokey = $url . DIRECTORY_SEPARATOR . $file;
+            if (isset($treekey[$gokey]) && $treekey[$gokey] !== 'No var') {
+                $treeleaf->datavar = $treekey[$gokey];
+            } else {
+                $treeleaf->url = new \moodle_url($url . DIRECTORY_SEPARATOR . $file);
+            }
+        };
+
+        foreach ($tree as $key => $value) {
+            $newtree[$key] = new \stdClass();
+            if (is_array($value)) {
+                $newtree[$key]->itemtype = 'treeitem';
+                // The array merge recursive adds a numeric index, and so we only add to the current
+                // key if it is now numeric.
+                $currentkey = is_numeric($key) ? $currentkey : array_merge($currentkey, [$key]);
+
+                // Try to extract the context id and then add the context object.
+                $addcontext($key, $newtree);
+                $newtree[$key]->children = $this->make_tree_object($value, $treekey, $currentkey);
+
+                if (!is_numeric($key)) {
+                    // We're heading back down the tree, so remove the last key.
+                    array_pop($currentkey);
+                }
+            } else {
+                // If the key is not numeric then we want to add a directory and put the file under that.
+                if (!is_numeric($key)) {
+                    $newtree[$key]->itemtype = 'treeitem';
+                    // Try to extract the context id and then add the context object.
+                    $addcontext($key, $newtree);
+                     array_push($currentkey, $key);
+
+                    $newtree[$key]->children[$value] = new \stdClass();
+                    $addfinalfile($currentkey, $newtree[$key]->children[$value], $value);
+                    array_pop($currentkey);
+                } else {
+                    // If the key is just a number then we just want to show the file instead.
+                    $addfinalfile($currentkey, $newtree[$key], $value);
+                }
+            }
+        }
+        return $newtree;
+    }
+
+    /**
+     * Sorts the tree list into an order that makes more sense.
+     * Order is:
+     * 1 - Items with a context first, the lower the number the higher up the tree.
+     * 2 - Items that are directories.
+     * 3 - Items that are log directories.
+     * 4 - Links to a page.
+     *
+     * @param  array $tree The tree structure to order.
+     */
+    protected function sort_my_list(array &$tree) {
+        uasort($tree, function($a, $b) {
+            if (isset($a->context) && isset($b->context)) {
+                return $a->context->contextlevel <=> $b->context->contextlevel;
+            }
+            if (isset($a->context) && !isset($b->context)) {
+                return -1;
+            }
+            if (isset($b->context) && !isset($a->context)) {
+                return 1;
+            }
+            if ($a->itemtype == 'treeitem' && $b->itemtype == 'treeitem') {
+                // Ugh need to check that this plugin has not been uninstalled.
+                if ($this->check_plugin_is_installed('tool_log')) {
+                    if (trim($a->name) == get_string('privacy:path:logs', 'tool_log')) {
+                        return 1;
+                    } else if (trim($b->name) == get_string('privacy:path:logs', 'tool_log')) {
+                        return -1;
+                    }
+                    return 0;
+                }
+            }
+            if ($a->itemtype == 'treeitem' && $b->itemtype == 'item') {
+                return -1;
+            }
+            if ($b->itemtype == 'treeitem' && $a->itemtype == 'item') {
+                return 1;
+            }
+            return 0;
+        });
+        foreach ($tree as $treeobject) {
+            if (isset($treeobject->children)) {
+                $this->sort_my_list($treeobject->children);
+            }
+        }
+    }
+
+    /**
+     * Check to see if a specified plugin is installed.
+     *
+     * @param  string $component The component name e.g. tool_log
+     * @return bool Whether this component is installed.
+     */
+    protected function check_plugin_is_installed(string $component) : Bool {
+        if (!isset($this->checkedplugins[$component])) {
+            $pluginmanager = \core_plugin_manager::instance();
+            $plugin = $pluginmanager->get_plugin_info($component);
+            $this->checkedplugins[$component] = !is_null($plugin);
+        }
+        return $this->checkedplugins[$component];
+    }
+
+    /**
+     * Writes the appropriate files for creating an HTML index page for human navigation of the user data export.
+     */
+    protected function write_html_data() {
+        global $PAGE, $SITE, $USER, $CFG;
+
+        // Do this first before adding more files to $this->files.
+        list($tree, $treekey, $allfiles) = $this->prepare_for_export();
+        // Add more detail to the tree such as contexts.
+        $richtree = $this->make_tree_object($tree, $treekey);
+        // Now that we have more detail we can use that to sort it.
+        $this->sort_my_list($richtree);
+
+        // Copy over the JavaScript required to display the html page.
+        $jspath = ['privacy', 'export_files', 'general.js'];
+        $targetpath = ['js', 'general.js'];
+        $this->copy_data($jspath, $targetpath);
+
+        $jquery = ['lib', 'jquery', 'jquery-3.2.1.min.js'];
+        $jquerydestination = ['js', 'jquery-3.2.1.min.js'];
+        $this->copy_data($jquery, $jquerydestination);
+
+        $requirecurrentpath = ['lib', 'requirejs', 'require.min.js'];
+        $destination = ['js', 'require.min.js'];
+        $this->copy_data($requirecurrentpath, $destination);
+
+        $treepath = ['lib', 'amd', 'build', 'tree.min.js'];
+        $destination = ['js', 'tree.min.js'];
+        $this->copy_data($treepath, $destination);
+
+        // Icons to be used.
+        $expandediconpath = ['pix', 't', 'expanded.svg'];
+        $this->copy_data($expandediconpath, ['pix', 'expanded.svg']);
+        $collapsediconpath = ['pix', 't', 'collapsed.svg'];
+        $this->copy_data($collapsediconpath, ['pix', 'collapsed.svg']);
+        $naviconpath = ['pix', 'i', 'navigationitem.svg'];
+        $this->copy_data($naviconpath, ['pix', 'navigationitem.svg']);
+        $moodleimgpath = ['pix', 'moodlelogo.svg'];
+        $this->copy_data($moodleimgpath, ['pix', 'moodlelogo.svg']);
+
+        // Additional required css.
+        // Determine what direction to show the data export page according to the user preference.
+        $rtl = right_to_left();
+        if (!$rtl) {
+            $bootstrapdestination = 'bootstrap.min.css';
+            $this->write_url_content('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css',
+                    $bootstrapdestination);
+        } else {
+            $rtldestination = 'rtlbootstrap.min.css';
+            $this->write_url_content('https://cdn.rtlcss.com/bootstrap/v4.0.0/css/bootstrap.min.css', $rtldestination);
+        }
+
+        $csspath = ['privacy', 'export_files', 'general.css'];
+        $destination = ['general.css'];
+        $this->copy_data($csspath, $destination);
+
+        // Create an index file that lists all, to be newly created, js files.
+        $encoded = json_encode($allfiles,  JSON_PRETTY_PRINT);
+        $encoded = 'var user_data_index = ' . $encoded;
+
+        $path = 'js' . DIRECTORY_SEPARATOR . 'data_index.js';
+        $this->write_data($path, $encoded);
+
+        $output = $PAGE->get_renderer('core_privacy');
+        $navigationpage = new \core_privacy\output\exported_navigation_page(current($richtree));
+        $navigationhtml = $output->render_navigation($navigationpage);
+
+        $systemname = $SITE->fullname;
+        $fullusername = fullname($USER);
+        $siteurl = $CFG->wwwroot;
+
+        // Create custom index.html file.
+        $htmlpage = new \core_privacy\output\exported_html_page($navigationhtml, $systemname, $fullusername, $rtl, $siteurl);
+        $outputpage = $output->render_html_page($htmlpage);
+        $this->write_data('index.html', $outputpage);
+    }
+
     /**
      * Perform any required finalisation steps and return the location of the finalised export.
      *
      * @return  string
      */
     public function finalise_content() : string {
+        $this->write_html_data();
+
         $exportfile = make_request_directory() . '/export.zip';
 
         $fp = get_file_packer();
@@ -366,4 +695,53 @@ class moodle_content_writer implements content_writer {
 
         return $exportfile;
     }
+
+    /**
+     * Creates a multidimensional array out of array elements.
+     *
+     * @param  array  $array Array which items are to be condensed into a multidimensional array.
+     * @return array The multidimensional array.
+     */
+    protected function condense_array(array $array) : Array {
+        if (count($array) === 2) {
+            return [$array[0] => $array[1]];
+        }
+        if (isset($array[0])) {
+            return [$array[0] => $this->condense_array(array_slice($array, 1))];
+        }
+        return [];
+    }
+
+    /**
+     * Get the contents of a file.
+     *
+     * @param  string $filepath The file path.
+     * @return string contents of the file.
+     */
+    protected function get_file_content(string $filepath) : String {
+        $filepointer = fopen($filepath, 'r');
+        $content = '';
+        while (!feof($filepointer)) {
+            $content .= fread($filepointer, filesize($filepath));
+        }
+        return $content;
+    }
+
+    /**
+     * Write url files to the export.
+     *
+     * @param  string $url  Url of the file.
+     * @param  string $path Path to store the file.
+     */
+    protected function write_url_content(string $url, string $path) {
+        $filepointer = fopen($url, 'r');
+        $targetpath = $this->path . DIRECTORY_SEPARATOR . $path;
+        check_dir_exists(dirname($targetpath), true, true);
+        $status = file_put_contents($targetpath, $filepointer);
+        if ($status === false) {
+            // There was an error. Throw an exception to allow the download status to remain as requiring download.
+            throw new \moodle_exception('Content download was incomplete');
+        }
+        $this->files[$path] = $targetpath;
+    }
 }
diff --git a/privacy/classes/output/exported_html_page.php b/privacy/classes/output/exported_html_page.php
new file mode 100644 (file)
index 0000000..8dbea89
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the navigation renderable for user data exports.
+ *
+ * @package    core_privacy
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_privacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+
+/**
+ * Class containing the navigation renderable
+ *
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class exported_html_page implements renderable, templatable {
+
+    /** @var string $navigationdata navigation html to be displayed about the system. */
+    protected $navigationdata;
+
+    /** @var string $systemname systemname for the page. */
+    protected $systemname;
+
+    /** @var string $username The full name of the user. */
+    protected $username;
+
+    /** @var bool $rtl The direction to show the page (right to left) */
+    protected $rtl;
+
+    /** @var string $siteurl The url back to the site that created this export. */
+    protected $siteurl;
+
+    /**
+     * Constructor.
+     *
+     * @param string $navigationdata Navigation html to be displayed about the system.
+     * @param string $systemname systemname for the page.
+     * @param string $username The full name of the user.
+     * @param bool $righttoleft Is the language used right to left?
+     * @param string $siteurl The url to the site that created this export.
+     */
+    public function __construct(string $navigationdata, string $systemname, string $username, bool $righttoleft, string $siteurl) {
+        $this->navigationdata = $navigationdata;
+        $this->systemname = $systemname;
+        $this->username = $username;
+        $this->rtl = $righttoleft;
+        $this->siteurl = $siteurl;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return array
+     */
+    public function export_for_template(renderer_base $output) : Array {
+        return [
+            'navigation' => $this->navigationdata,
+            'systemname' => $this->systemname,
+            'timegenerated' => time(),
+            'username' => $this->username,
+            'righttoleft' => $this->rtl,
+            'siteurl' => $this->siteurl
+        ];
+    }
+}
\ No newline at end of file
diff --git a/privacy/classes/output/exported_navigation_page.php b/privacy/classes/output/exported_navigation_page.php
new file mode 100644 (file)
index 0000000..49819dd
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the navigation renderable for user data exports.
+ *
+ * @package    core_privacy
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_privacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+
+/**
+ * Class containing the navigation renderable
+ *
+ * @copyright  2018 Adrian Greeve
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class exported_navigation_page implements renderable, templatable {
+
+    /** @var array $tree Full tree in multidimensional form. */
+    protected $tree;
+
+    /** @var boolean $firstelement This is used to create unique classes for the first elements in the navigation tree. */
+    protected $firstelement = true;
+
+    /**
+     * Constructor
+     *
+     * @param \stdClass $tree Full tree to create navigation out of.
+     */
+    public function __construct(\stdClass $tree) {
+        $this->tree = $tree;
+    }
+
+    /**
+     * Creates the navigation list html. Why this and not a template? My attempts at trying to get a recursive template
+     * working failed.
+     *
+     * @param  \stdClass $tree Full tree to create navigation out of.
+     * @return string navigation html.
+     */
+    protected function create_navigation(\stdClass $tree) {
+        if ($this->firstelement) {
+            $html = \html_writer::start_tag('ul', ['class' => 'treeview parent block_tree list', 'id' => 'my-tree']);
+            $this->firstelement = false;
+        } else {
+            $html = \html_writer::start_tag('ul', ['class' => 'parent', 'role' => 'group']);
+        }
+        foreach ($tree->children as $child) {
+            if (isset($child->children)) {
+                $html .= \html_writer::start_tag('li', ['class' => 'menu-item', 'role' => 'treeitem', 'aria-expanded' => 'false']);
+                $html .= $child->name;
+                $html .= $this->create_navigation($child);
+            } else {
+                $html .= \html_writer::start_tag('li', ['class' => 'item', 'role' => 'treeitem', 'aria-expanded' => 'false']);
+                // Normal display.
+                if (isset($child->datavar)) {
+                    $html .= \html_writer::link('#', $child->name, ['data-var' => $child->datavar]);
+                } else {
+                    $html .= \html_writer::link($child->url, $child->name, ['target' => '_blank']);
+                }
+            }
+            $html .= \html_writer::end_tag('li');
+        }
+        $html .= \html_writer::end_tag('ul');
+        return $html;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return array navigation data for the template.
+     */
+    public function export_for_template(renderer_base $output) : Array {
+        $data = $this->create_navigation($this->tree);
+        return ['navigation' => $data];
+    }
+}
\ No newline at end of file
diff --git a/privacy/classes/output/renderer.php b/privacy/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..dfff737
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy renderer.
+ *
+ * @package    core_privacy
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_privacy\output;
+defined('MOODLE_INTERNAL') || die;
+/**
+ * Privacy renderer's for privacy stuff.
+ *
+ * @since      Moodle 3.6
+ * @package    core_privacy
+ * @copyright  2018 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+    /**
+     * Render the whole tree.
+     *
+     * @param navigation_page $page
+     * @return string
+     */
+    public function render_navigation(exported_navigation_page $page) {
+        $data = $page->export_for_template($this);
+        return parent::render_from_template('core_privacy/navigation', $data);
+    }
+
+    /**
+     * Render the html page.
+     *
+     * @param html_page $page
+     * @return string
+     */
+    public function render_html_page(exported_html_page $page) {
+        $data = $page->export_for_template($this);
+        return parent::render_from_template('core_privacy/htmlpage', $data);
+    }
+}
\ No newline at end of file
diff --git a/privacy/export_files/general.css b/privacy/export_files/general.css
new file mode 100644 (file)
index 0000000..5eff504
--- /dev/null
@@ -0,0 +1,50 @@
+.hide {
+    display: none;
+}
+
+li.menu-item {
+    cursor: pointer;
+}
+
+li[aria-expanded=false]:not(.item) {
+    list-style-image: url('pix/collapsed.svg');
+}
+
+li[aria-expanded=true]:not(.item) {
+    list-style-image: url('pix/expanded.svg');
+}
+
+[aria-expanded="false"] > [role="group"] {
+    display: none;
+}
+
+#navigation {
+    display: inline-block;
+    width: 20%;
+    vertical-align: top;
+    overflow: scroll;
+    border-radius: 0.3rem;
+}
+
+[data-main-content] {
+    display: inline-block;
+    width: 69%;
+    vertical-align: top;
+}
+
+.title {
+    font-size: large;
+    font-weight: bold;
+}
+
+.block {
+    padding: 19px;
+}
+
+.item {
+    list-style-image: url('pix/navigationitem.svg');
+}
+
+.moodle-logo {
+    width: 110px;
+}
diff --git a/privacy/export_files/general.js b/privacy/export_files/general.js
new file mode 100644 (file)
index 0000000..b887861
--- /dev/null
@@ -0,0 +1,138 @@
+var currentlyloaded = [];
+
+/**
+ * Loads the data for the clicked navigation item.
+ *
+ * @param  {Object} clickednode The jquery object for the clicked node.
+ */
+function handleClick(clickednode) {
+    var contextcrumb = '';
+    var parentnodes = clickednode.parents('li');
+    for (var i = parentnodes.length; i >= 0; i--) {
+        var treenodes = window.$(parentnodes[i]);
+        if (treenodes.hasClass('item')) {
+            if (contextcrumb == '') {
+                contextcrumb = treenodes[0].innerText;
+            } else {
+                contextcrumb = contextcrumb + ' | ' + treenodes[0].innerText;
+            }
+        } else if (treenodes.hasClass('menu-item')) {
+            if (contextcrumb == '') {
+                contextcrumb = treenodes[0].firstChild.textContent;
+            } else {
+                contextcrumb = contextcrumb + ' | ' + treenodes[0].firstChild.textContent;
+            }
+        }
+    }
+    var datafile = clickednode.attr('data-var');
+    loadContent(datafile, function() {
+        addFileDataToMainArea(window[datafile], contextcrumb);
+    });
+}
+
+/**
+ * Load content to be displayed.
+ *
+ * @param  {String}   datafile The json data to be displayed.
+ * @param  {Function} callback The function to run after loading the json file.
+ */
+function loadContent(datafile, callback) {
+
+    // Check to see if this file has already been loaded. If so just go straight to the callback.
+    if (fileIsLoaded(datafile)) {
+        callback();
+        return;
+    }
+
+    // This (user_data_index) is defined in data_index.js
+    var data = window.user_data_index[datafile];
+    var newscript = document.createElement('script');
+
+    if (newscript.readyState) {
+        newscript.onreadystatechange = function() {
+            if (this.readyState == 'complete' || this.readyState == 'loaded') {
+                this.onreadystatechange = null;
+                callback();
+            }
+        };
+    } else {
+        newscript.onload = function() {
+            callback();
+        };
+    }
+
+    newscript.type = 'text/javascript';
+    newscript.src = data;
+    newscript.charset = 'utf-8';
+    document.getElementsByTagName("head")[0].appendChild(newscript);
+
+    // Keep track that this file has already been loaded.
+    currentlyloaded.push(datafile);
+}
+
+/**
+ * Checks to see if the datafile has already been loaded onto the page or not.
+ *
+ * @param  {String} datafile The file entry we are checking to see if it is already loaded.
+ * @return {Boolean} True if already loaded otherwise false.
+ */
+function fileIsLoaded(datafile) {
+    for (var index in currentlyloaded) {
+        if (currentlyloaded[index] == datafile) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/**
+ * Adds the loaded data to the main content area of the page.
+ *
+ * @param {Object} data  Data to be added to the main content area of the page.
+ * @param {String} title Title for the content area.
+ */
+function addFileDataToMainArea(data, title) {
+    var dataarea = window.$('[data-main-content]');
+    while (dataarea[0].firstChild) {
+        dataarea[0].removeChild(dataarea[0].firstChild);
+    }
+    var htmldata = makeList(data);
+
+    var areatitle = document.createElement('h2');
+    areatitle.innerHTML = title;
+    dataarea[0].appendChild(areatitle);
+
+    var maincontentlist = document.createElement('div');
+    maincontentlist.innerHTML = htmldata;
+    dataarea[0].appendChild(maincontentlist.firstChild);
+}
+
+/**
+ * Creates an unordered list with the json data provided.
+ *
+ * @param  {Object} jsondata The json data to turn into an unordered list.
+ * @return {String} The html string of the unordered list.
+ */
+function makeList(jsondata) {
+    var html = '<ul>';
+    for (var key in jsondata) {
+        html += '<li>';
+        if (typeof jsondata[key] == 'object') {
+            html += key;
+            html += makeList(jsondata[key]);
+        } else {
+            html += key + ': ' + jsondata[key];
+        }
+        html += '</li>';
+    }
+    html += '</ul>';
+    return html;
+}
+
+window.$(document).ready(function() {
+    window.$('[data-var]').click(function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        handleClick(window.$(this));
+    });
+});
\ No newline at end of file
diff --git a/privacy/templates/htmlpage.mustache b/privacy/templates/htmlpage.mustache
new file mode 100644 (file)
index 0000000..163dd3e
--- /dev/null
@@ -0,0 +1,110 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_privacy/htmlpage
+
+    Renders the user export html page.
+    This template is not for use within moodle.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * righttoleft
+    * navigation
+    * systemname
+    * timegenerated
+    * username
+
+    Example context (json):
+    {
+        "righttoleft": 0,
+        "navigation": "Navigation html",
+        "systemname": "Test System",
+        "siteurl": "#",
+        "timegenerated": 1536906530,
+        "username": "John Jones"
+    }
+}}
+
+<!DOCTYPE html>
+<html {{#righttoleft}}dir="rtl"{{/righttoleft}}>
+<head>
+    <meta charset="UTF-8">
+    <title>Data export</title>
+    {{^righttoleft}}
+    <link rel="stylesheet" href="bootstrap.min.css">
+    {{/righttoleft}}
+    {{#righttoleft}}
+    <link rel="stylesheet" href="rtlbootstrap.min.css">
+    {{/righttoleft}}
+    <link rel="stylesheet" type="text/css" href="general.css" />
+</head>
+<body>
+    <nav class="navbar navbar-light bg-light border-bottom">
+        <a class="navbar-brand" href="https://www.moodle.org" title="Moodle">
+            <img class="moodle-logo" src="pix/moodlelogo.svg" alt="Moodle logo" />
+        </a>
+        <span class="navbar-brand">{{#str}}broughtbymoodle, core_privacy{{/str}}</span>
+    </nav>
+    <div id="page" class="container-fluid mt-2">
+
+        {{{navigation}}}
+        <div data-main-content class="jumbotron bg-light border">
+            <h2 class="display-8">{{#str}}viewdata, core_privacy{{/str}}</h2>
+        </div>
+        <script src="js/jquery-3.2.1.min.js"></script>
+        <script src="js/data_index.js"></script>
+        <script src="js/general.js"></script>
+        <script src="js/require.min.js"></script>
+        <script>
+            requirejs.config({
+                "baseUrl": "./",
+                "paths": {
+                    "app": "./",
+                    "jquery": "./js/jquery-3.2.1.min",
+                    "tree": "./js/tree.min"
+                }
+            });
+            var tree;
+
+            require(['tree'], function(t) {
+                var tree = new t('#my-tree');
+                tree.registerEnterCallback(function(item) {
+                    var basenode = $(item[0].childNodes[0]);
+                    if (basenode.attr('data-var') != undefined) {
+                        handleClick(basenode);
+                    } else if (basenode.attr('href') != undefined) {
+                        window.location.href = basenode.attr('href');
+                    }
+                });
+            });
+        </script>
+        <footer class="footer">
+            <div class="container">
+                <hr />
+                <div class="text-center text-muted">{{#str}}exportfrom, core_privacy, <a href="{{{siteurl}}}" title="{{systemname}}">{{systemname}}</a>{{/str}}</div>
+                <div class="text-center text-muted">{{#str}}exporttime, core_privacy, {{#userdate}}{{timegenerated}},{{#str}}strftimedatetime, langconfig{{/str}}{{/userdate}}{{/str}}</div>
+                <div class="text-center text-muted">{{#str}}exportuser, core_privacy, {{username}}{{/str}}</div>
+            </div>
+        </footer>
+    </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/privacy/templates/navigation.mustache b/privacy/templates/navigation.mustache
new file mode 100644 (file)
index 0000000..4031d8d
--- /dev/null
@@ -0,0 +1,48 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_privacy/navigation
+
+    Renders the Navigation section for the user export html page.
+    This template is not for use within moodle.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * navigation
+
+    Example context (json):
+    {
+        "navigation": "Navigation html"
+    }
+}}
+<div id="navigation" class="bg-light border">
+    <div class="m-2">
+        <div class="header">
+            <div class="title">
+                <h2>{{#str}}navigation, core_privacy{{/str}}</h2>
+            </div>
+        </div>
+        <div class="content">
+            {{{navigation}}}
+        </div>
+    </div>
+</div>
\ No newline at end of file
index f4ccc3b..4de799c 100644 (file)
@@ -426,7 +426,7 @@ class moodle_content_writer_test extends advanced_testcase {
     public function test_export_file($filearea, $itemid, $filepath, $filename, $content) {
         $this->resetAfterTest();
         $context = \context_system::instance();
-        $filenamepath = '/' . $filearea . '/' . ($itemid ?: '') . $filepath . $filename;
+        $filenamepath = '/' . $filearea . '/' . ($itemid ? '_' . $itemid : '') . $filepath . $filename;
 
         $filerecord = array(
             'contextid' => $context->id,
@@ -989,7 +989,7 @@ class moodle_content_writer_test extends advanced_testcase {
         $fileroot = $this->fetch_exported_content($writer);
 
         $contextpath = $this->get_context_path($context, $subcontext, 'data.json');
-        $expectedpath = "System {$context->id}/{$expected}/data.json";
+        $expectedpath = "System _.{$context->id}/{$expected}/data.json";
         $this->assertEquals($expectedpath, $contextpath);
 
         $json = $fileroot->getChild($contextpath)->getContent();
@@ -1019,7 +1019,7 @@ class moodle_content_writer_test extends advanced_testcase {
         $fileroot = $this->fetch_exported_content($writer);
 
         $contextpath = $this->get_context_path($context, $subcontext, 'name.json');
-        $expectedpath = "System {$context->id}/{$expected}/name.json";
+        $expectedpath = "System _.{$context->id}/{$expected}/name.json";
         $this->assertEquals($expectedpath, $contextpath);
 
         $json = $fileroot->getChild($contextpath)->getContent();
@@ -1049,7 +1049,7 @@ class moodle_content_writer_test extends advanced_testcase {
         $fileroot = $this->fetch_exported_content($writer);
 
         $contextpath = $this->get_context_path($context, $subcontext, 'metadata.json');
-        $expectedpath = "System {$context->id}/{$expected}/metadata.json";
+        $expectedpath = "System _.{$context->id}/{$expected}/metadata.json";
         $this->assertEquals($expectedpath, $contextpath);
 
         $json = $fileroot->getChild($contextpath)->getContent();
@@ -1077,7 +1077,7 @@ class moodle_content_writer_test extends advanced_testcase {
             core_filetypes::add_type('json', 'application/json', 'archive', [], '', 'JSON file archive');
         }
         $context = \context_system::instance();
-        $expectedpath = "System {$context->id}/User preferences/{$expected}.json";
+        $expectedpath = "System _.{$context->id}/User preferences/{$expected}.json";
 
         $component = $longtext;
 
@@ -1206,20 +1206,354 @@ class moodle_content_writer_test extends advanced_testcase {
                 'intro',
                 0,
                 '<p><img src="@@PLUGINFILE@@/hello.gif" /></p>',
-                '<p><img src="_files/intro/hello.gif" /></p>',
+                '<p><img src="System _.1/_files/intro/hello.gif" /></p>',
             ],
             'nonzeroitemid' => [
                 'submission_content',
                 34,
                 '<p><img src="@@PLUGINFILE@@/first.png" alt="First" /></p>',
-                '<p><img src="_files/submission_content/34/first.png" alt="First" /></p>',
+                '<p><img src="System _.1/_files/submission_content/_34/first.png" alt="First" /></p>',
             ],
             'withfilepath' => [
                 'post_content',
                 9889,
                 '<a href="@@PLUGINFILE@@/embedded/docs/muhehe.exe">Click here!</a>',
-                '<a href="_files/post_content/9889/embedded/docs/muhehe.exe">Click here!</a>',
+                '<a href="System _.1/_files/post_content/_9889/embedded/docs/muhehe.exe">Click here!</a>',
             ],
         ];
     }
+
+    public function test_export_html_functions() {
+        $this->resetAfterTest();
+
+        $data = (object) ['key' => 'value'];
+
+        $context = \context_system::instance();
+        $subcontext = [];
+
+        $writer = $this->get_writer_instance()
+            ->set_context($context)
+            ->export_data($subcontext, (object) $data);
+
+        $writer->set_context($context)->export_data(['paper'], $data);
+
+        $coursecategory = $this->getDataGenerator()->create_category();
+        $categorycontext = \context_coursecat::instance($coursecategory->id);
+        $course = $this->getDataGenerator()->create_course();
+        $misccoursecxt = \context_coursecat::instance($course->category);
+        $coursecontext = \context_course::instance($course->id);
+        $cm = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]);
+        $modulecontext = \context_module::instance($cm->cmid);
+
+        $writer->set_context($modulecontext)->export_data([], $data);
+        $writer->set_context($coursecontext)->export_data(['grades'], $data);
+        $writer->set_context($categorycontext)->export_data([], $data);
+        $writer->set_context($context)->export_data([get_string('privacy:path:logs', 'tool_log'), 'Standard log'], $data);
+
+        // Add a file.
+        $fs = get_file_storage();
+        $file = (object) [
+            'component' => 'core_privacy',
+            'filearea' => 'tests',
+            'itemid' => 0,
+            'path' => '/',
+            'name' => 'a.txt',
+            'content' => 'Test file 0',
+        ];
+        $record = [
+            'contextid' => $context->id,
+            'component' => $file->component,
+            'filearea'  => $file->filearea,
+            'itemid'    => $file->itemid,
+            'filepath'  => $file->path,
+            'filename'  => $file->name,
+        ];
+
+        $file->namepath = '/' . $file->filearea . '/' . ($file->itemid ?: '') . $file->path . $file->name;
+        $file->storedfile = $fs->create_file_from_string($record, $file->content);
+        $writer->set_context($context)->export_area_files([], 'core_privacy', 'tests', 0);
+
+        list($tree, $treelist, $indexdata) = phpunit_util::call_internal_method($writer, 'prepare_for_export', [],
+                '\core_privacy\local\request\moodle_content_writer');
+
+        $expectedtreeoutput = [
+            'System _.1' => [
+                'data.json',
+                'paper' => 'data.json',
+                'Category Miscellaneous _.' . $misccoursecxt->id => [
+                    'Course Test course 1 _.' . $coursecontext->id => [
+                        'Chat Chat 1 _.' . $modulecontext->id => 'data.json',
+                        'grades' => 'data.json'
+                    ]
+                ],
+                'Category Course category 1 _.' . $categorycontext->id => 'data.json',
+                '_files' => [
+                    'tests' => 'a.txt'
+                ],
+                'Logs' => [
+                    'Standard log' => 'data.json'
+                ]
+            ]
+        ];
+        $this->assertEquals($expectedtreeoutput, $tree);
+
+        $expectedlistoutput = [
+            'System _.1/data.json' => 'data_file_1',
+            'System _.1/paper/data.json' => 'data_file_2',
+            'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
+                    $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.json'   => 'data_file_3',
+            'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
+                    $coursecontext->id . '/grades/data.json'   => 'data_file_4',
+            'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.json' => 'data_file_5',
+            'System _.1/_files/tests/a.txt' => 'No var',
+            'System _.1/Logs/Standard log/data.json' => 'data_file_6'
+        ];
+        $this->assertEquals($expectedlistoutput, $treelist);
+
+        $expectedindex = [
+            'data_file_1' => 'System _.1/data.js',
+            'data_file_2' => 'System _.1/paper/data.js',
+            'data_file_3' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
+                    $coursecontext->id . '/Chat Chat 1 _.' . $modulecontext->id . '/data.js',
+            'data_file_4' => 'System _.1/Category Miscellaneous _.' . $misccoursecxt->id . '/Course Test course 1 _.' .
+                    $coursecontext->id . '/grades/data.js',
+            'data_file_5' => 'System _.1/Category Course category 1 _.' . $categorycontext->id . '/data.js',
+            'data_file_6' => 'System _.1/Logs/Standard log/data.js'
+        ];
+        $this->assertEquals($expectedindex, $indexdata);
+
+        $richtree = phpunit_util::call_internal_method($writer, 'make_tree_object', [$tree, $treelist],
+                '\core_privacy\local\request\moodle_content_writer');
+
+        // This is a big one.
+        $expectedrichtree = [
+            'System _.1' => (object) [
+                'itemtype' => 'treeitem',
+                'name' => 'System ',
+                'context' => \context_system::instance(),
+                'children' => [
+                    (object) [
+                        'name' => 'data.json',
+                        'itemtype' => 'item',
+                        'datavar' => 'data_file_1'
+                    ],
+                    'paper' => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'paper',
+                        'children' => [
+                            'data.json' => (object) [
+                                'name' => 'data.json',
+                                'itemtype' => 'item',
+                                'datavar' => 'data_file_2'
+                            ]
+                        ]
+                    ],
+                    'Category Miscellaneous _.' . $misccoursecxt->id => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'Category Miscellaneous ',
+                        'context' => $misccoursecxt,
+                        'children' => [
+                            'Course Test course 1 _.' . $coursecontext->id => (object) [
+                                'itemtype' => 'treeitem',
+                                'name' => 'Course Test course 1 ',
+                                'context' => $coursecontext,
+                                'children' => [
+                                    'Chat Chat 1 _.' . $modulecontext->id => (object) [
+                                        'itemtype' => 'treeitem',
+                                        'name' => 'Chat Chat 1 ',
+                                        'context' => $modulecontext,
+                                        'children' => [
+                                            'data.json' => (object) [
+                                                'name' => 'data.json',
+                                                'itemtype' => 'item',
+                                                'datavar' => 'data_file_3'
+                                            ]
+                                        ]
+                                    ],
+                                    'grades' => (object) [
+                                        'itemtype' => 'treeitem',
+                                        'name' => 'grades',
+                                        'children' => [
+                                            'data.json' => (object) [
+                                                'name' => 'data.json',
+                                                'itemtype' => 'item',
+                                                'datavar' => 'data_file_4'
+                                            ]
+                                        ]
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ],
+                    'Category Course category 1 _.' . $categorycontext->id => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'Category Course category 1 ',
+                        'context' => $categorycontext,
+                        'children' => [
+                            'data.json' => (object) [
+                                'name' => 'data.json',
+                                'itemtype' => 'item',
+                                'datavar' => 'data_file_5'
+                            ]
+                        ]
+                    ],
+                    '_files' => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => '_files',
+                        'children' => [
+                            'tests' => (object) [
+                                'itemtype' => 'treeitem',
+                                'name' => 'tests',
+                                'children' => [
+                                    'a.txt' => (object) [
+                                        'name' => 'a.txt',
+                                        'itemtype' => 'item',
+                                        'url' => new \moodle_url('System _.1/_files/tests/a.txt')
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ],
+                    'Logs' => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'Logs',
+                        'children' => [
+                            'Standard log' => (object) [
+                                'itemtype' => 'treeitem',
+                                'name' => 'Standard log',
+                                'children' => [
+                                    'data.json' => (object) [
+                                        'name' => 'data.json',
+                                        'itemtype' => 'item',
+                                        'datavar' => 'data_file_6'
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ]
+                ]
+            ]
+        ];
+        $this->assertEquals($expectedrichtree, $richtree);
+
+        // The phpunit_util::call_internal_method() method doesn't allow for referenced parameters so we have this joyful code
+        // instead to do the same thing, but with references working obviously.
+        $funfunction = function($object, $data) {
+            return $object->sort_my_list($data);
+        };
+
+        $funfunction = Closure::bind($funfunction, null, $writer);
+        $funfunction($writer, $richtree);
+
+        // This is a big one.
+        $expectedsortedtree = [
+            'System _.1' => (object) [
+                'itemtype' => 'treeitem',
+                'name' => 'System ',
+                'context' => \context_system::instance(),
+                'children' => [
+                    'Category Miscellaneous _.' . $misccoursecxt->id => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'Category Miscellaneous ',
+                        'context' => $misccoursecxt,
+                        'children' => [
+                            'Course Test course 1 _.' . $coursecontext->id => (object) [
+                                'itemtype' => 'treeitem',
+                                'name' => 'Course Test course 1 ',
+                                'context' => $coursecontext,
+                                'children' => [
+                                    'Chat Chat 1 _.' . $modulecontext->id => (object) [
+                                        'itemtype' => 'treeitem',
+                                        'name' => 'Chat Chat 1 ',
+                                        'context' => $modulecontext,
+                                        'children' => [
+                                            'data.json' => (object) [
+                                                'name' => 'data.json',
+                                                'itemtype' => 'item',
+                                                'datavar' => 'data_file_3'
+                                            ]
+                                        ]
+                                    ],
+                                    'grades' => (object) [
+                                        'itemtype' => 'treeitem',
+                                        'name' => 'grades',
+                                        'children' => [
+                                            'data.json' => (object) [
+                                                'name' => 'data.json',
+                                                'itemtype' => 'item',
+                                                'datavar' => 'data_file_4'
+                                            ]
+                                        ]
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ],
+                    'Category Course category 1 _.' . $categorycontext->id => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'Category Course category 1 ',
+                        'context' => $categorycontext,
+                        'children' => [
+                            'data.json' => (object) [
+                                'name' => 'data.json',
+                                'itemtype' => 'item',
+                                'datavar' => 'data_file_5'
+                            ]
+                        ]
+                    ],
+                    '_files' => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => '_files',
+                        'children' => [
+                            'tests' => (object) [
+                                'itemtype' => 'treeitem',
+                                'name' => 'tests',
+                                'children' => [
+                                    'a.txt' => (object) [
+                                        'name' => 'a.txt',
+                                        'itemtype' => 'item',
+                                        'url' => new \moodle_url('System _.1/_files/tests/a.txt')
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ],
+                    'Logs' => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'Logs',
+                        'children' => [
+                            'Standard log' => (object) [
+                                'itemtype' => 'treeitem',
+                                'name' => 'Standard log',
+                                'children' => [
+                                    'data.json' => (object) [
+                                        'name' => 'data.json',
+                                        'itemtype' => 'item',
+                                        'datavar' => 'data_file_6'
+                                    ]
+                                ]
+                            ]
+                        ]
+                    ],
+                    'paper' => (object) [
+                        'itemtype' => 'treeitem',
+                        'name' => 'paper',
+                        'children' => [
+                            'data.json' => (object) [
+                                'name' => 'data.json',
+                                'itemtype' => 'item',
+                                'datavar' => 'data_file_2'
+                            ]
+                        ]
+                    ],
+                    (object) [
+                        'name' => 'data.json',
+                        'itemtype' => 'item',
+                        'datavar' => 'data_file_1'
+                    ]
+                ]
+            ]
+        ];
+        $this->assertEquals($expectedsortedtree, $richtree);
+    }
 }
index fd08e44..af137a1 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018100500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018100500.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.