MDL-57898 core_customfield: Custom fields API
authorDavid Matamoros <davidmc@moodle.com>
Fri, 11 Jan 2019 11:12:18 +0000 (12:12 +0100)
committerMarina Glancy <marina@moodle.com>
Fri, 18 Jan 2019 13:28:10 +0000 (14:28 +0100)
This commit is part of work on Custom fields API,
to minimize commit history in moodle core the work of a team of developers was split
into several commits with different authors but the authorship of individual
lines of code may be different from the commit author.

15 files changed:
customfield/classes/api.php [new file with mode: 0644]
customfield/classes/field_config_form.php [new file with mode: 0644]
customfield/classes/handler.php [new file with mode: 0644]
customfield/classes/output/management.php [new file with mode: 0644]
customfield/classes/output/renderer.php [new file with mode: 0644]
customfield/classes/privacy/provider.php [new file with mode: 0644]
customfield/edit.php [new file with mode: 0644]
customfield/externallib.php [new file with mode: 0644]
customfield/field/checkbox/classes/field_controller.php [new file with mode: 0644]
customfield/field/date/classes/field_controller.php [new file with mode: 0644]
customfield/field/date/lang/en/customfield_date.php [new file with mode: 0644]
customfield/field/select/classes/field_controller.php [new file with mode: 0644]
customfield/field/text/classes/field_controller.php [new file with mode: 0644]
customfield/field/textarea/classes/field_controller.php [new file with mode: 0644]
customfield/field/textarea/version.php [new file with mode: 0644]

diff --git a/customfield/classes/api.php b/customfield/classes/api.php
new file mode 100644 (file)
index 0000000..52ddc64
--- /dev/null
@@ -0,0 +1,415 @@
+<?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/>.
+
+/**
+ * Api customfield package
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_customfield;
+
+use core\output\inplace_editable;
+use core_customfield\event\category_created;
+use core_customfield\event\category_deleted;
+use core_customfield\event\category_updated;
+use core_customfield\event\field_created;
+use core_customfield\event\field_deleted;
+use core_customfield\event\field_updated;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class api
+ *
+ * @package core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class api {
+
+    /**
+     * For the given instance and list of fields fields retrieves data associated with them
+     *
+     * @param field_controller[] $fields list of fields indexed by field id
+     * @param int $instanceid
+     * @param bool $adddefaults
+     * @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
+     *    some data_controller objects may have 'id', some not
+     *     If ($adddefaults): All fieldids are present, some data_controller objects may have 'id', some not.
+     *     If (!$adddefaults): Only fieldids with data are present, all data_controller objects have 'id'.
+     */
+    public static function get_instance_fields_data(array $fields, int $instanceid, bool $adddefaults = true): array {
+        return self::get_instances_fields_data($fields, [$instanceid], $adddefaults)[$instanceid];
+    }
+
+    /**
+     * For given list of instances and fields retrieves data associated with them
+     *
+     * @param field_controller[] $fields list of fields indexed by field id
+     * @param int[] $instanceids
+     * @param bool $adddefaults
+     * @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
+     *     If ($adddefaults): All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
+     *     If (!$adddefaults): All instanceids are present but only fieldids with data are present, all
+     *         data_controller objects have 'id'.
+     */
+    public static function get_instances_fields_data(array $fields, array $instanceids, bool $adddefaults = true): array {
+        global $DB;
+
+        // Create the results array where instances and fields order is the same as in the input arrays.
+        $result = array_fill_keys($instanceids, array_fill_keys(array_keys($fields), null));
+
+        if (empty($instanceids) || empty($fields)) {
+            return $result;
+        }
+
+        // Retrieve all existing data.
+        list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($fields), SQL_PARAMS_NAMED, 'fld');
+        list($sqlinstances, $iparams) = $DB->get_in_or_equal($instanceids, SQL_PARAMS_NAMED, 'ins');
+        $sql = "SELECT d.*
+                  FROM {customfield_field} f
+                  JOIN {customfield_data} d ON (f.id = d.fieldid AND d.instanceid {$sqlinstances})
+                 WHERE f.id {$sqlfields}";
+        $fieldsdata = $DB->get_recordset_sql($sql, $params + $iparams);
+        foreach ($fieldsdata as $data) {
+            $result[$data->instanceid][$data->fieldid] = data_controller::create(0, $data, $fields[$data->fieldid]);
+        }
+        $fieldsdata->close();
+
+        if ($adddefaults) {
+            // Add default data where it was not retrieved.
+            foreach ($instanceids as $instanceid) {
+                foreach ($fields as $fieldid => $field) {
+                    if ($result[$instanceid][$fieldid] === null) {
+                        $result[$instanceid][$fieldid] =
+                            data_controller::create(0, (object)['instanceid' => $instanceid], $field);
+                    }
+                }
+            }
+        } else {
+            // Remove null-placeholders for data that was not retrieved.
+            foreach ($instanceids as $instanceid) {
+                $result[$instanceid] = array_filter($result[$instanceid]);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Retrieve a list of all available custom field types
+     *
+     * @return   array   a list of the fieldtypes suitable to use in a select statement
+     */
+    public static function get_available_field_types() {
+        $fieldtypes = array();
+
+        $plugins = \core\plugininfo\customfield::get_enabled_plugins();
+        foreach ($plugins as $type => $unused) {
+            $fieldtypes[$type] = get_string('pluginname', 'customfield_' . $type);
+        }
+        asort($fieldtypes);
+
+        return $fieldtypes;
+    }
+
+    /**
+     * Updates or creates a field with data that came from a form
+     *
+     * @param field_controller $field
+     * @param \stdClass $formdata
+     */
+    public static function save_field_configuration(field_controller $field, \stdClass $formdata) {
+        foreach ($formdata as $key => $value) {
+            if ($key === 'configdata' && is_array($formdata->configdata)) {
+                $field->set($key, json_encode($value));
+            } else if ($key === 'id' || ($key === 'type' && $field->get('id'))) {
+                continue;
+            } else if (field::has_property($key)) {
+                $field->set($key, $value);
+            }
+        }
+
+        $isnewfield = empty($field->get('id'));
+
+        // Process files in description.
+        if (isset($formdata->description_editor)) {
+            if (!$field->get('id')) {
+                // We need 'id' field to store files used in description.
+                $field->save();
+            }
+
+            $data = (object) ['description_editor' => $formdata->description_editor];
+            $textoptions = $field->get_handler()->get_description_text_options();
+            $data = file_postupdate_standard_editor($data, 'description', $textoptions, $textoptions['context'],
+                'core_customfield', 'description', $field->get('id'));
+            $field->set('description', $data->description);
+            $field->set('descriptionformat', $data->descriptionformat);
+        }
+
+        // Save the field.
+        $field->save();
+
+        if ($isnewfield) {
+            // Move to the end of the category.
+            self::move_field($field, $field->get('categoryid'));
+        }
+
+        if ($isnewfield) {
+            field_created::create_from_object($field)->trigger();
+        } else {
+            field_updated::create_from_object($field)->trigger();
+        }
+    }
+
+    /**
+     * Change fields sort order, move field to another category
+     *
+     * @param field_controller $field field that needs to be moved
+     * @param int $categoryid category that needs to be moved
+     * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
+     */
+    public static function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
+        global $DB;
+
+        if ($field->get('categoryid') != $categoryid) {
+            // Move field to another category. Validate that this category exists and belongs to the same component/area/itemid.
+            $category = $field->get_category();
+            $DB->get_record(category::TABLE, [
+                'component' => $category->get('component'),
+                'area' => $category->get('area'),
+                'itemid' => $category->get('itemid'),
+                'id' => $categoryid], 'id', MUST_EXIST);
+            $field->set('categoryid', $categoryid);
+            $field->save();
+            field_updated::create_from_object($field)->trigger();
+        }
+
+        // Reorder fields in the target category.
+        $records = $DB->get_records(field::TABLE, ['categoryid' => $categoryid], 'sortorder, id', '*');
+
+        $id = $field->get('id');
+        $fieldsids = array_values(array_diff(array_keys($records), [$id]));
+        $idx = $beforeid ? array_search($beforeid, $fieldsids) : false;
+        if ($idx === false) {
+            // Set as the last field.
+            $fieldsids = array_merge($fieldsids, [$id]);
+        } else {
+            // Set before field with id $beforeid.
+            $fieldsids = array_merge(array_slice($fieldsids, 0, $idx), [$id], array_slice($fieldsids, $idx));
+        }
+
+        foreach (array_values($fieldsids) as $idx => $fieldid) {
+            // Use persistent class to update the sortorder for each field that needs updating.
+            if ($records[$fieldid]->sortorder != $idx) {
+                $f = ($fieldid == $id) ? $field : new field(0, $records[$fieldid]);
+                $f->set('sortorder', $idx);
+                $f->save();
+            }
+        }
+    }
+
+    /**
+     * Delete a field
+     *
+     * @param field_controller $field
+     */
+    public static function delete_field_configuration(field_controller $field): bool {
+        $event = field_deleted::create_from_object($field);
+        get_file_storage()->delete_area_files($field->get_handler()->get_configuration_context()->id, 'core_customfield',
+            'description', $field->get('id'));
+        $result = $field->delete();
+        $event->trigger();
+        return $result;
+    }
+
+    /**
+     * Returns an object for inplace editable
+     *
+     * @param category_controller $category category that needs to be moved
+     * @param bool $editable
+     * @return inplace_editable
+     */
+    public static function get_category_inplace_editable(category_controller $category, bool $editable = true) : inplace_editable {
+        return new inplace_editable('core_customfield',
+                                    'category',
+                                    $category->get('id'),
+                                    $editable,
+                                    $category->get_formatted_name(),
+                                    $category->get('name'),
+                                    get_string('editcategoryname', 'core_customfield'),
+                                    get_string('newvaluefor', 'core_form', format_string($category->get('name')))
+        );
+    }
+
+    /**
+     * Reorder categories, move given category before another category
+     *
+     * @param category_controller $category category that needs to be moved
+     * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
+     */
+    public static function move_category(category_controller $category, int $beforeid = 0) {
+        global $DB;
+        $records = $DB->get_records(category::TABLE, [
+            'component' => $category->get('component'),
+            'area' => $category->get('area'),
+            'itemid' => $category->get('itemid')
+        ], 'sortorder, id', '*');
+
+        $id = $category->get('id');
+        $categoriesids = array_values(array_diff(array_keys($records), [$id]));
+        $idx = $beforeid ? array_search($beforeid, $categoriesids) : false;
+        if ($idx === false) {
+            // Set as the last category.
+            $categoriesids = array_merge($categoriesids, [$id]);
+        } else {
+            // Set before category with id $beforeid.
+            $categoriesids = array_merge(array_slice($categoriesids, 0, $idx), [$id], array_slice($categoriesids, $idx));
+        }
+
+        foreach (array_values($categoriesids) as $idx => $categoryid) {
+            // Use persistent class to update the sortorder for each category that needs updating.
+            if ($records[$categoryid]->sortorder != $idx) {
+                $c = ($categoryid == $id) ? $category : category_controller::create(0, $records[$categoryid]);
+                $c->set('sortorder', $idx);
+                $c->save();
+            }
+        }
+    }
+
+    /**
+     * Insert or update custom field category
+     *
+     * @param category_controller $category
+     */
+    public static function save_category(category_controller $category) {
+        $isnewcategory = empty($category->get('id'));
+
+        $category->save();
+
+        if ($isnewcategory) {
+            // Move to the end.
+            self::move_category($category);
+            category_created::create_from_object($category)->trigger();
+        } else {
+            category_updated::create_from_object($category)->trigger();
+        }
+    }
+
+    /**
+     * Delete a custom field category
+     *
+     * @param category_controller $category
+     * @return bool
+     */
+    public static function delete_category(category_controller $category): bool {
+        $event = category_deleted::create_from_object($category);
+
+        // Delete all fields.
+        foreach ($category->get_fields() as $field) {
+            self::delete_field_configuration($field);
+        }
+
+        $result = $category->delete();
+        $event->trigger();
+        return $result;
+    }
+
+    /**
+     * Returns a list of categories with their related fields.
+     *
+     * @param string $component
+     * @param string $area
+     * @param int $itemid
+     * @return category_controller[]
+     */
+    public static function get_categories_with_fields(string $component, string $area, int $itemid): array {
+        global $DB;
+
+        $categories = [];
+
+        $options = [
+                'component' => $component,
+                'area'      => $area,
+                'itemid'    => $itemid
+        ];
+
+        $plugins = \core\plugininfo\customfield::get_enabled_plugins();
+        list($sqlfields, $params) = $DB->get_in_or_equal(array_keys($plugins), SQL_PARAMS_NAMED, 'param', true, null);
+
+        $fields = 'f.*, ' . join(', ', array_map(function($field) {
+                return "c.$field AS category_$field";
+        }, array_diff(array_keys(category::properties_definition()), ['usermodified', 'timemodified'])));
+        $sql = "SELECT $fields
+                  FROM {customfield_category} c
+             LEFT JOIN {customfield_field} f ON c.id = f.categoryid AND f.type $sqlfields
+                 WHERE c.component = :component AND c.area = :area AND c.itemid = :itemid
+              ORDER BY c.sortorder, f.sortorder";
+        $fieldsdata = $DB->get_recordset_sql($sql, $options + $params);
+
+        foreach ($fieldsdata as $data) {
+            if (!array_key_exists($data->category_id, $categories)) {
+                $categoryobj = new \stdClass();
+                foreach ($data as $key => $value) {
+                    if (preg_match('/^category_(.*)$/', $key, $matches)) {
+                        $categoryobj->{$matches[1]} = $value;
+                    }
+                }
+                $category = category_controller::create(0, $categoryobj);
+                $categories[$categoryobj->id] = $category;
+            } else {
+                $category = $categories[$data->categoryid];
+            }
+            if ($data->id) {
+                $fieldobj = new \stdClass();
+                foreach ($data as $key => $value) {
+                    if (!preg_match('/^category_/', $key)) {
+                        $fieldobj->{$key} = $value;
+                    }
+                }
+                $field = field_controller::create(0, $fieldobj, $category);
+            }
+        }
+        $fieldsdata->close();
+
+        return $categories;
+    }
+
+    /**
+     * Prepares the object to pass to field configuration form set_data() method
+     *
+     * @param field_controller $field
+     * @return \stdClass
+     */
+    public static function prepare_field_for_config_form(field_controller $field): \stdClass {
+        if ($field->get('id')) {
+            $formdata = $field->to_record();
+            $formdata->configdata = $field->get('configdata');
+            // Preprocess the description.
+            $textoptions = $field->get_handler()->get_description_text_options();
+            file_prepare_standard_editor($formdata, 'description', $textoptions, $textoptions['context'], 'core_customfield',
+                'description', $formdata->id);
+        } else {
+            $formdata = (object)['categoryid' => $field->get('categoryid'), 'type' => $field->get('type'), 'configdata' => []];
+        }
+        // Allow field to do more preprocessing (usually for editor or filemanager elements).
+        $field->prepare_for_config_form($formdata);
+        return $formdata;
+    }
+}
diff --git a/customfield/classes/field_config_form.php b/customfield/classes/field_config_form.php
new file mode 100644 (file)
index 0000000..06a6c40
--- /dev/null
@@ -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/>.
+
+/**
+ * Customfield package
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_customfield;
+
+defined('MOODLE_INTERNAL') || die;
+
+global $CFG;
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Class field_config_form
+ *
+ * @package core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_config_form extends \moodleform {
+
+    /**
+     * Class definition
+     *
+     * @throws \coding_exception
+     */
+    public function definition() {
+        global $PAGE;
+        $mform = $this->_form;
+
+        $field = $this->_customdata['field'];
+        if (!($field && $field instanceof field_controller)) {
+            throw new \coding_exception('Field must be passed in customdata');
+        }
+        $handler = $field->get_handler();
+
+        $mform->addElement('header', '_commonsettings', get_string('commonsettings', 'core_customfield'));
+
+        $mform->addElement('text', 'name', get_string('fieldname', 'core_customfield'), 'size="50"');
+        $mform->addRule('name', null, 'required', null, 'client');
+        $mform->setType('name', PARAM_TEXT);
+
+        // Accepted values for 'shortname' would follow [a-z0-9_] pattern,
+        // but we are accepting any PARAM_TEXT value here,
+        // and checking [a-zA-Z0-9_] pattern in validation() function to throw an error when needed.
+        $mform->addElement('text', 'shortname', get_string('fieldshortname', 'core_customfield'), 'size=20');
+        $mform->addHelpButton('shortname', 'shortname', 'core_customfield');
+        $mform->addRule('shortname', null, 'required', null, 'client');
+        $mform->setType('shortname', PARAM_TEXT);
+
+        $desceditoroptions = $handler->get_description_text_options();
+        $mform->addElement('editor', 'description_editor', get_string('description', 'core_customfield'), null, $desceditoroptions);
+        $mform->addHelpButton('description_editor', 'description', 'core_customfield');
+
+        // If field is required.
+        $mform->addElement('selectyesno', 'configdata[required]', get_string('isfieldrequired', 'core_customfield'));
+        $mform->addHelpButton('configdata[required]', 'isfieldrequired', 'core_customfield');
+        $mform->setType('configdata[required]', PARAM_BOOL);
+
+        // If field data is unique.
+        $mform->addElement('selectyesno', 'configdata[uniquevalues]', get_string('isdataunique', 'core_customfield'));
+        $mform->addHelpButton('configdata[uniquevalues]', 'isdataunique', 'core_customfield');
+        $mform->setType('configdata[uniquevalues]', PARAM_BOOL);
+
+        // Field specific settings from field type.
+        $field->config_form_definition($mform);
+
+        // Handler/component settings.
+        $handler->config_form_definition($mform);
+
+        // We add hidden fields.
+        $mform->addElement('hidden', 'categoryid');
+        $mform->setType('categoryid', PARAM_INT);
+
+        $mform->addElement('hidden', 'type');
+        $mform->setType('type', PARAM_COMPONENT);
+
+        $mform->addElement('hidden', 'id');
+        $mform->setType('id', PARAM_INT);
+
+        $this->add_action_buttons(true);
+    }
+
+    /**
+     * Field data validation
+     *
+     * @param array $data
+     * @param array $files
+     * @return array
+     */
+    public function validation($data, $files = array()) {
+        global $DB;
+
+        $errors = array();
+        /** @var field_controller $field */
+        $field = $this->_customdata['field'];
+        $handler = $field->get_handler();
+
+        // Check the shortname is specified and is unique for this component-area-itemid combination.
+        if (!preg_match('/^[a-z0-9_]+$/', $data['shortname'])) {
+            // Check allowed pattern (numbers, letters and underscore).
+            $errors['shortname'] = get_string('invalidshortnameerror', 'core_customfield');
+        } else if ($DB->record_exists_sql('SELECT 1 FROM {customfield_field} f ' .
+            'JOIN {customfield_category} c ON c.id = f.categoryid ' .
+            'WHERE f.shortname = ? AND f.id <> ? AND c.component = ? AND c.area = ? AND c.itemid = ?',
+            [$data['shortname'], $data['id'],
+                $handler->get_component(), $handler->get_area(), $handler->get_itemid()])) {
+            $errors['shortname'] = get_string('formfieldcheckshortname', 'core_customfield');
+        }
+
+        $errors = array_merge($errors, $field->config_form_validation($data, $files));
+
+        return $errors;
+    }
+}
diff --git a/customfield/classes/handler.php b/customfield/classes/handler.php
new file mode 100644 (file)
index 0000000..fe633f1
--- /dev/null
@@ -0,0 +1,840 @@
+<?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/>.
+
+/**
+ * The abstract custom fields handler
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_customfield;
+
+use core_customfield\output\field_data;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Base class for custom fields handlers
+ *
+ * This handler provides callbacks for field configuration form and also allows to add the fields to the instance editing form
+ *
+ * Every plugin that wants to use custom fields must define a handler class:
+ * <COMPONENT_OR_PLUGIN>\customfield\<AREA>_handler extends \core_customfield\handler
+ *
+ * To initiate a class use an appropriate static method:
+ * - <handlerclass>::create - to create an instance of a known handler
+ * - \core_customfield\handler::get_handler - to create an instance of a handler for given component/area/itemid
+ *
+ * Also handler is automatically created when the following methods are called:
+ * - \core_customfield\api::get_field($fieldid)
+ * - \core_customfield\api::get_category($categoryid)
+ *
+ * @package core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class handler {
+
+    /**
+     * The component this handler handles
+     *
+     * @var string $component
+     */
+    private $component;
+
+    /**
+     * The area within the component
+     *
+     * @var string $area
+     */
+    private $area;
+
+    /**
+     * The id of the item within the area and component
+
+     * @var int $itemid
+     */
+    private $itemid;
+
+    /**
+     * @var category_controller[]
+     */
+    protected $categories = null;
+
+    /**
+     * Handler constructor.
+     *
+     * @param int $itemid
+     */
+    protected final function __construct(int $itemid = 0) {
+        if (!preg_match('|^(\w+_[\w_]+)\\\\customfield\\\\([\w_]+)_handler$|', static::class, $matches)) {
+            throw new \coding_exception('Handler class name must have format: <PLUGIN>\\customfield\\<AREA>_handler');
+        }
+        $this->component = $matches[1];
+        $this->area = $matches[2];
+        $this->itemid = $itemid;
+    }
+
+    /**
+     * Returns an instance of the handler
+     *
+     * Some areas may choose to use singleton/caching here
+     *
+     * @param int $itemid
+     * @return handler
+     */
+    public static function create(int $itemid = 0) : handler {
+        return new static($itemid);
+    }
+
+    /**
+     * Returns an instance of handler by component/area/itemid
+     *
+     * @param string $component component name of full frankenstyle plugin name
+     * @param string $area name of the area (each component/plugin may define handlers for multiple areas)
+     * @param int $itemid item id if the area uses them (usually not used)
+     * @return handler
+     */
+    public static function get_handler(string $component, string $area, int $itemid = 0) : handler {
+        $classname = $component . '\\customfield\\' . $area . '_handler';
+        if (class_exists($classname) && is_subclass_of($classname, self::class)) {
+            return $classname::create($itemid);
+        }
+        $a = ['component' => s($component), 'area' => s($area)];
+        throw new \moodle_exception('unknownhandler', 'core_customfield', (object)$a);
+    }
+
+    /**
+     * Get component
+     *
+     * @return string
+     */
+    public function get_component() : string {
+        return $this->component;
+    }
+
+    /**
+     * Get area
+     *
+     * @return string
+     */
+    public function get_area() : string {
+        return $this->area;
+    }
+
+    /**
+     * Context that should be used for new categories created by this handler
+     *
+     * @return \context
+     */
+    abstract public function get_configuration_context() : \context;
+
+    /**
+     * URL for configuration of the fields on this handler.
+     *
+     * @return \moodle_url
+     */
+    abstract public function get_configuration_url() : \moodle_url;
+
+    /**
+     * Context that should be used for data stored for the given record
+     *
+     * @param int $instanceid id of the instance or 0 if the instance is being created
+     * @return \context
+     */
+    abstract public function get_instance_context(int $instanceid = 0) : \context;
+
+    /**
+     * Get itemid
+     *
+     * @return int|null
+     */
+    public function get_itemid() : int {
+        return $this->itemid;
+    }
+
+    /**
+     * Uses categories
+     *
+     * @return bool
+     */
+    public function uses_categories(): bool {
+        return true;
+    }
+
+    /**
+     * The form to create or edit a field
+     *
+     * @param field_controller $field
+     * @return field_config_form
+     */
+    public function get_field_config_form(field_controller $field): field_config_form {
+         $form = new field_config_form(null, ['field' => $field]);
+         $form->set_data(api::prepare_field_for_config_form($field));
+         return $form;
+    }
+
+    /**
+     * Generates a name for the new category
+     *
+     * @param int $suffix
+     * @return string
+     */
+    protected function generate_category_name($suffix = 0) : string {
+        if ($suffix) {
+            return get_string('otherfieldsn', 'core_customfield', $suffix);
+        } else {
+            return get_string('otherfields', 'core_customfield');
+        }
+    }
+
+    /**
+     * Creates a new category and inserts it to the database
+     *
+     * @param string $name name of the category, null to generate automatically
+     * @return int id of the new category
+     */
+    public function create_category(string $name = null): int {
+        global $DB;
+        $params = ['component' => $this->get_component(), 'area' => $this->get_area(), 'itemid' => $this->get_itemid()];
+
+        if (empty($name)) {
+            for ($suffix = 0; $suffix < 100; $suffix++) {
+                $name = $this->generate_category_name($suffix);
+                if (!$DB->record_exists(category::TABLE, $params + ['name' => $name])) {
+                    break;
+                }
+            }
+        }
+
+        $category = category_controller::create(0, (object)['name' => $name], $this);
+        api::save_category($category);
+        $this->clear_configuration_cache();
+        return $category->get('id');
+    }
+
+    /**
+     * Validate that the given category belongs to this handler
+     *
+     * @param category_controller $category
+     * @return category_controller
+     * @throws \moodle_exception
+     */
+    protected function validate_category(category_controller $category): category_controller {
+        $categories = $this->get_categories_with_fields();
+        if (!array_key_exists($category->get('id'), $categories)) {
+            throw new \moodle_exception('categorynotfound', 'core_customfield');
+        }
+        return $categories[$category->get('id')];
+    }
+
+    /**
+     * Validate that the given field belongs to this handler
+     *
+     * @param field_controller $field
+     * @return field_controller
+     * @throws \moodle_exception
+     */
+    protected function validate_field(field_controller $field): field_controller {
+        if (!array_key_exists($field->get('categoryid'), $this->get_categories_with_fields())) {
+            throw new \moodle_exception('fieldnotfound', 'core_customfield');
+        }
+        $category = $this->get_categories_with_fields()[$field->get('categoryid')];
+        if (!array_key_exists($field->get('id'), $category->get_fields())) {
+            throw new \moodle_exception('fieldnotfound', 'core_customfield');
+        }
+        return $category->get_fields()[$field->get('id')];
+    }
+
+    /**
+     * Change name for a field category
+     *
+     * @param category_controller $category
+     * @param string $name
+     */
+    public function rename_category(category_controller $category, string $name) {
+        $this->validate_category($category);
+        $category->set('name', $name);
+        api::save_category($category);
+        $this->clear_configuration_cache();
+    }
+
+    /**
+     * Change sort order of the categories
+     *
+     * @param category_controller $category category that needs to be moved
+     * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
+     */
+    public function move_category(category_controller $category, int $beforeid = 0) {
+        $category = $this->validate_category($category);
+        api::move_category($category, $beforeid);
+        $this->clear_configuration_cache();
+    }
+
+    /**
+     * Permanently delete category, all fields in it and all associated data
+     *
+     * @param category_controller $category
+     * @return bool
+     */
+    public function delete_category(category_controller $category): bool {
+        $category = $this->validate_category($category);
+        $result = api::delete_category($category);
+        $this->clear_configuration_cache();
+        return $result;
+    }
+
+    /**
+     * Deletes all data and all fields and categories defined in this handler
+     */
+    public function delete_all() {
+        $categories = $this->get_categories_with_fields();
+        foreach ($categories as $category) {
+            api::delete_category($category);
+        }
+        $this->clear_configuration_cache();
+    }
+
+    /**
+     * Permanently delete a custom field configuration and all associated data
+     *
+     * @param field_controller $field
+     * @return bool
+     */
+    public function delete_field_configuration(field_controller $field): bool {
+        $field = $this->validate_field($field);
+        $result = api::delete_field_configuration($field);
+        $this->clear_configuration_cache();
+        return $result;
+    }
+
+    /**
+     * Change fields sort order, move field to another category
+     *
+     * @param field_controller $field field that needs to be moved
+     * @param int $categoryid category that needs to be moved
+     * @param int $beforeid id of the category this category needs to be moved before, 0 to move to the end
+     */
+    public function move_field(field_controller $field, int $categoryid, int $beforeid = 0) {
+        $field = $this->validate_field($field);
+        api::move_field($field, $categoryid, $beforeid);
+        $this->clear_configuration_cache();
+    }
+
+    /**
+     * The current user can configure custom fields on this component.
+     *
+     * @return bool
+     */
+    abstract public function can_configure(): bool;
+
+    /**
+     * The current user can edit given custom fields on the given instance
+     *
+     * Called to filter list of fields displayed on the instance edit form
+     *
+     * Capability to edit/create instance is checked separately
+     *
+     * @param field_controller $field
+     * @param int $instanceid id of the instance or 0 if the instance is being created
+     * @return bool
+     */
+    abstract public function can_edit(field_controller $field, int $instanceid = 0): bool;
+
+    /**
+     * The current user can view the value of the custom field for a given custom field and instance
+     *
+     * Called to filter list of fields returned by methods get_instance_data(), get_instances_data(),
+     * export_instance_data(), export_instance_data_object()
+     *
+     * Access to the instance itself is checked by handler before calling these methods
+     *
+     * @param field_controller $field
+     * @param int $instanceid
+     * @return bool
+     */
+    abstract public function can_view(field_controller $field, int $instanceid): bool;
+
+    /**
+     * Returns the custom field values for an individual instance
+     *
+     * The caller must check access to the instance itself before invoking this method
+     *
+     * The result is an array of data_controller objects
+     *
+     * @param int $instanceid
+     * @param bool $returnall return data for all fields (by default only visible fields)
+     * @return data_controller[] array of data_controller objects indexed by fieldid. All fields are present,
+     *     some data_controller objects may have 'id', some not
+     *     In the last case data_controller::get_value() and export_value() functions will return default values.
+     */
+    public function get_instance_data(int $instanceid, bool $returnall = false) : array {
+        $fields = $returnall ? $this->get_fields() : $this->get_visible_fields($instanceid);
+        return api::get_instance_fields_data($fields, $instanceid);
+    }
+
+    /**
+     * Returns the custom fields values for multiple instances
+     *
+     * The caller must check access to the instance itself before invoking this method
+     *
+     * The result is an array of data_controller objects
+     *
+     * @param int[] $instanceids
+     * @param bool $returnall return data for all fields (by default only visible fields)
+     * @return data_controller[][] 2-dimension array, first index is instanceid, second index is fieldid.
+     *     All instanceids and all fieldids are present, some data_controller objects may have 'id', some not.
+     *     In the last case data_controller::get_value() and export_value() functions will return default values.
+     */
+    public function get_instances_data(array $instanceids, bool $returnall = false) : array {
+        $result = api::get_instances_fields_data($this->get_fields(), $instanceids);
+
+        if (!$returnall) {
+            // Filter only by visible fields (list of visible fields may be different for each instance).
+            $handler = $this;
+            foreach ($instanceids as $instanceid) {
+                $result[$instanceid] = array_filter($result[$instanceid], function(data_controller $d) use ($handler) {
+                    return $handler->can_view($d->get_field(), $d->get('instanceid'));
+                });
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the custom field values for an individual instance ready to be displayed
+     *
+     * The caller must check access to the instance itself before invoking this method
+     *
+     * The result is an array of \core_customfield\output\field_data objects
+     *
+     * @param int $instanceid
+     * @param bool $returnall
+     * @return \core_customfield\output\field_data[]
+     */
+    public function export_instance_data(int $instanceid, bool $returnall = false) : array {
+        return array_map(function($d) {
+            return new field_data($d);
+        }, $this->get_instance_data($instanceid, $returnall));
+    }
+
+    /**
+     * Returns the custom field values for an individual instance ready to be displayed
+     *
+     * The caller must check access to the instance itself before invoking this method
+     *
+     * The result is a class where properties are fields short names and the values their export values for this instance
+     *
+     * @param int $instanceid
+     * @param bool $returnall
+     * @return stdClass
+     */
+    public function export_instance_data_object(int $instanceid, bool $returnall = false) : stdClass {
+        $rv = new stdClass();
+        foreach ($this->export_instance_data($instanceid, $returnall) as $d) {
+            $rv->{$d->get_shortname()} = $d->get_value();
+        }
+        return $rv;
+    }
+
+    /**
+     * Display visible custom fields.
+     * This is a sample implementation that can be overridden in each handler.
+     *
+     * @param data_controller[] $fieldsdata
+     * @return string
+     */
+    public function display_custom_fields_data(array $fieldsdata): string {
+        global $PAGE;
+        $output = $PAGE->get_renderer('core_customfield');
+        $content = '';
+        foreach ($fieldsdata as $data) {
+            $fd = new field_data($data);
+            $content .= $output->render($fd);
+        }
+
+        return $content;
+    }
+
+    /**
+     * Returns array of categories, each of them contains a list of fields definitions.
+     *
+     * @return category_controller[]
+     */
+    public function get_categories_with_fields() : array {
+        if ($this->categories === null) {
+            $this->categories = api::get_categories_with_fields($this->get_component(), $this->get_area(), $this->get_itemid());
+        }
+        $handler = $this;
+        array_walk($this->categories, function(category_controller $c) use ($handler) {
+            $c->set_handler($handler);
+        });
+        return $this->categories;
+    }
+
+    /**
+     * Clears a list of categories with corresponding fields definitions.
+     */
+    protected function clear_configuration_cache() {
+        $this->categories = null;
+    }
+
+    /**
+     * Checks if current user can backup a given field
+     *
+     * Capability to backup the instance does not need to be checked here
+     *
+     * @param field_controller $field
+     * @param int $instanceid
+     * @return bool
+     */
+    protected function can_backup(field_controller $field, int $instanceid) : bool {
+        return $this->can_view($field, $instanceid) || $this->can_edit($field, $instanceid);
+    }
+
+    /**
+     * Get raw data associated with all fields current user can view or edit
+     *
+     * @param int $instanceid
+     * @return array
+     */
+    public function get_instance_data_for_backup(int $instanceid) : array {
+        $finalfields = [];
+        $data = $this->get_instance_data($instanceid, true);
+        foreach ($data as $d) {
+            if ($d->get('id') && $this->can_backup($d->get_field(), $instanceid)) {
+                $finalfields[] = [
+                    'id' => $d->get('id'),
+                    'shortname' => $d->get_field()->get('shortname'),
+                    'type' => $d->get_field()->get('type'),
+                    'value' => $d->get_value(),
+                    'valueformat' => $d->get('valueformat')];
+            }
+        }
+        return $finalfields;
+    }
+
+    /**
+     * Form data definition callback.
+     *
+     * This method is called from moodleform::definition_after_data and allows to tweak
+     * mform with some data coming directly from the field plugin data controller.
+     *
+     * @param \MoodleQuickForm $mform
+     * @param int $instanceid
+     */
+    public function instance_form_definition_after_data(\MoodleQuickForm $mform, int $instanceid = 0) {
+        $editablefields = $this->get_editable_fields($instanceid);
+        $fields = api::get_instance_fields_data($editablefields, $instanceid);
+
+        foreach ($fields as $formfield) {
+            $formfield->instance_form_definition_after_data($mform);
+        }
+    }
+
+    /**
+     * Prepares the custom fields data related to the instance to pass to mform->set_data()
+     *
+     * Example:
+     *   $instance = $DB->get_record(...);
+     *   // .... prepare editor, filemanager, add tags, etc.
+     *   $handler->instance_form_before_set_data($instance);
+     *   $form->set_data($instance);
+     *
+     * @param stdClass $instance the instance that has custom fields, if 'id' attribute is present the custom
+     *    fields for this instance will be added, otherwise the default values will be added.
+     */
+    public function instance_form_before_set_data(stdClass $instance) {
+        $instanceid = !empty($instance->id) ? $instance->id : 0;
+        $fields = api::get_instance_fields_data($this->get_editable_fields($instanceid), $instanceid);
+
+        foreach ($fields as $formfield) {
+            $formfield->instance_form_before_set_data($instance);
+        }
+    }
+
+    /**
+     * Saves the given data for custom fields, must be called after the instance is saved and id is present
+     *
+     * Example:
+     *   if ($data = $form->get_data()) {
+     *     // ... save main instance, set $data->id if instance was created.
+     *     $handler->instance_form_save($data);
+     *     redirect(...);
+     *   }
+     *
+     * @param stdClass $instance data received from a form
+     * @param bool $isnewinstance if this is call is made during instance creation
+     */
+    public function instance_form_save(stdClass $instance, bool $isnewinstance = false) {
+        if (empty($instance->id)) {
+            throw new \coding_exception('Caller must ensure that id is already set in data before calling this method');
+        }
+        if (!preg_grep('/^customfield_/', array_keys((array)$instance))) {
+            // For performance.
+            return;
+        }
+        $editablefields = $this->get_editable_fields($isnewinstance ? 0 : $instance->id);
+        $fields = api::get_instance_fields_data($editablefields, $instance->id);
+        foreach ($fields as $data) {
+            if (!$data->get('id')) {
+                $data->set('contextid', $this->get_instance_context($instance->id)->id);
+            }
+            $data->instance_form_save($instance);
+        }
+    }
+
+    /**
+     * Validates the given data for custom fields, used in moodleform validation() function
+     *
+     * Example:
+     *   public function validation($data, $files) {
+     *     $errors = [];
+     *     // .... check other fields.
+     *     $errors = array_merge($errors, $handler->instance_form_validation($data, $files));
+     *     return $errors;
+     *   }
+     *
+     * @param array $data
+     * @param array $files
+     * @return array validation errors
+     */
+    public function instance_form_validation(array $data, array $files) {
+        $instanceid = empty($data['id']) ? 0 : $data['id'];
+        $editablefields = $this->get_editable_fields($instanceid);
+        $fields = api::get_instance_fields_data($editablefields, $instanceid);
+        $errors = [];
+        foreach ($fields as $formfield) {
+            $errors += $formfield->instance_form_validation($data, $files);
+        }
+        return $errors;
+    }
+
+    /**
+     * Adds custom fields to instance editing form
+     *
+     * Example:
+     *   public function definition() {
+     *     // ... normal instance definition, including hidden 'id' field.
+     *     $handler->instance_form_definition($this->_form, $instanceid);
+     *     $this->add_action_buttons();
+     *   }
+     *
+     * @param \MoodleQuickForm $mform
+     * @param int $instanceid id of the instance, can be null when instance is being created
+     */
+    public function instance_form_definition(\MoodleQuickForm $mform, int $instanceid = 0) {
+
+        $editablefields = $this->get_editable_fields($instanceid);
+        $fieldswithdata = api::get_instance_fields_data($editablefields, $instanceid);
+        $lastcategoryid = null;
+        foreach ($fieldswithdata as $data) {
+            $categoryid = $data->get_field()->get_category()->get('id');
+            if ($categoryid != $lastcategoryid) {
+                $mform->addElement('header', 'category_' . $categoryid,
+                    format_string($data->get_field()->get_category()->get('name')));
+                $lastcategoryid = $categoryid;
+            }
+            $data->instance_form_definition($mform);
+            $field = $data->get_field()->to_record();
+            if (strlen($field->description)) {
+                // Add field description.
+                $context = $this->get_configuration_context();
+                $value = file_rewrite_pluginfile_urls($field->description, 'pluginfile.php',
+                    $context->id, 'core_customfield', 'description', $field->id);
+                $value = format_text($value, $field->descriptionformat, ['context' => $context]);
+                $mform->addElement('static', 'customfield_' . $field->shortname . '_static', '', $value);
+            }
+        }
+    }
+
+    /**
+     * Get field types array
+     *
+     * @return array
+     */
+    public function get_available_field_types() :array {
+        return api::get_available_field_types();
+    }
+
+    /**
+     * Options for processing embedded files in the field description.
+     *
+     * Handlers may want to extend it to disable files support and/or specify 'noclean'=>true
+     * Context is not necessary here
+     *
+     * @return array
+     */
+    public function get_description_text_options() : array {
+        global $CFG;
+        require_once($CFG->libdir.'/formslib.php');
+        return [
+            'maxfiles' => EDITOR_UNLIMITED_FILES,
+            'maxbytes' => $CFG->maxbytes,
+            'context' => $this->get_configuration_context()
+        ];
+    }
+
+    /**
+     * Save the field configuration with the data from the form
+     *
+     * @param field_controller $field
+     * @param stdClass $data data from the form
+     */
+    public function save_field_configuration(field_controller $field, stdClass $data) {
+        if ($field->get('id')) {
+            $field = $this->validate_field($field);
+        } else {
+            $this->validate_category($field->get_category());
+        }
+        api::save_field_configuration($field, $data);
+        $this->clear_configuration_cache();
+    }
+
+    /**
+     * Creates or updates custom field data for a instanceid from backup data.
+     *
+     * The handlers have to override it if they support backup
+     *
+     * @param \restore_task $task
+     * @param array $data
+     */
+    public function restore_instance_data_from_backup(\restore_task $task, array $data) {
+        throw new \coding_exception('Must be implemented in the handler');
+    }
+
+    /**
+     * Returns list of fields defined for this instance as an array (not groupped by categories)
+     *
+     * Fields are sorted in the same order they would appear on the instance edit form
+     *
+     * Note that this function returns all fields in all categories regardless of whether the current user
+     * can view or edit data associated with them
+     *
+     * @return field_controller[]
+     */
+    public function get_fields(): array {
+        $categories = $this->get_categories_with_fields();
+        $fields = [];
+        foreach ($categories as $category) {
+            foreach ($category->get_fields() as $field) {
+                $fields[$field->get('id')] = $field;
+            }
+        }
+        return $fields;
+    }
+
+    /**
+     * Get visible fields
+     *
+     * @param int $instanceid
+     * @return field_controller[]
+     */
+    protected function get_visible_fields(int $instanceid): array {
+        $handler = $this;
+        return array_filter($this->get_fields(),
+            function($field) use($handler, $instanceid) {
+                return $handler->can_view($field, $instanceid);
+            }
+        );
+    }
+
+    /**
+     * Get editable fields
+     *
+     * @param int $instanceid
+     * @return field_controller[]
+     */
+    public function get_editable_fields(int $instanceid): array {
+        $handler = $this;
+        return array_filter($this->get_fields(),
+            function($field) use($handler, $instanceid) {
+                return $handler->can_edit($field, $instanceid);
+            }
+        );
+    }
+
+    /**
+     * Allows to add custom controls to the field configuration form that will be saved in configdata
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function config_form_definition(\MoodleQuickForm $mform) {
+    }
+
+    /**
+     * Deletes all data related to all fields of an instance.
+     *
+     * @param int $instanceid
+     */
+    public function delete_instance(int $instanceid) {
+        $fielddata = api::get_instance_fields_data($this->get_fields(), $instanceid, false);
+        foreach ($fielddata as $data) {
+            $data->delete();
+        }
+    }
+
+    /**
+     * Set up page customfield/edit.php
+     *
+     * Handler should override this method and set page context
+     *
+     * @param field_controller $field
+     * @return string page heading
+     */
+    public function setup_edit_page(field_controller $field): string {
+        global $PAGE;
+
+        // Page context.
+        $context = $this->get_configuration_context();
+        if ($context->contextlevel == CONTEXT_MODULE) {
+            list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid, '', $context->get_course_context()->instanceid);
+            require_login($course, false, $cm);
+        } else if ($context->contextlevel == CONTEXT_COURSE) {
+            require_login($context->instanceid, false);
+        } else {
+            $PAGE->set_context(null); // This will set to system context only if the context was not set before.
+            if ($PAGE->context->id != $context->id) {
+                // In case of user or block context level this method must be overridden.
+                debugging('Handler must override setup_edit_page() and set the page context before calling parent method.',
+                    DEBUG_DEVELOPER);
+            }
+        }
+
+        // Set up url and title.
+        if ($field->get('id')) {
+            $field = $this->validate_field($field);
+        } else {
+            $this->validate_category($field->get_category());
+        }
+        $url = new \moodle_url('/customfield/edit.php',
+            ['id' => $field->get('id'), 'type' => $field->get('type'), 'categoryid' => $field->get('categoryid')]);
+
+        $PAGE->set_url($url);
+        $typestr = get_string('pluginname', 'customfield_' . $field->get('type'));
+        if ($field->get('id')) {
+            $title = get_string('editingfield', 'core_customfield',
+                $field->get_formatted_name());
+        } else {
+            $title = get_string('addingnewcustomfield', 'core_customfield', $typestr);
+        }
+        $PAGE->set_title($title);
+        return $title;
+    }
+}
diff --git a/customfield/classes/output/management.php b/customfield/classes/output/management.php
new file mode 100644 (file)
index 0000000..e451b27
--- /dev/null
@@ -0,0 +1,132 @@
+<?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/>.
+
+/**
+ * Customfield component output.
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_customfield\output;
+
+use core_customfield\api;
+use core_customfield\handler;
+use renderable;
+use templatable;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class management
+ *
+ * @package core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class management implements renderable, templatable {
+
+    /**
+     * @var handler
+     */
+    protected $handler;
+    /**
+     * @var
+     */
+    protected $categoryid;
+
+    /**
+     * management constructor.
+     *
+     * @param \core_customfield\handler $handler
+     */
+    public function __construct(\core_customfield\handler $handler) {
+        $this->handler = $handler;
+    }
+
+    /**
+     * Export for template
+     *
+     * @param \renderer_base $output
+     * @return array|object|\stdClass
+     */
+    public function export_for_template(\renderer_base $output) {
+        $data = new \stdClass();
+
+        $fieldtypes = $this->handler->get_available_field_types();
+
+        $data->component = $this->handler->get_component();
+        $data->area = $this->handler->get_area();
+        $data->itemid = $this->handler->get_itemid();
+        $data->usescategories = $this->handler->uses_categories();
+        $categories = $this->handler->get_categories_with_fields();
+
+        $categoriesarray = array();
+
+        foreach ($categories as $category) {
+
+            $categoryarray = array();
+            $categoryarray['id'] = $category->get('id');
+            $categoryarray['nameeditable'] = $output->render(api::get_category_inplace_editable($category, true));
+            $categoryarray['movetitle'] = get_string('movecategory', 'core_customfield',
+                $category->get_formatted_name());
+
+            $categoryarray['fields'] = array();
+
+            foreach ($category->get_fields() as $field) {
+
+                $fieldname = $field->get_formatted_name();
+                $fieldarray['type'] = $fieldtypes[$field->get('type')];
+                $fieldarray['id'] = $field->get('id');
+                $fieldarray['name'] = $fieldname;
+                $fieldarray['shortname'] = $field->get('shortname');
+                $fieldarray['movetitle'] = get_string('movefield', 'core_customfield', $fieldname);
+
+                $fieldarray['editfieldurl'] = (new \moodle_url('/customfield/edit.php', [
+                        'id' => $fieldarray['id'],
+                ]))->out(false);
+
+                $categoryarray['fields'][] = $fieldarray;
+            }
+
+            $menu = new \action_menu();
+            $menu->set_alignment(\action_menu::BL, \action_menu::BL);
+            $menu->set_menu_trigger(get_string('createnewcustomfield', 'core_customfield'));
+
+            $baseaddfieldurl = new \moodle_url('/customfield/edit.php',
+                    array('action' => 'editfield', 'categoryid' => $category->get('id')));
+            foreach ($fieldtypes as $type => $fieldname) {
+                $addfieldurl = new \moodle_url($baseaddfieldurl, array('type' => $type));
+                $action = new \action_menu_link_secondary($addfieldurl, null, $fieldname);
+                $menu->add($action);
+            }
+            $menu->attributes['class'] .= ' float-left mr-1';
+
+            $categoryarray['addfieldmenu'] = $output->render($menu);
+
+            $categoriesarray[] = $categoryarray;
+        }
+
+        $data->categories = $categoriesarray;
+
+        if (empty($data->categories)) {
+            $data->nocategories = get_string('nocategories', 'core_customfield');
+        }
+
+        return $data;
+    }
+}
diff --git a/customfield/classes/output/renderer.php b/customfield/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..2fa455d
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * Renderer.
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_customfield\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+
+/**
+ * Renderer class.
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+    /**
+     * Render custom field management interface.
+     *
+     * @param \core_customfield\output\management $list
+     * @return string HTML
+     */
+    protected function render_management(\core_customfield\output\management $list) {
+        $context = $list->export_for_template($this);
+
+        return $this->render_from_template('core_customfield/list', $context);
+    }
+
+    /**
+     * Render single custom field value
+     *
+     * @param \core_customfield\output\field_data $field
+     * @return string HTML
+     */
+    protected function render_field_data(\core_customfield\output\field_data $field) {
+        $context = $field->export_for_template($this);
+        return $this->render_from_template('core_customfield/field_data', $context);
+    }
+}
\ No newline at end of file
diff --git a/customfield/classes/privacy/provider.php b/customfield/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..9f3f408
--- /dev/null
@@ -0,0 +1,486 @@
+<?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/>.
+
+/**
+ * Customfield component provider class
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_customfield\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_customfield\data_controller;
+use core_customfield\handler;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\writer;
+use core_privacy\manager;
+use Horde\Socket\Client\Exception;
+
+/**
+ * Class provider
+ *
+ * Customfields API does not directly store userid and does not perform any export or delete functionality by itself
+ *
+ * However this class defines several functions that can be utilized by components that use customfields API to
+ * export/delete user data.
+ *
+ * @package core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @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\plugin_provider {
+
+    /**
+     * Return the fields which contain personal data.
+     *
+     * @param collection $collection a reference to the collection to use to store the metadata.
+     * @return collection the updated collection of metadata items.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_database_table(
+            'customfield_data',
+            [
+                'fieldid' => 'privacy:metadata:customfield_data:fieldid',
+                'instanceid' => 'privacy:metadata:customfield_data:instanceid',
+                'intvalue' => 'privacy:metadata:customfield_data:intvalue',
+                'decvalue' => 'privacy:metadata:customfield_data:decvalue',
+                'shortcharvalue' => 'privacy:metadata:customfield_data:shortcharvalue',
+                'charvalue' => 'privacy:metadata:customfield_data:charvalue',
+                'value' => 'privacy:metadata:customfield_data:value',
+                'valueformat' => 'privacy:metadata:customfield_data:valueformat',
+                'timecreated' => 'privacy:metadata:customfield_data:timecreated',
+                'timemodified' => 'privacy:metadata:customfield_data:timemodified',
+                'contextid' => 'privacy:metadata:customfield_data:contextid',
+            ],
+            'privacy:metadata:customfield_data'
+        );
+
+        // Link to subplugins.
+        $collection->add_plugintype_link('customfield', [], 'privacy:metadata:customfieldpluginsummary');
+
+        $collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
+
+        return $collection;
+    }
+
+    /**
+     * Returns contexts that have customfields data
+     *
+     * To be used in implementations of core_user_data_provider::get_contexts_for_userid
+     * Caller needs to transfer the $userid to the select subqueries for
+     * customfield_category->itemid and/or customfield_data->instanceid
+     *
+     * @param string $component
+     * @param string $area
+     * @param string $itemidstest subquery for selecting customfield_category->itemid
+     * @param string $instanceidstest subquery for selecting customfield_data->instanceid
+     * @param array $params array of named parameters
+     * @return contextlist
+     */
+    public static function get_customfields_data_contexts(string $component, string $area,
+            string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []) : contextlist {
+
+        $sql = "SELECT d.contextid FROM {customfield_category} c
+            JOIN {customfield_field} f ON f.categoryid = c.id
+            JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest
+            WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
+
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
+
+        return $contextlist;
+    }
+
+    /**
+     * Returns contexts that have customfields configuration (categories and fields)
+     *
+     * To be used in implementations of core_user_data_provider::get_contexts_for_userid in cases when user is
+     * an owner of the fields configuration
+     * Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
+     *
+     * @param string $component
+     * @param string $area
+     * @param string $itemidstest subquery for selecting customfield_category->itemid
+     * @param array $params array of named parameters for itemidstest subquery
+     * @return contextlist
+     */
+    public static function get_customfields_configuration_contexts(string $component, string $area,
+            string $itemidstest = 'IS NOT NULL', array $params = []) : contextlist {
+
+        $sql = "SELECT c.contextid FROM {customfield_category} c
+            WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
+        $params['component'] = $component;
+        $params['area'] = $area;
+
+        $contextlist = new contextlist();
+        $contextlist->add_from_sql($sql, self::get_params($component, $area, $params));
+
+        return $contextlist;
+
+    }
+
+    /**
+     * Exports customfields data
+     *
+     * To be used in implementations of core_user_data_provider::export_user_data
+     * Caller needs to transfer the $userid to the select subqueries for
+     * customfield_category->itemid and/or customfield_data->instanceid
+     *
+     * @param approved_contextlist $contextlist
+     * @param string $component
+     * @param string $area
+     * @param string $itemidstest subquery for selecting customfield_category->itemid
+     * @param string $instanceidstest subquery for selecting customfield_data->instanceid
+     * @param array $params array of named parameters for itemidstest and instanceidstest subqueries
+     * @param array $subcontext subcontext to use in context_writer::export_data, if null (default) the
+     *     "Custom fields data" will be used;
+     *     the data id will be appended to the subcontext array.
+     */
+    public static function export_customfields_data(approved_contextlist $contextlist, string $component, string $area,
+                string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = [],
+                array $subcontext = null) {
+        global $DB;
+
+        // This query is very similar to api::get_instances_fields_data() but also works for multiple itemids
+        // and has a context filter.
+        list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
+        $sql = "SELECT d.*, f.type AS fieldtype, f.name as fieldname, f.shortname as fieldshortname, c.itemid
+            FROM {customfield_category} c
+            JOIN {customfield_field} f ON f.categoryid = c.id
+            JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
+            WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest
+            ORDER BY c.itemid, c.sortorder, f.sortorder";
+        $params = self::get_params($component, $area, $params) + $contextparams;
+        $records = $DB->get_recordset_sql($sql, $params);
+
+        if ($subcontext === null) {
+            $subcontext = [get_string('customfielddata', 'core_customfield')];
+        }
+
+        /** @var handler $handler */
+        $handler = null;
+        $fields = null;
+        foreach ($records as $record) {
+            if (!$handler || $handler->get_itemid() != $record->itemid) {
+                $handler = handler::get_handler($component, $area, $record->itemid);
+                $fields = $handler->get_fields();
+            }
+            $field = (object)['type' => $record->fieldtype, 'shortname' => $record->fieldshortname, 'name' => $record->fieldname];
+            unset($record->itemid, $record->fieldtype, $record->fieldshortname, $record->fieldname);
+            try {
+                $field = array_key_exists($record->fieldid, $fields) ? $fields[$record->fieldid] : null;
+                $data = data_controller::create(0, $record, $field);
+                self::export_customfield_data($data, array_merge($subcontext, [$record->id]));
+            } catch (Exception $e) {
+                // We store some data that we can not initialise controller for. We still need to export it.
+                self::export_customfield_data_unknown($record, $field, array_merge($subcontext, [$record->id]));
+            }
+        }
+        $records->close();
+    }
+
+    /**
+     * Deletes customfields data
+     *
+     * To be used in implementations of core_user_data_provider::delete_data_for_user
+     * Caller needs to transfer the $userid to the select subqueries for
+     * customfield_category->itemid and/or customfield_data->instanceid
+     *
+     * @param approved_contextlist $contextlist
+     * @param string $component
+     * @param string $area
+     * @param string $itemidstest subquery for selecting customfield_category->itemid
+     * @param string $instanceidstest subquery for selecting customfield_data->instanceid
+     * @param array $params array of named parameters for itemidstest and instanceidstest subqueries
+     */
+    public static function delete_customfields_data(approved_contextlist $contextlist, string $component, string $area,
+            string $itemidstest = 'IS NOT NULL', string $instanceidstest = 'IS NOT NULL', array $params = []) {
+        global $DB;
+
+        list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
+        $sql = "SELECT d.id
+            FROM {customfield_category} c
+            JOIN {customfield_field} f ON f.categoryid = c.id
+            JOIN {customfield_data} d ON d.fieldid = f.id AND d.instanceid $instanceidstest AND d.contextid $contextidstest
+            WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest";
+        $params = self::get_params($component, $area, $params) + $contextparams;
+
+        self::before_delete_data('IN (' . $sql . ') ', $params);
+
+        $DB->execute("DELETE FROM {customfield_data}
+            WHERE instanceid $instanceidstest
+            AND contextid $contextidstest
+            AND fieldid IN (SELECT f.id
+                FROM {customfield_category} c
+                JOIN {customfield_field} f ON f.categoryid = c.id
+                WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest)", $params);
+    }
+
+    /**
+     * Deletes customfields configuration (categories and fields) and all relevant data
+     *
+     * To be used in implementations of core_user_data_provider::delete_data_for_user in cases when user is
+     * an owner of the fields configuration and it is considered user information (quite unlikely situtation but we never
+     * know what customfields API can be used for)
+     *
+     * Caller needs to transfer the $userid to the select subquery for customfield_category->itemid
+     *
+     * @param approved_contextlist $contextlist
+     * @param string $component
+     * @param string $area
+     * @param string $itemidstest subquery for selecting customfield_category->itemid
+     * @param array $params array of named parameters for itemidstest subquery
+     */
+    public static function delete_customfields_configuration(approved_contextlist $contextlist, string $component, string $area,
+            string $itemidstest = 'IS NOT NULL', array $params = []) {
+        global $DB;
+
+        list($contextidstest, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED, 'cfctx');
+        $params = self::get_params($component, $area, $params) + $contextparams;
+
+        $categoriesids = $DB->get_fieldset_sql("SELECT c.id
+            FROM {customfield_category} c
+            WHERE c.component = :cfcomponent AND c.area = :cfarea AND c.itemid $itemidstest AND c.contextid $contextidstest",
+            $params);
+
+        self::delete_categories($contextlist->get_contextids(), $categoriesids);
+    }
+
+    /**
+     * Deletes all customfields configuration (categories and fields) and all relevant data for the given category context
+     *
+     * To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
+     *
+     * @param string $component
+     * @param string $area
+     * @param \context $context
+     */
+    public static function delete_customfields_configuration_for_context(string $component, string $area, \context $context) {
+        global $DB;
+        $categoriesids = $DB->get_fieldset_sql("SELECT c.id
+            FROM {customfield_category} c
+            JOIN {context} ctx ON ctx.id = c.contextid AND ctx.path LIKE :ctxpath
+            WHERE c.component = :cfcomponent AND c.area = :cfarea",
+            self::get_params($component, $area, ['ctxpath' => $context->path]));
+
+        self::delete_categories([$context->id], $categoriesids);
+    }
+
+    /**
+     * Deletes all customfields data for the given context
+     *
+     * To be used in implementations of core_user_data_provider::delete_data_for_all_users_in_context
+     *
+     * @param string $component
+     * @param string $area
+     * @param \context $context
+     */
+    public static function delete_customfields_data_for_context(string $component, string $area, \context $context) {
+        global $DB;
+
+        $sql = "SELECT d.id
+            FROM {customfield_category} c
+            JOIN {customfield_field} f ON f.categoryid = c.id
+            JOIN {customfield_data} d ON d.fieldid = f.id
+            JOIN {context} ctx ON ctx.id = d.contextid AND ctx.path LIKE :ctxpath
+            WHERE c.component = :cfcomponent AND c.area = :cfarea";
+        $params = self::get_params($component, $area, ['ctxpath' => $context->path . '%']);
+
+        self::before_delete_data('IN (' . $sql . ') ', $params);
+
+        $DB->execute("DELETE FROM {customfield_data}
+            WHERE fieldid IN (SELECT f.id
+                FROM {customfield_category} c
+                JOIN {customfield_field} f ON f.categoryid = c.id
+                WHERE c.component = :cfcomponent AND c.area = :cfarea)
+            AND contextid IN (SELECT id FROM {context} WHERE path LIKE :ctxpath)",
+            $params);
+    }
+
+    /**
+     * Checks that $params is an associative array and adds parameters for component and area
+     *
+     * @param string $component
+     * @param string $area
+     * @param array $params
+     * @return array
+     * @throws \coding_exception
+     */
+    protected static function get_params(string $component, string $area, array $params): array {
+        if (!empty($params) && (array_keys($params) === range(0, count($params) - 1))) {
+            // Argument $params is not an associative array.
+            throw new \coding_exception('Argument $params must be an associative array!');
+        }
+        return $params + ['cfcomponent' => $component, 'cfarea' => $area];
+    }
+
+    /**
+     * Delete custom fields categories configurations, all their fields and data
+     *
+     * @param array $contextids
+     * @param array $categoriesids
+     */
+    protected static function delete_categories(array $contextids, array $categoriesids) {
+        global $DB;
+
+        if (!$categoriesids) {
+            return;
+        }
+
+        list($categoryidstest, $catparams) = $DB->get_in_or_equal($categoriesids, SQL_PARAMS_NAMED, 'cfcat');
+        $datasql = "SELECT d.id FROM {customfield_data} d JOIN {customfield_field} f ON f.id = d.fieldid " .
+            "WHERE f.categoryid $categoryidstest";
+        $fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
+
+        self::before_delete_data("IN ($datasql)", $catparams);
+        self::before_delete_fields($categoryidstest, $catparams);
+
+        $DB->execute('DELETE FROM {customfield_data} WHERE fieldid IN (' . $fieldsql . ')', $catparams);
+        $DB->execute("DELETE FROM {customfield_field} WHERE categoryid $categoryidstest", $catparams);
+        $DB->execute("DELETE FROM {customfield_category} WHERE id $categoryidstest", $catparams);
+
+    }
+
+    /**
+     * Executes callbacks from the customfield plugins to delete anything related to the data records (usually files)
+     *
+     * @param string $dataidstest
+     * @param array $params
+     */
+    protected static function before_delete_data(string $dataidstest, array $params) {
+        global $DB;
+        // Find all field types and all contexts for each field type.
+        $records = $DB->get_recordset_sql("SELECT ff.type, dd.contextid
+            FROM {customfield_data} dd
+            JOIN {customfield_field} ff ON ff.id = dd.fieldid
+            WHERE dd.id $dataidstest
+            GROUP BY ff.type, dd.contextid",
+            $params);
+
+        $fieldtypes = [];
+        foreach ($records as $record) {
+            $fieldtypes += [$record->type => []];
+            $fieldtypes[$record->type][] = $record->contextid;
+        }
+        $records->close();
+
+        // Call plugin callbacks to delete data customfield_provider::before_delete_data().
+        foreach ($fieldtypes as $fieldtype => $contextids) {
+            $classname = manager::get_provider_classname_for_component('customfield_' . $fieldtype);
+            if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
+                component_class_callback($classname, 'before_delete_data', [$dataidstest, $params, $contextids]);
+            }
+        }
+    }
+
+    /**
+     * Executes callbacks from the plugins to delete anything related to the fields (usually files)
+     *
+     * Also deletes description files
+     *
+     * @param string $categoryidstest
+     * @param array $params
+     */
+    protected static function before_delete_fields(string $categoryidstest, array $params) {
+        global $DB;
+        // Find all field types and contexts.
+        $fieldsql = "SELECT f.id AS fieldid FROM {customfield_field} f WHERE f.categoryid $categoryidstest";
+        $records = $DB->get_recordset_sql("SELECT f.type, c.contextid
+            FROM {customfield_field} f
+            JOIN {customfield_category} c ON c.id = f.categoryid
+            WHERE c.id $categoryidstest",
+            $params);
+
+        $contexts = [];
+        $fieldtypes = [];
+        foreach ($records as $record) {
+            $contexts[$record->contextid] = $record->contextid;
+            $fieldtypes += [$record->type => []];
+            $fieldtypes[$record->type][] = $record->contextid;
+        }
+        $records->close();
+
+        // Delete description files.
+        foreach ($contexts as $contextid) {
+            get_file_storage()->delete_area_files_select($contextid, 'core_customfield', 'description',
+                " IN ($fieldsql) ", $params);
+        }
+
+        // Call plugin callbacks to delete fields customfield_provider::before_delete_fields().
+        foreach ($fieldtypes as $type => $contextids) {
+            $classname = manager::get_provider_classname_for_component('customfield_' . $type);
+            if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
+                component_class_callback($classname, 'before_delete_fields',
+                    [" IN ($fieldsql) ", $params, $contextids]);
+            }
+        }
+        $records->close();
+    }
+
+    /**
+     * Exports one instance of custom field data
+     *
+     * @param data_controller $data
+     * @param array $subcontext subcontext to pass to content_writer::export_data
+     */
+    public static function export_customfield_data(data_controller $data, array $subcontext) {
+        $context = $data->get_context();
+
+        $exportdata = $data->to_record();
+        $exportdata->fieldtype = $data->get_field()->get('type');
+        $exportdata->fieldshortname = $data->get_field()->get('shortname');
+        $exportdata->fieldname = $data->get_field()->get_formatted_name();
+        $exportdata->timecreated = \core_privacy\local\request\transform::datetime($exportdata->timecreated);
+        $exportdata->timemodified = \core_privacy\local\request\transform::datetime($exportdata->timemodified);
+        unset($exportdata->contextid);
+        // Use the "export_value" by default for the 'value' attribute, however the plugins may override it in their callback.
+        $exportdata->value = $data->export_value();
+
+        $classname = manager::get_provider_classname_for_component('customfield_' . $data->get_field()->get('type'));
+        if (class_exists($classname) && is_subclass_of($classname, customfield_provider::class)) {
+            component_class_callback($classname, 'export_customfield_data', [$data, $exportdata, $subcontext]);
+        } else {
+            // Custom field plugin does not implement customfield_provider, just export default value.
+            writer::with_context($context)->export_data($subcontext, $exportdata);
+        }
+    }
+
+    /**
+     * Export data record of unknown type when we were not able to create instance of data_controller
+     *
+     * @param \stdClass $record record from db table {customfield_data}
+     * @param \stdClass $field field record with at least fields type, shortname, name
+     * @param array $subcontext
+     */
+    protected static function export_customfield_data_unknown(\stdClass $record, \stdClass $field, array $subcontext) {
+        $context = \context::instance_by_id($record->contextid);
+
+        $record->fieldtype = $field->type;
+        $record->fieldshortname = $field->shortname;
+        $record->fieldname = format_string($field->name);
+        $record->timecreated = \core_privacy\local\request\transform::datetime($record->timecreated);
+        $record->timemodified = \core_privacy\local\request\transform::datetime($record->timemodified);
+        unset($record->contextid);
+        $record->value = format_text($record->value, $record->valueformat, ['context' => $context]);
+        writer::with_context($context)->export_data($subcontext, $record);
+    }
+}
diff --git a/customfield/edit.php b/customfield/edit.php
new file mode 100644 (file)
index 0000000..1685e7e
--- /dev/null
@@ -0,0 +1,61 @@
+<?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/>.
+
+/**
+ * Edit configuration of a custom field
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+$id         = optional_param('id', 0, PARAM_INT);
+$categoryid = optional_param('categoryid', 0, PARAM_INT);
+$type       = optional_param('type', null, PARAM_COMPONENT);
+
+if ($id) {
+    $field = \core_customfield\field_controller::create($id);
+} else if ($categoryid && $type) {
+    $category = \core_customfield\category_controller::create($categoryid);
+    $field = \core_customfield\field_controller::create(0, (object)['type' => $type], $category);
+} else {
+    print_error('fieldnotfound', 'core_customfield');
+}
+
+$handler = $field->get_handler();
+require_login();
+if (!$handler->can_configure()) {
+    print_error('nopermissionconfigure', 'core_customfield');
+}
+$title = $handler->setup_edit_page($field);
+
+$mform = $handler->get_field_config_form($field);
+if ($mform->is_cancelled()) {
+    redirect($handler->get_configuration_url());
+} else if ($data = $mform->get_data()) {
+    $handler->save_field_configuration($field, $data);
+    redirect($handler->get_configuration_url());
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading($title);
+
+$mform->display();
+
+echo $OUTPUT->footer();
diff --git a/customfield/externallib.php b/customfield/externallib.php
new file mode 100644 (file)
index 0000000..24da2bb
--- /dev/null
@@ -0,0 +1,296 @@
+<?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 interface library for customfields component
+ *
+ * @package   core_customfield
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . "/externallib.php");
+
+/**
+ * Class core_customfield_external
+ *
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_customfield_external extends external_api {
+
+    /**
+     * Parameters for delete_field
+     *
+     * @return external_function_parameters
+     */
+    public static function delete_field_parameters() {
+        return new external_function_parameters(
+                array('id' => new external_value(PARAM_INT, 'Custom field ID to delete', VALUE_REQUIRED))
+        );
+    }
+
+    /**
+     * Delete custom field function
+     *
+     * @param int $id
+     */
+    public static function delete_field($id) {
+        $params = self::validate_parameters(self::delete_field_parameters(), ['id' => $id]);
+
+        $record = \core_customfield\field_controller::create($params['id']);
+        $handler = $record->get_handler();
+        if (!$handler->can_configure()) {
+            throw new moodle_exception('nopermissionconfigure', 'core_customfield');
+        }
+        $handler->delete_field_configuration($record);
+    }
+
+    /**
+     * Return for delete_field
+     */
+    public static function delete_field_returns() {
+    }
+
+    /**
+     * Parameters for reload template function
+     *
+     * @return external_function_parameters
+     */
+    public static function reload_template_parameters() {
+        return new external_function_parameters(
+            array(
+                'component' => new external_value(PARAM_COMPONENT, 'component', VALUE_REQUIRED),
+                'area' => new external_value(PARAM_ALPHANUMEXT, 'area', VALUE_REQUIRED),
+                'itemid' => new external_value(PARAM_INT, 'itemid', VALUE_REQUIRED)
+            )
+        );
+    }
+
+    /**
+     * Reload template function
+     *
+     * @param string $component
+     * @param string $area
+     * @param int $itemid
+     * @return array|object|stdClass
+     */
+    public static function reload_template($component, $area, $itemid) {
+        global $PAGE;
+
+        $params = self::validate_parameters(self::reload_template_parameters(),
+                      ['component' => $component, 'area' => $area, 'itemid' => $itemid]);
+
+        $PAGE->set_context(context_system::instance());
+        $handler = \core_customfield\handler::get_handler($params['component'], $params['area'], $params['itemid']);
+        self::validate_context($handler->get_configuration_context());
+        if (!$handler->can_configure()) {
+            throw new moodle_exception('nopermissionconfigure', 'core_customfield');
+        }
+        $output = $PAGE->get_renderer('core_customfield');
+        $outputpage = new \core_customfield\output\management($handler);
+        return $outputpage->export_for_template($output);
+    }
+
+    /**
+     * Ajax returns on reload template.
+     *
+     * @return external_single_structure
+     */
+    public static function reload_template_returns() {
+        return new external_single_structure(
+            array(
+                'component' => new external_value(PARAM_COMPONENT, 'component'),
+                'area' => new external_value(PARAM_ALPHANUMEXT, 'area'),
+                'itemid' => new external_value(PARAM_INT, 'itemid'),
+                'usescategories' => new external_value(PARAM_INT, 'view has categories'),
+                'categories' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'id'),
+                            'nameeditable' => new external_value(PARAM_RAW, 'inplace editable name'),
+                            'addfieldmenu' => new external_value(PARAM_RAW, 'addfieldmenu'),
+                            'fields' => new external_multiple_structure(
+                                new external_single_structure(
+                                    array(
+                                        'name' => new external_value(PARAM_NOTAGS, 'name'),
+                                        'shortname' => new external_value(PARAM_NOTAGS, 'shortname'),
+                                        'type' => new external_value(PARAM_NOTAGS, 'type'),
+                                        'editfieldurl' => new external_value(PARAM_URL, 'edit field url'),
+                                        'id' => new external_value(PARAM_INT, 'id'),
+                                    )
+                                )
+                            , '', VALUE_OPTIONAL),
+                        )
+                    )
+                ),
+            )
+        );
+    }
+
+    /**
+     * Parameters for delete category
+     *
+     * @return external_function_parameters
+     */
+    public static function delete_category_parameters() {
+        return new external_function_parameters(
+                array('id' => new external_value(PARAM_INT, 'category ID to delete', VALUE_REQUIRED))
+        );
+    }
+
+    /**
+     * Delete category function
+     *
+     * @param int $id
+     */
+    public static function delete_category($id) {
+        $category = core_customfield\category_controller::create($id);
+        $handler = $category->get_handler();
+        self::validate_context($handler->get_configuration_context());
+        if (!$handler->can_configure()) {
+            throw new moodle_exception('nopermissionconfigure', 'core_customfield');
+        }
+        $handler->delete_category($category);
+    }
+
+    /**
+     * Return for delete category
+     */
+    public static function delete_category_returns() {
+    }
+
+
+    /**
+     * Parameters for create category
+     *
+     * @return external_function_parameters
+     */
+    public static function create_category_parameters() {
+        return new external_function_parameters(
+            array(
+                'component' => new external_value(PARAM_COMPONENT, 'component', VALUE_REQUIRED),
+                'area' => new external_value(PARAM_ALPHANUMEXT, 'area', VALUE_REQUIRED),
+                'itemid' => new external_value(PARAM_INT, 'itemid', VALUE_REQUIRED)
+            )
+        );
+    }
+
+    /**
+     * Create category function
+     *
+     * @param string $component
+     * @param string $area
+     * @param int    $itemid
+     * @return mixed
+     */
+    public static function create_category($component, $area, $itemid) {
+        $params = self::validate_parameters(self::create_category_parameters(),
+            ['component' => $component, 'area' => $area, 'itemid' => $itemid]);
+
+        $handler = \core_customfield\handler::get_handler($params['component'], $params['area'], $params['itemid']);
+        self::validate_context($handler->get_configuration_context());
+        if (!$handler->can_configure()) {
+            throw new moodle_exception('nopermissionconfigure', 'core_customfield');
+        }
+        return $handler->create_category();
+    }
+
+    /**
+     * Return for create category
+     */
+    public static function create_category_returns() {
+        return new external_value(PARAM_INT, 'Id of the category');
+    }
+
+    /**
+     * Parameters for move field.
+     *
+     * @return external_function_parameters
+     */
+    public static function move_field_parameters() {
+        return new external_function_parameters(
+                ['id' => new external_value(PARAM_INT, 'Id of the field to move', VALUE_REQUIRED),
+                 'categoryid' => new external_value(PARAM_INT, 'New parent category id', VALUE_REQUIRED),
+                 'beforeid'   => new external_value(PARAM_INT, 'Id of the field before which it needs to be moved',
+                     VALUE_DEFAULT, 0)]
+        );
+    }
+
+    /**
+     * Move/reorder field. Move a field to another category and/or change sortorder of fields
+     *
+     * @param int $id field id
+     * @param int $categoryid
+     * @param int $beforeid
+     */
+    public static function move_field($id, $categoryid, $beforeid) {
+        $params = self::validate_parameters(self::move_field_parameters(),
+            ['id' => $id, 'categoryid' => $categoryid, 'beforeid' => $beforeid]);
+        $field = \core_customfield\field_controller::create($params['id']);
+        $handler = $field->get_handler();
+        self::validate_context($handler->get_configuration_context());
+        if (!$handler->can_configure()) {
+            throw new moodle_exception('nopermissionconfigure', 'core_customfield');
+        }
+        $handler->move_field($field, $params['categoryid'], $params['beforeid']);
+    }
+
+    /**
+     * Return for move field
+     */
+    public static function move_field_returns() {
+    }
+
+    /**
+     * Return for move category
+     *
+     * @return external_function_parameters
+     */
+    public static function move_category_parameters() {
+        return new external_function_parameters(
+                ['id' => new external_value(PARAM_INT, 'Category ID to move', VALUE_REQUIRED),
+                 'beforeid'   => new external_value(PARAM_INT, 'Id of the category before which it needs to be moved',
+                     VALUE_DEFAULT, 0)]
+        );
+    }
+
+    /**
+     * Reorder categories. Move category to the new position
+     *
+     * @param int $id category id
+     * @param int $beforeid
+     */
+    public static function move_category(int $id, int $beforeid) {
+        $params = self::validate_parameters(self::move_category_parameters(),
+            ['id' => $id, 'beforeid' => $beforeid]);
+        $category = core_customfield\category_controller::create($id);
+        $handler = $category->get_handler();
+        self::validate_context($handler->get_configuration_context());
+        if (!$handler->can_configure()) {
+            throw new moodle_exception('nopermissionconfigure', 'core_customfield');
+        }
+        $handler->move_category($category, $params['beforeid']);
+    }
+
+    /**
+     * Return for move category
+     */
+    public static function move_category_returns() {
+    }
+}
diff --git a/customfield/field/checkbox/classes/field_controller.php b/customfield/field/checkbox/classes/field_controller.php
new file mode 100644 (file)
index 0000000..d01df95
--- /dev/null
@@ -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/>.
+
+/**
+ * Customfields checkbox plugin
+ *
+ * @package   customfield_checkbox
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace customfield_checkbox;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class field
+ *
+ * @package customfield_checkbox
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_controller  extends \core_customfield\field_controller {
+    /**
+     * Plugin type
+     */
+    const TYPE = 'checkbox';
+
+    /**
+     * Add fields for editing a checkbox field.
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function config_form_definition(\MoodleQuickForm $mform) {
+        $mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_checkbox'));
+        $mform->setExpanded('header_specificsettings', true);
+
+        $mform->addElement('selectyesno', 'configdata[checkbydefault]', get_string('checkedbydefault', 'customfield_checkbox'));
+        $mform->setType('configdata[checkbydefault]', PARAM_BOOL);
+    }
+
+    /**
+     * Validate the data on the field configuration form
+     *
+     * @param array $data from the add/edit profile field form
+     * @param array $files
+     * @return array associative array of error messages
+     */
+    public function config_form_validation(array $data, $files = array()): array {
+        $errors = parent::config_form_validation($data, $files);
+
+        if ($data['configdata']['uniquevalues']) {
+            $errors['configdata[uniquevalues]'] = get_string('errorconfigunique', 'customfield_checkbox');
+        }
+
+        return $errors;
+    }
+}
diff --git a/customfield/field/date/classes/field_controller.php b/customfield/field/date/classes/field_controller.php
new file mode 100644 (file)
index 0000000..213fe8d
--- /dev/null
@@ -0,0 +1,86 @@
+<?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/>.
+
+/**
+ * Customfield date plugin
+ *
+ * @package   customfield_date
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace customfield_date;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class field
+ *
+ * @package customfield_date
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_controller extends \core_customfield\field_controller {
+    /**
+     * Type of plugin data
+     */
+    const TYPE = 'date';
+
+    /**
+     * Validate the data from the config form.
+     *
+     * @param array $data
+     * @param array $files
+     * @return array associative array of error messages
+     */
+    public function config_form_validation(array $data, $files = array()) : array {
+        $errors = array();
+
+        // Make sure the start year is not greater than the end year.
+        if (!empty($data['configdata']['mindate']) && !empty($data['configdata']['maxdate']) &&
+                $data['configdata']['mindate'] > $data['configdata']['maxdate']) {
+            $errors['configdata[mindate]'] = get_string('mindateaftermax', 'customfield_date');
+        }
+
+        return $errors;
+    }
+
+    /**
+     * Add fields for editing a date field.
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function config_form_definition(\MoodleQuickForm $mform) {
+        $config = $this->get('configdata');
+
+        // Add elements.
+        $mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_date'));
+        $mform->setExpanded('header_specificsettings', true);
+
+        $mform->addElement('advcheckbox', 'configdata[includetime]', get_string('includetime', 'customfield_date'));
+
+        $mform->addElement('date_time_selector', 'configdata[mindate]', get_string('mindate', 'customfield_date'),
+            ['optional' => true]);
+
+        $mform->addElement('date_time_selector', 'configdata[maxdate]', get_string('maxdate', 'customfield_date'),
+            ['optional' => true]);
+
+        $mform->hideIf('configdata[maxdate][hour]', 'configdata[includetime]');
+        $mform->hideIf('configdata[maxdate][minute]', 'configdata[includetime]');
+        $mform->hideIf('configdata[mindate][hour]', 'configdata[includetime]');
+        $mform->hideIf('configdata[mindate][minute]', 'configdata[includetime]');
+    }
+}
diff --git a/customfield/field/date/lang/en/customfield_date.php b/customfield/field/date/lang/en/customfield_date.php
new file mode 100644 (file)
index 0000000..27d0582
--- /dev/null
@@ -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/>.
+
+/**
+ * Customfields date plugin
+ *
+ * @package   customfield_date
+ * @copyright 2018 David Matamoros
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['errormaxdate'] = 'Please enter date no later than {$a}';
+$string['errormindate'] = 'Please enter date on or after {$a}';
+$string['includetime'] = 'Include time';
+$string['maxdate'] = 'Maximum value';
+$string['mindate'] = 'Minimum value';
+$string['mindateaftermax'] = 'The minimum value can not be bigger than the maximum value';
+$string['pluginname'] = 'Date and time';
+$string['privacy:metadata'] = 'Date and time field type plugin does not store any personal data, it uses tables defined in core';
+$string['specificsettings'] = 'Settings for the date and time field';
diff --git a/customfield/field/select/classes/field_controller.php b/customfield/field/select/classes/field_controller.php
new file mode 100644 (file)
index 0000000..c0d2dd9
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class field
+ *
+ * @package   customfield_select
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace customfield_select;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class field
+ *
+ * @package customfield_select
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_controller extends \core_customfield\field_controller {
+    /**
+     * Customfield type
+     */
+    const TYPE = 'select';
+
+    /**
+     * Add fields for editing a select field.
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function config_form_definition(\MoodleQuickForm $mform) {
+        $mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_select'));
+        $mform->setExpanded('header_specificsettings', true);
+
+        $mform->addElement('textarea', 'configdata[options]', get_string('menuoptions', 'customfield_select'));
+        $mform->setType('configdata[options]', PARAM_TEXT);
+
+        $mform->addElement('text', 'configdata[defaultvalue]', get_string('defaultvalue', 'core_customfield'), 'size="50"');
+        $mform->setType('configdata[defaultvalue]', PARAM_TEXT);
+    }
+
+    /**
+     * Returns the options available as an array.
+     *
+     * @param \core_customfield\field_controller $field
+     * @return array
+     */
+    public static function get_options_array(\core_customfield\field_controller $field): array {
+        if ($field->get_configdata_property('options')) {
+            $options = preg_split("/\s*\n\s*/", trim($field->get_configdata_property('options')));
+        } else {
+            $options = array();
+        }
+        return array_merge([''], $options);
+    }
+
+    /**
+     * Validate the data from the config form.
+     * Sub classes must reimplement it.
+     *
+     * @param array $data from the add/edit profile field form
+     * @param array $files
+     * @return array associative array of error messages
+     */
+    public function config_form_validation(array $data, $files = array()): array {
+        $options = preg_split("/\s*\n\s*/", trim($data['configdata']['options']));
+        $errors = [];
+        if (!$options || count($options) < 2) {
+            $errors['configdata[options]'] = get_string('errornotenoughoptions', 'customfield_select');
+        } else if (!empty($data['configdata']['defaultvalue'])) {
+            $defaultkey = array_search($data['configdata']['defaultvalue'], $options);
+            if ($defaultkey === false) {
+                $errors['configdata[defaultvalue]'] = get_string('errordefaultvaluenotinlist', 'customfield_select');
+            }
+        }
+        return $errors;
+    }
+}
diff --git a/customfield/field/text/classes/field_controller.php b/customfield/field/text/classes/field_controller.php
new file mode 100644 (file)
index 0000000..2c03b6c
--- /dev/null
@@ -0,0 +1,121 @@
+<?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/>.
+
+/**
+ * Customfields text plugin
+ *
+ * @package   customfield_text
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace customfield_text;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class field
+ *
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package customfield_text
+ */
+class field_controller extends \core_customfield\field_controller {
+    /**
+     * Plugin type text
+     */
+    const TYPE = 'text';
+
+    /**
+     * Add fields for editing a text field.
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function config_form_definition(\MoodleQuickForm $mform) {
+
+        $mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_text'));
+        $mform->setExpanded('header_specificsettings', true);
+
+        $mform->addElement('text', 'configdata[defaultvalue]', get_string('defaultvalue', 'core_customfield'),
+            ['size' => 50]);
+        $mform->setType('configdata[defaultvalue]', PARAM_TEXT);
+
+        $mform->addElement('text', 'configdata[displaysize]', get_string('displaysize', 'customfield_text'), ['size' => 6]);
+        $mform->setType('configdata[displaysize]', PARAM_INT);
+        $mform->setDefault('configdata[displaysize]', 50);
+        $mform->addRule('configdata[displaysize]', null, 'numeric', null, 'client');
+
+        $mform->addElement('text', 'configdata[maxlength]', get_string('maxlength', 'customfield_text'), ['size' => 6]);
+        $mform->setType('configdata[maxlength]', PARAM_INT);
+        $mform->setDefault('configdata[maxlength]', 1333);
+        $mform->addRule('configdata[maxlength]', null, 'numeric', null, 'client');
+
+        $mform->addElement('selectyesno', 'configdata[ispassword]', get_string('ispassword', 'customfield_text'));
+        $mform->setType('configdata[ispassword]', PARAM_INT);
+
+        $mform->addElement('text', 'configdata[link]', get_string('islink', 'customfield_text'), ['size' => 50]);
+        $mform->setType('configdata[link]', PARAM_RAW_TRIMMED);
+        $mform->addHelpButton('configdata[link]', 'islink', 'customfield_text');
+
+        $mform->disabledIf('configdata[link]', 'configdata[ispassword]', 'eq', 1);
+
+        $linkstargetoptions = array(
+            ''       => get_string('none', 'customfield_text'),
+            '_blank' => get_string('newwindow', 'customfield_text'),
+            '_self'  => get_string('sameframe', 'customfield_text'),
+            '_top'   => get_string('samewindow', 'customfield_text')
+        );
+        $mform->addElement('select', 'configdata[linktarget]', get_string('linktarget', 'customfield_text'),
+            $linkstargetoptions);
+
+        $mform->disabledIf('configdata[linktarget]', 'configdata[link]', 'eq', '');
+    }
+
+    /**
+     * Validate the data on the field configuration form
+     *
+     * @param array $data from the add/edit profile field form
+     * @param array $files
+     * @return array associative array of error messages
+     */
+    public function config_form_validation(array $data, $files = array()): array {
+        global $CFG;
+        $errors = parent::config_form_validation($data, $files);
+
+        $maxlength = (int)$data['configdata']['maxlength'];
+        if ($maxlength < 1 || $maxlength > 1333) {
+            $errors['configdata[maxlength]'] = get_string('errorconfigmaxlen', 'customfield_text');
+        }
+
+        $displaysize = (int)$data['configdata']['displaysize'];
+        if ($displaysize < 1 || $displaysize > 200) {
+            $errors['configdata[displaysize]'] = get_string('errorconfigdisplaysize', 'customfield_text');
+        }
+
+        $link = $data['configdata']['link'];
+        if (strlen($link)) {
+            require_once($CFG->dirroot . '/lib/validateurlsyntax.php');
+            if (strpos($link, '$$') === false) {
+                $errors['configdata[link]'] = get_string('errorconfiglinkplaceholder', 'customfield_text');
+            } else if (!validateUrlSyntax(str_replace('$$', 'XYZ', $link), 's+H?S?F-E-u-P-a?I?p?f?q?r?')) {
+                // This validation is more strict than PARAM_URL - it requires the protocol and it must be either http or https.
+                $errors['configdata[link]'] = get_string('errorconfigdisplaysize', 'customfield_text');
+            }
+        }
+
+        return $errors;
+    }
+}
diff --git a/customfield/field/textarea/classes/field_controller.php b/customfield/field/textarea/classes/field_controller.php
new file mode 100644 (file)
index 0000000..e53d88b
--- /dev/null
@@ -0,0 +1,145 @@
+<?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/>.
+
+/**
+ * Customfield textarea plugin
+ *
+ * @package   customfield_textarea
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace customfield_textarea;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class field
+ *
+ * @package customfield_textarea
+ * @copyright 2018 David Matamoros <davidmc@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class field_controller extends \core_customfield\field_controller {
+    /**
+     * Const type
+     */
+    const TYPE = 'textarea';
+
+    /**
+     * Before delete bulk actions
+     */
+    public function delete(): bool {
+        global $DB;
+        $fs = get_file_storage();
+
+        // Delete files in the defaultvalue.
+        $fs->delete_area_files($this->get_handler()->get_configuration_context()->id, 'customfield_textarea',
+            'defaultvalue', $this->get('id'));
+
+        // Delete files in the data. We can not use $fs->delete_area_files_select() because context may be different.
+        $params = ['component' => 'customfield_textarea', 'filearea' => 'value', 'fieldid' => $this->get('id')];
+        $where = "component = :component AND filearea = :filearea
+                AND itemid IN (SELECT cfd.id FROM {customfield_data} cfd WHERE cfd.fieldid = :fieldid)";
+        $filerecords = $DB->get_recordset_select('files', $where, $params);
+        foreach ($filerecords as $filerecord) {
+            $fs->get_file_instance($filerecord)->delete();
+        }
+        $filerecords->close();
+
+        // Delete data and field.
+        return parent::delete();
+    }
+
+    /**
+     * Prepare the field data to set in the configuration form
+     *
+     * Necessary if some preprocessing required for editor or filemanager fields
+     *
+     * @param \stdClass $formdata
+     */
+    public function prepare_for_config_form(\stdClass $formdata) {
+
+        if (!empty($formdata->configdata['defaultvalue'])) {
+            $textoptions = $this->value_editor_options();
+            $context = $textoptions['context'];
+
+            $record = new \stdClass();
+            $record->defaultvalue = $formdata->configdata['defaultvalue'];
+            $record->defaultvalueformat = $formdata->configdata['defaultvalueformat'];
+            file_prepare_standard_editor($record, 'defaultvalue', $textoptions, $context,
+                'customfield_textarea', 'defaultvalue', $formdata->id);
+            $formdata->configdata['defaultvalue_editor'] = $record->defaultvalue_editor;
+        }
+    }
+
+    /**
+     * Add fields for editing a textarea field.
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function config_form_definition(\MoodleQuickForm $mform) {
+        $mform->addElement('header', 'header_specificsettings', get_string('specificsettings', 'customfield_textarea'));
+        $mform->setExpanded('header_specificsettings', true);
+
+        $desceditoroptions = $this->value_editor_options();
+
+        $mform->addElement('editor', 'configdata[defaultvalue_editor]', get_string('defaultvalue', 'core_customfield'),
+            null, $desceditoroptions);
+    }
+
+    /**
+     * Options for editor
+     *
+     * @param \context|null $context context if known, otherwise configuration context will be used
+     * @return array
+     */
+    public function value_editor_options(\context $context = null) {
+        global $CFG;
+        require_once($CFG->libdir.'/formslib.php');
+        if (!$context) {
+            $context = $this->get_handler()->get_configuration_context();
+        }
+        return ['maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $CFG->maxbytes, 'context' => $context];
+    }
+
+    /**
+     * Saves the field configuration
+     */
+    public function save() {
+        $configdata = $this->get('configdata');
+        if (!array_key_exists('defaultvalue_editor', $configdata)) {
+            $this->field->save();
+            return;
+        }
+
+        if (!$this->get('id')) {
+            $this->field->save();
+        }
+
+        // Store files.
+        $textoptions = $this->value_editor_options();
+        $tempvalue = (object) ['defaultvalue_editor' => $configdata['defaultvalue_editor']];
+        $tempvalue = file_postupdate_standard_editor($tempvalue, 'defaultvalue', $textoptions, $textoptions['context'],
+            'customfield_textarea', 'defaultvalue', $this->get('id'));
+
+        $configdata['defaultvalue'] = $tempvalue->defaultvalue;
+        $configdata['defaultvalueformat'] = $tempvalue->defaultvalueformat;
+        unset($configdata['defaultvalue_editor']);
+        $this->field->set('configdata', json_encode($configdata));
+        $this->field->save();
+    }
+}
diff --git a/customfield/field/textarea/version.php b/customfield/field/textarea/version.php
new file mode 100644 (file)
index 0000000..6d2e8c7
--- /dev/null
@@ -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/>.
+
+/**
+ * Customfield text area plugin
+ *
+ * @package   customfield_textarea
+ * @copyright 2018 David Matamoros <toni@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->component = 'customfield_textarea';
+$plugin->version   = 2018120300;
+$plugin->requires  = 2018122000;