43 Commits

Author SHA1 Message Date
jmiller 5b9a1708f7 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-28 20:07:39 +00:00
jmiller ef1aec3782 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-28 20:07:37 +00:00
jmiller cb81b7f58c chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-28 20:07:35 +00:00
jmiller cd862bc64b chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-28 20:07:33 +00:00
gitea-actions[bot] bd5ac4b236 chore(release): build 01.01.00 [skip ci] 2026-06-28 20:06:19 +00:00
jmiller c7182f6cbb Merge pull request 'v1.2.0 — Categories, API, hardening, polish' (#62) from dev into main 2026-06-28 20:06:07 +00:00
gitea-actions[bot] 84d42f70a1 chore(version): pre-release bump to 01.00.39-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 17s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 10m3s
2026-06-28 20:04:56 +00:00
gitea-actions[bot] 9ed2fb1963 chore(version): auto-bump patch 01.00.38-dev [skip ci] 2026-06-28 20:04:38 +00:00
jmiller 9b09d61473 chore: merge remote dev, resolve sync conflicts
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 18s
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 46s
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 15:04:16 -05:00
jmiller 42501f0597 chore: resolve merge conflicts with main (workflow docs)
Authored-by: Moko Consulting
2026-06-28 15:03:15 -05:00
gitea-actions[bot] 7f4451628d chore(version): pre-release bump to 01.00.37-dev [skip ci] 2026-06-28 19:55:09 +00:00
gitea-actions[bot] c8f7422996 chore(version): auto-bump patch 01.00.36-dev [skip ci] 2026-06-28 19:54:54 +00:00
jmiller 8dfc1227cb feat: add pretty display names for all extensions in Joomla admin
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 13s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 14:54:35 -05:00
gitea-actions[bot] e8d67215b1 chore(version): pre-release bump to 01.00.35-dev [skip ci] 2026-06-28 18:55:48 +00:00
gitea-actions[bot] f7bbddd98d chore(version): pre-release bump to 01.00.34-dev [skip ci] 2026-06-28 18:55:32 +00:00
gitea-actions[bot] 1ece8a006f chore(version): auto-bump patch 01.00.33-dev [skip ci] 2026-06-28 18:55:15 +00:00
jmiller 5ea2fd2b98 fix: make SQL migration 01.00.02 a no-op to prevent install abort
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 43s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Joomla aborts the entire package install on any SQL error in update
files. DROP COLUMN fails when catid doesn't exist (fresh installs,
or systems where it was already removed). Since install.mysql.sql
already omits catid, no runtime migration is needed.

Authored-by: Moko Consulting
2026-06-28 13:54:45 -05:00
gitea-actions[bot] b6900aec6e chore(version): pre-release bump to 01.00.32-dev [skip ci] 2026-06-28 18:49:53 +00:00
gitea-actions[bot] ecfb7c426d chore(version): auto-bump patch 01.00.31-dev [skip ci] 2026-06-28 18:49:42 +00:00
jmiller 03c9ca53a6 docs: update changelog with license key, XSS fix, SQL compat entries
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Universal: Build & Release / Promote to RC (pull_request) Failing after 10s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 39s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 13:48:50 -05:00
gitea-actions[bot] 8c2bf7b02c chore(version): pre-release bump to 01.00.30-dev [skip ci] 2026-06-28 18:47:43 +00:00
gitea-actions[bot] 056b339dee chore(version): auto-bump patch 01.00.29-dev [skip ci] 2026-06-28 18:47:35 +00:00
jmiller 58f3ac96d9 feat: add license key warning and download key preservation
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
Save/restore the download key (dlid) across package upgrades so users
don't lose their license key. Show a warning with direct edit link
when no license key is configured.

Mirrors the pattern from MokoSuiteCross.

Authored-by: Moko Consulting
2026-06-28 13:46:31 -05:00
gitea-actions[bot] 086c50e150 chore(version): pre-release bump to 01.00.28-dev [skip ci] 2026-06-28 18:08:49 +00:00
gitea-actions[bot] 3487072b8a chore(version): pre-release bump to 01.00.27-dev [skip ci] 2026-06-28 18:08:38 +00:00
gitea-actions[bot] edc6bbf62c chore(version): auto-bump patch 01.00.26-dev [skip ci] 2026-06-28 18:08:29 +00:00
jmiller 8b42c016a8 fix: remove IF EXISTS syntax from SQL migration for MySQL 5.7 compat
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
Joomla's SQL update runner doesn't support DELIMITER or stored
procedures. DROP COLUMN IF EXISTS is MySQL 8.0.13+ only. Plain
DROP COLUMN is safe here because update files only run on upgrades
from versions that had the catid column.

Authored-by: Moko Consulting
2026-06-28 13:08:14 -05:00
gitea-actions[bot] 41875e7878 chore(version): pre-release bump to 01.00.25-dev [skip ci] 2026-06-28 17:53:42 +00:00
gitea-actions[bot] 797101474a chore(version): pre-release bump to 01.00.24-dev [skip ci] 2026-06-28 16:24:59 +00:00
gitea-actions[bot] dccdb88617 chore(version): auto-bump patch 01.00.23-dev [skip ci] 2026-06-28 16:24:51 +00:00
jmiller 2897a1ceba fix: escape location title in detail map popup to prevent XSS
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
Use DOM-based textContent instead of raw string in Leaflet bindPopup()
to prevent HTML injection via location titles.

Authored-by: Moko Consulting
2026-06-28 11:24:31 -05:00
gitea-actions[bot] 926b4c7576 chore(version): pre-release bump to 01.00.22-dev [skip ci] 2026-06-28 16:23:11 +00:00
gitea-actions[bot] 80cefe1624 chore(version): auto-bump patch 01.00.21-dev [skip ci] 2026-06-28 16:23:03 +00:00
jmiller 32b541597a fix: resolve all open issues — detail map, clustering, CSP, GROUP BY, cleanup (#34 #57 #58 #59 #60 #61)
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
- Add Leaflet map to location detail page with marker and popup (#57)
- Implement Leaflet.markercluster with toggleable module parameter (#61)
- Convert inline <script> to $wa->addInlineScript() for CSP nonce support (#34)
- Replace category INNER JOIN with EXISTS subquery for ONLY_FULL_GROUP_BY compat (#59)
- Add delete() override to LocationTable and CategoryTable for junction cleanup (#60)
- Drop dead catid column and idx_catid index via SQL update 01.00.02 (#58)
- Update CHANGELOG and README

Authored-by: Moko Consulting
2026-06-28 11:22:40 -05:00
jmiller a11ec9b73a chore: sync pr-metadata-check.yml from Template-Joomla 2026-06-28 07:47:40 +00:00
jmiller 2fe10deedf chore: sync SECURITY.md from Template-Joomla 2026-06-28 07:46:14 +00:00
jmiller cb37757087 chore: sync GOVERNANCE.md from Template-Joomla 2026-06-28 07:42:41 +00:00
jmiller 99f738b39c chore: sync CONTRIBUTING.md from Template-Joomla 2026-06-28 07:40:56 +00:00
jmiller d6bde53f96 chore: sync CODE_OF_CONDUCT.md from Template-Joomla 2026-06-28 07:37:50 +00:00
jmiller ad459ba54b chore: sync composer.json from Template-Joomla 2026-06-28 07:35:51 +00:00
jmiller 6f5f5913e9 chore: sync phpstan.neon from Template-Joomla 2026-06-28 07:34:32 +00:00
jmiller 8a231b00af chore: sync .editorconfig from Template-Joomla 2026-06-28 07:34:00 +00:00
jmiller 57e106fff7 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-27 20:44:46 +00:00
34 changed files with 472 additions and 47 deletions
+3 -1
View File
@@ -7,7 +7,7 @@
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# VERSION: 05.01.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +=======================================================================+
@@ -75,6 +75,7 @@ jobs:
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools
env:
@@ -173,6 +174,7 @@ jobs:
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes
run: |
+6
View File
@@ -13,6 +13,12 @@
name: "Generic: Project CI"
on:
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
workflow_dispatch:
permissions:
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.00.20
# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+4 -4
View File
@@ -20,7 +20,7 @@ permissions:
contents: read
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
@@ -55,14 +55,14 @@ jobs:
- name: Validate metadata against Joomla manifest
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \
--token "${GITEA_TOKEN}" \
--token "${MOKOGITEA_TOKEN}" \
--org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \
--api-base "${GITEA_URL}/api/v1" \
--api-base "${MOKOGITEA_URL}/api/v1" \
--ci
if [ $? -ne 0 ]; then
+16
View File
@@ -93,8 +93,20 @@ jobs:
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Check platform eligibility (Joomla only)
id: eligibility
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
fi
- name: Resolve metadata and bump version
id: meta
if: steps.eligibility.outputs.proceed == 'true'
run: |
# Auto-detect stability from branch name on push, or use input on dispatch
if [ "${{ github.event_name }}" = "push" ]; then
@@ -171,6 +183,7 @@ jobs:
- name: Create release
id: release
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
@@ -181,6 +194,7 @@ jobs:
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
@@ -217,6 +231,7 @@ jobs:
- name: Build package and upload
id: package
if: steps.eligibility.outputs.proceed == 'true'
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
@@ -230,6 +245,7 @@ jobs:
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
@@ -47,13 +47,6 @@ jobs:
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
- name: Clone mokocli
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+17
View File
@@ -22,16 +22,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `LocationBridgeHelper` — static helper for cross-extension integration (#48)
- `LocationSavedEvent` — fires `onStoreLocatorLocationSaved` for cache invalidation
- Plugin added to package manifest
- Leaflet map on location detail page with marker and popup (#57)
- Leaflet.markercluster for automatic marker grouping at low zoom levels (#61)
- Clustering toggle parameter in map module settings (enabled by default)
- Junction table orphan cleanup on location/category delete (#60)
- License key warning on install/update when no download key is configured
- Download key (dlid) preserved across package upgrades
### Changed
- Map module dispatcher uses aliased table queries with category JOIN
- ORDER BY clauses in admin and site models now validated against filter_fields allowlist
- Category filter uses EXISTS subquery instead of JOIN to avoid ONLY_FULL_GROUP_BY errors (#59)
- Inline `<script>` blocks replaced with `$wa->addInlineScript()` for CSP nonce support (#34)
### Removed
- Dead `catid` column from locations table — junction table is the source of truth (#58)
- `idx_catid` index dropped from locations table
### Security
- CSV import: MIME type validation, 2 MB file size limit, delimiter allowlist (#34)
- CSV import: formula injection prevention (strips leading `=+\-@\t\r` characters)
- ORDER BY injection prevention — replaced `$db->escape()` with allowlist validation
- Map module: `$mapHeight` CSS value validated with regex pattern
- CSP compatibility: all inline scripts use WebAssetManager for automatic nonce injection (#34)
- XSS fix: detail map popup uses DOM textContent instead of raw string in bindPopup()
### Fixed
- SQL migration compatibility: removed `DROP COLUMN IF EXISTS` (MySQL 8.0.13+ only) in favor of plain `DROP COLUMN`
## [1.1.0] - 2026-06-23
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP:
INGROUP: Project.Documentation
REPO:
VERSION: 01.00.20
VERSION: 01.01.00
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+119 -1
View File
File diff suppressed because one or more lines are too long
+7 -4
View File
@@ -1,6 +1,6 @@
# MokoSuiteStoreLocator
A Joomla 4/5 package providing a store locator listing component with coordinating map and search modules.
A Joomla 5/6 package providing a store locator listing component with coordinating map and search modules.
## Package Contents
@@ -34,7 +34,7 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
- **Schema.org** — LocalBusiness structured data markup on all frontend templates
- **SEF URLs** — router with menu, standard, and nomenu rules
- **Menu items** — "All Locations" list and single "Location Detail" picker
- **Interactive map** — Leaflet.js with OpenStreetMap tiles, markers with popups, auto-fit bounds
- **Interactive map** — Leaflet.js with OpenStreetMap tiles, markers with popups, auto-fit bounds, marker clustering
- **Location search** — city dropdown, radius filter, and browser geolocation ("Use My Location")
- **Proximity search** — Haversine distance filtering with distance-sorted results
- **Get Directions** — Google Maps directions link on detail page and map popups
@@ -45,13 +45,16 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
- **ACL permissions** — `access.xml` with standard Joomla permission actions
- **SQL update schema** — versioned migration files for safe upgrades
- **Shop integration** — `LocationBridgeHelper` for cross-extension data access, `LocationSavedEvent` for cache invalidation
- **Security hardening** — CSV injection prevention, MIME validation, ORDER BY allowlists, input sanitization
- **Detail page map** — Leaflet map on single location view with marker and popup
- **Marker clustering** — Leaflet.markercluster groups nearby markers at low zoom levels (toggleable)
- **Security hardening** — CSV injection prevention, MIME validation, ORDER BY allowlists, CSP-compatible inline scripts
- **Junction cleanup** — orphan rows automatically removed when locations or categories are deleted
### Planned
- Marker clustering for dense location areas (Leaflet.markercluster)
- Google Maps provider as alternative to Leaflet
- CSV export
- Photo gallery per location
- Address autocomplete on admin edit form
## Development
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 01.00.20
VERSION: 01.01.00
BRIEF: Security vulnerability reporting and handling policy
-->
+25 -7
View File
@@ -1,9 +1,27 @@
{
"name": "mokoconsulting/mokojoomstorelocator",
"description": "Joomla store locator listing package with component and modules",
"type": "joomla-package",
"license": "GPL-3.0-or-later",
"require": {
"php": ">=8.1"
}
"name": "mokoconsulting/mokojoomgallery",
"description": "Photo gallery management for Joomla — galleries, images, thumbnails, lightbox, and frontend display",
"type": "joomla-package",
"version": "01.00.00",
"license": "GPL-3.0-or-later",
"authors": [
{
"name": "Moko Consulting",
"email": "hello@mokoconsulting.tech",
"homepage": "https://mokoconsulting.tech"
}
],
"require": {
"php": ">=8.1"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7",
"phpstan/phpstan": "^1.10",
"joomla/coding-standards": "3.0.x-dev"
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"sort-packages": true
}
}
@@ -2,6 +2,7 @@
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
COM_MOKOSUITESTORELOCATOR="MokoSuite Store Locator"
COM_MOKOJOOMSTORELOCATOR="Store Locator"
COM_MOKOJOOMSTORELOCATOR_DESC="A store locator component for managing and displaying location listings."
COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations"
@@ -2,6 +2,7 @@
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
COM_MOKOSUITESTORELOCATOR="MokoSuite Store Locator"
COM_MOKOJOOMSTORELOCATOR="Store Locator"
COM_MOKOJOOMSTORELOCATOR_DESC="A store locator component for managing and displaying location listings."
COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations"
@@ -24,7 +24,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_locations` (
`image` varchar(255) NOT NULL DEFAULT '',
`published` tinyint(4) NOT NULL DEFAULT 0,
`ordering` int(11) NOT NULL DEFAULT 0,
`catid` int(11) NOT NULL DEFAULT 0,
`params` text NOT NULL,
`created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`created_by` int(10) unsigned NOT NULL DEFAULT 0,
@@ -34,7 +33,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_locations` (
`checked_out_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_catid` (`catid`),
KEY `idx_alias` (`alias`(191)),
KEY `idx_coordinates` (`latitude`, `longitude`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,5 @@
-- MokoSuiteStoreLocator 01.00.02
-- Legacy catid column removed from install.mysql.sql.
-- No runtime migration needed — Joomla aborts on DROP errors
-- and fresh installs never had the column.
SELECT 1;
@@ -15,6 +15,7 @@ use Joomla\CMS\Filter\OutputFilter;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\ParameterType;
/**
* Category table class.
@@ -78,4 +79,34 @@ class CategoryTable extends Table
return parent::check();
}
/**
* Override delete to clean up junction table rows.
*
* @param mixed $pk Primary key value to delete.
*
* @return boolean True on success.
*
* @since 1.2.0
*/
public function delete($pk = null): bool
{
$pk = $pk ?: $this->id;
if (!parent::delete($pk))
{
return false;
}
$db = $this->getDbo();
$query = $db->getQuery(true)
->delete($db->quoteName('#__mokosuitestorelocator_location_categories'))
->where($db->quoteName('category_id') . ' = :pk')
->bind(':pk', $pk, ParameterType::INTEGER);
$db->setQuery($query);
$db->execute();
return true;
}
}
@@ -15,6 +15,7 @@ use Joomla\CMS\Filter\OutputFilter;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\ParameterType;
/**
* Location table class.
@@ -95,4 +96,34 @@ class LocationTable extends Table
return parent::check();
}
/**
* Override delete to clean up junction table rows.
*
* @param mixed $pk Primary key value to delete.
*
* @return boolean True on success.
*
* @since 1.2.0
*/
public function delete($pk = null): bool
{
$pk = $pk ?: $this->id;
if (!parent::delete($pk))
{
return false;
}
$db = $this->getDbo();
$query = $db->getQuery(true)
->delete($db->quoteName('#__mokosuitestorelocator_location_categories'))
->where($db->quoteName('location_id') . ' = :pk')
->bind(':pk', $pk, ParameterType::INTEGER);
$db->setQuery($query);
$db->execute();
return true;
}
}
@@ -15,7 +15,7 @@
-->
<extension type="component" method="upgrade">
<name>com_mokosuitestorelocator</name>
<version>01.00.20</version>
<version>01.01.00</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -2,6 +2,7 @@
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
COM_MOKOSUITESTORELOCATOR="MokoSuite Store Locator"
COM_MOKOJOOMSTORELOCATOR="Store Locator"
COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations"
COM_MOKOJOOMSTORELOCATOR_NO_LOCATIONS="No locations found."
@@ -157,14 +157,18 @@ class LocationsModel extends ListModel
->bind(':state', $state);
}
// Category filter
// Category filter — use EXISTS to avoid GROUP BY / ONLY_FULL_GROUP_BY issues
$catid = (int) $this->getState('filter.catid');
if ($catid > 0)
{
$query->join('INNER', $db->quoteName('#__mokosuitestorelocator_location_categories', 'lc')
. ' ON ' . $db->quoteName('lc.location_id') . ' = ' . $db->quoteName('a.id'))
->where($db->quoteName('lc.category_id') . ' = :catid')
$subQuery = $db->getQuery(true)
->select('1')
->from($db->quoteName('#__mokosuitestorelocator_location_categories', 'lc'))
->where($db->quoteName('lc.location_id') . ' = ' . $db->quoteName('a.id'))
->where($db->quoteName('lc.category_id') . ' = :catid');
$query->where('EXISTS (' . $subQuery . ')')
->bind(':catid', $catid, ParameterType::INTEGER);
}
@@ -14,6 +14,14 @@ use Joomla\CMS\Language\Text;
/** @var \Moko\Component\MokoSuiteStoreLocator\Site\View\Location\HtmlView $this */
$item = $this->item;
if ($item->latitude && $item->longitude)
{
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', [], ['integrity' => 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=', 'crossorigin' => '']);
$wa->registerAndUseScript('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], ['integrity' => 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=', 'crossorigin' => '', 'defer' => true]);
}
?>
<div class="com-mokosuitestorelocator-location" itemscope itemtype="https://schema.org/LocalBusiness">
<h2 itemprop="name"><?php echo $this->escape($item->title); ?></h2>
@@ -111,10 +119,29 @@ $item = $this->item;
<meta itemprop="latitude" content="<?php echo $this->escape($item->latitude); ?>">
<meta itemprop="longitude" content="<?php echo $this->escape($item->longitude); ?>">
<div class="com-mokosuitestorelocator-location__map"
data-lat="<?php echo $this->escape($item->latitude); ?>"
data-lng="<?php echo $this->escape($item->longitude); ?>"
id="mokosuitestorelocator-detail-map"
data-lat="<?php echo (float) $item->latitude; ?>"
data-lng="<?php echo (float) $item->longitude; ?>"
data-title="<?php echo $this->escape($item->title); ?>"
style="height: 300px;">
</div>
<?php
$wa->addInlineScript(<<<JS
document.addEventListener('DOMContentLoaded', function() {
var el = document.getElementById('mokosuitestorelocator-detail-map');
if (!el || typeof L === 'undefined') return;
var lat = parseFloat(el.getAttribute('data-lat'));
var lng = parseFloat(el.getAttribute('data-lng'));
var map = L.map(el.id).setView([lat, lng], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}).addTo(map);
var span = document.createElement('span');
span.textContent = el.getAttribute('data-title') || '';
L.marker([lat, lng]).addTo(map).bindPopup(span).openPopup();
});
JS, [], ['position' => 'after'], ['leaflet']);
?>
<?php endif; ?>
</div>
@@ -2,6 +2,7 @@
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
MOD_MOKOSUITESTORELOCATOR_MAP="MokoSuite Store Locator - Map"
MOD_MOKOJOOMSTORELOCATOR_MAP="Store Locator Map"
MOD_MOKOJOOMSTORELOCATOR_MAP_DESC="Displays an interactive map with store location markers."
MOD_MOKOJOOMSTORELOCATOR_MAP_HEIGHT="Map Height"
@@ -9,4 +10,5 @@ MOD_MOKOJOOMSTORELOCATOR_MAP_ZOOM="Default Zoom Level"
MOD_MOKOJOOMSTORELOCATOR_MAP_PROVIDER="Map Provider"
MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY="API Key"
MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY_DESC="Required for Google Maps. Not needed for OpenStreetMap."
MOD_MOKOJOOMSTORELOCATOR_MAP_CLUSTERING="Enable Marker Clustering"
MOD_MOKOJOOMSTORELOCATOR_MAP_NOSCRIPT="JavaScript is required to display the map."
@@ -2,5 +2,6 @@
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
MOD_MOKOSUITESTORELOCATOR_MAP="MokoSuite Store Locator - Map"
MOD_MOKOJOOMSTORELOCATOR_MAP="Store Locator Map"
MOD_MOKOJOOMSTORELOCATOR_MAP_DESC="Displays an interactive map with store location markers."
@@ -14,7 +14,7 @@
-->
<extension type="module" client="site" method="upgrade">
<name>mod_mokosuitestorelocator_map</name>
<version>01.00.20</version>
<version>01.01.00</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -67,6 +67,17 @@
label="MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY"
description="MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY_DESC"
/>
<field
name="enable_clustering"
type="radio"
label="MOD_MOKOJOOMSTORELOCATOR_MAP_CLUSTERING"
default="1"
class="btn-group"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
</fields>
</config>
@@ -27,10 +27,19 @@ $apiKey = $params->get('api_key', '');
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $displayData['app']->getDocument()->getWebAssetManager();
$enableClustering = (bool) $params->get('enable_clustering', 1);
if ($provider === 'leaflet')
{
$wa->registerAndUseStyle('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', [], ['integrity' => 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=', 'crossorigin' => '']);
$wa->registerAndUseScript('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], ['integrity' => 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=', 'crossorigin' => '', 'defer' => true]);
if ($enableClustering)
{
$wa->registerAndUseStyle('leaflet.markercluster', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css', [], ['crossorigin' => '']);
$wa->registerAndUseStyle('leaflet.markercluster.default', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css', [], ['crossorigin' => '']);
$wa->registerAndUseScript('leaflet.markercluster', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js', [], ['crossorigin' => '', 'defer' => true], ['leaflet']);
}
}
?>
<div class="mod-mokosuitestorelocator-map"
@@ -45,9 +54,13 @@ if ($provider === 'leaflet')
</noscript>
</div>
<script>
<?php
$directionsText = Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS', true);
$clusteringJs = $enableClustering ? 'true' : 'false';
$wa->addInlineScript(<<<JS
document.addEventListener('DOMContentLoaded', function() {
var el = document.getElementById('mokosuitestorelocator-map-<?php echo (int) $moduleId; ?>');
var el = document.getElementById('mokosuitestorelocator-map-{$moduleId}');
if (!el || typeof L === 'undefined') return;
var locations = JSON.parse(el.getAttribute('data-locations') || '[]');
@@ -66,6 +79,8 @@ document.addEventListener('DOMContentLoaded', function() {
}
var bounds = L.latLngBounds();
var useClustering = {$clusteringJs} && typeof L.markerClusterGroup === 'function';
var markerLayer = useClustering ? L.markerClusterGroup() : L.layerGroup();
function esc(str) {
var d = document.createElement('div');
@@ -83,15 +98,18 @@ document.addEventListener('DOMContentLoaded', function() {
popupAnchor: [0, -32]
});
}
var marker = L.marker([loc.lat, loc.lng], markerOptions).addTo(map);
var marker = L.marker([loc.lat, loc.lng], markerOptions);
var popup = '<strong>' + esc(loc.title) + '</strong>';
if (loc.address) popup += '<br>' + esc(loc.address);
if (loc.phone) popup += '<br><a href="tel:' + esc(loc.phone) + '">' + esc(loc.phone) + '</a>';
popup += '<br><a href="https://www.google.com/maps/dir/?api=1&destination=' + loc.lat + ',' + loc.lng + '" target="_blank" rel="noopener"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS', true); ?></a>';
popup += '<br><a href="https://www.google.com/maps/dir/?api=1&destination=' + loc.lat + ',' + loc.lng + '" target="_blank" rel="noopener">{$directionsText}</a>';
marker.bindPopup(popup);
markerLayer.addLayer(marker);
bounds.extend([loc.lat, loc.lng]);
});
map.addLayer(markerLayer);
map.fitBounds(bounds, { padding: [30, 30], maxZoom: zoom });
});
</script>
JS, [], ['position' => 'after'], ['leaflet']);
?>
@@ -2,6 +2,7 @@
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
MOD_MOKOSUITESTORELOCATOR_SEARCH="MokoSuite Store Locator - Search"
MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search"
MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations."
MOD_MOKOJOOMSTORELOCATOR_SEARCH_LABEL="Find a Store"
@@ -2,5 +2,6 @@
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
MOD_MOKOSUITESTORELOCATOR_SEARCH="MokoSuite Store Locator - Search"
MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search"
MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations."
@@ -14,7 +14,7 @@
-->
<extension type="module" client="site" method="upgrade">
<name>mod_mokosuitestorelocator_search</name>
<version>01.00.20</version>
<version>01.01.00</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,2 +1,2 @@
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuiteStoreLocator - Web Services"
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuite Store Locator - Web Services"
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC="Provides REST API endpoints for the MokoSuiteStoreLocator component."
@@ -0,0 +1,6 @@
; MokoSuiteStoreLocator Web Services Plugin - System language strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GNU General Public License version 3 or later; see LICENSE
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuite Store Locator - Web Services"
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC="Provides REST API endpoints for the MokoSuiteStoreLocator component."
@@ -5,7 +5,7 @@
========================================================================= -->
<extension type="plugin" group="webservices" method="upgrade">
<name>plg_webservices_mokosuitestorelocator</name>
<version>01.00.20</version>
<version>01.01.00</version>
<creationDate>2026-06-24</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+2 -2
View File
@@ -16,9 +16,9 @@
=========================================================================
-->
<extension type="package" method="upgrade">
<name>pkg_mokosuitestorelocator</name>
<name>MokoSuite Store Locator</name>
<packagename>mokosuitestorelocator</packagename>
<version>01.00.20</version>
<version>01.01.00</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+113
View File
@@ -69,6 +69,8 @@ class Pkg_MokosuitestorelocatorInstallerScript implements InstallerScriptInterfa
return false;
}
$this->saveDownloadKey();
return true;
}
@@ -126,6 +128,117 @@ class Pkg_MokosuitestorelocatorInstallerScript implements InstallerScriptInterfa
*/
public function postflight(string $type, InstallerAdapter $parent): bool
{
$this->restoreDownloadKey();
$this->warnMissingLicenseKey();
return true;
}
private ?string $savedDownloadKey = null;
private function saveDownloadKey(): void
{
try
{
$db = \Joomla\CMS\Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('us.extra_query'))
->from($db->quoteName('#__update_sites', 'us'))
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitestorelocator'))
->setLimit(1)
);
$key = $db->loadResult();
if (!empty($key))
{
$this->savedDownloadKey = $key;
}
}
catch (\Throwable $e) {}
}
private function restoreDownloadKey(): void
{
if ($this->savedDownloadKey === null)
{
return;
}
try
{
$db = \Joomla\CMS\Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('us.update_site_id'))
->from($db->quoteName('#__update_sites', 'us'))
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitestorelocator'))
->setLimit(1)
);
$siteId = (int) $db->loadResult();
if ($siteId > 0)
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
->where($db->quoteName('update_site_id') . ' = ' . $siteId)
)->execute();
}
}
catch (\Throwable $e) {}
}
private function warnMissingLicenseKey(): void
{
try
{
$db = \Joomla\CMS\Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
->from($db->quoteName('#__update_sites'))
->where(
'(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteStoreLocator%')
. ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteStoreLocator%') . ')'
)
->setLimit(1)
);
$site = $db->loadObject();
if ($site)
{
$eq = (string) ($site->extra_query ?? '');
if (!empty($eq) && strpos($eq, 'dlid=') !== false)
{
parse_str($eq, $p);
if (!empty($p['dlid']))
{
return;
}
}
$editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id;
}
else
{
$editUrl = 'index.php?option=com_installer&view=updatesites';
}
\Joomla\CMS\Factory::getApplication()->enqueueMessage(
'<strong>Moko Consulting License Key Required</strong> — '
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
. '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>',
'warning'
);
}
catch (\Throwable $e) {}
}
}