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

[5.3] Maintain usable admin on system plugin errors (Ref #39457) #39484

Open
wants to merge 2 commits into
base: 5.3-dev
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions administrator/language/en-GB/joomla.ini
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ JERROR_NOLOGIN_BLOCKED="Login denied! Your account has either been blocked or yo
JERROR_SAVE_FAILED="Could not save data. Error: %s"
JERROR_SENDING_EMAIL="Email could not be sent."
JERROR_SESSION_STARTUP="Error starting the session."
JERROR_CRASH_RECOVERY="We have detected that plugin \"%1$s\" is causing a system-wide error. Would you like to disable it?<br /><a class=\"btn btn-primary\" href=\"%2$s\">Disable %1$s</a>"
JERROR_PLUGIN_DISABLED="Plugin disabled."
JERROR_LAYOUT_DISABLE_PLUGIN="Disable %s"

JFIELD_ACCESS_DESC="The access level group that is allowed to view this item."
JFIELD_ACCESS_LABEL="Access"
Expand Down
5 changes: 5 additions & 0 deletions administrator/templates/atum/error_full.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@
<a href="<?php echo $this->baseurl; ?>" class="btn btn-secondary">
<span class="icon-dashboard" aria-hidden="true"></span>
<?php echo Text::_('JGLOBAL_TPL_CPANEL_LINK_TEXT'); ?></a>
<?php if (!is_null($app->getUserState('exceptionhandler.culprit_name', null)) && !is_null($app->getUserState('exceptionhandler.restore_link', null))) : ?>
<a class="btn btn-danger" href="<?php print htmlspecialchars($app->getUserState('exceptionhandler.restore_link', null), ENT_QUOTES, 'UTF-8'); ?>">
<?php echo Text::sprintf('JERROR_LAYOUT_DISABLE_PLUGIN', $app->getUserState('exceptionhandler.culprit_name', null)); ?>
</a>
<?php endif; ?>
</p>
</div>

Expand Down
138 changes: 138 additions & 0 deletions libraries/src/Exception/ExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
use Joomla\CMS\Error\AbstractRenderer;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Session\Session;

// phpcs:disable PSR1.Files.SideEffects
\defined('JPATH_PLATFORM') or die;
Expand Down Expand Up @@ -69,6 +72,7 @@ public static function handleUserDeprecatedErrors(int $errorNumber, string $erro
public static function handleException(\Throwable $error)
{
static::logException($error);
static::identifyCulprit($error);
static::render($error);
}

Expand Down Expand Up @@ -214,4 +218,138 @@ protected static function logException(\Throwable $error)
// Logging failed, don't make a stink about it though
}
}

/**
* Tries to identify if the error is caused by a plugin that completely blocks the administrator interface.
*
* @param \Throwable $error An Exception or Throwable (PHP 7+) object to get error message from.
*
* @return void
*
* @since 4.2
*/
protected static function identifyCulprit(\Throwable $error)
{
try {
$app = Factory::getApplication();
if ($app->isClient('administrator') && !Factory::getUser()->guest) {

// Clear previous detections
$app->setUserState('exceptionhandler.culprit_name', null);
$app->setUserState('exceptionhandler.restore_link', null);

$traceFiles = array($error->getFile());
foreach($error->getTrace() as $trace) {
$traceFiles[] = $trace['file'];
}

$db = Factory::getDbo();
$culprit = null;
foreach($traceFiles as $traceFile) {
$traceLoc = explode(DIRECTORY_SEPARATOR, str_replace(JPATH_ROOT.DIRECTORY_SEPARATOR, '', $traceFile));
if (
(count($traceLoc) > 3)
&& ($traceLoc[0] == 'plugins')
&& ($traceLoc[1] == 'system')
) {
$query = $db->getQuery(true)
->select(
$db->quoteName(
[
'type',
'name',
'element',
'extension_id',
]
)
)
->from($db->quoteName('#__extensions'))
->where(
[
$db->quoteName('type') . ' = ' . $db->quote('plugin'),
$db->quoteName('folder') . ' = ' . $db->quote('system'),
$db->quoteName('element') . ' = ' . $db->quote($db->escape($traceLoc[2]))
]
);
$db->setQuery($query);

$plugin = $db->loadObject();
if ($plugin) {
$culprit = $plugin;
break;
}
}
}

if ($culprit) {
// A plugin's directory has been found in the error log.
static::handleCulprit($culprit);
}
}
} catch(\Throwable $e) {
// Identifying failed, but we don't want to alter the existing error log.
}
}

/**
* Handles the culprit plugin.
*
* @param \Throwable $error An Exception or Throwable (PHP 7+) object to get error message from.
*
* @return void
*
* @since 4.2
*/
protected static function handleCulprit($culprit) {
try {
$app = Factory::getApplication();
if ($app->isClient('administrator') && !Factory::getUser()->guest) {
if ($app->input->exists('disable_culprit')
&& $app->input->exists('culprit_id')
&& ($app->input->get('culprit_id', 0, 'int') == $culprit->extension_id)
) {
Session::checkToken('get') or die();

// If the admin confirms to disable the culprit, go ahead.
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 0')
->where($db->quoteName('extension_id') . ' = ' . (int) $culprit->extension_id);

$db->lockTable('#__extensions');

$result = $db->setQuery($query)->execute();

$db->unlockTables();

$app->enqueueMessage(Text::_('JERROR_PLUGIN_DISABLED'));

// Where should we return to?
$returnUrl = $app->input->exists('return') ? base64_decode($app->input->getBase64('return')) : 'index.php';
if (!Uri::isInternal($returnUrl)) {
$returnUrl = 'index.php';
}

$app->redirect($returnUrl);
} else {
// Present an option to the admin to disable the offending plugin.
$uri = (string) Uri::getInstance();
$return = urlencode(base64_encode($uri));

$token = urlencode(Session::getFormToken());

$restoreLink = 'index.php?disable_culprit=1&culprit_id=' . (int) $culprit->extension_id . '&return=' . $return . '&' . $token .'=1';

$app->enqueueMessage(Text::sprintf('JERROR_CRASH_RECOVERY', $culprit->name, $restoreLink ), 'error');

// If that fails, we try to render the button through the template.
$app->setUserState('exceptionhandler.culprit_name', $culprit->name);
$app->setUserState('exceptionhandler.restore_link', $restoreLink);
}
}
} catch(\Throwable $e) {
// Handling failed, but we don't want to alter the existing error log.
}
}
}