Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2ba5d7123 | |||
| f52df1912d | |||
| 4e797a5f74 | |||
| 6aee7353b9 | |||
| 82c3e96759 | |||
| 6f84af130d |
@@ -9,7 +9,7 @@
|
||||
<display-name>Package - MokoSuite</display-name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>White-label identity, security hardening, and tenant restriction layer for Suite-managed Joomla environments</description>
|
||||
<version>02.34.50</version>
|
||||
<version>02.34.53</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 02.34.50
|
||||
# VERSION: 02.34.53
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoPlatform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
|
||||
name: "RC Revert"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
name: Rename rc/ back to dev/
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == false &&
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
SUFFIX="${BRANCH#rc/}"
|
||||
DEV_BRANCH="dev/${SUFFIX}"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Create dev/ branch from rc/ branch
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||
"${API}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "201" ]; then
|
||||
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delete rc/ branch
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||
fi
|
||||
|
||||
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
PATH: ./CHANGELOG.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
BRIEF: Version history using `Keep a Changelog`
|
||||
-->
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||
-->
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
DEFGROUP: mokoconsulting-tech.MokoSuiteBrand
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoSuiteBrand
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for MokoSuiteBrand
|
||||
-->
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
PATH: ./LICENSE.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
BRIEF: Project license (GPL-3.0-or-later)
|
||||
-->
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /README.md
|
||||
BRIEF: MokoSuite platform plugin for Joomla
|
||||
-->
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
||||
INGROUP: [PROJECT_NAME].Documentation
|
||||
REPO: [REPOSITORY_URL]
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
INGROUP: MokoSuite.Build
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
FILE: build-guide.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/
|
||||
BRIEF: Build and packaging guide for the MokoSuite system plugin
|
||||
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
|
||||
-->
|
||||
|
||||
# MokoSuite Build Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Build Guide (VERSION: 02.34.53)
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/configuration-guide.md
|
||||
BRIEF: Configuration guide for the MokoSuite system plugin
|
||||
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
|
||||
-->
|
||||
|
||||
# MokoSuite Configuration Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Configuration Guide (VERSION: 02.34.53)
|
||||
|
||||
## 1. Objective
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/installation-guide.md
|
||||
BRIEF: Installation guide for the MokoSuite system plugin
|
||||
NOTE: First document in the guide set
|
||||
-->
|
||||
|
||||
# MokoSuite Installation Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Installation Guide (VERSION: 02.34.53)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/operations-guide.md
|
||||
BRIEF: Operational guide for administering and managing the MokoSuite system plugin
|
||||
NOTE: Defines lifecycle, responsibilities, and operational behaviors
|
||||
-->
|
||||
|
||||
# MokoSuite Operations Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Operations Guide (VERSION: 02.34.53)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/rollback-and-recovery-guide.md
|
||||
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
|
||||
NOTE: Completes the core guide set for Suite plugin governance
|
||||
-->
|
||||
|
||||
# MokoSuite Rollback and Recovery Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Rollback and Recovery Guide (VERSION: 02.34.53)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/testing-guide.md
|
||||
BRIEF: Testing guide for MokoSuite v02.01.08
|
||||
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
|
||||
-->
|
||||
|
||||
# MokoSuite Testing Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Testing Guide (VERSION: 02.34.53)
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/troubleshooting-guide.md
|
||||
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuite plugin
|
||||
NOTE: Designed for administrators and Suite operations teams
|
||||
-->
|
||||
|
||||
# MokoSuite Troubleshooting Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Troubleshooting Guide (VERSION: 02.34.53)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/guides/upgrade-and-versioning-guide.md
|
||||
BRIEF: Guide for updating, versioning, and maintaining the MokoSuite plugin
|
||||
NOTE: Defines release flow, version rules, and upgrade validation
|
||||
-->
|
||||
|
||||
# MokoSuite Upgrade and Versioning Guide (VERSION: 02.34.50)
|
||||
# MokoSuite Upgrade and Versioning Guide (VERSION: 02.34.53)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
+2
-2
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
PATH: /docs/index.md
|
||||
BRIEF: Master index of all documentation for the MokoSuite plugin
|
||||
NOTE: Automatically maintained index for all guide canvases
|
||||
-->
|
||||
|
||||
# MokoSuite Documentation Index (VERSION: 02.34.50)
|
||||
# MokoSuite Documentation Index (VERSION: 02.34.53)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
INGROUP: MokoSuite
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
PATH: /docs/plugin-basic.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
BRIEF: Baseline documentation for the MokoSuite system plugin
|
||||
NOTE: Foundational reference for internal and external stakeholders
|
||||
-->
|
||||
|
||||
# MokoSuite Plugin Overview (VERSION: 02.34.50)
|
||||
# MokoSuite Plugin Overview (VERSION: 02.34.53)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ DEFGROUP: MokoSuite.Documentation
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoSuite
|
||||
PATH: /docs/update-server.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.34.53
|
||||
BRIEF: How this extension's Joomla update server file (update.xml) is managed
|
||||
-->
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>MokoSuite admin dashboard and REST API. Provides a control panel for managing MokoSuite feature plugins, site health monitoring, and remote management endpoints.</description>
|
||||
|
||||
<namespace path="src">Moko\Component\MokoSuite</namespace>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>MOD_MOKOSUITE_CACHE_DESC</description>
|
||||
<namespace path="src">Moko\Module\MokoSuiteCache</namespace>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>MOD_MOKOSUITE_CATEGORIES_DESC</description>
|
||||
<namespace path="src">Moko\Module\MokoSuiteCategories</namespace>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>MOD_MOKOSUITE_CPANEL_DESC</description>
|
||||
<namespace path="src">Moko\Module\MokoSuiteCpanel</namespace>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>MokoSuite admin sidebar menu — renders a dedicated MokoSuite section in the admin menu before Joomla's default menu.</description>
|
||||
<namespace path="src">Moko\Module\MokoSuiteMenu</namespace>
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
/**
|
||||
* MokoSuite Admin Sidebar Menu
|
||||
*
|
||||
* Renders MokoSuite static views first, then auto-discovers installed
|
||||
* Moko components from #__menu and renders their submenu items as
|
||||
* nested MetisMenu collapsible sections.
|
||||
* Each installed Moko component gets its own top-level collapsible section.
|
||||
* com_mokosuitehq is always pinned first. com_mokosuite uses static views
|
||||
* as children. All other components auto-discover their submenu items.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
@@ -17,8 +17,8 @@ $app = Factory::getApplication();
|
||||
$currentOption = $app->getInput()->get('option', '');
|
||||
$currentView = $app->getInput()->get('view', '');
|
||||
|
||||
// ── Static MokoSuite views ────────────────────────────────────────────
|
||||
$mokosuiteItems = [
|
||||
// ── Static views for com_mokosuite ──────────────────────────────────
|
||||
$mokosuiteStaticViews = [
|
||||
['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokosuite'],
|
||||
['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokosuite&view=tickets'],
|
||||
['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokosuite&view=extensions'],
|
||||
@@ -30,27 +30,25 @@ $mokosuiteItems = [
|
||||
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuite'],
|
||||
];
|
||||
|
||||
// ── Auto-discover Moko component menus from #__menu ──────────────────
|
||||
// ── Auto-discover all Moko components from #__menu ──────────────────
|
||||
$mokoComponents = [];
|
||||
|
||||
try
|
||||
{
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
|
||||
// Find all Moko component menu items (exclude com_mokosuite — handled above)
|
||||
$db->setQuery(
|
||||
"SELECT m.id, m.title, m.link, m.level, m.parent_id, m.img, e.element"
|
||||
. " FROM " . $db->quoteName('#__menu') . " m"
|
||||
. " LEFT JOIN " . $db->quoteName('#__extensions') . " e ON m.component_id = e.extension_id"
|
||||
. " WHERE m.client_id = 1 AND m.level >= 1 AND m.published = 1"
|
||||
. " AND e.element LIKE 'com_moko%'"
|
||||
. " AND e.element != 'com_mokosuite'"
|
||||
. " AND e.enabled = 1"
|
||||
. " ORDER BY e.element, m.level, m.lft"
|
||||
);
|
||||
$menuItems = $db->loadObjectList() ?: [];
|
||||
|
||||
// Load sys.ini language files for discovered components
|
||||
// Load language files for discovered components
|
||||
$lang = Factory::getLanguage();
|
||||
$loadedLangs = [];
|
||||
foreach ($menuItems as $m)
|
||||
@@ -92,100 +90,112 @@ catch (\Throwable $e)
|
||||
// Silent — menu works without auto-discovered components
|
||||
}
|
||||
|
||||
// ── Determine active state ───────────────────────────────────────────
|
||||
$mokosuiteActive = ($currentOption === 'com_mokosuite');
|
||||
$anyMokoActive = $mokosuiteActive;
|
||||
|
||||
foreach ($mokoComponents as $comp)
|
||||
// Override com_mokosuite children with static views
|
||||
if (isset($mokoComponents['com_mokosuite']))
|
||||
{
|
||||
$parsed = [];
|
||||
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $parsed);
|
||||
if (($parsed['option'] ?? '') === $currentOption)
|
||||
$mokoComponents['com_mokosuite']['children'] = $mokosuiteStaticViews;
|
||||
$mokoComponents['com_mokosuite']['icon'] = 'icon-shield-alt';
|
||||
}
|
||||
else
|
||||
{
|
||||
// com_mokosuite not in admin menu — add it manually
|
||||
$mokoComponents['com_mokosuite'] = [
|
||||
'id' => 0,
|
||||
'title' => 'MokoSuite',
|
||||
'link' => 'index.php?option=com_mokosuite',
|
||||
'icon' => 'icon-shield-alt',
|
||||
'element' => 'com_mokosuite',
|
||||
'children' => $mokosuiteStaticViews,
|
||||
];
|
||||
}
|
||||
|
||||
// ── Sort: com_mokosuitehq first, then alphabetical by title ─────────
|
||||
$hq = null;
|
||||
$rest = [];
|
||||
|
||||
foreach ($mokoComponents as $key => $comp)
|
||||
{
|
||||
if ($key === 'com_mokosuitehq')
|
||||
{
|
||||
$anyMokoActive = true;
|
||||
$hq = $comp;
|
||||
}
|
||||
else
|
||||
{
|
||||
$rest[$key] = $comp;
|
||||
}
|
||||
}
|
||||
|
||||
$topClass = 'item parent item-level-1' . ($anyMokoActive ? ' mm-active' : '');
|
||||
$topCollapse = 'collapse-level-1 mm-collapse' . ($anyMokoActive ? ' mm-show' : '');
|
||||
usort($rest, fn($a, $b) => strcasecmp($a['title'], $b['title']));
|
||||
|
||||
$sorted = [];
|
||||
if ($hq !== null)
|
||||
{
|
||||
$sorted[] = $hq;
|
||||
}
|
||||
foreach ($rest as $comp)
|
||||
{
|
||||
$sorted[] = $comp;
|
||||
}
|
||||
?>
|
||||
|
||||
<style>
|
||||
.sidebar-wrapper .item-level-1 > a { padding-inline-start: 1.5rem; }
|
||||
.sidebar-wrapper .mokosuite-menu-item > a { padding-inline-start: 2rem; }
|
||||
.sidebar-wrapper .mokosuite-menu-child > a { padding-inline-start: 2.5rem; }
|
||||
.sidebar-wrapper .mokosuite-ext-item > a { padding-inline-start: 1.5rem; }
|
||||
.sidebar-wrapper .mokosuite-ext-child > a { padding-inline-start: 2.5rem; }
|
||||
</style>
|
||||
|
||||
<ul class="nav flex-column main-nav">
|
||||
<li class="<?php echo $topClass; ?>">
|
||||
<a class="has-arrow" href="#" aria-label="MokoSuite">
|
||||
<span class="icon-shield-alt" aria-hidden="true"></span>
|
||||
<span class="sidebar-item-title">MokoSuite</span>
|
||||
<?php foreach ($sorted as $comp): ?>
|
||||
<?php
|
||||
$compParsed = [];
|
||||
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
|
||||
$compOption = $compParsed['option'] ?? '';
|
||||
$compActive = ($compOption === $currentOption);
|
||||
|
||||
// For com_mokosuite static children, also check the plugins filter link
|
||||
if (!$compActive && $comp['element'] === 'com_mokosuite' && $currentOption === 'com_plugins')
|
||||
{
|
||||
$compActive = true;
|
||||
}
|
||||
|
||||
$hasChildren = !empty($comp['children']);
|
||||
$liClass = 'item mokosuite-ext-item' . ($hasChildren ? ' parent item-level-1' : '') . ($compActive ? ' mm-active' : '');
|
||||
$aClass = ($hasChildren ? 'has-arrow' : 'no-dropdown') . ($compActive ? ' mm-active' : '');
|
||||
$childCollapse = 'collapse-level-1 mm-collapse' . ($compActive ? ' mm-show' : '');
|
||||
?>
|
||||
<li class="<?php echo $liClass; ?>">
|
||||
<a class="<?php echo $aClass; ?>" href="<?php echo $hasChildren ? '#' : Route::_($comp['link']); ?>"<?php echo ($compActive && !$hasChildren) ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
|
||||
</a>
|
||||
<ul class="<?php echo $topCollapse; ?>" style="padding-inline-start:0.5rem;">
|
||||
|
||||
<?php // ── MokoSuite static items ── ?>
|
||||
<?php foreach ($mokosuiteItems as $item): ?>
|
||||
<?php if ($hasChildren): ?>
|
||||
<ul class="<?php echo $childCollapse; ?>" style="padding-inline-start:0.5rem;">
|
||||
<?php foreach ($comp['children'] as $child): ?>
|
||||
<?php
|
||||
$active = false;
|
||||
$parsed = [];
|
||||
parse_str(parse_url($item['link'], PHP_URL_QUERY) ?? '', $parsed);
|
||||
if (($parsed['option'] ?? '') === $currentOption)
|
||||
$childParsed = [];
|
||||
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
|
||||
$childOption = $childParsed['option'] ?? '';
|
||||
$childView = $childParsed['view'] ?? '';
|
||||
|
||||
$childActive = false;
|
||||
if ($childOption === $currentOption)
|
||||
{
|
||||
$active = empty($parsed['view'])
|
||||
$childActive = empty($childView)
|
||||
? ($currentView === '' || $currentView === 'dashboard')
|
||||
: ($currentView === ($parsed['view'] ?? ''));
|
||||
: ($currentView === $childView);
|
||||
}
|
||||
$liClass = 'item mokosuite-menu-item' . ($active ? ' mm-active' : '');
|
||||
$aClass = 'no-dropdown' . ($active ? ' mm-active' : '');
|
||||
|
||||
$childLiClass = 'item mokosuite-ext-child' . ($childActive ? ' mm-active' : '');
|
||||
$childAClass = 'no-dropdown' . ($childActive ? ' mm-active' : '');
|
||||
?>
|
||||
<li class="<?php echo $liClass; ?>">
|
||||
<a class="<?php echo $aClass; ?>" href="<?php echo Route::_($item['link']); ?>"<?php echo $active ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $item['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $item['title']; ?></span>
|
||||
<li class="<?php echo $childLiClass; ?>">
|
||||
<a class="<?php echo $childAClass; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $child['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $child['title']; ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php // ── Auto-discovered Moko components with submenus ── ?>
|
||||
<?php foreach ($mokoComponents as $comp): ?>
|
||||
<?php
|
||||
$compParsed = [];
|
||||
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
|
||||
$compActive = ($compParsed['option'] ?? '') === $currentOption;
|
||||
$hasChildren = !empty($comp['children']);
|
||||
$compLiClass = 'item mokosuite-menu-item' . ($hasChildren ? ' parent' : '') . ($compActive ? ' mm-active' : '');
|
||||
$compAClass = ($hasChildren ? 'has-arrow' : 'no-dropdown') . ($compActive ? ' mm-active' : '');
|
||||
$childCollapse = 'collapse-level-2 mm-collapse' . ($compActive ? ' mm-show' : '');
|
||||
?>
|
||||
<li class="<?php echo $compLiClass; ?>">
|
||||
<a class="<?php echo $compAClass; ?>" href="<?php echo $hasChildren ? '#' : Route::_($comp['link']); ?>"<?php echo ($compActive && !$hasChildren) ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
|
||||
</a>
|
||||
<?php if ($hasChildren): ?>
|
||||
<ul class="<?php echo $childCollapse; ?>" style="padding-inline-start:0.75rem;">
|
||||
<?php foreach ($comp['children'] as $child): ?>
|
||||
<?php
|
||||
$childParsed = [];
|
||||
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
|
||||
$childActive = ($childParsed['option'] ?? '') === $currentOption
|
||||
&& ($childParsed['view'] ?? '') === $currentView;
|
||||
$childLiClass = 'item mokosuite-menu-child' . ($childActive ? ' mm-active' : '');
|
||||
$childAClass = 'no-dropdown' . ($childActive ? ' mm-active' : '');
|
||||
?>
|
||||
<li class="<?php echo $childLiClass; ?>">
|
||||
<a class="<?php echo $childAClass; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $child['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $child['title']; ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoSuite
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
* VERSION: 02.34.50
|
||||
* VERSION: 02.34.53
|
||||
* PATH: /src/Extension/MokoSuite.php
|
||||
* NOTE: Core system plugin for MokoSuite admin tools suite
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoSuite
|
||||
* VERSION: 02.34.50
|
||||
* VERSION: 02.34.53
|
||||
* PATH: /src/Field/CopyableTokenField.php
|
||||
* BRIEF: Read-only token field with a copy-to-clipboard button
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>MokoSuite core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
|
||||
<namespace path=".">Moko\Plugin\System\MokoSuite</namespace>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoSuite
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
* VERSION: 02.34.50
|
||||
* VERSION: 02.34.53
|
||||
* PATH: /src/script.php
|
||||
* BRIEF: Installation script for MokoSuite plugin
|
||||
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoSuite
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
* VERSION: 02.34.50
|
||||
* VERSION: 02.34.53
|
||||
* PATH: /src/services/provider.php
|
||||
* BRIEF: Service provider for dependency injection in Joomla 5.x
|
||||
* NOTE: Registers the plugin with Joomla's DI container
|
||||
|
||||
Binary file not shown.
+29
@@ -0,0 +1,29 @@
|
||||
; MokoSuite DB-IP Plugin
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
; IP Geolocation by DB-IP — https://db-ip.com
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP="System - MokoSuite DB-IP"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_DESC="IP geolocation for MokoSuite using DB-IP Lite databases. Ships with country-level data; city-level data is downloaded from CDN or loaded from a local file."
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_FIELDSET_BASIC="DB-IP Settings"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_FIELDSET_BASIC_DESC="Configure IP geolocation database source and level."
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_LABEL="Database Source"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_DESC="CDN downloads the city database automatically from the configured URL. Local uses a MMDB file you provide on the server."
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_CDN="CDN (auto-download)"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_LOCAL="Local file"
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_LEVEL_LABEL="Database Level"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_LEVEL_DESC="Country is bundled (~8 MB). City provides region, city, and coordinates but requires a separate download (~125 MB)."
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_COUNTRY="Country (bundled)"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_CITY="City (remote download)"
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_AUTO_UPDATE_LABEL="Auto-Update Database"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_AUTO_UPDATE_DESC="Automatically download the latest city database monthly when an admin visits the backend."
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_CDN_URL_LABEL="CDN Download URL"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_CDN_URL_DESC="URL to download the city-level MMDB file. Default points to the MokoConsulting geoip-data repository."
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_LOCAL_PATH_LABEL="Local MMDB Path"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_LOCAL_PATH_DESC="Absolute path to a DB-IP MMDB file on the server (e.g. /home/user/dbip-city-lite.mmdb)."
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
; MokoSuite DB-IP Plugin (system strings)
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP="System - MokoSuite DB-IP"
|
||||
PLG_SYSTEM_MOKOSUITE_DBIP_DESC="IP geolocation for MokoSuite using DB-IP Lite databases."
|
||||
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db;
|
||||
|
||||
use MaxMind\Db\Reader\Decoder;
|
||||
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||
use MaxMind\Db\Reader\Metadata;
|
||||
use MaxMind\Db\Reader\Util;
|
||||
|
||||
/**
|
||||
* Instances of this class provide a reader for the MaxMind DB format. IP
|
||||
* addresses can be looked up using the get method.
|
||||
*/
|
||||
class Reader
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private static $DATA_SECTION_SEPARATOR_SIZE = 16;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
|
||||
|
||||
/**
|
||||
* @var int<0, max>
|
||||
*/
|
||||
private static $METADATA_START_MARKER_LENGTH = 14;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KiB
|
||||
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $fileHandle;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $fileSize;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $ipV4Start;
|
||||
|
||||
/**
|
||||
* @var Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* Constructs a Reader for the MaxMind DB format. The file passed to it must
|
||||
* be a valid MaxMind DB file such as a DBIP database file.
|
||||
*
|
||||
* @param string $database the MaxMind DB file to use
|
||||
*
|
||||
* @throws \InvalidArgumentException for invalid database path or unknown arguments
|
||||
* @throws InvalidDatabaseException
|
||||
* if the database is invalid or there is an error reading
|
||||
* from it
|
||||
*/
|
||||
public function __construct(string $database)
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
if (is_dir($database)) {
|
||||
// This matches the error that the C extension throws.
|
||||
throw new InvalidDatabaseException(
|
||||
"Error opening database file ($database). Is this a valid MaxMind DB file?"
|
||||
);
|
||||
}
|
||||
|
||||
$fileHandle = @fopen($database, 'rb');
|
||||
if ($fileHandle === false) {
|
||||
throw new \InvalidArgumentException(
|
||||
"The file \"$database\" does not exist or is not readable."
|
||||
);
|
||||
}
|
||||
$this->fileHandle = $fileHandle;
|
||||
|
||||
$fstat = fstat($fileHandle);
|
||||
if ($fstat === false) {
|
||||
throw new \UnexpectedValueException(
|
||||
"Error determining the size of \"$database\"."
|
||||
);
|
||||
}
|
||||
$this->fileSize = $fstat['size'];
|
||||
|
||||
$start = $this->findMetadataStart($database);
|
||||
$metadataDecoder = new Decoder($this->fileHandle, $start);
|
||||
[$metadataArray] = $metadataDecoder->decode($start);
|
||||
$this->metadata = new Metadata($metadataArray);
|
||||
$this->decoder = new Decoder(
|
||||
$this->fileHandle,
|
||||
$this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
|
||||
);
|
||||
$this->ipV4Start = $this->ipV4StartNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the record for the IP address.
|
||||
*
|
||||
* @param string $ipAddress the IP address to look up
|
||||
*
|
||||
* @throws \BadMethodCallException if this method is called on a closed database
|
||||
* @throws \InvalidArgumentException if something other than a single IP address is passed to the method
|
||||
* @throws InvalidDatabaseException
|
||||
* if the database is invalid or there is an error reading
|
||||
* from it
|
||||
*
|
||||
* @return mixed the record for the IP address
|
||||
*/
|
||||
public function get(string $ipAddress)
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
[$record] = $this->getWithPrefixLen($ipAddress);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the record for the IP address and its associated network prefix length.
|
||||
*
|
||||
* @param string $ipAddress the IP address to look up
|
||||
*
|
||||
* @throws \BadMethodCallException if this method is called on a closed database
|
||||
* @throws \InvalidArgumentException if something other than a single IP address is passed to the method
|
||||
* @throws InvalidDatabaseException
|
||||
* if the database is invalid or there is an error reading
|
||||
* from it
|
||||
*
|
||||
* @return array{0:mixed, 1:int} an array where the first element is the record and the
|
||||
* second the network prefix length for the record
|
||||
*/
|
||||
public function getWithPrefixLen(string $ipAddress): array
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_resource($this->fileHandle)) {
|
||||
throw new \BadMethodCallException(
|
||||
'Attempt to read from a closed MaxMind DB.'
|
||||
);
|
||||
}
|
||||
|
||||
[$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
|
||||
if ($pointer === 0) {
|
||||
return [null, $prefixLen];
|
||||
}
|
||||
|
||||
return [$this->resolveDataPointer($pointer), $prefixLen];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:int, 1:int}
|
||||
*/
|
||||
private function findAddressInTree(string $ipAddress): array
|
||||
{
|
||||
$packedAddr = @inet_pton($ipAddress);
|
||||
if ($packedAddr === false) {
|
||||
throw new \InvalidArgumentException(
|
||||
"The value \"$ipAddress\" is not a valid IP address."
|
||||
);
|
||||
}
|
||||
|
||||
$rawAddress = unpack('C*', $packedAddr);
|
||||
if ($rawAddress === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned char of the packed in_addr representation.'
|
||||
);
|
||||
}
|
||||
|
||||
$bitCount = \count($rawAddress) * 8;
|
||||
|
||||
// The first node of the tree is always node 0, at the beginning of the
|
||||
// value
|
||||
$node = 0;
|
||||
|
||||
$metadata = $this->metadata;
|
||||
|
||||
// Check if we are looking up an IPv4 address in an IPv6 tree. If this
|
||||
// is the case, we can skip over the first 96 nodes.
|
||||
if ($metadata->ipVersion === 6) {
|
||||
if ($bitCount === 32) {
|
||||
$node = $this->ipV4Start;
|
||||
}
|
||||
} elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Error looking up $ipAddress. You attempted to look up an"
|
||||
. ' IPv6 address in an IPv4-only database.'
|
||||
);
|
||||
}
|
||||
|
||||
$nodeCount = $metadata->nodeCount;
|
||||
|
||||
for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
|
||||
$tempBit = 0xFF & $rawAddress[($i >> 3) + 1];
|
||||
$bit = 1 & ($tempBit >> 7 - ($i % 8));
|
||||
|
||||
$node = $this->readNode($node, $bit);
|
||||
}
|
||||
if ($node === $nodeCount) {
|
||||
// Record is empty
|
||||
return [0, $i];
|
||||
}
|
||||
if ($node > $nodeCount) {
|
||||
// Record is a data pointer
|
||||
return [$node, $i];
|
||||
}
|
||||
|
||||
throw new InvalidDatabaseException(
|
||||
'Invalid or corrupt database. Maximum search depth reached without finding a leaf node'
|
||||
);
|
||||
}
|
||||
|
||||
private function ipV4StartNode(): int
|
||||
{
|
||||
// If we have an IPv4 database, the start node is the first node
|
||||
if ($this->metadata->ipVersion === 4) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$node = 0;
|
||||
|
||||
for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
|
||||
$node = $this->readNode($node, 0);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function readNode(int $nodeNumber, int $index): int
|
||||
{
|
||||
$baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
|
||||
|
||||
switch ($this->metadata->recordSize) {
|
||||
case 24:
|
||||
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
|
||||
$rc = unpack('N', "\x00" . $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned long of the node.'
|
||||
);
|
||||
}
|
||||
[, $node] = $rc;
|
||||
|
||||
return $node;
|
||||
|
||||
case 28:
|
||||
$bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
|
||||
if ($index === 0) {
|
||||
$middle = (0xF0 & \ord($bytes[3])) >> 4;
|
||||
} else {
|
||||
$middle = 0x0F & \ord($bytes[0]);
|
||||
}
|
||||
$rc = unpack('N', \chr($middle) . substr($bytes, $index, 3));
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned long of the node.'
|
||||
);
|
||||
}
|
||||
[, $node] = $rc;
|
||||
|
||||
return $node;
|
||||
|
||||
case 32:
|
||||
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
|
||||
$rc = unpack('N', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack the unsigned long of the node.'
|
||||
);
|
||||
}
|
||||
[, $node] = $rc;
|
||||
|
||||
return $node;
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
'Unknown record size: '
|
||||
. $this->metadata->recordSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
private function resolveDataPointer(int $pointer)
|
||||
{
|
||||
$resolved = $pointer - $this->metadata->nodeCount
|
||||
+ $this->metadata->searchTreeSize;
|
||||
if ($resolved >= $this->fileSize) {
|
||||
throw new InvalidDatabaseException(
|
||||
"The MaxMind DB file's search tree is corrupt"
|
||||
);
|
||||
}
|
||||
|
||||
[$data] = $this->decoder->decode($resolved);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is an extremely naive but reasonably readable implementation. There
|
||||
* are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
|
||||
* an issue, but I suspect it won't be.
|
||||
*/
|
||||
private function findMetadataStart(string $filename): int
|
||||
{
|
||||
$handle = $this->fileHandle;
|
||||
$fileSize = $this->fileSize;
|
||||
$marker = self::$METADATA_START_MARKER;
|
||||
$markerLength = self::$METADATA_START_MARKER_LENGTH;
|
||||
|
||||
$minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize);
|
||||
|
||||
for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
|
||||
if (fseek($handle, $offset) !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$value = fread($handle, $markerLength);
|
||||
if ($value === $marker) {
|
||||
return $offset + $markerLength;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidDatabaseException(
|
||||
"Error opening database file ($filename). "
|
||||
. 'Is this a valid MaxMind DB file?'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException if arguments are passed to the method
|
||||
* @throws \BadMethodCallException if the database has been closed
|
||||
*
|
||||
* @return Metadata object for the database
|
||||
*/
|
||||
public function metadata(): Metadata
|
||||
{
|
||||
if (\func_num_args()) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
// Not technically required, but this makes it consistent with
|
||||
// C extension and it allows us to change our implementation later.
|
||||
if (!\is_resource($this->fileHandle)) {
|
||||
throw new \BadMethodCallException(
|
||||
'Attempt to read from a closed MaxMind DB.'
|
||||
);
|
||||
}
|
||||
|
||||
return clone $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the MaxMind DB and returns resources to the system.
|
||||
*
|
||||
* @throws \Exception
|
||||
* if an I/O error occurs
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if (\func_num_args()) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
if (!\is_resource($this->fileHandle)) {
|
||||
throw new \BadMethodCallException(
|
||||
'Attempt to close a closed MaxMind DB.'
|
||||
);
|
||||
}
|
||||
fclose($this->fileHandle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
// @codingStandardsIgnoreLine
|
||||
|
||||
class Decoder
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $fileStream;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $pointerBase;
|
||||
|
||||
/**
|
||||
* This is only used for unit testing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $pointerTestHack;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $switchByteOrder;
|
||||
|
||||
private const _EXTENDED = 0;
|
||||
private const _POINTER = 1;
|
||||
private const _UTF8_STRING = 2;
|
||||
private const _DOUBLE = 3;
|
||||
private const _BYTES = 4;
|
||||
private const _UINT16 = 5;
|
||||
private const _UINT32 = 6;
|
||||
private const _MAP = 7;
|
||||
private const _INT32 = 8;
|
||||
private const _UINT64 = 9;
|
||||
private const _UINT128 = 10;
|
||||
private const _ARRAY = 11;
|
||||
// 12 is the container type
|
||||
// 13 is the end marker type
|
||||
private const _BOOLEAN = 14;
|
||||
private const _FLOAT = 15;
|
||||
|
||||
/**
|
||||
* @param resource $fileStream
|
||||
*/
|
||||
public function __construct(
|
||||
$fileStream,
|
||||
int $pointerBase = 0,
|
||||
bool $pointerTestHack = false
|
||||
) {
|
||||
$this->fileStream = $fileStream;
|
||||
$this->pointerBase = $pointerBase;
|
||||
|
||||
$this->pointerTestHack = $pointerTestHack;
|
||||
|
||||
$this->switchByteOrder = $this->isPlatformLittleEndian();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function decode(int $offset): array
|
||||
{
|
||||
$ctrlByte = \ord(Util::read($this->fileStream, $offset, 1));
|
||||
++$offset;
|
||||
|
||||
$type = $ctrlByte >> 5;
|
||||
|
||||
// Pointers are a special case, we don't read the next $size bytes, we
|
||||
// use the size to determine the length of the pointer and then follow
|
||||
// it.
|
||||
if ($type === self::_POINTER) {
|
||||
[$pointer, $offset] = $this->decodePointer($ctrlByte, $offset);
|
||||
|
||||
// for unit testing
|
||||
if ($this->pointerTestHack) {
|
||||
return [$pointer];
|
||||
}
|
||||
|
||||
[$result] = $this->decode($pointer);
|
||||
|
||||
return [$result, $offset];
|
||||
}
|
||||
|
||||
if ($type === self::_EXTENDED) {
|
||||
$nextByte = \ord(Util::read($this->fileStream, $offset, 1));
|
||||
|
||||
$type = $nextByte + 7;
|
||||
|
||||
if ($type < 8) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Something went horribly wrong in the decoder. An extended type '
|
||||
. 'resolved to a type number < 8 ('
|
||||
. $type
|
||||
. ')'
|
||||
);
|
||||
}
|
||||
|
||||
++$offset;
|
||||
}
|
||||
|
||||
[$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset);
|
||||
|
||||
return $this->decodeByType($type, $offset, $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int<0, max> $size
|
||||
*
|
||||
* @return array{0:mixed, 1:int}
|
||||
*/
|
||||
private function decodeByType(int $type, int $offset, int $size): array
|
||||
{
|
||||
switch ($type) {
|
||||
case self::_MAP:
|
||||
return $this->decodeMap($size, $offset);
|
||||
|
||||
case self::_ARRAY:
|
||||
return $this->decodeArray($size, $offset);
|
||||
|
||||
case self::_BOOLEAN:
|
||||
return [$this->decodeBoolean($size), $offset];
|
||||
}
|
||||
|
||||
$newOffset = $offset + $size;
|
||||
$bytes = Util::read($this->fileStream, $offset, $size);
|
||||
|
||||
switch ($type) {
|
||||
case self::_BYTES:
|
||||
case self::_UTF8_STRING:
|
||||
return [$bytes, $newOffset];
|
||||
|
||||
case self::_DOUBLE:
|
||||
$this->verifySize(8, $size);
|
||||
|
||||
return [$this->decodeDouble($bytes), $newOffset];
|
||||
|
||||
case self::_FLOAT:
|
||||
$this->verifySize(4, $size);
|
||||
|
||||
return [$this->decodeFloat($bytes), $newOffset];
|
||||
|
||||
case self::_INT32:
|
||||
return [$this->decodeInt32($bytes, $size), $newOffset];
|
||||
|
||||
case self::_UINT16:
|
||||
case self::_UINT32:
|
||||
case self::_UINT64:
|
||||
case self::_UINT128:
|
||||
return [$this->decodeUint($bytes, $size), $newOffset];
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
'Unknown or unexpected type: ' . $type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function verifySize(int $expected, int $actual): void
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
throw new InvalidDatabaseException(
|
||||
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:array<mixed>, 1:int}
|
||||
*/
|
||||
private function decodeArray(int $size, int $offset): array
|
||||
{
|
||||
$array = [];
|
||||
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
[$value, $offset] = $this->decode($offset);
|
||||
$array[] = $value;
|
||||
}
|
||||
|
||||
return [$array, $offset];
|
||||
}
|
||||
|
||||
private function decodeBoolean(int $size): bool
|
||||
{
|
||||
return $size !== 0;
|
||||
}
|
||||
|
||||
private function decodeDouble(string $bytes): float
|
||||
{
|
||||
// This assumes IEEE 754 doubles, but most (all?) modern platforms
|
||||
// use them.
|
||||
$rc = unpack('E', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack a double value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $double] = $rc;
|
||||
|
||||
return $double;
|
||||
}
|
||||
|
||||
private function decodeFloat(string $bytes): float
|
||||
{
|
||||
// This assumes IEEE 754 floats, but most (all?) modern platforms
|
||||
// use them.
|
||||
$rc = unpack('G', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack a float value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $float] = $rc;
|
||||
|
||||
return $float;
|
||||
}
|
||||
|
||||
private function decodeInt32(string $bytes, int $size): int
|
||||
{
|
||||
switch ($size) {
|
||||
case 0:
|
||||
return 0;
|
||||
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
$bytes = str_pad($bytes, 4, "\x00", \STR_PAD_LEFT);
|
||||
|
||||
break;
|
||||
|
||||
case 4:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)"
|
||||
);
|
||||
}
|
||||
|
||||
$rc = unpack('l', $this->maybeSwitchByteOrder($bytes));
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack a 32bit integer value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $int] = $rc;
|
||||
|
||||
return $int;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:array<string, mixed>, 1:int}
|
||||
*/
|
||||
private function decodeMap(int $size, int $offset): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
[$key, $offset] = $this->decode($offset);
|
||||
[$value, $offset] = $this->decode($offset);
|
||||
$map[$key] = $value;
|
||||
}
|
||||
|
||||
return [$map, $offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:int, 1:int}
|
||||
*/
|
||||
private function decodePointer(int $ctrlByte, int $offset): array
|
||||
{
|
||||
$pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
|
||||
|
||||
$buffer = Util::read($this->fileStream, $offset, $pointerSize);
|
||||
$offset += $pointerSize;
|
||||
|
||||
switch ($pointerSize) {
|
||||
case 1:
|
||||
$packed = \chr($ctrlByte & 0x7) . $buffer;
|
||||
$rc = unpack('n', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned short value from the given bytes (pointerSize is 1).'
|
||||
);
|
||||
}
|
||||
[, $pointer] = $rc;
|
||||
$pointer += $this->pointerBase;
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer;
|
||||
$rc = unpack('N', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned long value from the given bytes (pointerSize is 2).'
|
||||
);
|
||||
}
|
||||
[, $pointer] = $rc;
|
||||
$pointer += $this->pointerBase + 2048;
|
||||
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$packed = \chr($ctrlByte & 0x7) . $buffer;
|
||||
|
||||
// It is safe to use 'N' here, even on 32 bit machines as the
|
||||
// first bit is 0.
|
||||
$rc = unpack('N', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned long value from the given bytes (pointerSize is 3).'
|
||||
);
|
||||
}
|
||||
[, $pointer] = $rc;
|
||||
$pointer += $this->pointerBase + 526336;
|
||||
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// We cannot use unpack here as we might overflow on 32 bit
|
||||
// machines
|
||||
$pointerOffset = $this->decodeUint($buffer, $pointerSize);
|
||||
|
||||
$pointerBase = $this->pointerBase;
|
||||
|
||||
if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) {
|
||||
$pointer = $pointerOffset + $pointerBase;
|
||||
} else {
|
||||
throw new \RuntimeException(
|
||||
'The database offset is too large to be represented on your platform.'
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidDatabaseException(
|
||||
'Unexpected pointer size ' . $pointerSize
|
||||
);
|
||||
}
|
||||
|
||||
return [$pointer, $offset];
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function decodeUint(string $bytes, int $byteLength)
|
||||
{
|
||||
if ($byteLength === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// PHP integers are signed. PHP_INT_SIZE - 1 is the number of
|
||||
// complete bytes that can be converted to an integer. However,
|
||||
// we can convert another byte if the leading bit is zero.
|
||||
$useRealInts = $byteLength <= \PHP_INT_SIZE - 1
|
||||
|| ($byteLength === \PHP_INT_SIZE && (\ord($bytes[0]) & 0x80) === 0);
|
||||
|
||||
if ($useRealInts) {
|
||||
$integer = 0;
|
||||
for ($i = 0; $i < $byteLength; ++$i) {
|
||||
$part = \ord($bytes[$i]);
|
||||
$integer = ($integer << 8) + $part;
|
||||
}
|
||||
|
||||
return $integer;
|
||||
}
|
||||
|
||||
// We only use gmp or bcmath if the final value is too big
|
||||
$integerAsString = '0';
|
||||
for ($i = 0; $i < $byteLength; ++$i) {
|
||||
$part = \ord($bytes[$i]);
|
||||
|
||||
if (\extension_loaded('gmp')) {
|
||||
$integerAsString = gmp_strval(gmp_add(gmp_mul($integerAsString, '256'), $part));
|
||||
} elseif (\extension_loaded('bcmath')) {
|
||||
$integerAsString = bcadd(bcmul($integerAsString, '256'), (string) $part);
|
||||
} else {
|
||||
throw new \RuntimeException(
|
||||
'The gmp or bcmath extension must be installed to read this database.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $integerAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:int, 1:int}
|
||||
*/
|
||||
private function sizeFromCtrlByte(int $ctrlByte, int $offset): array
|
||||
{
|
||||
$size = $ctrlByte & 0x1F;
|
||||
|
||||
if ($size < 29) {
|
||||
return [$size, $offset];
|
||||
}
|
||||
|
||||
$bytesToRead = $size - 28;
|
||||
$bytes = Util::read($this->fileStream, $offset, $bytesToRead);
|
||||
|
||||
if ($size === 29) {
|
||||
$size = 29 + \ord($bytes);
|
||||
} elseif ($size === 30) {
|
||||
$rc = unpack('n', $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned short value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $adjust] = $rc;
|
||||
$size = 285 + $adjust;
|
||||
} else {
|
||||
$rc = unpack('N', "\x00" . $bytes);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned long value from the given bytes.'
|
||||
);
|
||||
}
|
||||
[, $adjust] = $rc;
|
||||
$size = $adjust + 65821;
|
||||
}
|
||||
|
||||
return [$size, $offset + $bytesToRead];
|
||||
}
|
||||
|
||||
private function maybeSwitchByteOrder(string $bytes): string
|
||||
{
|
||||
return $this->switchByteOrder ? strrev($bytes) : $bytes;
|
||||
}
|
||||
|
||||
private function isPlatformLittleEndian(): bool
|
||||
{
|
||||
$testint = 0x00FF;
|
||||
$packed = pack('S', $testint);
|
||||
$rc = unpack('v', $packed);
|
||||
if ($rc === false) {
|
||||
throw new InvalidDatabaseException(
|
||||
'Could not unpack an unsigned short value from the given bytes.'
|
||||
);
|
||||
}
|
||||
|
||||
return $testint === current($rc);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
/**
|
||||
* This class should be thrown when unexpected data is found in the database.
|
||||
*/
|
||||
// phpcs:disable
|
||||
class InvalidDatabaseException extends \Exception {}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
/**
|
||||
* This class provides the metadata for the MaxMind DB file.
|
||||
*/
|
||||
class Metadata
|
||||
{
|
||||
/**
|
||||
* This is an unsigned 16-bit integer indicating the major version number
|
||||
* for the database's binary format.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $binaryFormatMajorVersion;
|
||||
|
||||
/**
|
||||
* This is an unsigned 16-bit integer indicating the minor version number
|
||||
* for the database's binary format.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $binaryFormatMinorVersion;
|
||||
|
||||
/**
|
||||
* This is an unsigned 64-bit integer that contains the database build
|
||||
* timestamp as a Unix epoch value.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $buildEpoch;
|
||||
|
||||
/**
|
||||
* This is a string that indicates the structure of each data record
|
||||
* associated with an IP address. The actual definition of these
|
||||
* structures is left up to the database creator.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $databaseType;
|
||||
|
||||
/**
|
||||
* This key will always point to a map (associative array). The keys of
|
||||
* that map will be language codes, and the values will be a description
|
||||
* in that language as a UTF-8 string. May be undefined for some
|
||||
* databases.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* This is an unsigned 16-bit integer which is always 4 or 6. It indicates
|
||||
* whether the database contains IPv4 or IPv6 address data.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $ipVersion;
|
||||
|
||||
/**
|
||||
* An array of strings, each of which is a language code. A given record
|
||||
* may contain data items that have been localized to some or all of
|
||||
* these languages. This may be undefined.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
public $languages;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $nodeByteSize;
|
||||
|
||||
/**
|
||||
* This is an unsigned 32-bit integer indicating the number of nodes in
|
||||
* the search tree.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $nodeCount;
|
||||
|
||||
/**
|
||||
* This is an unsigned 16-bit integer. It indicates the number of bits in a
|
||||
* record in the search tree. Note that each node consists of two records.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $recordSize;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $searchTreeSize;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $metadata
|
||||
*/
|
||||
public function __construct(array $metadata)
|
||||
{
|
||||
if (\func_num_args() !== 1) {
|
||||
throw new \ArgumentCountError(
|
||||
\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
|
||||
);
|
||||
}
|
||||
|
||||
$this->binaryFormatMajorVersion
|
||||
= $metadata['binary_format_major_version'];
|
||||
$this->binaryFormatMinorVersion
|
||||
= $metadata['binary_format_minor_version'];
|
||||
$this->buildEpoch = $metadata['build_epoch'];
|
||||
$this->databaseType = $metadata['database_type'];
|
||||
$this->languages = $metadata['languages'];
|
||||
$this->description = $metadata['description'];
|
||||
$this->ipVersion = $metadata['ip_version'];
|
||||
$this->nodeCount = $metadata['node_count'];
|
||||
$this->recordSize = $metadata['record_size'];
|
||||
$this->nodeByteSize = $this->recordSize / 4;
|
||||
$this->searchTreeSize = $this->nodeCount * $this->nodeByteSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MaxMind\Db\Reader;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* @param resource $stream
|
||||
* @param int<0, max> $numberOfBytes
|
||||
*/
|
||||
public static function read($stream, int $offset, int $numberOfBytes): string
|
||||
{
|
||||
if ($numberOfBytes === 0) {
|
||||
return '';
|
||||
}
|
||||
if (fseek($stream, $offset) === 0) {
|
||||
$value = fread($stream, $numberOfBytes);
|
||||
|
||||
// We check that the number of bytes read is equal to the number
|
||||
// asked for. We use ftell as getting the length of $value is
|
||||
// much slower.
|
||||
if ($value !== false && ftell($stream) - $offset === $numberOfBytes) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidDatabaseException(
|
||||
'The MaxMind DB file contains bad data'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuite DB-IP</name>
|
||||
<element>mokosuite_dbip</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-06-07</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITE_DBIP_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteDBIP</namespace>
|
||||
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>language</folder>
|
||||
<folder>lib</folder>
|
||||
<folder>data</folder>
|
||||
</files>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_system_mokosuite_dbip.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_system_mokosuite_dbip.sys.ini</language>
|
||||
</languages>
|
||||
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic"
|
||||
label="PLG_SYSTEM_MOKOSUITE_DBIP_FIELDSET_BASIC"
|
||||
description="PLG_SYSTEM_MOKOSUITE_DBIP_FIELDSET_BASIC_DESC">
|
||||
|
||||
<field name="database_source" type="list" default="cdn"
|
||||
label="PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_LABEL"
|
||||
description="PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_DESC">
|
||||
<option value="cdn">PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_CDN</option>
|
||||
<option value="local">PLG_SYSTEM_MOKOSUITE_DBIP_SOURCE_LOCAL</option>
|
||||
</field>
|
||||
|
||||
<field name="database_level" type="list" default="country"
|
||||
label="PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_LEVEL_LABEL"
|
||||
description="PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_LEVEL_DESC">
|
||||
<option value="country">PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_COUNTRY</option>
|
||||
<option value="city">PLG_SYSTEM_MOKOSUITE_DBIP_DATABASE_CITY</option>
|
||||
</field>
|
||||
|
||||
<field name="auto_update" type="radio" default="1"
|
||||
label="PLG_SYSTEM_MOKOSUITE_DBIP_AUTO_UPDATE_LABEL"
|
||||
description="PLG_SYSTEM_MOKOSUITE_DBIP_AUTO_UPDATE_DESC"
|
||||
class="btn-group btn-group-yesno"
|
||||
showon="database_source:cdn">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field name="cdn_url" type="url"
|
||||
default="https://git.mokoconsulting.tech/MokoConsulting/geoip-data/releases/download/latest/dbip-city-lite.mmdb"
|
||||
label="PLG_SYSTEM_MOKOSUITE_DBIP_CDN_URL_LABEL"
|
||||
description="PLG_SYSTEM_MOKOSUITE_DBIP_CDN_URL_DESC"
|
||||
filter="url"
|
||||
showon="database_source:cdn" />
|
||||
|
||||
<field name="local_path" type="text"
|
||||
default=""
|
||||
label="PLG_SYSTEM_MOKOSUITE_DBIP_LOCAL_PATH_LABEL"
|
||||
description="PLG_SYSTEM_MOKOSUITE_DBIP_LOCAL_PATH_DESC"
|
||||
filter="path"
|
||||
showon="database_source:local" />
|
||||
|
||||
<field name="last_updated" type="hidden" default="" filter="raw" />
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Moko.Plugin.System.MokoSuiteDBIP
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Moko\Plugin\System\MokoSuiteDBIP\Extension\DBIP;
|
||||
|
||||
return new class implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$dispatcher = $container->get(DispatcherInterface::class);
|
||||
$plugin = new DBIP($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuite_dbip'));
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Moko.Plugin.System.MokoSuiteDBIP
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GPL-3.0-or-later
|
||||
*
|
||||
* IP Geolocation by DB-IP — https://db-ip.com
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteDBIP\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
use Moko\Plugin\System\MokoSuiteDBIP\Helper\DBIPHelper;
|
||||
|
||||
class DBIP extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
protected $autoloadLanguage = true;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'onAfterInitialise' => 'onAfterInitialise',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DB-IP: set local path if configured, auto-download city DB if needed.
|
||||
*/
|
||||
public function onAfterInitialise(): void
|
||||
{
|
||||
$source = $this->params->get('database_source', 'cdn');
|
||||
$level = $this->params->get('database_level', 'country');
|
||||
|
||||
// If using a local MMDB file, configure the helper
|
||||
if ($source === 'local')
|
||||
{
|
||||
$localPath = $this->params->get('local_path', '');
|
||||
|
||||
if ($localPath !== '')
|
||||
{
|
||||
DBIPHelper::setLocalPath($localPath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// CDN mode: auto-download city DB if selected and needed
|
||||
if ($level !== 'city' || !$this->params->get('auto_update', 1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$cityPath = DBIPHelper::getCityDbPath();
|
||||
|
||||
if (file_exists($cityPath))
|
||||
{
|
||||
$age = time() - filemtime($cityPath);
|
||||
|
||||
if ($age < 86400 * 30)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only download during admin page loads
|
||||
$app = $this->getApplication();
|
||||
|
||||
if (!$app->isClient('administrator'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$url = $this->params->get(
|
||||
'cdn_url',
|
||||
'https://git.mokoconsulting.tech/MokoConsulting/geoip-data/releases/download/latest/dbip-city-lite.mmdb'
|
||||
);
|
||||
|
||||
DBIPHelper::downloadCityDb($url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Moko.Plugin.System.MokoSuiteDBIP
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GPL-3.0-or-later
|
||||
*
|
||||
* IP Geolocation by DB-IP — https://db-ip.com
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteDBIP\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use MaxMind\Db\Reader;
|
||||
|
||||
class DBIPHelper
|
||||
{
|
||||
private static ?Reader $countryReader = null;
|
||||
|
||||
private static ?Reader $cityReader = null;
|
||||
|
||||
private static bool $libLoaded = false;
|
||||
|
||||
private static string $customLocalPath = '';
|
||||
|
||||
/**
|
||||
* Set a custom local path for the city database.
|
||||
*/
|
||||
public static function setLocalPath(string $path): void
|
||||
{
|
||||
self::$customLocalPath = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the bundled country database.
|
||||
*/
|
||||
public static function getCountryDbPath(): string
|
||||
{
|
||||
return JPATH_PLUGINS . '/system/mokosuite_dbip/data/dbip-country-lite.mmdb';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the city database.
|
||||
* Uses custom local path if set, otherwise the CDN cache location.
|
||||
*/
|
||||
public static function getCityDbPath(): string
|
||||
{
|
||||
if (self::$customLocalPath !== '' && file_exists(self::$customLocalPath))
|
||||
{
|
||||
return self::$customLocalPath;
|
||||
}
|
||||
|
||||
return JPATH_ADMINISTRATOR . '/cache/mokosuite_dbip/dbip-city-lite.mmdb';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the MaxMind DB Reader library.
|
||||
*/
|
||||
private static function loadLib(): void
|
||||
{
|
||||
if (self::$libLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$libPath = JPATH_PLUGINS . '/system/mokosuite_dbip/lib';
|
||||
|
||||
require_once $libPath . '/MaxMind/Db/Reader.php';
|
||||
require_once $libPath . '/MaxMind/Db/Reader/Decoder.php';
|
||||
require_once $libPath . '/MaxMind/Db/Reader/InvalidDatabaseException.php';
|
||||
require_once $libPath . '/MaxMind/Db/Reader/Metadata.php';
|
||||
require_once $libPath . '/MaxMind/Db/Reader/Util.php';
|
||||
|
||||
self::$libLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up an IP address and return geolocation data.
|
||||
*
|
||||
* @param string $ip The IP address to look up.
|
||||
*
|
||||
* @return array|null Geolocation data or null if not found.
|
||||
*
|
||||
* Result keys (country DB): country_code, country_name, continent_code, continent_name
|
||||
* Result keys (city DB): + region, city, latitude, longitude, timezone
|
||||
*/
|
||||
public static function lookup(string $ip): ?array
|
||||
{
|
||||
try
|
||||
{
|
||||
self::loadLib();
|
||||
|
||||
// Try city database first
|
||||
$cityPath = self::getCityDbPath();
|
||||
|
||||
if (file_exists($cityPath))
|
||||
{
|
||||
if (self::$cityReader === null)
|
||||
{
|
||||
self::$cityReader = new Reader($cityPath);
|
||||
}
|
||||
|
||||
$record = self::$cityReader->get($ip);
|
||||
|
||||
if ($record !== null)
|
||||
{
|
||||
return self::normalizeCityRecord($record);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to bundled country database
|
||||
$countryPath = self::getCountryDbPath();
|
||||
|
||||
if (file_exists($countryPath))
|
||||
{
|
||||
if (self::$countryReader === null)
|
||||
{
|
||||
self::$countryReader = new Reader($countryPath);
|
||||
}
|
||||
|
||||
$record = self::$countryReader->get($ip);
|
||||
|
||||
if ($record !== null)
|
||||
{
|
||||
return self::normalizeCountryRecord($record);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Silent — don't break the site if DB-IP fails
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up country only (uses bundled DB, always available).
|
||||
*/
|
||||
public static function lookupCountry(string $ip): ?string
|
||||
{
|
||||
$result = self::lookup($ip);
|
||||
|
||||
return $result['country_code'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the city database is installed.
|
||||
*/
|
||||
public static function hasCityDb(): bool
|
||||
{
|
||||
return file_exists(self::getCityDbPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the city database from the configured URL.
|
||||
*
|
||||
* @param string $url The download URL for the city MMDB file.
|
||||
*
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public static function downloadCityDb(string $url): bool
|
||||
{
|
||||
$destPath = JPATH_ADMINISTRATOR . '/cache/mokosuite_dbip/dbip-city-lite.mmdb';
|
||||
$destDir = \dirname($destPath);
|
||||
|
||||
if (!is_dir($destDir))
|
||||
{
|
||||
mkdir($destDir, 0755, true);
|
||||
}
|
||||
|
||||
$tmpFile = $destPath . '.tmp';
|
||||
|
||||
try
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
$fp = fopen($tmpFile, 'wb');
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
\CURLOPT_FILE => $fp,
|
||||
\CURLOPT_FOLLOWLOCATION => true,
|
||||
\CURLOPT_TIMEOUT => 300,
|
||||
\CURLOPT_CONNECTTIMEOUT => 30,
|
||||
\CURLOPT_USERAGENT => 'MokoSuite-DBIP/1.0',
|
||||
]);
|
||||
|
||||
$success = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, \CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
|
||||
if ($success && $code === 200 && filesize($tmpFile) > 1024)
|
||||
{
|
||||
if (self::$cityReader !== null)
|
||||
{
|
||||
self::$cityReader->close();
|
||||
self::$cityReader = null;
|
||||
}
|
||||
|
||||
rename($tmpFile, $destPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a DB-IP city record into a flat array.
|
||||
*/
|
||||
private static function normalizeCityRecord(array $record): array
|
||||
{
|
||||
return [
|
||||
'country_code' => $record['country']['iso_code'] ?? '',
|
||||
'country_name' => $record['country']['names']['en'] ?? '',
|
||||
'continent_code' => $record['continent']['code'] ?? '',
|
||||
'continent_name' => $record['continent']['names']['en'] ?? '',
|
||||
'region' => $record['subdivisions'][0]['names']['en'] ?? '',
|
||||
'city' => $record['city']['names']['en'] ?? '',
|
||||
'latitude' => $record['location']['latitude'] ?? null,
|
||||
'longitude' => $record['location']['longitude'] ?? null,
|
||||
'timezone' => $record['location']['time_zone'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a DB-IP country record into a flat array.
|
||||
*/
|
||||
private static function normalizeCountryRecord(array $record): array
|
||||
{
|
||||
return [
|
||||
'country_code' => $record['country']['iso_code'] ?? '',
|
||||
'country_name' => $record['country']['names']['en'] ?? '',
|
||||
'continent_code' => $record['continent']['code'] ?? '',
|
||||
'continent_name' => $record['continent']['names']['en'] ?? '',
|
||||
'region' => '',
|
||||
'city' => '',
|
||||
'latitude' => null,
|
||||
'longitude' => null,
|
||||
'timezone' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down readers.
|
||||
*/
|
||||
public static function close(): void
|
||||
{
|
||||
if (self::$countryReader !== null)
|
||||
{
|
||||
self::$countryReader->close();
|
||||
self::$countryReader = null;
|
||||
}
|
||||
|
||||
if (self::$cityReader !== null)
|
||||
{
|
||||
self::$cityReader->close();
|
||||
self::$cityReader = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITE_DEVTOOLS_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteDevTools</namespace>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITE_FIREWALL_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteFirewall</namespace>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITE_LICENSE_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteLicense</namespace>
|
||||
<files><folder>src</folder><folder>services</folder><folder>language</folder></files>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITE_MONITOR_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteMonitor</namespace>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITE_OFFLINE_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteOffline</namespace>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_SYSTEM_MOKOSUITE_TENANT_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoSuiteTenant</namespace>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteTickets</namespace>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_TASK_MOKOSUITEDEMO_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteDemo</namespace>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: MokoSuite
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
|
||||
* PATH: /src/packages/plg_system_mokosuite/Service/DemoResetService.php
|
||||
* VERSION: 02.34.50
|
||||
* VERSION: 02.34.53
|
||||
* BRIEF: Content-only snapshot/restore for demo site reset
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>PLG_TASK_MOKOSUITESYNC_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteSync</namespace>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: MokoSuite
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
|
||||
* PATH: /src/packages/plg_system_mokosuite/Service/ContentSyncReceiver.php
|
||||
* VERSION: 02.34.50
|
||||
* VERSION: 02.34.53
|
||||
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: MokoSuite
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
|
||||
* PATH: /src/packages/plg_system_mokosuite/Service/ContentSyncService.php
|
||||
* VERSION: 02.34.50
|
||||
* VERSION: 02.34.53
|
||||
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
|
||||
*/
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<description>Joomla Web Services API routes for MokoSuite site management — health checks, cache, updates, backups, and site info.</description>
|
||||
<namespace path="src">Moko\Plugin\WebServices\MokoSuite</namespace>
|
||||
<files>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuite</name>
|
||||
<packagename>mokosuite</packagename>
|
||||
<version>02.34.50-dev</version>
|
||||
<version>02.34.53-dev</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -20,6 +20,7 @@
|
||||
<file type="plugin" id="plg_system_mokosuite_tenant" group="system">plg_system_mokosuite_tenant.zip</file>
|
||||
<file type="plugin" id="plg_system_mokosuite_devtools" group="system">plg_system_mokosuite_devtools.zip</file>
|
||||
<file type="plugin" id="plg_system_mokosuite_offline" group="system">plg_system_mokosuite_offline.zip</file>
|
||||
<file type="plugin" id="plg_system_mokosuite_dbip" group="system">plg_system_mokosuite_dbip.zip</file>
|
||||
<file type="component" id="com_mokosuite">com_mokosuite.zip</file>
|
||||
<file type="module" id="mod_mokosuite_cpanel" client="administrator">mod_mokosuite_cpanel.zip</file>
|
||||
<file type="module" id="mod_mokosuite_menu" client="administrator">mod_mokosuite_menu.zip</file>
|
||||
|
||||
Reference in New Issue
Block a user