From d6b75235c3b11aeea88fcf2c38de3d831bd5a095 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 10 Sep 2021 15:35:40 +0800 Subject: [PATCH 1/1] ACADEMY-73: Proof of concept --- dmlhack.php | 240 +++++++++++++++++++++++ lib/dml/pgsql_native_moodle_database.php | 17 ++ 2 files changed, 257 insertions(+) create mode 100644 dmlhack.php diff --git a/dmlhack.php b/dmlhack.php new file mode 100644 index 0000000000..5733a42bb7 --- /dev/null +++ b/dmlhack.php @@ -0,0 +1,240 @@ +dml_pre_update_handler = MoodleHQ\Hacks\DmlCallbacks::class; + * + * + * Tested with postgres. Will need modification to other drivers and possibly other functions for some drivers. + * + * Only detects updates, _not_ inserts. + */ + +namespace MoodleHQ\Hacks; + +class DmlCallbacks { + /** + * @var A list of tables to field mappings. + * + * TODO: This list is not complete. It currently contains course, blocks, and activity modules. + * + * It is missing individual tables in plugins, areas like question data, and a range of other places. + */ + protected static $tablelist = [ + 'course' => [ + // If the field is plain and does not support format, the mapping is to null. + 'fullname' => null, + 'shortname' => null, + + // If the field is a text field and can have a format set, the mapping is set to the name of the format field. + 'summary' => 'summaryformat', + ], + + 'block' => [ + 'name' => null, + ], + 'assign' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'assignment' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'book' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'chat' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'choice' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'data' => [ + 'name' => null, + 'intro' => 'introformat', + 'listtemplate' => null, + 'listtemplateheader' => null, + 'listtemplatefooter' => null, + 'addtemplate' => null, + 'rsstemplate' => null, + 'csstemplate' => null, + 'jstemplate' => null, + 'asearchtemplate' => null, + ], + 'feedback' => [ + 'name' => null, + 'intro' => 'introformat', + 'pageaftersubmit' => null, + ], + 'folder' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'forum' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'glossary' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'h5pactivity' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'imscp' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'label' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'lesson' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'lti' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'page' => [ + 'name' => null, + 'intro' => 'introformat', + 'page' => 'pageformat', + ], + 'quiz' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'resource' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'scorm' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'survey' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'url' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'wiki' => [ + 'name' => null, + 'intro' => 'introformat', + ], + 'workshop' => [ + 'name' => null, + 'intro' => 'introformat', + 'instructauthors' => 'instructauthorsformat', + 'instructreviewers' => 'instructreviewersformat', + 'conclusion' => 'conclusionformat', + ], + + 'question' => [ + 'name' => null, + 'questiontext' => 'questiontextformat', + 'generalfeedback' => 'generalfeedbackformat', + ], + + ]; + + protected static function is_table_watched(string $tablename): bool { + return array_key_exists($tablename, self::$tablelist); + } + + protected static function does_any_field_match(string $tablename, array $fields): bool { + $watchedfields = self::$tablelist[$tablename]; + + foreach (array_keys($watchedfields) as $fieldname) { + if (array_key_exists($fieldname, $fields)) { + return true; + } + } + + return false; + } + + public static function update_record(string $tablename, int $id, ?array $params): void { + global $DB; + + // Filter the table. + if (!self::is_table_watched($tablename)) { + return; + } + + $intersect = array_intersect_key(self::$tablelist[$tablename], $params); + if (empty($intersect)) { + return; + } + + $fetchfields = array_filter(array_merge(array_keys($intersect), array_values($intersect))); + $currentdata = $DB->get_record($tablename, ['id' => $id], implode(',', $fetchfields)); + + foreach ($currentdata as $fieldname => $value) { + $value = trim($value); + $newvalue = trim($params[$fieldname]); + + if ($value !== $newvalue) { + $oldsum = sha1($value); + $newsum = sha1($newvalue); + self::store_updated_hash($tablename, $id, $fieldname, $newvalue, $newsum, $oldsum); + } + } + } + + public static function set_field_select(string $tablename, string $fieldname, $newvalue, string $select, ?array $params = null): void { + global $DB; + + // Filter the table. + if (!self::is_table_watched($tablename)) { + return; + } + + if (!array_key_exists($fieldname, self::$tablelist[$tablename])) { + return; + } + + if ($select) { + $select = substr($select, strlen('SELECT')); + } + + $fields = ['id', $fieldname]; + if ($format = self::$tablelist[$tablename][$fieldname]) { + $fields[] = $format; + } + $currentdata = $DB->get_records_select($tablename, $select, $params, '', implode(',', $fields)); + + foreach ($currentdata as $row) { + $oldvalue = trim($row->{$fieldname}); + $newvalue = trim($newvalue); + + // TODO Also check the format of the field and decide how to handle that. + if ($oldvalue !== $newvalue) { + $oldsum = sha1($oldvalue); + $newsum = sha1($newvalue); + + self::store_updated_hash($tablename, $row->id, $fieldname, $newvalue, $newsum, $oldsum); + } + } + } + + // TODO Fetch contentformat. + protected static function store_updated_hash(string $tablename, string $id, string $fieldname, string $content, string $contenthash, string $oldhash, ?int $contentformat = null): void { + // TODO This is where you would insert the new language string data. + // You may want to store tablename, id, contenthash, oldsum, content, contentformat + // You may want to store the content and contentformat in a separate table, alongside the contenthash. + error_log("!! CHANGE detected for id {$id} in {$tablename}:{$fieldname} from {$oldhash} to {$contenthash}"); + } +} diff --git a/lib/dml/pgsql_native_moodle_database.php b/lib/dml/pgsql_native_moodle_database.php index 93903984bc..2f774134de 100644 --- a/lib/dml/pgsql_native_moodle_database.php +++ b/lib/dml/pgsql_native_moodle_database.php @@ -1261,6 +1261,8 @@ class pgsql_native_moodle_database extends moodle_database { * @throws dml_exception A DML specific exception is thrown for any errors. */ public function update_record_raw($table, $params, $bulk=false) { + global $CFG; + $params = (array)$params; if (!isset($params['id'])) { @@ -1286,6 +1288,13 @@ class pgsql_native_moodle_database extends moodle_database { $sets = implode(',', $sets); $sql = "UPDATE {$this->prefix}$table SET $sets WHERE id=\$".$i; + if (!empty($CFG->dml_pre_update_handler)) { + $callback = [$CFG->dml_pre_update_handler, 'update_record']; + if (is_callable($callback)) { + call_user_func($callback, $table, $id, $params); + } + } + $this->query_start($sql, $params, SQL_QUERY_UPDATE); $result = pg_query_params($this->pgsql, $sql, $params); $this->query_end($result); @@ -1354,6 +1363,14 @@ class pgsql_native_moodle_database extends moodle_database { $normalisedvalue = $this->normalise_value($column, $newvalue); + global $CFG; + if (!empty($CFG->dml_pre_update_handler)) { + $callback = [$CFG->dml_pre_update_handler, 'set_field_select']; + if (is_callable($callback)) { + call_user_func($callback, $table, $newfield, $normalisedvalue, $select, $params); + } + } + $newfield = "$newfield = \$" . $i; $params[] = $normalisedvalue; $sql = "UPDATE {$this->prefix}$table SET $newfield $select"; -- 2.30.0