| Current Path : /proc/831732/cwd/public_html/pano.4uweb.ca/src/Task/ |
| Current File : //proc/831732/cwd/public_html/pano.4uweb.ca/src/Task/PluginUpdatesDirector.php |
<?php
/**
* @package panopticon
* @copyright Copyright (c)2023-2024 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license https://www.gnu.org/licenses/agpl-3.0.txt GNU Affero General Public License, version 3 or later
*/
namespace Akeeba\Panopticon\Task;
defined('AKEEBA') || die;
use Akeeba\Panopticon\Library\Task\AbstractCallback;
use Akeeba\Panopticon\Library\Task\Attribute\AsTask;
use Akeeba\Panopticon\Library\Task\Status;
use Akeeba\Panopticon\Library\Version\Version;
use Akeeba\Panopticon\Model\Reports;
use Akeeba\Panopticon\Model\Site;
use Akeeba\Panopticon\Model\Sysconfig;
use Akeeba\Panopticon\Task\Trait\EnqueuePluginUpdateTrait;
use Awf\Registry\Registry;
use Awf\Utils\ArrayHelper;
#[AsTask(
name: 'pluginupdatesdirector',
description: 'PANOPTICON_TASKTYPE_PLUGINUPDATESDIRECTOR'
)]
class PluginUpdatesDirector extends AbstractCallback
{
use EnqueuePluginUpdateTrait;
public function __invoke(object $task, Registry $storage): int
{
$task->params ??= new Registry();
$params = ($task->params instanceof Registry) ? $task->params : new Registry($task->params);
$limitStart = (int)$storage->get('limitStart', 0);
$limit = (int)$storage->get('limit', $params->get('limit', 100));
$force = (bool)$storage->get('force', $params->get('force', false));
$filterIDs = $storage->get('filter.ids', $params->get('ids', []));
/**
* Reasoning behind this code:
*
* “The correct way to use LOCK TABLES and UNLOCK TABLES with transactional tables, such as InnoDB tables, is to
* begin a transaction with SET autocommit = 0 (not START TRANSACTION) followed by LOCK TABLES, and to not call
* UNLOCK TABLES until you commit the transaction explicitly.”
*
* This is meant to avoid deadlocks.
*
* @see https://dev.mysql.com/doc/refman/5.7/en/lock-tables.html
*/
// Lock the #__sites and #__tasks tables
$db = $this->container->db;
$db->setQuery('SET autocommit = 0')->execute();
$sql = 'LOCK TABLES ' . $db->quoteName('#__sites') . ' WRITE, '
. $db->quoteName('#__tasks') . ' WRITE, '
. $db->quoteName('#__queue') . ' WRITE';
$db->setQuery($sql)->execute();
$siteIDs = $this->getSiteIDs($limitStart, $limit, $force, $filterIDs);
if (empty($siteIDs))
{
$this->logger->info('No more sites in need of automatic plugin / theme updates.');
$db->unlockTables();
$db->setQuery('SET autocommit = 1')->execute();
return Status::OK->value;
}
$this->logger->info(sprintf(
'Found a further %d site(s) to process for automatic plugin / theme updates.',
count($siteIDs)
));
// Set `extensions.lastAutoUpdateEnqueueTime` to current timestamp for all sites to be processed
$query = $db->getQuery(true)
->update($db->quoteName('#__sites'));
$query->set(
$db->quoteName('config') . '= JSON_SET(' . $db->quoteName('config') . ',' .
$db->quote('$.extensions.lastAutoUpdateEnqueueTime') . ',' . $db->quote(time()) . ')'
)
->where($db->quoteName('id') . ' IN(' . implode(',', $siteIDs) . ')');
$db->setQuery($query)->execute();
// End the transaction
$db->setQuery('COMMIT')->execute();
$db->unlockTables();
$db->setQuery('SET autocommit = 1')->execute();
/** @var Site $site */
$site = $this->container->mvcFactory->makeTempModel('Site');
/** @var Sysconfig $sysConfigModel */
$sysConfigModel = $this->container->mvcFactory->makeTempModel('Sysconfig');
$globalExtUpdatePreferences = $sysConfigModel->getExtensionPreferencesAndMeta(null);
$defaultExtUpdatePreference = $this->container->appConfig->get('tasks_extupdate_install', 'none');
foreach ($siteIDs as $id)
{
$site->reset();
try
{
$site->findOrFail($id);
}
catch (\RuntimeException $e)
{
$this->logger->notice(
sprintf('Site %d not found; skipping.', $id)
);
continue;
}
$siteConfig = $site->getConfig() ?? new Registry();
$extensions = (array)$siteConfig->get('extensions.list');
if (empty($extensions))
{
$this->logger->debug(
sprintf(
'There are no known plugins / themes for site #%d (%s)',
$site->id, $site->name
)
);
continue;
}
$extensionsWithMeta = $sysConfigModel->getExtensionPreferencesAndMeta($site->id);
$lastSeenVersions = $siteConfig->get('director.pluginupdates.lastSeen', []) ?: [];
$lastSeenVersions = is_object($lastSeenVersions) ? (array)$lastSeenVersions : $lastSeenVersions;
$lastSeenVersions = is_array($lastSeenVersions) ? $lastSeenVersions : [];
$extensions = array_filter(
$extensions,
function ($item) use (
$globalExtUpdatePreferences, $defaultExtUpdatePreference, $sysConfigModel, $extensionsWithMeta,
$lastSeenVersions
): bool
{
/**
* Can't update a plugin / theme without version information, or when the installed version is newer
* than the ostensibly latest version.
*/
$currentVersion = $item->version?->current;
$newVersion = $item->version?->new;
if (empty($currentVersion) || empty($newVersion) || $currentVersion === $newVersion
|| version_compare($currentVersion, $newVersion, 'ge'))
{
return false;
}
// Skip plugin / theme if its new version is the same we last saw trying to update it.
$key = $sysConfigModel->getExtensionShortname($item->type, $item->element, $item->folder, $item->client_id);
$lastSeenVersion = $lastSeenVersions[$key] ?? null;
if ($lastSeenVersion !== null && $lastSeenVersion == $newVersion)
{
return false;
}
// Get the plugin / theme shortname (key)
$key = $sysConfigModel
->getExtensionShortname($item->type, $item->element, $item->folder, $item->client_id);
// Parse the update preference
$effectivePreference =
$extensionsWithMeta[$key]?->preference ?: $globalExtUpdatePreferences[$key]?->preference;
$effectivePreference = $effectivePreference ?: $defaultExtUpdatePreference;
// Exclude plugins / themes we are told to do nothing with.
return $effectivePreference !== 'none';
}
);
if (empty($extensions))
{
$this->logger->debug(
sprintf(
'There are no plugins / themes which need to be updated on site #%d (%s)',
$site->id, $site->name
)
);
continue;
}
// Enqueue necessary updates
$numExtensionsUpdate = 0;
$numExtensionsEmail = 0;
foreach ($extensions as $item)
{
$key = $sysConfigModel
->getExtensionShortname($item->type, $item->element, $item->folder, $item->client_id);
if (empty($key))
{
// Invalid plugin / theme
continue;
}
// Log plugin / theme update found
try {
Reports::fromExtensionUpdateFound(
$site->getId(),
$key,
$item->name,
$item->version?->current,
$item->version?->new
)->save();
} catch (\Throwable $e) {
$this->logger->error(
sprintf(
'Problem saving report log entry [%s:%s]: %d %s',
$e->getFile(), $e->getLine(), $e->getCode(), $e->getMessage()
)
);
}
$effectivePreference =
$extensionsWithMeta[$key]?->preference ?: $globalExtUpdatePreferences[$key]?->preference;
$effectivePreference = $effectivePreference ?: $defaultExtUpdatePreference;
/**
* Plugins / themes here can have an effective preference of email, major, minor, or patch.
*
* - `email`. Always sends an email. No post-processing necessary.
* - `major`. Always installs an update. No post-processing necessary.
* - `minor`. Only install an update if it's a minor or patch version update, otherwise send email.
* - `patch`. Only install an update if it's a patch version update, otherwise send email.
*
* Therefore, if the effective preference is `minor` or `patch` I need to check the old and new versions
* to decide what kind of action to execute.
*/
if (in_array($effectivePreference, ['minor', 'patch']))
{
// Parse the old (current) and new versions.
$currentVersion = $item->version?->current;
$newVersion = $item->version?->new;
$vOld = Version::create($currentVersion);
$vNew = Version::create($newVersion);
// Install a new version by comparing the old and new versions, base on the effective preference.
$isInstall = match ($effectivePreference)
{
default => false,
'minor' => $vOld->major() === $vNew->major(),
'patch' => $vOld->versionFamily() === $vNew->versionFamily(),
};
// If I am not installing an update then I must just send an email.
if (!$isInstall)
{
$effectivePreference = 'email';
}
}
$added = $this->enqueuePluginUpdate(
$site,
($item->type === 'plugin' ? 'plg_' : 'tpl_') .
str_replace('/', '_', $item->extension_id),
$effectivePreference
);
if ($added)
{
$effectivePreference === 'email' ? $numExtensionsEmail++ : $numExtensionsUpdate++;
}
}
/**
* If I am here there is at least one extension which needs automatic updates. Therefore, I need to make
* sure that there is an extensions update task scheduled for this site.
*/
$this->schedulePluginsUpdateForSite($site, $this->container);
/**
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* LOGGING ONLY BELOW THIS LINE
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
if ($numExtensionsUpdate > 0)
{
if ($numExtensionsUpdate === 1)
{
$this->logger->debug(
sprintf(
'1 plugin / theme was queued for automatic updates on site #%d (%s)',
$site->id, $site->name
)
);
}
else
{
$this->logger->debug(
sprintf(
'%d plugins / themes were queued for automatic updates on site #%d (%s)',
$numExtensionsUpdate, $site->id, $site->name
)
);
}
continue;
}
if ($numExtensionsEmail > 0)
{
if ($numExtensionsEmail === 1)
{
$this->logger->debug(
sprintf(
'1 plugin / theme was queued for update email on site #%d (%s)',
$site->id, $site->name
)
);
}
else
{
$this->logger->debug(
sprintf(
'%d plugins / themes were queued for update emails on site #%d (%s)',
$numExtensionsUpdate, $site->id, $site->name
)
);
}
continue;
}
if ($numExtensionsUpdate <= 0 && $numExtensionsEmail <= 0)
{
$this->logger->debug(
sprintf(
'No plugins / themes were queued for automatic updates or email on site #%d (%s) - all plugins / themes were already queued up',
$site->id, $site->name
)
);
}
}
$storage->set('limitStart', $limitStart + $limit);
return Status::WILL_RESUME->value;
}
private function getSiteIDs(int $limitStart = 0, int $limit = 100, bool $force = false, array $ids = []): array
{
$db = $this->container->db;
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__sites'));
$query->where([
$db->quoteName('enabled') . ' = 1',
$query->jsonExtract($db->quoteName('config'), '$.cmsType') . ' = ' . $db->quote('wordpress')
]);
if (!$force)
{
// Only allow update enqueueing to run every 30'
$timeLimit = time() - 1800;
$query->extendWhere('AND', [
$query->jsonExtract($db->quoteName('config'), '$.extensions.lastAutoUpdateEnqueueTime') . ' IS NULL',
$query->jsonExtract($db->quoteName('config'), '$.extensions.lastAutoUpdateEnqueueTime') . ' <= ' .
$db->quote($timeLimit),
], 'OR');
}
$ids = ArrayHelper::toInteger($ids);
$ids = array_filter($ids, fn($x) => !empty($x));
if (!empty($ids))
{
$query->where(
$db->quoteName('id') . ' IN (' . implode(',', $ids) . ')'
);
}
return $db->setQuery($query, $limitStart, $limit)->loadColumn() ?: [];
}
}