Skip to content

Commit

Permalink
Add ability to import and export profiles #362
Browse files Browse the repository at this point in the history
  • Loading branch information
bwalkerl committed Sep 16, 2024
1 parent 7eb18e4 commit 2982f61
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 8 deletions.
25 changes: 25 additions & 0 deletions classes/flamed3_node.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ public static function from_excimer_log_entries(iterable $entries): flamed3_node
return $root;
}

/**
* Loads data from an import. This will generally be a stdClass that has been loaded from json.
*
* @param \stdClass $data loaded from json
* @return flamed3_node|null flamed3_node if valid, null if any part of the import is invalid
*/
public static function from_import(\stdClass $data): ?flamed3_node {
// Validate whether the stdClass is a flamed3_node.
if (!isset($data->name) || !isset($data->value) || !isset($data->children)) {
return null;
}

// Map children to flamenode instances.
$children = [];
foreach ($data->children as $child) {
$node = self::from_import($child);
if (empty($node)) {
return null;
}
$children[] = $node;
}

return new flamed3_node($data->name, $data->value, $children);
}

/**
* Returns a name to represent the call in the trace node.
*
Expand Down
106 changes: 106 additions & 0 deletions classes/form/import_form.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?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_excimer\form;

defined('MOODLE_INTERNAL') || die();

require_once($CFG->libdir.'/formslib.php');

/**
* Import excimer form class.
*
* @package tool_excimer
* @author Benjamin Walker <benjaminwalker@catalyst-au.net>
* @copyright 2024, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class import_form extends \moodleform {

/**
* Build form for importing woekflows.
*
* {@inheritDoc}
* @see \moodleform::definition()
*/
public function definition() {
global $CFG;

$mform = $this->_form;

// Profile file.
$mform->addElement(
'filepicker',
'userfile',
get_string('profile_file', 'tool_excimer'),
null,
['maxbytes' => $CFG->maxbytes, 'accepted_types' => ['.json']]
);
$mform->addRule('userfile', get_string('required'), 'required');

$this->add_action_buttons();
}

/**
* Validate uploaded json file.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
global $USER;

$validationerrors = [];

// Get the file from the filestystem. $files will always be empty.
$fs = get_file_storage();

$context = \context_user::instance($USER->id);
$itemid = $data['userfile'];

// This is how core gets files in this case.
if (!$files = $fs->get_area_files($context->id, 'user', 'draft', $itemid, 'id DESC', false)) {
$validationerrors['nofile'] = get_string('no_profile_file', 'tool_excimer');
return $validationerrors;
}
$file = reset($files);

// Check if file is valid json.
$content = $file->get_content();
if (!empty($content)) {
json_decode($content);
if (json_last_error() !== JSON_ERROR_NONE) {
$validationerrors['userfile'] = json_last_error_msg();
}
}

return $validationerrors;
}

/**
* Get the errors returned during form validation.
*
* @return array|mixed
*/
public function get_errors() {
$form = $this->_form;
$errors = $form->_errors;

return $errors;
}
}
3 changes: 3 additions & 0 deletions classes/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public static function reason_display(int $reason): string {
if ($reason & profile::REASON_STACK) {
$reasonsmatched[] = get_string('reason_stack', 'tool_excimer');
}
if ($reason & profile::REASON_IMPORT) {
$reasonsmatched[] = get_string('reason_import', 'tool_excimer');
}
return implode(',', $reasonsmatched);
}

Expand Down
84 changes: 79 additions & 5 deletions classes/profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ class profile extends persistent {
/** Reason - STACK - Set when maxstackdepth exceeds a predefined limit. */
const REASON_STACK = 0b1000;

/** Reason - IMPORTED - Set when a profile is imported. */
const REASON_IMPORT = 0b010000;

/** Reasons for profiling (bitmask flags). NOTE: Excluding the NONE option intentionally. */
const REASONS = [
self::REASON_FLAMEME,
self::REASON_SLOW,
self::REASON_FLAMEALL,
self::REASON_STACK,
self::REASON_IMPORT,
];

/** String map for profiling reasons. */
Expand All @@ -62,6 +65,7 @@ class profile extends persistent {
self::REASON_SLOW => 'slowest',
self::REASON_FLAMEALL => 'flameall',
self::REASON_STACK => 'stackdepth',
self::REASON_IMPORT => 'import',
];

/** Ajax scripts */
Expand Down Expand Up @@ -89,9 +93,9 @@ protected function set_flamedatad3(flamed3_node $node): void {
/**
* Custom getter for flame data.
*
* @return string
* @return array|\stdClass|null json decoded value
*/
protected function get_flamedatad3(): string {
protected function get_flamedatad3() {
return json_decode($this->get_flamedatad3json());
}

Expand All @@ -101,7 +105,7 @@ protected function get_flamedatad3(): string {
* @return string
*/
public function get_flamedatad3json(): string {
return gzuncompress($this->raw_get('flamedatad3'));
return $this->get_uncompressed_json('flamedatad3');
}

/**
Expand All @@ -117,9 +121,9 @@ protected function set_memoryusagedatad3(array $node): void {
/**
* Custom getter for memory usage data.
*
* @return string
* @return array|\stdClass|null json decoded value
*/
protected function get_memoryusagedatad3(): string {
protected function get_memoryusagedatad3() {
return json_decode($this->get_uncompressed_json('memoryusagedatad3'));
}

Expand All @@ -137,6 +141,76 @@ public function get_uncompressed_json(string $fieldname): string {
return json_encode(null);
}

/**
* Gets the JSON for an export.
*
* @param mixed $removesitedata whether to remove site specific data
* @return string JSON
*/
protected function get_export_json($removesitedata = true): string {
$data = new \stdClass();

// Remove unneeded properties from the export.
$allproperties = array_keys(static::properties_definition());
$removeproperties = ['id'];
if ($removesitedata) {
// Also remove data that is site specific or contains full urls.
$removeproperties += ['referer', 'userid', 'courseid', 'sessionid', 'lockreason', 'lockwaiturl'];
}
$properties = array_diff($allproperties, $removeproperties);

// Load data the same way as to_record(), but use get() to transform d3 data.
foreach ($properties as $property) {
$data->$property = $this->get($property);
}

return json_encode($data);
}

/**
* Download the profile in JSON format.
*/
public function download(): void {
$id = $this->raw_get('id');
$filename = "excimer-profile-$id.json";

header('Content-Type: application/json; charset: utf-8');
header("Content-Disposition: attachment; filename=\"$filename\"");

echo $this->get_export_json();
exit();
}

/**
* Imports a prfile from JSON format.
*
* @param string $json
* @return bool|int the id of the imported profile, or false if unsuccessful
*/
public static function import(string $json) {
global $DB;

$profile = new profile();
$data = json_decode($json);

// Don't mark this as slow on external sites.
$data->reason = self::REASON_IMPORT;

// Convert flamedatad3 to flame_node.
if (isset($data->flamedatad3)) {
$data->flamedatad3 = flamed3_node::from_import($data->flamedatad3);
}

foreach ($data as $property => $value) {
if (isset($value)) {
$profile->set($property, $value);
}
}

// The normal profile->save() doesn't work with flamedatad3 and memoryuseagedatad3.
return $DB->insert_record(self::TABLE, $profile->to_record());
}

/**
* Convenience method to add environment data to the profile.
*
Expand Down
40 changes: 40 additions & 0 deletions export.php
Original file line number Diff line number Diff line change
@@ -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/>.

/**
* Export profiles.
*
* @package tool_excimer
* @author Benjamin Walker <benjaminwalker@catalyst-au.net>
* @copyright 2024, Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

use tool_excimer\profile;

require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');

require_login();
$context = context_system::instance();

// Check for caps.
require_capability('moodle/site:config', context_system::instance());

$exportid = required_param('exportid', PARAM_INT);

$profile = new profile($exportid);
$profile->download();
Loading

0 comments on commit 2982f61

Please sign in to comment.