Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add uninstall option to modules #3449

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 11 additions & 7 deletions core/classes/Core/Util.php
Expand Up @@ -49,8 +49,13 @@ public static function cyrillicToLatin(string $string): string
*/
public static function recursiveRemoveDirectory(string $directory): bool
{
// safety precaution, only allow deleting files in "custom" directory
if (!str_contains($directory, 'custom')) {
// safety precaution, only allow deleting files in "custom", "modules" or "uploads" directory
if (
str_contains($directory, 'Core') ||
!str_contains($directory, 'custom') ||
!str_contains($directory, 'modules') ||
!str_contains($directory, 'uploads')
) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not look right. Won’t this always return false unless the string contains custom, modules AND uploads?

Copy link
Member

@Derkades Derkades Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine to me. It aborts if:

  • The path contains "Core" (to prevent deleting modules/Core)
  • OR the path does not contain "custom"
  • OR the path does not contain "modules"
  • OR the path does not contain "uploads"

A resolved absolute path check would be better. Although this is not a security feature so it doesn't matter if it can be bypassed.

return false;
}

Expand All @@ -59,10 +64,8 @@ public static function recursiveRemoveDirectory(string $directory): bool
if (!self::recursiveRemoveDirectory($file)) {
return false;
}
} else {
if (!unlink($file)) {
return false;
}
} elseif (!unlink($file)) {
return false;
}
}

Expand All @@ -74,7 +77,8 @@ public static function recursiveRemoveDirectory(string $directory): bool
/**
* Get an array containing all timezone lists.
*
* @return array All timezones.
* @throws Exception
* @return array All timezones.
*/
public static function listTimezones(): array
{
Expand Down
40 changes: 38 additions & 2 deletions custom/panel_templates/Default/core/modules.tpl
Expand Up @@ -67,7 +67,7 @@
<div class="float-md-right">
{if $module.enabled}
{if $module.disable_link}
<form action="{$module.disable_link}" method="post">
<form action="{$module.disable_link}" method="post" style="display:inline">
<input type="hidden" name="token" value="{$TOKEN}" />
<input type="submit" class="btn btn-danger btn-sm"
value="{$DISABLE}" />
Expand All @@ -77,12 +77,17 @@
class="fa fa-lock"></i></a>
{/if}
{else}
<form action="{$module.enable_link}" method="post">
<form action="{$module.enable_link}" method="post" style="display:inline">
<input type="hidden" name="token" value="{$TOKEN}" />
<input type="submit" class="btn btn-primary btn-sm"
value="{$ENABLE}" />
</form>
{/if}
{if $module.uninstall_link}
<button class="btn btn-danger btn-sm" onclick="uninstallModule('{$module.uninstall_link}', '{$module.confirm_uninstall}')" style="display:inline">
{$UNINSTALL}
</button>
{/if}
</div>
</td>
</tr>
Expand Down Expand Up @@ -168,6 +173,29 @@
<!-- End Content Wrapper -->
</div>

<div class="modal fade" id="uninstallModuleModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{$UNINSTALL}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form action="" method="post" id="uninstallModuleForm">
<div class="modal-body">
<p id="confirmUninstallModule"></p>
</div>
<div class="modal-footer">
<input type="hidden" name="token" value="{$TOKEN}">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{$CANCEL}</button>
<input type="submit" class="btn btn-primary" value="{$UNINSTALL}">
</div>
</form>
</div>
</div>
</div>

<!-- End Wrapper -->
</div>

Expand All @@ -191,6 +219,14 @@
SetRatingStar();
</script>

<script type="text/javascript">
function uninstallModule(action, confirmationText) {
$('#uninstallModuleForm').attr('action', action);
$('#confirmUninstallModule').html(confirmationText);
$('#uninstallModuleModal').modal().show();
}
</script>

</body>

</html>
4 changes: 4 additions & 0 deletions modules/Core/language/en_UK.json
Expand Up @@ -695,6 +695,7 @@
"admin/type": "Type",
"admin/type_required": "A type is required",
"admin/unable_to_delete_group": "Unable to delete a default group, or a group that can view the StaffCP. Please update the group settings first!",
"admin/unable_to_delete_module_files": "Unable to fully delete module. Please check file permissions.",
"admin/unable_to_delete_template": "Unable to fully delete template. Please check file permissions.",
"admin/unable_to_disable_module": "Unable to disable module - the module {{module}} depends on it.",
"admin/unable_to_enable_module": "Unable to enable incompatible module.",
Expand All @@ -706,7 +707,10 @@
"admin/unable_to_retrieve_modules": "Unable to retrieve modules",
"admin/unable_to_retrieve_nameless_news": "Unable to retrieve the latest news",
"admin/unable_to_retrieve_templates": "Unable to retrieve templates",
"admin/unable_to_uninstall_module": "Unable to uninstall module - the module {{module}} depends on it.",
"admin/unknown": "Unknown",
"admin/uninstall": "Uninstall",
"admin/uninstall_confirm": "Are you sure you want to uninstall {{item}}?",
"admin/unlink": "Unlink",
"admin/unlink_account_confirm": "Are you sure you want to forcibly unlink this provider from this user?",
"admin/unlink_account_success": "Successfully unlinked their account from {{provider}}.",
Expand Down
65 changes: 64 additions & 1 deletion modules/Core/pages/panel/modules.php
Expand Up @@ -47,6 +47,7 @@
}

try {
/** @var Module $module */
require_once ROOT_PATH . '/modules/' . $item->name . '/init.php';
} catch (Exception $e) {
$term = 'unable_to_load_module';
Expand Down Expand Up @@ -74,6 +75,8 @@
'actualVersion' => Text::bold(NAMELESS_VERSION)
]) : false,
'disable_link' => (($module->getName() != 'Core' && $item->enabled) ? URL::build('/panel/core/modules/', 'action=disable&m=' . urlencode($item->id)) : null),
'uninstall_link' => ($module->getName() != 'Core' ? URL::build('/panel/core/modules/', 'action=uninstall&m=' . urlencode($item->id)) : null),
'confirm_uninstall' => $language->get('admin', 'uninstall_confirm', ['item' => Output::getClean($module->getName())]),
'enable_link' => (($module->getName() != 'Core' && !$item->enabled) ? URL::build('/panel/core/modules/', 'action=enable&m=' . urlencode($item->id)) : null),
'enabled' => $item->enabled
];
Expand Down Expand Up @@ -133,6 +136,7 @@
'AUTHOR' => $language->get('admin', 'author'),
'ENABLE' => $language->get('admin', 'enable'),
'DISABLE' => $language->get('admin', 'disable'),
'UNINSTALL' => $language->get('admin', 'uninstall'),
'MODULE_LIST' => $template_array,
'FIND_MODULES' => $language->get('admin', 'find_modules'),
'WEBSITE_MODULES' => $all_modules,
Expand Down Expand Up @@ -333,6 +337,64 @@

Redirect::to(URL::build('/panel/core/modules'));
}

if ($_GET['action'] === 'uninstall') {
// Disable a module
if (!isset($_GET['m']) || !is_numeric($_GET['m']) || $_GET['m'] == 1) {
die('Invalid module!');
}

if (Token::check($_POST['token'])) {
// Get module name
$name = DB::getInstance()->get('modules', ['id', $_GET['m']])->results();
$name = Output::getClean($name[0]->name);

foreach (Module::getModules() as $item) {
if (in_array($name, $item->getLoadAfter())) {
// Unable to disable module
Session::flash('admin_modules_error', $language->get('admin', 'unable_to_uninstall_module', ['module' => Output::getClean($item->getName())]));
Redirect::to(URL::build('/panel/core/modules'));
}
}

DB::getInstance()->delete('modules', [
'id' => $_GET['m'],
]);

// Cache
$cache->setCache('modulescache');
$modules = [];

$order = Module::determineModuleOrder();

foreach ($order['modules'] as $key => $item) {
if ($item != $name) {
$modules[] = [
'name' => $item,
'priority' => $key
];
}
}

// Store
$cache->store('enabled_modules', $modules);

if (file_exists(ROOT_PATH . '/modules/' . $name . '/init.php')) {
/** @var Module $module */
require_once(ROOT_PATH . '/modules/' . $name . '/init.php');
$module->onUninstall();
}

if (!Util::recursiveRemoveDirectory(ROOT_PATH . '/modules/' . $name)) {
Session::flash('admin_modules_error', $language->get('admin', 'unable_to_delete_module_files'));
}

Session::flash('admin_modules', $language->get('admin', 'module_uninstalled'));

} else {
Session::flash('admin_modules_error', $language->get('general', 'invalid_token'));
}
}
}

if (Session::exists('admin_modules')) {
Expand Down Expand Up @@ -367,7 +429,8 @@
'MODULES' => $language->get('admin', 'modules'),
'PAGE' => PANEL_PAGE,
'TOKEN' => Token::get(),
'SUBMIT' => $language->get('general', 'submit')
'SUBMIT' => $language->get('general', 'submit'),
'CANCEL' => $language->get('general', 'cancel'),
]);

$template->onPageLoad();
Expand Down