Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core_communication;
  18  
  19  use core\context;
  20  use core_communication\task\add_members_to_room_task;
  21  use core_communication\task\create_and_configure_room_task;
  22  use core_communication\task\delete_room_task;
  23  use core_communication\task\remove_members_from_room;
  24  use core_communication\task\update_room_task;
  25  use core_communication\task\update_room_membership_task;
  26  use stdClass;
  27  
  28  /**
  29   * Class api is the public endpoint of the communication api. This class is the point of contact for api usage.
  30   *
  31   * Communication api allows to add ad-hoc tasks to the queue to perform actions on the communication providers. This api will
  32   * not allow any immediate actions to be performed on the communication providers. It will only add the tasks to the queue. The
  33   * exception has been made for deletion of members in case of deleting the user. This is because the user will not be available.
  34   * The member management api part allows run actions immediately if required.
  35   *
  36   * Communication api does allow to have form elements related to communication api in the required forms. This is done by using
  37   * the form_definition method. This method will add the form elements to the form.
  38   *
  39   * @package    core_communication
  40   * @copyright  2023 Safat Shahin <safat.shahin@moodle.com>
  41   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class api {
  44      /**
  45       * @var null|processor $communication The communication settings object
  46       */
  47      private ?processor $communication;
  48  
  49      /**
  50       * Communication handler constructor to manage and handle all communication related actions.
  51       *
  52       * This class is the entrypoint for all kinda usages.
  53       * It will be used by the other api to manage the communication providers.
  54       *
  55       * @param context $context The context of the item for the instance
  56       * @param string $component The component of the item for the instance
  57       * @param string $instancetype The type of the item for the instance
  58       * @param int $instanceid The id of the instance
  59       * @param string|null $provider The provider type - if null will load for this context's active provider.
  60       *
  61       */
  62      private function __construct(
  63          private context $context,
  64          private string $component,
  65          private string $instancetype,
  66          private int $instanceid,
  67          private ?string $provider = null,
  68      ) {
  69          $this->communication = processor::load_by_instance(
  70              context: $context,
  71              component: $component,
  72              instancetype: $instancetype,
  73              instanceid: $instanceid,
  74              provider: $provider,
  75          );
  76      }
  77  
  78      /**
  79       * Get the communication processor object.
  80       *
  81       * @param context $context The context of the item for the instance
  82       * @param string $component The component of the item for the instance
  83       * @param string $instancetype The type of the item for the instance
  84       * @param int $instanceid The id of the instance
  85       * @param string|null $provider The provider type - if null will load for this context's active provider.
  86       * @return api
  87       */
  88      public static function load_by_instance(
  89          context $context,
  90          string $component,
  91          string $instancetype,
  92          int $instanceid,
  93          ?string $provider = null,
  94      ): self {
  95          return new self(
  96              context: $context,
  97              component: $component,
  98              instancetype: $instancetype,
  99              instanceid: $instanceid,
 100              provider: $provider,
 101          );
 102      }
 103  
 104      /**
 105       * Reload in the internal instance data.
 106       */
 107      public function reload(): void {
 108          $this->communication = processor::load_by_instance(
 109              context: $this->context,
 110              component: $this->component,
 111              instancetype: $this->instancetype,
 112              instanceid: $this->instanceid,
 113              provider: $this->provider,
 114          );
 115      }
 116  
 117      /**
 118       * Return the underlying communication processor object.
 119       *
 120       * @return processor
 121       */
 122      public function get_processor(): processor {
 123          return $this->communication;
 124      }
 125  
 126      /**
 127       * Return the room provider.
 128       *
 129       * @return \core_communication\room_chat_provider
 130       */
 131      public function get_room_provider(): \core_communication\room_chat_provider {
 132          return $this->communication->get_room_provider();
 133      }
 134  
 135      /**
 136       * Return the user provider.
 137       *
 138       * @return \core_communication\user_provider
 139       */
 140      public function get_user_provider(): \core_communication\user_provider {
 141          return $this->communication->get_user_provider();
 142      }
 143  
 144      /**
 145       * Return the room user provider.
 146       *
 147       * @return \core_communication\room_user_provider
 148       */
 149      public function get_room_user_provider(): \core_communication\room_user_provider {
 150          return $this->communication->get_room_user_provider();
 151      }
 152  
 153      /**
 154       * Return the form provider.
 155       *
 156       * @return \core_communication\form_provider
 157       */
 158      public function get_form_provider(): \core_communication\form_provider {
 159          return $this->communication->get_form_provider();
 160      }
 161  
 162      /**
 163       * Check if the communication api is enabled.
 164       */
 165      public static function is_available(): bool {
 166          return (bool) get_config('core', 'enablecommunicationsubsystem');
 167      }
 168  
 169      /**
 170       * Get the communication room url.
 171       *
 172       * @return string|null
 173       */
 174      public function get_communication_room_url(): ?string {
 175          return $this->communication?->get_room_url();
 176      }
 177  
 178      /**
 179       * Get the list of plugins for form selection.
 180       *
 181       * @return array
 182       */
 183      public static function get_communication_plugin_list_for_form(): array {
 184          // Add the option to have communication disabled.
 185          $selection[processor::PROVIDER_NONE] = get_string('nocommunicationselected', 'communication');
 186          $communicationplugins = \core\plugininfo\communication::get_enabled_plugins();
 187          foreach ($communicationplugins as $pluginname => $notusing) {
 188              $provider = 'communication_' . $pluginname;
 189              if (processor::is_provider_available($provider)) {
 190                  $selection[$provider] = get_string('pluginname', 'communication_' . $pluginname);
 191              }
 192          }
 193          return $selection;
 194      }
 195  
 196      /**
 197       * Get the enabled communication providers and default provider according to the selected provider.
 198       *
 199       * @param string|null $selecteddefaulprovider
 200       * @return array
 201       */
 202      public static function get_enabled_providers_and_default(string $selecteddefaulprovider = null): array {
 203          $communicationproviders = self::get_communication_plugin_list_for_form();
 204          $defaulprovider = processor::PROVIDER_NONE;
 205          if (!empty($selecteddefaulprovider) && array_key_exists($selecteddefaulprovider, $communicationproviders)) {
 206              $defaulprovider = $selecteddefaulprovider;
 207          }
 208          return [$communicationproviders, $defaulprovider];
 209      }
 210  
 211      /**
 212       * Define the form elements for the communication api.
 213       * This method will be called from the form definition method of the instance.
 214       *
 215       * @param \MoodleQuickForm $mform The form element
 216       * @param string $selectdefaultcommunication The default selected communication provider in the form field
 217       */
 218      public function form_definition(
 219          \MoodleQuickForm $mform,
 220          string $selectdefaultcommunication = processor::PROVIDER_NONE
 221      ): void {
 222          global $PAGE;
 223  
 224          [$communicationproviders, $defaulprovider] = self::get_enabled_providers_and_default($selectdefaultcommunication);
 225  
 226          $PAGE->requires->js_call_amd('core_communication/providerchooser', 'init');
 227  
 228          // List the communication providers.
 229          $mform->addElement(
 230              'select',
 231              'selectedcommunication',
 232              get_string('selectcommunicationprovider', 'communication'),
 233              $communicationproviders,
 234              ['data-communicationchooser-field' => 'selector'],
 235          );
 236          $mform->addHelpButton('selectedcommunication', 'selectcommunicationprovider', 'communication');
 237          $mform->setDefault('selectedcommunication', $defaulprovider);
 238  
 239          $mform->registerNoSubmitButton('updatecommunicationprovider');
 240          $mform->addElement(
 241              'submit',
 242              'updatecommunicationprovider',
 243              'update communication',
 244              ['data-communicationchooser-field' => 'updateButton', 'class' => 'd-none']
 245          );
 246  
 247          // Just a placeholder for the communication options.
 248          $mform->addElement('hidden', 'addcommunicationoptionshere');
 249          $mform->setType('addcommunicationoptionshere', PARAM_BOOL);
 250      }
 251  
 252      /**
 253       * Set the form definitions for the plugins.
 254       *
 255       * @param \MoodleQuickForm $mform The moodle form
 256       * @param string $provider The provider name
 257       */
 258      public function form_definition_for_provider(\MoodleQuickForm $mform, string $provider = processor::PROVIDER_NONE): void {
 259          if ($provider === processor::PROVIDER_NONE) {
 260              return;
 261          }
 262  
 263          // Room name for the communication provider.
 264          $mform->insertElementBefore(
 265              $mform->createElement(
 266                  'text',
 267                  'communicationroomname',
 268                  get_string('communicationroomname', 'communication'),
 269                  'maxlength="100" size="20"'
 270              ),
 271              'addcommunicationoptionshere'
 272          );
 273          $mform->setType('communicationroomname', PARAM_TEXT);
 274  
 275          $mform->insertElementBefore(
 276              $mform->createElement(
 277                  'static',
 278                  'communicationroomnameinfo',
 279                  '',
 280                  get_string('communicationroomnameinfo', 'communication'),
 281              ),
 282              'addcommunicationoptionshere',
 283          );
 284  
 285          processor::set_provider_specific_form_definition($provider, $mform);
 286      }
 287  
 288      /**
 289       * Get the avatar file.
 290       *
 291       * @return null|\stored_file
 292       */
 293      public function get_avatar(): ?\stored_file {
 294          $filename = $this->communication->get_avatar_filename();
 295          if ($filename === null) {
 296              return null;
 297          }
 298          $fs = get_file_storage();
 299          $args = (array) $this->get_avatar_filerecord($filename);
 300          return $fs->get_file(...$args) ?: null;
 301      }
 302  
 303      /**
 304       * Get the avatar file record for the avatar for filesystem.
 305       *
 306       * @param string $filename The filename of the avatar
 307       * @return stdClass
 308       */
 309      protected function get_avatar_filerecord(string $filename): stdClass {
 310          return (object) [
 311              'contextid' => \core\context\system::instance()->id,
 312              'component' => 'core_communication',
 313              'filearea' => 'avatar',
 314              'itemid' => $this->communication->get_id(),
 315              'filepath' => '/',
 316              'filename' => $filename,
 317          ];
 318      }
 319  
 320      /**
 321       * Get the avatar file.
 322       *
 323       * If null is set, then delete the old area file and set the avatarfilename to null.
 324       * This will make sure the plugin api deletes the avatar from the room.
 325       *
 326       * @param null|\stored_file $avatar The stored file for the avatar
 327       * @return bool
 328       */
 329      public function set_avatar(?\stored_file $avatar): bool {
 330          $currentfilename = $this->communication->get_avatar_filename();
 331          if ($avatar === null && empty($currentfilename)) {
 332              return false;
 333          }
 334  
 335          $currentfilerecord = $this->get_avatar();
 336          if ($avatar && $currentfilerecord) {
 337              $currentfilehash = $currentfilerecord->get_contenthash();
 338              $updatedfilehash = $avatar->get_contenthash();
 339  
 340              // No update required.
 341              if ($currentfilehash === $updatedfilehash) {
 342                  return false;
 343              }
 344          }
 345  
 346          $context = \core\context\system::instance();
 347  
 348          $fs = get_file_storage();
 349          $fs->delete_area_files(
 350              $context->id,
 351              'core_communication',
 352              'avatar',
 353              $this->communication->get_id()
 354          );
 355  
 356          if ($avatar) {
 357              $fs->create_file_from_storedfile(
 358                  $this->get_avatar_filerecord($avatar->get_filename()),
 359                  $avatar,
 360              );
 361              $this->communication->set_avatar_filename($avatar->get_filename());
 362          } else {
 363              $this->communication->set_avatar_filename(null);
 364          }
 365  
 366          // Indicate that we need to sync the avatar when the update task is run.
 367          $this->communication->set_avatar_synced_flag(false);
 368  
 369          return true;
 370      }
 371  
 372      /**
 373       * A helper to fetch the room name
 374       *
 375       * @return string
 376       */
 377      public function get_room_name(): string {
 378          return $this->communication->get_room_name();
 379      }
 380  
 381      /**
 382       * Set the form data if the data is already available.
 383       *
 384       * @param \stdClass $instance The instance object
 385       */
 386      public function set_data(\stdClass $instance): void {
 387          if (!empty($instance->id) && $this->communication) {
 388              $instance->selectedcommunication = $this->communication->get_provider();
 389              $instance->communicationroomname = $this->communication->get_room_name();
 390  
 391              $this->communication->get_form_provider()->set_form_data($instance);
 392          }
 393      }
 394  
 395      /**
 396       * Get the communication provider.
 397       *
 398       * @return string
 399       */
 400      public function get_provider(): string {
 401          if (!$this->communication) {
 402              return '';
 403          }
 404          return $this->communication->get_provider();
 405      }
 406  
 407      /**
 408       * Create a communication ad-hoc task for create operation.
 409       * This method will add a task to the queue to create the room.
 410       *
 411       * @param string $communicationroomname The communication room name
 412       * @param null|\stored_file $avatar The stored file for the avatar
 413       * @param \stdClass|null $instance The actual instance object
 414       */
 415      public function create_and_configure_room(
 416          string $communicationroomname,
 417          ?\stored_file $avatar = null,
 418          ?\stdClass $instance = null,
 419      ): void {
 420          if ($this->provider === processor::PROVIDER_NONE || $this->provider === '') {
 421              return;
 422          }
 423  
 424          // Create communication record.
 425          $this->communication = processor::create_instance(
 426              context: $this->context,
 427              provider: $this->provider,
 428              instanceid: $this->instanceid,
 429              component: $this->component,
 430              instancetype: $this->instancetype,
 431              roomname: $communicationroomname,
 432          );
 433  
 434          // Update provider record from form data.
 435          if ($instance !== null) {
 436              $this->communication->get_form_provider()->save_form_data($instance);
 437          }
 438  
 439          // Set the avatar.
 440          if (!empty($avatar)) {
 441              $this->set_avatar($avatar);
 442          }
 443  
 444          // Add ad-hoc task to create the provider room.
 445          create_and_configure_room_task::queue(
 446              $this->communication,
 447          );
 448      }
 449  
 450      /**
 451       * Create a communication ad-hoc task for update operation.
 452       * This method will add a task to the queue to update the room.
 453       *
 454       * @param null|int $active The selected active state of the provider
 455       * @param null|string $communicationroomname The communication room name
 456       * @param null|\stored_file $avatar The stored file for the avatar
 457       * @param \stdClass|null $instance The actual instance object
 458       */
 459      public function update_room(
 460          ?int $active = null,
 461          ?string $communicationroomname = null,
 462          ?\stored_file $avatar = null,
 463          ?\stdClass $instance = null,
 464      ): void {
 465          // If the provider is none, we don't need to do anything from room point of view.
 466          if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
 467              return;
 468          }
 469  
 470          $roomnamechange = null;
 471          $activestatuschange = null;
 472  
 473          // Check if the room name is being changed.
 474          if (
 475              $communicationroomname !== null &&
 476              $communicationroomname !== $this->communication->get_room_name()
 477          ) {
 478              $roomnamechange = $communicationroomname;
 479          }
 480  
 481          // Check if the active status of the provider is being changed.
 482          if (
 483              $active !== null &&
 484              $active !== $this->communication->is_instance_active()
 485          ) {
 486              $activestatuschange = $active;
 487          }
 488  
 489          if ($roomnamechange !== null || $activestatuschange !== null) {
 490              $this->communication->update_instance(
 491                  active: $active,
 492                  roomname: $communicationroomname,
 493              );
 494          }
 495  
 496          // Update provider record from form data.
 497          if ($instance !== null) {
 498              $this->communication->get_form_provider()->save_form_data($instance);
 499          }
 500  
 501          // Update the avatar.
 502          // If the value is `null`, then unset the avatar.
 503          $this->set_avatar($avatar);
 504  
 505          // Always queue a room update, even if none of the above standard fields have changed.
 506          // It is possible for providers to have custom fields that have been updated.
 507          update_room_task::queue(
 508              $this->communication,
 509          );
 510      }
 511  
 512      /**
 513       * Create a communication ad-hoc task for delete operation.
 514       * This method will add a task to the queue to delete the room.
 515       */
 516      public function delete_room(): void {
 517          if ($this->communication !== null) {
 518              // Add the ad-hoc task to remove the room data from the communication table and associated provider actions.
 519              delete_room_task::queue(
 520                  $this->communication,
 521              );
 522          }
 523      }
 524  
 525      /**
 526       * Create a communication ad-hoc task for add members operation and add the user mapping.
 527       *
 528       * This method will add a task to the queue to add the room users.
 529       *
 530       * @param array $userids The user ids to add to the room
 531       * @param bool $queue Whether to queue the task or not
 532       */
 533      public function add_members_to_room(array $userids, bool $queue = true): void {
 534          // No communication object? something not done right.
 535          if (!$this->communication) {
 536              return;
 537          }
 538  
 539          // No user IDs or this provider does not manage users? No action required.
 540          if (empty($userids) || !$this->communication->supports_user_features()) {
 541              return;
 542          }
 543  
 544          $this->communication->create_instance_user_mapping($userids);
 545  
 546          if ($queue) {
 547              add_members_to_room_task::queue(
 548                  $this->communication
 549              );
 550          }
 551      }
 552  
 553      /**
 554       * Create a communication ad-hoc task for updating members operation and update the user mapping.
 555       *
 556       * This method will add a task to the queue to update the room users.
 557       *
 558       * @param array $userids The user ids to add to the room
 559       * @param bool $queue Whether to queue the task or not
 560       */
 561      public function update_room_membership(array $userids, bool $queue = true): void {
 562          // No communication object? something not done right.
 563          if (!$this->communication) {
 564              return;
 565          }
 566  
 567          // No userids? don't bother doing anything.
 568          if (empty($userids)) {
 569              return;
 570          }
 571  
 572          $this->communication->reset_users_sync_flag($userids);
 573  
 574          if ($queue) {
 575              update_room_membership_task::queue(
 576                  $this->communication
 577              );
 578          }
 579      }
 580  
 581      /**
 582       * Create a communication ad-hoc task for remove members operation or action immediately.
 583       *
 584       * This method will add a task to the queue to remove the room users.
 585       *
 586       * @param array $userids The user ids to remove from the room
 587       * @param bool $queue Whether to queue the task or not
 588       */
 589      public function remove_members_from_room(array $userids, bool $queue = true): void {
 590          // No communication object? something not done right.
 591          if (!$this->communication) {
 592              return;
 593          }
 594  
 595          $provider = $this->communication->get_provider();
 596  
 597          if ($provider === processor::PROVIDER_NONE) {
 598              return;
 599          }
 600  
 601          // No user IDs or this provider does not manage users? No action required.
 602          if (empty($userids) || !$this->communication->supports_user_features()) {
 603              return;
 604          }
 605  
 606          $this->communication->add_delete_user_flag($userids);
 607  
 608          if ($queue) {
 609              remove_members_from_room::queue(
 610                  $this->communication
 611              );
 612          }
 613      }
 614  
 615      /**
 616       * Display the communication room status notification.
 617       */
 618      public function show_communication_room_status_notification(): void {
 619          // No communication, no room.
 620          if (!$this->communication) {
 621              return;
 622          }
 623  
 624          if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
 625              return;
 626          }
 627  
 628          $roomstatus = $this->get_communication_room_url() ? 'ready' : 'pending';
 629          $pluginname = get_string('pluginname', $this->get_provider());
 630          $message = get_string('communicationroom' . $roomstatus, 'communication', $pluginname);
 631  
 632          switch ($roomstatus) {
 633              case 'pending':
 634                  \core\notification::add($message, \core\notification::INFO);
 635                  break;
 636  
 637              case 'ready':
 638                  // We only show the ready notification once per user.
 639                  // We check this with a custom user preference.
 640                  $roomreadypreference = "{$this->component}_{$this->instancetype}_{$this->instanceid}_room_ready";
 641  
 642                  if (empty(get_user_preferences($roomreadypreference))) {
 643                      \core\notification::add($message, \core\notification::SUCCESS);
 644                      set_user_preference($roomreadypreference, true);
 645                  }
 646                  break;
 647          }
 648      }
 649  }