From c85215e9553c62e5a94661ba793e0af0b50ceb4a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Mudr=C3=A1k?= <david@moodle.com>
Date: Mon, 11 Jan 2021 20:57:05 +0100
Subject: [PATCH] MDL-70608 lang: Install langpacks asynchronously via ad hoc
 task

When multiple language packs are selected for installation, perform the
installation asynchronously in the background via ad hoc task.
---
 admin/tool/langimport/classes/controller.php  |  21 +++-
 .../classes/task/install_langpacks.php        | 109 ++++++++++++++++++
 admin/tool/langimport/index.php               |  20 +++-
 .../langimport/lang/en/tool_langimport.php    |   4 +
 .../tests/behat/manage_langpacks.feature      |  28 ++++-
 5 files changed, 175 insertions(+), 7 deletions(-)
 create mode 100644 admin/tool/langimport/classes/task/install_langpacks.php

diff --git a/admin/tool/langimport/classes/controller.php b/admin/tool/langimport/classes/controller.php
index b6520f6b818..82b6c32d1d4 100644
--- a/admin/tool/langimport/classes/controller.php
+++ b/admin/tool/langimport/classes/controller.php
@@ -82,7 +82,7 @@ class controller {
                     $a->url  = $this->installer->lang_pack_url($langcode);
                     $a->dest = $CFG->dataroot.'/lang';
                     $this->errors[] = get_string('remotedownloaderror', 'error', $a);
-                    throw new \moodle_exception('remotedownloaderror', 'error', $a);
+                    throw new \moodle_exception('remotedownloaderror', 'error', '', $a);
                     break;
                 case \lang_installer::RESULT_INSTALLED:
                     $updatedpacks++;
@@ -221,4 +221,23 @@ class controller {
     public function lang_pack_url($langcode = '') {
         return $this->installer->lang_pack_url($langcode);
     }
+
+    /**
+     * Schedule installation of the given language packs asynchronously via ad hoc task.
+     *
+     * @param string|array $langs array of langcodes or individual langcodes
+     */
+    public function schedule_languagepacks_installation($langs): void {
+        global $USER;
+
+        $task = new \tool_langimport\task\install_langpacks();
+        $task->set_userid($USER->id);
+        $task->set_custom_data([
+            'langs' => $langs,
+        ]);
+
+        \core\task\manager::queue_adhoc_task($task, true);
+
+        $this->info[] = get_string('installscheduled', 'tool_langimport');
+    }
 }
diff --git a/admin/tool/langimport/classes/task/install_langpacks.php b/admin/tool/langimport/classes/task/install_langpacks.php
new file mode 100644
index 00000000000..eb8d78f74c5
--- /dev/null
+++ b/admin/tool/langimport/classes/task/install_langpacks.php
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
+
+namespace tool_langimport\task;
+
+/**
+ * Ad hoc task to install one or more language packs.
+ *
+ * @package     tool_langimport
+ * @category    task
+ * @copyright   2021 David Mudrák <david@moodle.com>
+ * @license     https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class install_langpacks extends \core\task\adhoc_task {
+
+    /**
+     * Execute the ad hoc task.
+     */
+    public function execute(): void {
+
+        $data = $this->get_custom_data();
+
+        if (empty($data->langs)) {
+            mtrace('No language packs to install');
+        }
+
+        get_string_manager()->reset_caches();
+
+        $controller = new \tool_langimport\controller();
+
+        \core_php_time_limit::raise();
+
+        try {
+            $controller->install_languagepacks($data->langs);
+            $this->notify_user_success($controller);
+
+        } catch (\Throwable $e) {
+            $this->notify_user_error($e->getMessage());
+
+        } finally {
+            get_string_manager()->reset_caches();
+        }
+    }
+
+    /**
+     * Notify user that the task finished successfully.
+     *
+     * @param \tool_langimport\controller $controller
+     */
+    protected function notify_user_success(\tool_langimport\controller $controller): void {
+
+        $message = new \core\message\message();
+
+        $message->component = 'moodle';
+        $message->name = 'notices';
+        $message->userfrom = \core_user::get_noreply_user();
+        $message->userto = $this->get_userid();
+        $message->notification = 1;
+        $message->contexturl = (new \moodle_url('/admin/tool/langimport/index.php'))->out(false);
+        $message->contexturlname = get_string('pluginname', 'tool_langimport');
+
+        $message->subject = get_string('installfinished', 'tool_langimport');
+        $message->fullmessage = '* ' . implode(PHP_EOL . '* ', $controller->info);
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml = markdown_to_html($message->fullmessage);
+        $message->smallmessage = get_string('installfinished', 'tool_langimport');
+
+        message_send($message);
+    }
+
+    /**
+     * Notify user that the task failed.
+     *
+     * @param string $error The error text
+     */
+    protected function notify_user_error(string $error): void {
+
+        $message = new \core\message\message();
+
+        $message->component = 'moodle';
+        $message->name = 'notices';
+        $message->userfrom = \core_user::get_noreply_user();
+        $message->userto = $this->get_userid();
+        $message->notification = 1;
+        $message->contexturl = (new \moodle_url('/admin/tool/langimport/index.php'))->out(false);
+        $message->contexturlname = get_string('pluginname', 'tool_langimport');
+
+        $message->subject = get_string('installfailed', 'tool_langimport');
+        $message->fullmessage = $error;
+        $message->fullmessageformat = FORMAT_PLAIN;
+        $message->fullmessagehtml = text_to_html($message->fullmessage);
+        $message->smallmessage = get_string('installfailed', 'tool_langimport');
+
+        message_send($message);
+    }
+}
diff --git a/admin/tool/langimport/index.php b/admin/tool/langimport/index.php
index 1180abc09a8..da57a7cbe90 100644
--- a/admin/tool/langimport/index.php
+++ b/admin/tool/langimport/index.php
@@ -66,8 +66,15 @@ get_string_manager()->reset_caches();
 $controller = new tool_langimport\controller();
 
 if (($mode == INSTALLATION_OF_SELECTED_LANG) and confirm_sesskey() and !empty($pack)) {
-    core_php_time_limit::raise();
-    $controller->install_languagepacks($pack);
+    if (is_array($pack) && count($pack) > 1) {
+        // Installing multiple languages can take a while - perform it asynchronously in the background.
+        $controller->schedule_languagepacks_installation($pack);
+
+    } else {
+        // Single language pack to be installed synchronously. It should be reasonably quick and can be used for debugging, too.
+        core_php_time_limit::raise();
+        $controller->install_languagepacks($pack);
+    }
 }
 
 if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($confirmtounistall))) {
@@ -159,6 +166,15 @@ if ($controller->errors) {
     \core\notification::error($info);
 }
 
+// Inform about pending language packs installations.
+foreach (\core\task\manager::get_adhoc_tasks('\tool_langimport\task\install_langpacks') as $installtask) {
+    $installtaskdata = $installtask->get_custom_data();
+
+    if (!empty($installtaskdata->langs)) {
+        \core\notification::info(get_string('installpending', 'tool_langimport', implode(', ', $installtaskdata->langs)));
+    }
+}
+
 if ($missingparents) {
     foreach ($missingparents as $l => $parent) {
         $a = new stdClass();
diff --git a/admin/tool/langimport/lang/en/tool_langimport.php b/admin/tool/langimport/lang/en/tool_langimport.php
index 7fddab115f6..d9ca31005ff 100644
--- a/admin/tool/langimport/lang/en/tool_langimport.php
+++ b/admin/tool/langimport/lang/en/tool_langimport.php
@@ -25,6 +25,10 @@
 
 $string['downloadnotavailable'] = 'Unable to connect to the download server. It is not possible to install or update the language packs automatically. Please download the appropriate ZIP file(s) from <a href="{$a->src}">{$a->src}</a> and unzip them manually to your data directory <code>{$a->dest}</code>';
 $string['install'] = 'Install selected language pack(s)';
+$string['installfailed'] = 'Language packs installation failed!';
+$string['installfinished'] = 'Language packs installation finished.';
+$string['installpending'] = 'The following language packs will be installed soon: {$a}.';
+$string['installscheduled'] = 'Language packs scheduled for installation.';
 $string['installedlangs'] = 'Installed language packs';
 $string['langimport'] = 'Language import utility';
 $string['langimportdisabled'] = 'Language import feature has been disabled. You have to update your language packs manually at the file-system level. Do not forget to purge string caches after you do so.';
diff --git a/admin/tool/langimport/tests/behat/manage_langpacks.feature b/admin/tool/langimport/tests/behat/manage_langpacks.feature
index 6af3c4c8d32..1569dd6a42d 100644
--- a/admin/tool/langimport/tests/behat/manage_langpacks.feature
+++ b/admin/tool/langimport/tests/behat/manage_langpacks.feature
@@ -18,7 +18,22 @@ Feature: Manage language packs
     And the "Installed language packs" select box should contain "en_ar"
     And I navigate to "Reports > Live logs" in site administration
     And I should see "The language pack 'en_ar' was installed."
-    And I log out
+
+  Scenario: Install multiple language packs asynchronously in the background
+    Given I log in as "admin"
+    And I navigate to "Language > Language packs" in site administration
+    And I set the field "Available language packs" to "en_us,en_us_k12"
+    When I press "Install selected language pack(s)"
+    Then I should see "Language packs scheduled for installation."
+    And I should see "The following language packs will be installed soon: en_us, en_us_k12."
+    And I trigger cron
+    And I am on homepage
+    And I navigate to "Language > Language packs" in site administration
+    And the "Installed language packs" select box should contain "en_us"
+    And the "Installed language packs" select box should contain "en_us_k12"
+    And I navigate to "Reports > Live logs" in site administration
+    And I should see "The language pack 'en_us' was installed."
+    And I should see "The language pack 'en_us_k12' was installed."
 
   @javascript
   Scenario: Search for available language pack
@@ -39,7 +54,14 @@ Feature: Manage language packs
     And I should see "Language pack update completed"
     And I navigate to "Reports > Live logs" in site administration
     And I should see "The language pack 'en_ar' was updated."
-    And I log out
+
+  Scenario: Inform admin that there are multiple installed languages and updating them all can take too long
+    Given outdated langpack 'en_ar' is installed
+    And outdated langpack 'en_us' is installed
+    And outdated langpack 'en_us_k12' is installed
+    When I log in as "admin"
+    And I navigate to "Language > Language packs" in site administration
+    Then I should see "Updating all installed language packs by clicking the button can take a long time and lead to timeouts."
 
   Scenario: Try to uninstall language pack
     Given I log in as "admin"
@@ -55,7 +77,6 @@ Feature: Manage language packs
     And I navigate to "Reports > Live logs" in site administration
     And I should see "The language pack 'en_ar' was removed."
     And I should see "Language pack uninstalled"
-    And I log out
 
   Scenario: Try to uninstall English language pack
     Given I log in as "admin"
@@ -65,4 +86,3 @@ Feature: Manage language packs
     Then I should see "The English language pack cannot be uninstalled."
     And I navigate to "Reports > Live logs" in site administration
     And I should not see "Language pack uninstalled"
-    And I log out