<?php
/**
* @package Regular Labs Extension Manager
* @version 9.1.0
*
* @author Peter van Westen <[email protected]>
* @link https://regularlabs.com
* @copyright Copyright © 2025 Regular Labs All Rights Reserved
* @license GNU General Public License version 2 or later
*/
namespace RegularLabs\Component\RegularLabsExtensionsManager\Administrator\Helper;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Language\Text as JText;
use RegularLabs\Library\Cache as RL_Cache;
use RegularLabs\Library\DB as RL_DB;
use RegularLabs\Library\DownloadKey as RL_DownloadKey;
use RegularLabs\Library\Http as RL_Http;
use RegularLabs\Library\Input as RL_Input;
use RegularLabs\Library\Language as RL_Language;
use RegularLabs\Library\Parameters as RL_Parameters;
use RegularLabs\Library\RegEx as RL_RegEx;
defined('_JEXEC') or die;
class ExtensionsHelper
{
public static function get($refresh = false)
{
$extensions = self::getExternalData($refresh);
if (empty($extensions))
{
return [];
}
$items = (object) [
'extensionmanager' => [],
'updates_available' => [],
'installed' => [],
'not_installed' => [],
'no_access' => [],
'broken' => [],
];
foreach ($extensions as $alias => &$extension)
{
if ( ! $extension->can_download)
{
unset($extensions[$alias]);
continue;
}
self::initItem($extension);
if ($extension->state === 'not_installed')
{
continue;
}
$has_update = self::hasUpdate($extension);
if ( ! $extension->has_access
&& ($has_update || $extension->state === 'broken')
)
{
$extension->state = 'no_access';
continue;
}
if ($has_update)
{
$extension->state = 'updates_available';
continue;
}
if ($extension->state == 'broken')
{
continue;
}
$extension->state = 'installed';
}
foreach ($extensions as $alias => &$extension)
{
if (
$extension->alias === 'extensionmanager'
&& in_array($extension->state, ['updates_available', 'broken'])
)
{
$items->extensionmanager[$alias] = $extension;
continue;
}
$items->{$extension->state}[$alias] = $extension;
}
return $items;
}
public static function getAvailable($refresh = false)
{
$extensions = self::get($refresh);
return $extensions->not_installed;
}
public static function getBroken($refresh = false)
{
$extensions = self::get($refresh);
return $extensions->broken;
}
public static function getByAlias($alias, $refresh = false)
{
$extensions = self::getExternalData($refresh);
return $extensions[$alias] ?? false;
}
public static function getFromUrl($refresh = false)
{
$extensions = self::get($refresh);
$selection = RL_Input::getAsArray('extensions');
if (empty($selection))
{
$extension = RL_Input::getCmd('extension');
$selection = $extension ? [$extension] : [];
}
if (empty($selection))
{
return [];
}
$list = [];
foreach ($extensions as $group)
{
foreach ($group as $extension)
{
if ( ! in_array($extension->alias, $selection))
{
continue;
}
$list[] = $extension;
}
}
return $list;
}
public static function getInstalled($refresh = false)
{
$extensions = self::get($refresh);
return $extensions->installed;
}
public static function getUpdates($refresh = false)
{
$extensions = self::get($refresh);
return $extensions->updates_available;
}
private static function getAvailableVersion($extension)
{
if (is_object($extension->version))
{
return $extension->version;
}
return (object) [
'version' => $extension->version,
'stable' => self::getStableVersion($extension),
'is_pro' => $extension->pro && $extension->has_access,
];
}
private static function getCurrentVersion($extension)
{
if (isset($extension->current_version))
{
return $extension->current_version;
}
$version = 0;
foreach ($extension->types as $type)
{
if (empty($type->version))
{
continue;
}
$version = $type->version;
break;
}
if ( ! $version)
{
return (object) [
'version' => '',
'is_pro' => false,
];
}
return (object) [
'version' => str_ireplace(['PRO', 'FREE'], '', $version),
'is_pro' => stripos($version, 'PRO') !== false,
];
}
/**
* @param false $refresh
*
* @return array
*/
private static function getExternalData($refresh = false)
{
$cache = new RL_Cache('ExtensionsManager.getExternalData');
$cache->useFiles(5);
if ( ! $refresh && $cache->exists())
{
return $cache->get();
}
$config = RL_Parameters::getComponent('regularlabsmanager');
$url = 'https://download.regularlabs.com/extensions.json?j=4';
if ($config->updatesource == 'dev')
{
$url .= '&dev=1';
}
$key = RL_DownloadKey::get(false);
if ($key)
{
$url .= '&k=' . $key;
}
$timeout = ((int) $config->timeout ?? 5) ?: 5;
$content = RL_Http::getFromUrl($url, $timeout);
if ( ! $content)
{
return [];
}
$content = (array) json_decode($content);
return $cache->set($content);
}
private static function getInstalledState($extension)
{
$installed = false;
foreach ($extension->types as $type)
{
if (empty($type->version))
{
continue;
}
$installed = true;
break;
}
foreach ($extension->types as $type)
{
if (empty($type->version) && $installed)
{
return 'broken';
}
}
return $installed ? 'installed' : 'not_installed';
}
private static function getJoomlaVersion($path)
{
$cache = new RL_Cache;
if ($cache->exists())
{
return $cache->get();
}
if ( ! $path)
{
return $cache->set(false);
}
if ( ! file_exists($path))
{
return $cache->set(false);
}
$xml = simplexml_load_file($path);
if ( ! $xml)
{
return $cache->set(false);
}
return $cache->set((int) ($xml->attributes()['version'] ?? 0));
}
private static function getStableVersion($extension)
{
if ( ! str_contains($extension->version, 'dev'))
{
return $extension->version;
}
RL_RegEx::match('>[0-9]+-[a-zA-Z]+-[0-9]+ : v([0-9\\.]+)<br>', $extension->changelog, $stable_version);
if (empty($stable_version))
{
return strtok($extension->version, '-');
}
return $stable_version[1];
}
private static function getTypeData($type, $extension)
{
switch ($type)
{
case 'package':
case 'pkg':
return (object) [
'type' => 'pkg',
'xml_path' => JPATH_ADMINISTRATOR . '/manifests/packages/pkg_' . $extension->extname . '.xml',
];
case 'component':
case 'com':
return (object) [
'type' => 'com',
'text' => JText::_('RL_COM'),
'letter' => 'C',
'class' => 'success',
'url' => self::getURL($type, $extension->extname),
'xml_path' => JPATH_ADMINISTRATOR . '/components/com_' . $extension->extname . '/' . $extension->extname . '.xml',
];
case 'module':
case 'mod':
return (object) [
'type' => 'mod',
'text' => JText::_('RL_MOD'),
'letter' => 'M',
'class' => 'danger',
'url' => self::getURL($type, $extension->extname),
'xml_path' => JPATH_ADMINISTRATOR . '/modules/mod_' . $extension->extname . '/mod_' . $extension->extname . '.xml',
];
case 'library':
case 'lib':
return (object) [
'type' => 'lib',
'text' => JText::_('RL_LIB'),
'letter' => 'L',
'class' => 'warning text-black',
'url' => '',
'xml_path' => JPATH_LIBRARIES . '/' . $extension->extname . '/' . $extension->extname . '.xml',
];
case 'plg_actionlog':
case 'plg_editors-xtd':
case 'plg_fields':
case 'plg_system':
default:
$plugin_type = substr($type, 4);
$plugin_letter = strtoupper(substr($plugin_type, 0, 1));
if ($plugin_type === 'editors-xtd')
{
$plugin_letter = 'B';
}
return (object) [
'type' => $type,
'text' => JText::_('RL_' . strtoupper($type)),
'letter' => 'P<small>' . $plugin_letter . '</small>',
'class' => 'info',
'url' => self::getURL($type, $extension->extname),
'xml_path' => JPATH_PLUGINS . '/' . $plugin_type . '/' . $extension->extname . '/' . $extension->extname . '.xml',
];
}
}
/**
* Get the extension url
*/
private static function getURL($type, $element, $client_id = 1)
{
[$type, $folder] = explode('_', $type . '_');
switch ($type)
{
case 'com':
return 'index.php?option=com_' . $element;
case 'mod':
return 'index.php?option=com_modules&client_id=' . $client_id
. '&filter[module]=mod_' . $element . '&filter[search]=';
case 'plg':
$query = RL_DB::getQuery()
->select('name')
->from('#__extensions')
->where(RL_DB::is('type', 'plugin'))
->where(RL_DB::is('folder', $folder))
->where(RL_DB::is('element', $element));
$name = RL_DB::get()->setQuery($query)->loadResult();
RL_Language::load('plg_' . $folder . '_' . $element . '.sys', '', true);
$name = JText::_($name);
$name = RL_RegEx::replace('^(.*?)\?.*$', '\1', $name);
return 'index.php?option=com_plugins&filter[folder]=&filter[search]=' . $name;
default:
return '';
}
}
private static function getXMLVersion($path)
{
$cache = new RL_Cache;
if ($cache->exists())
{
return $cache->get();
}
if ( ! $path)
{
return $cache->set(false);
}
$xml = Installer::parseXMLInstallFile($path);
return $cache->set($xml['version'] ?? '');
}
private static function hasAccess($extension)
{
if ( ! $extension->has_pro)
{
return true;
}
if ( ! $extension->current_version->is_pro)
{
return true;
}
return $extension->pro;
}
private static function hasUpdate($extension)
{
if ($extension->joomla_version !== 4)
{
return true;
}
$current = self::getCurrentVersion($extension);
if ( ! $current->is_pro && $extension->pro)
{
return true;
}
return version_compare($current->version, $extension->version->version, '<');
}
private static function initItem(&$extension)
{
self::setTypeData($extension);
$extension->current_version = self::getCurrentVersion($extension);
$extension->has_access = self::hasAccess($extension);
$extension->version = self::getAvailableVersion($extension);
$extension->state = self::getInstalledState($extension);
if ($extension->alias == 'extensionmanager')
{
$extension->name = 'Regular Labs Extension Manager';
}
}
private static function setTypeData(&$extension)
{
$extension->joomla_version = 4;
$pkg_file = JPATH_ADMINISTRATOR . '/manifests/packages/pkg_' . $extension->extname . '.xml';
if (file_exists($pkg_file))
{
array_unshift($extension->types, 'pkg');
}
foreach ($extension->types as &$type)
{
if (is_object($type))
{
continue;
}
$type = self::getTypeData($type, $extension);
$type->version = self::getXMLVersion($type->xml_path);
$xml_joomla_version = self::getJoomlaVersion($type->xml_path);
if ($xml_joomla_version)
{
$extension->joomla_version = min($extension->joomla_version, $xml_joomla_version);
}
}
}
}