first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,37 @@
<?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/>.
/**
* Backup support for tool_log logstore subplugins.
*
* @package tool_log
* @category backup
* @copyright 2015 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Parent class of all the logstore subplugin implementations.
*
* Note: While this intermediate class is not strictly required and all the
* subplugin implementations can extend directly {@link backup_subplugin},
* it is always recommended to have it, both for better testing and also
* for sharing code between all subplugins.
*/
abstract class backup_tool_log_logstore_subplugin extends backup_subplugin {
}
@@ -0,0 +1,157 @@
<?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/>.
/**
* Restore support for tool_log logstore subplugins.
*
* @package tool_log
* @category backup
* @copyright 2015 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Parent class of all the logstore subplugin implementations.
*
* Note: While this intermediate class is not strictly required and all the
* subplugin implementations can extend directly {@link restore_subplugin},
* it is always recommended to have it, both for better testing and also
* for sharing code between all subplugins.
*/
abstract class restore_tool_log_logstore_subplugin extends restore_subplugin {
/**
* Process log entries.
*
* This method proceeds to read, complete, remap and, finally,
* discard or save every log entry.
*
* @param array $data log entry.
* @param bool $jsonformat If true, uses JSON format for the 'other' field
* @return object|null $dataobject A data object with values for one or more fields in the record,
* or null if we are not going to process the log.
*/
protected function process_log($data, bool $jsonformat = false) {
$data = (object) $data;
// Complete the information that does not come from backup.
$contextid = $data->contextid;
if (!$data->contextid = $this->get_mappingid('context', $contextid)) {
$message = "Context id \"$contextid\" could not be mapped. Skipping log record.";
$this->log($message, backup::LOG_DEBUG);
return;
}
$context = context::instance_by_id($data->contextid, MUST_EXIST);
$data->contextlevel = $context->contextlevel;
$data->contextinstanceid = $context->instanceid;
$data->courseid = $this->task->get_courseid();
// Remap users.
$userid = $data->userid;
if (!$data->userid = $this->get_mappingid('user', $userid)) {
$message = "User id \"$userid\" could not be mapped. Skipping log record.";
$this->log($message, backup::LOG_DEBUG);
return;
}
if (!empty($data->relateduserid)) { // This is optional.
$relateduserid = $data->relateduserid;
if (!$data->relateduserid = $this->get_mappingid('user', $relateduserid)) {
$message = "Related user id \"$relateduserid\" could not be mapped. Skipping log record.";
$this->log($message, backup::LOG_DEBUG);
return;
}
}
if (!empty($data->realuserid)) { // This is optional.
$realuserid = $data->realuserid;
if (!$data->realuserid = $this->get_mappingid('user', $realuserid)) {
$message = "Real user id \"$realuserid\" could not be mapped. Skipping log record.";
$this->log($message, backup::LOG_DEBUG);
return;
}
}
// There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
// Revert other to its original php way.
$data->other = \tool_log\local\privacy\helper::decode_other(base64_decode($data->other));
// Arrived here, we have both 'objectid' and 'other' to be converted. This is the tricky part.
// Both are pointing to other records id, but the sources are not identified in the
// same way restore mappings work. So we need to delegate them to some resolver that
// will give us the correct restore mapping to be used.
if (!empty($data->objectid)) {
// Check if there is an available class for this event we can use to map this value.
$eventclass = $data->eventname;
if (class_exists($eventclass)) {
$mapping = $eventclass::get_objectid_mapping();
if ($mapping) {
// Check if it can not be mapped.
if ((is_int($mapping) && $mapping === \core\event\base::NOT_MAPPED) ||
($mapping['restore'] === \core\event\base::NOT_MAPPED)) {
$data->objectid = \core\event\base::NOT_MAPPED;
} else {
$data->objectid = $this->get_mappingid($mapping['restore'], $data->objectid,
\core\event\base::NOT_FOUND);
}
}
} else {
$message = "Event class not found: \"$eventclass\". Skipping log record.";
$this->log($message, backup::LOG_DEBUG);
return; // No such class, can not restore.
}
}
if (!empty($data->other)) {
// Check if there is an available class for this event we can use to map this value.
$eventclass = $data->eventname;
if (class_exists($eventclass)) {
$othermapping = $eventclass::get_other_mapping();
if ($othermapping) {
// Go through the data we have.
foreach ($data->other as $key => $value) {
// Check if there is a corresponding key we can use to map to.
if (isset($othermapping[$key]) && !empty($value)) {
// Ok, let's map this.
$mapping = $othermapping[$key];
// Check if it can not be mapped.
if ((is_int($mapping) && $mapping === \core\event\base::NOT_MAPPED) ||
($mapping['restore'] === \core\event\base::NOT_MAPPED)) {
$data->other[$key] = \core\event\base::NOT_MAPPED;
} else {
$data->other[$key] = $this->get_mappingid($mapping['restore'], $value,
\core\event\base::NOT_FOUND);
}
}
}
}
} else {
$message = "Event class not found: \"$eventclass\". Skipping log record.";
$this->log($message, backup::LOG_DEBUG);
return; // No such class, can not restore.
}
}
// Serialize 'other' field so we can store it in the DB.
if ($jsonformat) {
$data->other = json_encode($data->other);
} else {
$data->other = serialize($data->other);
}
return $data;
}
}
@@ -0,0 +1,134 @@
<?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/>.
/**
* Helper trait buffered_writer
*
* @package tool_log
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\helper;
defined('MOODLE_INTERNAL') || die();
/**
* Helper trait buffered_writer. Adds buffer support for the store.
*
* @package tool_log
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait buffered_writer {
/** @var array $buffer buffer of events. */
protected $buffer = array();
/** @var array $buffer buffer size of events. */
protected $buffersize;
/** @var int $count Counter. */
protected $count = 0;
/** @var bool If true, writes JSON instead of PHP serialized data for 'other' field */
protected $jsonformat = false;
/**
* Should the event be ignored (== not logged)?
* @param \core\event\base $event
* @return bool
*/
abstract protected function is_event_ignored(\core\event\base $event);
/**
* Write event in the store with buffering. Method insert_event_entries() must be
* defined.
*
* @param \core\event\base $event
*
* @return void
*/
public function write(\core\event\base $event) {
global $PAGE;
if ($this->is_event_ignored($event)) {
return;
}
// We need to capture current info at this moment,
// at the same time this lowers memory use because
// snapshots and custom objects may be garbage collected.
$entry = $event->get_data();
if ($this->jsonformat) {
$entry['other'] = json_encode($entry['other']);
} else {
$entry['other'] = serialize($entry['other']);
}
$entry['origin'] = $PAGE->requestorigin;
$entry['ip'] = $PAGE->requestip;
$entry['realuserid'] = \core\session\manager::is_loggedinas() ? $GLOBALS['USER']->realuser : null;
$this->buffer[] = $entry;
$this->count++;
if (!isset($this->buffersize)) {
$this->buffersize = $this->get_config('buffersize', 50);
}
if ($this->count >= $this->buffersize) {
$this->flush();
}
}
/**
* Flush event buffer.
*/
public function flush() {
if ($this->count == 0) {
return;
}
$events = $this->buffer;
$this->count = 0;
$this->buffer = array();
$this->insert_event_entries($events);
}
/**
* Bulk write a given array of events to the backend. Stores must implement this.
*
* @param array $evententries raw event data
*/
abstract protected function insert_event_entries($evententries);
/**
* Get a config value for the store.
*
* @param string $name Config name
* @param mixed $default default value
*
* @return mixed config value if set, else the default value.
*/
abstract protected function get_config($name, $default = null);
/**
* Push any remaining events to the database. Insert_events() must be
* defined. override in stores if the store doesn't support buffering.
*
*/
public function dispose() {
$this->flush();
}
}
+112
View File
@@ -0,0 +1,112 @@
<?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/>.
/**
* Reader helper trait.
*
* @package tool_log
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\helper;
defined('MOODLE_INTERNAL') || die();
/**
* Reader helper trait.
* \tool_log\helper\store must be included before using this trait.
*
* @package tool_log
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @property string $component Frankenstyle plugin name initialised in store trait.
* @property string $store short plugin name initialised in store trait.
*/
trait reader {
/** @var string Frankenstyle plugin name initialised in store trait. */
protected $component;
/** @var string short plugin name initialised in store trait. */
protected $store;
/**
* Default get name api.
*
* @return string name of the store.
*/
public function get_name() {
if (get_string_manager()->string_exists('pluginname', $this->component)) {
return get_string('pluginname', $this->component);
}
return $this->store;
}
/**
* Default get description method.
*
* @return string description of the store.
*/
public function get_description() {
if (get_string_manager()->string_exists('pluginname_desc', $this->component)) {
return get_string('pluginname_desc', $this->component);
}
return $this->store;
}
/**
* Function decodes the other field into an array using either PHP serialisation or JSON.
*
* Note that this does not rely on the config setting, it supports both formats, so you can
* use it for data before/after making a change to the config setting.
*
* The return value is usually an array but it can also be null or a boolean or something.
*
* @param string $other Other value
* @return mixed Decoded value
*/
public static function decode_other(?string $other) {
if ($other === 'N;' || preg_match('~^.:~', $other ?? '')) {
return unserialize($other, ['allowed_classes' => [stdClass::class]]);
} else {
return json_decode($other ?? '', true);
}
}
/**
* Adds ID column to $sort to make sure events from one request
* within 1 second are returned in the same order.
*
* @param string $sort
* @return string sort string
*/
protected static function tweak_sort_by_id($sort) {
if (empty($sort)) {
// Mysql does this - unlikely to be used in real life because $sort is always expected.
$sort = "id ASC";
} else if (stripos($sort, 'timecreated') === false) {
$sort .= ", id ASC";
} else if (stripos($sort, 'timecreated DESC') !== false) {
$sort .= ", id DESC";
} else {
$sort .= ", id ASC";
}
return $sort;
}
}
+79
View File
@@ -0,0 +1,79 @@
<?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/>.
/**
* Helper trait store.
*
* @package tool_log
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\helper;
defined('MOODLE_INTERNAL') || die();
/**
* Helper trait store. Adds some helper methods for stores.
*
* @package tool_log
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait store {
/** @var \tool_log\log\manager $manager manager instance. */
protected $manager;
/** @var string $component Frankenstyle store name. */
protected $component;
/** @var string $store name of the store. */
protected $store;
/**
* Setup store specific variables.
*
* @param \tool_log\log\manager $manager manager instance.
*/
protected function helper_setup(\tool_log\log\manager $manager) {
$this->manager = $manager;
$called = get_called_class();
$parts = explode('\\', $called);
if (!isset($parts[0]) || strpos($parts[0], 'logstore_') !== 0) {
throw new \coding_exception("Store $called doesn't define classes in correct namespaces.");
}
$this->component = $parts[0];
$this->store = str_replace('logstore_', '', $this->component);
}
/**
* Api to get plugin config
*
* @param string $name name of the config.
* @param null|mixed $default default value to return.
*
* @return mixed|null return config value.
*/
protected function get_config($name, $default = null) {
$value = get_config($this->component, $name);
if ($value !== false) {
return $value;
}
return $default;
}
}
@@ -0,0 +1,149 @@
<?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 helper.
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\local\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\request\transform;
/**
* Privacy helper class.
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
use \tool_log\helper\reader;
/**
* Returns an event from a standard record.
*
* @see \logstore_standard\log\store::get_log_event()
* @param object $data Log data.
* @return \core\event\base
*/
protected static function restore_event_from_standard_record($data) {
$extra = ['origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid];
$data = (array) $data;
$id = $data['id'];
$data['other'] = self::decode_other($data['other']);
if ($data['other'] === false) {
$data['other'] = [];
}
unset($data['origin']);
unset($data['ip']);
unset($data['realuserid']);
unset($data['id']);
if (!$event = \core\event\base::restore($data, $extra)) {
return null;
}
return $event;
}
/**
* Transform a standard log record for a user.
*
* @param object $record The record.
* @param int $userid The user ID.
* @return array
*/
public static function transform_standard_log_record_for_userid($record, $userid) {
// Restore the event to try to get the name, description and other field.
$restoredevent = static::restore_event_from_standard_record($record);
if ($restoredevent) {
$name = $restoredevent->get_name();
$description = $restoredevent->get_description();
$other = $restoredevent->other;
} else {
$name = $record->eventname;
$description = "Unknown event ({$name})";
$other = self::decode_other($record->other);
}
$realuserid = $record->realuserid;
$isauthor = $record->userid == $userid;
$isrelated = $record->relateduserid == $userid;
$isrealuser = $realuserid == $userid;
$ismasqueraded = $realuserid !== null && $record->userid != $realuserid;
$ismasquerading = $isrealuser && !$isauthor;
$isanonymous = $record->anonymous;
$data = [
'name' => $name,
'description' => $description,
'timecreated' => transform::datetime($record->timecreated),
'origin' => static::transform_origin($record->origin),
'ip' => $isauthor ? $record->ip : '',
'other' => $other ? $other : []
];
if ($isanonymous) {
$data['action_was_done_anonymously'] = transform::yesno($isanonymous);
}
if ($isauthor || !$isanonymous) {
$data['authorid'] = transform::user($record->userid);
$data['author_of_the_action_was_you'] = transform::yesno($isauthor);
}
if ($record->relateduserid) {
$data['relateduserid'] = transform::user($record->relateduserid);
$data['related_user_was_you'] = transform::yesno($isrelated);
}
if ($ismasqueraded) {
$data['author_of_the_action_was_masqueraded'] = transform::yesno(true);
if ($ismasquerading || !$isanonymous) {
$data['masqueradinguserid'] = transform::user($realuserid);
$data['masquerading_user_was_you'] = transform::yesno($ismasquerading);
}
}
return $data;
}
/**
* Transform origin.
*
* @param string $origin The page request origin.
* @return string
*/
public static function transform_origin($origin) {
switch ($origin) {
case 'cli':
case 'restore':
case 'web':
case 'ws':
return get_string('privacy:request:origin:' . $origin, 'tool_log');
break;
}
return $origin;
}
}
@@ -0,0 +1,78 @@
<?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/>.
/**
* Logstore provider interface.
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\local\privacy;
defined('MOODLE_INTERNAL') || die();
use context;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
/**
* Logstore provider interface.
*
* Logstore subplugins providers must implement this interface.
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface logstore_provider extends \core_privacy\local\request\plugin\subplugin_provider {
/**
* Add contexts that contain user information for the specified user.
*
* @param contextlist $contextlist The contextlist to add the contexts to.
* @param int $userid The user to find the contexts for.
* @return void
*/
public static function add_contexts_for_userid(contextlist $contextlist, $userid);
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
* @return void
*/
public static function export_user_data(approved_contextlist $contextlist);
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
* @return void
*/
public static function delete_data_for_all_users_in_context(context $context);
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
* @return void
*/
public static function delete_data_for_user(approved_contextlist $contextlist);
}
@@ -0,0 +1,60 @@
<?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/>.
/**
* Logstore userlist provider interface.
*
* @package tool_log
* @copyright 2018 Adrian Greeve
* @author Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\local\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Logstore userlist provider interface.
*
* Logstore subplugins providers must implement this interface.
*
* @package tool_log
* @copyright 2018 Adrian Greeve
* @author Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface logstore_userlist_provider extends
\core_privacy\local\request\plugin\subplugin_provider,
\core_privacy\local\request\shared_userlist_provider
{
/**
* Add user IDs that contain user information for the specified context.
*
* @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
* @return void
*/
public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist);
/**
* Delete all data for a list of users in the specified context.
*
* @param \core_privacy\local\request\approved_userlist $userlist The specific context and users to delete data for.
* @return void
*/
public static function delete_data_for_userlist(\core_privacy\local\request\approved_userlist $userlist);
}
@@ -0,0 +1,138 @@
<?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/>.
/**
* Moodle database: export and delete.
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\local\privacy;
defined('MOODLE_INTERNAL') || die();
use context;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\writer;
/**
* Moodle database: export and delete trait.
*
* This is to be used with logstores which use a database and table with the same columns
* as the core plugin 'logstore_standard'.
*
* This trait expects the following methods to be present in the object:
*
* - public static function get_database_and_table(): [moodle_database|null, string|null]
* - public static function get_export_subcontext(): []
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait moodle_database_export_and_delete {
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
list($db, $table) = static::get_database_and_table();
if (!$db || !$table) {
return;
}
$userid = $contextlist->get_user()->id;
list($insql, $inparams) = $db->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$sql = "(userid = :userid1 OR relateduserid = :userid2 OR realuserid = :userid3) AND contextid $insql";
$params = array_merge($inparams, [
'userid1' => $userid,
'userid2' => $userid,
'userid3' => $userid,
]);
$path = static::get_export_subcontext();
$flush = function($lastcontextid, $data) use ($path) {
$context = context::instance_by_id($lastcontextid);
writer::with_context($context)->export_data($path, (object) ['logs' => $data]);
};
$lastcontextid = null;
$data = [];
$recordset = $db->get_recordset_select($table, $sql, $params, 'contextid, timecreated, id');
foreach ($recordset as $record) {
if ($lastcontextid && $lastcontextid != $record->contextid) {
$flush($lastcontextid, $data);
$data = [];
}
$data[] = helper::transform_standard_log_record_for_userid($record, $userid);
$lastcontextid = $record->contextid;
}
if ($lastcontextid) {
$flush($lastcontextid, $data);
}
$recordset->close();
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(context $context) {
list($db, $table) = static::get_database_and_table();
if (!$db || !$table) {
return;
}
$db->delete_records($table, ['contextid' => $context->id]);
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
list($db, $table) = static::get_database_and_table();
if (!$db || !$table) {
return;
}
list($insql, $inparams) = $db->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$params = array_merge($inparams, ['userid' => $contextlist->get_user()->id]);
$db->delete_records_select($table, "userid = :userid AND contextid $insql", $params);
}
/**
* Delete all user data for the specified users, in the specified context.
*
* @param \core_privacy\local\request\approved_userlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_userlist(\core_privacy\local\request\approved_userlist $userlist) {
list($db, $table) = static::get_database_and_table();
if (!$db || !$table) {
return;
}
list($insql, $inparams) = $db->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
$params = array_merge($inparams, ['contextid' => $userlist->get_context()->id]);
$db->delete_records_select($table, "contextid = :contextid AND userid $insql", $params);
}
}
+202
View File
@@ -0,0 +1,202 @@
<?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/>.
/**
* Log store manager.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\log;
defined('MOODLE_INTERNAL') || die();
class manager implements \core\log\manager {
/** @var \core\log\reader[] $readers list of initialised log readers */
protected $readers;
/** @var \tool_log\log\writer[] $writers list of initialised log writers */
protected $writers;
/** @var \tool_log\log\store[] $stores list of all enabled stores */
protected $stores;
/**
* Delayed initialisation of singleton.
*/
protected function init() {
if (isset($this->stores)) {
// Do not bother checking readers and writers here
// because everything is init here.
return;
}
$this->stores = array();
$this->readers = array();
$this->writers = array();
// Register shutdown handler - this may be useful for buffering, file handle closing, etc.
\core_shutdown_manager::register_function(array($this, 'dispose'));
$plugins = get_config('tool_log', 'enabled_stores');
if (empty($plugins)) {
return;
}
$plugins = explode(',', $plugins);
foreach ($plugins as $plugin) {
$classname = "\\$plugin\\log\\store";
if (class_exists($classname)) {
$store = new $classname($this);
$this->stores[$plugin] = $store;
if ($store instanceof \tool_log\log\writer) {
$this->writers[$plugin] = $store;
}
if ($store instanceof \core\log\reader) {
$this->readers[$plugin] = $store;
}
}
}
}
/**
* Called from the observer only.
*
* @param \core\event\base $event
*/
public function process(\core\event\base $event) {
$this->init();
foreach ($this->writers as $plugin => $writer) {
try {
$writer->write($event, $this);
} catch (\Exception $e) {
debugging('Exception detected when logging event ' . $event->eventname . ' in ' . $plugin . ': ' .
$e->getMessage(), DEBUG_NORMAL, $e->getTrace());
}
}
}
/**
* Returns list of available log readers.
*
* This way the reports find out available sources of data.
*
* @param string $interface Returned stores must implement this interface.
*
* @return \core\log\reader[] list of available log data readers
*/
public function get_readers($interface = null) {
$this->init();
$return = array();
foreach ($this->readers as $plugin => $reader) {
if (empty($interface) || ($reader instanceof $interface)) {
$return[$plugin] = $reader;
}
}
return $return;
}
/**
* Get a list of reports that support the given store instance.
*
* @param string $logstore Name of the store.
*
* @return array List of supported reports
*/
public function get_supported_reports($logstore) {
$allstores = self::get_store_plugins();
if (empty($allstores[$logstore])) {
// Store doesn't exist.
return array();
}
$reports = get_plugin_list_with_function('report', 'supports_logstore', 'lib.php');
$enabled = $this->stores;
if (empty($enabled[$logstore])) {
// Store is not enabled, init an instance.
$classname = '\\' . $logstore . '\log\store';
$instance = new $classname($this);
} else {
$instance = $enabled[$logstore];
}
$return = array();
foreach ($reports as $report => $fulldir) {
if (component_callback($report, 'supports_logstore', array($instance), false)) {
$return[$report] = get_string('pluginname', $report);
}
}
return $return;
}
/**
* For a given report, returns a list of log stores that are supported.
*
* @param string $component component.
*
* @return false|array list of logstores that support the given report. It returns false if the given $component doesn't
* require logstores.
*/
public function get_supported_logstores($component) {
$allstores = self::get_store_plugins();
$enabled = $this->stores;
$function = component_callback_exists($component, 'supports_logstore');
if (!$function) {
// The report doesn't define the callback, most probably it doesn't need log stores.
return false;
}
$return = array();
foreach ($allstores as $store => $logclass) {
$instance = empty($enabled[$store]) ? new $logclass($this) : $enabled[$store];
if ($function($instance)) {
$return[$store] = get_string('pluginname', $store);
}
}
return $return;
}
/**
* Intended for store management, do not use from reports.
*
* @return store[] Returns list of available store plugins.
*/
public static function get_store_plugins() {
return \core_component::get_plugin_list_with_class('logstore', 'log\store');
}
/**
* Usually called automatically from shutdown manager,
* this allows us to implement buffering of write operations.
*/
public function dispose() {
if ($this->stores) {
foreach ($this->stores as $store) {
$store->dispose();
}
}
$this->stores = null;
$this->readers = null;
$this->writers = null;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?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/>.
/**
* Event observer.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\log;
defined('MOODLE_INTERNAL') || die();
class observer {
/**
* Redirect all events to this log manager, but only if this
* log manager is actually used.
*
* @param \core\event\base $event
*/
public static function store(\core\event\base $event) {
$logmanager = get_log_manager();
if (get_class($logmanager) === 'tool_log\log\manager') {
/** @var \tool_log\log\manager $logmanager */
$logmanager->process($event);
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<?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/>.
/**
* Log store interface.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\log;
defined('MOODLE_INTERNAL') || die();
interface store {
/**
* Create new instance of store,
* the calling code must make sure only one instance exists.
*
* @param manager $manager
*/
public function __construct(\tool_log\log\manager $manager);
/**
* Notify store no more events are going to be written/read from it.
* @return void
*/
public function dispose();
}
+37
View File
@@ -0,0 +1,37 @@
<?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/>.
/**
* Log store writer interface.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\log;
defined('MOODLE_INTERNAL') || die();
interface writer extends store {
/**
* Write one event to the store.
*
* @param \core\event\base $event
* @return void
*/
public function write(\core\event\base $event);
}
@@ -0,0 +1,122 @@
<?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/>.
/**
* Subplugin info class.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\plugininfo;
use admin_settingpage;
use core\plugininfo\base;
use moodle_url;
use part_of_admin_tree;
/**
* Plugin info class for logging store plugins.
*/
class logstore extends base {
public static function plugintype_supports_disabling(): bool {
return true;
}
public function is_enabled() {
$enabled = get_config('tool_log', 'enabled_stores');
if (!$enabled) {
return false;
}
$enabled = array_flip(explode(',', $enabled));
return isset($enabled['logstore_' . $this->name]);
}
public static function enable_plugin(string $pluginname, int $enabled): bool {
$haschanged = false;
$plugins = [];
$oldvalue = get_config('tool_log', 'enabled_stores');
if (!empty($oldvalue)) {
$plugins = array_flip(explode(',', $oldvalue));
}
// Only set visibility if it's different from the current value.
if ($enabled && !array_key_exists($pluginname, $plugins)) {
$plugins[$pluginname] = $pluginname;
$haschanged = true;
} else if (!$enabled && array_key_exists($pluginname, $plugins)) {
unset($plugins[$pluginname]);
$haschanged = true;
}
if ($haschanged) {
$new = implode(',', array_flip($plugins));
add_to_config_log('tool_logstore_visibility', !$enabled, $enabled, $pluginname);
set_config('enabled_stores', $new, 'tool_log');
// Reset caches.
\core_plugin_manager::reset_caches();
}
return $haschanged;
}
public function get_settings_section_name() {
return 'logsetting' . $this->name;
}
public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
/** @var \admin_root $ADMIN */
$ADMIN = $adminroot; // May be used in settings.php.
$section = $this->get_settings_section_name();
if (!$this->is_installed_and_upgraded()) {
return;
}
if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
return;
}
$settings = new admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
include($this->full_path('settings.php'));
if ($settings) {
$ADMIN->add($parentnodename, $settings);
}
}
public static function get_manage_url() {
return new moodle_url('/admin/settings.php', array('section' => 'managelogging'));
}
public function is_uninstall_allowed() {
return true;
}
public function uninstall_cleanup() {
$enabled = get_config('tool_log', 'enabled_stores');
if ($enabled) {
$enabled = array_flip(explode(',', $enabled));
unset($enabled['logstore_' . $this->name]);
$enabled = array_flip($enabled);
set_config('enabled_stores', implode(',', $enabled), 'tool_log');
}
parent::uninstall_cleanup();
}
}
+137
View File
@@ -0,0 +1,137 @@
<?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/>.
/**
* Data provider.
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\privacy;
defined('MOODLE_INTERNAL') || die();
use context;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use tool_log\log\manager;
/**
* Data provider class.
*
* @package tool_log
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\subsystem\provider,
\core_privacy\local\request\core_userlist_provider {
/**
* Returns metadata.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_plugintype_link('logstore', [], 'privacy:metadata:logstore');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {
$contextlist = new \core_privacy\local\request\contextlist();
static::call_subplugins_method_with_args('add_contexts_for_userid', [$contextlist, $userid]);
return $contextlist;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param \core_privacy\local\request\userlist $userlist The userlist containing the list of users who have data in
* this context/plugin combination.
*/
public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
$interface = \tool_log\local\privacy\logstore_userlist_provider::class;
static::call_subplugins_method_with_args('add_userids_for_context', [$userlist], $interface);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
if (get_config('tool_log', 'exportlog')) {
static::call_subplugins_method_with_args('export_user_data', [$contextlist]);
}
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(context $context) {
static::call_subplugins_method_with_args('delete_data_for_all_users_in_context', [$context]);
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
static::call_subplugins_method_with_args('delete_data_for_user', [$contextlist]);
}
/**
* Delete multiple users within a single context.
*
* @param \core_privacy\local\request\approved_userlist $userlist The approved context and user information to delete
* information for.
*/
public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
$interface = \tool_log\local\privacy\logstore_userlist_provider::class;
static::call_subplugins_method_with_args('delete_data_for_userlist', [$userlist], $interface);
}
/**
* Invoke the subplugins method with arguments.
*
* @param string $method The method name.
* @param array $args The arguments.
* @param string $interface The interface to use. By default uses the logstore_provider.
* @return void
*/
protected static function call_subplugins_method_with_args($method, array $args = [], string $interface = null) {
if (!isset($interface)) {
$interface = \tool_log\local\privacy\logstore_provider::class;
}
\core_privacy\manager::plugintype_class_callback('logstore', $interface, $method, $args);
}
}
@@ -0,0 +1,243 @@
<?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/>.
/**
* Store management setting.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/adminlib.php");
class tool_log_setting_managestores extends admin_setting {
/**
* Calls parent::__construct with specific arguments
*/
public function __construct() {
$this->nosave = true;
parent::__construct('tool_log_manageui', get_string('managelogging', 'tool_log'), '', '');
}
/**
* Always returns true, does nothing.
*
* @return true
*/
public function get_setting() {
return true;
}
/**
* Always returns true, does nothing.
*
* @return true
*/
public function get_defaultsetting() {
return true;
}
/**
* Always returns '', does not write anything.
*
* @param mixed $data ignored
* @return string Always returns ''
*/
public function write_setting($data) {
// Do not write any setting.
return '';
}
/**
* Checks if $query is one of the available log plugins.
*
* @param string $query The string to search for
* @return bool Returns true if found, false if not
*/
public function is_related($query) {
if (parent::is_related($query)) {
return true;
}
$query = core_text::strtolower($query);
$plugins = \tool_log\log\manager::get_store_plugins();
foreach ($plugins as $plugin => $fulldir) {
if (strpos(core_text::strtolower($plugin), $query) !== false) {
return true;
}
$localised = get_string('pluginname', $plugin);
if (strpos(core_text::strtolower($localised), $query) !== false) {
return true;
}
}
return false;
}
/**
* Builds the XHTML to display the control.
*
* @param string $data Unused
* @param string $query
* @return string
*/
public function output_html($data, $query = '') {
global $OUTPUT, $PAGE;
// Display strings.
$strup = get_string('up');
$strdown = get_string('down');
$strsettings = get_string('settings');
$strenable = get_string('enable');
$strdisable = get_string('disable');
$struninstall = get_string('uninstallplugin', 'core_admin');
$strversion = get_string('version');
$pluginmanager = core_plugin_manager::instance();
$logmanager = new \tool_log\log\manager();
$available = $logmanager->get_store_plugins();
$enabled = get_config('tool_log', 'enabled_stores');
if (!$enabled) {
$enabled = array();
} else {
$enabled = array_flip(explode(',', $enabled));
}
$allstores = array();
foreach ($enabled as $key => $store) {
$allstores[$key] = true;
$enabled[$key] = true;
}
foreach ($available as $key => $store) {
$allstores[$key] = true;
$available[$key] = true;
}
$return = $OUTPUT->heading(get_string('actlogshdr', 'tool_log'), 3, 'main', true);
$return .= $OUTPUT->box_start('generalbox loggingui');
$table = new html_table();
$table->head = array(get_string('name'), get_string('reportssupported', 'tool_log'), $strversion, $strenable,
$strup . '/' . $strdown, $strsettings, $struninstall);
$table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign',
'centeralign');
$table->id = 'logstoreplugins';
$table->attributes['class'] = 'admintable generaltable';
$table->data = array();
// Iterate through store plugins and add to the display table.
$updowncount = 1;
$storecount = count($enabled);
$url = new moodle_url('/admin/tool/log/stores.php', array('sesskey' => sesskey()));
$printed = array();
foreach ($allstores as $store => $unused) {
$plugininfo = $pluginmanager->get_plugin_info($store);
$version = get_config($store, 'version');
if ($version === false) {
$version = '';
}
if (get_string_manager()->string_exists('pluginname', $store)) {
$name = get_string('pluginname', $store);
} else {
$name = $store;
}
$reports = $logmanager->get_supported_reports($store);
if (!empty($reports)) {
$supportedreports = implode(', ', $reports);
} else {
$supportedreports = '-';
}
// Hide/show links.
if (isset($enabled[$store])) {
$aurl = new moodle_url($url, array('action' => 'disable', 'store' => $store));
$hideshow = "<a href=\"$aurl\">";
$hideshow .= $OUTPUT->pix_icon('t/hide', $strdisable) . '</a>';
$isenabled = true;
$displayname = "<span>$name</span>";
} else {
if (isset($available[$store])) {
$aurl = new moodle_url($url, array('action' => 'enable', 'store' => $store));
$hideshow = "<a href=\"$aurl\">";
$hideshow .= $OUTPUT->pix_icon('t/show', $strenable) . '</a>';
$isenabled = false;
$displayname = "<span>$name</span>";
} else {
$hideshow = '';
$isenabled = false;
$displayname = '<span class="notifyproblem">' . $name . '</span>';
}
}
if ($PAGE->theme->resolve_image_location('icon', $store, false)) {
$icon = $OUTPUT->pix_icon('icon', '', $store, array('class' => 'icon pluginicon'));
} else {
$icon = $OUTPUT->spacer();
}
// Up/down link (only if store is enabled).
$updown = '';
if ($isenabled) {
if ($updowncount > 1) {
$aurl = new moodle_url($url, array('action' => 'up', 'store' => $store));
$updown .= "<a href=\"$aurl\">";
$updown .= $OUTPUT->pix_icon('t/up', $strup) . '</a>&nbsp;';
} else {
$updown .= $OUTPUT->spacer();
}
if ($updowncount < $storecount) {
$aurl = new moodle_url($url, array('action' => 'down', 'store' => $store));
$updown .= "<a href=\"$aurl\">";
$updown .= $OUTPUT->pix_icon('t/down', $strdown) . '</a>&nbsp;';
} else {
$updown .= $OUTPUT->spacer();
}
++$updowncount;
}
// Add settings link.
if (!$version) {
$settings = '';
} else {
if ($surl = $plugininfo->get_settings_url()) {
$settings = html_writer::link($surl, $strsettings);
} else {
$settings = '';
}
}
// Add uninstall info.
$uninstall = '';
if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url($store, 'manage')) {
$uninstall = html_writer::link($uninstallurl, $struninstall);
}
// Add a row to the table.
$table->data[] = array($icon . $displayname, $supportedreports, $version, $hideshow, $updown, $settings, $uninstall);
$table->rowclasses[] = $isenabled ? '' : 'dimmed_text';
$printed[$store] = true;
}
$return .= html_writer::table($table);
$return .= get_string('configlogplugins', 'tool_log') . '<br />' . get_string('tablenosave', 'admin');
$return .= $OUTPUT->box_end();
return highlight($query, $return);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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/>.
/**
* Event observer.
*
* @package tool_log
* @category event
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$observers = array(
array(
'eventname' => '*',
'callback' => '\tool_log\log\observer::store',
'internal' => false, // This means that we get events only after transaction commit.
'priority' => 1000,
),
);
+41
View File
@@ -0,0 +1,41 @@
<?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/>.
/**
* Logging support.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Install the plugin.
*/
function xmldb_tool_log_install() {
global $CFG, $DB;
$enabled = array();
// Add data to new log only from now on.
if (file_exists("$CFG->dirroot/$CFG->admin/tool/log/store/standard")) {
$enabled[] = 'logstore_standard';
}
set_config('enabled_stores', implode(',', $enabled), 'tool_log');
}
+5
View File
@@ -0,0 +1,5 @@
{
"plugintypes": {
"logstore": "admin\/tool\/log\/store"
}
}
+45
View File
@@ -0,0 +1,45 @@
<?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/>.
/**
* Logging support.
*
* @package tool_log
* @copyright 2014 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Upgrade the plugin.
*
* @param int $oldversion
* @return bool always true
*/
function xmldb_tool_log_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
+40
View File
@@ -0,0 +1,40 @@
<?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/>.
/**
* Store management UI lang strings.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['actlogshdr'] = 'Available log stores';
$string['configlogplugins'] = 'Please enable all required plugins and arrange them in appropriate order.';
$string['exportlog'] = 'Include logs when exporting';
$string['exportlogdetail'] = 'Include logs that relate to the user when exporting.';
$string['logging'] = 'Logging';
$string['managelogging'] = 'Manage log stores';
$string['pluginname'] = 'Log store manager';
$string['privacy:metadata:logstore'] = 'The log stores';
$string['privacy:path:logs'] = 'Logs';
$string['privacy:request:origin:cli'] = 'Command line tool';
$string['privacy:request:origin:restore'] = 'Backup being restored';
$string['privacy:request:origin:web'] = 'Standard web request';
$string['privacy:request:origin:ws'] = 'Mobile app or web service';
$string['reportssupported'] = 'Reports supported';
$string['subplugintype_logstore'] = 'Log store';
$string['subplugintype_logstore_plural'] = 'Log stores';
+48
View File
@@ -0,0 +1,48 @@
<?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/>.
/**
* Logging settings.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
$privacysettings = $ADMIN->locate('privacysettings');
if ($ADMIN->fulltree) {
$privacysettings->add(new admin_setting_configcheckbox('tool_log/exportlog',
new lang_string('exportlog', 'tool_log'),
new lang_string('exportlogdetail', 'tool_log'), 1)
);
}
$ADMIN->add('modules', new admin_category('logging', new lang_string('logging', 'tool_log')));
$temp = new admin_settingpage('managelogging', new lang_string('managelogging', 'tool_log'));
$temp->add(new tool_log_setting_managestores());
$ADMIN->add('logging', $temp);
foreach (core_plugin_manager::instance()->get_plugins_of_type('logstore') as $plugin) {
/** @var \tool_log\plugininfo\logstore $plugin */
$plugin->load_settings($ADMIN, 'logging', $hassiteconfig);
}
}
@@ -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/>.
/**
* Backup implementation for the (tool_log) logstore_database nested element.
*
* @package logstore_database
* @category backup
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Custom subclass of backup_nested_element that iterates over an external DB connection.
*
* @package logstore_database
* @category backup
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_logstore_database_nested_element extends backup_nested_element {
/**
* @var \moodle_database $sourcedb
*/
protected $sourcedb;
/**
* Constructor - instantiates one backup_nested_element, specifying its basic info.
*
* @param string $name name of the element
* @param array $attributes attributes this element will handle (optional, defaults to null)
* @param array $finalelements this element will handle (optional, defaults to null)
*/
public function __construct($name, $attributes = null, $finalelements = null) {
global $DB;
parent::__construct($name, $attributes, $finalelements);
$this->sourcedb = $DB;
}
/**
* For sql or table datasources, this will iterate over the "external" DB connection
* stored in this class instead of the default $DB. All other cases use the parent default.
* @param object $processor the processor
*/
protected function get_iterator($processor) {
if ($this->get_source_table() !== null) { // It's one table, return recordset iterator.
return $this->get_source_db()->get_recordset(
$this->get_source_table(),
backup_structure_dbops::convert_params_to_values($this->procparams, $processor),
$this->get_source_table_sortby()
);
} else if ($this->get_source_sql() !== null) { // It's one sql, return recordset iterator.
return $this->get_source_db()->get_recordset_sql(
$this->get_source_sql(),
backup_structure_dbops::convert_params_to_values($this->procparams, $processor)
);
}
return parent::get_iterator($processor);
}
/**
* Set the database we want to use.
*
* @param \moodle_database $db
*/
public function set_source_db($db) {
$this->sourcedb = $db;
}
/**
* Get the database we want to use.
*
* @return \moodle_database $db
*/
public function get_source_db() {
return $this->sourcedb;
}
}
@@ -0,0 +1,65 @@
<?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/>.
/**
* Backup implementation for the (tool_log) logstore_database subplugin.
*
* @package logstore_database
* @category backup
* @copyright 2015 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once('backup_logstore_database_nested_element.php');
class backup_logstore_database_subplugin extends backup_tool_log_logstore_subplugin {
/**
* Returns the subplugin structure to attach to the 'logstore' XML element.
*
* @return backup_subplugin_element the subplugin structure to be attached.
*/
protected function define_logstore_subplugin_structure() {
$subplugin = $this->get_subplugin_element();
$subpluginwrapper = new backup_nested_element($this->get_recommended_name());
// Create the custom (base64 encoded, xml safe) 'other' final element.
$otherelement = new base64_encode_final_element('other');
$subpluginlog = new backup_logstore_database_nested_element('logstore_database_log', array('id'), array(
'eventname', 'component', 'action', 'target', 'objecttable',
'objectid', 'crud', 'edulevel', 'contextid', 'userid', 'relateduserid',
'anonymous', $otherelement, 'timecreated', 'ip', 'realuserid'));
$subplugin->add_child($subpluginwrapper);
$subpluginwrapper->add_child($subpluginlog);
// Get the details for the external database.
$manager = new \tool_log\log\manager();
$store = new \logstore_database\log\store($manager);
$extdb = $store->get_extdb();
if (!$extdb) {
return false;
}
$subpluginlog->set_source_db($extdb);
$subpluginlog->set_source_table($store->get_config_value('dbtable'), array('contextid' => backup::VAR_CONTEXTID));
return $subplugin;
}
}
@@ -0,0 +1,102 @@
<?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/>.
/**
* Restore implementation for the (tool_log) logstore_database subplugin.
*
* @package logstore_database
* @category backup
* @copyright 2015 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
class restore_logstore_database_subplugin extends restore_tool_log_logstore_subplugin {
/**
* @var moodle_database the external database.
*/
private static $extdb = null;
/**
* @var string the external database table name.
*/
private static $extdbtablename = null;
/**
* The constructor for this logstore.
*
* @param string $subplugintype the subplugin type.
* @param string $subpluginname the subplugin name.
* @param restore_structure_step $step.
*/
public function __construct($subplugintype, $subpluginname, $step) {
// Check that the logstore is enabled before setting variables.
$enabledlogstores = explode(',', get_config('tool_log', 'enabled_stores'));
if (in_array('logstore_database', $enabledlogstores)) {
$manager = new \tool_log\log\manager();
$store = new \logstore_database\log\store($manager);
self::$extdb = $store->get_extdb();
self::$extdbtablename = $store->get_config_value('dbtable');
}
parent::__construct($subplugintype, $subpluginname, $step);
}
/**
* Returns the subplugin structure to attach to the 'logstore' XML element.
*
* @return restore_path_element[] array of elements to be processed on restore.
*/
protected function define_logstore_subplugin_structure() {
// If the logstore is not enabled we don't add structures for it.
$enabledlogstores = explode(',', get_config('tool_log', 'enabled_stores'));
if (!in_array('logstore_database', $enabledlogstores)) {
return array(); // The logstore is not enabled, nothing to restore.
}
$paths = array();
$elename = $this->get_namefor('log');
$elepath = $this->get_pathfor('/logstore_database_log');
$paths[] = new restore_path_element($elename, $elepath);
return $paths;
}
/**
* Process logstore_database_log entries.
*
* This method proceeds to read, complete, remap and, finally,
* discard or save every log entry.
*
* @param array() $data log entry.
* @return null if we are not restoring the log.
*/
public function process_logstore_database_log($data) {
// Do not bother processing if we can not add it to a database.
if (!self::$extdb || !self::$extdbtablename) {
return;
}
$data = $this->process_log($data, get_config('logstore_database', 'jsonformat'));
if ($data) {
self::$extdb->insert_record(self::$extdbtablename, $data);
}
}
}
@@ -0,0 +1,77 @@
<?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/>.
/**
* Helper class locally used.
*
* @package logstore_database
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_database;
defined('MOODLE_INTERNAL') || die();
/**
* Helper class locally used.
*
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Returns list of fully working database drivers present in system.
* @return array
*/
public static function get_drivers() {
return array(
'' => get_string('choosedots'),
'native/mysqli' => \moodle_database::get_driver_instance('mysqli', 'native')->get_name(),
'native/mariadb' => \moodle_database::get_driver_instance('mariadb', 'native')->get_name(),
'native/pgsql' => \moodle_database::get_driver_instance('pgsql', 'native')->get_name(),
'native/oci' => \moodle_database::get_driver_instance('oci', 'native')->get_name(),
'native/sqlsrv' => \moodle_database::get_driver_instance('sqlsrv', 'native')->get_name()
);
}
/**
* Get a list of edu levels.
*
* @return array
*/
public static function get_level_options() {
return array(
\core\event\base::LEVEL_TEACHING => get_string('teaching', 'logstore_database'),
\core\event\base::LEVEL_PARTICIPATING => get_string('participating', 'logstore_database'),
\core\event\base::LEVEL_OTHER => get_string('other', 'logstore_database'),
);
}
/**
* Get a list of database actions.
*
* @return array
*/
public static function get_action_options() {
return array(
'c' => get_string('create', 'logstore_database'),
'r' => get_string('read', 'logstore_database'),
'u' => get_string('update', 'logstore_database'),
'd' => get_string('delete')
);
}
}
@@ -0,0 +1,331 @@
<?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/>.
/**
* External database store.
*
* @package logstore_database
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_database\log;
defined('MOODLE_INTERNAL') || die();
class store implements \tool_log\log\writer, \core\log\sql_reader {
use \tool_log\helper\store,
\tool_log\helper\reader,
\tool_log\helper\buffered_writer {
dispose as helper_dispose;
}
/** @var \moodle_database $extdb */
protected $extdb;
/** @var bool $logguests true if logging guest access */
protected $logguests;
/** @var array $includelevels An array of education levels to include */
protected $includelevels = array();
/** @var array $includeactions An array of actions types to include */
protected $includeactions = array();
/**
* Construct
*
* @param \tool_log\log\manager $manager
*/
public function __construct(\tool_log\log\manager $manager) {
$this->helper_setup($manager);
$this->buffersize = $this->get_config('buffersize', 50);
$this->logguests = $this->get_config('logguests', 1);
$actions = $this->get_config('includeactions', '');
$levels = $this->get_config('includelevels', '');
$this->includeactions = $actions === '' ? array() : explode(',', $actions);
$this->includelevels = $levels === '' ? array() : explode(',', $levels);
// JSON writing defaults to false (table format compatibility with older versions).
// Note: This variable is defined in the buffered_writer trait.
$this->jsonformat = (bool)$this->get_config('jsonformat', false);
}
/**
* Setup the Database.
*
* @return bool
*/
protected function init() {
if (isset($this->extdb)) {
return !empty($this->extdb);
}
$dbdriver = $this->get_config('dbdriver');
if (empty($dbdriver)) {
$this->extdb = false;
return false;
}
list($dblibrary, $dbtype) = explode('/', $dbdriver);
if (!$db = \moodle_database::get_driver_instance($dbtype, $dblibrary, true)) {
debugging("Unknown driver $dblibrary/$dbtype", DEBUG_DEVELOPER);
$this->extdb = false;
return false;
}
$dboptions = array();
$dboptions['dbpersist'] = $this->get_config('dbpersist', '0');
$dboptions['dbsocket'] = $this->get_config('dbsocket', '');
$dboptions['dbport'] = $this->get_config('dbport', '');
$dboptions['dbschema'] = $this->get_config('dbschema', '');
$dboptions['dbcollation'] = $this->get_config('dbcollation', '');
$dboptions['dbhandlesoptions'] = $this->get_config('dbhandlesoptions', false);
try {
$db->connect($this->get_config('dbhost'), $this->get_config('dbuser'), $this->get_config('dbpass'),
$this->get_config('dbname'), false, $dboptions);
$tables = $db->get_tables();
if (!in_array($this->get_config('dbtable'), $tables)) {
debugging('Cannot find the specified table', DEBUG_DEVELOPER);
$this->extdb = false;
return false;
}
} catch (\moodle_exception $e) {
debugging('Cannot connect to external database: ' . $e->getMessage(), DEBUG_DEVELOPER);
$this->extdb = false;
return false;
}
$this->extdb = $db;
return true;
}
/**
* Should the event be ignored (== not logged)?
* @param \core\event\base $event
* @return bool
*/
protected function is_event_ignored(\core\event\base $event) {
if (!in_array($event->crud, $this->includeactions) &&
!in_array($event->edulevel, $this->includelevels)
) {
// Ignore event if the store settings do not want to store it.
return true;
}
if ((!CLI_SCRIPT or PHPUNIT_TEST) and !$this->logguests) {
// Always log inside CLI scripts because we do not login there.
if (!isloggedin() or isguestuser()) {
return true;
}
}
return false;
}
/**
* Insert events in bulk to the database.
*
* @param array $evententries raw event data
*/
protected function insert_event_entries($evententries) {
if (!$this->init()) {
return;
}
if (!$dbtable = $this->get_config('dbtable')) {
return;
}
try {
$this->extdb->insert_records($dbtable, $evententries);
} catch (\moodle_exception $e) {
debugging('Cannot write to external database: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
}
/**
* Get an array of events based on the passed on params.
*
* @param string $selectwhere select conditions.
* @param array $params params.
* @param string $sort sortorder.
* @param int $limitfrom limit constraints.
* @param int $limitnum limit constraints.
*
* @return array|\core\event\base[] array of events.
*/
public function get_events_select($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
if (!$this->init()) {
return array();
}
if (!$dbtable = $this->get_config('dbtable')) {
return array();
}
$sort = self::tweak_sort_by_id($sort);
$events = array();
$records = $this->extdb->get_records_select($dbtable, $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
foreach ($records as $data) {
if ($event = $this->get_log_event($data)) {
$events[$data->id] = $event;
}
}
return $events;
}
/**
* Fetch records using given criteria returning a Traversable object.
*
* Note that the traversable object contains a moodle_recordset, so
* remember that is important that you call close() once you finish
* using it.
*
* @param string $selectwhere
* @param array $params
* @param string $sort
* @param int $limitfrom
* @param int $limitnum
* @return \core\dml\recordset_walk|\core\event\base[]
*/
public function get_events_select_iterator($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
if (!$this->init()) {
return array();
}
if (!$dbtable = $this->get_config('dbtable')) {
return array();
}
$sort = self::tweak_sort_by_id($sort);
$recordset = $this->extdb->get_recordset_select($dbtable, $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
return new \core\dml\recordset_walk($recordset, array($this, 'get_log_event'));
}
/**
* Returns an event from the log data.
*
* @param stdClass $data Log data
* @return \core\event\base
*/
public function get_log_event($data) {
$extra = array('origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid);
$data = (array)$data;
$id = $data['id'];
$data['other'] = self::decode_other($data['other']);
if ($data['other'] === false) {
$data['other'] = array();
}
unset($data['origin']);
unset($data['ip']);
unset($data['realuserid']);
unset($data['id']);
if (!$event = \core\event\base::restore($data, $extra)) {
return null;
}
return $event;
}
/**
* Get number of events present for the given select clause.
*
* @param string $selectwhere select conditions.
* @param array $params params.
*
* @return int Number of events available for the given conditions
*/
public function get_events_select_count($selectwhere, array $params) {
if (!$this->init()) {
return 0;
}
if (!$dbtable = $this->get_config('dbtable')) {
return 0;
}
return $this->extdb->count_records_select($dbtable, $selectwhere, $params);
}
/**
* Get whether events are present for the given select clause.
*
* @param string $selectwhere select conditions.
* @param array $params params.
*
* @return bool Whether events available for the given conditions
*/
public function get_events_select_exists(string $selectwhere, array $params): bool {
if (!$this->init()) {
return false;
}
if (!$dbtable = $this->get_config('dbtable')) {
return false;
}
return $this->extdb->record_exists_select($dbtable, $selectwhere, $params);
}
/**
* Get a config value for the store.
*
* @param string $name Config name
* @param mixed $default default value
* @return mixed config value if set, else the default value.
*/
public function get_config_value($name, $default = null) {
return $this->get_config($name, $default);
}
/**
* Get the external database object.
*
* @return \moodle_database $extdb
*/
public function get_extdb() {
if (!$this->init()) {
return false;
}
return $this->extdb;
}
/**
* Are the new events appearing in the reader?
*
* @return bool true means new log events are being added, false means no new data will be added
*/
public function is_logging() {
if (!$this->init()) {
return false;
}
return true;
}
/**
* Dispose off database connection after pushing any buffered events to the database.
*/
public function dispose() {
$this->helper_dispose();
if ($this->extdb) {
$this->extdb->dispose();
}
$this->extdb = null;
}
}
@@ -0,0 +1,147 @@
<?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/>.
/**
* Data provider.
*
* @package logstore_database
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_database\privacy;
defined('MOODLE_INTERNAL') || die();
use context;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\contextlist;
/**
* Data provider class.
*
* @package logstore_database
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\tool_log\local\privacy\logstore_provider,
\tool_log\local\privacy\logstore_userlist_provider {
use \tool_log\local\privacy\moodle_database_export_and_delete;
/**
* Returns metadata.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_external_location_link('log', [
'eventname' => 'privacy:metadata:log:eventname',
'userid' => 'privacy:metadata:log:userid',
'relateduserid' => 'privacy:metadata:log:relateduserid',
'anonymous' => 'privacy:metadata:log:anonymous',
'other' => 'privacy:metadata:log:other',
'timecreated' => 'privacy:metadata:log:timecreated',
'origin' => 'privacy:metadata:log:origin',
'ip' => 'privacy:metadata:log:ip',
'realuserid' => 'privacy:metadata:log:realuserid',
], 'privacy:metadata:log');
return $collection;
}
/**
* Add contexts that contain user information for the specified user.
*
* @param contextlist $contextlist The contextlist to add the contexts to.
* @param int $userid The user to find the contexts for.
* @return void
*/
public static function add_contexts_for_userid(contextlist $contextlist, $userid) {
list($db, $table) = static::get_database_and_table();
if (!$db || !$table) {
return;
}
$sql = 'userid = :userid1 OR relateduserid = :userid2 OR realuserid = :userid3';
$params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
$contextids = $db->get_fieldset_select($table, 'DISTINCT contextid', $sql, $params);
if (empty($contextids)) {
return;
}
$sql = implode(' UNION ', array_map(function($id) use ($db) {
return 'SELECT ' . $id . $db->sql_null_from_clause();
}, $contextids));
$contextlist->add_from_sql($sql, []);
}
/**
* Add user IDs that contain user information for the specified context.
*
* @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
* @return void
*/
public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist) {
list($db, $table) = static::get_database_and_table();
if (!$db || !$table) {
return;
}
$userids = [];
$records = $db->get_records($table, ['contextid' => $userlist->get_context()->id], '',
'id, userid, relateduserid, realuserid');
if (empty($records)) {
return;
}
foreach ($records as $record) {
$userids[] = $record->userid;
if (!empty($record->relateduserid)) {
$userids[] = $record->relateduserid;
}
if (!empty($record->realuserid)) {
$userids[] = $record->realuserid;
}
}
$userids = array_unique($userids);
$userlist->add_users($userids);
}
/**
* Get the database object.
*
* @return array Containing moodle_database, string, or null values.
*/
protected static function get_database_and_table() {
$manager = get_log_manager();
$store = new \logstore_database\log\store($manager);
$db = $store->get_extdb();
return $db ? [$db, $store->get_config_value('dbtable')] : [null, null];
}
/**
* Get the path to export the logs to.
*
* @return array
*/
protected static function get_export_subcontext() {
return [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_database')];
}
}
@@ -0,0 +1,39 @@
<?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/>.
/**
* Database log store upgrade.
*
* @package logstore_database
* @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
function xmldb_logstore_database_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,65 @@
<?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/>.
/**
* Log store lang strings.
*
* @package logstore_database
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['buffersize'] = 'Buffer size';
$string['buffersize_help'] = 'Number of log entries inserted in one batch database operation, which improves performance.';
$string['conectexception'] = 'Cannot connect to the database.';
$string['create'] = 'Create';
$string['databasesettings'] = 'Database settings';
$string['databasesettings_help'] = 'Connection details for the external log database: {$a}';
$string['databasepersist'] = 'Persistent database connections';
$string['databaseschema'] = 'Database schema';
$string['databasecollation'] = 'Database collation';
$string['databasehandlesoptions'] = 'Database handles options';
$string['databasehandlesoptions_help'] = 'Does the remote database handle its own options.';
$string['databasetable'] = 'Database table';
$string['databasetable_help'] = 'Name of the table where logs will be stored. This table should have a structure identical to the one used by logstore_standard (mdl_logstore_standard_log).';
$string['includeactions'] = 'Include actions of these types';
$string['includelevels'] = 'Include actions with these educational levels';
$string['filters'] = 'Filter logs';
$string['filters_help'] = 'Enable filters that exclude some actions from being logged.';
$string['jsonformat'] = 'JSON format';
$string['jsonformat_desc'] = 'Use standard JSON format instead of PHP serialised data in the \'other\' database field.';
$string['logguests'] = 'Log guest actions';
$string['other'] = 'Other';
$string['participating'] = 'Participating';
$string['pluginname'] = 'External database log';
$string['pluginname_desc'] = 'A log plugin that stores log entries in an external database table.';
$string['privacy:metadata:log'] = 'A collection of past events';
$string['privacy:metadata:log:anonymous'] = 'Whether the event was flagged as anonymous';
$string['privacy:metadata:log:eventname'] = 'The event name';
$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
$string['privacy:metadata:log:origin'] = 'The origin of the event';
$string['privacy:metadata:log:other'] = 'Additional information about the event';
$string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
$string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
$string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
$string['read'] = 'Read';
$string['tablenotfound'] = 'Specified table was not found';
$string['teaching'] = 'Teaching';
$string['testsettings'] = 'Test connection';
$string['testingsettings'] = 'Testing database settings...';
$string['update'] = 'Update';
@@ -0,0 +1,77 @@
<?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/>.
/**
* External database log store settings.
*
* @package logstore_database
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
$testurl = new moodle_url('/admin/tool/log/store/database/test_settings.php');
$test = new admin_externalpage('logstoredbtestsettings', get_string('testsettings', 'logstore_database'),
$testurl, 'moodle/site:config', true);
$ADMIN->add('logging', $test);
$drivers = \logstore_database\helper::get_drivers();
// Database settings.
$link = html_writer::link($testurl, get_string('testsettings', 'logstore_database'), array('target' => '_blank'));
$settings->add(new admin_setting_heading('dbsettings', get_string('databasesettings', 'logstore_database'),
get_string('databasesettings_help', 'logstore_database', $link)));
$settings->add(new admin_setting_configselect('logstore_database/dbdriver', get_string('databasetypehead', 'install'), '',
'', $drivers));
$settings->add(new admin_setting_configtext('logstore_database/dbhost', get_string('databasehost', 'install'), '', ''));
$settings->add(new admin_setting_configtext('logstore_database/dbuser', get_string('databaseuser', 'install'), '', ''));
$settings->add(new admin_setting_configpasswordunmask('logstore_database/dbpass', get_string('databasepass', 'install'), '', ''));
$settings->add(new admin_setting_configtext('logstore_database/dbname', get_string('databasename', 'install'), '', ''));
$settings->add(new admin_setting_configtext('logstore_database/dbtable', get_string('databasetable', 'logstore_database'),
get_string('databasetable_help', 'logstore_database'), ''));
$settings->add(new admin_setting_configcheckbox('logstore_database/dbpersist', get_string('databasepersist',
'logstore_database'), '', '0'));
$settings->add(new admin_setting_configtext('logstore_database/dbsocket', get_string('databasesocket', 'install'), '',
''));
$settings->add(new admin_setting_configtext('logstore_database/dbport', get_string('databaseport', 'install'), '', ''));
$settings->add(new admin_setting_configtext('logstore_database/dbschema', get_string('databaseschema',
'logstore_database'), '', ''));
$settings->add(new admin_setting_configtext('logstore_database/dbcollation', get_string('databasecollation',
'logstore_database'), '', ''));
$settings->add(new admin_setting_configcheckbox('logstore_database/dbhandlesoptions', get_string('databasehandlesoptions',
'logstore_database'), get_string('databasehandlesoptions_help', 'logstore_database'), '0'));
$settings->add(new admin_setting_configtext('logstore_database/buffersize', get_string('buffersize',
'logstore_database'), get_string('buffersize_help', 'logstore_database'), 50));
$settings->add(new admin_setting_configcheckbox('logstore_database/jsonformat',
new lang_string('jsonformat', 'logstore_database'),
new lang_string('jsonformat_desc', 'logstore_database'), 1));
// Filters.
$settings->add(new admin_setting_heading('filters', get_string('filters', 'logstore_database'), get_string('filters_help',
'logstore_database')));
$settings->add(new admin_setting_configcheckbox('logstore_database/logguests', get_string('logguests',
'logstore_database'), '', '0'));
$levels = \logstore_database\helper::get_level_options();
$settings->add(new admin_setting_configmulticheckbox('logstore_database/includelevels', get_string('includelevels',
'logstore_database'), '', $levels, $levels));
$actions = \logstore_database\helper::get_action_options();
$settings->add(new admin_setting_configmulticheckbox('logstore_database/includeactions', get_string('includeactions',
'logstore_database'), '', $actions, $actions));
}
@@ -0,0 +1,103 @@
<?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/>.
/**
* Filter form.
*
* @package logstore_database
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../../../../../config.php');
require_once($CFG->dirroot . '/lib/adminlib.php');
navigation_node::override_active_url(new moodle_url('/admin/settings.php', array('section' => 'logsettingdatabase')));
admin_externalpage_setup('logstoredbtestsettings');
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('testingsettings', 'logstore_database'));
// NOTE: this is not localised intentionally, admins are supposed to understand English at least a bit...
raise_memory_limit(MEMORY_HUGE);
$dbtable = get_config('logstore_database', 'dbtable');
if (empty($dbtable)) {
echo $OUTPUT->notification('External table not specified.', 'notifyproblem');
die();
}
$dbdriver = get_config('logstore_database', 'dbdriver');
list($dblibrary, $dbtype) = explode('/', $dbdriver);
if (!$db = \moodle_database::get_driver_instance($dbtype, $dblibrary, true)) {
echo $OUTPUT->notification("Unknown driver $dblibrary/$dbtype", "notifyproblem");
die();
}
$olddebug = $CFG->debug;
$olddisplay = ini_get('display_errors');
ini_set('display_errors', '1');
$CFG->debug = DEBUG_DEVELOPER;
error_reporting($CFG->debug);
$dboptions = array();
$dboptions['dbpersist'] = get_config('logstore_database', 'dbpersist');
$dboptions['dbsocket'] = get_config('logstore_database', 'dbsocket');
$dboptions['dbport'] = get_config('logstore_database', 'dbport');
$dboptions['dbschema'] = get_config('logstore_database', 'dbschema');
$dboptions['dbcollation'] = get_config('logstore_database', 'dbcollation');
$dboptions['dbhandlesoptions'] = get_config('logstore_database', 'dbhandlesoptions');
try {
$db->connect(get_config('logstore_database', 'dbhost'), get_config('logstore_database', 'dbuser'),
get_config('logstore_database', 'dbpass'), get_config('logstore_database', 'dbname'), false, $dboptions);
} catch (\moodle_exception $e) {
echo $OUTPUT->notification('Cannot connect to the database.', 'notifyproblem');
$CFG->debug = $olddebug;
ini_set('display_errors', $olddisplay);
error_reporting($CFG->debug);
ob_end_flush();
echo $OUTPUT->footer();
die();
}
echo $OUTPUT->notification('Connection made.', 'notifysuccess');
$tables = $db->get_tables();
if (!in_array($dbtable, $tables)) {
echo $OUTPUT->notification('Cannot find the specified table ' . $dbtable, 'notifyproblem');
$CFG->debug = $olddebug;
ini_set('display_errors', $olddisplay);
error_reporting($CFG->debug);
ob_end_flush();
echo $OUTPUT->footer();
die();
}
echo $OUTPUT->notification('Table ' . $dbtable . ' found.', 'notifysuccess');
$cols = $db->get_columns($dbtable);
if (empty($cols)) {
echo $OUTPUT->notification('Can not read external table.', 'notifyproblem');
} else {
$columns = array_keys((array)$cols);
echo $OUTPUT->notification('External table contains following columns:<br />' . implode(', ', $columns), 'notifysuccess');
}
$db->dispose();
$CFG->debug = $olddebug;
ini_set('display_errors', $olddisplay);
error_reporting($CFG->debug);
ob_end_flush();
echo $OUTPUT->footer();
+47
View File
@@ -0,0 +1,47 @@
<?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/>.
/**
* Fixtures for database log storage testing.
*
* @package logstore_database
* @copyright 2014 Petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_database\event;
defined('MOODLE_INTERNAL') || die();
class unittest_executed extends \core\event\base {
public static function get_name() {
return 'xxx';
}
public function get_description() {
return 'yyy';
}
protected function init() {
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
public function get_url() {
return new \moodle_url('/somepath/somefile.php', array('id' => $this->data['other']['sample']));
}
}
+40
View File
@@ -0,0 +1,40 @@
<?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/>.
/**
* Fixtures for database log storage testing.
*
* @package logstore_database
* @copyright 2014 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_database\test;
defined('MOODLE_INTERNAL') || die();
class store extends \logstore_database\log\store {
/**
* Public wrapper for testing.
*
* @param \core\event\base $event
*
* @return bool
*/
public function is_event_ignored(\core\event\base $event) {
return parent::is_event_ignored($event);
}
}
@@ -0,0 +1,511 @@
<?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/>.
/**
* Data provider tests.
*
* @package logstore_database
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_database\privacy;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use logstore_database\privacy\provider;
require_once(__DIR__ . '/../fixtures/event.php');
/**
* Data provider testcase class.
*
* This testcase is almost identical to the logstore_standard testcase, aside from the
* initialisation of the relevant logstore obviously.
*
* @package logstore_database
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
public function setUp(): void {
global $CFG;
$this->resetAfterTest();
$this->preventResetByRollback(); // Logging waits till the transaction gets committed.
// Fake the settings, we will abuse the standard plugin table here...
set_config('dbdriver', $CFG->dblibrary . '/' . $CFG->dbtype, 'logstore_database');
set_config('dbhost', $CFG->dbhost, 'logstore_database');
set_config('dbuser', $CFG->dbuser, 'logstore_database');
set_config('dbpass', $CFG->dbpass, 'logstore_database');
set_config('dbname', $CFG->dbname, 'logstore_database');
set_config('dbtable', $CFG->prefix . 'logstore_standard_log', 'logstore_database');
if (!empty($CFG->dboptions['dbpersist'])) {
set_config('dbpersist', 1, 'logstore_database');
} else {
set_config('dbpersist', 0, 'logstore_database');
}
if (!empty($CFG->dboptions['dbsocket'])) {
set_config('dbsocket', $CFG->dboptions['dbsocket'], 'logstore_database');
} else {
set_config('dbsocket', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbport'])) {
set_config('dbport', $CFG->dboptions['dbport'], 'logstore_database');
} else {
set_config('dbport', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbschema'])) {
set_config('dbschema', $CFG->dboptions['dbschema'], 'logstore_database');
} else {
set_config('dbschema', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbcollation'])) {
set_config('dbcollation', $CFG->dboptions['dbcollation'], 'logstore_database');
} else {
set_config('dbcollation', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbhandlesoptions'])) {
set_config('dbhandlesoptions', $CFG->dboptions['dbhandlesoptions'], 'logstore_database');
} else {
set_config('dbhandlesoptions', false, 'logstore_database');
}
}
public function test_get_contexts_for_userid(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]);
$c2 = $this->getDataGenerator()->create_course();
$cm2 = $this->getDataGenerator()->create_module('url', ['course' => $c2]);
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$cm1ctx = \context_module::instance($cm1->cmid);
$cm2ctx = \context_module::instance($cm2->cmid);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), []);
$e = \logstore_database\event\unittest_executed::create(['context' => $cm1ctx]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$cm1ctx]);
// User 2 is the related user.
$this->setUser(0);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u2), []);
$e = \logstore_database\event\unittest_executed::create(['context' => $cm2ctx, 'relateduserid' => $u2->id]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($u2), [$cm2ctx]);
// Admin user is the real user.
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), []);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), []);
$this->setAdminUser();
\core\session\manager::loginas($u3->id, $sysctx);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx]);
// By admin user masquerading u1 related to u3.
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$cm1ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx]);
$this->setAdminUser();
\core\session\manager::loginas($u1->id, \context_system::instance());
$e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u3->id]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$sysctx, $cm1ctx, $c2ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx, $c2ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx, $c2ctx]);
}
/**
* Check that user IDs are returned for a given context.
*/
public function test_add_userids_for_context(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$u4 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$this->enable_logging();
$manager = get_log_manager(true);
$userlist = new \core_privacy\local\request\userlist($sysctx, 'logstore_database');
$userids = $userlist->get_userids();
$this->assertEmpty($userids);
provider::add_userids_for_context($userlist);
$userids = $userlist->get_userids();
$this->assertEmpty($userids);
// User one should be added (userid).
$this->setUser($u1);
$e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
// User two (userids) and three (relateduserid) should be added.
$this->setUser($u2);
$e = \logstore_database\event\unittest_executed::create(['context' => $sysctx, 'relateduserid' => $u3->id]);
$e->trigger();
// The admin user should be added (realuserid).
$this->setAdminUser();
\core\session\manager::loginas($u2->id, \context_system::instance());
$e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
// Set off an event in a different context. User 4 should not be returned below.
$this->setUser($u4);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
provider::add_userids_for_context($userlist);
$userids = $userlist->get_userids();
$this->assertCount(4, $userids);
$expectedresult = [$admin->id, $u1->id, $u2->id, $u3->id];
$this->assertEmpty(array_diff($expectedresult, $userids));
}
public function test_delete_data_for_user(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// User 2 is the author.
$this->setUser($u2);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// Confirm data present.
$this->assertTrue($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id]));
$this->assertEquals(3, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
// Delete all the things!
provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_database', [$c1ctx->id]));
$this->assertFalse($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
}
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// User 2 is the author.
$this->setUser($u2);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// Confirm data present.
$this->assertTrue($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id]));
$this->assertEquals(3, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
// Delete all the things!
provider::delete_data_for_all_users_in_context($c1ctx);
$this->assertFalse($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
}
/**
* Check that data is removed for the listed users in a given context.
*/
public function test_delete_data_for_userlist(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$u4 = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($course->id);
$this->enable_logging();
$manager = get_log_manager(true);
$this->setUser($u1);
$e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
$this->setUser($u2);
$e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
$this->setUser($u3);
$e = \logstore_database\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
$this->setUser($u4);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
// Check that four records were created.
$this->assertEquals(4, $DB->count_records('logstore_standard_log'));
$userlist = new \core_privacy\local\request\approved_userlist($sysctx, 'logstore_database', [$u1->id, $u3->id]);
provider::delete_data_for_userlist($userlist);
// We should have a record for u2 and u4.
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
$records = $DB->get_records('logstore_standard_log', ['contextid' => $sysctx->id]);
$this->assertCount(1, $records);
$currentrecord = array_shift($records);
$this->assertEquals($u2->id, $currentrecord->userid);
}
public function test_export_data_for_user(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$u4 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]);
$c2 = $this->getDataGenerator()->create_course();
$cm2 = $this->getDataGenerator()->create_module('url', ['course' => $c2]);
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$cm1ctx = \context_module::instance($cm1->cmid);
$cm2ctx = \context_module::instance($cm2->cmid);
$path = [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_database')];
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx, 'other' => ['i' => 0]]);
$e->trigger();
// User 2 is related.
$this->setUser(0);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx, 'relateduserid' => $u2->id,
'other' => ['i' => 1]]);
$e->trigger();
// Admin user masquerades u3, which is related to u4.
$this->setAdminUser();
\core\session\manager::loginas($u3->id, $sysctx);
$e = \logstore_database\event\unittest_executed::create(['context' => $c1ctx, 'relateduserid' => $u4->id,
'other' => ['i' => 2]]);
$e->trigger();
// Confirm data present for u1.
provider::export_user_data(new approved_contextlist($u1, 'logstore_database', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
$this->assertSame(0, $data->logs[0]['other']['i']);
// Confirm data present for u2.
writer::reset();
provider::export_user_data(new approved_contextlist($u2, 'logstore_database', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(false), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
$this->assertSame(1, $data->logs[0]['other']['i']);
// Confirm data present for u3.
writer::reset();
provider::export_user_data(new approved_contextlist($u3, 'logstore_database', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(false), $data->logs[0]['related_user_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertEquals(transform::yesno(false), $data->logs[0]['masquerading_user_was_you']);
$this->assertSame(2, $data->logs[0]['other']['i']);
// Confirm data present for u4.
writer::reset();
provider::export_user_data(new approved_contextlist($u4, 'logstore_database', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(false), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertEquals(transform::yesno(false), $data->logs[0]['masquerading_user_was_you']);
$this->assertSame(2, $data->logs[0]['other']['i']);
// Add anonymous events.
$this->setUser($u1);
$e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u2->id,
'anonymous' => true]);
$e->trigger();
$this->setAdminUser();
\core\session\manager::loginas($u3->id, $sysctx);
$e = \logstore_database\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u4->id,
'anonymous' => true]);
$e->trigger();
// Confirm data present for u1.
provider::export_user_data(new approved_contextlist($u1, 'logstore_database', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
// Confirm data present for u2.
writer::reset();
provider::export_user_data(new approved_contextlist($u2, 'logstore_database', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertArrayNotHasKey('author_of_the_action_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('authorid', $data->logs[0]);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
// Confirm data present for u3.
writer::reset();
provider::export_user_data(new approved_contextlist($u3, 'logstore_database', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertArrayNotHasKey('masquerading_user_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('masqueradinguserid', $data->logs[0]);
// Confirm data present for u4.
writer::reset();
provider::export_user_data(new approved_contextlist($u4, 'logstore_database', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertArrayNotHasKey('author_of_the_action_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('authorid', $data->logs[0]);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertArrayNotHasKey('masquerading_user_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('masqueradinguserid', $data->logs[0]);
}
/**
* Assert the content of a context list.
*
* @param contextlist $contextlist The collection.
* @param array $expected List of expected contexts or IDs.
* @return void
*/
protected function assert_contextlist_equals($contextlist, array $expected) {
$expectedids = array_map(function($context) {
if (is_object($context)) {
return $context->id;
}
return $context;
}, $expected);
$contextids = array_map('intval', $contextlist->get_contextids());
sort($contextids);
sort($expectedids);
$this->assertEquals($expectedids, $contextids);
}
/**
* Enable logging.
*
* @return void
*/
protected function enable_logging() {
set_config('enabled_stores', 'logstore_database', 'tool_log');
set_config('buffersize', 0, 'logstore_database');
set_config('logguests', 1, 'logstore_database');
get_log_manager(true);
}
/**
* Get the contextlist for a user.
*
* @param object $user The user.
* @return contextlist
*/
protected function get_contextlist_for_user($user) {
$contextlist = new contextlist();
provider::add_contexts_for_userid($contextlist, $user->id);
return $contextlist;
}
}
@@ -0,0 +1,325 @@
<?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/>.
namespace logstore_database;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/event.php');
require_once(__DIR__ . '/fixtures/store.php');
/**
* External database log store tests.
*
* @package logstore_database
* @copyright 2014 Petr Skoda {@link http://skodak.org/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class store_test extends \advanced_testcase {
/**
* Tests log writing.
*
* @param bool $jsonformat True to test with JSON format
* @dataProvider log_writing_provider
*/
public function test_log_writing(bool $jsonformat): void {
global $DB, $CFG;
$this->resetAfterTest();
$this->preventResetByRollback(); // Logging waits till the transaction gets committed.
// Apply JSON format system setting.
set_config('jsonformat', $jsonformat ? 1 : 0, 'logstore_database');
$dbman = $DB->get_manager();
$this->assertTrue($dbman->table_exists('logstore_standard_log'));
$DB->delete_records('logstore_standard_log');
$this->setAdminUser();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$course1 = $this->getDataGenerator()->create_course();
$module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1));
$course2 = $this->getDataGenerator()->create_course();
$module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2));
// Test all plugins are disabled by this command.
set_config('enabled_stores', '', 'tool_log');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
$this->assertCount(0, $stores);
// Fake the settings, we will abuse the standard plugin table here...
set_config('dbdriver', $CFG->dblibrary . '/' . $CFG->dbtype, 'logstore_database');
set_config('dbhost', $CFG->dbhost, 'logstore_database');
set_config('dbuser', $CFG->dbuser, 'logstore_database');
set_config('dbpass', $CFG->dbpass, 'logstore_database');
set_config('dbname', $CFG->dbname, 'logstore_database');
set_config('dbtable', $CFG->prefix . 'logstore_standard_log', 'logstore_database');
if (!empty($CFG->dboptions['dbpersist'])) {
set_config('dbpersist', 1, 'logstore_database');
} else {
set_config('dbpersist', 0, 'logstore_database');
}
if (!empty($CFG->dboptions['dbsocket'])) {
set_config('dbsocket', $CFG->dboptions['dbsocket'], 'logstore_database');
} else {
set_config('dbsocket', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbport'])) {
set_config('dbport', $CFG->dboptions['dbport'], 'logstore_database');
} else {
set_config('dbport', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbschema'])) {
set_config('dbschema', $CFG->dboptions['dbschema'], 'logstore_database');
} else {
set_config('dbschema', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbcollation'])) {
set_config('dbcollation', $CFG->dboptions['dbcollation'], 'logstore_database');
} else {
set_config('dbcollation', '', 'logstore_database');
}
if (!empty($CFG->dboptions['dbhandlesoptions'])) {
set_config('dbhandlesoptions', $CFG->dboptions['dbhandlesoptions'], 'logstore_database');
} else {
set_config('dbhandlesoptions', false, 'logstore_database');
}
// Enable logging plugin.
set_config('enabled_stores', 'logstore_database', 'tool_log');
set_config('buffersize', 0, 'logstore_database');
set_config('logguests', 1, 'logstore_database');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
$this->assertCount(1, $stores);
$this->assertEquals(array('logstore_database'), array_keys($stores));
$store = $stores['logstore_database'];
$this->assertInstanceOf('logstore_database\log\store', $store);
$this->assertInstanceOf('tool_log\log\writer', $store);
$this->assertTrue($store->is_logging());
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
$this->assertCount(0, $logs);
$this->setCurrentTimeStart();
$this->setUser(0);
$event1 = \logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)));
$event1->trigger();
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
$this->assertCount(1, $logs);
$log1 = reset($logs);
unset($log1->id);
if ($jsonformat) {
$log1->other = json_decode($log1->other, true);
} else {
$log1->other = unserialize($log1->other);
}
$log1 = (array)$log1;
$data = $event1->get_data();
$data['origin'] = 'cli';
$data['ip'] = null;
$data['realuserid'] = null;
$this->assertEquals($data, $log1);
$this->setAdminUser();
\core\session\manager::loginas($user1->id, \context_system::instance());
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
$event2 = \logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module2->cmid), 'other' => array('sample' => 6, 'xx' => 9)));
$event2->trigger();
\core\session\manager::init_empty_session();
$this->assertFalse(\core\session\manager::is_loggedinas());
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
$this->assertCount(3, $logs);
array_shift($logs);
$log2 = array_shift($logs);
$this->assertSame('\core\event\user_loggedinas', $log2->eventname);
$log3 = array_shift($logs);
unset($log3->id);
if ($jsonformat) {
$log3->other = json_decode($log3->other, true);
} else {
$log3->other = unserialize($log3->other);
}
$log3 = (array)$log3;
$data = $event2->get_data();
$data['origin'] = 'cli';
$data['ip'] = null;
$data['realuserid'] = 2;
$this->assertEquals($data, $log3);
// Test reading.
$this->assertSame(3, $store->get_events_select_count('', array()));
$events = $store->get_events_select('', array(), 'timecreated ASC', 0, 0); // Is actually sorted by "timecreated ASC, id ASC".
$this->assertCount(3, $events);
$resev1 = array_shift($events);
array_shift($events);
$resev2 = array_shift($events);
$this->assertEquals($event1->get_data(), $resev1->get_data());
$this->assertEquals($event2->get_data(), $resev2->get_data());
// Test buffering.
set_config('buffersize', 3, 'logstore_database');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
/** @var \logstore_database\log\store $store */
$store = $stores['logstore_database'];
$DB->delete_records('logstore_standard_log');
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
$store->flush();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(5, $DB->count_records('logstore_standard_log'));
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(5, $DB->count_records('logstore_standard_log'));
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(5, $DB->count_records('logstore_standard_log'));
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(8, $DB->count_records('logstore_standard_log'));
// Test guest logging setting.
set_config('logguests', 0, 'logstore_database');
set_config('buffersize', 0, 'logstore_database');
get_log_manager(true);
$DB->delete_records('logstore_standard_log');
get_log_manager(true);
$this->setUser(null);
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
$this->setGuestUser();
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
$this->setUser($user1);
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(1, $DB->count_records('logstore_standard_log'));
$this->setUser($user2);
\logstore_database\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* Returns different JSON format settings so the test can be run with JSON format either on or
* off.
*
* @return bool[] Array of true/false
*/
public static function log_writing_provider(): array {
return [
[false],
[true]
];
}
/**
* Test method is_event_ignored.
*/
public function test_is_event_ignored(): void {
$this->resetAfterTest();
// Test guest filtering.
set_config('logguests', 0, 'logstore_database');
$this->setGuestUser();
$event = \logstore_database\event\unittest_executed::create(
array('context' => \context_system::instance(), 'other' => array('sample' => 5, 'xx' => 10)));
$logmanager = get_log_manager();
$store = new \logstore_database\test\store($logmanager);
$this->assertTrue($store->is_event_ignored($event));
set_config('logguests', 1, 'logstore_database');
$store = new \logstore_database\test\store($logmanager); // Reload.
$this->assertFalse($store->is_event_ignored($event));
// Test action/level filtering.
set_config('includelevels', '', 'logstore_database');
set_config('includeactions', '', 'logstore_database');
$store = new \logstore_database\test\store($logmanager); // Reload.
$this->assertTrue($store->is_event_ignored($event));
set_config('includelevels', '0,1', 'logstore_database');
$store = new \logstore_database\test\store($logmanager); // Reload.
$this->assertTrue($store->is_event_ignored($event));
set_config('includelevels', '0,1,2', 'logstore_database');
$store = new \logstore_database\test\store($logmanager); // Reload.
$this->assertFalse($store->is_event_ignored($event));
set_config('includelevels', '', 'logstore_database');
set_config('includeactions', 'c,r,d', 'logstore_database');
$store = new \logstore_database\test\store($logmanager); // Reload.
$this->assertTrue($store->is_event_ignored($event));
set_config('includeactions', 'c,r,u,d', 'logstore_database');
$store = new \logstore_database\test\store($logmanager); // Reload.
$this->assertFalse($store->is_event_ignored($event));
}
/**
* Test logmanager::get_supported_reports returns all reports that require this store.
*/
public function test_get_supported_reports(): void {
$logmanager = get_log_manager();
$allreports = \core_component::get_plugin_list('report');
// Make sure all supported reports are installed.
$expectedreports = array_intersect_key([
'log' => 'report_log',
'loglive' => 'report_loglive',
], $allreports);
$supportedreports = $logmanager->get_supported_reports('logstore_database');
foreach ($expectedreports as $expectedreport) {
$this->assertArrayHasKey($expectedreport, $supportedreports);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
This files describes API changes in the logstore_database code.
=== 3.4 ===
* PostgreSQL connections now use advanced options to reduce connection overhead. These options are not compatible
with some connection poolers. The dbhandlesoptions parameter has been added to allow the database to configure the
required defaults. The parameters that are required in the database are;
ALTER DATABASE moodle SET client_encoding = UTF8;
ALTER DATABASE moodle SET standard_conforming_strings = on;
ALTER DATABASE moodle SET search_path = 'moodle,public'; -- Optional, if you wish to use a custom schema.
You can set these options against the database or the moodle user who connects.
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* External database log store.
*
* @package logstore_database
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'logstore_database'; // Full name of the plugin (used for diagnostics).
@@ -0,0 +1,55 @@
<?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/>.
/**
* Backup implementation for the (tool_log) logstore_standard subplugin.
*
* @package logstore_standard
* @category backup
* @copyright 2015 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
class backup_logstore_standard_subplugin extends backup_tool_log_logstore_subplugin {
/**
* Returns the subplugin structure to attach to the 'logstore' XML element.
*
* @return backup_subplugin_element the subplugin structure to be attached.
*/
protected function define_logstore_subplugin_structure() {
$subplugin = $this->get_subplugin_element();
$subpluginwrapper = new backup_nested_element($this->get_recommended_name());
// Create the custom (base64 encoded, xml safe) 'other' final element.
$otherelement = new base64_encode_final_element('other');
$subpluginlog = new backup_nested_element('logstore_standard_log', array('id'), array(
'eventname', 'component', 'action', 'target', 'objecttable',
'objectid', 'crud', 'edulevel', 'contextid', 'userid', 'relateduserid',
'anonymous', $otherelement, 'timecreated', 'ip', 'realuserid'));
$subplugin->add_child($subpluginwrapper);
$subpluginwrapper->add_child($subpluginlog);
$subpluginlog->set_source_table('logstore_standard_log', array('contextid' => backup::VAR_CONTEXTID));
return $subplugin;
}
}
@@ -0,0 +1,69 @@
<?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/>.
/**
* Restore implementation for the (tool_log) logstore_standard subplugin.
*
* @package logstore_standard
* @category backup
* @copyright 2015 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
class restore_logstore_standard_subplugin extends restore_tool_log_logstore_subplugin {
/**
* Returns the subplugin structure to attach to the 'logstore' XML element.
*
* @return restore_path_element[] array of elements to be processed on restore.
*/
protected function define_logstore_subplugin_structure() {
// If the logstore is not enabled we don't add structures for it.
$enabledlogstores = explode(',', get_config('tool_log', 'enabled_stores'));
if (!in_array('logstore_standard', $enabledlogstores)) {
return array(); // The logstore is not enabled, nothing to restore.
}
$paths = array();
$elename = $this->get_namefor('log');
$elepath = $this->get_pathfor('/logstore_standard_log');
$paths[] = new restore_path_element($elename, $elepath);
return $paths;
}
/**
* Process logstore_standard_log entries.
*
* This method proceeds to read, complete, remap and, finally,
* discard or save every log entry.
*
* @param array() $data log entry.
*/
public function process_logstore_standard_log($data) {
global $DB;
$data = $this->process_log($data, get_config('logstore_standard', 'jsonformat'));
if ($data) {
$DB->insert_record('logstore_standard_log', $data);
}
}
}
@@ -0,0 +1,182 @@
<?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/>.
/**
* Standard log reader/writer.
*
* @package logstore_standard
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_standard\log;
defined('MOODLE_INTERNAL') || die();
class store implements \tool_log\log\writer, \core\log\sql_internal_table_reader {
use \tool_log\helper\store,
\tool_log\helper\buffered_writer,
\tool_log\helper\reader;
/** @var string $logguests true if logging guest access */
protected $logguests;
public function __construct(\tool_log\log\manager $manager) {
$this->helper_setup($manager);
// Log everything before setting is saved for the first time.
$this->logguests = $this->get_config('logguests', 1);
// JSON writing defaults to false (table format compatibility with older versions).
// Note: This variable is defined in the buffered_writer trait.
$this->jsonformat = (bool)$this->get_config('jsonformat', false);
}
/**
* Should the event be ignored (== not logged)?
* @param \core\event\base $event
* @return bool
*/
protected function is_event_ignored(\core\event\base $event) {
if ((!CLI_SCRIPT or PHPUNIT_TEST) and !$this->logguests) {
// Always log inside CLI scripts because we do not login there.
if (!isloggedin() or isguestuser()) {
return true;
}
}
return false;
}
/**
* Finally store the events into the database.
*
* @param array $evententries raw event data
*/
protected function insert_event_entries($evententries) {
global $DB;
$DB->insert_records('logstore_standard_log', $evententries);
}
public function get_events_select($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
global $DB;
$sort = self::tweak_sort_by_id($sort);
$events = array();
$records = $DB->get_recordset_select('logstore_standard_log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
foreach ($records as $data) {
if ($event = $this->get_log_event($data)) {
$events[$data->id] = $event;
}
}
$records->close();
return $events;
}
/**
* Fetch records using given criteria returning a Traversable object.
*
* Note that the traversable object contains a moodle_recordset, so
* remember that is important that you call close() once you finish
* using it.
*
* @param string $selectwhere
* @param array $params
* @param string $sort
* @param int $limitfrom
* @param int $limitnum
* @return \core\dml\recordset_walk|\core\event\base[]
*/
public function get_events_select_iterator($selectwhere, array $params, $sort, $limitfrom, $limitnum) {
global $DB;
$sort = self::tweak_sort_by_id($sort);
$recordset = $DB->get_recordset_select('logstore_standard_log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum);
return new \core\dml\recordset_walk($recordset, array($this, 'get_log_event'));
}
/**
* Returns an event from the log data.
*
* @param stdClass $data Log data
* @return \core\event\base
*/
public function get_log_event($data) {
$extra = array('origin' => $data->origin, 'ip' => $data->ip, 'realuserid' => $data->realuserid);
$data = (array)$data;
$id = $data['id'];
$data['other'] = self::decode_other($data['other']);
if ($data['other'] === false) {
$data['other'] = array();
}
unset($data['origin']);
unset($data['ip']);
unset($data['realuserid']);
unset($data['id']);
if (!$event = \core\event\base::restore($data, $extra)) {
return null;
}
return $event;
}
/**
* Get number of events present for the given select clause.
*
* @param string $selectwhere select conditions.
* @param array $params params.
*
* @return int Number of events available for the given conditions
*/
public function get_events_select_count($selectwhere, array $params) {
global $DB;
return $DB->count_records_select('logstore_standard_log', $selectwhere, $params);
}
/**
* Get whether events are present for the given select clause.
*
* @param string $selectwhere select conditions.
* @param array $params params.
*
* @return bool Whether events available for the given conditions
*/
public function get_events_select_exists(string $selectwhere, array $params): bool {
global $DB;
return $DB->record_exists_select('logstore_standard_log', $selectwhere, $params);
}
public function get_internal_log_table_name() {
return 'logstore_standard_log';
}
/**
* Are the new events appearing in the reader?
*
* @return bool true means new log events are being added, false means no new data will be added
*/
public function is_logging() {
// Only enabled stpres are queried,
// this means we can return true here unless store has some extra switch.
return true;
}
}
@@ -0,0 +1,124 @@
<?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/>.
/**
* Data provider.
*
* @package logstore_standard
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_standard\privacy;
defined('MOODLE_INTERNAL') || die();
use context;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\contextlist;
/**
* Data provider class.
*
* @package logstore_standard
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\tool_log\local\privacy\logstore_provider,
\tool_log\local\privacy\logstore_userlist_provider {
use \tool_log\local\privacy\moodle_database_export_and_delete;
/**
* Returns metadata.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('logstore_standard_log', [
'eventname' => 'privacy:metadata:log:eventname',
'userid' => 'privacy:metadata:log:userid',
'relateduserid' => 'privacy:metadata:log:relateduserid',
'anonymous' => 'privacy:metadata:log:anonymous',
'other' => 'privacy:metadata:log:other',
'timecreated' => 'privacy:metadata:log:timecreated',
'origin' => 'privacy:metadata:log:origin',
'ip' => 'privacy:metadata:log:ip',
'realuserid' => 'privacy:metadata:log:realuserid',
], 'privacy:metadata:log');
return $collection;
}
/**
* Add contexts that contain user information for the specified user.
*
* @param contextlist $contextlist The contextlist to add the contexts to.
* @param int $userid The user to find the contexts for.
* @return void
*/
public static function add_contexts_for_userid(contextlist $contextlist, $userid) {
$sql = "
SELECT l.contextid
FROM {logstore_standard_log} l
WHERE l.userid = :userid1
OR l.relateduserid = :userid2
OR l.realuserid = :userid3";
$contextlist->add_from_sql($sql, [
'userid1' => $userid,
'userid2' => $userid,
'userid3' => $userid,
]);
}
/**
* Add user IDs that contain user information for the specified context.
*
* @param \core_privacy\local\request\userlist $userlist The userlist to add the users to.
* @return void
*/
public static function add_userids_for_context(\core_privacy\local\request\userlist $userlist) {
$params = ['contextid' => $userlist->get_context()->id];
$sql = "SELECT userid, relateduserid, realuserid
FROM {logstore_standard_log}
WHERE contextid = :contextid";
$userlist->add_from_sql('userid', $sql, $params);
$userlist->add_from_sql('relateduserid', $sql, $params);
$userlist->add_from_sql('realuserid', $sql, $params);
}
/**
* Get the database object.
*
* @return array Containing moodle_database, string, or null values.
*/
protected static function get_database_and_table() {
global $DB;
return [$DB, 'logstore_standard_log'];
}
/**
* Get the path to export the logs to.
*
* @return array
*/
protected static function get_export_subcontext() {
return [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_standard')];
}
}
@@ -0,0 +1,71 @@
<?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/>.
/**
* Standard log reader/writer.
*
* @package logstore_standard
* @copyright 2014 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_standard\task;
defined('MOODLE_INTERNAL') || die();
class cleanup_task extends \core\task\scheduled_task {
/**
* Get a descriptive name for this task (shown to admins).
*
* @return string
*/
public function get_name() {
return get_string('taskcleanup', 'logstore_standard');
}
/**
* Do the job.
* Throw exceptions on errors (the job will be retried).
*/
public function execute() {
global $DB;
$loglifetime = (int)get_config('logstore_standard', 'loglifetime');
if (empty($loglifetime) || $loglifetime < 0) {
return;
}
$loglifetime = time() - ($loglifetime * 3600 * 24); // Value in days.
$lifetimep = array($loglifetime);
$start = time();
while ($min = $DB->get_field_select("logstore_standard_log", "MIN(timecreated)", "timecreated < ?", $lifetimep)) {
// Break this down into chunks to avoid transaction for too long and generally thrashing database.
// Experiments suggest deleting one day takes up to a few seconds; probably a reasonable chunk size usually.
// If the cleanup has just been enabled, it might take e.g a month to clean the years of logs.
$params = array(min($min + 3600 * 24, $loglifetime));
$DB->delete_records_select("logstore_standard_log", "timecreated < ?", $params);
if (time() > $start + 300) {
// Do not churn on log deletion for too long each run.
break;
}
}
mtrace(" Deleted old log records from standard store.");
}
}
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="admin/tool/log/store/standard/db" VERSION="20220616" COMMENT="XMLDB file for Moodle admin/tool/log/store/standard"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="logstore_standard_log" COMMENT="Standard log table">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="eventname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="action" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="target" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="objecttable" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="objectid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="crud" TYPE="char" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="edulevel" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="contextlevel" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="contextinstanceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="relateduserid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="anonymous" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Was this event anonymous at the time of triggering?"/>
<FIELD NAME="other" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="origin" TYPE="char" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="cli, cron, ws, etc."/>
<FIELD NAME="ip" TYPE="char" LENGTH="45" NOTNULL="false" SEQUENCE="false" COMMENT="IP address"/>
<FIELD NAME="realuserid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="real user id when logged-in-as"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="courseid" TYPE="foreign" FIELDS="courseid" REFTABLE="course" REFFIELDS="id"/>
<KEY NAME="realuserid" TYPE="foreign" FIELDS="realuserid" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="relateduserid" TYPE="foreign" FIELDS="relateduserid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="timecreated" UNIQUE="false" FIELDS="timecreated"/>
<INDEX NAME="course-time" UNIQUE="false" FIELDS="courseid, anonymous, timecreated"/>
<INDEX NAME="user-module" UNIQUE="false" FIELDS="userid, contextlevel, contextinstanceid, crud, edulevel, timecreated"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
@@ -0,0 +1,37 @@
<?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/>.
/**
* Standard log reader/writer cron task.
*
* @package logstore_standard
* @copyright 2014 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = array(
array(
'classname' => '\logstore_standard\task\cleanup_task',
'blocking' => 0,
'minute' => 'R',
'hour' => '4',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
);
@@ -0,0 +1,39 @@
<?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/>.
/**
* Standard log store upgrade.
*
* @package logstore_standard
* @copyright 2014 Petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
function xmldb_logstore_standard_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,40 @@
<?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/>.
/**
* Log store lang strings.
*
* @package logstore_standard
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['buffersize'] = 'Write buffer size';
$string['jsonformat'] = 'JSON format';
$string['jsonformat_desc'] = 'Use standard JSON format instead of PHP serialised data in the \'other\' database field.';
$string['pluginname'] = 'Standard log';
$string['pluginname_desc'] = 'A log plugin stores log entries in a Moodle database table.';
$string['privacy:metadata:log'] = 'A collection of past events';
$string['privacy:metadata:log:anonymous'] = 'Whether the event was flagged as anonymous';
$string['privacy:metadata:log:eventname'] = 'The event name';
$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
$string['privacy:metadata:log:origin'] = 'The origin of the event';
$string['privacy:metadata:log:other'] = 'Additional information about the event';
$string['privacy:metadata:log:realuserid'] = 'The ID of the real user behind the event, when masquerading a user.';
$string['privacy:metadata:log:relateduserid'] = 'The ID of a user related to this event';
$string['privacy:metadata:log:timecreated'] = 'The time when the event occurred';
$string['privacy:metadata:log:userid'] = 'The ID of the user who triggered this event';
$string['taskcleanup'] = 'Log table cleanup';
@@ -0,0 +1,57 @@
<?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/>.
/**
* Standard log store settings.
*
* @package logstore_standard
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
$settings->add(new admin_setting_configcheckbox('logstore_standard/logguests',
new lang_string('logguests', 'core_admin'),
new lang_string('logguests_help', 'core_admin'), 1));
$settings->add(new admin_setting_configcheckbox('logstore_standard/jsonformat',
new lang_string('jsonformat', 'logstore_standard'),
new lang_string('jsonformat_desc', 'logstore_standard'), 1));
$options = array(
0 => new lang_string('neverdeletelogs'),
1000 => new lang_string('numdays', '', 1000),
365 => new lang_string('numdays', '', 365),
180 => new lang_string('numdays', '', 180),
150 => new lang_string('numdays', '', 150),
120 => new lang_string('numdays', '', 120),
90 => new lang_string('numdays', '', 90),
60 => new lang_string('numdays', '', 60),
35 => new lang_string('numdays', '', 35),
10 => new lang_string('numdays', '', 10),
5 => new lang_string('numdays', '', 5),
2 => new lang_string('numdays', '', 2));
$settings->add(new admin_setting_configselect('logstore_standard/loglifetime',
new lang_string('loglifetime', 'core_admin'),
new lang_string('configloglifetime', 'core_admin'), 0, $options));
$settings->add(new admin_setting_configtext('logstore_standard/buffersize',
get_string('buffersize', 'logstore_standard'),
'', '50', PARAM_INT));
}
+57
View File
@@ -0,0 +1,57 @@
<?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/>.
/**
* Fixtures for standard log storage testing.
*
* @package logstore_standard
* @copyright 2014 Petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_standard\event;
defined('MOODLE_INTERNAL') || die();
class unittest_executed extends \core\event\base {
public static function get_name() {
return 'xxx';
}
public function get_description() {
return 'yyy';
}
protected function init() {
$this->data['crud'] = 'u';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
public function get_url() {
return new \moodle_url('/somepath/somefile.php', array('id' => $this->data['other']['sample']));
}
/**
* The 'other' fields for this event do not need to mapped during backup and restore as they
* only contain test values, not IDs for anything on the course.
*
* @return array Empty array
*/
public static function get_other_mapping(): array {
return [];
}
}
@@ -0,0 +1,34 @@
<?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/>.
/**
* Restore controller hackery.
*
* @package tool_log
* @copyright 2014 Petr Skoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
class logstore_standard_restore extends restore_controller {
public static function hack_executing($state) {
self::$executing = $state;
}
}
@@ -0,0 +1,462 @@
<?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/>.
/**
* Data provider tests.
*
* @package logstore_standard
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace logstore_standard\privacy;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use logstore_standard\privacy\provider;
require_once(__DIR__ . '/../fixtures/event.php');
/**
* Data provider testcase class.
*
* @package logstore_standard
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
public function setUp(): void {
$this->resetAfterTest();
$this->preventResetByRollback(); // Logging waits till the transaction gets committed.
}
public function test_get_contexts_for_userid(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]);
$c2 = $this->getDataGenerator()->create_course();
$cm2 = $this->getDataGenerator()->create_module('url', ['course' => $c2]);
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$cm1ctx = \context_module::instance($cm1->cmid);
$cm2ctx = \context_module::instance($cm2->cmid);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), []);
$e = \logstore_standard\event\unittest_executed::create(['context' => $cm1ctx]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$cm1ctx]);
// User 2 is the related user.
$this->setUser(0);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u2), []);
$e = \logstore_standard\event\unittest_executed::create(['context' => $cm2ctx, 'relateduserid' => $u2->id]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($u2), [$cm2ctx]);
// Admin user is the real user.
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), []);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), []);
$this->setAdminUser();
\core\session\manager::loginas($u3->id, $sysctx);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx]);
// By admin user masquerading u1 related to u3.
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$cm1ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx]);
$this->setAdminUser();
\core\session\manager::loginas($u1->id, \context_system::instance());
$e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u3->id]);
$e->trigger();
$this->assert_contextlist_equals($this->get_contextlist_for_user($u1), [$sysctx, $cm1ctx, $c2ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($u3), [$sysctx, $c1ctx, $c2ctx]);
$this->assert_contextlist_equals($this->get_contextlist_for_user($admin), [$sysctx, $c1ctx, $c2ctx]);
}
public function test_add_userids_for_context(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$u4 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$this->enable_logging();
$manager = get_log_manager(true);
$userlist = new \core_privacy\local\request\userlist($sysctx, 'logstore_standard_log');
$userids = $userlist->get_userids();
$this->assertEmpty($userids);
provider::add_userids_for_context($userlist);
$userids = $userlist->get_userids();
$this->assertEmpty($userids);
// User one should be added (userid).
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
// User two (userids) and three (relateduserid) should be added.
$this->setUser($u2);
$e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx, 'relateduserid' => $u3->id]);
$e->trigger();
// The admin user should be added (realuserid).
$this->setAdminUser();
\core\session\manager::loginas($u2->id, \context_system::instance());
$e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
// Set off an event in a different context. User 4 should not be returned below.
$this->setUser($u4);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
provider::add_userids_for_context($userlist);
$userids = $userlist->get_userids();
$this->assertCount(4, $userids);
$expectedresult = [$admin->id, $u1->id, $u2->id, $u3->id];
$this->assertEmpty(array_diff($expectedresult, $userids));
}
public function test_delete_data_for_user(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// User 2 is the author.
$this->setUser($u2);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// Confirm data present.
$this->assertTrue($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id]));
$this->assertEquals(3, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
// Delete all the things!
provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_standard', [$c1ctx->id]));
$this->assertFalse($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
}
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// User 2 is the author.
$this->setUser($u2);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx]);
$e->trigger();
// Confirm data present.
$this->assertTrue($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id]));
$this->assertEquals(3, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
// Delete all the things!
provider::delete_data_for_all_users_in_context($c1ctx);
$this->assertFalse($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
}
public function test_delete_data_for_userlist(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$u4 = $this->getDataGenerator()->create_user();
$course = $this->getDataGenerator()->create_course();
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($course->id);
$this->enable_logging();
$manager = get_log_manager(true);
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
$this->setUser($u2);
$e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
$this->setUser($u3);
$e = \logstore_standard\event\unittest_executed::create(['context' => $sysctx]);
$e->trigger();
$this->setUser($u4);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
// Check that four records were created.
$this->assertEquals(4, $DB->count_records('logstore_standard_log'));
$userlist = new \core_privacy\local\request\approved_userlist($sysctx, 'logstore_standard_log', [$u1->id, $u3->id]);
provider::delete_data_for_userlist($userlist);
// We should have a record for u2 and u4.
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
$records = $DB->get_records('logstore_standard_log', ['contextid' => $sysctx->id]);
$this->assertCount(1, $records);
$currentrecord = array_shift($records);
$this->assertEquals($u2->id, $currentrecord->userid);
}
public function test_export_data_for_user(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$u3 = $this->getDataGenerator()->create_user();
$u4 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$cm1 = $this->getDataGenerator()->create_module('url', ['course' => $c1]);
$c2 = $this->getDataGenerator()->create_course();
$cm2 = $this->getDataGenerator()->create_module('url', ['course' => $c2]);
$sysctx = \context_system::instance();
$c1ctx = \context_course::instance($c1->id);
$c2ctx = \context_course::instance($c2->id);
$cm1ctx = \context_module::instance($cm1->cmid);
$cm2ctx = \context_module::instance($cm2->cmid);
$path = [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_standard')];
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx, 'other' => ['i' => 0]]);
$e->trigger();
// User 2 is related.
$this->setUser(0);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx, 'relateduserid' => $u2->id,
'other' => ['i' => 1]]);
$e->trigger();
// Admin user masquerades u3, which is related to u4.
$this->setAdminUser();
\core\session\manager::loginas($u3->id, $sysctx);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx, 'relateduserid' => $u4->id,
'other' => ['i' => 2]]);
$e->trigger();
// Confirm data present for u1.
provider::export_user_data(new approved_contextlist($u1, 'logstore_standard', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
$this->assertSame(0, $data->logs[0]['other']['i']);
// Confirm data present for u2.
writer::reset();
provider::export_user_data(new approved_contextlist($u2, 'logstore_standard', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(false), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
$this->assertSame(1, $data->logs[0]['other']['i']);
// Confirm data present for u3.
writer::reset();
provider::export_user_data(new approved_contextlist($u3, 'logstore_standard', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(false), $data->logs[0]['related_user_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertEquals(transform::yesno(false), $data->logs[0]['masquerading_user_was_you']);
$this->assertSame(2, $data->logs[0]['other']['i']);
// Confirm data present for u4.
writer::reset();
provider::export_user_data(new approved_contextlist($u4, 'logstore_standard', [$c2ctx->id, $c1ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertEmpty($data);
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(false), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertEquals(transform::yesno(false), $data->logs[0]['masquerading_user_was_you']);
$this->assertSame(2, $data->logs[0]['other']['i']);
// Add anonymous events.
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u2->id,
'anonymous' => true]);
$e->trigger();
$this->setAdminUser();
\core\session\manager::loginas($u3->id, $sysctx);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c2ctx, 'relateduserid' => $u4->id,
'anonymous' => true]);
$e->trigger();
// Confirm data present for u1.
provider::export_user_data(new approved_contextlist($u1, 'logstore_standard', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
// Confirm data present for u2.
writer::reset();
provider::export_user_data(new approved_contextlist($u2, 'logstore_standard', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertArrayNotHasKey('author_of_the_action_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('authorid', $data->logs[0]);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
// Confirm data present for u3.
writer::reset();
provider::export_user_data(new approved_contextlist($u3, 'logstore_standard', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertArrayNotHasKey('masquerading_user_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('masqueradinguserid', $data->logs[0]);
// Confirm data present for u4.
writer::reset();
provider::export_user_data(new approved_contextlist($u4, 'logstore_standard', [$c2ctx->id]));
$data = writer::with_context($c2ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['action_was_done_anonymously']);
$this->assertArrayNotHasKey('author_of_the_action_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('authorid', $data->logs[0]);
$this->assertEquals(transform::yesno(true), $data->logs[0]['related_user_was_you']);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_masqueraded']);
$this->assertArrayNotHasKey('masquerading_user_was_you', $data->logs[0]);
$this->assertArrayNotHasKey('masqueradinguserid', $data->logs[0]);
}
/**
* Assert the content of a context list.
*
* @param contextlist $contextlist The collection.
* @param array $expected List of expected contexts or IDs.
* @return void
*/
protected function assert_contextlist_equals($contextlist, array $expected) {
$expectedids = array_map(function($context) {
if (is_object($context)) {
return $context->id;
}
return $context;
}, $expected);
$contextids = array_map('intval', $contextlist->get_contextids());
sort($contextids);
sort($expectedids);
$this->assertEquals($expectedids, $contextids);
}
/**
* Enable logging.
*
* @return void
*/
protected function enable_logging() {
set_config('enabled_stores', 'logstore_standard', 'tool_log');
set_config('buffersize', 0, 'logstore_standard');
set_config('logguests', 1, 'logstore_standard');
}
/**
* Get the contextlist for a user.
*
* @param object $user The user.
* @return contextlist
*/
protected function get_contextlist_for_user($user) {
$contextlist = new contextlist();
provider::add_contexts_for_userid($contextlist, $user->id);
return $contextlist;
}
}
@@ -0,0 +1,551 @@
<?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/>.
namespace logstore_standard;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/event.php');
require_once(__DIR__ . '/fixtures/restore_hack.php');
/**
* Standard log store tests.
*
* @package logstore_standard
* @copyright 2014 Petr Skoda {@link http://skodak.org/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class store_test extends \advanced_testcase {
/**
* @var bool Determine if we disabled the GC, so it can be re-enabled in tearDown.
*/
private $wedisabledgc = false;
/**
* Tests log writing.
*
* @param bool $jsonformat True to test with JSON format
* @dataProvider log_writing_provider
*/
public function test_log_writing(bool $jsonformat): void {
global $DB;
$this->resetAfterTest();
$this->preventResetByRollback(); // Logging waits till the transaction gets committed.
// Apply JSON format system setting.
set_config('jsonformat', $jsonformat ? 1 : 0, 'logstore_standard');
$this->setAdminUser();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$course1 = $this->getDataGenerator()->create_course();
$module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1));
$course2 = $this->getDataGenerator()->create_course();
$module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2));
// Test all plugins are disabled by this command.
set_config('enabled_stores', '', 'tool_log');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
$this->assertCount(0, $stores);
// Enable logging plugin.
set_config('enabled_stores', 'logstore_standard', 'tool_log');
set_config('buffersize', 0, 'logstore_standard');
set_config('logguests', 1, 'logstore_standard');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
$this->assertCount(1, $stores);
$this->assertEquals(array('logstore_standard'), array_keys($stores));
/** @var \logstore_standard\log\store $store */
$store = $stores['logstore_standard'];
$this->assertInstanceOf('logstore_standard\log\store', $store);
$this->assertInstanceOf('tool_log\log\writer', $store);
$this->assertTrue($store->is_logging());
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
$this->assertCount(0, $logs);
$exists = $store->get_events_select_exists('', array(), 'timecreated ASC', 0, 0);
$this->assertFalse($exists);
$this->setCurrentTimeStart();
$this->setUser(0);
$event1 = \logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)));
$event1->trigger();
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
$this->assertCount(1, $logs);
$log1 = reset($logs);
unset($log1->id);
if ($jsonformat) {
$log1->other = json_decode($log1->other, true);
} else {
$log1->other = unserialize($log1->other);
}
$log1 = (array)$log1;
$data = $event1->get_data();
$data['origin'] = 'cli';
$data['ip'] = null;
$data['realuserid'] = null;
$this->assertEquals($data, $log1);
$this->setAdminUser();
\core\session\manager::loginas($user1->id, \context_system::instance());
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
\logstore_standard_restore::hack_executing(1);
$event2 = \logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module2->cmid), 'other' => array('sample' => 6, 'xx' => 9)));
$event2->trigger();
\logstore_standard_restore::hack_executing(0);
\core\session\manager::init_empty_session();
$this->assertFalse(\core\session\manager::is_loggedinas());
$logs = $DB->get_records('logstore_standard_log', array(), 'id ASC');
$this->assertCount(3, $logs);
array_shift($logs);
$log2 = array_shift($logs);
$this->assertSame('\core\event\user_loggedinas', $log2->eventname);
$this->assertSame('cli', $log2->origin);
$log3 = array_shift($logs);
unset($log3->id);
if ($jsonformat) {
$log3->other = json_decode($log3->other, true);
} else {
$log3->other = unserialize($log3->other);
}
$log3 = (array)$log3;
$data = $event2->get_data();
$data['origin'] = 'restore';
$data['ip'] = null;
$data['realuserid'] = 2;
$this->assertEquals($data, $log3);
// Test table exists.
$tablename = $store->get_internal_log_table_name();
$this->assertTrue($DB->get_manager()->table_exists($tablename));
// Test reading.
$this->assertSame(3, $store->get_events_select_count('', array()));
$events = $store->get_events_select('', array(), 'timecreated ASC', 0, 0); // Is actually sorted by "timecreated ASC, id ASC".
$this->assertCount(3, $events);
$exists = $store->get_events_select_exists('', array(), 'timecreated ASC', 0, 0);
$this->assertTrue($exists);
$resev1 = array_shift($events);
array_shift($events);
$resev2 = array_shift($events);
$this->assertEquals($event1->get_data(), $resev1->get_data());
$this->assertEquals($event2->get_data(), $resev2->get_data());
// Test buffering.
set_config('buffersize', 3, 'logstore_standard');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
/** @var \logstore_standard\log\store $store */
$store = $stores['logstore_standard'];
$DB->delete_records('logstore_standard_log');
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
$store->flush();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(5, $DB->count_records('logstore_standard_log'));
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(5, $DB->count_records('logstore_standard_log'));
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(5, $DB->count_records('logstore_standard_log'));
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(8, $DB->count_records('logstore_standard_log'));
// Test guest logging setting.
set_config('logguests', 0, 'logstore_standard');
set_config('buffersize', 0, 'logstore_standard');
get_log_manager(true);
$DB->delete_records('logstore_standard_log');
get_log_manager(true);
$this->setUser(null);
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
$this->setGuestUser();
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(0, $DB->count_records('logstore_standard_log'));
$this->setUser($user1);
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(1, $DB->count_records('logstore_standard_log'));
$this->setUser($user2);
\logstore_standard\event\unittest_executed::create(
array('context' => \context_module::instance($module1->cmid), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
$this->assertEquals(2, $DB->count_records('logstore_standard_log'));
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* Returns different JSON format settings so the test can be run with JSON format either on or
* off.
*
* @return bool[] Array of true/false
*/
public static function log_writing_provider(): array {
return [
[false],
[true]
];
}
/**
* Test logmanager::get_supported_reports returns all reports that require this store.
*/
public function test_get_supported_reports(): void {
$logmanager = get_log_manager();
$allreports = \core_component::get_plugin_list('report');
// Make sure all supported reports are installed.
$expectedreports = array_intersect_key([
'log' => 'report_log',
'loglive' => 'report_loglive',
'outline' => 'report_outline',
'participation' => 'report_participation',
'stats' => 'report_stats'
], $allreports);
$supportedreports = $logmanager->get_supported_reports('logstore_standard');
foreach ($expectedreports as $expectedreport) {
$this->assertArrayHasKey($expectedreport, $supportedreports);
}
}
/**
* Verify that gc disabling works
*/
public function test_gc_enabled_as_expected(): void {
if (!gc_enabled()) {
$this->markTestSkipped('Garbage collector (gc) is globally disabled.');
}
$this->disable_gc();
$this->assertTrue($this->wedisabledgc);
$this->assertFalse(gc_enabled());
}
/**
* Test sql_reader::get_events_select_iterator.
* @return void
*/
public function test_events_traversable(): void {
global $DB;
$this->disable_gc();
$this->resetAfterTest();
$this->preventResetByRollback();
$this->setAdminUser();
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$manager = get_log_manager(true);
$stores = $manager->get_readers();
$store = $stores['logstore_standard'];
$events = $store->get_events_select_iterator('', array(), '', 0, 0);
$this->assertFalse($events->valid());
// Here it should be already closed, but we should be allowed to
// over-close it without exception.
$events->close();
$user = $this->getDataGenerator()->create_user();
for ($i = 0; $i < 1000; $i++) {
\core\event\user_created::create_from_userid($user->id)->trigger();
}
$store->flush();
// Check some various sizes get the right number of elements.
$this->assertEquals(1, iterator_count($store->get_events_select_iterator('', array(), '', 0, 1)));
$this->assertEquals(2, iterator_count($store->get_events_select_iterator('', array(), '', 0, 2)));
$iterator = $store->get_events_select_iterator('', array(), '', 0, 500);
$this->assertInstanceOf('\core\event\base', $iterator->current());
$this->assertEquals(500, iterator_count($iterator));
$iterator->close();
// Look for non-linear memory usage for the iterator version.
$mem = memory_get_usage();
$events = $store->get_events_select('', array(), '', 0, 0);
$arraymemusage = memory_get_usage() - $mem;
$mem = memory_get_usage();
$eventsit = $store->get_events_select_iterator('', array(), '', 0, 0);
$eventsit->close();
$itmemusage = memory_get_usage() - $mem;
$this->assertInstanceOf('\Traversable', $eventsit);
$this->assertLessThan($arraymemusage / 10, $itmemusage);
set_config('enabled_stores', '', 'tool_log');
get_log_manager(true);
}
/**
* Test that the standard log cleanup works correctly.
*/
public function test_cleanup_task(): void {
global $DB;
$this->resetAfterTest();
// Create some records spread over various days; test multiple iterations in cleanup.
$ctx = \context_course::instance(1);
$record = (object) array(
'edulevel' => 0,
'contextid' => $ctx->id,
'contextlevel' => $ctx->contextlevel,
'contextinstanceid' => $ctx->instanceid,
'userid' => 1,
'timecreated' => time(),
);
$DB->insert_record('logstore_standard_log', $record);
$record->timecreated -= 3600 * 24 * 30;
$DB->insert_record('logstore_standard_log', $record);
$record->timecreated -= 3600 * 24 * 30;
$DB->insert_record('logstore_standard_log', $record);
$record->timecreated -= 3600 * 24 * 30;
$DB->insert_record('logstore_standard_log', $record);
$this->assertEquals(4, $DB->count_records('logstore_standard_log'));
// Remove all logs before "today".
set_config('loglifetime', 1, 'logstore_standard');
$this->expectOutputString(" Deleted old log records from standard store.\n");
$clean = new \logstore_standard\task\cleanup_task();
$clean->execute();
$this->assertEquals(1, $DB->count_records('logstore_standard_log'));
}
/**
* Tests the decode_other function can cope with both JSON and PHP serialized format.
*
* @param mixed $value Value to encode and decode
* @dataProvider decode_other_provider
*/
public function test_decode_other($value): void {
$this->assertEquals($value, \logstore_standard\log\store::decode_other(serialize($value)));
$this->assertEquals($value, \logstore_standard\log\store::decode_other(json_encode($value)));
}
public function test_decode_other_with_wrongly_encoded_contents(): void {
$this->assertSame(null, \logstore_standard\log\store::decode_other(null));
}
/**
* List of possible values for 'other' field.
*
* I took these types from our logs based on the different first character of PHP serialized
* data - my query found only these types. The normal case is an array.
*
* @return array Array of parameters
*/
public function decode_other_provider(): array {
return [
[['info' => 'd2819896', 'logurl' => 'discuss.php?d=2819896']],
[null],
['just a string'],
[32768]
];
}
/**
* Checks that backup and restore of log data works correctly.
*
* @param bool $jsonformat True to test with JSON format
* @dataProvider log_writing_provider
*/
public function test_backup_restore(bool $jsonformat): void {
global $DB;
$this->resetAfterTest();
$this->preventResetByRollback();
// Enable logging plugin.
set_config('enabled_stores', 'logstore_standard', 'tool_log');
set_config('buffersize', 0, 'logstore_standard');
$manager = get_log_manager(true);
// User must be enrolled in course.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
$this->setUser($user);
// Apply JSON format system setting.
set_config('jsonformat', $jsonformat ? 1 : 0, 'logstore_standard');
// Create some log data in a course - one with other data, one without.
\logstore_standard\event\unittest_executed::create([
'context' => \context_course::instance($course->id),
'other' => ['sample' => 5, 'xx' => 10]])->trigger();
$this->waitForSecond();
\logstore_standard\event\unittest_executed::create([
'context' => \context_course::instance($course->id)])->trigger();
$records = array_values($DB->get_records('logstore_standard_log',
['courseid' => $course->id, 'target' => 'unittest'], 'timecreated'));
$this->assertCount(2, $records);
// Work out expected 'other' values based on JSON format.
$expected0 = [
false => 'a:2:{s:6:"sample";i:5;s:2:"xx";i:10;}',
true => '{"sample":5,"xx":10}'
];
$expected1 = [
false => 'N;',
true => 'null'
];
// Backup the course twice, including log data.
$this->setAdminUser();
$backupid1 = $this->backup($course);
$backupid2 = $this->backup($course);
// Restore it with same jsonformat.
$newcourseid = $this->restore($backupid1, $course, '_A');
// Check entries are correctly encoded.
$records = array_values($DB->get_records('logstore_standard_log',
['courseid' => $newcourseid, 'target' => 'unittest'], 'timecreated'));
$this->assertCount(2, $records);
$this->assertEquals($expected0[$jsonformat], $records[0]->other);
$this->assertEquals($expected1[$jsonformat], $records[1]->other);
// Change JSON format to opposite value and restore again.
set_config('jsonformat', $jsonformat ? 0 : 1, 'logstore_standard');
$newcourseid = $this->restore($backupid2, $course, '_B');
// Check entries are correctly encoded in other format.
$records = array_values($DB->get_records('logstore_standard_log',
['courseid' => $newcourseid, 'target' => 'unittest'], 'timecreated'));
$this->assertEquals($expected0[!$jsonformat], $records[0]->other);
$this->assertEquals($expected1[!$jsonformat], $records[1]->other);
}
/**
* Backs a course up to temp directory.
*
* @param \stdClass $course Course object to backup
* @return string ID of backup
*/
protected function backup($course): string {
global $USER, $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
// Turn off file logging, otherwise it can't delete the file (Windows).
$CFG->backup_file_logger_level = \backup::LOG_NONE;
// Do backup with default settings. MODE_IMPORT means it will just
// create the directory and not zip it.
$bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id,
\backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
$USER->id);
$bc->get_plan()->get_setting('users')->set_status(\backup_setting::NOT_LOCKED);
$bc->get_plan()->get_setting('users')->set_value(true);
$bc->get_plan()->get_setting('logs')->set_value(true);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
return $backupid;
}
/**
* Restores a course from temp directory.
*
* @param string $backupid Backup id
* @param \stdClass $course Original course object
* @param string $suffix Suffix to add after original course shortname and fullname
* @return int New course id
* @throws \restore_controller_exception
*/
protected function restore(string $backupid, $course, string $suffix): int {
global $USER, $CFG;
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
// Do restore to new course with default settings.
$newcourseid = \restore_dbops::create_new_course(
$course->fullname . $suffix, $course->shortname . $suffix, $course->category);
$rc = new \restore_controller($backupid, $newcourseid,
\backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
\backup::TARGET_NEW_COURSE);
$rc->get_plan()->get_setting('logs')->set_value(true);
$rc->get_plan()->get_setting('users')->set_value(true);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
return $newcourseid;
}
/**
* Disable the garbage collector if it's enabled to ensure we don't adjust memory statistics.
*/
private function disable_gc() {
if (gc_enabled()) {
$this->wedisabledgc = true;
gc_disable();
}
}
/**
* Reset any garbage collector changes to the previous state at the end of the test.
*/
public function tearDown(): void {
if ($this->wedisabledgc) {
gc_enable();
}
$this->wedisabledgc = false;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Standard log store.
*
* @package logstore_standard
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'logstore_standard'; // Full name of the plugin (used for diagnostics).
+93
View File
@@ -0,0 +1,93 @@
<?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/>.
/**
* Logging store management.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../../../config.php');
require_once($CFG->libdir . '/adminlib.php');
$action = required_param('action', PARAM_ALPHANUMEXT);
$store = required_param('store', PARAM_PLUGIN);
$PAGE->set_url('/admin/tool/log/stores.php');
$PAGE->set_context(context_system::instance());
require_admin();
require_sesskey();
$all = \tool_log\log\manager::get_store_plugins();
$enabled = get_config('tool_log', 'enabled_stores');
if (!$enabled) {
$enabled = array();
} else {
$enabled = array_flip(explode(',', $enabled));
}
$return = new moodle_url('/admin/settings.php', array('section' => 'managelogging'));
$syscontext = context_system::instance();
switch ($action) {
case 'disable':
$class = \core_plugin_manager::resolve_plugininfo_class('logstore');
$class::enable_plugin($store, false);
break;
case 'enable':
$class = \core_plugin_manager::resolve_plugininfo_class('logstore');
$class::enable_plugin($store, true);
break;
case 'up':
if (!isset($enabled[$store])) {
break;
}
$enabled = array_keys($enabled);
$enabled = array_flip($enabled);
$current = $enabled[$store];
if ($current == 0) {
break; // Already at the top.
}
$enabled = array_flip($enabled);
$enabled[$current] = $enabled[$current - 1];
$enabled[$current - 1] = $store;
set_config('enabled_stores', implode(',', $enabled), 'tool_log');
break;
case 'down':
if (!isset($enabled[$store])) {
break;
}
$enabled = array_keys($enabled);
$enabled = array_flip($enabled);
$current = $enabled[$store];
if ($current == count($enabled) - 1) {
break; // Already at the end.
}
$enabled = array_flip($enabled);
$enabled[$current] = $enabled[$current + 1];
$enabled[$current + 1] = $store;
set_config('enabled_stores', implode(',', $enabled), 'tool_log');
break;
}
redirect($return);
@@ -0,0 +1,19 @@
@tool_log @report @report_configlog
Feature: In a report, admin can see logstore visibility changes
# Change log stores visibility so the report contains known data.
Background:
Given I log in as "admin"
And the following config values are set as admin:
| enabled_stores | logstore_standard | tool_log |
@javascript
Scenario: Display configuration changes report
When I navigate to "Plugins > Logging > Manage log stores" in site administration
And I click on "Disable" "icon" in the "Standard log" "table_row"
And I click on "Enable" "icon" in the "External database log" "table_row"
And I navigate to "Reports > Config changes" in site administration
Then the following should exist in the "reportbuilder-table" table:
| First name | Plugin | Setting | New value | Original value |
| Admin User | logstore_standard | tool_logstore_visibility | 0 | 1 |
| Admin User | logstore_database | tool_logstore_visibility | 1 | 0 |
+69
View File
@@ -0,0 +1,69 @@
<?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/>.
namespace tool_log;
/**
* Log manager and log API tests.
*
* @package tool_log
* @copyright 2014 Petr Skoda {@link http://skodak.org/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager_test extends \advanced_testcase {
public function test_get_log_manager(): void {
global $CFG;
$this->resetAfterTest();
$manager = get_log_manager();
$this->assertInstanceOf('core\log\manager', $manager);
$stores = $manager->get_readers();
$this->assertIsArray($stores);
$this->assertCount(0, $stores);
$this->assertFileExists("$CFG->dirroot/$CFG->admin/tool/log/store/standard/version.php");
set_config('enabled_stores', 'logstore_standard', 'tool_log');
$manager = get_log_manager(true);
$this->assertInstanceOf('core\log\manager', $manager);
$stores = $manager->get_readers();
$this->assertIsArray($stores);
$this->assertCount(1, $stores);
foreach ($stores as $key => $store) {
$this->assertIsString($key);
$this->assertInstanceOf('core\log\sql_reader', $store);
}
$stores = $manager->get_readers('core\log\sql_internal_table_reader');
$this->assertIsArray($stores);
$this->assertCount(1, $stores);
foreach ($stores as $key => $store) {
$this->assertIsString($key);
$this->assertSame('logstore_standard', $key);
$this->assertInstanceOf('core\log\sql_internal_table_reader', $store);
}
$stores = $manager->get_readers('core\log\sql_reader');
$this->assertIsArray($stores);
$this->assertCount(1, $stores);
foreach ($stores as $key => $store) {
$this->assertIsString($key);
$this->assertInstanceOf('core\log\sql_reader', $store);
}
}
}
@@ -0,0 +1,177 @@
<?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/>.
/**
* Data provider tests.
*
* @package tool_log
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_log\privacy;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use tool_log\privacy\provider;
require_once($CFG->dirroot . '/admin/tool/log/store/standard/tests/fixtures/event.php');
/**
* Data provider testcase class.
*
* We're not testing the full functionality, just that the provider passes the requests
* down to at least one of its subplugin. Each subplugin should have tests to cover the
* different provider methods in depth.
*
* @package tool_log
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
public function setUp(): void {
$this->resetAfterTest();
$this->preventResetByRollback(); // Logging waits till the transaction gets committed.
}
public function test_get_contexts_for_userid(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c1ctx = \context_course::instance($c1->id);
$this->enable_logging();
$manager = get_log_manager(true);
$this->setUser($u1);
$this->assertEmpty(provider::get_contexts_for_userid($u1->id)->get_contextids());
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$this->assertEquals($c1ctx->id, provider::get_contexts_for_userid($u1->id)->get_contextids()[0]);
}
public function test_delete_data_for_user(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c1ctx = \context_course::instance($c1->id);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
// User 2 is the author.
$this->setUser($u2);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
// Confirm data present.
$this->assertTrue($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
// Delete all the things!
provider::delete_data_for_user(new approved_contextlist($u1, 'logstore_standard', [$c1ctx->id]));
$this->assertFalse($DB->record_exists('logstore_standard_log', ['userid' => $u1->id, 'contextid' => $c1ctx->id]));
$this->assertEquals(0, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
}
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$u1 = $this->getDataGenerator()->create_user();
$u2 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c1ctx = \context_course::instance($c1->id);
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
// User 2 is the author.
$this->setUser($u2);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx]);
$e->trigger();
// Confirm data present.
$this->assertTrue($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id]));
$this->assertEquals(2, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(1, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
// Delete all the things!
provider::delete_data_for_all_users_in_context($c1ctx);
$this->assertFalse($DB->record_exists('logstore_standard_log', ['contextid' => $c1ctx->id]));
$this->assertEquals(0, $DB->count_records('logstore_standard_log', ['userid' => $u1->id]));
$this->assertEquals(0, $DB->count_records('logstore_standard_log', ['userid' => $u2->id]));
}
public function test_export_data_for_user(): void {
$admin = \core_user::get_user(2);
$u1 = $this->getDataGenerator()->create_user();
$c1 = $this->getDataGenerator()->create_course();
$c1ctx = \context_course::instance($c1->id);
$path = [get_string('privacy:path:logs', 'tool_log'), get_string('pluginname', 'logstore_standard')];
$this->enable_logging();
$manager = get_log_manager(true);
// User 1 is the author.
$this->setUser($u1);
$e = \logstore_standard\event\unittest_executed::create(['context' => $c1ctx, 'other' => ['i' => 123]]);
$e->trigger();
// Confirm data present for u1.
provider::export_user_data(new approved_contextlist($u1, 'tool_log', [$c1ctx->id]));
$data = writer::with_context($c1ctx)->get_data($path);
$this->assertCount(1, $data->logs);
$this->assertEquals(transform::yesno(true), $data->logs[0]['author_of_the_action_was_you']);
$this->assertSame(123, $data->logs[0]['other']['i']);
}
/**
* Enable logging.
*
* @return void
*/
protected function enable_logging() {
set_config('enabled_stores', 'logstore_standard', 'tool_log');
set_config('buffersize', 0, 'logstore_standard');
set_config('logguests', 1, 'logstore_standard');
}
}
+15
View File
@@ -0,0 +1,15 @@
This files describes API changes in /admin/tool/log - plugins,
information provided here is intended especially for developers.
=== 3.7 ===
* The new jsonformat option, which defaults to 'on' for a new install (and 'off' for existing installs) means that
the 'other' event field is now stored in JSON format instead of PHP serialize format in the database. The system
can read data in both formats but if any third-party software directly accesses the database field, it may need
to be modified (or require users to turn off jsonformat).
=== 3.6 ===
* The legacy log store is in its first stage of deprecation and is due for removal in Moodle 3.10. Please use one of
the other log stores such as "standard" and "database".
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Default log manager.
*
* @package tool_log
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'tool_log'; // Full name of the plugin (used for diagnostics).