diff --git a/.mokogitea/CLAUDE.md b/.mokogitea/CLAUDE.md index b674af3c..ad395c47 100644 --- a/.mokogitea/CLAUDE.md +++ b/.mokogitea/CLAUDE.md @@ -7,7 +7,7 @@ Cross-posting Joomla content to social media, email marketing, and chat platform | Field | Value | |---|---| | **Package** | `pkg_mokosuitecross` | -| **Language** | PHP 8.1+ | +| **Language** | PHP 8.3+ | | **Branch** | develop on `dev`, merge to `main` (protected) | | **Wiki** | [MokoSuiteCross Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/wiki) | @@ -28,7 +28,7 @@ Joomla **package** with core extensions + pluggable service plugins: ### com_mokosuitecross (Component) - Admin backend: dashboard, services, post queue, templates, logs -- Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each) +- Joomla 6 MVC: Dashboard, Services, Posts, Logs (list/edit each) - Namespace: `Joomla\Component\MokoSuiteCross\Administrator` ### plg_system_mokosuitecross (System Plugin) @@ -73,8 +73,8 @@ Each platform is a separate plugin implementing `MokoSuiteCrossServiceInterface` ## Coding Standards -- PHP 8.1+ minimum -- Joomla 5/6 DI container pattern: `services/provider.php` → Extension class +- PHP 8.3+ minimum +- Joomla 6 DI container pattern: `services/provider.php` → Extension class - Legacy stub `.php` file required for plugin loader but empty - `SubscriberInterface` for event subscription (not `on*` method naming) - `bind() → check() → store()` for Table operations (not `save()`) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index eb67f8f7..ecde30c0 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 01.13.21 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index a1fd6b5b..237b8943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Added +- **Admin menu short-name constant** — the Administrator → Components menu now shows a compact label via `COM_MOKOSUITECROSS_SHORT` ("Cross"); defined in the admin sys.ini (#255) + ## [01.13.00] --- 2026-06-29 ## [01.13.00] --- 2026-06-29 diff --git a/CLAUDE.md b/CLAUDE.md index 0621bee9..1bada9ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ This is a Joomla **package** extension (`pkg_mokosuitecross`) containing sub-ext ### com_mokosuitecross (Component) - Admin backend for managing services, post queue, templates, and logs -- Joomla 5/6 MVC: Dashboard, Services, Posts, Logs (list/edit each) +- Joomla 6 MVC: Dashboard, Services, Posts, Logs (list/edit each) - Namespace: `Joomla\Component\MokoSuiteCross\Administrator` - Database tables: `#__mokosuitecross_services`, `#__mokosuitecross_posts`, `#__mokosuitecross_templates`, `#__mokosuitecross_logs` @@ -102,8 +102,8 @@ Four tables: ## Coding Standards -- PHP 8.1+ minimum -- Joomla 5/6 DI container pattern: `services/provider.php` → Extension class +- PHP 8.3+ minimum +- Joomla 6 DI container pattern: `services/provider.php` → Extension class - Legacy stub `.php` file required for plugin loader but empty - `SubscriberInterface` for event subscription (not `on*` method naming) - `bind() → check() → store()` for Table operations (not `save()`) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6ff1224c..985f2199 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,7 +14,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://github.com/mokoconsulting-tech/Template-Joomla/ - VERSION: 01.13.00 + VERSION: 01.13.21 PATH: ./CODE_OF_CONDUCT.md BRIEF: Community expectations and enforcement guidelines NOTE: Adapted with attribution from the Contributor Covenant v2.1 diff --git a/GOVERNANCE.md b/GOVERNANCE.md index b0a65ac5..de7ba6cb 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -19,7 +19,7 @@ DEFGROUP: mokoconsulting-tech.Template-Joomla INGROUP: MokoStandards.Governance REPO: https://github.com/mokoconsulting-tech/Template-Joomla - VERSION: 01.13.00 + VERSION: 01.13.21 PATH: /GOVERNANCE.md BRIEF: Project governance rules, roles, and decision process for Template-Joomla --> diff --git a/README.md b/README.md index d12bda55..8fd0c874 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoSuiteCross - + Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 6. diff --git a/SECURITY.md b/SECURITY.md index 8a55a1e8..a60bd016 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 01.13.00 +VERSION: 01.13.21 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/composer.json b/composer.json index 3cab6524..08fe1383 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": ">=8.1" + "php": ">=8.3" }, "require-dev": { "phpunit/phpunit": "^10.5", diff --git a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini index d6e0e51d..cb0931f3 100644 --- a/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini +++ b/source/packages/com_mokosuitecross/language/en-GB/com_mokosuitecross.sys.ini @@ -3,6 +3,7 @@ ; License: GPL-3.0-or-later COM_MOKOSUITECROSS="MokoSuiteCross" +COM_MOKOSUITECROSS_SHORT="Cross" COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms" COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard" COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue" diff --git a/source/packages/com_mokosuitecross/mokosuitecross.xml b/source/packages/com_mokosuitecross/mokosuitecross.xml index 59d58acc..5a6b5d30 100644 --- a/source/packages/com_mokosuitecross/mokosuitecross.xml +++ b/source/packages/com_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ com_mokosuitecross - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -43,7 +43,7 @@ - COM_MOKOSUITECROSS + COM_MOKOSUITECROSS_SHORT COM_MOKOSUITECROSS_SUBMENU_DASHBOARD COM_MOKOSUITECROSS_SUBMENU_POSTS diff --git a/source/packages/com_mokosuitecross/script.php b/source/packages/com_mokosuitecross/script.php index 37d30cd4..701aa0b5 100644 --- a/source/packages/com_mokosuitecross/script.php +++ b/source/packages/com_mokosuitecross/script.php @@ -11,6 +11,7 @@ defined('_JEXEC') or die; +use Joomla\CMS\Factory; use Joomla\CMS\Installer\InstallerAdapter; class Com_MokoSuiteCrossInstallerScript @@ -22,5 +23,77 @@ class Com_MokoSuiteCrossInstallerScript public function postflight(string $type, InstallerAdapter $parent): void { + if ($type !== 'install') { + return; + } + + // Be smart: verify EVERY table the component's own install SQL declares is + // actually present. If any are missing the install did not complete — say + // so instead of implying "installed successfully". + $missing = $this->missingTables(); + + if ($missing !== []) { + $detail = count($missing) > 6 + ? count($missing) . ' expected database tables are missing' + : 'missing tables: ' . htmlspecialchars(implode(', ', $missing), ENT_QUOTES, 'UTF-8'); + + Factory::getApplication()->enqueueMessage( + 'MokoSuiteCross did not install correctly. ' . $detail + . ' — review the errors above, then reinstall.', + 'error' + ); + + return; + } + + // Success path: schema installed correctly. + Factory::getApplication()->enqueueMessage( + '

MokoSuiteCross installed successfully!

' + . '

To enable automatic updates, enter your license key in the ' + . 'component configuration.

', + 'info' + ); + } + + /** + * Smart schema check: parse the component's own installed install SQL for every + * CREATE TABLE, then return the ones missing from the database. An empty array + * means the full schema installed. Fails open (returns []) if the SQL can't be + * read, so a good install is never flagged as failed. + * + * @return string[] Names (without the #__ prefix) of missing tables. + */ + private function missingTables(): array + { + try { + $sqlFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitecross/sql/install.mysql.sql'; + + if (!is_file($sqlFile)) { + return []; + } + + $sql = file_get_contents($sqlFile); + + if ($sql === false + || !preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(#__[A-Za-z0-9_]+)/i', $sql, $m)) { + return []; + } + + $db = Factory::getDbo(); + $prefix = strtolower($db->getPrefix()); + $have = array_map('strtolower', $db->getTableList()); + $missing = []; + + foreach (array_unique($m[1]) as $decl) { + $bare = substr($decl, 3); // strip "#__" + if (!in_array($prefix . strtolower($bare), $have, true)) { + $missing[] = $bare; + } + } + + return $missing; + } catch (\Throwable $e) { + return []; + } } } diff --git a/source/packages/com_mokosuitecross/sql/uninstall.mysql.sql b/source/packages/com_mokosuitecross/sql/uninstall.mysql.sql index 94b03c57..8bdbc187 100644 --- a/source/packages/com_mokosuitecross/sql/uninstall.mysql.sql +++ b/source/packages/com_mokosuitecross/sql/uninstall.mysql.sql @@ -1,5 +1,7 @@ --- MokoSuiteCross — Uninstall +-- MokoSuiteCross -- Uninstall DROP TABLE IF EXISTS `#__mokosuitecross_logs`; +DROP TABLE IF EXISTS `#__mokosuitecross_analytics`; +DROP TABLE IF EXISTS `#__mokosuitecross_category_rules`; DROP TABLE IF EXISTS `#__mokosuitecross_posts`; DROP TABLE IF EXISTS `#__mokosuitecross_templates`; DROP TABLE IF EXISTS `#__mokosuitecross_services`; diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.01.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.01.sql new file mode 100644 index 00000000..4b2edd58 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.01.sql @@ -0,0 +1 @@ +/* 01.13.01 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.02.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.02.sql new file mode 100644 index 00000000..a3428a8a --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.02.sql @@ -0,0 +1 @@ +/* 01.13.02 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.03.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.03.sql new file mode 100644 index 00000000..6dc975a9 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.03.sql @@ -0,0 +1 @@ +/* 01.13.03 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.04.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.04.sql new file mode 100644 index 00000000..3ed9641a --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.04.sql @@ -0,0 +1 @@ +/* 01.13.04 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.06.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.06.sql new file mode 100644 index 00000000..727bedad --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.06.sql @@ -0,0 +1 @@ +/* 01.13.06 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.08.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.08.sql new file mode 100644 index 00000000..c538ab30 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.08.sql @@ -0,0 +1 @@ +/* 01.13.08 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.09.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.09.sql new file mode 100644 index 00000000..578dd77b --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.09.sql @@ -0,0 +1 @@ +/* 01.13.09 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.10.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.10.sql new file mode 100644 index 00000000..02136d33 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.10.sql @@ -0,0 +1 @@ +/* 01.13.10 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.11.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.11.sql new file mode 100644 index 00000000..71468a37 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.11.sql @@ -0,0 +1 @@ +/* 01.13.11 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.12.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.12.sql new file mode 100644 index 00000000..3da0c883 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.12.sql @@ -0,0 +1 @@ +/* 01.13.12 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.13.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.13.sql new file mode 100644 index 00000000..e354d314 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.13.sql @@ -0,0 +1 @@ +/* 01.13.13 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.14.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.14.sql new file mode 100644 index 00000000..5d084c47 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.14.sql @@ -0,0 +1 @@ +/* 01.13.14 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.15.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.15.sql new file mode 100644 index 00000000..e5439d79 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.15.sql @@ -0,0 +1 @@ +/* 01.13.15 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.16.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.16.sql new file mode 100644 index 00000000..94a13e6d --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.16.sql @@ -0,0 +1 @@ +/* 01.13.16 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.17.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.17.sql new file mode 100644 index 00000000..0b03f630 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.17.sql @@ -0,0 +1 @@ +/* 01.13.17 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.18.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.18.sql new file mode 100644 index 00000000..3d065f19 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.18.sql @@ -0,0 +1 @@ +/* 01.13.18 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.19.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.19.sql new file mode 100644 index 00000000..cd9a4b8f --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.19.sql @@ -0,0 +1 @@ +/* 01.13.19 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.20.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.20.sql new file mode 100644 index 00000000..13ed5887 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.20.sql @@ -0,0 +1 @@ +/* 01.13.20 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.21.sql b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.21.sql new file mode 100644 index 00000000..48e286f4 --- /dev/null +++ b/source/packages/com_mokosuitecross/sql/updates/mysql/01.13.21.sql @@ -0,0 +1 @@ +/* 01.13.21 — no schema changes */ diff --git a/source/packages/com_mokosuitecross/src/Controller/PostsController.php b/source/packages/com_mokosuitecross/src/Controller/PostsController.php index 8f049d5e..6a994734 100644 --- a/source/packages/com_mokosuitecross/src/Controller/PostsController.php +++ b/source/packages/com_mokosuitecross/src/Controller/PostsController.php @@ -130,6 +130,10 @@ class PostsController extends AdminController { $this->checkToken(); + if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.manage', 'com_mokosuitecross')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + $db = Factory::getDbo(); $query = $db->getQuery(true) @@ -238,6 +242,10 @@ class PostsController extends AdminController { $this->checkToken(); + if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.manage', 'com_mokosuitecross')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + $db = Factory::getDbo(); $query = $db->getQuery(true) diff --git a/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php b/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php index 47e917a9..2839cbd8 100644 --- a/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php +++ b/source/packages/com_mokosuitecross/src/Helper/AnalyticsHelper.php @@ -19,13 +19,6 @@ class AnalyticsHelper { /** * Record or update engagement metrics for a post. - * - * @param int $postId The post ID - * @param int $serviceId The service ID - * @param string $serviceType The service type (e.g. twitter, facebook) - * @param array $metrics Engagement metrics: impressions, engagements, clicks, shares, posted_at - * - * @return bool True on success */ public static function recordEngagement(int $postId, int $serviceId, string $serviceType, array $metrics): bool { @@ -51,7 +44,6 @@ class AnalyticsHelper ? round(($engagements / $impressions) * 100, 2) : 0.00; - // Check if a row already exists for this post $query = $db->getQuery(true) ->select($db->quoteName('id')) ->from($db->quoteName('#__mokosuitecross_analytics')) @@ -96,12 +88,7 @@ class AnalyticsHelper } /** - * Get heatmap data as a 7x24 grid of average engagement rates. - * - * @param string $serviceType Optional service type filter - * @param int $days Number of days to look back (0 = all time) - * - * @return array 7x24 grid: [ day_of_week => [ hour_of_day => avg_engagement_rate ] ] + * Get heatmap data as a 7x24 grid derived from actual post success data. */ public static function getHeatmapData(string $serviceType = '', int $days = 90): array { @@ -109,30 +96,40 @@ class AnalyticsHelper $query = $db->getQuery(true) ->select([ - $db->quoteName('day_of_week'), - $db->quoteName('hour_of_day'), - 'AVG(' . $db->quoteName('engagement_rate') . ') AS avg_rate', + 'DAYOFWEEK(' . $db->quoteName('p.posted_at') . ') - 1 AS day_of_week', + 'HOUR(' . $db->quoteName('p.posted_at') . ') AS hour_of_day', 'COUNT(*) AS post_count', ]) - ->from($db->quoteName('#__mokosuitecross_analytics')) - ->group($db->quoteName('day_of_week')) - ->group($db->quoteName('hour_of_day')) - ->order($db->quoteName('day_of_week') . ' ASC') - ->order($db->quoteName('hour_of_day') . ' ASC'); + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->where($db->quoteName('p.status') . ' = ' . $db->quote('posted')) + ->where($db->quoteName('p.posted_at') . ' IS NOT NULL') + ->group('day_of_week') + ->group('hour_of_day') + ->order('day_of_week ASC') + ->order('hour_of_day ASC'); if ($serviceType !== '') { - $query->where($db->quoteName('service_type') . ' = ' . $db->quote($serviceType)); + $query->where($db->quoteName('s.service_type') . ' = ' . $db->quote($serviceType)); } if ($days > 0) { $cutoff = Factory::getDate('-' . $days . ' days')->toSql(); - $query->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff)); + $query->where($db->quoteName('p.posted_at') . ' >= ' . $db->quote($cutoff)); } $db->setQuery($query); $rows = $db->loadObjectList(); - // Build 7x24 grid initialised to zero + $maxCount = 1; + + foreach ($rows as $row) { + if ((int) $row->post_count > $maxCount) { + $maxCount = (int) $row->post_count; + } + } + $grid = []; for ($d = 0; $d < 7; $d++) { @@ -142,9 +139,10 @@ class AnalyticsHelper } foreach ($rows as $row) { + $count = (int) $row->post_count; $grid[(int) $row->day_of_week][(int) $row->hour_of_day] = [ - 'avg_rate' => round((float) $row->avg_rate, 2), - 'post_count' => (int) $row->post_count, + 'avg_rate' => round(($count / $maxCount) * 100, 2), + 'post_count' => $count, ]; } @@ -152,12 +150,7 @@ class AnalyticsHelper } /** - * Get the best times to post ranked by average engagement rate. - * - * @param string $serviceType Optional service type filter - * @param int $limit Number of results to return - * - * @return array List of [day_of_week, hour_of_day, avg_rate, post_count] + * Get the best times to post ranked by post success frequency. */ public static function getBestTimes(string $serviceType = '', int $limit = 5): array { @@ -165,19 +158,22 @@ class AnalyticsHelper $query = $db->getQuery(true) ->select([ - $db->quoteName('day_of_week'), - $db->quoteName('hour_of_day'), - 'AVG(' . $db->quoteName('engagement_rate') . ') AS avg_rate', + 'DAYOFWEEK(' . $db->quoteName('p.posted_at') . ') - 1 AS day_of_week', + 'HOUR(' . $db->quoteName('p.posted_at') . ') AS hour_of_day', 'COUNT(*) AS post_count', ]) - ->from($db->quoteName('#__mokosuitecross_analytics')) - ->group($db->quoteName('day_of_week')) - ->group($db->quoteName('hour_of_day')) + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->where($db->quoteName('p.status') . ' = ' . $db->quote('posted')) + ->where($db->quoteName('p.posted_at') . ' IS NOT NULL') + ->group('day_of_week') + ->group('hour_of_day') ->having('COUNT(*) >= 1') - ->order('avg_rate DESC'); + ->order('post_count DESC'); if ($serviceType !== '') { - $query->where($db->quoteName('service_type') . ' = ' . $db->quote($serviceType)); + $query->where($db->quoteName('s.service_type') . ' = ' . $db->quote($serviceType)); } $db->setQuery($query, 0, $limit); @@ -188,16 +184,16 @@ class AnalyticsHelper $results = []; foreach ($rows as $row) { - $hour = (int) $row['hour_of_day']; - $ampm = $hour < 12 ? 'AM' : 'PM'; - $hour12 = $hour % 12 ?: 12; + $hour = (int) $row['hour_of_day']; + $ampm = $hour < 12 ? 'AM' : 'PM'; + $hour12 = $hour % 12 ?: 12; $results[] = [ 'day_of_week' => (int) $row['day_of_week'], 'day_name' => $dayNames[(int) $row['day_of_week']], 'hour_of_day' => $hour, 'hour_label' => $hour12 . ':00 ' . $ampm, - 'avg_rate' => round((float) $row['avg_rate'], 2), + 'avg_rate' => round((float) $row['post_count'], 2), 'post_count' => (int) $row['post_count'], ]; } @@ -206,11 +202,7 @@ class AnalyticsHelper } /** - * Get engagement stats grouped by service type. - * - * @param int $days Number of days to look back (0 = all time) - * - * @return array List of [service_type, total_posts, avg_engagement_rate, total_impressions, total_engagements] + * Get stats grouped by service type from actual post data. */ public static function getServiceBreakdown(int $days = 30): array { @@ -218,35 +210,41 @@ class AnalyticsHelper $query = $db->getQuery(true) ->select([ - $db->quoteName('service_type'), + $db->quoteName('s.service_type'), 'COUNT(*) AS total_posts', - 'AVG(' . $db->quoteName('engagement_rate') . ') AS avg_engagement_rate', - 'SUM(' . $db->quoteName('impressions') . ') AS total_impressions', - 'SUM(' . $db->quoteName('engagements') . ') AS total_engagements', - 'SUM(' . $db->quoteName('clicks') . ') AS total_clicks', - 'SUM(' . $db->quoteName('shares') . ') AS total_shares', + 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' = ' . $db->quote('posted') . ' THEN 1 ELSE 0 END) AS total_succeeded', + 'SUM(CASE WHEN ' . $db->quoteName('p.status') . ' IN (' + . $db->quote('failed') . ',' . $db->quote('permanently_failed') + . ') THEN 1 ELSE 0 END) AS total_failed', ]) - ->from($db->quoteName('#__mokosuitecross_analytics')) - ->group($db->quoteName('service_type')) - ->order('avg_engagement_rate DESC'); + ->from($db->quoteName('#__mokosuitecross_posts', 'p')) + ->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') + . ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')) + ->group($db->quoteName('s.service_type')) + ->order('total_posts DESC'); if ($days > 0) { $cutoff = Factory::getDate('-' . $days . ' days')->toSql(); - $query->where($db->quoteName('posted_at') . ' >= ' . $db->quote($cutoff)); + $query->where($db->quoteName('p.created') . ' >= ' . $db->quote($cutoff)); } $db->setQuery($query); $rows = $db->loadAssocList(); foreach ($rows as &$row) { - $row['avg_engagement_rate'] = round((float) $row['avg_engagement_rate'], 2); - $row['total_posts'] = (int) $row['total_posts']; - $row['total_impressions'] = (int) $row['total_impressions']; - $row['total_engagements'] = (int) $row['total_engagements']; - $row['total_clicks'] = (int) $row['total_clicks']; - $row['total_shares'] = (int) $row['total_shares']; + $total = (int) $row['total_posts']; + $succeeded = (int) $row['total_succeeded']; + + $row['total_posts'] = $total; + $row['total_succeeded'] = $succeeded; + $row['total_failed'] = (int) $row['total_failed']; + $row['avg_engagement_rate'] = $total > 0 ? round(($succeeded / $total) * 100, 2) : 0; + $row['total_impressions'] = 0; + $row['total_engagements'] = 0; + $row['total_clicks'] = 0; + $row['total_shares'] = 0; } return $rows; } -} \ No newline at end of file +} diff --git a/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php b/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php index b1391fd4..509da82e 100644 --- a/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php +++ b/source/packages/com_mokosuitecross/src/Helper/CrossPostDispatcher.php @@ -594,13 +594,26 @@ class CrossPostDispatcher return; } - // Load service plugins + // Load service plugins using Joomla 5/6-compatible dispatcher pattern PluginHelper::importPlugin('mokosuitecross'); - $plugins = []; - Factory::getApplication()->triggerEvent('onMokoSuiteCrossGetServices', [&$plugins]); + $servicePlugins = []; + $event = new \Joomla\Event\Event('onMokoSuiteCrossGetServices', [$servicePlugins]); + + try { + Factory::getApplication()->getDispatcher()->dispatch('onMokoSuiteCrossGetServices', $event); + } catch (\Throwable $e) { + // Dispatcher may not be available + } + + $idx = 1; + + while (isset($event[$idx])) { + $servicePlugins[] = $event[$idx]; + $idx++; + } $pluginMap = []; - foreach ($plugins as $plugin) { + foreach ($servicePlugins as $plugin) { $pluginMap[$plugin->getServiceType()] = $plugin; } @@ -613,7 +626,7 @@ class CrossPostDispatcher continue; } - $credentials = json_decode($post->credentials, true) ?: []; + $credentials = CredentialHelper::decrypt($post->credentials ?: ''); try { $result = $plugin->deletePost($post->platform_post_id, $credentials); diff --git a/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php b/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php index e9bce359..711db717 100644 --- a/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php +++ b/source/packages/com_mokosuitecross/src/Helper/MokoSuiteCrossHelper.php @@ -41,9 +41,8 @@ class MokoSuiteCrossHelper 'services' => 'COM_MOKOSUITECROSS_SUBMENU_SERVICES', 'templates' => 'COM_MOKOSUITECROSS_SUBMENU_TEMPLATES', 'calendar' => 'COM_MOKOSUITECROSS_SUBMENU_CALENDAR', - 'logs' => 'COM_MOKOSUITECROSS_SUBMENU_LOGS', - 'calendar' => 'COM_MOKOSUITECROSS_SUBMENU_CALENDAR', 'analytics' => 'COM_MOKOSUITECROSS_SUBMENU_ANALYTICS', + 'logs' => 'COM_MOKOSUITECROSS_SUBMENU_LOGS', ]; // Joomla 5+ toolbar submenu diff --git a/source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php b/source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php index c515ca4c..feb5b896 100644 --- a/source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php +++ b/source/packages/com_mokosuitecross/src/Helper/QueueProcessor.php @@ -91,6 +91,16 @@ class QueueProcessor $db->setQuery($query); $retryPosts = $db->loadObjectList() ?: []; + // 3. Recover stale "posting" entries (stuck > 10 minutes) + $staleQuery = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitecross_posts')) + ->set($db->quoteName('status') . ' = ' . $db->quote('queued')) + ->set($db->quoteName('modified') . ' = ' . $db->quote($now)) + ->where($db->quoteName('status') . ' = ' . $db->quote('posting')) + ->where($db->quoteName('modified') . ' < DATE_SUB(NOW(), INTERVAL 600 SECOND)'); + $db->setQuery($staleQuery); + $db->execute(); + $allPosts = array_merge($queuedPosts, $retryPosts); foreach ($allPosts as $post) { diff --git a/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php index caacba03..c94dba51 100644 --- a/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php +++ b/source/packages/com_mokosuitecross/src/View/Calendar/HtmlView.php @@ -22,7 +22,7 @@ use Joomla\Component\MokoSuiteCross\Administrator\Helper\MokoSuiteCrossHelper; class HtmlView extends BaseHtmlView { - public $sidebar; + public $ajaxUrl; public function display($tpl = null): void @@ -40,7 +40,6 @@ class HtmlView extends BaseHtmlView $this->addToolbar(); MokoSuiteCrossHelper::addSubmenu('calendar'); - $this->sidebar = \Joomla\CMS\HTML\Sidebar::render(); // Set document title Factory::getApplication()->getDocument()->setTitle( diff --git a/source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php b/source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php index 671fc0d0..22b87c21 100644 --- a/source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php +++ b/source/packages/com_mokosuitecross/src/View/Dashboard/HtmlView.php @@ -26,7 +26,7 @@ class HtmlView extends BaseHtmlView protected $serviceBreakdown; protected $dailyTrend; protected $topArticles; - public $sidebar; + public $period; public function display($tpl = null): void @@ -58,7 +58,6 @@ class HtmlView extends BaseHtmlView $this->addToolbar(); MokoSuiteCrossHelper::addSubmenu('dashboard'); - $this->sidebar = \Joomla\CMS\HTML\Sidebar::render(); parent::display($tpl); } diff --git a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml index 2161e06e..d7bddc12 100644 --- a/source/packages/plg_content_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_content_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Content - MokoSuiteCross - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php index 99fe1bb1..92193260 100644 --- a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php +++ b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php @@ -535,6 +535,19 @@ XML; continue; } + // Respect first-publish-only: skip if article was previously posted + if ($params->get('post_on_first_publish_only', 0)) { + $existsQuery = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokosuitecross_posts')) + ->where($db->quoteName('article_id') . ' = ' . (int) $pk); + $db->setQuery($existsQuery); + + if ((int) $db->loadResult() > 0) { + continue; + } + } + $url = Uri::root() . 'index.php?option=com_content&view=article&id=' . $article->id; if (!empty($article->catid)) { diff --git a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml index fc5a4775..5dc8eaef 100644 --- a/source/packages/plg_mokosuitecross_activitypub/activitypub.xml +++ b/source/packages/plg_mokosuitecross_activitypub/activitypub.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ActivityPub (Fediverse) - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_blogger/blogger.xml b/source/packages/plg_mokosuitecross_blogger/blogger.xml index 63bc4e27..260395db 100644 --- a/source/packages/plg_mokosuitecross_blogger/blogger.xml +++ b/source/packages/plg_mokosuitecross_blogger/blogger.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Blogger - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml index eb773d69..5729c8a8 100644 --- a/source/packages/plg_mokosuitecross_bluesky/bluesky.xml +++ b/source/packages/plg_mokosuitecross_bluesky/bluesky.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Bluesky - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_brevo/brevo.xml b/source/packages/plg_mokosuitecross_brevo/brevo.xml index c796256d..2163f995 100644 --- a/source/packages/plg_mokosuitecross_brevo/brevo.xml +++ b/source/packages/plg_mokosuitecross_brevo/brevo.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Brevo (Sendinblue) - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml index c9d73bb5..ce52502d 100644 --- a/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml +++ b/source/packages/plg_mokosuitecross_constantcontact/constantcontact.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Constant Contact - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml index 55b4273f..fec18e07 100644 --- a/source/packages/plg_mokosuitecross_convertkit/convertkit.xml +++ b/source/packages/plg_mokosuitecross_convertkit/convertkit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - ConvertKit - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_devto/devto.xml b/source/packages/plg_mokosuitecross_devto/devto.xml index e682294d..8374a417 100644 --- a/source/packages/plg_mokosuitecross_devto/devto.xml +++ b/source/packages/plg_mokosuitecross_devto/devto.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Dev.to - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_discord/discord.xml b/source/packages/plg_mokosuitecross_discord/discord.xml index 9143accf..d168b03d 100644 --- a/source/packages/plg_mokosuitecross_discord/discord.xml +++ b/source/packages/plg_mokosuitecross_discord/discord.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Discord - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_facebook/facebook.xml b/source/packages/plg_mokosuitecross_facebook/facebook.xml index 3ba39c71..f6f0eaf9 100644 --- a/source/packages/plg_mokosuitecross_facebook/facebook.xml +++ b/source/packages/plg_mokosuitecross_facebook/facebook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Facebook / Meta - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ghost/ghost.xml b/source/packages/plg_mokosuitecross_ghost/ghost.xml index 479b818b..eb150620 100644 --- a/source/packages/plg_mokosuitecross_ghost/ghost.xml +++ b/source/packages/plg_mokosuitecross_ghost/ghost.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ghost - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml index 825d7e52..ff6ebb99 100644 --- a/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml +++ b/source/packages/plg_mokosuitecross_googlebusiness/googlebusiness.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Business Profile - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml index 53f1b0ae..0fa9b0e3 100644 --- a/source/packages/plg_mokosuitecross_googlechat/googlechat.xml +++ b/source/packages/plg_mokosuitecross_googlechat/googlechat.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Google Chat - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml index d4171b65..013ce3c1 100644 --- a/source/packages/plg_mokosuitecross_hashnode/hashnode.xml +++ b/source/packages/plg_mokosuitecross_hashnode/hashnode.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Hashnode - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_instagram/instagram.xml b/source/packages/plg_mokosuitecross_instagram/instagram.xml index 98901b47..8b46e058 100644 --- a/source/packages/plg_mokosuitecross_instagram/instagram.xml +++ b/source/packages/plg_mokosuitecross_instagram/instagram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Instagram - 01.13.00 + 01.13.21 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml index 69d5a1c2..78876a41 100644 --- a/source/packages/plg_mokosuitecross_linkedin/linkedin.xml +++ b/source/packages/plg_mokosuitecross_linkedin/linkedin.xml @@ -1,7 +1,7 @@ MokoSuiteCross - LinkedIn - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml index 590fba19..1d41468c 100644 --- a/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml +++ b/source/packages/plg_mokosuitecross_mailchimp/mailchimp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mailchimp - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml index 1b3f6c8f..7263bbc1 100644 --- a/source/packages/plg_mokosuitecross_mastodon/mastodon.xml +++ b/source/packages/plg_mokosuitecross_mastodon/mastodon.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Mastodon - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_matrix/matrix.xml b/source/packages/plg_mokosuitecross_matrix/matrix.xml index 6141b657..d4d4e035 100644 --- a/source/packages/plg_mokosuitecross_matrix/matrix.xml +++ b/source/packages/plg_mokosuitecross_matrix/matrix.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Matrix / Element - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_medium/medium.xml b/source/packages/plg_mokosuitecross_medium/medium.xml index 07174769..bada0a4e 100644 --- a/source/packages/plg_mokosuitecross_medium/medium.xml +++ b/source/packages/plg_mokosuitecross_medium/medium.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Medium - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml index 705d10a1..bf08a0bd 100644 --- a/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml +++ b/source/packages/plg_mokosuitecross_mokosuitecalendar/mokosuitecalendar.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteCalendar Events - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml index 424367ce..6c9a5b4e 100644 --- a/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml +++ b/source/packages/plg_mokosuitecross_mokosuitegallery/mokosuitegallery.xml @@ -1,7 +1,7 @@ MokoSuiteCross - MokoSuiteGallery - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_nostr/nostr.xml b/source/packages/plg_mokosuitecross_nostr/nostr.xml index 247e5c7a..9ec3f4f2 100644 --- a/source/packages/plg_mokosuitecross_nostr/nostr.xml +++ b/source/packages/plg_mokosuitecross_nostr/nostr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Nostr - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml index beb37da2..f7cc73a9 100644 --- a/source/packages/plg_mokosuitecross_ntfy/ntfy.xml +++ b/source/packages/plg_mokosuitecross_ntfy/ntfy.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Ntfy Push Notifications - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml index 60eea07c..8d1a595b 100644 --- a/source/packages/plg_mokosuitecross_pinterest/pinterest.xml +++ b/source/packages/plg_mokosuitecross_pinterest/pinterest.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Pinterest - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_reddit/reddit.xml b/source/packages/plg_mokosuitecross_reddit/reddit.xml index eb4e9f61..ff528edc 100644 --- a/source/packages/plg_mokosuitecross_reddit/reddit.xml +++ b/source/packages/plg_mokosuitecross_reddit/reddit.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Reddit - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml index 461d03a2..94fabfd4 100644 --- a/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml +++ b/source/packages/plg_mokosuitecross_rssfeed/rssfeed.xml @@ -1,7 +1,7 @@ MokoSuiteCross - RSS Feed - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml index b18569ca..d4705114 100644 --- a/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml +++ b/source/packages/plg_mokosuitecross_sendgrid/sendgrid.xml @@ -1,7 +1,7 @@ MokoSuiteCross - SendGrid - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_slack/slack.xml b/source/packages/plg_mokosuitecross_slack/slack.xml index f92c62ae..b95536b7 100644 --- a/source/packages/plg_mokosuitecross_slack/slack.xml +++ b/source/packages/plg_mokosuitecross_slack/slack.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Slack - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_teams/teams.xml b/source/packages/plg_mokosuitecross_teams/teams.xml index b7020295..b1395868 100644 --- a/source/packages/plg_mokosuitecross_teams/teams.xml +++ b/source/packages/plg_mokosuitecross_teams/teams.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Microsoft Teams - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_telegram/telegram.xml b/source/packages/plg_mokosuitecross_telegram/telegram.xml index 7de8eba8..5a57310e 100644 --- a/source/packages/plg_mokosuitecross_telegram/telegram.xml +++ b/source/packages/plg_mokosuitecross_telegram/telegram.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Telegram - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_threads/threads.xml b/source/packages/plg_mokosuitecross_threads/threads.xml index d81f6f68..c1a1de94 100644 --- a/source/packages/plg_mokosuitecross_threads/threads.xml +++ b/source/packages/plg_mokosuitecross_threads/threads.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Threads (Meta) - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml index f8525033..913316ce 100644 --- a/source/packages/plg_mokosuitecross_tiktok/tiktok.xml +++ b/source/packages/plg_mokosuitecross_tiktok/tiktok.xml @@ -1,7 +1,7 @@ MokoSuiteCross - TikTok - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml index 868a38fc..946bf22b 100644 --- a/source/packages/plg_mokosuitecross_tumblr/tumblr.xml +++ b/source/packages/plg_mokosuitecross_tumblr/tumblr.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Tumblr - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_twitter/twitter.xml b/source/packages/plg_mokosuitecross_twitter/twitter.xml index 2755455a..5d7ea8f3 100644 --- a/source/packages/plg_mokosuitecross_twitter/twitter.xml +++ b/source/packages/plg_mokosuitecross_twitter/twitter.xml @@ -1,7 +1,7 @@ MokoSuiteCross - X / Twitter - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_webhook/webhook.xml b/source/packages/plg_mokosuitecross_webhook/webhook.xml index 8072d0c2..206affc5 100644 --- a/source/packages/plg_mokosuitecross_webhook/webhook.xml +++ b/source/packages/plg_mokosuitecross_webhook/webhook.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Generic Webhook - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml index b23875f6..e36658dc 100644 --- a/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml +++ b/source/packages/plg_mokosuitecross_whatsapp/whatsapp.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WhatsApp Business - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml index d3bd619e..9ae06c78 100644 --- a/source/packages/plg_mokosuitecross_wordpress/wordpress.xml +++ b/source/packages/plg_mokosuitecross_wordpress/wordpress.xml @@ -1,7 +1,7 @@ MokoSuiteCross - WordPress - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_mokosuitecross_youtube/youtube.xml b/source/packages/plg_mokosuitecross_youtube/youtube.xml index f50752b4..9749fc47 100644 --- a/source/packages/plg_mokosuitecross_youtube/youtube.xml +++ b/source/packages/plg_mokosuitecross_youtube/youtube.xml @@ -1,7 +1,7 @@ MokoSuiteCross - Youtube - 01.13.00 + 01.13.21 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml index d9bcd5f9..5c6d6f9b 100644 --- a/source/packages/plg_system_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_system_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml index 3b12d665..736f602d 100644 --- a/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml +++ b/source/packages/plg_system_mokosuitecross_events/mokosuitecross_events.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Events - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml index 9fe1c0fa..74de6209 100644 --- a/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml +++ b/source/packages/plg_system_mokosuitecross_gallery/mokosuitecross_gallery.xml @@ -1,7 +1,7 @@ System - MokoSuiteCross Gallery - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml index 91883a2d..52c7628e 100644 --- a/source/packages/plg_task_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_task_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Task - MokoSuiteCross Queue Processor - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml index 20bdfbb7..d888c12b 100644 --- a/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml +++ b/source/packages/plg_webservices_mokosuitecross/mokosuitecross.xml @@ -1,7 +1,7 @@ Web Services - MokoSuiteCross - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitecross.xml b/source/pkg_mokosuitecross.xml index 56885011..84a7c90c 100644 --- a/source/pkg_mokosuitecross.xml +++ b/source/pkg_mokosuitecross.xml @@ -2,7 +2,7 @@ MokoSuiteCross mokosuitecross - 01.13.00 + 01.13.21 2026-05-28 Moko Consulting hello@mokoconsulting.tech @@ -76,6 +76,6 @@ true - https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/updates.xml + https://git.mokoconsulting.tech/api/packages/MokoConsulting/generic/MokoSuiteCross/latest/updates.xml diff --git a/source/script.php b/source/script.php index 7c4bfcbc..859e94f9 100644 --- a/source/script.php +++ b/source/script.php @@ -40,8 +40,8 @@ class Pkg_MokoSuiteCrossInstallerScript public function postflight(string $type, InstallerAdapter $parent): void { + // The download key must be restored regardless of the install outcome. $this->restoreDownloadKey(); - $this->warnMissingLicenseKey(); $db = Factory::getDbo(); @@ -76,6 +76,97 @@ class Pkg_MokoSuiteCrossInstallerScript $this->detectPerfectPublisherPro($db); } + + // Be smart: only run the success path (and show the licence notice, which + // implies a completed install) when every child extension declared in the + // package manifest actually registered. If any are missing, the package + // did NOT install successfully — surface that instead of implying success. + $missing = $this->missingChildExtensions($parent); + + if ($missing !== []) + { + Factory::getApplication()->enqueueMessage( + 'MokoSuiteCross did not install completely. ' + . 'The following extension(s) failed to install: ' + . htmlspecialchars(implode(', ', $missing), ENT_QUOTES, 'UTF-8') . '. ' + . 'Resolve the errors shown above, then uninstall and reinstall the package.', + 'error' + ); + + return; + } + + $this->warnMissingLicenseKey(); + } + + /** + * Compare the child extensions the package manifest declares against what is + * actually registered in #__extensions after the install/update. Returns the + * label of each declared extension that is missing (i.e. failed to install); + * an empty array means every child installed and the package succeeded. + * + * Fails open: any error returns [] so a query problem can never turn a good + * install into a false "did not install completely" warning. + * + * @param InstallerAdapter $parent + * @return string[] + */ + private function missingChildExtensions(InstallerAdapter $parent): array + { + $missing = []; + + try + { + $manifest = $parent->getParent()->getManifest(); + + if (!$manifest || !isset($manifest->files) || !isset($manifest->files->file)) + { + return []; + } + + $db = Factory::getDbo(); + + foreach ($manifest->files->file as $file) + { + $type = (string) $file['type']; + $element = (string) $file['id']; + $group = (string) $file['group']; + + if ($element === '') + { + continue; + } + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)); + + if ($type !== '') + { + $query->where($db->quoteName('type') . ' = ' . $db->quote($type)); + } + + // Plugins are only unique by element + folder (the manifest "group"). + if ($type === 'plugin' && $group !== '') + { + $query->where($db->quoteName('folder') . ' = ' . $db->quote($group)); + } + + $db->setQuery($query); + + if ((int) $db->loadResult() === 0) + { + $missing[] = $element . ($group !== '' ? ' (' . $group . ')' : ''); + } + } + } + catch (\Throwable $e) + { + return []; + } + + return $missing; } private function detectPerfectPublisherPro($db): void