From 7eaca5a810123746cdc69bf04dc7dc32a13533bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0koda?= Date: Mon, 13 Jan 2014 09:08:58 +0800 Subject: [PATCH] MDL-37658 add new logging API with basic implemenation --- admin/settings/server.php | 15 -- admin/tool/log/classes/log/manager.php | 166 +++++++++++++ admin/tool/log/classes/log/observer.php | 43 ++++ admin/tool/log/classes/log/store.php | 49 ++++ admin/tool/log/classes/log/writer.php | 37 +++ .../tool/log/classes/plugininfo/logstore.php | 89 +++++++ .../tool/log/classes/setting_managestores.php | 229 ++++++++++++++++++ admin/tool/log/db/events.php | 35 +++ admin/tool/log/db/install.php | 41 ++++ admin/tool/log/db/subplugins.php | 24 ++ admin/tool/log/lang/en/tool_log.php | 29 +++ admin/tool/log/lib.php | 37 +++ admin/tool/log/settings.php | 38 +++ .../log/store/database/classes/log/store.php | 114 +++++++++ .../database/lang/en/logstore_database.php | 26 ++ admin/tool/log/store/database/settings.php | 54 +++++ admin/tool/log/store/database/version.php | 29 +++ .../legacy/classes/event/legacy_logged.php | 53 ++++ .../log/store/legacy/classes/log/store.php | 191 +++++++++++++++ admin/tool/log/store/legacy/db/access.php | 38 +++ .../store/legacy/lang/en/logstore_legacy.php | 30 +++ admin/tool/log/store/legacy/settings.php | 52 ++++ admin/tool/log/store/legacy/version.php | 29 +++ .../log/store/standard/classes/log/store.php | 114 +++++++++ admin/tool/log/store/standard/db/access.php | 38 +++ admin/tool/log/store/standard/db/install.xml | 37 +++ .../standard/lang/en/logstore_standard.php | 27 +++ admin/tool/log/store/standard/settings.php | 32 +++ admin/tool/log/store/standard/version.php | 29 +++ admin/tool/log/stores.php | 98 ++++++++ admin/tool/log/version.php | 29 +++ lang/en/admin.php | 1 - lib/classes/event/base.php | 82 ++++++- lib/classes/log/dummy_manager.php | 36 +++ .../delete_logs_task.php => log/manager.php} | 48 ++-- lib/classes/log/reader.php | 72 ++++++ lib/classes/log/sql_reader.php | 43 ++++ lib/classes/plugin_manager.php | 6 +- lib/datalib.php | 138 +++-------- lib/db/tasks.php | 9 - lib/deprecatedlib.php | 44 ++-- lib/tests/event_test.php | 9 +- 42 files changed, 2168 insertions(+), 172 deletions(-) create mode 100644 admin/tool/log/classes/log/manager.php create mode 100644 admin/tool/log/classes/log/observer.php create mode 100644 admin/tool/log/classes/log/store.php create mode 100644 admin/tool/log/classes/log/writer.php create mode 100644 admin/tool/log/classes/plugininfo/logstore.php create mode 100644 admin/tool/log/classes/setting_managestores.php create mode 100644 admin/tool/log/db/events.php create mode 100644 admin/tool/log/db/install.php create mode 100644 admin/tool/log/db/subplugins.php create mode 100644 admin/tool/log/lang/en/tool_log.php create mode 100644 admin/tool/log/lib.php create mode 100644 admin/tool/log/settings.php create mode 100644 admin/tool/log/store/database/classes/log/store.php create mode 100644 admin/tool/log/store/database/lang/en/logstore_database.php create mode 100644 admin/tool/log/store/database/settings.php create mode 100644 admin/tool/log/store/database/version.php create mode 100644 admin/tool/log/store/legacy/classes/event/legacy_logged.php create mode 100644 admin/tool/log/store/legacy/classes/log/store.php create mode 100644 admin/tool/log/store/legacy/db/access.php create mode 100644 admin/tool/log/store/legacy/lang/en/logstore_legacy.php create mode 100644 admin/tool/log/store/legacy/settings.php create mode 100644 admin/tool/log/store/legacy/version.php create mode 100644 admin/tool/log/store/standard/classes/log/store.php create mode 100644 admin/tool/log/store/standard/db/access.php create mode 100644 admin/tool/log/store/standard/db/install.xml create mode 100644 admin/tool/log/store/standard/lang/en/logstore_standard.php create mode 100644 admin/tool/log/store/standard/settings.php create mode 100644 admin/tool/log/store/standard/version.php create mode 100644 admin/tool/log/stores.php create mode 100644 admin/tool/log/version.php create mode 100644 lib/classes/log/dummy_manager.php rename lib/classes/{task/delete_logs_task.php => log/manager.php} (50%) create mode 100644 lib/classes/log/reader.php create mode 100644 lib/classes/log/sql_reader.php diff --git a/admin/settings/server.php b/admin/settings/server.php index 20f69b27e72..ec37e4351a9 100644 --- a/admin/settings/server.php +++ b/admin/settings/server.php @@ -137,21 +137,6 @@ $temp->add(new admin_setting_configselect('deleteincompleteusers', new lang_stri 48 => new lang_string('numdays', '', 2), 24 => new lang_string('numdays', '', 1)))); -$temp->add(new admin_setting_configcheckbox('logguests', new lang_string('logguests', 'admin'), - new lang_string('logguests_help', 'admin'), 1)); -$temp->add(new admin_setting_configselect('loglifetime', new lang_string('loglifetime', 'admin'), new lang_string('configloglifetime', 'admin'), 0, 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)))); - $temp->add(new admin_setting_configcheckbox('disablegradehistory', new lang_string('disablegradehistory', 'grades'), new lang_string('disablegradehistory_help', 'grades'), 0)); diff --git a/admin/tool/log/classes/log/manager.php b/admin/tool/log/classes/log/manager.php new file mode 100644 index 00000000000..0697f774991 --- /dev/null +++ b/admin/tool/log/classes/log/manager.php @@ -0,0 +1,166 @@ +. + +/** + * 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 */ + 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 \context $context + * @return \core\log\reader[] list of available log data readers + */ + public function get_readers(\context $context) { + $this->init(); + $return = array(); + foreach ($this->readers as $plugin => $reader) { + if ($reader->can_access($context)) { + $return[$plugin] = $reader; + } + } + 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; + } + + /** + * Execute cron actions. + */ + public function cron() { + $this->init(); + foreach ($this->stores['legacy'] as $store) { + $store->cron(); + } + } + + /** + * Legacy add_to_log() redirection. + * + * To be used only from deprecated add_to_log() function and event trigger() method. + * + * NOTE: this is hardcoded to legacy log store plugin, hopefully we can get rid of it soon. + * + * @param int $courseid The course id + * @param string $module The module name e.g. forum, journal, resource, course, user etc + * @param string $action 'view', 'update', 'add' or 'delete', possibly followed by another word to clarify. + * @param string $url The file and parameters used to see the results of the action + * @param string $info Additional description information + * @param int $cm The course_module->id if there is one + * @param int|\stdClass $user If log regards $user other than $USER + */ + public function legacy_add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user=0) { + $this->init(); + if (isset($this->stores['legacy'])) { + $this->stores['legacy']->legacy_add_to_log($courseid, $module, $action, $url, $info, $cm, $user); + } + } +} diff --git a/admin/tool/log/classes/log/observer.php b/admin/tool/log/classes/log/observer.php new file mode 100644 index 00000000000..0728937db60 --- /dev/null +++ b/admin/tool/log/classes/log/observer.php @@ -0,0 +1,43 @@ +. + +/** + * 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); + } + } +} diff --git a/admin/tool/log/classes/log/store.php b/admin/tool/log/classes/log/store.php new file mode 100644 index 00000000000..1e402655d07 --- /dev/null +++ b/admin/tool/log/classes/log/store.php @@ -0,0 +1,49 @@ +. + +/** + * 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(); + + /** + * Execute cron actions. + * @return void + */ + public function cron(); +} diff --git a/admin/tool/log/classes/log/writer.php b/admin/tool/log/classes/log/writer.php new file mode 100644 index 00000000000..82537ceebde --- /dev/null +++ b/admin/tool/log/classes/log/writer.php @@ -0,0 +1,37 @@ +. + +/** + * Log store writer interface. + * + * @package core + * @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); +} diff --git a/admin/tool/log/classes/plugininfo/logstore.php b/admin/tool/log/classes/plugininfo/logstore.php new file mode 100644 index 00000000000..bfd3854809d --- /dev/null +++ b/admin/tool/log/classes/plugininfo/logstore.php @@ -0,0 +1,89 @@ +. + +/** + * 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 core\plugininfo\base, moodle_url, part_of_admin_tree, admin_settingpage; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Plugin info class for logging store plugins. + */ +class logstore extends base { + + 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 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. + $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(); + } +} diff --git a/admin/tool/log/classes/setting_managestores.php b/admin/tool/log/classes/setting_managestores.php new file mode 100644 index 00000000000..2e9b8e5f519 --- /dev/null +++ b/admin/tool/log/classes/setting_managestores.php @@ -0,0 +1,229 @@ +. + +/** + * 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(); + + $available = \tool_log\log\manager::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'), $strversion, $strenable, $strup.'/'.$strdown, $strsettings, $struninstall); + $table->colclasses = array('leftalign', '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; + } + + // Hide/show links. + if (isset($enabled[$store])) { + $aurl = new moodle_url($url, array('action'=>'disable', 'store'=>$store)); + $hideshow = ""; + $hideshow .= "pix_url('t/hide') . "\" class=\"iconsmall\" alt=\"$strdisable\" />"; + $isenabled = true; + $displayname = "$name"; + } else if (isset($available[$store])) { + $aurl = new moodle_url($url, array('action'=>'enable', 'store'=>$store)); + $hideshow = ""; + $hideshow .= "pix_url('t/show') . "\" class=\"iconsmall\" alt=\"$strenable\" />"; + $isenabled = false; + $displayname = "$name"; + } else { + $hideshow = ''; + $isenabled = false; + $displayname = ''.$name.''; + } + if ($PAGE->theme->resolve_image_location('icon', $store, false)) { + $icon = $OUTPUT->pix_icon('icon', '', $store, array('class' => 'icon pluginicon')); + } else { + $icon = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'icon pluginicon noicon')); + } + + // 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 .= ""; + $updown .= "pix_url('t/up') . "\" alt=\"$strup\" class=\"iconsmall\" /> "; + } else { + $updown .= "pix_url('spacer') . "\" class=\"iconsmall\" alt=\"\" /> "; + } + if ($updowncount < $storecount) { + $aurl = new moodle_url($url, array('action'=>'down', 'store'=>$store)); + $updown .= ""; + $updown .= "pix_url('t/down') . "\" alt=\"$strdown\" class=\"iconsmall\" />"; + } else { + $updown .= "pix_url('spacer') . "\" class=\"iconsmall\" alt=\"\" />"; + } + ++$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, $version, $hideshow, $updown, $settings, $uninstall); + + $printed[$store] = true; + } + + $return .= html_writer::table($table); + $return .= get_string('configlogplugins', 'tool_log').'
'.get_string('tablenosave', 'admin'); + $return .= $OUTPUT->box_end(); + return highlight($query, $return); + } +} diff --git a/admin/tool/log/db/events.php b/admin/tool/log/db/events.php new file mode 100644 index 00000000000..ceab58ff147 --- /dev/null +++ b/admin/tool/log/db/events.php @@ -0,0 +1,35 @@ +. + +/** + * 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, + ), +); diff --git a/admin/tool/log/db/install.php b/admin/tool/log/db/install.php new file mode 100644 index 00000000000..934db5977bf --- /dev/null +++ b/admin/tool/log/db/install.php @@ -0,0 +1,41 @@ +. + +/** + * 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(); + +function xmldb_tool_log_install() { + global $CFG; + + $enabled = array(); + + if (file_exists("$CFG->dirroot/$CFG->admin/tool/log/store/standard")) { + $enabled[] = 'logstore_standard'; + } + + if (file_exists("$CFG->dirroot/$CFG->admin/tool/log/store/legacy")) { + $enabled[] = 'logstore_legacy'; + } + + set_config('enabled_stores', implode(',', $enabled), 'tool_log'); +} diff --git a/admin/tool/log/db/subplugins.php b/admin/tool/log/db/subplugins.php new file mode 100644 index 00000000000..4eb8fa6da20 --- /dev/null +++ b/admin/tool/log/db/subplugins.php @@ -0,0 +1,24 @@ +. + +/** + * Logging subplugins. + * + * @package tool_log + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$subplugins = array('logstore'=>'admin/tool/log/store'); diff --git a/admin/tool/log/lang/en/tool_log.php b/admin/tool/log/lang/en/tool_log.php new file mode 100644 index 00000000000..7301031633b --- /dev/null +++ b/admin/tool/log/lang/en/tool_log.php @@ -0,0 +1,29 @@ +. + +/** + * 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 then in appropriate order.'; +$string['logging'] = 'Logging'; +$string['managelogging'] = 'Manage log stores'; +$string['pluginname'] = 'Log store manager'; diff --git a/admin/tool/log/lib.php b/admin/tool/log/lib.php new file mode 100644 index 00000000000..32be81b49ca --- /dev/null +++ b/admin/tool/log/lib.php @@ -0,0 +1,37 @@ +. + +/** + * Log tool API. + * + * @package tool_log + * @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; + +/** + * Execute cron actions. + */ +function tool_log_cron() { + // Execute cron only if this log manager used. + $logmanager = get_log_manager(); + if (get_class($logmanager) === 'tool_log\log\manager') { + /** @var \tool_log\log\manager $logmanager */ + $logmanager->cron(); + } +} diff --git a/admin/tool/log/settings.php b/admin/tool/log/settings.php new file mode 100644 index 00000000000..27c263e45a8 --- /dev/null +++ b/admin/tool/log/settings.php @@ -0,0 +1,38 @@ +. + +/** + * 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) { + $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); + } +} diff --git a/admin/tool/log/store/database/classes/log/store.php b/admin/tool/log/store/database/classes/log/store.php new file mode 100644 index 00000000000..8a4e63389da --- /dev/null +++ b/admin/tool/log/store/database/classes/log/store.php @@ -0,0 +1,114 @@ +. + +/** + * External database writer. + * + * @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 { + /** @var \tool_log\log\manager $manager */ + protected $manager; + /** @var \moodle_database $extdb */ + protected $extdb; + + public function __construct(\tool_log\log\manager $manager) { + $this->manager = $manager; + } + + protected function init() { + if (isset($this->extdb)) { + return !empty($this->extdb); + } + + $dbdriver = $this->get_config('dbdriver'); + if (!$dbdriver) { + $this->extdb = false; + return false; + } + list($dbtype, $dblibrary) = 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', ''); + + try { + $db->connect($this->get_config('dbhost'), $this->get_config('dbuser'), $this->get_config('dbpass'), + $this->get_config('dbname'), $this->get_config('dbprefix'), $dboptions); + } catch (\moodle_exception $e) { + debugging('Cannot connect to external database: '.$e->getMessage(), DEBUG_DEVELOPER); + $this->extdb = false; + return false; + } + + $this->extdb = $db; + return true; + } + + protected function get_config($name, $default = null) { + $value = \get_config('logstore_database', $name); + if ($value !== false) { + return $value; + } + return $default; + } + + public function write(\core\event\base $event) { + if (!$this->init()) { + return; + } + + if (!$dbtable = $this->get_config('dbtable')) { + return; + } + + $data = $event->get_data(); + if (CLI_SCRIPT) { + $data['origin'] = 'cli'; + } else { + $data['origin'] = getremoteaddr(); + } + $data['realuserid'] = \core\session\manager::is_loggedinas() ? $_SESSION['USER']->realuser : null; + + $this->extdb->insert_record($dbtable, $data); + } + + public function cron() { + } + + public function dispose() { + if ($this->extdb) { + $this->extdb->dispose(); + } + $this->extdb = null; + } +} diff --git a/admin/tool/log/store/database/lang/en/logstore_database.php b/admin/tool/log/store/database/lang/en/logstore_database.php new file mode 100644 index 00000000000..7d95b322bee --- /dev/null +++ b/admin/tool/log/store/database/lang/en/logstore_database.php @@ -0,0 +1,26 @@ +. + +/** + * 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['pluginname'] = 'External database log'; +$string['pluginname_desc'] = 'External database log plugin, the data is stored in external database table.'; diff --git a/admin/tool/log/store/database/settings.php b/admin/tool/log/store/database/settings.php new file mode 100644 index 00000000000..776f456ef76 --- /dev/null +++ b/admin/tool/log/store/database/settings.php @@ -0,0 +1,54 @@ +. + +/** + * 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) { + $options = array( + '' => get_string('choose'), + '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(), + 'native/mssql' => moodle_database::get_driver_instance('mssql', 'native')->get_name(), + ); + + // TODO: Localise these settings. + + $settings->add(new admin_setting_configselect('logstore_database/dbdriver', 'dbdriver', '', '', $options)); + + $settings->add(new admin_setting_configtext('logstore_database/dbhost', 'dbhost', '', '')); + $settings->add(new admin_setting_configtext('logstore_database/dbuser', 'dbuser', '', '')); + $settings->add(new admin_setting_configtext('logstore_database/dbpass', 'dbpass', '', '')); + $settings->add(new admin_setting_configtext('logstore_database/dbname', 'dbname', '', '')); + $settings->add(new admin_setting_configtext('logstore_database/dbname', 'dbprefix', '', '')); + + $settings->add(new admin_setting_configcheckbox('logstore_database/dbpersist', 'dbpersist', '', '0')); + $settings->add(new admin_setting_configtext('logstore_database/dbsocket', 'dbsocket', '', '')); + $settings->add(new admin_setting_configtext('logstore_database/dbport', 'dbport', '', '')); + $settings->add(new admin_setting_configtext('logstore_database/dbschema', 'dbschema', '', '')); + $settings->add(new admin_setting_configtext('logstore_database/dbcollation', 'dbcollation', '', '')); + +} diff --git a/admin/tool/log/store/database/version.php b/admin/tool/log/store/database/version.php new file mode 100644 index 00000000000..85eb8c22c41 --- /dev/null +++ b/admin/tool/log/store/database/version.php @@ -0,0 +1,29 @@ +. + +/** + * External database 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 = 2014011300; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014011000; // Requires this Moodle version. +$plugin->component = 'logstore_database'; // Full name of the plugin (used for diagnostics). diff --git a/admin/tool/log/store/legacy/classes/event/legacy_logged.php b/admin/tool/log/store/legacy/classes/event/legacy_logged.php new file mode 100644 index 00000000000..4a2a10c90f2 --- /dev/null +++ b/admin/tool/log/store/legacy/classes/event/legacy_logged.php @@ -0,0 +1,53 @@ +. + +namespace logstore_legacy\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Legacy log emulation event class. + * + * @package core + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class legacy_logged extends \core\event\base { + + public function init() { + throw new \coding_exception('legacy events cannot be triggered'); + } + + public static function get_name() { + return get_string('event_legacy_logged', 'logstore_legacy'); + } + + public function get_description() { + return $this->other['module'].' '.$this->other['action'].' '.$this->other['info']; + } + + public function get_url() { + global $CFG; + require_once("$CFG->dirroot/course/lib.php"); + + $url = \make_log_url($this->other['module'], $this->other['url']); + if (!$url) { + return null; + } + return new \moodle_url($url); + } +} diff --git a/admin/tool/log/store/legacy/classes/log/store.php b/admin/tool/log/store/legacy/classes/log/store.php new file mode 100644 index 00000000000..6b049620d65 --- /dev/null +++ b/admin/tool/log/store/legacy/classes/log/store.php @@ -0,0 +1,191 @@ +. + +/** + * Legacy log reader. + * + * @package logstore_legacy + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace logstore_legacy\log; + +defined('MOODLE_INTERNAL') || die(); + +class store implements \tool_log\log\store, \core\log\reader { + public function __construct(\tool_log\log\manager $manager) { + } + + public function get_name() { + return get_string('pluginname', 'logstore_legacy'); + } + + public function get_description() { + return get_string('pluginname_desc', 'logstore_legacy'); + } + + public function can_access(\context $context) { + return has_capability('logstore/legacy:read', $context); + } + + public function get_events($selectwhere, array $params, $sort, $limitfrom, $limitnum) { + global $DB; + + $selectwhere = str_replace('timecreated', 'time', $selectwhere); + $sort = str_replace('timecreated', 'time', $sort); + + $events = array(); + + try { + $records = $DB->get_records_select('log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum); + } catch (\moodle_exception $ex) { + debugging("error converting legacy event data", $ex->getMessage().$ex->debuginfo); + } + + foreach ($records as $data) { + $events[$data->id] = \logstore_legacy\event\legacy_logged::restore_legacy($data); + } + + return $events; + } + + public function get_events_count($selectwhere, array $params) { + global $DB; + $selectwhere = str_replace('timecreated', 'time', $selectwhere); + try { + return $DB->count_records_select('log', $selectwhere, $params); + } catch (\moodle_exception $ex) { + debugging("error converting legacy event data", $ex->getMessage().$ex->debuginfo); + return 0; + } + } + + public function cron() { + global $CFG, $DB; + + // Delete old logs to save space (this might need a timer to slow it down...). + if (!empty($CFG->loglifetime)) { // value in days + $loglifetime = time(0) - ($CFG->loglifetime * 3600 * 24); + $DB->delete_records_select("log", "time < ?", array($loglifetime)); + mtrace(" Deleted old log records"); + } + } + + public function dispose() { + } + + /** + * Legacy add_to_log() code. + * + * @param int $courseid The course id + * @param string $module The module name e.g. forum, journal, resource, course, user etc + * @param string $action 'view', 'update', 'add' or 'delete', possibly followed by another word to clarify. + * @param string $url The file and parameters used to see the results of the action + * @param string $info Additional description information + * @param int $cm The course_module->id if there is one + * @param int|\stdClass $user If log regards $user other than $USER + */ + public function legacy_add_to_log($courseid, $module, $action, $url, $info, $cm, $user) { + // Note that this function intentionally does not follow the normal Moodle DB access idioms. + // This is for a good reason: it is the most frequently used DB update function, + // so it has been optimised for speed. + global $DB, $CFG, $USER; + + if ($this->legacy_logging_enabled()) { + return; + } + + if ($cm === '' || is_null($cm)) { // Postgres won't translate empty string to its default. + $cm = 0; + } + + if ($user) { + $userid = $user; + } else { + if (\core\session\manager::is_loggedinas()) { // Don't log. + return; + } + $userid = empty($USER->id) ? '0' : $USER->id; + } + + if (isset($CFG->logguests) and !$CFG->logguests) { + if (!$userid or isguestuser($userid)) { + return; + } + } + + $REMOTE_ADDR = getremoteaddr(); + + $timenow = time(); + if (!empty($url)) { // Could break doing html_entity_decode on an empty var. + $url = html_entity_decode($url, ENT_QUOTES, 'UTF-8'); + } else { + $url = ''; + } + + // Restrict length of log lines to the space actually available in the + // database so that it doesn't cause a DB error. Log a warning so that + // developers can avoid doing things which are likely to cause this on a + // routine basis. + if(!empty($info) && \core_text::strlen($info)>255) { + $info = \core_text::substr($info,0,252).'...'; + debugging('Warning: logged very long info',DEBUG_DEVELOPER); + } + + // If the 100 field size is changed, also need to alter print_log in course/lib.php. + if(!empty($url) && \core_text::strlen($url)>100) { + $url = \core_text::substr($url,0,97).'...'; + debugging('Warning: logged very long URL',DEBUG_DEVELOPER); + } + + if (defined('MDL_PERFDB')) { global $PERF ; $PERF->logwrites++;}; + + $log = array('time'=>$timenow, 'userid'=>$userid, 'course'=>$courseid, 'ip'=>$REMOTE_ADDR, 'module'=>$module, + 'cmid'=>$cm, 'action'=>$action, 'url'=>$url, 'info'=>$info); + + try { + $DB->insert_record_raw('log', $log, false); + } catch (\dml_exception $e) { + debugging('Error: Could not insert a new entry to the Moodle log. '. $e->error, DEBUG_ALL); + + // MDL-11893, alert $CFG->supportemail if insert into log failed. + if ($CFG->supportemail and empty($CFG->noemailever)) { + // Function email_to_user is not usable because email_to_user tries to write to the logs table, + // and this will get caught in an infinite loop, if disk is full. + $site = get_site(); + $subject = 'Insert into log failed at your moodle site '.$site->fullname; + $message = "Insert into log table failed at ". date('l dS \of F Y h:i:s A') .".\n It is possible that your disk is full.\n\n"; + $message .= "The failed query parameters are:\n\n" . var_export($log, true); + + $lasttime = get_config('admin', 'lastloginserterrormail'); + if(empty($lasttime) || time() - $lasttime > 60*60*24) { // limit to 1 email per day + // Using email directly rather than messaging as they may not be able to log in to access a message. + mail($CFG->supportemail, $subject, $message); + set_config('lastloginserterrormail', time(), 'admin'); + } + } + } + } + + /** + * Did admin request to keep adding new data to legacy log table? + * @return bool + */ + protected function legacy_logging_enabled() { + return (bool)get_config('logstore_legacy', 'loglegacy'); + } +} diff --git a/admin/tool/log/store/legacy/db/access.php b/admin/tool/log/store/legacy/db/access.php new file mode 100644 index 00000000000..713e828ba6a --- /dev/null +++ b/admin/tool/log/store/legacy/db/access.php @@ -0,0 +1,38 @@ +. + +/** + * Defines the capabilities used by standard log store. + * + * @package logstore_legacy + * @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(); + +$capabilities = array( + 'logstore/legacy:read' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + ), + ), +); diff --git a/admin/tool/log/store/legacy/lang/en/logstore_legacy.php b/admin/tool/log/store/legacy/lang/en/logstore_legacy.php new file mode 100644 index 00000000000..391eeb444c9 --- /dev/null +++ b/admin/tool/log/store/legacy/lang/en/logstore_legacy.php @@ -0,0 +1,30 @@ +. + +/** + * Legacy log reader lang strings. + * + * @package logstore_legacy + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['event_legacy_logged'] = 'Legacy event'; +$string['legacy:read'] = 'Read logs'; +$string['loglegacy'] = 'Log legacy data'; +$string['loglegacy_help'] = 'Add new records to the legacy log table. It is recommended to disable this for performance reasons.'; +$string['pluginname'] = 'Legacy log'; +$string['pluginname_desc'] = 'Legacy log plugin, it can be used for reading of legacy log data stored in mdl_log table.'; diff --git a/admin/tool/log/store/legacy/settings.php b/admin/tool/log/store/legacy/settings.php new file mode 100644 index 00000000000..6b385616ab3 --- /dev/null +++ b/admin/tool/log/store/legacy/settings.php @@ -0,0 +1,52 @@ +. + +/** + * Legacy logging settings. + * + * @package logstore_legacy + * @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(); + +if ($hassiteconfig) { + $settings->add(new admin_setting_configcheckbox('logstore_legacy/loglegacy', + new lang_string('loglegacy', 'logstore_legacy'), + new lang_string('loglegacy_help', 'logstore_legacy'), 0)); + + $settings->add(new admin_setting_configcheckbox('logguests', + new lang_string('logguests', 'admin'), + new lang_string('logguests_help', 'admin'), 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('loglifetime', + new lang_string('loglifetime', 'admin'), + new lang_string('configloglifetime', 'admin'), 0, $options)); +} diff --git a/admin/tool/log/store/legacy/version.php b/admin/tool/log/store/legacy/version.php new file mode 100644 index 00000000000..a10af9ff2a3 --- /dev/null +++ b/admin/tool/log/store/legacy/version.php @@ -0,0 +1,29 @@ +. + +/** + * Legacy log reader. + * + * @package logstore_legacy + * @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 = 2014011300; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014011000; // Requires this Moodle version. +$plugin->component = 'logstore_legacy'; // Full name of the plugin (used for diagnostics). diff --git a/admin/tool/log/store/standard/classes/log/store.php b/admin/tool/log/store/standard/classes/log/store.php new file mode 100644 index 00000000000..481060c01df --- /dev/null +++ b/admin/tool/log/store/standard/classes/log/store.php @@ -0,0 +1,114 @@ +. + +/** + * 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_reader { + /** @var string $logguests true if logging guest access */ + protected $logguests; + + public function __construct(\tool_log\log\manager $manager) { + $this->logguests = get_config('logstore_standard', 'logguests'); + if ($this->logguests === false) { + // Log everything before setting is saved for the first time. + $this->logguests = '1'; + } + } + + public function write(\core\event\base $event) { + global $DB; + + // Filter events. + if (!CLI_SCRIPT and !$this->logguests) { + // Always log inside CLI scripts because we do not login there. + if (!isloggedin() or isguestuser()) { + return; + } + } + + $data = $event->get_data(); + $data['other'] = serialize($data['other']); + if (CLI_SCRIPT) { + $data['origin'] = 'cli'; + } else { + $data['origin'] = getremoteaddr(); + } + $data['realuserid'] = \core\session\manager::is_loggedinas() ? $_SESSION['USER']->realuser : null; + + $DB->insert_record('logstore_standard_log', $data); + } + + public function get_name() { + return get_string('pluginname', 'logstore_standard'); + } + + public function get_description() { + return get_string('pluginname_desc', 'logstore_standard'); + } + + public function can_access(\context $context) { + return has_capability('logstore/standard:read', $context); + } + + public function get_events($selectwhere, array $params, $sort, $limitfrom, $limitnum) { + global $DB; + + $events = array(); + $records = $DB->get_records_select('logstore_standard_log', $selectwhere, $params, $sort, '*', $limitfrom, $limitnum); + + foreach ($records as $data) { + $extra = array('origin'=>$data->origin, 'realuserid'=>$data->realuserid); + $data = (array)$data; + $id = $data['id']; + $data['other'] = unserialize($data['other']); + if ($data['other'] === false) { + $data['other'] = array(); + } + unset($data['origin']); + unset($data['realuserid']); + unset($data['id']); + + $events[$id] = \core\event\base::restore($data, $extra); + } + + return $events; + } + + public function get_events_count($selectwhere, array $params) { + global $DB; + return $DB->count_records_select('logstore_standard_log', $selectwhere, $params); + } + + public function get_log_table() { + return 'logstore_standard_log'; + } + + public function cron() { + } + + public function dispose() { + } +} diff --git a/admin/tool/log/store/standard/db/access.php b/admin/tool/log/store/standard/db/access.php new file mode 100644 index 00000000000..81e64e5ae4e --- /dev/null +++ b/admin/tool/log/store/standard/db/access.php @@ -0,0 +1,38 @@ +. + +/** + * Defines the capabilities used by 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(); + +$capabilities = array( + 'logstore/standard:read' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + ), + ), +); diff --git a/admin/tool/log/store/standard/db/install.xml b/admin/tool/log/store/standard/db/install.xml new file mode 100644 index 00000000000..dddadb382bd --- /dev/null +++ b/admin/tool/log/store/standard/db/install.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/admin/tool/log/store/standard/lang/en/logstore_standard.php b/admin/tool/log/store/standard/lang/en/logstore_standard.php new file mode 100644 index 00000000000..9cd59c18f67 --- /dev/null +++ b/admin/tool/log/store/standard/lang/en/logstore_standard.php @@ -0,0 +1,27 @@ +. + +/** + * 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['pluginname'] = 'Standard log'; +$string['pluginname_desc'] = 'Standard log plugin, the data is stored in Moodle database table.'; +$string['standard:read'] = 'Read logs'; diff --git a/admin/tool/log/store/standard/settings.php b/admin/tool/log/store/standard/settings.php new file mode 100644 index 00000000000..fae162cd5cd --- /dev/null +++ b/admin/tool/log/store/standard/settings.php @@ -0,0 +1,32 @@ +. + +/** + * 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) { + + // TODO: Localise these settings. + + $settings->add(new admin_setting_configcheckbox('logstore_standard/logguests', 'Log guest actions', '', '1')); +} diff --git a/admin/tool/log/store/standard/version.php b/admin/tool/log/store/standard/version.php new file mode 100644 index 00000000000..9ef5f966600 --- /dev/null +++ b/admin/tool/log/store/standard/version.php @@ -0,0 +1,29 @@ +. + +/** + * 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 = 2014011300; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014011000; // Requires this Moodle version. +$plugin->component = 'logstore_standard'; // Full name of the plugin (used for diagnostics). diff --git a/admin/tool/log/stores.php b/admin/tool/log/stores.php new file mode 100644 index 00000000000..9f24cd49b81 --- /dev/null +++ b/admin/tool/log/stores.php @@ -0,0 +1,98 @@ +. + +/** + * 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); +$enrol = required_param('store', PARAM_PLUGIN); + +$PAGE->set_url('/admin/tool/log/stores.php'); +$PAGE->set_context(context_system::instance()); + +require_login(); +require_capability('moodle/site:config', context_system::instance()); +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': + unset($enabled[$enrol]); + set_config('enabled_stores', implode(',', array_keys($enabled)), 'tool_log'); + break; + + case 'enable': + if (!isset($all[$enrol])) { + break; + } + $enabled = array_keys($enabled); + $enabled[] = $enrol; + set_config('enabled_stores', implode(',', $enabled), 'tool_log'); + break; + + case 'up': + if (!isset($enabled[$enrol])) { + break; + } + $enabled = array_keys($enabled); + $enabled = array_flip($enabled); + $current = $enabled[$enrol]; + if ($current == 0) { + break; // Already at the top. + } + $enabled = array_flip($enabled); + $enabled[$current] = $enabled[$current - 1]; + $enabled[$current - 1] = $enrol; + set_config('enabled_stores', implode(',', $enabled), 'tool_log'); + break; + + case 'down': + if (!isset($enabled[$enrol])) { + break; + } + $enabled = array_keys($enabled); + $enabled = array_flip($enabled); + $current = $enabled[$enrol]; + if ($current == count($enabled) - 1) { + break; // Already at the end. + } + $enabled = array_flip($enabled); + $enabled[$current] = $enabled[$current + 1]; + $enabled[$current + 1] = $enrol; + set_config('enabled_stores', implode(',', $enabled), 'tool_log'); + break; +} + +redirect($return); diff --git a/admin/tool/log/version.php b/admin/tool/log/version.php new file mode 100644 index 00000000000..d72972b86bc --- /dev/null +++ b/admin/tool/log/version.php @@ -0,0 +1,29 @@ +. + +/** + * 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 = 2014011300; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014011000; // Requires this Moodle version. +$plugin->component = 'tool_log'; // Full name of the plugin (used for diagnostics). diff --git a/lang/en/admin.php b/lang/en/admin.php index 2db067d8918..4dda3090b78 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1007,7 +1007,6 @@ $string['taskcontextcleanup'] = 'Cleanup contexts'; $string['taskcreatecontexts'] = 'Create missing contexts'; $string['taskdeletecachetext'] = 'Delete old text cache records'; $string['taskdeleteincompleteusers'] = 'Delete incomplete users'; -$string['taskdeletelogs'] = 'Delete logs'; $string['taskdeleteunconfirmedusers'] = 'Delete unconfirmed users'; $string['taskeventscron'] = 'Background processing for events'; $string['taskfiletrashcleanup'] = 'Cleanup files in trash'; diff --git a/lib/classes/event/base.php b/lib/classes/event/base.php index 38a999e53ea..4795ffa2cee 100644 --- a/lib/classes/event/base.php +++ b/lib/classes/event/base.php @@ -347,6 +347,83 @@ abstract class base implements \IteratorAggregate { return $event; } + /** + * Create fake event from legacy log data. + * + * @param stdClass $legacy + * @return base + */ + public static final function restore_legacy($legacy) { + $classname = get_called_class(); + $event = new $classname(); + $event->restored = true; + $event->triggered = true; + $event->dispatched = true; + + $context = false; + $component = 'legacy'; + if ($legacy->cmid) { + $context = \context_module::instance($legacy->cmid, IGNORE_MISSING); + $component = 'mod_'.$legacy->module; + } else if ($legacy->course) { + $context = \context_course::instance($legacy->course, IGNORE_MISSING); + } + if (!$context) { + $context = \context_system::instance(); + } + + $event->data = array(); + + $event->data['eventname'] = $legacy->module.'_'.$legacy->action; + $event->data['component'] = $component; + $event->data['action'] = $legacy->action; + $event->data['target'] = null; + $event->data['objecttable'] = null; + $event->data['objectid'] = null; + if (strpos($legacy->action, 'view') !== false) { + $event->data['crud'] = 'r'; + } else if (strpos($legacy->action, 'print') !== false) { + $event->data['crud'] = 'r'; + } else if (strpos($legacy->action, 'update') !== false) { + $event->data['crud'] = 'u'; + } else if (strpos($legacy->action, 'hide') !== false) { + $event->data['crud'] = 'u'; + } else if (strpos($legacy->action, 'move') !== false) { + $event->data['crud'] = 'u'; + } else if (strpos($legacy->action, 'write') !== false) { + $event->data['crud'] = 'u'; + } else if (strpos($legacy->action, 'tag') !== false) { + $event->data['crud'] = 'u'; + } else if (strpos($legacy->action, 'remove') !== false) { + $event->data['crud'] = 'u'; + } else if (strpos($legacy->action, 'delete') !== false) { + $event->data['crud'] = 'p'; + } else if (strpos($legacy->action, 'create') !== false) { + $event->data['crud'] = 'c'; + } else if (strpos($legacy->action, 'post') !== false) { + $event->data['crud'] = 'c'; + } else if (strpos($legacy->action, 'add') !== false) { + $event->data['crud'] = 'c'; + } else { + // End of guessing... + $event->data['crud'] = 'r'; + } + $event->data['edulevel'] = $event::LEVEL_OTHER; + $event->data['contextid'] = $context->id; + $event->data['contextlevel'] = $context->contextlevel; + $event->data['contextinstanceid'] = $context->instanceid; + $event->data['userid'] = ($legacy->userid ? $legacy->userid : null); + $event->data['courseid'] = ($legacy->course ? $legacy->course : null); + $event->data['relateduserid'] = ($legacy->userid ? $legacy->userid : null); + $event->data['timecreated'] = $legacy->time; + + $event->logextra = array('origin'=>$legacy->ip, 'realuserid'=>null); + + $event->data['other'] = (array)$legacy; + + return $event; + } + /** * Returns event context. * @return \context @@ -504,7 +581,10 @@ abstract class base implements \IteratorAggregate { if (isset($CFG->loglifetime) and $CFG->loglifetime != -1) { if ($data = $this->get_legacy_logdata()) { - call_user_func_array('add_to_log', $data); + $manager = get_log_manager(); + if (method_exists($manager, 'legacy_add_to_log')) { + call_user_func_array(array($manager, 'legacy_add_to_log'), $data); + } } } diff --git a/lib/classes/log/dummy_manager.php b/lib/classes/log/dummy_manager.php new file mode 100644 index 00000000000..6ca9ecf4b50 --- /dev/null +++ b/lib/classes/log/dummy_manager.php @@ -0,0 +1,36 @@ +. + +/** + * Dummy storage manager, returns nothing. + * used when no other manager available. + * + * @package core + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\log; + +defined('MOODLE_INTERNAL') || die(); + +class dummy_manager implements manager { + public function get_readers(\context $context) { + return array(); + } + public function dispose() { + } +} diff --git a/lib/classes/task/delete_logs_task.php b/lib/classes/log/manager.php similarity index 50% rename from lib/classes/task/delete_logs_task.php rename to lib/classes/log/manager.php index d25104eace6..1c40f92981d 100644 --- a/lib/classes/task/delete_logs_task.php +++ b/lib/classes/log/manager.php @@ -15,44 +15,38 @@ // along with Moodle. If not, see . /** - * A scheduled task. + * Log storage manager interface. * * @package core - * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace core\task; + +namespace core\log; + +defined('MOODLE_INTERNAL') || die(); /** - * Simple task to delete old log records. + * Interface describing log readers. + * + * This is intended for reports, use get_log_manager() to get + * the configured instance. + * + * @package core\log */ -class delete_logs_task extends scheduled_task { - +interface manager { /** - * Get a descriptive name for this task (shown to admins). + * Return list of available log readers in given + * context for current user. * - * @return string + * @param \context $context + * @return \core\log\reader[] */ - public function get_name() { - return get_string('taskdeletelogs', 'admin'); - } + public function get_readers(\context $context); /** - * Do the job. - * Throw exceptions on errors (the job will be retried). + * Dispose all initialised stores. + * @return void */ - public function execute() { - global $CFG, $DB; - - $timenow = time(); - - // Delete old logs to save space. - // Value in days. - if (!empty($CFG->loglifetime)) { - $loglifetime = $timenow - ($CFG->loglifetime * 3600 * 24); - $DB->delete_records_select("log", "time < ?", array($loglifetime)); - } - - } - + public function dispose(); } diff --git a/lib/classes/log/reader.php b/lib/classes/log/reader.php new file mode 100644 index 00000000000..84782259178 --- /dev/null +++ b/lib/classes/log/reader.php @@ -0,0 +1,72 @@ +. + +/** + * Log storage reader interface. + * + * @package core + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\log; + +defined('MOODLE_INTERNAL') || die(); + +interface reader { + /** + * Localised name of the reader. + * + * To be used in selection for in reports. + * + * @return string + */ + public function get_name(); + + /** + * Longer description of the log data source. + * @return string + */ + public function get_description(); + + /** + * Can the current user access this store? + * @param \context $context + * @return bool + */ + public function can_access(\context $context); + + /** + * Fetch records using given criteria. + * + * @param string $selectwhere + * @param array $params + * @param string $sort + * @param int $limitfrom + * @param int $limitnum + * @return \core\event\base[] + */ + public function get_events($selectwhere, array $params, $sort, $limitfrom, $limitnum); + + /** + * Return number of events matching given criteria. + * + * @param string $selectwhere + * @param array $params + * @return int + */ + public function get_events_count($selectwhere, array $params); +} diff --git a/lib/classes/log/sql_reader.php b/lib/classes/log/sql_reader.php new file mode 100644 index 00000000000..d2c5bafc149 --- /dev/null +++ b/lib/classes/log/sql_reader.php @@ -0,0 +1,43 @@ +. + +/** + * Log storage sql reader interface. + * + * @package core + * @copyright 2013 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\log; + +defined('MOODLE_INTERNAL') || die(); + +interface sql_reader extends reader { + + /** + * Returns name of the table or database view that + * holds the log data in standardised format. + * + * Note: this table must be used for reading only, + * it is strongly recommended to use this in complex reports only. + * + * TODO: define the standard log columns - watch out for "level" reserved word! + * + * @return string + */ + public function get_log_table(); +} diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 167c5444396..802642c85d2 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1028,6 +1028,10 @@ class core_plugin_manager { 'local' => array( ), + 'logstore' => array( + 'database', 'legacy', 'standard', + ), + 'message' => array( 'email', 'jabber', 'popup' ), @@ -1117,7 +1121,7 @@ class core_plugin_manager { 'tool' => array( 'assignmentupgrade', 'behat', 'capability', 'customlang', 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon', - 'langimport', 'multilangupgrade', 'phpunit', 'profiling', + 'langimport', 'log', 'multilangupgrade', 'phpunit', 'profiling', 'qeupgradehelper', 'replace', 'spamcleaner', 'task', 'timezoneimport', 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb' ), diff --git a/lib/datalib.php b/lib/datalib.php index a9a44da8083..b3e27769e1e 100644 --- a/lib/datalib.php +++ b/lib/datalib.php @@ -1583,6 +1583,41 @@ function coursemodule_visible_for_user($cm, $userid=0) { /// LOG FUNCTIONS ///////////////////////////////////////////////////// +/** + * Get instance of log manager. + * + * @param bool $forcereload + * @return \core\log\manager + */ +function get_log_manager($forcereload = false) { + /** @var \core\log\manager $singleton */ + static $singleton = null; + + if ($forcereload and isset($singleton)) { + $singleton->dispose(); + $singleton = null; + } + + if (isset($singleton)) { + return $singleton; + } + + $classname = '\tool_log\log\manager'; + if (defined('LOG_MANAGER_CLASS')) { + $classname = LOG_MANAGER_CLASS; + } + + if (!class_exists($classname)) { + if (!empty($classname)) { + debugging("Cannot find log manager class '$classname'.", DEBUG_DEVELOPER); + } + $classname = '\core\log\dummy_manager'; + } + + $singleton = new $classname(); + return $singleton; +} + /** * Add an entry to the config log table. * @@ -1613,109 +1648,6 @@ function add_to_config_log($name, $oldvalue, $value, $plugin) { $DB->insert_record('config_log', $log); } -/** - * Add an entry to the log table. - * - * Add an entry to the log table. These are "action" focussed rather - * than web server hits, and provide a way to easily reconstruct what - * any particular student has been doing. - * - * @package core - * @category log - * @global moodle_database $DB - * @global stdClass $CFG - * @global stdClass $USER - * @uses SITEID - * @uses DEBUG_DEVELOPER - * @uses DEBUG_ALL - * @param int $courseid The course id - * @param string $module The module name e.g. forum, journal, resource, course, user etc - * @param string $action 'view', 'update', 'add' or 'delete', possibly followed by another word to clarify. - * @param string $url The file and parameters used to see the results of the action - * @param string $info Additional description information - * @param string $cm The course_module->id if there is one - * @param string $user If log regards $user other than $USER - * @return void - */ -function add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user=0) { - // Note that this function intentionally does not follow the normal Moodle DB access idioms. - // This is for a good reason: it is the most frequently used DB update function, - // so it has been optimised for speed. - global $DB, $CFG, $USER; - - if ($cm === '' || is_null($cm)) { // postgres won't translate empty string to its default - $cm = 0; - } - - if ($user) { - $userid = $user; - } else { - if (\core\session\manager::is_loggedinas()) { // Don't log - return; - } - $userid = empty($USER->id) ? '0' : $USER->id; - } - - if (isset($CFG->logguests) and !$CFG->logguests) { - if (!$userid or isguestuser($userid)) { - return; - } - } - - $REMOTE_ADDR = getremoteaddr(); - - $timenow = time(); - $info = $info; - if (!empty($url)) { // could break doing html_entity_decode on an empty var. - $url = html_entity_decode($url, ENT_QUOTES, 'UTF-8'); - } else { - $url = ''; - } - - // Restrict length of log lines to the space actually available in the - // database so that it doesn't cause a DB error. Log a warning so that - // developers can avoid doing things which are likely to cause this on a - // routine basis. - if(!empty($info) && core_text::strlen($info)>255) { - $info = core_text::substr($info,0,252).'...'; - debugging('Warning: logged very long info',DEBUG_DEVELOPER); - } - - // If the 100 field size is changed, also need to alter print_log in course/lib.php - if(!empty($url) && core_text::strlen($url)>100) { - $url = core_text::substr($url,0,97).'...'; - debugging('Warning: logged very long URL',DEBUG_DEVELOPER); - } - - if (defined('MDL_PERFDB')) { global $PERF ; $PERF->logwrites++;}; - - $log = array('time'=>$timenow, 'userid'=>$userid, 'course'=>$courseid, 'ip'=>$REMOTE_ADDR, 'module'=>$module, - 'cmid'=>$cm, 'action'=>$action, 'url'=>$url, 'info'=>$info); - - try { - $DB->insert_record_raw('log', $log, false); - } catch (dml_exception $e) { - debugging('Error: Could not insert a new entry to the Moodle log. '. $e->error, DEBUG_ALL); - - // MDL-11893, alert $CFG->supportemail if insert into log failed - if ($CFG->supportemail and empty($CFG->noemailever)) { - // email_to_user is not usable because email_to_user tries to write to the logs table, - // and this will get caught in an infinite loop, if disk is full - $site = get_site(); - $subject = 'Insert into log failed at your moodle site '.$site->fullname; - $message = "Insert into log table failed at ". date('l dS \of F Y h:i:s A') .".\n It is possible that your disk is full.\n\n"; - $message .= "The failed query parameters are:\n\n" . var_export($log, true); - - $lasttime = get_config('admin', 'lastloginserterrormail'); - if(empty($lasttime) || time() - $lasttime > 60*60*24) { // limit to 1 email per day - //using email directly rather than messaging as they may not be able to log in to access a message - mail($CFG->supportemail, $subject, $message); - set_config('lastloginserterrormail', time(), 'admin'); - } - } - } -} - /** * Store user last access times - called when use enters a course or site * diff --git a/lib/db/tasks.php b/lib/db/tasks.php index 028a8e2354b..3526517ce10 100644 --- a/lib/db/tasks.php +++ b/lib/db/tasks.php @@ -59,15 +59,6 @@ $tasks = array( 'dayofweek' => '*', 'month' => '*' ), - array( - 'classname' => 'core\task\delete_logs_task', - 'blocking' => 0, - 'minute' => '0', - 'hour' => '2', - 'day' => '*', - 'dayofweek' => '*', - 'month' => '*' - ), array( 'classname' => 'core\task\backup_cleanup_task', 'blocking' => 0, diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index 7368dc840ca..8a4916b17a0 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -30,6 +30,36 @@ defined('MOODLE_INTERNAL') || die(); +/** + * Add an entry to the log table. + * + * Add an entry to the log table. These are "action" focussed rather + * than web server hits, and provide a way to easily reconstruct what + * any particular student has been doing. + * + * @deprecated since 2.7 use new events instead + * + * @param int $courseid The course id + * @param string $module The module name e.g. forum, journal, resource, course, user etc + * @param string $action 'view', 'update', 'add' or 'delete', possibly followed by another word to clarify. + * @param string $url The file and parameters used to see the results of the action + * @param string $info Additional description information + * @param int $cm The course_module->id if there is one + * @param int|stdClass $user If log regards $user other than $USER + * @return void + */ +function add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user=0) { + // TODO: Uncomment after all add_to_log() are removed from standard distribution - ideally before 2.7 release. + //debugging('ideally all add_to_log() calls should be replaced() with new events', DEBUG_DEVELOPER); + + // This is a nasty hack that allows us to put all the legacy stuff into legacy storage, + // this way we may move all the legacy settings there too. + $manager = get_log_manager(); + if (method_exists($manager, 'legacy_add_to_log')) { + $manager->legacy_add_to_log($courseid, $module, $action, $url, $info, $cm, $user); + } +} + /** * Adds a file upload to the log table so that clam can resolve the filename to the user later if necessary * @@ -4318,17 +4348,3 @@ function can_use_html_editor() { debugging('can_use_html_editor has been deprecated please update your code to assume it returns true.', DEBUG_DEVELOPER); return true; } - -/** - * Returns whether ajax is enabled/allowed or not. - * This function is deprecated and always returns true. - * - * @param array $unused - not used any more. - * @return bool - * @deprecated since 2.7 MDL-33099 - please do not use this function any more. - * @todo MDL-44088 This will be removed in Moodle 2.9. - */ -function ajaxenabled(array $browsers = null) { - debugging('ajaxenabled() is deprecated - please update your code to assume it returns true.', DEBUG_DEVELOPER); - return true; -} diff --git a/lib/tests/event_test.php b/lib/tests/event_test.php index 713fc14b649..2574f293311 100644 --- a/lib/tests/event_test.php +++ b/lib/tests/event_test.php @@ -566,6 +566,8 @@ class core_event_testcase extends advanced_testcase { } public function test_trigger_problems() { + $this->resetAfterTest(true); + $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>5, 'xx'=>10))); $event->trigger(); try { @@ -597,6 +599,8 @@ class core_event_testcase extends advanced_testcase { } public function test_bad_events() { + $this->resetAfterTest(true); + try { $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'other'=>array('sample'=>5, 'xx'=>10))); $this->fail('Exception expected when context and contextid missing'); @@ -654,7 +658,8 @@ class core_event_testcase extends advanced_testcase { } public function test_problematic_events() { - global $CFG; + $this->resetAfterTest(true); + $event1 = \core_tests\event\problematic_event1::create(array('context'=>\context_system::instance())); $this->assertDebuggingNotCalled(); $this->assertNull($event1->xxx); @@ -703,6 +708,8 @@ class core_event_testcase extends advanced_testcase { public function test_record_snapshots() { global $DB; + $this->resetAfterTest(true); + $event = \core_tests\event\unittest_executed::create(array('courseid'=>1, 'context'=>\context_system::instance(), 'other'=>array('sample'=>1, 'xx'=>10))); $course1 = $DB->get_record('course', array('id'=>1)); $this->assertNotEmpty($course1);