fix: derive Joomla element name from package_type + repo name #635

Closed
opened 2026-06-18 13:28:17 +00:00 by jmiller · 0 comments
Owner

Problem

The Joomla update server uses the manifest <name> field directly as the <element> in updates.xml. This produces incorrect element names that don't match what Joomla registers in #__extensions.

Current behavior:

<element>MokoSuiteBackup</element>
<type>package</type>

Expected behavior:

<element>pkg_mokosuitebackup</element>
<type>package</type>

Joomla's updater matches updates by element + type + client_id. When the element name doesn't match, updates are invisible.

How Joomla Actually Derives Element Names (from source)

Analyzed from joomla-cms/6.1-dev/libraries/src/Installer/.

Base class: InstallerAdapter::getElement()

public function getElement($element = null)
{
    if (!$element) {
        $element = (string) $this->getManifest()->element;  // <element> tag
    }
    if (!$element) {
        $element = $this->getName();  // <name> tag
    }
    return strtolower(InputFilter::getInstance()->clean($element, 'cmd'));
}

Key: Base class uses <element> tag first, falls back to <name>, then lowercases and strips non-alphanumeric chars via InputFilter::clean('cmd').

PackageAdapter::getElement()

$element = (string) $this->getManifest()->packagename;  // <packagename> tag
$element = 'pkg_' . InputFilter::getInstance()->clean($element, 'cmd');

Source of element: <packagename> tag (NOT <name>), prefixed with pkg_. The cmd filter lowercases and strips special chars.

ComponentAdapter::getElement()

$element = parent::getElement($element);  // base class logic
if (!str_starts_with($element, 'com_')) {
    $element = 'com_' . $element;
}

Source: <element> tag or <name> tag from base class, prefixed with com_.

ModuleAdapter::getElement()

if ((string) $this->getManifest()->element) {
    return (string) $this->getManifest()->element;  // <element> tag (Joomla 4+)
}
// Legacy: reads module attribute from <files> children
return strtolower((string) $file->attributes()->module);

Source: <element> tag (J4+), or module attribute on files (legacy). No prefix added — modules already include mod_ in their element name.

PluginAdapter::getElement()

// Reads the type-named attribute from <files> children
$element = (string) $file->attributes()->$type;  // e.g. plugin="mokosuitebackup"

Source: The attribute matching the extension type (e.g. plugin=\"name\") on the files element. No prefix — plugins use bare names.

LibraryAdapter::getElement()

$element = (string) $this->getManifest()->libraryname;  // <libraryname> tag

Source: <libraryname> tag. No prefix added — libraries already include lib_ in their name.

TemplateAdapter

No getElement() override — uses base class (<element> or <name>, lowercased).

Summary Table

Type Element Source Prefix Lowercased
package <packagename> pkg_ Yes (InputFilter cmd)
component <element> or <name> com_ (if missing) Yes (strtolower)
module <element> tag None (already has mod_) Yes (strtolower)
plugin plugin=\"name\" file attr None No explicit lowercase
library <libraryname> None (already has lib_) No explicit lowercase
template <element> or <name> None Yes (strtolower)
file <element> or <name> None Yes (strtolower)

Proposed Fix

In services/updateserver/joomla.go, resolveExtensionMetadata() should derive the element name based on extType:

func deriveJoomlaElement(extType, name string) string {
    // InputFilter::clean('cmd') strips non-alphanumeric (except . _ -)
    // and lowercases. We replicate that here.
    lower := strings.ToLower(name)
    // Remove characters that Joomla's cmd filter would strip
    var clean strings.Builder
    for _, r := range lower {
        if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
            clean.WriteRune(r)
        }
    }
    element := clean.String()

    switch extType {
    case \"package\":
        return \"pkg_\" + element
    case \"component\":
        if !strings.HasPrefix(element, \"com_\") {
            return \"com_\" + element
        }
        return element
    default:
        // modules, plugins, templates, libraries — element already
        // includes the correct prefix from the manifest
        return element
    }
}

For packages, MokoGitea should use the <packagename> field from pkg_mokosuitebackup.xml if available, or derive from the repo name. The element_name manifest API field should remain as an explicit override.

Client ID Mapping

The <client> tag in updates.xml maps to client_id:

  • \"site\"client_id = 0
  • \"administrator\"client_id = 1

Packages always use client_id = 0 (site) in #__extensions, regardless of their children. The update feed must match.

Affected

All Joomla repos using the built-in update server. Currently blocking update delivery for MokoSuiteBackup.

## Problem The Joomla update server uses the manifest `<name>` field directly as the `<element>` in `updates.xml`. This produces incorrect element names that don't match what Joomla registers in `#__extensions`. **Current behavior:** ```xml <element>MokoSuiteBackup</element> <type>package</type> ``` **Expected behavior:** ```xml <element>pkg_mokosuitebackup</element> <type>package</type> ``` Joomla's updater matches updates by `element` + `type` + `client_id`. When the element name doesn't match, updates are invisible. ## How Joomla Actually Derives Element Names (from source) Analyzed from `joomla-cms/6.1-dev/libraries/src/Installer/`. ### Base class: `InstallerAdapter::getElement()` ```php public function getElement($element = null) { if (!$element) { $element = (string) $this->getManifest()->element; // <element> tag } if (!$element) { $element = $this->getName(); // <name> tag } return strtolower(InputFilter::getInstance()->clean($element, 'cmd')); } ``` **Key:** Base class uses `<element>` tag first, falls back to `<name>`, then **lowercases** and strips non-alphanumeric chars via `InputFilter::clean('cmd')`. ### PackageAdapter::getElement() ```php $element = (string) $this->getManifest()->packagename; // <packagename> tag $element = 'pkg_' . InputFilter::getInstance()->clean($element, 'cmd'); ``` **Source of element:** `<packagename>` tag (NOT `<name>`), prefixed with `pkg_`. The `cmd` filter lowercases and strips special chars. ### ComponentAdapter::getElement() ```php $element = parent::getElement($element); // base class logic if (!str_starts_with($element, 'com_')) { $element = 'com_' . $element; } ``` **Source:** `<element>` tag or `<name>` tag from base class, prefixed with `com_`. ### ModuleAdapter::getElement() ```php if ((string) $this->getManifest()->element) { return (string) $this->getManifest()->element; // <element> tag (Joomla 4+) } // Legacy: reads module attribute from <files> children return strtolower((string) $file->attributes()->module); ``` **Source:** `<element>` tag (J4+), or `module` attribute on files (legacy). No prefix added — modules already include `mod_` in their element name. ### PluginAdapter::getElement() ```php // Reads the type-named attribute from <files> children $element = (string) $file->attributes()->$type; // e.g. plugin="mokosuitebackup" ``` **Source:** The attribute matching the extension type (e.g. `plugin=\"name\"`) on the files element. No prefix — plugins use bare names. ### LibraryAdapter::getElement() ```php $element = (string) $this->getManifest()->libraryname; // <libraryname> tag ``` **Source:** `<libraryname>` tag. No prefix added — libraries already include `lib_` in their name. ### TemplateAdapter No `getElement()` override — uses base class (`<element>` or `<name>`, lowercased). ## Summary Table | Type | Element Source | Prefix | Lowercased | |------|--------------|--------|-----------| | package | `<packagename>` | `pkg_` | Yes (InputFilter cmd) | | component | `<element>` or `<name>` | `com_` (if missing) | Yes (strtolower) | | module | `<element>` tag | None (already has `mod_`) | Yes (strtolower) | | plugin | `plugin=\"name\"` file attr | None | No explicit lowercase | | library | `<libraryname>` | None (already has `lib_`) | No explicit lowercase | | template | `<element>` or `<name>` | None | Yes (strtolower) | | file | `<element>` or `<name>` | None | Yes (strtolower) | ## Proposed Fix In `services/updateserver/joomla.go`, `resolveExtensionMetadata()` should derive the element name based on `extType`: ```go func deriveJoomlaElement(extType, name string) string { // InputFilter::clean('cmd') strips non-alphanumeric (except . _ -) // and lowercases. We replicate that here. lower := strings.ToLower(name) // Remove characters that Joomla's cmd filter would strip var clean strings.Builder for _, r := range lower { if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' { clean.WriteRune(r) } } element := clean.String() switch extType { case \"package\": return \"pkg_\" + element case \"component\": if !strings.HasPrefix(element, \"com_\") { return \"com_\" + element } return element default: // modules, plugins, templates, libraries — element already // includes the correct prefix from the manifest return element } } ``` For packages, MokoGitea should use the `<packagename>` field from `pkg_mokosuitebackup.xml` if available, or derive from the repo name. The `element_name` manifest API field should remain as an explicit override. ## Client ID Mapping The `<client>` tag in updates.xml maps to `client_id`: - `\"site\"` → `client_id = 0` - `\"administrator\"` → `client_id = 1` Packages always use `client_id = 0` (site) in `#__extensions`, regardless of their children. The update feed must match. ## Affected All Joomla repos using the built-in update server. Currently blocking update delivery for MokoSuiteBackup.
jmiller reopened this issue 2026-06-18 17:57:51 +00:00
Sign in to join this conversation.