+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use core_course\local\entity\activity_chooser_footer;
+
+/**
+ * Build activity chooser footer content for Marketplace.
+ *
+ * @param int $courseid The selected course id.
+ * @param int $sectionid The selected section id.
+ * @return activity_chooser_footer
+ */
+function tool_installaddon_custom_chooser_footer(int $courseid, int $sectionid): activity_chooser_footer {
+ global $OUTPUT;
+
+ $installer = tool_installaddon_installer::instance();
+ $marketplaceurl = $installer->get_marketplace_url();
+
+ $renderedfooter = $OUTPUT->render_from_template('tool_installaddon/chooser_footer', [
+ 'url' => $marketplaceurl->out(false),
+ ]);
+
+ return new activity_chooser_footer(
+ 'tool_installaddon/footer',
+ $renderedfooter
+ );
+}
diff --git a/admin/tool/installaddon/settings.php b/admin/tool/installaddon/settings.php
index 17f0d51e91d..8a42859bb41 100644
--- a/admin/tool/installaddon/settings.php
+++ b/admin/tool/installaddon/settings.php
@@ -26,6 +26,12 @@
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig and empty($CFG->disableupdateautodeploy)) {
+ $installer = tool_installaddon_installer::instance();
+ $ADMIN->add('modules', new admin_externalpage(
+ 'tool_installaddon_marketplace',
+ get_string('marketplaceadminlinktext', 'tool_installaddon'),
+ $installer->get_marketplace_url()->out(false)
+ ), 'modsettings');
$ADMIN->add('modules', new admin_externalpage('tool_installaddon_index',
get_string('installaddons', 'tool_installaddon'),
diff --git a/admin/tool/installaddon/templates/chooser_footer.mustache b/admin/tool/installaddon/templates/chooser_footer.mustache
new file mode 100644
index 00000000000..fb88aa65c24
--- /dev/null
+++ b/admin/tool/installaddon/templates/chooser_footer.mustache
@@ -0,0 +1,30 @@
+{{!
+ 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 .
+}}
+{{!
+ @template tool_installaddon/chooser_footer
+
+ Activity chooser footer link to Marketplace.
+
+ Example context (json):
+ {
+ "url": "https://marketplace.moodle.com/?site=hash"
+ }
+}}
+
+ {{#str}} activitychooserfootertext, tool_installaddon {{/str}}
+ {{> tool_installaddon/marketplace_link }}
+
diff --git a/admin/tool/installaddon/templates/marketplace_link.mustache b/admin/tool/installaddon/templates/marketplace_link.mustache
new file mode 100644
index 00000000000..5ac949b80f9
--- /dev/null
+++ b/admin/tool/installaddon/templates/marketplace_link.mustache
@@ -0,0 +1,36 @@
+{{!
+ 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 .
+}}
+{{!
+ @template tool_installaddon/marketplace_link
+
+ Reusable link to Moodle Marketplace.
+
+ Example context (json):
+ {
+ "url": "https://marketplace.moodle.com/?site=hash"
+ }
+}}
+
+ {{#str}} marketplacelink, tool_installaddon {{/str}}
+
+
+
diff --git a/admin/tool/installaddon/tests/behat/activity_chooser_footer.feature b/admin/tool/installaddon/tests/behat/activity_chooser_footer.feature
new file mode 100644
index 00000000000..5a53b7c97b8
--- /dev/null
+++ b/admin/tool/installaddon/tests/behat/activity_chooser_footer.feature
@@ -0,0 +1,25 @@
+@tool @tool_installaddon @javascript
+Feature: Marketplace activity chooser footer
+ In order to browse plugins from the activity chooser
+ As a teacher
+ I need to see the Marketplace footer link when installaddon is the active footer plugin
+
+ Background:
+ Given the following config values are set as admin:
+ | activitychooseractivefooter | tool_installaddon |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category | format |
+ | Course 1 | C1 | 0 | topics |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+
+ Scenario: Activity chooser footer includes Marketplace link
+ Given I log in as "teacher1"
+ When I am on "Course 1" course homepage with editing mode on
+ And I open the activity chooser
+ Then "Browse more activities on" "text" should exist in the "Add an activity or resource" "dialogue"
+ And "Marketplace" "link" should exist in the "Add an activity or resource" "dialogue"
diff --git a/admin/tool/installaddon/tests/fixtures/testable_installer_without_site_info.php b/admin/tool/installaddon/tests/fixtures/testable_installer_without_site_info.php
new file mode 100644
index 00000000000..2e7223663f7
--- /dev/null
+++ b/admin/tool/installaddon/tests/fixtures/testable_installer_without_site_info.php
@@ -0,0 +1,46 @@
+.
+
+/**
+ * Provides a mock testable_tool_installaddon_installer_without_site_info class.
+ *
+ * @package tool_installaddon
+ * @subpackage fixtures
+ * @category test
+ * @copyright 2026 Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/testable_installer.php');
+
+/**
+ * Testable subclass with site-info sharing disabled.
+ *
+ * @copyright 2026 Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_tool_installaddon_installer_without_site_info extends testable_tool_installaddon_installer {
+ /**
+ * Disable site info sharing.
+ *
+ * @return bool
+ */
+ protected function should_send_site_info() {
+ return false;
+ }
+}
diff --git a/admin/tool/installaddon/tests/installer_test.php b/admin/tool/installaddon/tests/installer_test.php
index f0f2fa2af3e..ac2645c0f14 100644
--- a/admin/tool/installaddon/tests/installer_test.php
+++ b/admin/tool/installaddon/tests/installer_test.php
@@ -18,12 +18,14 @@
namespace tool_installaddon;
use testable_tool_installaddon_installer;
+use testable_tool_installaddon_installer_without_site_info;
use tool_installaddon_installer;
defined('MOODLE_INTERNAL') || die();
global $CFG;
-require_once(__DIR__.'/fixtures/testable_installer.php');
+require_once(__DIR__ . '/fixtures/testable_installer.php');
+require_once(__DIR__ . '/fixtures/testable_installer_without_site_info.php');
/**
* Unit tests for the {@link tool_installaddon_installer} class
@@ -32,16 +34,14 @@ require_once(__DIR__.'/fixtures/testable_installer.php');
* @category test
* @copyright 2013 David Mudrak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \tool_installaddon_installer
*/
final class installer_test extends \advanced_testcase {
public function test_get_addons_repository_url(): void {
$installer = testable_tool_installaddon_installer::instance();
$url = $installer->get_addons_repository_url();
- $query = parse_url($url, PHP_URL_QUERY);
- $this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
- $site = rawurldecode($matches[1]);
- $site = json_decode(base64_decode($site), true);
+ $site = $this->decode_site_info_from_url($url);
$this->assertIsArray($site);
$this->assertEquals(3, count($site));
$this->assertSame('Nasty site', $site['fullname']);
@@ -49,6 +49,34 @@ final class installer_test extends \advanced_testcase {
$this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']);
}
+ /**
+ * Tests that marketplace URL includes expected host, scheme and site payload.
+ */
+ public function test_get_marketplace_url(): void {
+ $installer = testable_tool_installaddon_installer::instance();
+ $addonsurl = $installer->get_addons_repository_url();
+ $marketplaceurl = $installer->get_marketplace_url();
+
+ $this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
+ $this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
+ $this->assertSame(
+ $this->decode_site_info_from_url($addonsurl),
+ $this->decode_site_info_from_url($marketplaceurl),
+ );
+ }
+
+ /**
+ * Tests that marketplace URL has no query string when site info is unavailable.
+ */
+ public function test_get_marketplace_url_without_site_info(): void {
+ $installer = testable_tool_installaddon_installer_without_site_info::instance();
+ $marketplaceurl = $installer->get_marketplace_url();
+
+ $this->assertSame('marketplace.moodle.com', parse_url($marketplaceurl, PHP_URL_HOST));
+ $this->assertSame('https', parse_url($marketplaceurl, PHP_URL_SCHEME));
+ $this->assertEmpty(parse_url($marketplaceurl, PHP_URL_QUERY));
+ }
+
public function test_decode_remote_request(): void {
$installer = testable_tool_installaddon_installer::instance();
@@ -132,7 +160,7 @@ $plugin->version = 2014121300;
$versionphp = file_get_contents($fixtures.'/github/moodle-repository_mahara-master/version.php');
$this->assertEquals('repository_mahara', $installer->testable_detect_plugin_component_from_versionphp($versionphp));
- $versionphp = file_get_contents($fixtures.'/nocomponent/baz/version.php');
+ $versionphp = file_get_contents($fixtures . '/nocomponent/baz/version.php');
$this->assertFalse($installer->testable_detect_plugin_component_from_versionphp($versionphp));
}
@@ -143,15 +171,28 @@ $plugin->version = 2014121300;
$storage1 = $installer->make_installfromzip_storage();
$this->assertTrue(is_dir($storage1));
$this->assertTrue(is_writable($storage1));
- file_put_contents($storage1.'/hello.txt', 'Find me if you can!');
+ file_put_contents($storage1 . '/hello.txt', 'Find me if you can!');
// Check we get unique directory on each call.
$storage2 = $installer->make_installfromzip_storage();
$this->assertTrue(is_dir($storage2));
$this->assertTrue(is_writable($storage2));
- $this->assertFalse(file_exists($storage2.'/hello.txt'));
+ $this->assertFalse(file_exists($storage2 . '/hello.txt'));
// Check both are in the same parent directory.
$this->assertEquals(dirname($storage1), dirname($storage2));
}
+
+ /**
+ * Decodes the encoded 'site' URL query parameter.
+ *
+ * @param moodle_url $url
+ * @return array
+ */
+ private function decode_site_info_from_url(\moodle_url $url): array {
+ $query = parse_url($url, PHP_URL_QUERY);
+ $this->assertEquals(1, preg_match('~^site=(.+)$~', $query, $matches));
+ $site = rawurldecode($matches[1]);
+ return json_decode(base64_decode($site), true);
+ }
}
diff --git a/admin/tool/installaddon/tests/lib_test.php b/admin/tool/installaddon/tests/lib_test.php
new file mode 100644
index 00000000000..d215534a4eb
--- /dev/null
+++ b/admin/tool/installaddon/tests/lib_test.php
@@ -0,0 +1,53 @@
+.
+
+/**
+ * Unit tests for tool_installaddon lib.
+ *
+ * @package tool_installaddon
+ * @copyright 2026 Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_installaddon;
+
+use core_course\local\entity\activity_chooser_footer;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/admin/tool/installaddon/lib.php');
+
+/**
+ * Test installaddon lib functions.
+ */
+final class lib_test extends \advanced_testcase {
+ /**
+ * Tests chooser footer generation for marketplace link content.
+ *
+ * @covers ::tool_installaddon_custom_chooser_footer
+ */
+ public function test_tool_installaddon_custom_chooser_footer(): void {
+ $this->resetAfterTest();
+
+ $footer = \tool_installaddon_custom_chooser_footer(1, 1);
+
+ $this->assertInstanceOf(activity_chooser_footer::class, $footer);
+ $this->assertSame('tool_installaddon/footer', $footer->get_footer_js_file());
+ $this->assertStringContainsString('https://marketplace.moodle.com/', $footer->get_footer_template());
+ $this->assertStringContainsString('site=', $footer->get_footer_template());
+ }
+}
diff --git a/admin/tool/installaddon/tests/task/post_install_test.php b/admin/tool/installaddon/tests/task/post_install_test.php
new file mode 100644
index 00000000000..9a984ad5f2c
--- /dev/null
+++ b/admin/tool/installaddon/tests/task/post_install_test.php
@@ -0,0 +1,47 @@
+.
+
+declare(strict_types=1);
+
+namespace tool_installaddon\task;
+
+/**
+ * Unit tests for the post install task.
+ *
+ * @package tool_installaddon
+ * @category test
+ * @copyright 2026 Safat Shahin
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @covers \tool_installaddon\task\post_install
+ */
+final class post_install_test extends \advanced_testcase {
+ /**
+ * Tests that the task sets the activity chooser footer plugin to tool_installaddon.
+ */
+ public function test_execute_sets_activitychooseractivefooter_config(): void {
+ $this->resetAfterTest();
+
+ set_config('activitychooseractivefooter', 'hidden');
+
+ $task = new post_install();
+ $task->execute();
+
+ $this->assertSame(
+ 'tool_installaddon',
+ get_config('core', 'activitychooseractivefooter'),
+ );
+ }
+}
diff --git a/admin/tool/installaddon/version.php b/admin/tool/installaddon/version.php
index 8722c166b37..2ad5d8eff23 100644
--- a/admin/tool/installaddon/version.php
+++ b/admin/tool/installaddon/version.php
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'tool_installaddon';
-$plugin->version = 2025041400;
+$plugin->version = 2025041401;
$plugin->requires = 2025040800;
$plugin->maturity = MATURITY_STABLE;