���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/api.php.tar
���ѧ٧ѧ�
home3/cpr76684/public_html/Aem/customfield/classes/api.php 0000644 00000043113 15152001244 0017217 0 ustar 00 <?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; } /** * Get a list of the course custom fields that support course grouping in * block_myoverview * @return array $shortname => $name */ public static function get_fields_supporting_course_grouping() { global $DB; $sql = " SELECT f.* FROM {customfield_field} f JOIN {customfield_category} cat ON cat.id = f.categoryid WHERE cat.component = 'core_course' AND cat.area = 'course' ORDER BY f.name "; $ret = []; $fields = $DB->get_records_sql($sql); foreach ($fields as $field) { $inst = field_controller::create(0, $field); $isvisible = $inst->get_configdata_property('visibility') == \core_course\customfield\course_handler::VISIBLETOALL; // Only visible fields to everybody supporting course grouping will be displayed. if ($inst->supports_course_grouping() && $isvisible) { $ret[$inst->get('shortname')] = $inst->get('name'); } } return $ret; } } home3/cpr76684/public_html/Aem/lib/classes/oauth2/api.php 0000644 00000052311 15152114233 0016654 0 ustar 00 <?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 for loading/storing oauth2 endpoints from the DB. * * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\oauth2; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/filelib.php'); use stdClass; use moodle_url; use context_system; use moodle_exception; /** * Static list of api methods for system oauth2 configuration. * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * Initializes a record for one of the standard issuers to be displayed in the settings. * The issuer is not yet created in the database. * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1 * @return \core\oauth2\issuer */ public static function init_standard_issuer($type) { require_capability('moodle/site:config', context_system::instance()); $classname = self::get_service_classname($type); if (class_exists($classname)) { return $classname::init(); } throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); } /** * Create endpoints for standard issuers, based on the issuer created from submitted data. * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1 * @param issuer $issuer issuer the endpoints should be created for. * @return \core\oauth2\issuer */ public static function create_endpoints_for_standard_issuer($type, $issuer) { require_capability('moodle/site:config', context_system::instance()); $classname = self::get_service_classname($type); if (class_exists($classname)) { $classname::create_endpoints($issuer); return $issuer; } throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); } /** * Create one of the standard issuers. * * @param string $type One of google, facebook, microsoft, nextcloud or imsobv2p1 * @param string|false $baseurl Baseurl (only required for nextcloud and imsobv2p1) * @return \core\oauth2\issuer */ public static function create_standard_issuer($type, $baseurl = false) { require_capability('moodle/site:config', context_system::instance()); switch ($type) { case 'imsobv2p1': if (!$baseurl) { throw new moodle_exception('IMS OBv2.1 service type requires the baseurl parameter.'); } case 'nextcloud': if (!$baseurl) { throw new moodle_exception('Nextcloud service type requires the baseurl parameter.'); } case 'google': case 'facebook': case 'microsoft': $classname = self::get_service_classname($type); $issuer = $classname::init(); if ($baseurl) { $issuer->set('baseurl', $baseurl); } $issuer->create(); return self::create_endpoints_for_standard_issuer($type, $issuer); } throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); } /** * List all the issuers, ordered by the sortorder field * * @param bool $includeloginonly also include issuers that are configured to be shown only on login page, * By default false, in this case the method returns all issuers that can be used in services * @return \core\oauth2\issuer[] */ public static function get_all_issuers(bool $includeloginonly = false) { if ($includeloginonly) { return issuer::get_records([], 'sortorder'); } else { return array_values(issuer::get_records_select('showonloginpage<>?', [issuer::LOGINONLY], 'sortorder')); } } /** * Get a single issuer by id. * * @param int $id * @return \core\oauth2\issuer */ public static function get_issuer($id) { return new issuer($id); } /** * Get a single endpoint by id. * * @param int $id * @return \core\oauth2\endpoint */ public static function get_endpoint($id) { return new endpoint($id); } /** * Get a single user field mapping by id. * * @param int $id * @return \core\oauth2\user_field_mapping */ public static function get_user_field_mapping($id) { return new user_field_mapping($id); } /** * Get the system account for an installed OAuth service. * Never ever ever expose this to a webservice because it contains the refresh token which grants API access. * * @param \core\oauth2\issuer $issuer * @return system_account|false */ public static function get_system_account(issuer $issuer) { return system_account::get_record(['issuerid' => $issuer->get('id')]); } /** * Get the full list of system scopes required by an oauth issuer. * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins. * * @param \core\oauth2\issuer $issuer * @return string */ public static function get_system_scopes_for_issuer($issuer) { $scopes = $issuer->get('loginscopesoffline'); $pluginsfunction = get_plugins_with_function('oauth2_system_scopes', 'lib.php'); foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { // Get additional scopes from the plugin. $pluginscopes = $pluginfunction($issuer); if (empty($pluginscopes)) { continue; } // Merge the additional scopes with the existing ones. $additionalscopes = explode(' ', $pluginscopes); foreach ($additionalscopes as $scope) { if (!empty($scope)) { if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { $scopes .= ' ' . $scope; } } } } } return $scopes; } /** * Get an authenticated oauth2 client using the system account. * This call uses the refresh token to get an access token. * * @param \core\oauth2\issuer $issuer * @return \core\oauth2\client|false An authenticated client (or false if the token could not be upgraded) * @throws moodle_exception Request for token upgrade failed for technical reasons */ public static function get_system_oauth_client(issuer $issuer) { $systemaccount = self::get_system_account($issuer); if (empty($systemaccount)) { return false; } // Get all the scopes! $scopes = self::get_system_scopes_for_issuer($issuer); $class = self::get_client_classname($issuer->get('servicetype')); $client = new $class($issuer, null, $scopes, true); if (!$client->is_logged_in()) { if (!$client->upgrade_refresh_token($systemaccount)) { return false; } } return $client; } /** * Get an authenticated oauth2 client using the current user account. * This call does the redirect dance back to the current page after authentication. * * @param \core\oauth2\issuer $issuer The desired OAuth issuer * @param moodle_url $currenturl The url to the current page. * @param string $additionalscopes The additional scopes required for authorization. * @param bool $autorefresh Should the client support the use of refresh tokens to persist access across sessions. * @return \core\oauth2\client */ public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '', $autorefresh = false) { $class = self::get_client_classname($issuer->get('servicetype')); $client = new $class($issuer, $currenturl, $additionalscopes, false, $autorefresh); return $client; } /** * Get the client classname for an issuer. * * @param string $type The OAuth issuer type (google, facebook...). * @return string The classname for the custom client or core client class if the class for the defined type * doesn't exist or null type is defined. */ protected static function get_client_classname(?string $type): string { // Default core client class. $classname = 'core\\oauth2\\client'; if (!empty($type)) { $typeclassname = 'core\\oauth2\\client\\' . $type; if (class_exists($typeclassname)) { $classname = $typeclassname; } } return $classname; } /** * Get the list of defined endpoints for this OAuth issuer * * @param \core\oauth2\issuer $issuer The desired OAuth issuer * @return \core\oauth2\endpoint[] */ public static function get_endpoints(issuer $issuer) { return endpoint::get_records(['issuerid' => $issuer->get('id')]); } /** * Get the list of defined mapping from OAuth user fields to moodle user fields. * * @param \core\oauth2\issuer $issuer The desired OAuth issuer * @return \core\oauth2\user_field_mapping[] */ public static function get_user_field_mappings(issuer $issuer) { return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]); } /** * Guess an image from the discovery URL. * * @param \core\oauth2\issuer $issuer The desired OAuth issuer */ protected static function guess_image($issuer) { if (empty($issuer->get('image')) && !empty($issuer->get('baseurl'))) { $baseurl = parse_url($issuer->get('baseurl')); $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico'; $issuer->set('image', $imageurl); $issuer->update(); } } /** * Take the data from the mform and update the issuer. * * @param stdClass $data * @return \core\oauth2\issuer */ public static function update_issuer($data) { return self::create_or_update_issuer($data, false); } /** * Take the data from the mform and create the issuer. * * @param stdClass $data * @return \core\oauth2\issuer */ public static function create_issuer($data) { return self::create_or_update_issuer($data, true); } /** * Take the data from the mform and create or update the issuer. * * @param stdClass $data Form data for them issuer to be created/updated. * @param bool $create If true, the issuer will be created; otherwise, it will be updated. * @return issuer The created/updated issuer. */ protected static function create_or_update_issuer($data, bool $create): issuer { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($data->id ?? 0, $data); // Will throw exceptions on validation failures. if ($create) { $issuer->create(); // Perform service discovery. $classname = self::get_service_classname($issuer->get('servicetype')); $classname::discover_endpoints($issuer); self::guess_image($issuer); } else { $issuer->update(); } return $issuer; } /** * Get the service classname for an issuer. * * @param string $type The OAuth issuer type (google, facebook...). * * @return string The classname for this issuer or "Custom" service class if the class for the defined type doesn't exist * or null type is defined. */ protected static function get_service_classname(?string $type): string { // Default custom service class. $classname = 'core\\oauth2\\service\\custom'; if (!empty($type)) { $typeclassname = 'core\\oauth2\\service\\' . $type; if (class_exists($typeclassname)) { $classname = $typeclassname; } } return $classname; } /** * Take the data from the mform and update the endpoint. * * @param stdClass $data * @return \core\oauth2\endpoint */ public static function update_endpoint($data) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint(0, $data); // Will throw exceptions on validation failures. $endpoint->update(); return $endpoint; } /** * Take the data from the mform and create the endpoint. * * @param stdClass $data * @return \core\oauth2\endpoint */ public static function create_endpoint($data) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint(0, $data); // Will throw exceptions on validation failures. $endpoint->create(); return $endpoint; } /** * Take the data from the mform and update the user field mapping. * * @param stdClass $data * @return \core\oauth2\user_field_mapping */ public static function update_user_field_mapping($data) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping(0, $data); // Will throw exceptions on validation failures. $userfieldmapping->update(); return $userfieldmapping; } /** * Take the data from the mform and create the user field mapping. * * @param stdClass $data * @return \core\oauth2\user_field_mapping */ public static function create_user_field_mapping($data) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping(0, $data); // Will throw exceptions on validation failures. $userfieldmapping->create(); return $userfieldmapping; } /** * Reorder this identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to move. * @return boolean */ public static function move_up_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $current = new issuer($id); $sortorder = $current->get('sortorder'); if ($sortorder == 0) { return false; } $sortorder = $sortorder - 1; $current->set('sortorder', $sortorder); $filters = array('sortorder' => $sortorder); $children = issuer::get_records($filters, 'id'); foreach ($children as $needtoswap) { $needtoswap->set('sortorder', $sortorder + 1); $needtoswap->update(); } // OK - all set. $result = $current->update(); return $result; } /** * Reorder this identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to move. * @return boolean */ public static function move_down_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $current = new issuer($id); $max = issuer::count_records(); if ($max > 0) { $max--; } $sortorder = $current->get('sortorder'); if ($sortorder >= $max) { return false; } $sortorder = $sortorder + 1; $current->set('sortorder', $sortorder); $filters = array('sortorder' => $sortorder); $children = issuer::get_records($filters); foreach ($children as $needtoswap) { $needtoswap->set('sortorder', $sortorder - 1); $needtoswap->update(); } // OK - all set. $result = $current->update(); return $result; } /** * Disable an identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to disable. * @return boolean */ public static function disable_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($id); $issuer->set('enabled', 0); return $issuer->update(); } /** * Enable an identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to enable. * @return boolean */ public static function enable_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($id); $issuer->set('enabled', 1); return $issuer->update(); } /** * Delete an identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to delete. * @return boolean */ public static function delete_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($id); $systemaccount = self::get_system_account($issuer); if ($systemaccount) { $systemaccount->delete(); } $endpoints = self::get_endpoints($issuer); if ($endpoints) { foreach ($endpoints as $endpoint) { $endpoint->delete(); } } // Will throw exceptions on validation failures. return $issuer->delete(); } /** * Delete an endpoint. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the endpoint to delete. * @return boolean */ public static function delete_endpoint($id) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint($id); // Will throw exceptions on validation failures. return $endpoint->delete(); } /** * Delete a user_field_mapping. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the user_field_mapping to delete. * @return boolean */ public static function delete_user_field_mapping($id) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping($id); // Will throw exceptions on validation failures. return $userfieldmapping->delete(); } /** * Perform the OAuth dance and get a refresh token. * * Requires moodle/site:config capability at the system context. * * @param \core\oauth2\issuer $issuer * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication). * @return boolean */ public static function connect_system_account($issuer, $returnurl) { require_capability('moodle/site:config', context_system::instance()); // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access. $scopes = self::get_system_scopes_for_issuer($issuer); // Allow callbacks to inject non-standard scopes to the auth request. $class = self::get_client_classname($issuer->get('servicetype')); $client = new $class($issuer, $returnurl, $scopes, true); if (!optional_param('response', false, PARAM_BOOL)) { $client->log_out(); } if (optional_param('error', '', PARAM_RAW)) { return false; } if (!$client->is_logged_in()) { redirect($client->get_login_url()); } $refreshtoken = $client->get_refresh_token(); if (!$refreshtoken) { return false; } $systemaccount = self::get_system_account($issuer); if ($systemaccount) { $systemaccount->delete(); } $userinfo = $client->get_userinfo(); $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->refreshtoken = $refreshtoken; $record->grantedscopes = $scopes; $record->email = isset($userinfo['email']) ? $userinfo['email'] : ''; $record->username = $userinfo['username']; $systemaccount = new system_account(0, $record); $systemaccount->create(); $client->log_out(); return true; } } home3/cpr76684/public_html/Aem/h5p/classes/api.php 0000644 00000107065 15152206212 0015406 0 ustar 00 <?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/>. /** * Contains API class for the H5P area. * * @package core_h5p * @copyright 2020 Sara Arjona <sara@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_h5p; use core\lock\lock_config; use Moodle\H5PCore; /** * Contains API class for the H5P area. * * @copyright 2020 Sara Arjona <sara@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * Delete a library and also all the libraries depending on it and the H5P contents using it. For the H5P content, only the * database entries in {h5p} are removed (the .h5p files are not removed in order to let users to deploy them again). * * @param factory $factory The H5P factory. * @param \stdClass $library The library to delete. */ public static function delete_library(factory $factory, \stdClass $library): void { global $DB; // Get the H5P contents using this library, to remove them from DB. The .h5p files won't be removed // so they will be displayed by the player next time a user with the proper permissions accesses it. $sql = 'SELECT DISTINCT hcl.h5pid FROM {h5p_contents_libraries} hcl WHERE hcl.libraryid = :libraryid'; $params = ['libraryid' => $library->id]; $h5pcontents = $DB->get_records_sql($sql, $params); foreach ($h5pcontents as $h5pcontent) { $factory->get_framework()->deleteContentData($h5pcontent->h5pid); } $fs = $factory->get_core()->fs; $framework = $factory->get_framework(); // Delete the library from the file system. $fs->delete_library(array('libraryId' => $library->id)); // Delete also the cache assets to rebuild them next time. $framework->deleteCachedAssets($library->id); // Remove library data from database. $DB->delete_records('h5p_library_dependencies', array('libraryid' => $library->id)); $DB->delete_records('h5p_libraries', array('id' => $library->id)); // Remove the libraries using this library. $requiredlibraries = self::get_dependent_libraries($library->id); foreach ($requiredlibraries as $requiredlibrary) { self::delete_library($factory, $requiredlibrary); } } /** * Get all the libraries using a defined library. * * @param int $libraryid The library to get its dependencies. * @return array List of libraryid with all the libraries required by a defined library. */ public static function get_dependent_libraries(int $libraryid): array { global $DB; $sql = 'SELECT * FROM {h5p_libraries} WHERE id IN (SELECT DISTINCT hl.id FROM {h5p_library_dependencies} hld JOIN {h5p_libraries} hl ON hl.id = hld.libraryid WHERE hld.requiredlibraryid = :libraryid)'; $params = ['libraryid' => $libraryid]; return $DB->get_records_sql($sql, $params); } /** * Get a library from an identifier. * * @param int $libraryid The library identifier. * @return \stdClass The library object having the library identifier defined. * @throws dml_exception A DML specific exception is thrown if the libraryid doesn't exist. */ public static function get_library(int $libraryid): \stdClass { global $DB; return $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST); } /** * Returns a library as an object with properties that correspond to the fetched row's field names. * * @param array $params An associative array with the values of the machinename, majorversion and minorversion fields. * @param bool $configurable A library that has semantics so it can be configured in the editor. * @param string $fields Library attributes to retrieve. * * @return \stdClass|null An object with one attribute for each field name in $fields param. */ public static function get_library_details(array $params, bool $configurable, string $fields = ''): ?\stdClass { global $DB; $select = "machinename = :machinename AND majorversion = :majorversion AND minorversion = :minorversion"; if ($configurable) { $select .= " AND semantics IS NOT NULL"; } $fields = $fields ?: '*'; $record = $DB->get_record_select('h5p_libraries', $select, $params, $fields); return $record ?: null; } /** * Get all the H5P content type libraries versions. * * @param string|null $fields Library fields to return. * * @return array An array with an object for each content type library installed. */ public static function get_contenttype_libraries(?string $fields = ''): array { global $DB; $libraries = []; $fields = $fields ?: '*'; $select = "runnable = :runnable AND semantics IS NOT NULL"; $params = ['runnable' => 1]; $sort = 'title, majorversion DESC, minorversion DESC'; $records = $DB->get_records_select('h5p_libraries', $select, $params, $sort, $fields); $added = []; foreach ($records as $library) { // Remove unique index. unset($library->id); // Convert snakes to camels. $library->majorVersion = (int) $library->majorversion; unset($library->major_version); $library->minorVersion = (int) $library->minorversion; unset($library->minorversion); $library->metadataSettings = json_decode($library->metadatasettings ?? ''); // If we already add this library means that it is an old version,as the previous query was sorted by version. if (isset($added[$library->name])) { $library->isOld = true; } else { $added[$library->name] = true; } // Add new library. $libraries[] = $library; } return $libraries; } /** * Get the H5P DB instance id for a H5P pluginfile URL. If it doesn't exist, it's not created. * * @param string $url H5P pluginfile URL. * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they * might be controlled before calling this method. * * @return array of [file, stdClass|false]: * - file local file for this $url. * - stdClass is an H5P object or false if there isn't any H5P with this URL. */ public static function get_content_from_pluginfile_url(string $url, bool $preventredirect = true, bool $skipcapcheck = false): array { global $DB; // Deconstruct the URL and get the pathname associated. if ($skipcapcheck || self::can_access_pluginfile_hash($url, $preventredirect)) { $pathnamehash = self::get_pluginfile_hash($url); } if (!$pathnamehash) { return [false, false]; } // Get the file. $fs = get_file_storage(); $file = $fs->get_file_by_hash($pathnamehash); if (!$file) { return [false, false]; } $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); return [$file, $h5p]; } /** * Get the original file and H5P DB instance for a given H5P pluginfile URL. If it doesn't exist, it's not created. * If the file has been added as a reference, this method will return the original linked file. * * @param string $url H5P pluginfile URL. * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions. * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they * might be controlled before calling this method. * * @return array of [\stored_file|false, \stdClass|false, \stored_file|false]: * - \stored_file: original local file for the given url (if it has been added as a reference, this method * will return the linked file) or false if there isn't any H5P file with this URL. * - \stdClass: an H5P object or false if there isn't any H5P with this URL. * - \stored_file: file associated to the given url (if it's different from original) or false when both files * (original and file) are the same. * @since Moodle 4.0 */ public static function get_original_content_from_pluginfile_url(string $url, bool $preventredirect = true, bool $skipcapcheck = false): array { $file = false; list($originalfile, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck); if ($originalfile) { if ($reference = $originalfile->get_reference()) { $file = $originalfile; // If the file has been added as a reference to any other file, get it. $fs = new \file_storage(); $referenced = \file_storage::unpack_reference($reference); $originalfile = $fs->get_file( $referenced['contextid'], $referenced['component'], $referenced['filearea'], $referenced['itemid'], $referenced['filepath'], $referenced['filename'] ); $h5p = self::get_content_from_pathnamehash($originalfile->get_pathnamehash()); if (empty($h5p)) { $h5p = false; } } } return [$originalfile, $h5p, $file]; } /** * Check if the user can edit an H5P file. It will return true in the following situations: * - The user is the author of the file. * - The component is different from user (i.e. private files). * - If the component is contentbank, the user can edit this file (calling the ContentBank API). * - If the component is mod_xxx or block_xxx, the user has the addinstance capability. * - If the component implements the can_edit_content in the h5p\canedit class and the callback to this method returns true. * * @param \stored_file $file The H5P file to check. * * @return boolean Whether the user can edit or not the given file. * @since Moodle 4.0 */ public static function can_edit_content(\stored_file $file): bool { global $USER; list($type, $component) = \core_component::normalize_component($file->get_component()); // Private files. $currentuserisauthor = $file->get_userid() == $USER->id; $isuserfile = $component === 'user'; if ($currentuserisauthor && $isuserfile) { // The user can edit the content because it's a private user file and she is the owner. return true; } // Check if the plugin where the file belongs implements the custom can_edit_content method and call it if that's the case. $classname = '\\' . $file->get_component() . '\\h5p\\canedit'; $methodname = 'can_edit_content'; if (method_exists($classname, $methodname)) { return $classname::{$methodname}($file); } // For mod/block files, check if the user has the addinstance capability of the component where the file belongs. if ($type === 'mod' || $type === 'block') { // For any other component, check whether the user can add/edit them. $context = \context::instance_by_id($file->get_contextid()); $plugins = \core_component::get_plugin_list($type); $isvalid = array_key_exists($component, $plugins); if ($isvalid && has_capability("$type/$component:addinstance", $context)) { // The user can edit the content because she has the capability for creating instances where the file belongs. return true; } } // For contentbank files, use the API to check if the user has access. if ($component == 'contentbank') { $cb = new \core_contentbank\contentbank(); $content = $cb->get_content_from_id($file->get_itemid()); $contenttype = $content->get_content_type_instance(); if ($contenttype instanceof \contenttype_h5p\contenttype) { // Only H5P contenttypes should be considered here. if ($contenttype->can_edit($content)) { // The user has permissions to edit the H5P in the content bank. return true; } } } return false; } /** * Create, if it doesn't exist, the H5P DB instance id for a H5P pluginfile URL. If it exists: * - If the content is not the same, remove the existing content and re-deploy the H5P content again. * - If the content is the same, returns the H5P identifier. * * @param string $url H5P pluginfile URL. * @param stdClass $config Configuration for H5P buttons. * @param factory $factory The \core_h5p\factory object * @param stdClass $messages The error, exception and info messages, raised while preparing and running an H5P content. * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they * might be controlled before calling this method. * * @return array of [file, h5pid]: * - file local file for this $url. * - h5pid is the H5P identifier or false if there isn't any H5P with this URL. */ public static function create_content_from_pluginfile_url(string $url, \stdClass $config, factory $factory, \stdClass &$messages, bool $preventredirect = true, bool $skipcapcheck = false): array { global $USER; $core = $factory->get_core(); list($file, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck); if (!$file) { $core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p')); return [false, false]; } $contenthash = $file->get_contenthash(); if ($h5p && $h5p->contenthash != $contenthash) { // The content exists and it is different from the one deployed previously. The existing one should be removed before // deploying the new version. self::delete_content($h5p, $factory); $h5p = false; } $context = \context::instance_by_id($file->get_contextid()); if ($h5p) { // The H5P content has been deployed previously. // If the main library for this H5P content is disabled, the content won't be displayed. $mainlibrary = (object) ['id' => $h5p->mainlibraryid]; if (!self::is_library_enabled($mainlibrary)) { $core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p')); return [$file, false]; } else { $displayoptions = helper::get_display_options($core, $config); // Check if the user can set the displayoptions. if ($displayoptions != $h5p->displayoptions && has_capability('moodle/h5p:setdisplayoptions', $context)) { // If displayoptions has changed and user has permission to modify it, update this information in DB. $core->h5pF->updateContentFields($h5p->id, ['displayoptions' => $displayoptions]); } return [$file, $h5p->id]; } } else { // The H5P content hasn't been deployed previously. // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this // capability, the content won't be deployed and an error message will be displayed. if (!helper::can_deploy_package($file)) { $core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p')); return [$file, false]; } // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content. $onlyupdatelibs = !helper::can_update_library($file); // Start lock to prevent synchronous access to save the same H5P. $lockfactory = lock_config::get_lock_factory('core_h5p'); $lockkey = 'core_h5p_' . $file->get_pathnamehash(); if ($lock = $lockfactory->get_lock($lockkey, 10)) { try { // Validate and store the H5P content before displaying it. $h5pid = helper::save_h5p($factory, $file, $config, $onlyupdatelibs, false); } finally { $lock->release(); } } else { $core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p')); return [$file, false]; }; if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $context)) { // The user has permission to update libraries but the package has been uploaded by a different // user without this permission. Check if there is some missing required library error. $missingliberror = false; $messages = helper::get_messages($messages, $factory); if (!empty($messages->error)) { foreach ($messages->error as $error) { if ($error->code == 'missing-required-library') { $missingliberror = true; break; } } } if ($missingliberror) { // The message about the permissions to upload libraries should be removed. $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " . "new libraries. Contact the site administrator about this."; if (($key = array_search($infomsg, $messages->info)) !== false) { unset($messages->info[$key]); } // No library will be installed and an error will be displayed, because this content is not trustable. $core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p')); } return [$file, false]; } return [$file, $h5pid]; } } /** * Delete an H5P package. * * @param stdClass $content The H5P package to delete with, at least content['id]. * @param factory $factory The \core_h5p\factory object */ public static function delete_content(\stdClass $content, factory $factory): void { $h5pstorage = $factory->get_storage(); // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists. // It's not used when deleting a package, so the real slug value is not required at this point. $content->slug = $content->slug ?? ''; $h5pstorage->deletePackage( (array) $content); } /** * Delete an H5P package deployed from the defined $url. * * @param string $url pluginfile URL of the H5P package to delete. * @param factory $factory The \core_h5p\factory object */ public static function delete_content_from_pluginfile_url(string $url, factory $factory): void { global $DB; // Get the H5P to delete. $pathnamehash = self::get_pluginfile_hash($url); $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); if ($h5p) { self::delete_content($h5p, $factory); } } /** * If user can access pathnamehash from an H5P internal URL. * * @param string $url H5P pluginfile URL poiting to an H5P file. * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions * * @return bool if user can access pluginfile hash. * @throws \moodle_exception * @throws \coding_exception * @throws \require_login_exception */ protected static function can_access_pluginfile_hash(string $url, bool $preventredirect = true): bool { global $USER, $CFG; // Decode the URL before start processing it. $url = new \moodle_url(urldecode($url)); // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors. $url->remove_params(array_keys($url->params())); $path = $url->out_as_local_url(); // We only need the slasharguments. $path = substr($path, strpos($path, '.php/') + 5); $parts = explode('/', $path); // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey. if (strpos($url, '/tokenpluginfile.php')) { array_shift($parts); } // Get the contextid, component and filearea. $contextid = array_shift($parts); $component = array_shift($parts); $filearea = array_shift($parts); // Get the context. try { list($context, $course, $cm) = get_context_info_array($contextid); } catch (\moodle_exception $e) { throw new \moodle_exception('invalidcontextid', 'core_h5p'); } // For CONTEXT_USER, such as the private files, raise an exception if the owner of the file is not the current user. if ($context->contextlevel == CONTEXT_USER && $USER->id !== $context->instanceid) { throw new \moodle_exception('h5pprivatefile', 'core_h5p'); } if (!is_siteadmin($USER)) { // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere. if ($context->contextlevel == CONTEXT_COURSECAT) { if ($CFG->forcelogin) { require_login(null, true, null, false, true); } } // For CONTEXT_BLOCK. if ($context->contextlevel == CONTEXT_BLOCK) { if ($context->get_course_context(false)) { // If block is in course context, then check if user has capability to access course. require_course_login($course, true, null, false, true); } else if ($CFG->forcelogin) { // No login necessary - unless login forced everywhere. require_login(null, true, null, false, true); } else { // Get parent context and see if user have proper permission. $parentcontext = $context->get_parent_context(); if ($parentcontext->contextlevel === CONTEXT_COURSECAT) { // Check if category is visible and user can view this category. if (!\core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) { send_file_not_found(); } } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) { // The block is in the context of a user, it is only visible to the user who it belongs to. send_file_not_found(); } if ($filearea !== 'content') { send_file_not_found(); } } } // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course. // And for CONTEXT_MODULE has permissions view this .h5p file. if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_COURSE) { // Require login to the course first (without login to the module). require_course_login($course, true, null, !$preventredirect, $preventredirect); // Now check if module is available OR it is restricted but the intro is shown on the course page. if ($context->contextlevel == CONTEXT_MODULE) { $cminfo = \cm_info::create($cm); if (!$cminfo->uservisible) { if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) { // Module intro is not visible on the course page and module is not available, show access error. require_course_login($course, true, $cminfo, !$preventredirect, $preventredirect); } } } } } return true; } /** * Get the pathnamehash from an H5P internal URL. * * @param string $url H5P pluginfile URL poiting to an H5P file. * * @return string|false pathnamehash for the file in the internal URL. * * @throws \moodle_exception */ protected static function get_pluginfile_hash(string $url) { // Decode the URL before start processing it. $url = new \moodle_url(urldecode($url)); // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors. $url->remove_params(array_keys($url->params())); $path = $url->out_as_local_url(); // We only need the slasharguments. $path = substr($path, strpos($path, '.php/') + 5); $parts = explode('/', $path); $filename = array_pop($parts); // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey. if (strpos($url, '/tokenpluginfile.php')) { array_shift($parts); } // Get the contextid, component and filearea. $contextid = array_shift($parts); $component = array_shift($parts); $filearea = array_shift($parts); // Ignore draft files, because they are considered temporary files, so shouldn't be displayed. if ($filearea == 'draft') { return false; } // Get the context. try { list($context, $course, $cm) = get_context_info_array($contextid); } catch (\moodle_exception $e) { throw new \moodle_exception('invalidcontextid', 'core_h5p'); } // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems. // So the URL contains this revision number as itemid but a 0 is always stored in the files table. // In order to get the proper hash, a callback should be done (looking for those exceptions). $pathdata = null; if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_BLOCK) { $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null); } if (null === $pathdata) { // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile. $hasnullitemid = false; $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile')); $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro'); $hasnullitemid = $hasnullitemid || ($component === 'course' && ($filearea === 'summary' || $filearea === 'overviewfiles')); $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description'); $hasnullitemid = $hasnullitemid || ($component === 'backup' && ($filearea === 'course' || $filearea === 'activity' || $filearea === 'automated')); if ($hasnullitemid) { $itemid = 0; } else { $itemid = array_shift($parts); } if (empty($parts)) { $filepath = '/'; } else { $filepath = '/' . implode('/', $parts) . '/'; } } else { // The itemid and filepath have been returned by the component callback. [ 'itemid' => $itemid, 'filepath' => $filepath, ] = $pathdata; } $fs = get_file_storage(); $pathnamehash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); return $pathnamehash; } /** * Returns the H5P content object corresponding to an H5P content file. * * @param string $pathnamehash The pathnamehash of the file associated to an H5P content. * * @return null|\stdClass H5P content object or null if not found. */ public static function get_content_from_pathnamehash(string $pathnamehash): ?\stdClass { global $DB; $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); return ($h5p) ? $h5p : null; } /** * Return the H5P export information file when the file has been deployed. * Otherwise, return null if H5P file: * i) has not been deployed. * ii) has changed the content. * * The information returned will be: * - filename, filepath, mimetype, filesize, timemodified and fileurl. * * @param int $contextid ContextId of the H5P activity. * @param factory $factory The \core_h5p\factory object. * @param string $component component * @param string $filearea file area * @return array|null Return file info otherwise null. */ public static function get_export_info_from_context_id(int $contextid, factory $factory, string $component, string $filearea): ?array { $core = $factory->get_core(); $fs = get_file_storage(); $files = $fs->get_area_files($contextid, $component, $filearea, 0, 'id', false); $file = reset($files); if ($h5p = self::get_content_from_pathnamehash($file->get_pathnamehash())) { if ($h5p->contenthash == $file->get_contenthash()) { $content = $core->loadContent($h5p->id); $slug = $content['slug'] ? $content['slug'] . '-' : ''; $filename = "{$slug}{$content['id']}.h5p"; $deployedfile = helper::get_export_info($filename, null, $factory); return $deployedfile; } } return null; } /** * Enable or disable a library. * * @param int $libraryid The id of the library to enable/disable. * @param bool $isenabled True if the library should be enabled; false otherwise. */ public static function set_library_enabled(int $libraryid, bool $isenabled): void { global $DB; $library = $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST); if ($library->runnable) { // For now, only runnable libraries can be enabled/disabled. $record = [ 'id' => $libraryid, 'enabled' => $isenabled, ]; $DB->update_record('h5p_libraries', $record); } } /** * Check whether a library is enabled or not. When machinename is passed, it will return false if any of the versions * for this machinename is disabled. * If the library doesn't exist, it will return true. * * @param \stdClass $librarydata Supported fields for library: 'id' and 'machichename'. * @return bool * @throws \moodle_exception */ public static function is_library_enabled(\stdClass $librarydata): bool { global $DB; $params = []; if (property_exists($librarydata, 'machinename')) { $params['machinename'] = $librarydata->machinename; } if (property_exists($librarydata, 'id')) { $params['id'] = $librarydata->id; } if (empty($params)) { throw new \moodle_exception("Missing 'machinename' or 'id' in librarydata parameter"); } $libraries = $DB->get_records('h5p_libraries', $params); // If any of the libraries with these values have been disabled, return false. foreach ($libraries as $id => $library) { if (!$library->enabled) { return false; } } return true; } /** * Check whether an H5P package is valid or not. * * @param \stored_file $file The file with the H5P content. * @param bool $onlyupdatelibs Whether new libraries can be installed or only the existing ones can be updated * @param bool $skipcontent Should the content be skipped (so only the libraries will be saved)? * @param factory|null $factory The \core_h5p\factory object * @param bool $deletefiletree Should the temporary files be deleted before returning? * @return bool True if the H5P file is valid (expected format, valid libraries...); false otherwise. */ public static function is_valid_package(\stored_file $file, bool $onlyupdatelibs, bool $skipcontent = false, ?factory $factory = null, bool $deletefiletree = true): bool { // This may take a long time. \core_php_time_limit::raise(); $isvalid = false; if (empty($factory)) { $factory = new factory(); } $core = $factory->get_core(); $h5pvalidator = $factory->get_validator(); // Set the H5P file path. $core->h5pF->set_file($file); $path = $core->fs->getTmpPath(); $core->h5pF->getUploadedH5pFolderPath($path); // Add manually the extension to the file to avoid the validation fails. $path .= '.h5p'; $core->h5pF->getUploadedH5pPath($path); // Copy the .h5p file to the temporary folder. $file->copy_content_to($path); if ($h5pvalidator->isValidPackage($skipcontent, $onlyupdatelibs)) { if ($skipcontent) { $isvalid = true; } else if (!empty($h5pvalidator->h5pC->mainJsonData['mainLibrary'])) { $mainlibrary = (object) ['machinename' => $h5pvalidator->h5pC->mainJsonData['mainLibrary']]; if (self::is_library_enabled($mainlibrary)) { $isvalid = true; } else { // If the main library of the package is disabled, the H5P content will be considered invalid. $core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p')); } } } if ($deletefiletree) { // Remove temp content folder. H5PCore::deleteFileTree($path); } return $isvalid; } } home3/cpr76684/public_html/Aem/message/classes/api.php 0000644 00000407603 15152232524 0016344 0 ustar 00 <?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/>. /** * Contains class used to return information to display for the message area. * * @package core_message * @copyright 2016 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_message; use core_favourites\local\entity\favourite; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/lib/messagelib.php'); /** * Class used to return information to display for the message area. * * @copyright 2016 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * The action for reading a message. */ const MESSAGE_ACTION_READ = 1; /** * The action for deleting a message. */ const MESSAGE_ACTION_DELETED = 2; /** * The action for reading a message. */ const CONVERSATION_ACTION_MUTED = 1; /** * The privacy setting for being messaged by anyone within courses user is member of. */ const MESSAGE_PRIVACY_COURSEMEMBER = 0; /** * The privacy setting for being messaged only by contacts. */ const MESSAGE_PRIVACY_ONLYCONTACTS = 1; /** * The privacy setting for being messaged by anyone on the site. */ const MESSAGE_PRIVACY_SITE = 2; /** * An individual conversation. */ const MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1; /** * A group conversation. */ const MESSAGE_CONVERSATION_TYPE_GROUP = 2; /** * A self conversation. */ const MESSAGE_CONVERSATION_TYPE_SELF = 3; /** * The state for an enabled conversation area. */ const MESSAGE_CONVERSATION_ENABLED = 1; /** * The state for a disabled conversation area. */ const MESSAGE_CONVERSATION_DISABLED = 0; /** * The max message length. */ const MESSAGE_MAX_LENGTH = 4096; /** * Handles searching for messages in the message area. * * @param int $userid The user id doing the searching * @param string $search The string the user is searching * @param int $limitfrom * @param int $limitnum * @return array */ public static function search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) { global $DB; // Get the user fields we want. $userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess'); $ufields = $userfieldsapi->get_sql('u', false, 'userfrom_', '', false)->selects; $ufields2 = $userfieldsapi->get_sql('u2', false, 'userto_', '', false)->selects; // Add the uniqueid column to make each row unique and avoid SQL errors. $uniqueidsql = $DB->sql_concat('m.id', "'_'", 'm.useridfrom', "'_'", 'mcm.userid'); $sql = "SELECT $uniqueidsql AS uniqueid, m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat, m.smallmessage, m.conversationid, m.timecreated, 0 as isread, $ufields, mub.id as userfrom_blocked, $ufields2, mub2.id as userto_blocked FROM ( SELECT m2.id AS id FROM {messages} m2 WHERE m2.useridfrom = ? UNION SELECT m3.id AS id FROM {message_conversation_members} mcm3 INNER JOIN {messages} m3 ON mcm3.conversationid = m3.conversationid WHERE mcm3.userid = ? ) der INNER JOIN {messages} m ON der.id = m.id INNER JOIN {user} u ON u.id = m.useridfrom INNER JOIN {message_conversations} mc ON mc.id = m.conversationid INNER JOIN {message_conversation_members} mcm ON mcm.conversationid = m.conversationid INNER JOIN {user} u2 ON u2.id = mcm.userid LEFT JOIN {message_users_blocked} mub ON (mub.blockeduserid = u.id AND mub.userid = ?) LEFT JOIN {message_users_blocked} mub2 ON (mub2.blockeduserid = u2.id AND mub2.userid = ?) LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?) WHERE (m.useridfrom = ? OR mcm.userid = ?) AND (m.useridfrom != mcm.userid OR mc.type = ?) AND u.deleted = 0 AND u2.deleted = 0 AND mua.id is NULL AND " . $DB->sql_like('smallmessage', '?', false) . " ORDER BY timecreated DESC"; $params = array($userid, $userid, $userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid, self::MESSAGE_CONVERSATION_TYPE_SELF, '%' . $search . '%'); // Convert the messages into searchable contacts with their last message being the message that was searched. $conversations = array(); if ($messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) { foreach ($messages as $message) { $prefix = 'userfrom_'; if ($userid == $message->useridfrom) { $prefix = 'userto_'; // If it from the user, then mark it as read, even if it wasn't by the receiver. $message->isread = true; } $blockedcol = $prefix . 'blocked'; $message->blocked = $message->$blockedcol ? 1 : 0; $message->messageid = $message->id; // To avoid duplicate messages, only add the message if it hasn't been added previously. if (!array_key_exists($message->messageid, $conversations)) { $conversations[$message->messageid] = helper::create_contact($message, $prefix); } } // Remove the messageid keys (to preserve the expected type). $conversations = array_values($conversations); } return $conversations; } /** * @deprecated since 3.6 */ public static function search_users_in_course() { throw new \coding_exception('\core_message\api::search_users_in_course has been removed.'); } /** * @deprecated since 3.6 */ public static function search_users() { throw new \coding_exception('\core_message\api::search_users has been removed.'); } /** * Handles searching for user. * * @param int $userid The user id doing the searching * @param string $search The string the user is searching * @param int $limitfrom * @param int $limitnum * @return array */ public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20) : array { global $CFG, $DB; // Check if messaging is enabled. if (empty($CFG->messaging)) { throw new \moodle_exception('disabled', 'message'); } require_once($CFG->dirroot . '/user/lib.php'); // Used to search for contacts. $fullname = $DB->sql_fullname(); // Users not to include. $excludeusers = array($CFG->siteguest); if (!$selfconversation = self::get_self_conversation($userid)) { // Userid should only be excluded when she hasn't a self-conversation. $excludeusers[] = $userid; } list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false); $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid); // Ok, let's search for contacts first. $sql = "SELECT u.id FROM {user} u JOIN {message_contacts} mc ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2) WHERE u.deleted = 0 AND u.confirmed = 1 AND " . $DB->sql_like($fullname, ':search', false) . " AND u.id $exclude ORDER BY " . $DB->sql_fullname(); $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum); $contacts = []; if (!empty($foundusers)) { $contacts = helper::get_member_info($userid, array_keys($foundusers)); foreach ($contacts as $memberuserid => $memberinfo) { $contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000); } } // We need to get all the user details for a fullname in the visibility checks. $namefields = \core_user\fields::for_name() // Required by the visibility checks. ->including('deleted'); // Let's get those non-contacts. // Because we can't achieve all the required visibility checks in SQL, we'll iterate through the non-contact records // and stop once we have enough matching the 'visible' criteria. // Use a local generator to achieve this iteration. $getnoncontactusers = function ($limitfrom = 0, $limitnum = 0) use ( $fullname, $exclude, $params, $excludeparams, $userid, $selfconversation, $namefields ) { global $DB, $CFG; $joinenrolled = ''; $enrolled = ''; $unionself = ''; $enrolledparams = []; // Since we want to order a UNION we need to list out all the user fields individually this will // allow us to reference the fullname correctly. $userfields = $namefields->get_sql('u')->selects; $select = "u.id, " . $DB->sql_fullname() . " AS sortingname" . $userfields; // When messageallusers is false valid non-contacts must be enrolled on one of the users courses. if (empty($CFG->messagingallusers)) { $joinenrolled = "JOIN {user_enrolments} ue ON ue.userid = u.id JOIN {enrol} e ON e.id = ue.enrolid"; $enrolled = "AND e.courseid IN ( SELECT e.courseid FROM {user_enrolments} ue JOIN {enrol} e ON e.id = ue.enrolid WHERE ue.userid = :enroluserid )"; if ($selfconversation !== false) { // We must include the user themselves, when they have a self conversation, even if they are not // enrolled on any courses. $unionself = "UNION SELECT u.id FROM {user} u WHERE u.id = :self AND ". $DB->sql_like($fullname, ':selfsearch', false); } $enrolledparams = ['enroluserid' => $userid, 'self' => $userid, 'selfsearch' => $params['search']]; } $sql = "SELECT $select FROM ( SELECT DISTINCT u.id FROM {user} u $joinenrolled WHERE u.deleted = 0 AND u.confirmed = 1 AND " . $DB->sql_like($fullname, ':search', false) . " AND u.id $exclude $enrolled AND NOT EXISTS (SELECT mc.id FROM {message_contacts} mc WHERE (mc.userid = u.id AND mc.contactid = :userid1) OR (mc.userid = :userid2 AND mc.contactid = u.id)) $unionself ) targetedusers JOIN {user} u ON u.id = targetedusers.id ORDER BY 2"; while ($records = $DB->get_records_sql($sql, $params + $excludeparams + $enrolledparams, $limitfrom, $limitnum)) { yield $records; $limitfrom += $limitnum; } }; // Fetch in batches of $limitnum * 2 to improve the chances of matching a user without going back to the DB. // The generator cannot function without a sensible limiter, so set one if this is not set. $batchlimit = ($limitnum == 0) ? 20 : $limitnum; // We need to make the offset param work with the generator. // Basically, if we want to get say 10 records starting at the 40th record, we need to see 50 records and return only // those after the 40th record. We can never pass the method's offset param to the generator as we need to manage the // position within those valid records ourselves. // See MDL-63983 dealing with performance improvements to this area of code. $noofvalidseenrecords = 0; $returnedusers = []; // Only fields that are also part of user_get_default_fields() are valid when passed into user_get_user_details(). $fields = array_intersect($namefields->get_required_fields(), user_get_default_fields()); foreach ($getnoncontactusers(0, $batchlimit) as $users) { foreach ($users as $id => $user) { // User visibility checks: only return users who are visible to the user performing the search. // Which visibility check to use depends on the 'messagingallusers' (site wide messaging) setting: // - If enabled, return matched users whose profiles are visible to the current user anywhere (site or course). // - If disabled, only return matched users whose course profiles are visible to the current user. $userdetails = \core_message\helper::search_get_user_details($user, $fields); // Return the user only if the searched field is returned. // Otherwise it means that the $USER was not allowed to search the returned user. if (!empty($userdetails) and !empty($userdetails['fullname'])) { // We know we've matched, but only save the record if it's within the offset area we need. if ($limitfrom == 0) { // No offset specified, so just save. $returnedusers[$id] = $user; } else { // There is an offset in play. // If we've passed enough records already (> offset value), then we can save this one. if ($noofvalidseenrecords >= $limitfrom) { $returnedusers[$id] = $user; } } if (count($returnedusers) == $limitnum) { break 2; } $noofvalidseenrecords++; } } } $foundusers = $returnedusers; $noncontacts = []; if (!empty($foundusers)) { $noncontacts = helper::get_member_info($userid, array_keys($foundusers)); foreach ($noncontacts as $memberuserid => $memberinfo) { if ($memberuserid !== $userid) { $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000); } else { $noncontacts[$memberuserid]->conversations[$selfconversation->id] = $selfconversation; } } } return array(array_values($contacts), array_values($noncontacts)); } /** * Gets extra fields, like image url and subname for any conversations linked to components. * * The subname is like a subtitle for the conversation, to compliment it's name. * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user. * * @param array $conversations a list of conversations records. * @return array the array of subnames, index by conversation id. * @throws \coding_exception * @throws \dml_exception */ protected static function get_linked_conversation_extra_fields(array $conversations) : array { global $DB, $PAGE; $renderer = $PAGE->get_renderer('core'); $linkedconversations = []; foreach ($conversations as $conversation) { if (!is_null($conversation->component) && !is_null($conversation->itemtype)) { $linkedconversations[$conversation->component][$conversation->itemtype][$conversation->id] = $conversation->itemid; } } if (empty($linkedconversations)) { return []; } // TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way. // Get the itemid, but only for course group linked conversation for now. $extrafields = []; if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid]. // Get the name of the course to which the group belongs. list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid'); $sql = "SELECT g.*, c.shortname as courseshortname FROM {groups} g JOIN {course} c ON g.courseid = c.id WHERE g.id $groupidsql"; $courseinfo = $DB->get_records_sql($sql, $groupidparams); foreach ($linkeditems as $convid => $groupid) { if (array_key_exists($groupid, $courseinfo)) { $group = $courseinfo[$groupid]; // Subname. $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname); // Imageurl. $extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image. if ($url = get_group_picture_url($group, $group->courseid, true)) { $extrafields[$convid]['imageurl'] = $url->out(false); } } } } return $extrafields; } /** * Returns the contacts and their conversation to display in the contacts area. * * ** WARNING ** * It is HIGHLY recommended to use a sensible limit when calling this function. Trying * to retrieve too much information in a single call will cause performance problems. * ** WARNING ** * * This function has specifically been altered to break each of the data sets it * requires into separate database calls. This is to avoid the performance problems * observed when attempting to join large data sets (e.g. the message tables and * the user table). * * While it is possible to gather the data in a single query, and it may even be * more efficient with a correctly tuned database, we have opted to trade off some of * the benefits of a single query in order to ensure this function will work on * most databases with default tunings and with large data sets. * * @param int $userid The user id * @param int $limitfrom * @param int $limitnum * @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants). * @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting. * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false) * when private conversations are requested. * @return array the array of conversations * @throws \moodle_exception */ public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null, bool $favourites = null, bool $mergeself = false) { global $DB; if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) { throw new \moodle_exception("Invalid value ($type) for type param, please see api constants."); } self::lazy_create_self_conversation($userid); // We need to know which conversations are favourites, so we can either: // 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations) // 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true) // 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false). $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations'); $favouriteconversationids = array_column($favouriteconversations, 'itemid'); if ($favourites && empty($favouriteconversationids)) { return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do. } // CONVERSATIONS AND MOST RECENT MESSAGE. // Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which // don't have messages, such as newly created group conversations. // Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the // end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these. // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet. $favouritesql = ""; $favouriteparams = []; if (null !== $favourites && !empty($favouriteconversationids)) { list ($insql, $favouriteparams) = $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites); $favouritesql = " AND mc.id {$insql} "; } // If we need to restrict type, generate the SQL snippet. $typesql = ""; $typeparams = []; if (!is_null($type)) { if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { // When $megerself is set to true, the self-conversations are returned also with the private conversations. $typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) "; $typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF]; } else { $typesql = " AND mc.type = :convtype "; $typeparams = ['convtype' => $type]; } } $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom, m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated, mc.component, mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted FROM {message_conversations} mc INNER JOIN {message_conversation_members} mcm ON (mcm.conversationid = mc.id AND mcm.userid = :userid3) LEFT JOIN ( SELECT m.conversationid, MAX(m.id) AS messageid FROM {messages} m INNER JOIN ( SELECT m.conversationid, MAX(m.timecreated) as maxtime FROM {messages} m INNER JOIN {message_conversation_members} mcm ON mcm.conversationid = m.conversationid LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action) WHERE mua.id is NULL AND mcm.userid = :userid2 GROUP BY m.conversationid ) maxmessage ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid GROUP BY m.conversationid ) lastmessage ON lastmessage.conversationid = mc.id LEFT JOIN {messages} m ON m.id = lastmessage.messageid LEFT JOIN {message_conversation_actions} mca ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction) WHERE mc.id IS NOT NULL AND mc.enabled = 1 $typesql $favouritesql ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC"; $params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED, 'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]); $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum); $conversations = []; $selfconversations = []; // Used to track conversations with one's self. $members = []; $individualmembers = []; $groupmembers = []; $selfmembers = []; foreach ($conversationset as $conversation) { $conversations[$conversation->id] = $conversation; $members[$conversation->id] = []; } $conversationset->close(); // If there are no conversations found, then return early. if (empty($conversations)) { return []; } // COMPONENT-LINKED CONVERSATION FIELDS. // Conversations linked to components may have extra information, such as: // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname". // - imageurl: A URL to the image for the linked conversation. // For now, this is ONLY course groups. $convextrafields = self::get_linked_conversation_extra_fields($conversations); // MEMBERS. // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent // message or not. // // For 'individual' type conversations between 2 users, regardless of who sent the last message, // we want the details of the other member in the conversation (i.e. not the current user). // // For 'group' type conversations, we want the details of the member who sent the last message, if there is one. // This can be the current user or another group member, but for groups without messages, this will be empty. // // For 'self' type conversations, we want the details of the current user. // // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra // query to get the 'other' user as we already have that information. // Work out which members we have already, and which ones we might need to fetch. // If all the last messages were from another user, then we don't need to fetch anything further. foreach ($conversations as $conversation) { if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) { $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom; $individualmembers[$conversation->useridfrom] = $conversation->useridfrom; } else { $individualconversations[] = $conversation->id; } } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) { // If we have a recent message, the sender is our member. if (!is_null($conversation->useridfrom)) { $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom; $groupmembers[$conversation->useridfrom] = $conversation->useridfrom; } } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) { $selfconversations[$conversation->id] = $conversation->id; $members[$conversation->id][$userid] = $userid; $selfmembers[$userid] = $userid; } } // If we need to fetch any member information for any of the individual conversations. // This is the case if any of the individual conversations have a recent message sent by the current user. if (!empty($individualconversations)) { list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid'); $indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid FROM {message_conversation_members} mcm WHERE mcm.conversationid $icidinsql AND mcm.userid != :userid ORDER BY mcm.id"; $indmemberparams = array_merge($icidinparams, ['userid' => $userid]); $conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams); foreach ($conversationmembers as $mid => $member) { $members[$member->conversationid][$member->userid] = $member->userid; $individualmembers[$member->userid] = $member->userid; } } // We could fail early here if we're sure that: // a) we have no otherusers for all the conversations (users may have been deleted) // b) we're sure that all conversations are individual (1:1). // We need to pull out the list of users info corresponding to the memberids in the conversations.This // needs to be done in a separate query to avoid doing a join on the messages tables and the user // tables because on large sites these tables are massive which results in extremely slow // performance (typically due to join buffer exhaustion). if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) { // Now, we want to remove any duplicates from the group members array. For individual members we will // be doing a more extensive call as we want their contact requests as well as privacy information, // which is not necessary for group conversations. $diffgroupmembers = array_diff($groupmembers, $individualmembers); $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true); $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers); $selfmemberinfo = helper::get_member_info($userid, $selfmembers); // Don't use array_merge, as we lose array keys. $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo; if (empty($memberinfo)) { return []; } // Update the members array with the member information. $deletedmembers = []; foreach ($members as $convid => $memberarr) { foreach ($memberarr as $key => $memberid) { if (array_key_exists($memberid, $memberinfo)) { // If the user is deleted, remember that. if ($memberinfo[$memberid]->isdeleted) { $deletedmembers[$convid][] = $memberid; } $members[$convid][$key] = clone $memberinfo[$memberid]; if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) { // Remove data we don't need for group. $members[$convid][$key]->requirescontact = null; $members[$convid][$key]->canmessage = null; $members[$convid][$key]->contactrequests = []; } } else { // Remove all members and individual conversations where we could not get the member's information. unset($members[$convid][$key]); // If the conversation is an individual conversation, then we should remove it from the list. if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { unset($conversations[$convid]); } } } } } // MEMBER COUNT. $cids = array_column($conversations, 'id'); list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid'); $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount FROM {message_conversation_members} mcm WHERE mcm.conversationid $cidinsql GROUP BY mcm.conversationid"; $membercounts = $DB->get_records_sql($membercountsql, $cidinparams); // UNREAD MESSAGE COUNT. // Finally, let's get the unread messages count for this user so that we can add it // to the conversation. Remember we need to ignore the messages the user sent. $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount FROM {messages} m INNER JOIN {message_conversations} mc ON mc.id = m.conversationid INNER JOIN {message_conversation_members} mcm ON m.conversationid = mcm.conversationid LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND (mua.action = ? OR mua.action = ?)) WHERE mcm.userid = ? AND m.useridfrom != ? AND mua.id is NULL GROUP BY m.conversationid'; $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED, $userid, $userid]); // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied). $selfmessagessql = "SELECT COUNT(m.id) FROM {messages} m INNER JOIN {message_conversations} mc ON mc.id = m.conversationid WHERE mc.type = ? AND convhash = ?"; $selfmessagestotal = $DB->count_records_sql( $selfmessagessql, [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])] ); // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here. // This warms the cache and saves potentially hitting the DB once for each context fetch below. \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid')); // Now, create the final return structure. $arrconversations = []; foreach ($conversations as $conversation) { // Do not include any individual which do not contain a recent message for the user. // This happens if the user has deleted all messages. // Exclude the self-conversations with messages but without a recent message because the user has deleted all them. // Self-conversations without any message should be included, to display them first time they are created. // Group conversations with deleted users or no messages are always returned. if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) || ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid) && $selfmessagestotal > 0)) { continue; } $conv = new \stdClass(); $conv->id = $conversation->id; // Name should be formatted and depends on the context the conversation resides in. // If not set, the context is always context_user. if (is_null($conversation->contextid)) { $convcontext = \context_user::instance($userid); // We'll need to check the capability to delete messages for all users in context system when contextid is null. $contexttodeletemessageforall = \context_system::instance(); } else { $convcontext = \context::instance_by_id($conversation->contextid); $contexttodeletemessageforall = $convcontext; } $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]); $conv->subname = $convextrafields[$conv->id]['subname'] ?? null; $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null; $conv->type = $conversation->conversationtype; $conv->membercount = $membercounts[$conv->id]->membercount; $conv->isfavourite = in_array($conv->id, $favouriteconversationids); $conv->isread = isset($unreadcounts[$conv->id]) ? false : true; $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null; $conv->ismuted = $conversation->ismuted ? true : false; $conv->members = $members[$conv->id]; // Add the most recent message information. $conv->messages = []; // Add if the user has to allow delete messages for all users in the conversation. $conv->candeletemessagesforallusers = has_capability('moodle/site:deleteanymessage', $contexttodeletemessageforall); if ($conversation->smallmessage) { $msg = new \stdClass(); $msg->id = $conversation->messageid; $msg->text = message_format_message_text($conversation); $msg->useridfrom = $conversation->useridfrom; $msg->timecreated = $conversation->timecreated; $conv->messages[] = $msg; } $arrconversations[] = $conv; } return $arrconversations; } /** * Returns all conversations between two users * * @param int $userid1 One of the user's id * @param int $userid2 The other user's id * @param int $limitfrom * @param int $limitnum * @return array * @throws \dml_exception */ public static function get_conversations_between_users(int $userid1, int $userid2, int $limitfrom = 0, int $limitnum = 20) : array { global $DB; if ($userid1 == $userid2) { return array(); } // Get all conversation where both user1 and user2 are members. // TODO: Add subname value. Waiting for definite table structure. $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated FROM {message_conversations} mc INNER JOIN {message_conversation_members} mcm1 ON mc.id = mcm1.conversationid INNER JOIN {message_conversation_members} mcm2 ON mc.id = mcm2.conversationid WHERE mcm1.userid = :userid1 AND mcm2.userid = :userid2 AND mc.enabled != 0 ORDER BY mc.timecreated DESC"; return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum); } /** * Return a conversation. * * @param int $userid The user id to get the conversation for * @param int $conversationid The id of the conversation to fetch * @param bool $includecontactrequests Should contact requests be included between members * @param bool $includeprivacyinfo Should privacy info be included between members * @param int $memberlimit Limit number of members to load * @param int $memberoffset Offset members by this amount * @param int $messagelimit Limit number of messages to load * @param int $messageoffset Offset the messages * @param bool $newestmessagesfirst Order messages by newest first * @return \stdClass */ public static function get_conversation( int $userid, int $conversationid, bool $includecontactrequests = false, bool $includeprivacyinfo = false, int $memberlimit = 0, int $memberoffset = 0, int $messagelimit = 0, int $messageoffset = 0, bool $newestmessagesfirst = true ) { global $USER, $DB; $systemcontext = \context_system::instance(); $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext); if (($USER->id != $userid) && !$canreadallmessages) { throw new \moodle_exception('You do not have permission to perform this action.'); } $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]); if (!$conversation) { return null; } // Get the context of the conversation. This will be used to check whether the conversation is a favourite. // This will be either 'user' (for individual conversations) or, in the case of linked conversations, // the context stored in the record. $userctx = \context_user::instance($userid); $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid); $isconversationmember = $DB->record_exists( 'message_conversation_members', [ 'conversationid' => $conversationid, 'userid' => $userid ] ); if (!$isconversationmember && !$canreadallmessages) { throw new \moodle_exception('You do not have permission to view this conversation.'); } $members = self::get_conversation_members( $userid, $conversationid, $includecontactrequests, $includeprivacyinfo, $memberoffset, $memberlimit ); if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) { // Strip out the requesting user to match what get_conversations does, except for self-conversations. $members = array_filter($members, function($member) use ($userid) { return $member->id != $userid; }); } $messages = self::get_conversation_messages( $userid, $conversationid, $messageoffset, $messagelimit, $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC' ); $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx); $convextrafields = self::get_linked_conversation_extra_fields([$conversation]); $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null; $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null; $unreadcountssql = 'SELECT count(m.id) FROM {messages} m INNER JOIN {message_conversations} mc ON mc.id = m.conversationid LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND (mua.action = ? OR mua.action = ?)) WHERE m.conversationid = ? AND m.useridfrom != ? AND mua.id is NULL'; $unreadcount = $DB->count_records_sql( $unreadcountssql, [ $userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED, $conversationid, $userid ] ); $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]); $ismuted = false; if ($DB->record_exists('message_conversation_actions', ['userid' => $userid, 'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) { $ismuted = true; } // Get the context of the conversation. This will be used to check if the user can delete all messages in the conversation. $deleteallcontext = empty($conversation->contextid) ? $systemcontext : \context::instance_by_id($conversation->contextid); return (object) [ 'id' => $conversation->id, 'name' => $conversation->name, 'subname' => $subname, 'imageurl' => $imageurl, 'type' => $conversation->type, 'membercount' => $membercount, 'isfavourite' => $isfavourite, 'isread' => empty($unreadcount), 'unreadcount' => $unreadcount, 'ismuted' => $ismuted, 'members' => $members, 'messages' => $messages['messages'], 'candeletemessagesforallusers' => has_capability('moodle/site:deleteanymessage', $deleteallcontext) ]; } /** * Mark a conversation as a favourite for the given user. * * @param int $conversationid the id of the conversation to mark as a favourite. * @param int $userid the id of the user to whom the favourite belongs. * @return favourite the favourite object. * @throws \moodle_exception if the user or conversation don't exist. */ public static function set_favourite_conversation(int $conversationid, int $userid) : favourite { global $DB; if (!self::is_user_in_conversation($userid, $conversationid)) { throw new \moodle_exception("Conversation doesn't exist or user is not a member"); } // Get the context for this conversation. $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]); $userctx = \context_user::instance($userid); if (empty($conversation->contextid)) { // When the conversation hasn't any contextid value defined, the favourite will be added to the user context. $conversationctx = $userctx; } else { // If the contextid is defined, the favourite will be added there. $conversationctx = \context::instance_by_id($conversation->contextid); } $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx); if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) { return $favourite; } else { return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx); } } /** * Unset a conversation as a favourite for the given user. * * @param int $conversationid the id of the conversation to unset as a favourite. * @param int $userid the id to whom the favourite belongs. * @throws \moodle_exception if the favourite does not exist for the user. */ public static function unset_favourite_conversation(int $conversationid, int $userid) { global $DB; // Get the context for this conversation. $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]); $userctx = \context_user::instance($userid); if (empty($conversation->contextid)) { // When the conversation hasn't any contextid value defined, the favourite will be added to the user context. $conversationctx = $userctx; } else { // If the contextid is defined, the favourite will be added there. $conversationctx = \context::instance_by_id($conversation->contextid); } $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx); $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx); } /** * @deprecated since 3.6 */ public static function get_contacts() { throw new \coding_exception('\core_message\api::get_contacts has been removed.'); } /** * Get the contacts for a given user. * * @param int $userid * @param int $limitfrom * @param int $limitnum * @return array An array of contacts */ public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) { global $DB; $sql = "SELECT * FROM {message_contacts} mc WHERE mc.userid = ? OR mc.contactid = ? ORDER BY timecreated DESC, id ASC"; if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) { $userids = []; foreach ($contacts as $contact) { if ($contact->userid == $userid) { $userids[] = $contact->contactid; } else { $userids[] = $contact->userid; } } return helper::get_member_info($userid, $userids); } return []; } /** * Returns the contacts count. * * @param int $userid The user id * @return array */ public static function count_contacts(int $userid) : int { global $DB; $sql = "SELECT COUNT(id) FROM {message_contacts} WHERE userid = ? OR contactid = ?"; return $DB->count_records_sql($sql, [$userid, $userid]); } /** * Returns the an array of the users the given user is in a conversation * with who are a contact and the number of unread messages. * * @deprecated since 3.10 * TODO: MDL-69643 * @param int $userid The user id * @param int $limitfrom * @param int $limitnum * @return array */ public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) { global $DB; debugging('\core_message\api::get_contacts_with_unread_message_count is deprecated and no longer used', DEBUG_DEVELOPER); $userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess'); $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount FROM {message_contacts} mc INNER JOIN {user} u ON (u.id = mc.contactid OR u.id = mc.userid) LEFT JOIN {messages} m ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?) LEFT JOIN {message_conversation_members} mcm ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?) LEFT JOIN {message_users_blocked} mub ON (mub.userid = ? AND mub.blockeduserid = u.id) WHERE mua.id is NULL AND mub.id is NULL AND (mc.userid = ? OR mc.contactid = ?) AND u.id != ? AND u.deleted = 0 GROUP BY $userfields"; return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid, $userid], $limitfrom, $limitnum); } /** * Returns the an array of the users the given user is in a conversation * with who are not a contact and the number of unread messages. * * @deprecated since 3.10 * TODO: MDL-69643 * @param int $userid The user id * @param int $limitfrom * @param int $limitnum * @return array */ public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) { global $DB; debugging('\core_message\api::get_non_contacts_with_unread_message_count is deprecated and no longer used', DEBUG_DEVELOPER); $userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess'); $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount FROM {user} u INNER JOIN {messages} m ON m.useridfrom = u.id INNER JOIN {message_conversation_members} mcm ON mcm.conversationid = m.conversationid LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?) LEFT JOIN {message_contacts} mc ON (mc.userid = ? AND mc.contactid = u.id) LEFT JOIN {message_users_blocked} mub ON (mub.userid = ? AND mub.blockeduserid = u.id) WHERE mcm.userid = ? AND mcm.userid != m.useridfrom AND mua.id is NULL AND mub.id is NULL AND mc.id is NULL AND u.deleted = 0 GROUP BY $userfields"; return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid], $limitfrom, $limitnum); } /** * @deprecated since 3.6 */ public static function get_messages() { throw new \coding_exception('\core_message\api::get_messages has been removed.'); } /** * Returns the messages for the defined conversation. * * @param int $userid The current user. * @param int $convid The conversation where the messages belong. Could be an object or just the id. * @param int $limitfrom Return a subset of records, starting at this point (optional). * @param int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set). * @param string $sort The column name to order by including optionally direction. * @param int $timefrom The time from the message being sent. * @param int $timeto The time up until the message being sent. * @return array of messages */ public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0, string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array { if (!empty($timefrom)) { // Check the cache to see if we even need to do a DB query. $cache = \cache::make('core', 'message_time_last_message_between_users'); $key = helper::get_last_message_time_created_cache_key($convid); $lastcreated = $cache->get($key); // The last known message time is earlier than the one being requested so we can // just return an empty result set rather than having to query the DB. if ($lastcreated && $lastcreated < $timefrom) { return helper::format_conversation_messages($userid, $convid, []); } } $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto); return helper::format_conversation_messages($userid, $convid, $messages); } /** * @deprecated since 3.6 */ public static function get_most_recent_message() { throw new \coding_exception('\core_message\api::get_most_recent_message has been removed.'); } /** * Returns the most recent message in a conversation. * * @param int $convid The conversation identifier. * @param int $currentuserid The current user identifier. * @return \stdClass|null The most recent message. */ public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) { global $USER; if (empty($currentuserid)) { $currentuserid = $USER->id; } if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) { $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages); return array_pop($convmessages['messages']); } return null; } /** * @deprecated since 3.6 */ public static function get_profile() { throw new \coding_exception('\core_message\api::get_profile has been removed.'); } /** * Checks if a user can delete messages they have either received or sent. * * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin * but will still seem as if it was by the user) * @param int $conversationid The id of the conversation * @return bool Returns true if a user can delete the conversation, false otherwise. */ public static function can_delete_conversation(int $userid, int $conversationid = null) : bool { global $USER; if (is_null($conversationid)) { debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.', DEBUG_DEVELOPER); return false; } $systemcontext = \context_system::instance(); if (has_capability('moodle/site:deleteanymessage', $systemcontext)) { return true; } if (!self::is_user_in_conversation($userid, $conversationid)) { return false; } if (has_capability('moodle/site:deleteownmessage', $systemcontext) && $USER->id == $userid) { return true; } return false; } /** * @deprecated since 3.6 */ public static function delete_conversation() { throw new \coding_exception('\core_message\api::delete_conversation() is deprecated, please use ' . '\core_message\api::delete_conversation_by_id() instead.'); } /** * Deletes a conversation for a specified user. * * This function does not verify any permissions. * * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin * but will still seem as if it was by the user) * @param int $conversationid The id of the other user in the conversation */ public static function delete_conversation_by_id(int $userid, int $conversationid) { global $DB, $USER; // Get all messages belonging to this conversation that have not already been deleted by this user. $sql = "SELECT m.* FROM {messages} m INNER JOIN {message_conversations} mc ON m.conversationid = mc.id LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?) WHERE mua.id is NULL AND mc.id = ? ORDER BY m.timecreated ASC"; $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]); // Ok, mark these as deleted. foreach ($messages as $message) { $mua = new \stdClass(); $mua->userid = $userid; $mua->messageid = $message->id; $mua->action = self::MESSAGE_ACTION_DELETED; $mua->timecreated = time(); $mua->id = $DB->insert_record('message_user_actions', $mua); \core\event\message_deleted::create_from_ids($userid, $USER->id, $message->id, $mua->id)->trigger(); } } /** * Returns the count of unread conversations (collection of messages from a single user) for * the given user. * * @param \stdClass $user the user who's conversations should be counted * @return int the count of the user's unread conversations */ public static function count_unread_conversations($user = null) { global $USER, $DB; if (empty($user)) { $user = $USER; } $sql = "SELECT COUNT(DISTINCT(m.conversationid)) FROM {messages} m INNER JOIN {message_conversations} mc ON m.conversationid = mc.id INNER JOIN {message_conversation_members} mcm ON mc.id = mcm.conversationid LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?) WHERE mcm.userid = ? AND mc.enabled = ? AND mcm.userid != m.useridfrom AND mua.id is NULL"; return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id, self::MESSAGE_CONVERSATION_ENABLED]); } /** * Checks if a user can mark all messages as read. * * @param int $userid The user id of who we want to mark the messages for * @param int $conversationid The id of the conversation * @return bool true if user is permitted, false otherwise * @since 3.6 */ public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool { global $USER; $systemcontext = \context_system::instance(); if (has_capability('moodle/site:readallmessages', $systemcontext)) { return true; } if (!self::is_user_in_conversation($userid, $conversationid)) { return false; } if ($USER->id == $userid) { return true; } return false; } /** * Returns the count of conversations (collection of messages from a single user) for * the given user. * * @param int $userid The user whose conversations should be counted. * @return array the array of conversations counts, indexed by type. */ public static function get_conversation_counts(int $userid) : array { global $DB; self::lazy_create_self_conversation($userid); // Some restrictions we need to be aware of: // - Individual conversations containing soft-deleted user must be counted. // - Individual conversations containing only deleted messages must NOT be counted. // - Self-conversations with 0 messages must be counted. // - Self-conversations containing only deleted messages must NOT be counted. // - Group conversations with 0 messages must be counted. // - Linked conversations which are disabled (enabled = 0) must NOT be counted. // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere. // First, ask the favourites service to give us the join SQL for favourited conversations, // so we can include favourite information in the query. $usercontext = \context_user::instance($userid); $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id'); $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage FROM {message_conversations} mc INNER JOIN {message_conversation_members} mcm ON mcm.conversationid = mc.id LEFT JOIN ( SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime FROM {messages} m INNER JOIN {message_conversation_members} mcm ON mcm.conversationid = m.conversationid LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action) WHERE mua.id is NULL AND mcm.userid = :userid2 GROUP BY m.conversationid ) maxvisibleconvmessage ON maxvisibleconvmessage.convid = mc.id $favsql WHERE mcm.userid = :userid3 AND mc.enabled = :enabled AND ( (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR (mc.type = :grouptype) OR (mc.type = :selftype) ) GROUP BY mc.type, fav.itemtype ORDER BY mc.type ASC"; $params = [ 'userid' => $userid, 'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'userid5' => $userid, 'action' => self::MESSAGE_ACTION_DELETED, 'enabled' => self::MESSAGE_CONVERSATION_ENABLED, 'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, 'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP, 'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF, ] + $favparams; // Assemble the return array. $counts = [ 'favourites' => 0, 'types' => [ self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0, self::MESSAGE_CONVERSATION_TYPE_GROUP => 0, self::MESSAGE_CONVERSATION_TYPE_SELF => 0 ] ]; // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied). $selfmessagessql = "SELECT COUNT(m.id) FROM {messages} m INNER JOIN {message_conversations} mc ON mc.id = m.conversationid WHERE mc.type = ? AND convhash = ?"; $selfmessagestotal = $DB->count_records_sql( $selfmessagessql, [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])] ); $countsrs = $DB->get_recordset_sql($sql, $params); foreach ($countsrs as $key => $val) { // Empty self-conversations with deleted messages should be excluded. if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) { continue; } if (!empty($val->itemtype)) { $counts['favourites'] += $val->count; continue; } $counts['types'][$val->type] = $val->count; } $countsrs->close(); return $counts; } /** * Marks all messages being sent to a user in a particular conversation. * * If $conversationdid is null then it marks all messages as read sent to $userid. * * @param int $userid * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all */ public static function mark_all_messages_as_read($userid, $conversationid = null) { global $DB; $messagesql = "SELECT m.* FROM {messages} m INNER JOIN {message_conversations} mc ON mc.id = m.conversationid INNER JOIN {message_conversation_members} mcm ON mcm.conversationid = mc.id LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?) WHERE mua.id is NULL AND mcm.userid = ? AND m.useridfrom != ?"; $messageparams = []; $messageparams[] = $userid; $messageparams[] = self::MESSAGE_ACTION_READ; $messageparams[] = $userid; $messageparams[] = $userid; if (!is_null($conversationid)) { $messagesql .= " AND mc.id = ?"; $messageparams[] = $conversationid; } $messages = $DB->get_recordset_sql($messagesql, $messageparams); foreach ($messages as $message) { self::mark_message_as_read($userid, $message); } $messages->close(); } /** * Marks all notifications being sent from one user to another user as read. * * If the from user is null then it marks all notifications as read sent to the to user. * * @param int $touserid the id of the message recipient * @param int|null $fromuserid the id of the message sender, null if all messages * @param int|null $timecreatedto mark notifications created before this time as read * @return void */ public static function mark_all_notifications_as_read($touserid, $fromuserid = null, $timecreatedto = null) { global $DB; $notificationsql = "SELECT n.* FROM {notifications} n WHERE useridto = ? AND timeread is NULL"; $notificationsparams = [$touserid]; if (!empty($fromuserid)) { $notificationsql .= " AND useridfrom = ?"; $notificationsparams[] = $fromuserid; } if (!empty($timecreatedto)) { $notificationsql .= " AND timecreated <= ?"; $notificationsparams[] = $timecreatedto; } $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams); foreach ($notifications as $notification) { self::mark_notification_as_read($notification); } $notifications->close(); } /** * @deprecated since 3.5 */ public static function mark_all_read_for_user() { throw new \coding_exception('\core_message\api::mark_all_read_for_user has been removed. Please either use ' . '\core_message\api::mark_all_notifications_as_read or \core_message\api::mark_all_messages_as_read'); } /** * Returns message preferences. * * @param array $processors * @param array $providers * @param \stdClass $user * @return \stdClass * @since 3.2 */ public static function get_all_message_preferences($processors, $providers, $user) { $preferences = helper::get_providers_preferences($providers, $user->id); $preferences->userdefaultemail = $user->email; // May be displayed by the email processor. // For every processors put its options on the form (need to get function from processor's lib.php). foreach ($processors as $processor) { $processor->object->load_data($preferences, $user->id); } // Load general messaging preferences. $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id); $preferences->mailformat = $user->mailformat; $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id); return $preferences; } /** * Count the number of users blocked by a user. * * @param \stdClass $user The user object * @return int the number of blocked users */ public static function count_blocked_users($user = null) { global $USER, $DB; if (empty($user)) { $user = $USER; } $sql = "SELECT count(mub.id) FROM {message_users_blocked} mub WHERE mub.userid = :userid"; return $DB->count_records_sql($sql, array('userid' => $user->id)); } /** * @deprecated since 3.8 */ public static function can_post_message() { throw new \coding_exception( '\core_message\api::can_post_message is deprecated and no longer used, ' . 'please use \core_message\api::can_send_message instead.' ); } /** * Determines if a user is permitted to send another user a private message. * * @param int $recipientid The recipient user id. * @param int $senderid The sender user id. * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user * the user is still able to send a message. * @return bool true if user is permitted, false otherwise. */ public static function can_send_message(int $recipientid, int $senderid, bool $evenifblocked = false) : bool { $systemcontext = \context_system::instance(); if (!has_capability('moodle/site:sendmessage', $systemcontext, $senderid)) { return false; } if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) { return true; } // Check if the recipient can be messaged by the sender. return self::can_contact_user($recipientid, $senderid, $evenifblocked); } /** * Determines if a user is permitted to send a message to a given conversation. * If no sender is provided then it defaults to the logged in user. * * @param int $userid the id of the user on which the checks will be applied. * @param int $conversationid the id of the conversation we wish to check. * @return bool true if the user can send a message to the conversation, false otherwise. * @throws \moodle_exception */ public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool { global $DB; $systemcontext = \context_system::instance(); if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) { return false; } if (!self::is_user_in_conversation($userid, $conversationid)) { return false; } // User can post messages and is in the conversation, but we need to check the conversation type to // know whether or not to check the user privacy settings via can_contact_user(). $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST); if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP || $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) { return true; } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { // Get the other user in the conversation. $members = self::get_conversation_members($userid, $conversationid); $otheruser = array_filter($members, function($member) use($userid) { return $member->id != $userid; }); $otheruser = reset($otheruser); return self::can_contact_user($otheruser->id, $userid); } else { throw new \moodle_exception("Invalid conversation type '$conversation->type'."); } } /** * Send a message from a user to a conversation. * * This method will create the basic eventdata and delegate to message creation to message_send. * The message_send() method is responsible for event data that is specific to each recipient. * * @param int $userid the sender id. * @param int $conversationid the conversation id. * @param string $message the message to send. * @param int $format the format of the message to send. * @return \stdClass the message created. * @throws \coding_exception * @throws \moodle_exception if the user is not permitted to send a message to the conversation. */ public static function send_message_to_conversation(int $userid, int $conversationid, string $message, int $format) : \stdClass { global $DB, $PAGE; if (!self::can_send_message_to_conversation($userid, $conversationid)) { throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid"); } $eventdata = new \core\message\message(); $eventdata->courseid = 1; $eventdata->component = 'moodle'; $eventdata->name = 'instantmessage'; $eventdata->userfrom = \core_user::get_user($userid); $eventdata->convid = $conversationid; if ($format == FORMAT_HTML) { $eventdata->fullmessagehtml = $message; // Some message processors may revert to sending plain text even if html is supplied, // so we keep both plain and html versions if we're intending to send html. $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml); } else { $eventdata->fullmessage = $message; $eventdata->fullmessagehtml = ''; } $eventdata->fullmessageformat = $format; $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output. $eventdata->timecreated = time(); $eventdata->notification = 0; // Custom data for event. $customdata = [ 'actionbuttons' => [ 'send' => get_string('send', 'message'), ], 'placeholders' => [ 'send' => get_string('writeamessage', 'message'), ], ]; $userpicture = new \user_picture($eventdata->userfrom); $userpicture->size = 1; // Use f1 size. $userpicture = $userpicture->get_url($PAGE)->out(false); $conv = $DB->get_record('message_conversations', ['id' => $conversationid]); if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) { $convextrafields = self::get_linked_conversation_extra_fields([$conv]); // Conversation images. $customdata['notificationsendericonurl'] = $userpicture; $imageurl = isset($convextrafields[$conv->id]) ? $convextrafields[$conv->id]['imageurl'] : null; if ($imageurl) { $customdata['notificationiconurl'] = $imageurl; } // Conversation name. if (is_null($conv->contextid)) { $convcontext = \context_user::instance($userid); } else { $convcontext = \context::instance_by_id($conv->contextid); } $customdata['conversationname'] = format_string($conv->name, true, ['context' => $convcontext]); } else if ($conv->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { $customdata['notificationiconurl'] = $userpicture; } $eventdata->customdata = $customdata; $messageid = message_send($eventdata); if (!$messageid) { throw new \moodle_exception('messageundeliveredbynotificationsettings', 'moodle'); } $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage, timecreated, fullmessagetrust'); $message = (object) [ 'id' => $messagerecord->id, 'useridfrom' => $messagerecord->useridfrom, 'text' => $messagerecord->fullmessage, 'timecreated' => $messagerecord->timecreated, 'fullmessagetrust' => $messagerecord->fullmessagetrust ]; return $message; } /** * Get the messaging preference for a user. * If the user has not any messaging privacy preference: * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER. * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE. * * @param int $userid The user identifier. * @return int The default messaging preference. */ public static function get_user_privacy_messaging_preference(int $userid) : int { global $CFG, $USER; // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site"; // otherwise, the default value will be "My contacts and anyone in my courses". if (empty($CFG->messagingallusers)) { $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER; } else { $defaultprefvalue = self::MESSAGE_PRIVACY_SITE; } if ($userid == $USER->id) { $user = $USER; } else { $user = $userid; } $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user); // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER. if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) { $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER; } return $privacypreference; } /** * @deprecated since 3.6 */ public static function is_user_non_contact_blocked() { throw new \coding_exception('\core_message\api::is_user_non_contact_blocked() is deprecated'); } /** * @deprecated since 3.6 */ public static function is_user_blocked() { throw new \coding_exception('\core_message\api::is_user_blocked is deprecated and should not be used.'); } /** * Get specified message processor, validate corresponding plugin existence and * system configuration. * * @param string $name Name of the processor. * @param bool $ready only return ready-to-use processors. * @return mixed $processor if processor present else empty array. * @since Moodle 3.2 */ public static function get_message_processor($name, $ready = false) { global $DB, $CFG; $processor = $DB->get_record('message_processors', array('name' => $name)); if (empty($processor)) { // Processor not found, return. return array(); } $processor = self::get_processed_processor_object($processor); if ($ready) { if ($processor->enabled && $processor->configured) { return $processor; } else { return array(); } } else { return $processor; } } /** * Returns weather a given processor is enabled or not. * Note:- This doesn't check if the processor is configured or not. * * @param string $name Name of the processor * @return bool */ public static function is_processor_enabled($name) { $cache = \cache::make('core', 'message_processors_enabled'); $status = $cache->get($name); if ($status === false) { $processor = self::get_message_processor($name); if (!empty($processor)) { $cache->set($name, $processor->enabled); return $processor->enabled; } else { return false; } } return $status; } /** * Set status of a processor. * * @param \stdClass $processor processor record. * @param 0|1 $enabled 0 or 1 to set the processor status. * @return bool * @since Moodle 3.2 */ public static function update_processor_status($processor, $enabled) { global $DB; $cache = \cache::make('core', 'message_processors_enabled'); $cache->delete($processor->name); return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id)); } /** * Given a processor object, loads information about it's settings and configurations. * This is not a public api, instead use @see \core_message\api::get_message_processor() * or @see \get_message_processors() * * @param \stdClass $processor processor object * @return \stdClass processed processor object * @since Moodle 3.2 */ public static function get_processed_processor_object(\stdClass $processor) { global $CFG; $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php'; if (is_readable($processorfile)) { include_once($processorfile); $processclass = 'message_output_' . $processor->name; if (class_exists($processclass)) { $pclass = new $processclass(); $processor->object = $pclass; $processor->configured = 0; if ($pclass->is_system_configured()) { $processor->configured = 1; } $processor->hassettings = 0; if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) { $processor->hassettings = 1; } $processor->available = 1; } else { throw new \moodle_exception('errorcallingprocessor', 'message'); } } else { $processor->available = 0; } return $processor; } /** * Retrieve users blocked by $user1 * * @param int $userid The user id of the user whos blocked users we are returning * @return array the users blocked */ public static function get_blocked_users($userid) { global $DB; $userfieldsapi = \core_user\fields::for_userpic()->including('lastaccess'); $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $blockeduserssql = "SELECT $userfields FROM {message_users_blocked} mub INNER JOIN {user} u ON u.id = mub.blockeduserid WHERE u.deleted = 0 AND mub.userid = ? GROUP BY $userfields ORDER BY u.firstname ASC"; return $DB->get_records_sql($blockeduserssql, [$userid]); } /** * Mark a single message as read. * * @param int $userid The user id who marked the message as read * @param \stdClass $message The message * @param int|null $timeread The time the message was marked as read, if null will default to time() */ public static function mark_message_as_read($userid, $message, $timeread = null) { global $DB; if (is_null($timeread)) { $timeread = time(); } $mua = new \stdClass(); $mua->userid = $userid; $mua->messageid = $message->id; $mua->action = self::MESSAGE_ACTION_READ; $mua->timecreated = $timeread; $mua->id = $DB->insert_record('message_user_actions', $mua); // Get the context for the user who received the message. $context = \context_user::instance($userid, IGNORE_MISSING); // If the user no longer exists the context value will be false, in this case use the system context. if ($context === false) { $context = \context_system::instance(); } // Trigger event for reading a message. $event = \core\event\message_viewed::create(array( 'objectid' => $mua->id, 'userid' => $userid, // Using the user who read the message as they are the ones performing the action. 'context' => $context, 'relateduserid' => $message->useridfrom, 'other' => array( 'messageid' => $message->id ) )); $event->trigger(); } /** * Mark a single notification as read. * * @param \stdClass $notification The notification * @param int|null $timeread The time the message was marked as read, if null will default to time() */ public static function mark_notification_as_read($notification, $timeread = null) { global $DB; if (is_null($timeread)) { $timeread = time(); } if (is_null($notification->timeread)) { $updatenotification = new \stdClass(); $updatenotification->id = $notification->id; $updatenotification->timeread = $timeread; $DB->update_record('notifications', $updatenotification); // Trigger event for reading a notification. \core\event\notification_viewed::create_from_ids( $notification->useridfrom, $notification->useridto, $notification->id )->trigger(); } } /** * Checks if a user can delete a message. * * @param int $userid the user id of who we want to delete the message for (this may be done by the admin * but will still seem as if it was by the user) * @param int $messageid The message id * @return bool Returns true if a user can delete the message, false otherwise. */ public static function can_delete_message($userid, $messageid) { global $DB, $USER; $systemcontext = \context_system::instance(); $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST); if (has_capability('moodle/site:deleteanymessage', $systemcontext)) { return true; } if (!self::is_user_in_conversation($userid, $conversationid)) { return false; } if (has_capability('moodle/site:deleteownmessage', $systemcontext) && $USER->id == $userid) { return true; } return false; } /** * Deletes a message. * * This function does not verify any permissions. * * @param int $userid the user id of who we want to delete the message for (this may be done by the admin * but will still seem as if it was by the user) * @param int $messageid The message id * @return bool */ public static function delete_message($userid, $messageid) { global $DB, $USER; if (!$DB->record_exists('messages', ['id' => $messageid])) { return false; } // Check if the user has already deleted this message. if (!$DB->record_exists('message_user_actions', ['userid' => $userid, 'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) { $mua = new \stdClass(); $mua->userid = $userid; $mua->messageid = $messageid; $mua->action = self::MESSAGE_ACTION_DELETED; $mua->timecreated = time(); $mua->id = $DB->insert_record('message_user_actions', $mua); // Trigger event for deleting a message. \core\event\message_deleted::create_from_ids($userid, $USER->id, $messageid, $mua->id)->trigger(); return true; } return false; } /** * Returns the conversation between two users. * * @param array $userids * @return int|bool The id of the conversation, false if not found */ public static function get_conversation_between_users(array $userids) { global $DB; if (empty($userids)) { return false; } $hash = helper::get_conversation_hash($userids); if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, 'convhash' => $hash])) { return $conversation->id; } return false; } /** * @deprecated since 3.8 */ public static function get_individual_conversations_between_users() { throw new \coding_exception('\core_message\api::get_individual_conversations_between_users ' . ' is deprecated and no longer used.'); } /** * Returns the self conversation for a user. * * @param int $userid The user id to get the self-conversations * @return \stdClass|false The self-conversation object or false if it doesn't exist * @since Moodle 3.7 */ public static function get_self_conversation(int $userid) { global $DB; self::lazy_create_self_conversation($userid); $conditions = [ 'type' => self::MESSAGE_CONVERSATION_TYPE_SELF, 'convhash' => helper::get_conversation_hash([$userid]) ]; return $DB->get_record('message_conversations', $conditions); } /** * @deprecated since 3.6 */ public static function create_conversation_between_users() { throw new \coding_exception('\core_message\api::create_conversation_between_users is deprecated, please use ' . '\core_message\api::create_conversation instead.'); } /** * Creates a conversation with selected users and messages. * * @param int $type The type of conversation * @param int[] $userids The array of users to add to the conversation * @param string|null $name The name of the conversation * @param int $enabled Determines if the conversation is created enabled or disabled * @param string|null $component Defines the Moodle component which the conversation belongs to, if any * @param string|null $itemtype Defines the type of the component * @param int|null $itemid The id of the component * @param int|null $contextid The id of the context * @return \stdClass */ public static function create_conversation(int $type, array $userids, string $name = null, int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null, string $itemtype = null, int $itemid = null, int $contextid = null) { global $DB; $validtypes = [ self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF ]; if (!in_array($type, $validtypes)) { throw new \moodle_exception('An invalid conversation type was specified.'); } // Sanity check. if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { if (count($userids) > 2) { throw new \moodle_exception('An individual conversation can not have more than two users.'); } if ($userids[0] == $userids[1]) { throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.'); } } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) { if (count($userids) != 1) { throw new \moodle_exception('A self conversation can not have more than one user.'); } } $conversation = new \stdClass(); $conversation->type = $type; $conversation->name = $name; $conversation->convhash = null; if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) { $conversation->convhash = helper::get_conversation_hash($userids); // Don't blindly create a conversation between 2 users if there is already one present - return that. // This stops us making duplicate self and individual conversations, which is invalid. if ($record = $DB->get_record('message_conversations', ['convhash' => $conversation->convhash])) { return $record; } } $conversation->component = $component; $conversation->itemtype = $itemtype; $conversation->itemid = $itemid; $conversation->contextid = $contextid; $conversation->enabled = $enabled; $conversation->timecreated = time(); $conversation->timemodified = $conversation->timecreated; $conversation->id = $DB->insert_record('message_conversations', $conversation); // Add users to this conversation. $arrmembers = []; foreach ($userids as $userid) { $member = new \stdClass(); $member->conversationid = $conversation->id; $member->userid = $userid; $member->timecreated = time(); $member->id = $DB->insert_record('message_conversation_members', $member); $arrmembers[] = $member; } $conversation->members = $arrmembers; return $conversation; } /** * Checks if a user can create a group conversation. * * @param int $userid The id of the user attempting to create the conversation * @param \context $context The context they are creating the conversation from, most likely course context * @return bool */ public static function can_create_group_conversation(int $userid, \context $context) : bool { global $CFG; // If we can't message at all, then we can't create a conversation. if (empty($CFG->messaging)) { return false; } // We need to check they have the capability to create the conversation. return has_capability('moodle/course:creategroupconversations', $context, $userid); } /** * Checks if a user can create a contact request. * * @param int $userid The id of the user who is creating the contact request * @param int $requesteduserid The id of the user being requested * @return bool */ public static function can_create_contact(int $userid, int $requesteduserid) : bool { global $CFG; // If we can't message at all, then we can't create a contact. if (empty($CFG->messaging)) { return false; } // If we can message anyone on the site then we can create a contact. if ($CFG->messagingallusers) { return true; } // We need to check if they are in the same course. return enrol_sharing_course($userid, $requesteduserid); } /** * Handles creating a contact request. * * @param int $userid The id of the user who is creating the contact request * @param int $requesteduserid The id of the user being requested * @return \stdClass the request */ public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass { global $DB, $PAGE, $SITE; $request = new \stdClass(); $request->userid = $userid; $request->requesteduserid = $requesteduserid; $request->timecreated = time(); $request->id = $DB->insert_record('message_contact_requests', $request); // Send a notification. $userfrom = \core_user::get_user($userid); $userfromfullname = fullname($userfrom); $userto = \core_user::get_user($requesteduserid); $url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']); $subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [ 'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]), 'user' => $userfromfullname, ], $userto->lang); $fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [ 'url' => $url->out(), 'user' => $userfromfullname, ], $userto->lang); $message = new \core\message\message(); $message->courseid = SITEID; $message->component = 'moodle'; $message->name = 'messagecontactrequests'; $message->notification = 1; $message->userfrom = $userfrom; $message->userto = $userto; $message->subject = $subject; $message->fullmessage = text_to_html($fullmessage); $message->fullmessageformat = FORMAT_HTML; $message->fullmessagehtml = $fullmessage; $message->smallmessage = ''; $message->contexturl = $url->out(false); $userpicture = new \user_picture($userfrom); $userpicture->size = 1; // Use f1 size. $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message. $message->customdata = [ 'notificationiconurl' => $userpicture->get_url($PAGE)->out(false), 'actionbuttons' => [ 'accept' => get_string_manager()->get_string('accept', 'moodle', null, $userto->lang), 'reject' => get_string_manager()->get_string('reject', 'moodle', null, $userto->lang), ], ]; message_send($message); return $request; } /** * Handles confirming a contact request. * * @param int $userid The id of the user who created the contact request * @param int $requesteduserid The id of the user confirming the request */ public static function confirm_contact_request(int $userid, int $requesteduserid) { global $DB; if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid, 'requesteduserid' => $requesteduserid])) { self::add_contact($userid, $requesteduserid); $DB->delete_records('message_contact_requests', ['id' => $request->id]); } } /** * Handles declining a contact request. * * @param int $userid The id of the user who created the contact request * @param int $requesteduserid The id of the user declining the request */ public static function decline_contact_request(int $userid, int $requesteduserid) { global $DB; if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid, 'requesteduserid' => $requesteduserid])) { $DB->delete_records('message_contact_requests', ['id' => $request->id]); } } /** * Handles returning the contact requests for a user. * * This also includes the user data necessary to display information * about the user. * * It will not include blocked users. * * @param int $userid * @param int $limitfrom * @param int $limitnum * @return array The list of contact requests */ public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array { global $DB; $sql = "SELECT mcr.userid FROM {message_contact_requests} mcr LEFT JOIN {message_users_blocked} mub ON (mub.userid = ? AND mub.blockeduserid = mcr.userid) WHERE mcr.requesteduserid = ? AND mub.id is NULL ORDER BY mcr.timecreated ASC"; if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) { $userids = array_keys($contactrequests); return helper::get_member_info($userid, $userids); } return []; } /** * Returns the number of contact requests the user has received. * * @param int $userid The ID of the user we want to return the number of received contact requests for * @return int The count */ public static function get_received_contact_requests_count(int $userid) : int { global $DB; $sql = "SELECT COUNT(mcr.id) FROM {message_contact_requests} mcr LEFT JOIN {message_users_blocked} mub ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid WHERE mcr.requesteduserid = :requesteduserid AND mub.id IS NULL"; $params = ['requesteduserid' => $userid]; return $DB->count_records_sql($sql, $params); } /** * Handles adding a contact. * * @param int $userid The id of the user who requested to be a contact * @param int $contactid The id of the contact */ public static function add_contact(int $userid, int $contactid) { global $DB; $messagecontact = new \stdClass(); $messagecontact->userid = $userid; $messagecontact->contactid = $contactid; $messagecontact->timecreated = time(); $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact); $eventparams = [ 'objectid' => $messagecontact->id, 'userid' => $userid, 'relateduserid' => $contactid, 'context' => \context_user::instance($userid) ]; $event = \core\event\message_contact_added::create($eventparams); $event->add_record_snapshot('message_contacts', $messagecontact); $event->trigger(); } /** * Handles removing a contact. * * @param int $userid The id of the user who is removing a user as a contact * @param int $contactid The id of the user to be removed as a contact */ public static function remove_contact(int $userid, int $contactid) { global $DB; if ($contact = self::get_contact($userid, $contactid)) { $DB->delete_records('message_contacts', ['id' => $contact->id]); $event = \core\event\message_contact_removed::create(array( 'objectid' => $contact->id, 'userid' => $userid, 'relateduserid' => $contactid, 'context' => \context_user::instance($userid) )); $event->add_record_snapshot('message_contacts', $contact); $event->trigger(); } } /** * Handles blocking a user. * * @param int $userid The id of the user who is blocking * @param int $usertoblockid The id of the user being blocked */ public static function block_user(int $userid, int $usertoblockid) { global $DB; $blocked = new \stdClass(); $blocked->userid = $userid; $blocked->blockeduserid = $usertoblockid; $blocked->timecreated = time(); $blocked->id = $DB->insert_record('message_users_blocked', $blocked); // Trigger event for blocking a contact. $event = \core\event\message_user_blocked::create(array( 'objectid' => $blocked->id, 'userid' => $userid, 'relateduserid' => $usertoblockid, 'context' => \context_user::instance($userid) )); $event->add_record_snapshot('message_users_blocked', $blocked); $event->trigger(); } /** * Handles unblocking a user. * * @param int $userid The id of the user who is unblocking * @param int $usertounblockid The id of the user being unblocked */ public static function unblock_user(int $userid, int $usertounblockid) { global $DB; if ($blockeduser = $DB->get_record('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $usertounblockid])) { $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]); // Trigger event for unblocking a contact. $event = \core\event\message_user_unblocked::create(array( 'objectid' => $blockeduser->id, 'userid' => $userid, 'relateduserid' => $usertounblockid, 'context' => \context_user::instance($userid) )); $event->add_record_snapshot('message_users_blocked', $blockeduser); $event->trigger(); } } /** * Checks if users are already contacts. * * @param int $userid The id of one of the users * @param int $contactid The id of the other user * @return bool Returns true if they are a contact, false otherwise */ public static function is_contact(int $userid, int $contactid) : bool { global $DB; $sql = "SELECT id FROM {message_contacts} mc WHERE (mc.userid = ? AND mc.contactid = ?) OR (mc.userid = ? AND mc.contactid = ?)"; return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]); } /** * Returns the row in the database table message_contacts that represents the contact between two people. * * @param int $userid The id of one of the users * @param int $contactid The id of the other user * @return mixed A fieldset object containing the record, false otherwise */ public static function get_contact(int $userid, int $contactid) { global $DB; $sql = "SELECT mc.* FROM {message_contacts} mc WHERE (mc.userid = ? AND mc.contactid = ?) OR (mc.userid = ? AND mc.contactid = ?)"; return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]); } /** * Checks if a user is already blocked. * * @param int $userid * @param int $blockeduserid * @return bool Returns true if they are a blocked, false otherwise */ public static function is_blocked(int $userid, int $blockeduserid) : bool { global $DB; return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]); } /** * Get contact requests between users. * * @param int $userid The id of the user who is creating the contact request * @param int $requesteduserid The id of the user being requested * @return \stdClass[] */ public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array { global $DB; $sql = "SELECT * FROM {message_contact_requests} mcr WHERE (mcr.userid = ? AND mcr.requesteduserid = ?) OR (mcr.userid = ? AND mcr.requesteduserid = ?)"; return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]); } /** * Checks if a contact request already exists between users. * * @param int $userid The id of the user who is creating the contact request * @param int $requesteduserid The id of the user being requested * @return bool Returns true if a contact request exists, false otherwise */ public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool { global $DB; $sql = "SELECT id FROM {message_contact_requests} mcr WHERE (mcr.userid = ? AND mcr.requesteduserid = ?) OR (mcr.userid = ? AND mcr.requesteduserid = ?)"; return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]); } /** * Checks if a user is already in a conversation. * * @param int $userid The id of the user we want to check if they are in a group * @param int $conversationid The id of the conversation * @return bool Returns true if a contact request exists, false otherwise */ public static function is_user_in_conversation(int $userid, int $conversationid) : bool { global $DB; return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid, 'userid' => $userid]); } /** * Checks if the sender can message the recipient. * * @param int $recipientid * @param int $senderid * @param bool $evenifblocked This lets the user know, that even if the recipient has blocked the user * the user is still able to send a message. * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise. */ protected static function can_contact_user(int $recipientid, int $senderid, bool $evenifblocked = false) : bool { if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) || $recipientid == $senderid) { // The sender has the ability to contact any user across the entire site or themselves. return true; } // The initial value of $cancontact is null to indicate that a value has not been determined. $cancontact = null; if (self::is_blocked($recipientid, $senderid) || $evenifblocked) { // The recipient has specifically blocked this sender. $cancontact = false; } $sharedcourses = null; if (null === $cancontact) { // There are three user preference options: // - Site: Allow anyone not explicitly blocked to contact me; // - Course members: Allow anyone I am in a course with to contact me; and // - Contacts: Only allow my contacts to contact me. // // The Site option is only possible when the messagingallusers site setting is also enabled. $privacypreference = self::get_user_privacy_messaging_preference($recipientid); if (self::MESSAGE_PRIVACY_SITE === $privacypreference) { // The user preference is to allow any user to contact them. // No need to check anything else. $cancontact = true; } else { // This user only allows their own contacts, and possibly course peers, to contact them. // If the users are contacts then we can avoid the more expensive shared courses check. $cancontact = self::is_contact($senderid, $recipientid); if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) { // The users are not contacts and the user allows course member messaging. // Check whether these two users share any course together. $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true); $cancontact = (!empty($sharedcourses)); } } } if (false === $cancontact) { // At the moment the users cannot contact one another. // Check whether the messageanyuser capability applies in any of the shared courses. // This is intended to allow teachers to message students regardless of message settings. // Note: You cannot use empty($sharedcourses) here because this may be an empty array. if (null === $sharedcourses) { $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true); } foreach ($sharedcourses as $course) { // Note: enrol_get_shared_courses will preload any shared context. if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) { $cancontact = true; break; } } } return $cancontact; } /** * Add some new members to an existing conversation. * * @param array $userids User ids array to add as members. * @param int $convid The conversation id. Must exists. * @throws \dml_missing_record_exception If convid conversation doesn't exist * @throws \dml_exception If there is a database error * @throws \moodle_exception If trying to add a member(s) to a non-group conversation */ public static function add_members_to_conversation(array $userids, int $convid) { global $DB; $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST); // We can only add members to a group conversation. if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) { throw new \moodle_exception('You can not add members to a non-group conversation.'); } // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users. list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params); // Be sure we are not adding a user is already member of the conversation. Take all the members. $memberuserids = array_values($DB->get_records_menu( 'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid') ); // Work with existing new members. $members = array(); $newuserids = array_diff($existingusers, $memberuserids); foreach ($newuserids as $userid) { $member = new \stdClass(); $member->conversationid = $convid; $member->userid = $userid; $member->timecreated = time(); $members[] = $member; } $DB->insert_records('message_conversation_members', $members); } /** * Remove some members from an existing conversation. * * @param array $userids The user ids to remove from conversation members. * @param int $convid The conversation id. Must exists. * @throws \dml_exception * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation */ public static function remove_members_from_conversation(array $userids, int $convid) { global $DB; $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST); if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) { throw new \moodle_exception('You can not remove members from a non-group conversation.'); } list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); $params['convid'] = $convid; $DB->delete_records_select('message_conversation_members', "conversationid = :convid AND userid $useridcondition", $params); } /** * Count conversation members. * * @param int $convid The conversation id. * @return int Number of conversation members. * @throws \dml_exception */ public static function count_conversation_members(int $convid) : int { global $DB; return $DB->count_records('message_conversation_members', ['conversationid' => $convid]); } /** * Checks whether or not a conversation area is enabled. * * @param string $component Defines the Moodle component which the area was added to. * @param string $itemtype Defines the type of the component. * @param int $itemid The id of the component. * @param int $contextid The id of the context. * @return bool Returns if a conversation area exists and is enabled, false otherwise */ public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool { global $DB; return $DB->record_exists('message_conversations', [ 'itemid' => $itemid, 'contextid' => $contextid, 'component' => $component, 'itemtype' => $itemtype, 'enabled' => self::MESSAGE_CONVERSATION_ENABLED ] ); } /** * Get conversation by area. * * @param string $component Defines the Moodle component which the area was added to. * @param string $itemtype Defines the type of the component. * @param int $itemid The id of the component. * @param int $contextid The id of the context. * @return \stdClass */ public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) { global $DB; return $DB->get_record('message_conversations', [ 'itemid' => $itemid, 'contextid' => $contextid, 'component' => $component, 'itemtype' => $itemtype ] ); } /** * Enable a conversation. * * @param int $conversationid The id of the conversation. * @return void */ public static function enable_conversation(int $conversationid) { global $DB; $conversation = new \stdClass(); $conversation->id = $conversationid; $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED; $conversation->timemodified = time(); $DB->update_record('message_conversations', $conversation); } /** * Disable a conversation. * * @param int $conversationid The id of the conversation. * @return void */ public static function disable_conversation(int $conversationid) { global $DB; $conversation = new \stdClass(); $conversation->id = $conversationid; $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED; $conversation->timemodified = time(); $DB->update_record('message_conversations', $conversation); } /** * Update the name of a conversation. * * @param int $conversationid The id of a conversation. * @param string $name The main name of the area * @return void */ public static function update_conversation_name(int $conversationid, string $name) { global $DB; if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) { if ($name <> $conversation->name) { $conversation->name = $name; $conversation->timemodified = time(); $DB->update_record('message_conversations', $conversation); } } } /** * Returns a list of conversation members. * * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info. * @param int $conversationid The id of the conversation * @param bool $includecontactrequests Do we want to include contact requests with this data? * @param bool $includeprivacyinfo Do we want to include privacy requests with this data? * @param int $limitfrom * @param int $limitnum * @return array */ public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false, bool $includeprivacyinfo = false, int $limitfrom = 0, int $limitnum = 0) : array { global $DB; if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid], 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) { $userids = array_keys($members); $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo); return $members; } return []; } /** * Get the unread counts for all conversations for the user, sorted by type, and including favourites. * * @param int $userid the id of the user whose conversations we'll check. * @return array the unread counts for each conversation, indexed by type. */ public static function get_unread_conversation_counts(int $userid) : array { global $DB; // Get all conversations the user is in, and check unread. $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount FROM {message_conversations} conv INNER JOIN ( SELECT m.conversationid, count(m.id) as unreadcount FROM {messages} m INNER JOIN {message_conversations} mc ON mc.id = m.conversationid INNER JOIN {message_conversation_members} mcm ON m.conversationid = mcm.conversationid LEFT JOIN {message_user_actions} mua ON (mua.messageid = m.id AND mua.userid = ? AND (mua.action = ? OR mua.action = ?)) WHERE mcm.userid = ? AND m.useridfrom != ? AND mua.id is NULL GROUP BY m.conversationid ) indcounts ON indcounts.conversationid = conv.id WHERE conv.enabled = 1'; $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED, $userid, $userid]); // Get favourites, so we can track these separately. $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid)); $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations'); $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid')); // Assemble the return array. $counts = ['favourites' => 0, 'types' => [ self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0, self::MESSAGE_CONVERSATION_TYPE_GROUP => 0, self::MESSAGE_CONVERSATION_TYPE_SELF => 0 ]]; foreach ($unreadcounts as $convid => $info) { if (isset($favouriteconvids[$convid])) { $counts['favourites']++; continue; } $counts['types'][$info->type]++; } return $counts; } /** * Handles muting a conversation. * * @param int $userid The id of the user * @param int $conversationid The id of the conversation */ public static function mute_conversation(int $userid, int $conversationid) : void { global $DB; $mutedconversation = new \stdClass(); $mutedconversation->userid = $userid; $mutedconversation->conversationid = $conversationid; $mutedconversation->action = self::CONVERSATION_ACTION_MUTED; $mutedconversation->timecreated = time(); $DB->insert_record('message_conversation_actions', $mutedconversation); } /** * Handles unmuting a conversation. * * @param int $userid The id of the user * @param int $conversationid The id of the conversation */ public static function unmute_conversation(int $userid, int $conversationid) : void { global $DB; $DB->delete_records('message_conversation_actions', [ 'userid' => $userid, 'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED ] ); } /** * Checks whether a conversation is muted or not. * * @param int $userid The id of the user * @param int $conversationid The id of the conversation * @return bool Whether or not the conversation is muted or not */ public static function is_conversation_muted(int $userid, int $conversationid) : bool { global $DB; return $DB->record_exists('message_conversation_actions', [ 'userid' => $userid, 'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED ] ); } /** * Completely removes all related data in the DB for a given conversation. * * @param int $conversationid The id of the conversation */ public static function delete_all_conversation_data(int $conversationid) { global $DB; $conv = $DB->get_record('message_conversations', ['id' => $conversationid], 'id, contextid'); $convcontext = !empty($conv->contextid) ? \context::instance_by_id($conv->contextid) : null; $DB->delete_records('message_conversations', ['id' => $conversationid]); $DB->delete_records('message_conversation_members', ['conversationid' => $conversationid]); $DB->delete_records('message_conversation_actions', ['conversationid' => $conversationid]); // Now, go through and delete any messages and related message actions for the conversation. if ($messages = $DB->get_records('messages', ['conversationid' => $conversationid])) { $messageids = array_keys($messages); list($insql, $inparams) = $DB->get_in_or_equal($messageids); $DB->delete_records_select('message_user_actions', "messageid $insql", $inparams); // Delete the messages now. $DB->delete_records('messages', ['conversationid' => $conversationid]); } // Delete all favourite records for all users relating to this conversation. $service = \core_favourites\service_factory::get_service_for_component('core_message'); $service->delete_favourites_by_type_and_item('message_conversations', $conversationid, $convcontext); } /** * Checks if a user can delete a message for all users. * * @param int $userid the user id of who we want to delete the message for all users * @param int $messageid The message id * @return bool Returns true if a user can delete the message for all users, false otherwise. */ public static function can_delete_message_for_all_users(int $userid, int $messageid) : bool { global $DB; $sql = "SELECT mc.id, mc.contextid FROM {message_conversations} mc INNER JOIN {messages} m ON mc.id = m.conversationid WHERE m.id = :messageid"; $conversation = $DB->get_record_sql($sql, ['messageid' => $messageid]); if (!empty($conversation->contextid)) { return has_capability('moodle/site:deleteanymessage', \context::instance_by_id($conversation->contextid), $userid); } return has_capability('moodle/site:deleteanymessage', \context_system::instance(), $userid); } /** * Delete a message for all users. * * This function does not verify any permissions. * * @param int $messageid The message id * @return void */ public static function delete_message_for_all_users(int $messageid) { global $DB, $USER; if (!$DB->record_exists('messages', ['id' => $messageid])) { return false; } // Get all members in the conversation where the message belongs. $membersql = "SELECT mcm.id, mcm.userid FROM {message_conversation_members} mcm INNER JOIN {messages} m ON mcm.conversationid = m.conversationid WHERE m.id = :messageid"; $params = [ 'messageid' => $messageid ]; $members = $DB->get_records_sql($membersql, $params); if ($members) { foreach ($members as $member) { self::delete_message($member->userid, $messageid); } } } /** * Create a self conversation for a user, only if one doesn't already exist. * * @param int $userid the user to whom the conversation belongs. */ protected static function lazy_create_self_conversation(int $userid) : void { global $DB; // Check if the self-conversation for this user exists. // If not, create and star it for the user. // Don't use the API methods here, as they in turn may rely on // lazy creation and we'll end up with recursive loops of doom. $conditions = [ 'type' => self::MESSAGE_CONVERSATION_TYPE_SELF, 'convhash' => helper::get_conversation_hash([$userid]) ]; if (empty($DB->get_record('message_conversations', $conditions))) { $selfconversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_SELF, [$userid]); self::set_favourite_conversation($selfconversation->id, $userid); } } } home3/cpr76684/public_html/Aem/completion/classes/api.php 0000644 00000020126 15152343762 0017067 0 ustar 00 <?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/>. /** * Contains class containing completion API. * * @package core_completion * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_completion; defined('MOODLE_INTERNAL') || die(); /** * Class containing completion API. * * @package core_completion * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * @var string The completion expected on event. */ const COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED = 'expectcompletionon'; /** * Creates, updates or deletes an event for the expected completion date. * * @param int $cmid The course module id * @param string $modulename The name of the module (eg. assign, quiz) * @param \stdClass|int $instanceorid The instance object or ID. * @param int|null $completionexpectedtime The time completion is expected, null if not set * @return bool */ public static function update_completion_date_event($cmid, $modulename, $instanceorid, $completionexpectedtime) { global $CFG, $DB; // Required for calendar constant CALENDAR_EVENT_TYPE_ACTION. require_once($CFG->dirroot . '/calendar/lib.php'); $instance = null; if (is_object($instanceorid)) { $instance = $instanceorid; } else { $instance = $DB->get_record($modulename, array('id' => $instanceorid), '*', IGNORE_MISSING); } if (!$instance) { return false; } $course = get_course($instance->course); $completion = new \completion_info($course); // Can not create/update an event if completion is disabled. if (!$completion->is_enabled() && $completionexpectedtime !== null) { return true; } // Create the \stdClass we will be using for our language strings. $lang = new \stdClass(); $lang->modulename = get_string('pluginname', $modulename); $lang->instancename = $instance->name; // Create the calendar event. $event = new \stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = self::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED; if ($event->id = $DB->get_field('event', 'id', array('modulename' => $modulename, 'instance' => $instance->id, 'eventtype' => $event->eventtype))) { if ($completionexpectedtime !== null) { // Calendar event exists so update it. $event->name = get_string('completionexpectedfor', 'completion', $lang); $event->description = format_module_intro($modulename, $instance, $cmid, false); $event->format = FORMAT_HTML; $event->timestart = $completionexpectedtime; $event->timesort = $completionexpectedtime; $event->visible = instance_is_visible($modulename, $instance); $event->timeduration = 0; $calendarevent = \calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is no longer needed. $calendarevent = \calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if ($completionexpectedtime !== null) { $event->name = get_string('completionexpectedfor', 'completion', $lang); $event->description = format_module_intro($modulename, $instance, $cmid, false); $event->format = FORMAT_HTML; $event->courseid = $instance->course; $event->groupid = 0; $event->userid = 0; $event->modulename = $modulename; $event->instance = $instance->id; $event->timestart = $completionexpectedtime; $event->timesort = $completionexpectedtime; $event->visible = instance_is_visible($modulename, $instance); $event->timeduration = 0; \calendar_event::create($event, false); } } return true; } /** * Mark users who completed course based on activity criteria. * @param array $userdata If set only marks specified user in given course else checks all courses/users. * @return int Completion record id if $userdata is set, 0 else. * @since Moodle 4.0 */ public static function mark_course_completions_activity_criteria($userdata = null): int { global $DB; // Get all users who meet this criteria $sql = "SELECT DISTINCT c.id AS course, cr.id AS criteriaid, ra.userid AS userid, mc.timemodified AS timecompleted FROM {course_completion_criteria} cr INNER JOIN {course} c ON cr.course = c.id INNER JOIN {context} con ON con.instanceid = c.id INNER JOIN {role_assignments} ra ON ra.contextid = con.id INNER JOIN {course_modules} cm ON cm.id = cr.moduleinstance INNER JOIN {course_modules_completion} mc ON mc.coursemoduleid = cr.moduleinstance AND mc.userid = ra.userid LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = ra.userid WHERE cr.criteriatype = :criteriatype AND con.contextlevel = :contextlevel AND c.enablecompletion = 1 AND cc.id IS NULL AND ( mc.completionstate = :completionstate OR (cm.completionpassgrade = 1 AND mc.completionstate = :completionstatepass1) OR (cm.completionpassgrade = 0 AND (mc.completionstate = :completionstatepass2 OR mc.completionstate = :completionstatefail)) )"; $params = [ 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY, 'contextlevel' => CONTEXT_COURSE, 'completionstate' => COMPLETION_COMPLETE, 'completionstatepass1' => COMPLETION_COMPLETE_PASS, 'completionstatepass2' => COMPLETION_COMPLETE_PASS, 'completionstatefail' => COMPLETION_COMPLETE_FAIL ]; if ($userdata) { $params['courseid'] = $userdata['courseid']; $params['userid'] = $userdata['userid']; $sql .= " AND c.id = :courseid AND ra.userid = :userid"; // Mark as complete. $record = $DB->get_record_sql($sql, $params); if ($record) { $completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); $result = $completion->mark_complete($record->timecompleted); return $result; } } else { // Loop through completions, and mark as complete. $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $record) { $completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); $completion->mark_complete($record->timecompleted); } $rs->close(); } return 0; } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�