Compare commits

...

138 Commits

Author SHA1 Message Date
gitea-actions[bot] 89a15364ae chore(version): auto-bump patch 01.08.52-dev [skip ci] 2026-06-28 16:51:28 +00:00
jmiller e67fbdfe2b feat: add visual post calendar with drag-drop rescheduling (#160)
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Authored-by: Moko Consulting
2026-06-28 11:51:05 -05:00
gitea-actions[bot] b03c7c6ba7 chore(version): pre-release bump to 01.08.51-dev [skip ci] 2026-06-28 16:29:19 +00:00
jmiller 1c15497c32 Merge pull request 'fix: prevent GitHub Actions injection in CI issue reporter' (#197) from fix/ci-workflow-injection into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
2026-06-28 16:29:10 +00:00
gitea-actions[bot] 9e38609fe9 chore(version): pre-release bump to 01.08.50-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
2026-06-28 16:29:10 +00:00
jmiller b907b778c0 fix: pass workflow inputs via env block to prevent injection
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 27s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 11:26:44 -05:00
gitea-actions[bot] 4d758890a8 chore(version): pre-release bump to 01.08.49-dev [skip ci] 2026-06-28 16:25:20 +00:00
jmiller 824b4d9ecd Merge pull request 'feat: TikTok video upload and photo carousel (#164)' (#196) from feature/164-tiktok-enhancements into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
2026-06-28 16:25:09 +00:00
jmiller 307eb7741d feat: add TikTok video upload and photo carousel support (#164)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 29s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
- Video publishing via PULL_FROM_URL with async status polling
- Photo carousel up to 35 images via content/init endpoint
- Configurable posting mode: DIRECT_POST or MEDIA_UPLOAD
- Audit warning language string for unverified app limitations
- Updated getSupportedMediaTypes() to include carousel

Authored-by: Moko Consulting
2026-06-28 11:24:23 -05:00
gitea-actions[bot] 4a13ea6ade chore(version): pre-release bump to 01.08.47-dev [skip ci] 2026-06-28 16:23:21 +00:00
jmiller bcc17e4882 Merge pull request 'feat: AI caption generation with Claude/OpenAI (#161)' (#195) from feature/161-ai-post-generation into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
2026-06-28 16:23:13 +00:00
gitea-actions[bot] 4ce96dc95b chore(version): auto-bump patch 01.08.46-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-06-28 16:22:43 +00:00
jmiller 99e4a83ed5 feat: add AI caption generation with Claude and OpenAI support (#161)
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
- AiGeneratorHelper: Claude Messages API and OpenAI Chat Completions
  with structured JSON output for social/short/chat/email_subject
- AiController: AJAX endpoint with CSRF and ACL checks
- config.xml: new AI fieldset (provider, API key, model, tone)
- Content plugin: "Generate with AI" button in Share Content panel
- Language strings for all AI config and UI elements

Authored-by: Moko Consulting
2026-06-28 11:21:56 -05:00
gitea-actions[bot] 63c4fbcd14 chore(version): pre-release bump to 01.08.45-dev [skip ci] 2026-06-28 16:15:30 +00:00
jmiller 15a03b309b Merge pull request 'feat(#133): Site frontend with cross-post list and detail views' (#187) from feature/133-site-frontend into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
2026-06-28 16:15:20 +00:00
jmiller a537132836 feat(#133): add site frontend with cross-post list and detail views
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 30s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 11:14:58 -05:00
gitea-actions[bot] 6f29c077e2 chore(version): pre-release bump to 01.08.44-dev [skip ci] 2026-06-28 16:14:39 +00:00
jmiller 9fa2560ce4 Merge pull request 'feat(#159): Link shortening (Bitly, Rebrandly, YOURLS)' (#186) from feature/159-link-shortening into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
2026-06-28 16:14:27 +00:00
Jonathan Miller 45afb1f0b1 feat(#159): add link shortening support (Bitly, Rebrandly, YOURLS) with {url_short} placeholder
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 4s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 33s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 11:14:04 -05:00
gitea-actions[bot] 843c729828 chore(version): pre-release bump to 01.08.43-dev [skip ci] 2026-06-28 16:12:35 +00:00
jmiller db061e2b75 Merge pull request 'feat(#132): PHPUnit test suite' (#185) from feature/132-phpunit-tests into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
2026-06-28 16:12:18 +00:00
jmiller a6dc736787 feat(#132): add PHPUnit test suite with unit tests
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 39s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Add phpunit.xml.dist, bootstrap, and PSR-4 autoload config. Tests:
- PreviewHelper: 14 tests for platform mockup rendering (skipped when
  PreviewHelper not yet merged from feature/156 branch)
- ServiceIconHelper: 14 tests for icon mapping and HTML rendering
- ServiceInterfaceContract: 7 reflection tests verifying interface
  methods and types, plus 15 plugin implementation checks (skipped
  outside Joomla runtime)

21 tests pass immediately, 29 skip gracefully.

Authored-by: Moko Consulting
2026-06-28 11:11:45 -05:00
gitea-actions[bot] a247a5fd0e chore(version): pre-release bump to 01.08.41-dev [skip ci] 2026-06-28 16:10:55 +00:00
jmiller e0c95b4291 Merge pull request 'feat(#156): Social preview panel for article editor' (#188) from feature/156-social-preview into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
2026-06-28 16:10:39 +00:00
jmiller decb1ba8b7 feat(#156): add social preview panel for article editor
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 11s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
PreviewHelper renders platform-specific mockups (Twitter, Facebook,
LinkedIn, Mastodon, Bluesky, Telegram) showing how cross-posted content
will appear. PreviewController serves AJAX requests from the article
editor. Content plugin injects platform selector and preview button
into the Cross-Posting fieldset for existing articles.

Authored-by: Moko Consulting
2026-06-28 11:09:35 -05:00
gitea-actions[bot] 290284a0c9 chore(version): pre-release bump to 01.08.40-dev [skip ci] 2026-06-28 15:37:03 +00:00
gitea-actions[bot] c9eff72278 chore(version): pre-release bump to 01.08.39-dev [skip ci] 2026-06-28 15:36:43 +00:00
jmiller a86686c30a Merge pull request 'feat: add Twitter thread support and cost warning' (#194) from feature/163-twitter-threads into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-28 15:36:22 +00:00
jmiller bf8cc9bd0a Merge pull request 'feat: add Facebook Reels, Stories, and scheduled post support' (#193) from feature/162-facebook-enhancements into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-28 15:36:11 +00:00
gitea-actions[bot] bed05630ca chore(version): auto-bump patch 01.08.38-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
2026-06-28 15:36:09 +00:00
jmiller 831223f7bc feat: add Twitter thread support and cost warning (#163)
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 08:04:10 -05:00
jmiller 0428904ae8 feat: add Facebook Reels, Stories, and scheduled post support (#162)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 54s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-28 08:04:02 -05:00
gitea-actions[bot] 4cfde99e7f chore(version): pre-release bump to 01.08.37-dev [skip ci] 2026-06-28 08:37:47 +00:00
gitea-actions[bot] 1e105d6c7b chore(version): auto-bump patch 01.08.36-dev [skip ci] 2026-06-28 08:37:31 +00:00
jmiller 2140c9e07f chore: sync CODE_OF_CONDUCT.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Authored-by: Moko Consulting
2026-06-28 08:36:55 +00:00
gitea-actions[bot] 5cb6dd8008 chore(version): pre-release bump to 01.08.35-dev [skip ci] 2026-06-28 08:36:43 +00:00
gitea-actions[bot] 2264b00828 chore(version): auto-bump patch 01.08.34-dev [skip ci] 2026-06-28 08:36:27 +00:00
jmiller f87086bd0f chore: sync phpstan.neon from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Authored-by: Moko Consulting
2026-06-28 08:35:55 +00:00
gitea-actions[bot] fc23c771c0 chore(version): pre-release bump to 01.08.33-dev [skip ci] 2026-06-28 08:35:44 +00:00
gitea-actions[bot] 96299a6b9a chore(version): auto-bump patch 01.08.32-dev [skip ci] 2026-06-28 08:35:29 +00:00
jmiller 1961585e83 chore: sync .editorconfig from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Authored-by: Moko Consulting
2026-06-28 08:35:17 +00:00
gitea-actions[bot] 14f5407820 chore(version): pre-release bump to 01.08.31-dev [skip ci] 2026-06-28 08:31:16 +00:00
gitea-actions[bot] 407b30a437 chore(version): pre-release bump to 01.08.30-dev [skip ci] 2026-06-28 08:30:11 +00:00
jmiller 49041565eb chore: sync version.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Authored-by: Moko Consulting
2026-06-28 08:29:18 +00:00
jmiller 1d1026f7e7 chore: sync security.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Authored-by: Moko Consulting
2026-06-28 08:28:58 +00:00
gitea-actions[bot] 1f7329272d chore(version): pre-release bump to 01.08.29-dev [skip ci] 2026-06-28 08:28:41 +00:00
jmiller 4c855ac7c8 chore: sync rfc.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 9s
Authored-by: Moko Consulting
2026-06-28 08:28:19 +00:00
gitea-actions[bot] a350d02d08 chore(version): pre-release bump to 01.08.28-dev [skip ci] 2026-06-28 08:27:56 +00:00
gitea-actions[bot] a860d414bd chore(version): pre-release bump to 01.08.27-dev [skip ci] 2026-06-28 08:27:27 +00:00
jmiller e03c86f2c6 chore: sync question.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Authored-by: Moko Consulting
2026-06-28 08:27:13 +00:00
jmiller 1b0025e55f chore: sync joomla_issue.md from Template-Joomla
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
Authored-by: Moko Consulting
2026-06-28 08:26:33 +00:00
gitea-actions[bot] ffe599ee92 chore(version): pre-release bump to 01.08.26-dev [skip ci] 2026-06-28 08:26:31 +00:00
gitea-actions[bot] ac56b3a776 chore(version): pre-release bump to 01.08.25-dev [skip ci] 2026-06-28 08:25:52 +00:00
jmiller 95badba96e chore: sync feature_request.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Authored-by: Moko Consulting
2026-06-28 08:25:25 +00:00
gitea-actions[bot] de66983cda chore(version): pre-release bump to 01.08.24-dev [skip ci]
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
2026-06-28 08:25:23 +00:00
jmiller 89a59f8a8e chore: sync documentation.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Authored-by: Moko Consulting
2026-06-28 08:24:42 +00:00
jmiller 34367ae93c chore: sync config.yml from Template-Joomla
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
Authored-by: Moko Consulting
2026-06-28 08:24:12 +00:00
gitea-actions[bot] b2d4071193 chore(version): auto-bump patch 01.08.23-dev [skip ci] 2026-06-28 08:24:05 +00:00
jmiller 3bc5678768 chore: sync bug_report.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 10s
Authored-by: Moko Consulting
2026-06-28 08:23:47 +00:00
gitea-actions[bot] ca0cfd9a6d chore(version): pre-release bump to 01.08.22-dev [skip ci] 2026-06-28 08:23:32 +00:00
gitea-actions[bot] 9cc4b90b78 chore(version): auto-bump patch 01.08.21-dev [skip ci] 2026-06-28 08:23:24 +00:00
jmiller 4ebb9e30d6 chore: sync adr.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
Authored-by: Moko Consulting
2026-06-28 08:23:11 +00:00
gitea-actions[bot] 0f164b607c chore(version): pre-release bump to 01.08.20-dev [skip ci] 2026-06-28 08:08:25 +00:00
gitea-actions[bot] 6762764006 chore(version): pre-release bump to 01.08.19-dev [skip ci] 2026-06-28 08:05:57 +00:00
jmiller efdcaa712f chore: sync GOVERNANCE.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Authored-by: Moko Consulting
2026-06-28 07:59:33 +00:00
jmiller d3581564cf chore: sync ci-issue-reporter.yml from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 8s
Authored-by: Moko Consulting
2026-06-28 07:47:00 +00:00
jmiller a29d8f4e12 chore: add SECURITY.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
2026-06-28 07:25:42 +00:00
gitea-actions[bot] 109ca703ef chore(version): pre-release bump to 01.08.17-dev [skip ci] 2026-06-28 01:43:20 +00:00
gitea-actions[bot] 794746e20d chore(version): pre-release bump to 01.08.16-dev [skip ci] 2026-06-28 01:43:02 +00:00
jmiller 85848c2d6c Merge pull request 'feat: add Threads carousel, polls, and spoiler support' (#192) from feature/153-threads-enhancements into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
2026-06-28 01:42:51 +00:00
jmiller 86d4681fcd Merge pull request 'feat: add Instagram carousel, Reels, and Stories support' (#191) from feature/151-instagram-enhancements into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-28 01:42:44 +00:00
gitea-actions[bot] 0a14a29ac6 chore(version): auto-bump patch 01.08.15-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
2026-06-28 01:27:20 +00:00
gitea-actions[bot] df07b4b672 chore(version): auto-bump patch 01.08.15-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-06-28 01:27:10 +00:00
jmiller 7bd151ad62 feat: add Threads carousel, polls, and spoiler support (#153)
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Authored-by: Moko Consulting
2026-06-27 20:25:33 -05:00
jmiller ddc867ad06 feat: add Instagram carousel, Reels, and Stories support (#151)
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Authored-by: Moko Consulting
2026-06-27 20:25:26 -05:00
gitea-actions[bot] a111f5b5e9 chore(version): pre-release bump to 01.08.14-dev [skip ci] 2026-06-28 00:38:06 +00:00
jmiller 1897805483 Merge pull request 'fix: update package description to list all 38 platforms' (#190) from fix/package-description into dev
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
2026-06-28 00:37:13 +00:00
gitea-actions[bot] 8919db6fc3 chore(version): pre-release bump to 01.08.13-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 3s
2026-06-28 00:27:20 +00:00
jmiller d69b26af51 fix: update package description to list all 38 platforms and key features
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 36s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Authored-by: Moko Consulting
2026-06-27 19:26:44 -05:00
gitea-actions[bot] a8dae85f42 chore(version): pre-release bump to 01.08.12-dev [skip ci] 2026-06-27 20:34:25 +00:00
jmiller d3bc62f810 Merge pull request 'feat: implement Nostr NIP-01 WebSocket relay publishing' (#189) from feature/129-nostr-implementation into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-27 20:33:36 +00:00
gitea-actions[bot] 13683adfba chore(version): auto-bump patch 01.08.11-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 3s
2026-06-27 20:22:09 +00:00
jmiller e183b62aba feat: implement Nostr NIP-01 WebSocket relay publishing (#129)
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
- BIP-340 Schnorr signatures over secp256k1 (pure PHP, requires ext-gmp)
- Kind-1 text note events with SHA-256 event ID and tagged hashes
- Raw WebSocket client via stream_socket_client (zero external deps)
- Multi-relay failover: tries each relay until one accepts
- Public key derivation from private key for account display
- Validates 64-char hex private key format and wss:// relay URLs

Authored-by: Moko Consulting
2026-06-27 15:21:45 -05:00
gitea-actions[bot] ce9d72b50d chore(version): pre-release bump to 01.08.10-dev [skip ci] 2026-06-27 00:11:16 +00:00
gitea-actions[bot] 92358a673b chore(version): pre-release bump to 01.08.09-dev [skip ci] 2026-06-25 19:46:34 +00:00
gitea-actions[bot] 99308cd7a4 chore(version): pre-release bump to 01.08.08-dev [skip ci] 2026-06-25 17:09:50 +00:00
jmiller 561ba24090 Merge pull request 'fix: use typed Joomla 6 event parameters, remove legacy fallbacks' (#184) from fix/joomla6-event-handlers into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
2026-06-25 17:09:13 +00:00
jmiller 3e1cb9a500 fix: use typed Joomla 6 event parameters, remove legacy fallbacks
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 27s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Remove func_get_arg() legacy fallbacks from onContentBeforeDisplay,
onContentAfterSave, and onContentChangeState. All methods now use
typed event parameters (Joomla 6 only).
2026-06-25 12:08:54 -05:00
gitea-actions[bot] 5ae8e3e001 chore(version): pre-release bump to 01.08.07-dev [skip ci] 2026-06-25 16:27:32 +00:00
jmiller faea3637e0 Merge pull request 'fix: add SQL update file to match manifest version' (#183) from fix/schema-version-file into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
fix: add SQL update file to match manifest version
2026-06-25 16:26:37 +00:00
jmiller 79eaa5217d fix: add SQL update file to match manifest version
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Joomla's Database view requires a SQL update file matching the manifest
version. Missing file causes persistent schema version mismatch warning.
2026-06-25 11:25:24 -05:00
gitea-actions[bot] 0e0891f1a8 chore(version): pre-release bump to 01.08.05-dev [skip ci] 2026-06-25 16:15:51 +00:00
gitea-actions[bot] 33aaf666ae chore(version): pre-release bump to 01.08.04-dev [skip ci] 2026-06-25 16:15:19 +00:00
gitea-actions[bot] a634938799 chore(version): auto-bump patch 01.08.03-dev [skip ci] 2026-06-25 16:15:03 +00:00
jmiller 14ff4ab2f1 chore: update changelog with Joomla 6 webservices fix
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
2026-06-25 11:13:41 -05:00
gitea-actions[bot] b3de21e7d1 chore(version): pre-release bump to 01.08.02-dev [skip ci] 2026-06-25 14:54:01 +00:00
gitea-actions[bot] 72a373b17c chore(version): auto-bump patch 01.07.04-dev [skip ci] 2026-06-25 14:53:44 +00:00
jmiller bc290f3bed fix: Joomla 6 compat for webservices API route event
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Joomla 6 passes a BeforeApiRouteEvent object instead of the router
directly. Extract the router from the event for Joomla 5/6 dual compat.
2026-06-25 09:53:29 -05:00
gitea-actions[bot] a4704ad267 chore(version): pre-release bump to 01.07.03-dev [skip ci] 2026-06-23 22:53:09 +00:00
gitea-actions[bot] d1762ad5df chore(version): auto-bump patch 01.07.02-dev [skip ci] 2026-06-23 22:52:52 +00:00
Jonathan Miller df1467c518 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 17s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 38s
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 Issues (pull_request) Has been cancelled
2026-06-23 17:52:30 -05:00
Jonathan Miller 7cdd97ca59 chore: re-remove deploy-manual.yml synced from template 2026-06-23 17:51:24 -05:00
Jonathan Miller 5b36d10b04 Merge remote-tracking branch 'origin/main' into dev 2026-06-23 17:34:43 -05:00
gitea-actions[bot] 56699fdd4d chore(version): pre-release bump to 01.07.01-dev [skip ci] 2026-06-23 22:27:51 +00:00
gitea-actions[bot] fcf1cc41c8 chore(version): auto-bump patch 01.06.10-dev [skip ci] 2026-06-23 22:27:41 +00:00
Jonathan Miller b8640ccb1d fix: content plugin func_get_arg crash on non-article saves
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
onContentAfterSave and onContentChangeState fire for ALL content types
in Joomla (articles, update sites, installer, etc). The legacy fallback
used func_get_arg() without checking argument count, crashing when
com_installer saved update sites (only 1 arg, not 3).

Fix: check context early in the event object path, and guard legacy
path with func_num_args() >= N before calling func_get_arg().
2026-06-23 17:25:59 -05:00
jmiller 4b51e2dd9a chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-23 21:52:31 +00:00
jmiller e068e14004 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-23 21:52:31 +00:00
jmiller 941fd4c6cd chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-23 21:52:30 +00:00
jmiller f2021d478e chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-23 21:52:29 +00:00
gitea-actions[bot] 900ceb2bb5 chore: promote changelog [Unreleased] → [01.07.00] 2026-06-23 21:51:45 +00:00
gitea-actions[bot] 9b498e6786 chore(release): build 01.07.00 [skip ci] 2026-06-23 21:51:42 +00:00
gitea-actions[bot] ca06298e64 chore(version): pre-release bump to 01.06.09-dev [skip ci] 2026-06-23 21:51:30 +00:00
jmiller dc8d7d59d4 Release v01.06.00: Full ACL system, workflow cleanup, housekeeping 2026-06-23 21:51:24 +00:00
gitea-actions[bot] 55d2123c33 chore(version): auto-bump patch 01.06.08-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 16s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3m7s
2026-06-23 21:51:18 +00:00
Jonathan Miller 274c1f34af Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 11s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
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 Issues (pull_request) Has been cancelled
2026-06-23 16:51:01 -05:00
Jonathan Miller 75c878507e Merge remote-tracking branch 'origin/main' into dev
# Conflicts:
#	.mokogitea/manifest.xml
2026-06-23 16:50:28 -05:00
gitea-actions[bot] f1ea8ead74 chore(version): pre-release bump to 01.06.07-dev [skip ci] 2026-06-23 19:22:40 +00:00
gitea-actions[bot] 23de84610e chore(version): auto-bump patch 01.06.06-dev [skip ci] 2026-06-23 19:22:17 +00:00
Jonathan Miller 0cb24b4759 feat: full ACL system with 12 granular permissions across entire codebase
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
access.xml: 12 component-specific actions added:
- mokosuitecross.crosspost (auto cross-post on publish)
- mokosuitecross.crosspost.manual (manually create posts)
- mokosuitecross.delete.remote (delete from remote platforms)
- mokosuitecross.services.manage (create/edit/delete services)
- mokosuitecross.services.credentials (view decrypted credentials)
- mokosuitecross.templates.manage (create/edit/delete templates)
- mokosuitecross.logs.view (view activity logs)
- mokosuitecross.logs.purge (purge old logs)
- mokosuitecross.queue.manage (manage post queue)
- mokosuitecross.queue.export (export posts as CSV)
- mokosuitecross.dispatch (trigger REST API dispatch)
- mokosuitecross.migrate (run Perfect Publisher migration)

config.xml: permissions fieldset with rules field for admin UI.

Enforcement:
- DisplayController: core.manage gate on all views
- ServicesController: publish/delete ACL checks
- TemplatesController: publish/delete ACL checks
- PostsController: queue.export permission
- ServiceController: services.manage for test connection
- DispatchController: dispatch permission for REST API
- All list views: preferences button gated by core.admin
- All edit views: save/apply buttons gated by section permission
- MokoSuiteCrossHelper::getActions() centralizes ACL lookups
2026-06-23 14:21:55 -05:00
gitea-actions[bot] 7fa97231a1 chore(version): pre-release bump to 01.06.05-dev [skip ci] 2026-06-23 18:09:37 +00:00
gitea-actions[bot] 2291db32c5 chore(version): auto-bump patch 01.06.04-dev [skip ci] 2026-06-23 18:09:18 +00:00
Jonathan Miller 491bd3b858 chore: remove security-audit.yml -- handled by MokoGitea
Universal: Auto Version Bump / Version Bump (push) Successful in 30s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-23 13:02:32 -05:00
gitea-actions[bot] 064d5e3ab1 chore(version): pre-release bump to 01.06.03-dev [skip ci] 2026-06-23 17:57:35 +00:00
gitea-actions[bot] c512829cd4 chore(version): auto-bump patch 01.06.02-dev [skip ci] 2026-06-23 17:57:19 +00:00
Jonathan Miller 69c728cd5a chore: remove composer-publish, update-server, deploy-manual workflows
Universal: Auto Version Bump / Version Bump (push) Successful in 21s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s
2026-06-23 12:56:40 -05:00
jmiller fca6d4f25f chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-06-23 17:43:02 +00:00
jmiller b668e1d4ed chore: remove deprecated .mokogitea/workflows/composer-publish.yml [skip ci] 2026-06-23 17:37:02 +00:00
jmiller 5b760a1b74 chore: remove deprecated .mokogitea/workflows/update-server.yml [skip ci] 2026-06-23 17:36:58 +00:00
jmiller 1f8dccf898 chore: remove deprecated .mokogitea/workflows/deploy-manual.yml [skip ci] 2026-06-23 17:36:55 +00:00
gitea-actions[bot] b9d8eb3950 chore(version): pre-release bump to 01.06.01-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 8s
2026-06-23 17:35:55 +00:00
gitea-actions[bot] 053fe2d52c chore(version): auto-bump patch 01.05.04-dev [skip ci] 2026-06-23 17:35:28 +00:00
Jonathan Miller b3846fa633 chore: remove .mokogitea/manifest.xml -- metadata via API
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 36s
2026-06-23 12:34:51 -05:00
jmiller a8ef4cfb77 chore: sync workflow-sync-trigger.yml from Template-Joomla [skip ci] 2026-06-23 17:31:49 +00:00
jmiller 58317cd205 chore: sync version-set.yml from Template-Joomla [skip ci] 2026-06-23 17:11:27 +00:00
jmiller 642e2bffe7 chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-06-23 17:11:25 +00:00
jmiller d6d423b946 chore: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-06-23 17:11:24 +00:00
gitea-actions[bot] f65d261598 chore(version): pre-release bump to 01.05.03-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 6s
2026-06-23 17:09:09 +00:00
172 changed files with 5110 additions and 919 deletions
+41
View File
@@ -0,0 +1,41 @@
# EditorConfig helps maintain consistent coding styles across different editors and IDEs
# https://editorconfig.org/
root = true
# Default settings — Tabs preferred, width = 2 spaces
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
tab_width = 2
# PowerShell scripts — tabs, 2-space visual width
[*.ps1]
indent_style = tab
tab_width = 2
end_of_line = crlf
# Markdown files — keep trailing whitespace for line breaks
[*.md]
trim_trailing_whitespace = false
# JSON / YAML files — tabs, 2-space visual width
[*.{json,yml,yaml}]
indent_style = tab
tab_width = 2
# Makefiles — always tabs, default width
[Makefile]
indent_style = tab
tab_width = 2
# Windows batch scripts — keep CRLF endings
[*.{bat,cmd}]
end_of_line = crlf
# Shell scripts — ensure LF endings
[*.sh]
end_of_line = lf
+110
View File
@@ -0,0 +1,110 @@
---
name: Architecture Decision Record (ADR)
about: Propose or document an architectural decision
title: '[ADR] '
labels: 'architecture, decision'
assignees: ''
---
## ADR Number
ADR-XXXX
## Status
- [ ] Proposed
- [ ] Accepted
- [ ] Deprecated
- [ ] Superseded by ADR-XXXX
## Context
Describe the issue or problem that motivates this decision.
## Decision
State the architecture decision and provide rationale.
## Consequences
### Positive
- List positive consequences
### Negative
- List negative consequences or trade-offs
### Neutral
- List neutral aspects
## Alternatives Considered
### Alternative 1
- Description
- Pros
- Cons
- Why not chosen
### Alternative 2
- Description
- Pros
- Cons
- Why not chosen
## Implementation Plan
1. Step 1
2. Step 2
3. Step 3
## Stakeholders
- **Decision Makers**: @user1, @user2
- **Consulted**: @user3, @user4
- **Informed**: team-name
## Technical Details
### Architecture Diagram
```
[Add diagram or link]
```
### Dependencies
- Dependency 1
- Dependency 2
### Impact Analysis
- **Performance**: [Impact description]
- **Security**: [Impact description]
- **Scalability**: [Impact description]
- **Maintainability**: [Impact description]
## Testing Strategy
- [ ] Unit tests
- [ ] Integration tests
- [ ] Performance tests
- [ ] Security tests
## Documentation
- [ ] Architecture documentation updated
- [ ] API documentation updated
- [ ] Developer guide updated
- [ ] Runbook created
## Migration Path
Describe how to migrate from current state to new architecture.
## Rollback Plan
Describe how to rollback if issues occur.
## Timeline
- **Proposal Date**:
- **Decision Date**:
- **Implementation Start**:
- **Expected Completion**:
## References
- Related ADRs:
- External resources:
- RFCs:
## Review Checklist
- [ ] Aligns with enterprise architecture principles
- [ ] Security implications reviewed
- [ ] Performance implications reviewed
- [ ] Cost implications reviewed
- [ ] Compliance requirements met
- [ ] Team consensus achieved
+48
View File
@@ -0,0 +1,48 @@
---
name: Bug Report
about: Report a bug or issue with the project
title: '[BUG] '
labels: 'bug'
assignees: ''
---
## Bug Description
A clear and concise description of what the bug is.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
## Expected Behavior
A clear and concise description of what you expected to happen.
## Actual Behavior
A clear and concise description of what actually happened.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Environment
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
- **Version**: [e.g., 1.2.3]
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
- **PHP Version**: [e.g., 8.1]
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
## Additional Context
Add any other context about the problem here.
## Possible Solution
If you have suggestions on how to fix the issue, please describe them here.
## Checklist
- [ ] I have searched for similar issues before creating this one
- [ ] I have provided all the requested information
- [ ] I have tested this on the latest stable version
- [ ] I have checked the documentation and couldn't find a solution
+18
View File
@@ -0,0 +1,18 @@
---
blank_issues_enabled: true
contact_links:
- name: 💼 Enterprise Support
url: https://mokoconsulting.tech/enterprise
about: Enterprise-level support and consultation services
- name: 💬 Ask a Question
url: https://mokoconsulting.tech/
about: Get help or ask questions through our website
- name: 📚 MokoStandards Documentation
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
about: View our coding standards and best practices
- name: 🔒 Report a Security Vulnerability
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
about: Report security vulnerabilities privately (for critical issues)
- name: 💡 Community Discussions
url: https://github.com/orgs/mokoconsulting-tech/discussions
about: Join community discussions and Q&A
@@ -0,0 +1,52 @@
---
name: Documentation Issue
about: Report an issue with documentation
title: '[DOCS] '
labels: 'documentation'
assignees: ''
---
## Documentation Issue
**Location**:
<!-- Specify the file, page, or section with the issue -->
## Issue Type
<!-- Mark the relevant option with an "x" -->
- [ ] Typo or grammar error
- [ ] Outdated information
- [ ] Missing documentation
- [ ] Unclear explanation
- [ ] Broken links
- [ ] Missing examples
- [ ] Other (specify below)
## Description
<!-- Clearly describe the documentation issue -->
## Current Content
<!-- Quote or describe the current documentation (if applicable) -->
```
Current text here
```
## Suggested Improvement
<!-- Provide your suggestion for how to improve the documentation -->
```
Suggested text here
```
## Additional Context
<!-- Add any other context, screenshots, or references -->
## Standards Alignment
- [ ] Follows MokoStandards documentation guidelines
- [ ] Uses en_US/en_GB localization
- [ ] Includes proper SPDX headers where applicable
## Checklist
- [ ] I have searched for similar documentation issues
- [ ] I have provided a clear description
- [ ] I have suggested an improvement (if applicable)
@@ -0,0 +1,51 @@
---
name: Feature Request
about: Suggest a new feature or enhancement
title: '[FEATURE] '
labels: 'enhancement'
assignees: ''
---
## Feature Description
A clear and concise description of the feature you'd like to see.
## Problem or Use Case
Describe the problem this feature would solve or the use case it addresses.
Ex. I'm always frustrated when [...]
## Proposed Solution
A clear and concise description of what you want to happen.
## Alternative Solutions
A clear and concise description of any alternative solutions or features you've considered.
## Benefits
Describe how this feature would benefit users:
- Who would use this feature?
- What problems does it solve?
- What value does it add?
## Implementation Details (Optional)
If you have ideas about how this could be implemented, share them here:
- Technical approach
- Files/components that might need changes
- Any concerns or challenges you foresee
## Additional Context
Add any other context, mockups, or screenshots about the feature request here.
## Relevant Standards
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] Accessibility (WCAG 2.1 AA)
- [ ] Localization (en_US/en_GB)
- [ ] Security best practices
- [ ] Code quality standards
- [ ] Other: [specify]
## Checklist
- [ ] I have searched for similar feature requests before creating this one
- [ ] I have clearly described the use case and benefits
- [ ] I have considered alternative solutions
- [ ] This feature aligns with the project's goals and scope
+87
View File
@@ -0,0 +1,87 @@
---
name: Joomla Extension Issue
about: Report an issue with a Joomla extension
title: '[JOOMLA] '
labels: 'joomla'
assignees: ''
---
## Issue Type
- [ ] Component Issue
- [ ] Module Issue
- [ ] Plugin Issue
- [ ] Template Issue
## Extension Details
- **Extension Name**: [e.g., moko-cassiopeia]
- **Extension Version**: [e.g., 1.2.3]
- **Extension Type**: [Component / Module / Plugin / Template]
## Joomla Environment
- **Joomla Version**: [e.g., 4.4.0, 5.0.0]
- **PHP Version**: [e.g., 8.1.0]
- **Database**: [MySQL / PostgreSQL / MariaDB]
- **Database Version**: [e.g., 8.0]
- **Server**: [Apache / Nginx / IIS]
- **Hosting**: [Shared / VPS / Dedicated / Cloud]
## Issue Description
Provide a clear and detailed description of the issue.
## Steps to Reproduce
1. Go to '...'
2. Click on '...'
3. Configure '...'
4. See error
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Error Messages
```
# Paste any error messages from Joomla error logs
# Location: administrator/logs/error.php
```
## Browser Console Errors
```javascript
// Paste any JavaScript console errors (F12 in browser)
```
## Screenshots
Add screenshots to help explain the issue.
## Configuration
```ini
# Paste extension configuration (sanitize sensitive data)
```
## Installed Extensions
List other installed extensions that might conflict:
- Extension 1 (version)
- Extension 2 (version)
## Template Overrides
- [ ] Using template overrides
- [ ] Custom CSS
- [ ] Custom JavaScript
## Additional Context
- **Multilingual Site**: [Yes / No]
- **Cache Enabled**: [Yes / No]
- **Debug Mode**: [Yes / No]
- **SEF URLs**: [Yes / No]
## Checklist
- [ ] I have cleared Joomla cache
- [ ] I have disabled other extensions to test for conflicts
- [ ] I have checked Joomla error logs
- [ ] I have tested with a default Joomla template
- [ ] I have checked browser console for JavaScript errors
- [ ] I have searched for similar issues
- [ ] I am using a supported Joomla version
+82
View File
@@ -0,0 +1,82 @@
---
name: Question
about: Ask a question about usage, features, or best practices
title: '[QUESTION] '
labels: ['question']
assignees: ['jmiller']
---
## Question
**Your question:**
## Context
**What are you trying to accomplish?**
**What have you already tried?**
**Category**:
- [ ] Script usage
- [ ] Configuration
- [ ] Workflow setup
- [ ] Documentation interpretation
- [ ] Best practices
- [ ] Integration
- [ ] Other: __________
## Environment (if relevant)
**Your setup**:
- Operating System:
- Version:
## What You've Researched
**Documentation reviewed**:
- [ ] README.md
- [ ] Project documentation
- [ ] Other (specify): __________
**Similar issues/questions found**:
- #
- #
## Expected Outcome
**What result are you hoping for?**
## Code/Configuration Samples
**Relevant code or configuration** (if applicable):
```bash
# Your code here
```
## Additional Context
**Any other relevant information:**
**Screenshots** (if helpful):
## Urgency
- [ ] Urgent (blocking work)
- [ ] Normal (can work on other things meanwhile)
- [ ] Low priority (just curious)
## Checklist
- [ ] I have searched existing issues and discussions
- [ ] I have reviewed relevant documentation
- [ ] I have provided sufficient context
- [ ] I have included code/configuration samples if relevant
- [ ] This is a genuine question (not a bug report or feature request)
+126
View File
@@ -0,0 +1,126 @@
---
name: Request for Comments (RFC)
about: Propose a significant change for community discussion
title: '[RFC] '
labels: 'rfc, discussion'
assignees: ''
---
## RFC Summary
One-paragraph summary of the proposal.
## Motivation
Why are we doing this? What use cases does it support? What is the expected outcome?
## Detailed Design
### Overview
Provide a detailed explanation of the proposed change.
### API Changes (if applicable)
```php
// Before
function oldApi($param1) { }
// After
function newApi($param1, $param2) { }
```
### User Experience Changes
Describe how users will interact with this change.
### Implementation Approach
High-level implementation strategy.
## Drawbacks
Why should we *not* do this?
## Alternatives
What other designs have been considered? What is the impact of not doing this?
### Alternative 1
- Description
- Trade-offs
### Alternative 2
- Description
- Trade-offs
## Adoption Strategy
How will existing users adopt this? Is this a breaking change?
### Migration Guide
```bash
# Steps to migrate
```
### Deprecation Timeline
- **Announcement**:
- **Deprecation**:
- **Removal**:
## Unresolved Questions
- Question 1
- Question 2
## Future Possibilities
What future work does this enable?
## Impact Assessment
### Performance
Expected performance impact.
### Security
Security considerations and implications.
### Compatibility
- **Backward Compatible**: [Yes / No]
- **Breaking Changes**: [List]
### Maintenance
Long-term maintenance considerations.
## Community Input
### Stakeholders
- [ ] Core team
- [ ] Module developers
- [ ] End users
- [ ] Enterprise customers
### Feedback Period
**Duration**: [e.g., 2 weeks]
**Deadline**: [date]
## Implementation Timeline
### Phase 1: Design
- [ ] RFC discussion
- [ ] Design finalization
- [ ] Approval
### Phase 2: Implementation
- [ ] Core implementation
- [ ] Tests
- [ ] Documentation
### Phase 3: Release
- [ ] Beta release
- [ ] Feedback collection
- [ ] Stable release
## Success Metrics
How will we measure success?
- Metric 1
- Metric 2
## References
- Related RFCs:
- External documentation:
- Prior art:
## Open Questions for Community
1. Question 1?
2. Question 2?
---
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
+51
View File
@@ -0,0 +1,51 @@
---
name: Security Vulnerability Report
about: Report a security vulnerability (use only for non-critical issues)
title: '[SECURITY] '
labels: 'security'
assignees: ''
---
## ⚠️ IMPORTANT: Private Disclosure Required
**For critical security vulnerabilities, DO NOT use this template.**
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
Use this template only for:
- Security improvements
- Non-critical security suggestions
- Security documentation updates
---
## Security Issue
**Severity**:
<!-- Low, Medium, or informational only -->
## Description
<!-- Describe the security concern or improvement suggestion -->
## Affected Components
<!-- List the affected files, features, or components -->
## Suggested Mitigation
<!-- Describe how this could be addressed -->
## Standards Reference
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
- [ ] SPDX license identifiers
- [ ] Secret management
- [ ] Dependency security
- [ ] Access control
- [ ] Other: [specify]
## Additional Context
<!-- Add any other context about the security concern -->
## Checklist
- [ ] This is NOT a critical vulnerability requiring private disclosure
- [ ] I have reviewed the SECURITY.md policy
- [ ] I have provided sufficient detail for evaluation
+24
View File
@@ -0,0 +1,24 @@
---
name: Version Bump
about: Request or track a version change
title: '[VERSION] '
labels: 'version, type: version'
assignees: 'jmiller'
---
## Version Change
**Current version**: <!-- e.g., 01.02.03 -->
**Requested version**: <!-- e.g., 01.03.00 -->
**Change type**: <!-- patch / minor / major -->
## Reason
<!-- Why is this version bump needed? -->
## Checklist
- [ ] README.md `VERSION:` field updated
- [ ] CHANGELOG.md entry added
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
<identity>
<name>MokoSuiteCross</name>
<display-name>Package - MokoSuiteCross</display-name>
<org>MokoConsulting</org>
<description>Cross-posting Joomla content to social media, email marketing, and chat platforms</description>
<version>01.06.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>05.00.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/mokoplatform</standards-source>
</governance>
<build>
<language>PHP</language>
<package-type>joomla-extension</package-type>
<entry-point>source/</entry-point>
</build>
<licensing>
<enabled>true</enabled>
<dlid>true</dlid>
<update-server>https://git.mokoconsulting.tech/{org}/{repo}/updates.xml</update-server>
</licensing>
</mokoplatform>
@@ -0,0 +1,72 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
# VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
name: "Universal: CI Issue Reporter"
on:
workflow_call:
inputs:
gate:
description: "CI gate name (e.g. PR Validation, Repository Health)"
required: true
type: string
details:
description: "Human-readable failure description"
required: true
type: string
severity:
description: "error or warning"
required: false
type: string
default: "error"
workflow:
description: "Workflow name for the issue title"
required: false
type: string
default: ""
secrets:
MOKOGITEA_TOKEN:
required: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
report:
name: "Report: ${{ inputs.gate }}"
runs-on: ubuntu-latest
steps:
- name: Clone MokoCLI
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
- name: Report CI failure
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
INPUT_GATE: ${{ inputs.gate }}
INPUT_DETAILS: ${{ inputs.details }}
INPUT_SEVERITY: ${{ inputs.severity }}
INPUT_WORKFLOW: ${{ inputs.workflow }}
run: |
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
/tmp/mokocli/cli/ci_issue_reporter.sh \
--gate "$INPUT_GATE" \
--details "$INPUT_DETAILS" \
--severity "$INPUT_SEVERITY" \
--workflow "$INPUT_WORKFLOW"
+331
View File
@@ -164,6 +164,75 @@ jobs:
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Update server & packaging checks
continue-on-error: true
run: |
echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
# Find the extension manifest
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
# 1. Check <updateservers> exists and uses MokoGitea update server
if ! grep -q '<updateservers>' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Missing \`<updateservers>\` tag — extension will not receive OTA updates"
echo "- **Missing** \`<updateservers>\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
SERVER_URL=$(grep -oP '<server[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$SERVER_URL" ]; then
echo "::warning file=${MANIFEST}::\`<updateservers>\` is empty — no server URL defined"
echo "- **Empty** \`<updateservers>\` — no server URL defined" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then
echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}"
echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY
echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<updateservers>\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
# 2. Check <dlid> tag exists
if ! grep -q '<dlid' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Missing \`<dlid>\` tag — download ID authentication is not configured"
echo "- **Missing** \`<dlid>\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<dlid>\`: present ✓" >> $GITHUB_STEP_SUMMARY
fi
# 3. For packages: check <childuninstall> tag
if [ "$EXT_TYPE" = "package" ]; then
if ! grep -q '<childuninstall>' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Package is missing \`<childuninstall>\` — child extensions will not be removed on uninstall"
echo "- **Missing** \`<childuninstall>\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<childuninstall>\`: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY
else
echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check language files referenced in manifest
run: |
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
@@ -647,6 +716,268 @@ jobs:
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Script file reference check
run: |
echo "### Script File Reference" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
SCRIPT_FILE=$(grep -oP '<scriptfile>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$SCRIPT_FILE" ]; then
echo "No \`<scriptfile>\` referenced — skipping." >> $GITHUB_STEP_SUMMARY
elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then
echo "::error file=${MANIFEST}::Manifest references \`<scriptfile>${SCRIPT_FILE}</scriptfile>\` but file does not exist"
echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`<scriptfile>\` but not found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Media folder validation
run: |
echo "### Media Folder Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check <media> tag and its folder/filename children
MEDIA_DEST=$(grep -oP '<media[^>]*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
MEDIA_FOLDER=$(grep -oP '<media[^>]*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then
echo "No \`<media>\` tag found — skipping." >> $GITHUB_STEP_SUMMARY
else
if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then
echo "::error file=${MANIFEST}::\`<media folder=\"${MEDIA_FOLDER}\">\` references missing directory"
echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY
# Check child references inside <media> block
if [ -n "$MEDIA_FOLDER" ]; then
MEDIA_FOLDERS=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<folder>\K[^<]+' 2>/dev/null || true)
for F in $MEDIA_FOLDERS; do
if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
MEDIA_FILES=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<filename>\K[^<]+' 2>/dev/null || true)
for F in $MEDIA_FILES; do
if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Target platform check
continue-on-error: true
run: |
echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Check updates.xml for targetplatform if it exists
if [ -f "updates.xml" ]; then
if ! grep -q '<targetplatform' "updates.xml" 2>/dev/null; then
echo "::warning file=updates.xml::No \`<targetplatform>\` found — Joomla updater cannot filter by compatible version"
echo "- **Missing** \`<targetplatform>\` in updates.xml" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- \`<targetplatform>\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
# Check manifest for minimum PHP/Joomla version hints
if ! grep -qP '<php_minimum>|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest"
echo "- **Missing** version constraints (\`<php_minimum>\` or \`<targetplatform>\`)" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY
else
echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Changelog URL check
continue-on-error: true
run: |
echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
if ! grep -q '<changelogurl>' "$MANIFEST" 2>/dev/null; then
echo "::warning file=${MANIFEST}::Missing \`<changelogurl>\` — Joomla updater will not display changelogs"
echo "- **Missing** \`<changelogurl>\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
else
CHANGELOG_URL=$(grep -oP '<changelogurl>\K[^<]+' "$MANIFEST" | head -1)
echo "- \`<changelogurl>\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY
else
echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Duplicate file references check
continue-on-error: true
run: |
echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Extract all <filename> and <folder> references
ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true)
if [ -z "$ALL_REFS" ]; then
echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY
else
DUPES=$(echo "$ALL_REFS" | uniq -d)
if [ -n "$DUPES" ]; then
while IFS= read -r DUP; do
COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP")
echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)"
echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + 1))
done <<< "$DUPES"
else
TOTAL=$(echo "$ALL_REFS" | wc -l)
echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY
else
echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Empty language keys check
continue-on-error: true
run: |
echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$LANG_FILES" ]; then
echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY
else
TOTAL_FILES=0
for FILE in $LANG_FILES; do
TOTAL_FILES=$((TOTAL_FILES + 1))
# Find lines with KEY= but no value (empty or whitespace-only after =)
EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true)
if [ -n "$EMPTY_KEYS" ]; then
COUNT=$(echo "$EMPTY_KEYS" | wc -l)
echo "::warning file=${FILE}::${COUNT} empty language key(s)"
echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY
while IFS= read -r LINE; do
LINE_NUM=$(echo "$LINE" | cut -d: -f1)
KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1)
echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY
done <<< "$EMPTY_KEYS"
WARNINGS=$((WARNINGS + COUNT))
fi
done
if [ "$WARNINGS" -eq 0 ]; then
echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY
else
echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY
fi
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
-76
View File
@@ -1,76 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
name: "Publish to Composer"
on:
push:
tags:
- 'v*'
- '[0-9]*.[0-9]*.[0-9]*'
release:
types: [published]
workflow_dispatch:
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
publish:
name: Publish Package
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip publish]')
steps:
- name: Checkout
uses: actions/checkout@v4
- 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: Install dependencies
run: composer install --no-dev --no-interaction --prefer-dist --quiet
- name: Determine version
id: version
run: |
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Package version: ${VERSION}"
# Gitea Composer Registry — auto-publishes from tags
# The tag push itself registers the package at:
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
- name: Verify Gitea registry
run: |
echo "Gitea Composer registry auto-publishes from tags."
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
echo "Install: composer require mokoconsulting/mokocli"
# Packagist — notify of new version
- name: Notify Packagist
if: secrets.PACKAGIST_TOKEN != ''
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Notifying Packagist of version ${VERSION}..."
curl -sf -X POST \
-H "Content-Type: application/json" \
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
&& echo "Packagist notified" \
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
- name: Summary
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.06.00
# VERSION: 01.08.52
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+16
View File
@@ -88,8 +88,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
@@ -166,6 +178,7 @@ jobs:
- name: Create release
id: release
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
@@ -176,6 +189,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 }}"
@@ -212,6 +226,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 }}"
@@ -225,6 +240,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}"
-82
View File
@@ -1,82 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
-312
View File
@@ -1,312 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/update-server.yml
# VERSION: 05.00.00
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
#
# Thin wrapper around moko-platform CLI tools.
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
#
# Joomla filters update entries by the user's "Minimum Stability" setting.
name: "Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_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 }}
permissions:
contents: write
jobs:
update-xml:
name: Update Server
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /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
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: |
BRANCH="${{ github.ref_name }}"
# Configure git for bot pushes
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Auto-bump patch version
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Strip any existing suffix before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
else
STABILITY="development"
fi
# Version suffix per stability stream
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
*) SUFFIX=""; TAG="stable" ;;
esac
# Propagate version with stability suffix to all manifest files
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Re-read version (now includes suffix from version_set_platform)
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Commit version bump if changed
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
# Build package and upload
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
python3 -c "
import base64, json, urllib.request, sys
with open('updates.xml', 'rb') as f:
content = base64.b64encode(f.read()).decode()
payload = json.dumps({
'content': content,
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main'
}).encode()
req = urllib.request.Request(
'${API_BASE}/contents/updates.xml',
data=payload, method='PUT',
headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# Permission check: admin or maintain role required
ACTOR="${{ github.actor }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${{ steps.meta.outputs.display_version }}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
+130
View File
@@ -0,0 +1,130 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
# VERSION: 01.00.00
# BRIEF: Set or reset the extension version across all version-bearing files
name: "Joomla: Set Version"
on:
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 01.00.00)"
required: true
type: string
branch:
description: "Branch to update (default: current)"
required: false
type: string
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
VERSION="${{ inputs.version }}"
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
exit 1
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Update manifest version
run: |
MANIFEST=""
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla extension manifest found — skipping manifest update"
else
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
fi
- name: Update README.md version
run: |
if [ -f "README.md" ]; then
if grep -qP '^\s*VERSION:\s*\d' README.md; then
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
echo "README.md version updated to ${VERSION}"
else
echo "::warning::No VERSION line found in README.md — skipping"
fi
fi
- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
# Check if this version already has an entry
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
else
# Insert new version entry after [Unreleased] or at the top after header
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
else
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
fi
echo "CHANGELOG.md: added entry for ${VERSION}"
fi
else
echo "::warning::No CHANGELOG.md found — skipping"
fi
- name: Update FILE INFORMATION blocks
run: |
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
while IFS= read -r -d '' FILE; do
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
echo "Updated FILE INFORMATION VERSION in ${FILE}"
fi
done
- name: Commit and push
run: |
git config user.name "Moko Consulting [bot]"
git config user.email "hello@mokoconsulting.tech"
git add -A
if git diff --cached --quiet; then
echo "No version changes detected — nothing to commit"
else
git commit -m "chore: set version to ${VERSION} [skip bump]
Authored-by: Moko Consulting"
git push
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
fi
+61 -2
View File
@@ -1,9 +1,52 @@
# Changelog
## [Unreleased]
## [01.06.00] --- 2026-06-23
### Added
- **Visual post calendar**: FullCalendar-powered admin view with month/week/list modes (#160)
- **Post calendar**: color-coded events by status (posted/scheduled/queued/failed)
- **Post calendar**: drag-drop rescheduling with automatic status update
- **AI caption generation**: Generate platform-optimized cross-post captions from article content using Claude or OpenAI (#161)
- **AI provider config**: New "AI Caption Generation" fieldset in component options with provider, API key, model, and tone settings
- **AI Generate button**: One-click AI generation button in the Share Content panel that fills all caption fields
- **X/Twitter threads**: Auto-split messages exceeding 280 chars into reply chains at sentence boundaries
- **X/Twitter cost-optimized posting**: Optional mode to post text-only tweet first ($0.015) with URL as separate reply ($0.20)
- **X/Twitter cost warning**: Language string documenting X API pricing for text vs URL posts
- **Instagram carousel**: Multi-image/video posts via Meta carousel container flow (up to 10 items)
- **Instagram Reels**: Short-form video publishing via REELS media type
- **Instagram Stories**: Image and video story publishing via STORIES media type
- **Instagram alt text**: Alt text support for image containers
- **Nostr plugin**: Full NIP-01 WebSocket relay publishing with BIP-340 Schnorr signatures (pure PHP, requires ext-gmp)
- **Nostr**: Publishes kind-1 text note events to multiple relays with automatic failover
- **Nostr**: Raw WebSocket client using stream_socket_client (no external dependencies)
- **Nostr**: Public key derivation and event signing via secp256k1 elliptic curve math
- **Threads carousel**: Support up to 20-item carousel posts via Threads API multi-container flow
- **Threads polls**: Poll creation support via poll_options parameter (2-4 options)
- **Threads spoiler tags**: Content warning / spoiler flag support for Threads posts
- **Threads text-only optimization**: Simplified single-step flow for text-only posts without media
- **Facebook Reels**: Publish video Reels via Graph API video_reels endpoint (#162)
- **Facebook Stories**: Publish image and video Stories via photo_stories/video_stories endpoints (#162)
- **Facebook scheduled posts**: Schedule feed posts with scheduled_publish_time parameter (#162)
- **Facebook draft posts**: Save feed posts as unpublished drafts (#162)
- **TikTok video upload**: PULL_FROM_URL video publishing via video/init endpoint with status polling (#164)
- **TikTok photo carousel**: Up to 35 image carousel posts via content/init endpoint (#164)
- **TikTok posting mode**: Configurable DIRECT_POST or MEDIA_UPLOAD (sends to TikTok inbox for in-app editing) (#164)
- **TikTok audit warning**: Language string explaining that unverified apps can only create private posts (#164)
## [01.06.00] --- 2026-06-23
### Fixed
- Webservices plugin Joomla 6 compatibility — `onBeforeApiRoute` receives `BeforeApiRouteEvent` object, extract router via `$event->getRouter()`
## [01.07.00] --- 2026-06-23
## [01.07.00] --- 2026-06-23
### Added
- **Full ACL system**: 12 granular permissions in access.xml with permissions fieldset in config.xml
- **ACL enforcement**: All controllers and views check permissions before allowing actions
- **MokoSuiteCrossHelper::getActions()**: Centralized ACL helper for toolbar and view logic
### Fixed
- **License warning**: Removed duplicate from system plugin (install script already shows it)
- **Content plugin**: Fixed func_get_arg crash when non-article content is saved (e.g. update sites, installer)
## [01.05.00] --- 2026-06-23
@@ -45,3 +88,19 @@
## [01.04.01] --- 2026-06-21
## [01.04.00] --- 2026-06-21
### Fixed
- **Package manifest**: Added missing `plg_system_mokosuitecross_events` and `plg_system_mokosuitecross_gallery` to `pkg_mokosuitecross.xml` — these system plugins were not installed with the package
- **Cleanup**: Removed old `src/` directory (pre-rename cruft with `mokojoomcross` files)
## [01.03.00] --- 2026-06-21
<!-- VERSION: 01.08.52 -->
All notable changes to MokoSuiteCross will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+46
View File
@@ -0,0 +1,46 @@
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License (./LICENSE).
# FILE INFORMATION
DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
VERSION: 01.08.52
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Community expectations and enforcement guidelines
NOTE: Adapted with attribution from the Contributor Covenant v2.1
-->
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
## Our Standards
- Be empathetic and kind
- Be respectful of differing opinions
- Accept constructive feedback
- Own mistakes and learn from them
Unacceptable behavior includes sexualized language/imagery, trolling, harassment, doxing, and other inappropriate conduct.
## Enforcement
Report incidents to **hello@mokoconsulting.tech** or through GitHub Discussions if you prefer a community-visible approach. Private complaints will be reviewed promptly and fairly.
## Enforcement Guidelines
1. **Correction** — Private warning
2. **Warning** — Formal warning and limited interaction
3. **Temporary Ban** — Time-boxed exclusion
4. **Permanent Ban** — Removal from the community
## Attribution
Adapted from the Contributor Covenant v2.1.
+1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
# MokoSuiteCross
<!-- VERSION: 01.06.00 -->
<!-- VERSION: 01.08.52 -->
Cross-posting Joomla content to social media, email marketing, and chat platforms for Joomla 5/6.
+241
View File
@@ -0,0 +1,241 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 01.08.52
BRIEF: Security vulnerability reporting and handling policy
-->
# Security Policy
## Purpose and Scope
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
## Supported Versions
Security updates are provided for the following versions:
| Version | Supported |
| ------- | ------------------ |
| 01.x.x | :white_check_mark: |
| < 01.0 | :x: |
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
## Reporting a Vulnerability
### Where to Report
**DO NOT** create public GitHub issues for security vulnerabilities.
Report security vulnerabilities privately to:
**Email**: `security@mokoconsulting.tech`
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
### What to Include
A complete vulnerability report should include:
1. **Description**: Clear explanation of the vulnerability
2. **Impact**: Potential security impact and severity assessment
3. **Affected Versions**: Which versions are vulnerable
4. **Reproduction Steps**: Detailed steps to reproduce the issue
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
6. **Suggested Fix**: Proposed remediation (if known)
7. **Disclosure Timeline**: Your expectations for public disclosure
### Response Timeline
* **Initial Response**: Within 3 business days
* **Assessment Complete**: Within 7 business days
* **Fix Timeline**: Depends on severity (see below)
* **Disclosure**: Coordinated with reporter
## Severity Classification
Vulnerabilities are classified using the following severity levels:
### Critical
* Remote code execution
* Authentication bypass
* Data breach or exposure of sensitive information
* **Fix Timeline**: 7 days
### High
* Privilege escalation
* SQL injection or command injection
* Cross-site scripting (XSS) with significant impact
* **Fix Timeline**: 14 days
### Medium
* Information disclosure (limited scope)
* Denial of service
* Security misconfigurations with moderate impact
* **Fix Timeline**: 30 days
### Low
* Security best practice violations
* Minor information leaks
* Issues requiring user interaction or complex preconditions
* **Fix Timeline**: 60 days or next release
## Remediation Process
1. **Acknowledgment**: Security team confirms receipt and begins investigation
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
3. **Development**: Security patch is developed and tested
4. **Review**: Patch undergoes security review and validation
5. **Release**: Fixed version is released with security advisory
6. **Disclosure**: Public disclosure follows coordinated timeline
## Security Advisories
Security advisories are published via:
* GitHub Security Advisories
* Release notes and CHANGELOG.md
* Email notification to project users (if mailing list is established)
Advisories include:
* CVE identifier (if applicable)
* Severity rating
* Affected versions
* Fixed versions
* Mitigation steps
* Attribution (with reporter consent)
## Security Best Practices
For projects using this template:
### Required Controls
* Enable GitHub security features (Dependabot, code scanning)
* Implement branch protection on `main`
* Require code review for all changes
* Enforce signed commits (recommended)
* Use secrets management (never commit credentials)
* Maintain security documentation
* Follow secure coding standards defined in MokoStandards
### Joomla Plugin Security
* Follow Joomla security best practices
* Validate and sanitize all user input
* Use Joomla's database API to prevent SQL injection
* Properly escape output to prevent XSS
* Implement proper access control checks
* Use Joomla's session and authentication APIs
* Keep Joomla and dependencies up to date
### CI/CD Security
* Validate all inputs
* Sanitize outputs
* Use least privilege access
* Pin dependencies with hash verification
* Scan for vulnerabilities in dependencies
* Audit third-party actions and tools
#### Automated Security Scanning
All repositories SHOULD implement:
**CodeQL Analysis**:
* Enabled for PHP and other supported languages
* Runs on: push to main, pull requests, weekly schedule
* Query sets: `security-extended` and `security-and-quality`
* Configuration: `.github/workflows/codeql-analysis.yml`
**Dependabot Security Updates**:
* Weekly scans for vulnerable dependencies
* Automated pull requests for security patches
* Configuration: `.github/dependabot.yml`
**Secret Scanning**:
* Enabled by default with push protection
* Prevents accidental credential commits
### Dependency Management
* Keep dependencies up to date
* Monitor security advisories for dependencies
* Remove unused dependencies
* Audit new dependencies before adoption
* Document security-critical dependencies
## Compliance and Governance
This security policy is aligned with MokoStandards. Deviations require documented justification.
Security policies are reviewed and updated at least annually or following significant security incidents.
## Attribution and Recognition
We acknowledge and appreciate responsible disclosure. With your permission, we will:
* Credit you in security advisories
* List you in CHANGELOG.md for the fix release
* Recognize your contribution publicly (if desired)
## Contact and Escalation
* **Security Team**: security@mokoconsulting.tech
* **Primary Contact**: hello@mokoconsulting.tech
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
## Out of Scope
The following are explicitly out of scope:
* Issues in third-party dependencies (report directly to maintainers)
* Social engineering attacks
* Physical security issues
* Denial of service via resource exhaustion without amplification
* Issues requiring physical access to systems
* Theoretical vulnerabilities without proof of exploitability
---
## Metadata
| Field | Value |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| Document | Security Policy |
| Path | /SECURITY.md |
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
| Owner | Moko Consulting |
| Scope | Security vulnerability handling |
| Status | Active |
| Effective | 2026-01-16 |
## Revision History
| Date | Change Description | Author |
| ---------- | ------------------------------------------------- | --------------- |
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
+15 -1
View File
@@ -15,9 +15,23 @@
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.7",
"phpstan/phpstan": "^1.10",
"joomla/coding-standards": "^4.0"
"joomla/coding-standards": "dev-3.x-dev"
},
"autoload": {
"psr-4": {
"Joomla\\Component\\MokoSuiteCross\\Administrator\\": "source/packages/com_mokosuitecross/src/",
"Joomla\\Component\\MokoSuiteCross\\Site\\": "source/packages/com_mokosuitecross/site/src/",
"Joomla\\Plugin\\Content\\MokoSuiteCross\\": "source/packages/plg_content_mokosuitecross/src/",
"Joomla\\Plugin\\System\\MokoSuiteCross\\": "source/packages/plg_system_mokosuitecross/src/"
}
},
"autoload-dev": {
"psr-4": {
"MokoSuiteCross\\Tests\\": "tests/"
}
},
"config": {
"sort-packages": true
+32
View File
@@ -0,0 +1,32 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# PHPStan configuration for Joomla extension repositories.
# Extends the base MokoStandards config and adds Joomla framework class stubs
# so PHPStan can resolve Factory, CMSApplication, User, Table, etc.
# without requiring a full Joomla installation.
parameters:
level: 5
paths:
- src
excludePaths:
- vendor
- node_modules
# Joomla framework stubs — resolved via the enterprise package from vendor/
stubFiles:
- vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php
# Suppress errors that are structural in Joomla's service-container architecture
ignoreErrors:
# Joomla's service-based dependency injection returns mixed from getApplication()
- '#Cannot call method .+ on Joomla\\CMS\\Application\\CMSApplication\|null#'
# Factory::getX() patterns are safe at runtime even when nullable in stubs
- '#Call to static method [a-zA-Z]+\(\) on an interface#'
reportUnmatchedIgnoredErrors: false
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>source/packages/com_mokosuitecross/src</directory>
<directory>source/packages/plg_content_mokosuitecross/src</directory>
<directory>source/packages/plg_system_mokosuitecross/src</directory>
</include>
</source>
</phpunit>
@@ -3,6 +3,6 @@
; License: GPL-3.0-or-later
PKG_MOKOSUITECROSS="MokoSuiteCross"
PKG_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms. Automatically publish articles to Facebook, X/Twitter, LinkedIn, Mastodon, Bluesky, Mailchimp, Telegram, Discord, and Slack."
PKG_MOKOSUITECROSS_DESCRIPTION="Cross-post Joomla articles to 38 platforms including Facebook, Instagram, X/Twitter, LinkedIn, Threads, Mastodon, Bluesky, Nostr, TikTok, YouTube, Pinterest, Reddit, Medium, Telegram, Discord, Slack, Teams, Mailchimp, SendGrid, Brevo, and more. Features scheduled posting, template placeholders, UTM tagging, link shortening, caption rotation, and per-article service selection."
PKG_MOKOSUITECROSS_PHP_VERSION_ERROR="MokoSuiteCross requires PHP %s or later."
PKG_MOKOSUITECROSS_MIGRATION_DETECTED="Perfect Publisher Pro detected! Navigate to Components → MokoSuiteCross → Dashboard to migrate your settings."
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<access component="com_mokosuitecross">
<section name="component">
<!-- Joomla core actions -->
<action name="core.admin" title="JACTION_ADMIN" />
<action name="core.options" title="JACTION_OPTIONS" />
<action name="core.manage" title="JACTION_MANAGE" />
@@ -8,7 +9,18 @@
<action name="core.delete" title="JACTION_DELETE" />
<action name="core.edit" title="JACTION_EDIT" />
<action name="core.edit.state" title="JACTION_EDITSTATE" />
<!-- Component-specific actions -->
<action name="mokosuitecross.crosspost" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST" />
<action name="mokosuitecross.crosspost.manual" title="COM_MOKOSUITECROSS_ACTION_CROSSPOST_MANUAL" />
<action name="mokosuitecross.delete.remote" title="COM_MOKOSUITECROSS_ACTION_DELETE_REMOTE" />
<action name="mokosuitecross.services.manage" title="COM_MOKOSUITECROSS_ACTION_SERVICES_MANAGE" />
<action name="mokosuitecross.services.credentials" title="COM_MOKOSUITECROSS_ACTION_SERVICES_CREDENTIALS" />
<action name="mokosuitecross.templates.manage" title="COM_MOKOSUITECROSS_ACTION_TEMPLATES_MANAGE" />
<action name="mokosuitecross.logs.view" title="COM_MOKOSUITECROSS_ACTION_LOGS_VIEW" />
<action name="mokosuitecross.logs.purge" title="COM_MOKOSUITECROSS_ACTION_LOGS_PURGE" />
<action name="mokosuitecross.queue.manage" title="COM_MOKOSUITECROSS_ACTION_QUEUE_MANAGE" />
<action name="mokosuitecross.queue.export" title="COM_MOKOSUITECROSS_ACTION_QUEUE_EXPORT" />
<action name="mokosuitecross.dispatch" title="COM_MOKOSUITECROSS_ACTION_DISPATCH" />
<action name="mokosuitecross.migrate" title="COM_MOKOSUITECROSS_ACTION_MIGRATE" />
</section>
</access>
@@ -120,6 +120,42 @@
/>
</fieldset>
<fieldset name="link_shortening" label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENING">
<field
name="link_shortener"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_DESC"
default="none">
<option value="none">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_NONE</option>
<option value="bitly">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_BITLY</option>
<option value="rebrandly">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_REBRANDLY</option>
<option value="yourls">COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS</option>
</field>
<field
name="link_shortener_api_key"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY_DESC"
showon="link_shortener:bitly,rebrandly"
/>
<field
name="link_shortener_yourls_url"
type="url"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL_DESC"
hint="https://short.example.com/yourls-api.php"
showon="link_shortener:yourls"
/>
<field
name="link_shortener_yourls_token"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN"
description="COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN_DESC"
showon="link_shortener:yourls"
/>
</fieldset>
<fieldset name="evergreen" label="COM_MOKOSUITECROSS_CONFIG_EVERGREEN">
<field
name="evergreen_enabled"
@@ -191,6 +227,45 @@
/>
</fieldset>
<fieldset name="ai" label="COM_MOKOSUITECROSS_CONFIG_AI">
<field
name="ai_provider"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER"
description="COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_DESC"
default="none">
<option value="none">COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_NONE</option>
<option value="claude">COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_CLAUDE</option>
<option value="openai">COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_OPENAI</option>
</field>
<field
name="ai_api_key"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_AI_API_KEY"
description="COM_MOKOSUITECROSS_CONFIG_AI_API_KEY_DESC"
showon="ai_provider:claude,openai"
/>
<field
name="ai_model"
type="text"
label="COM_MOKOSUITECROSS_CONFIG_AI_MODEL"
description="COM_MOKOSUITECROSS_CONFIG_AI_MODEL_DESC"
hint="claude-haiku-4-5 / gpt-4o-mini"
showon="ai_provider:claude,openai"
/>
<field
name="ai_tone"
type="list"
label="COM_MOKOSUITECROSS_CONFIG_AI_TONE"
description="COM_MOKOSUITECROSS_CONFIG_AI_TONE_DESC"
default="professional"
showon="ai_provider:claude,openai">
<option value="professional">COM_MOKOSUITECROSS_CONFIG_AI_TONE_PROFESSIONAL</option>
<option value="friendly">COM_MOKOSUITECROSS_CONFIG_AI_TONE_FRIENDLY</option>
<option value="casual">COM_MOKOSUITECROSS_CONFIG_AI_TONE_CASUAL</option>
</field>
</fieldset>
<fieldset name="category_rules" label="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES">
<field
name="category_rules_note"
@@ -199,4 +274,19 @@
description="COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC"
/>
</fieldset>
<fieldset
name="permissions"
label="JCONFIG_PERMISSIONS_LABEL"
description="JCONFIG_PERMISSIONS_DESC">
<field
name="rules"
type="rules"
label="JCONFIG_PERMISSIONS_LABEL"
component="com_mokosuitecross"
filter="rules"
validate="rules"
section="component"
/>
</fieldset>
</config>
@@ -5,6 +5,20 @@
COM_MOKOSUITECROSS="MokoSuiteCross"
COM_MOKOSUITECROSS_DESCRIPTION="Cross-posting Joomla content to social media, email marketing, and chat platforms"
; ACL Actions
COM_MOKOSUITECROSS_ACTION_CROSSPOST="Cross-Post Articles"
COM_MOKOSUITECROSS_ACTION_CROSSPOST_MANUAL="Manually Create Posts"
COM_MOKOSUITECROSS_ACTION_DELETE_REMOTE="Delete from Remote Platforms"
COM_MOKOSUITECROSS_ACTION_SERVICES_MANAGE="Manage Services"
COM_MOKOSUITECROSS_ACTION_SERVICES_CREDENTIALS="View Service Credentials"
COM_MOKOSUITECROSS_ACTION_TEMPLATES_MANAGE="Manage Templates"
COM_MOKOSUITECROSS_ACTION_LOGS_VIEW="View Activity Logs"
COM_MOKOSUITECROSS_ACTION_LOGS_PURGE="Purge Activity Logs"
COM_MOKOSUITECROSS_ACTION_QUEUE_MANAGE="Manage Post Queue"
COM_MOKOSUITECROSS_ACTION_QUEUE_EXPORT="Export Post Queue"
COM_MOKOSUITECROSS_ACTION_DISPATCH="Trigger API Dispatch"
COM_MOKOSUITECROSS_ACTION_MIGRATE="Run Migration"
; Submenu
COM_MOKOSUITECROSS_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOSUITECROSS_SUBMENU_POSTS="Post Queue"
@@ -520,7 +534,58 @@ COM_MOKOSUITECROSS_DISPATCH_INVALID_SERVICES="service_ids must be a non-empty ar
COM_MOKOSUITECROSS_DISPATCH_ARTICLE_NOT_FOUND="Article not found."
COM_MOKOSUITECROSS_DISPATCH_NO_SERVICES="No enabled services found matching the request."
; Link Shortening
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENING="Link Shortening"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER="Link Shortener"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_DESC="Select a link shortening service. Shortened URLs are available via the {url_short} placeholder in templates."
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_NONE="None (disabled)"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_BITLY="Bitly"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_REBRANDLY="Rebrandly"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS="YOURLS (self-hosted)"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY="API Key"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_API_KEY_DESC="API key for Bitly or Rebrandly."
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL="YOURLS API URL"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_URL_DESC="Full URL to your YOURLS API endpoint (e.g. https://short.example.com/yourls-api.php)."
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN="YOURLS Signature Token"
COM_MOKOSUITECROSS_CONFIG_LINK_SHORTENER_YOURLS_TOKEN_DESC="Secret signature token from your YOURLS installation."
; AI Caption Generation
COM_MOKOSUITECROSS_CONFIG_AI="AI Caption Generation"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER="AI Provider"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_DESC="Select an AI provider to generate cross-post captions from article content. The API key is stored in Joomla component params (encrypted at rest)."
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_NONE="None (disabled)"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_CLAUDE="Anthropic Claude"
COM_MOKOSUITECROSS_CONFIG_AI_PROVIDER_OPENAI="OpenAI"
COM_MOKOSUITECROSS_CONFIG_AI_API_KEY="API Key"
COM_MOKOSUITECROSS_CONFIG_AI_API_KEY_DESC="API key for the selected AI provider."
COM_MOKOSUITECROSS_CONFIG_AI_MODEL="Model"
COM_MOKOSUITECROSS_CONFIG_AI_MODEL_DESC="AI model to use. Leave blank for the default (Claude Haiku 4.5 or GPT-4o Mini)."
COM_MOKOSUITECROSS_CONFIG_AI_TONE="Tone"
COM_MOKOSUITECROSS_CONFIG_AI_TONE_DESC="The writing tone for generated captions."
COM_MOKOSUITECROSS_CONFIG_AI_TONE_PROFESSIONAL="Professional"
COM_MOKOSUITECROSS_CONFIG_AI_TONE_FRIENDLY="Friendly"
COM_MOKOSUITECROSS_CONFIG_AI_TONE_CASUAL="Casual"
COM_MOKOSUITECROSS_AI_GENERATE="Generate with AI"
COM_MOKOSUITECROSS_AI_GENERATE_DESC="Generate platform-optimized captions from the article content using AI."
COM_MOKOSUITECROSS_AI_GENERATING="Generating captions..."
COM_MOKOSUITECROSS_AI_GENERATED="AI captions generated successfully."
COM_MOKOSUITECROSS_AI_ERROR="AI generation failed: %s"
COM_MOKOSUITECROSS_AI_NOT_CONFIGURED="AI is not configured. Go to Options to set up a provider and API key."
; Category Rules
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES="Category Rules"
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE="Category Routing"
COM_MOKOSUITECROSS_CONFIG_CATEGORY_RULES_NOTE_DESC="Category routing rules let you map Joomla categories to specific cross-post services. When rules exist for a category, only those services receive posts. When no rules exist, all services are used (default behaviour). Rules are managed in the database table #__mokosuitecross_category_rules. A full admin UI will be added in a future release."
; Post Calendar
COM_MOKOSUITECROSS_CALENDAR="Post Calendar"
COM_MOKOSUITECROSS_CALENDAR_DESC="Visual calendar of scheduled and posted content"
COM_MOKOSUITECROSS_SUBMENU_CALENDAR="Calendar"
COM_MOKOSUITECROSS_CALENDAR_TODAY="Today"
COM_MOKOSUITECROSS_CALENDAR_MONTH="Month"
COM_MOKOSUITECROSS_CALENDAR_WEEK="Week"
COM_MOKOSUITECROSS_CALENDAR_LIST="List"
COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_SUCCESS="Post rescheduled successfully"
COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR="Failed to reschedule post"
COM_MOKOSUITECROSS_CALENDAR_CANNOT_RESCHEDULE="Only scheduled or queued posts can be rescheduled"
COM_MOKOSUITECROSS_CALENDAR_LOAD_ERROR="Failed to load calendar events"
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="component" method="upgrade">
<name>com_mokosuitecross</name>
<version>01.06.00</version>
<version>01.08.52</version>
<creationDate>2026-05-28</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,5 +1,14 @@
; MokoSuiteCross Site Frontend Language File
; MokoSuiteCross -- Site Frontend Language File
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOSUITECROSS="MokoSuiteCross"
COM_MOKOSUITECROSS_POSTS_LIST_TITLE="Cross-Posted Content"
COM_MOKOSUITECROSS_POST_DETAIL_TITLE="Cross-Post History"
COM_MOKOSUITECROSS_COLUMN_ARTICLE="Article"
COM_MOKOSUITECROSS_COLUMN_PLATFORMS="Platforms"
COM_MOKOSUITECROSS_COLUMN_LAST_POSTED="Last Posted"
COM_MOKOSUITECROSS_COLUMN_STATUS="Status"
COM_MOKOSUITECROSS_COLUMN_POSTED_DATE="Posted Date"
COM_MOKOSUITECROSS_COLUMN_LINK="Platform Link"
COM_MOKOSUITECROSS_NO_POSTS="No cross-posted content found."
@@ -17,5 +17,5 @@ use Joomla\CMS\MVC\Controller\BaseController;
class DisplayController extends BaseController
{
protected $default_view = 'post';
protected $default_view = 'posts';
}
@@ -0,0 +1,64 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Site\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class PostModel extends BaseDatabaseModel
{
public function getArticle(int $articleId): ?object
{
$db = $this->getDatabase();
$user = Factory::getApplication()->getIdentity();
$query = $db->getQuery(true)
->select('a.id, a.title, a.alias, a.catid, a.access')
->from($db->quoteName('#__content', 'a'))
->where('a.id = ' . (int) $articleId)
->where('a.state = 1');
$groups = $user->getAuthorisedViewLevels();
$query->where('a.access IN (' . implode(',', array_map('intval', $groups)) . ')');
$db->setQuery($query);
return $db->loadObject() ?: null;
}
public function getPosts(int $articleId): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
'p.id',
'p.status',
'p.platform_post_id',
'p.posted_at',
'p.error_message',
'p.created',
's.title AS service_title',
's.service_type',
])
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON s.id = p.service_id')
->where('p.article_id = ' . (int) $articleId)
->order('p.created DESC');
$db->setQuery($query, 0, 50);
return $db->loadObjectList() ?: [];
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Site\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\ListModel;
class PostsModel extends ListModel
{
protected function getListQuery()
{
$db = $this->getDatabase();
$user = Factory::getApplication()->getIdentity();
$query = $db->getQuery(true);
$query->select([
'a.id AS article_id',
'a.title AS article_title',
'a.alias AS article_alias',
'a.catid',
'MAX(p.posted_at) AS last_posted',
'COUNT(p.id) AS post_count',
'GROUP_CONCAT(DISTINCT s.service_type ORDER BY s.service_type SEPARATOR \',\') AS service_types',
])
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
->join('INNER', $db->quoteName('#__content', 'a') . ' ON a.id = p.article_id')
->join('INNER', $db->quoteName('#__mokosuitecross_services', 's') . ' ON s.id = p.service_id')
->where('p.status = ' . $db->quote('posted'))
->where('a.state = 1');
// Access filtering
$groups = $user->getAuthorisedViewLevels();
$query->where('a.access IN (' . implode(',', array_map('intval', $groups)) . ')');
$query->group('a.id, a.title, a.alias, a.catid')
->order('last_posted DESC');
return $query;
}
}
@@ -0,0 +1,33 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Site\View\Post;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $article;
protected $posts;
public function display($tpl = null): void
{
$articleId = Factory::getApplication()->getInput()->getInt('id', 0);
$model = $this->getModel();
$this->article = $model->getArticle($articleId);
$this->posts = $model->getPosts($articleId);
parent::display($tpl);
}
}
@@ -0,0 +1,30 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Site\View\Posts;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $items;
protected $pagination;
public function display($tpl = null): void
{
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
parent::display($tpl);
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$statusClasses = [
'posted' => 'bg-success',
'failed' => 'bg-danger',
'permanently_failed' => 'bg-danger',
'queued' => 'bg-warning text-dark',
'posting' => 'bg-info',
'scheduled' => 'bg-primary',
'deleted' => 'bg-secondary',
'cancelled' => 'bg-secondary',
];
?>
<div class="com-mokosuitecross-post">
<?php if (!$this->article) : ?>
<div class="alert alert-warning">
<?php echo Text::_('COM_MOKOSUITECROSS_NO_POSTS'); ?>
</div>
<?php else : ?>
<h2><?php echo Text::_('COM_MOKOSUITECROSS_POST_DETAIL_TITLE'); ?></h2>
<p>
<strong><?php echo $this->escape($this->article->title); ?></strong>
</p>
<?php if (empty($this->posts)) : ?>
<div class="alert alert-info">
<?php echo Text::_('COM_MOKOSUITECROSS_NO_POSTS'); ?>
</div>
<?php else : ?>
<table class="table table-striped">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOSUITECROSS_HEADING_SERVICE'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_STATUS'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_POSTED_DATE'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_LINK'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->posts as $post) : ?>
<tr>
<td>
<span class="badge bg-secondary"><?php echo $this->escape($post->service_type); ?></span>
<?php echo $this->escape($post->service_title); ?>
</td>
<td>
<span class="badge <?php echo $statusClasses[$post->status] ?? 'bg-secondary'; ?>">
<?php echo $this->escape(ucfirst($post->status)); ?>
</span>
</td>
<td><?php echo $post->posted_at ? $this->escape($post->posted_at) : $this->escape($post->created); ?></td>
<td>
<?php if (!empty($post->platform_post_id)) : ?>
<span class="text-muted small"><?php echo $this->escape($post->platform_post_id); ?></span>
<?php else : ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitecross&view=posts'); ?>" class="btn btn-secondary">
&larr; <?php echo Text::_('COM_MOKOSUITECROSS_POSTS_LIST_TITLE'); ?>
</a>
<?php endif; ?>
</div>
@@ -0,0 +1,66 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
?>
<div class="com-mokosuitecross-posts">
<h2><?php echo Text::_('COM_MOKOSUITECROSS_POSTS_LIST_TITLE'); ?></h2>
<?php if (empty($this->items)) : ?>
<div class="alert alert-info">
<?php echo Text::_('COM_MOKOSUITECROSS_NO_POSTS'); ?>
</div>
<?php else : ?>
<table class="table table-striped">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_ARTICLE'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_PLATFORMS'); ?></th>
<th><?php echo Text::_('COM_MOKOSUITECROSS_COLUMN_LAST_POSTED'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $item) : ?>
<tr>
<td>
<a href="<?php echo Route::_('index.php?option=com_mokosuitecross&view=post&id=' . (int) $item->article_id); ?>">
<?php echo $this->escape($item->article_title); ?>
</a>
</td>
<td>
<?php
$types = explode(',', $item->service_types ?? '');
foreach ($types as $type) :
$type = trim($type);
if (empty($type)) continue;
?>
<span class="badge bg-secondary"><?php echo $this->escape($type); ?></span>
<?php endforeach; ?>
</td>
<td>
<?php echo $item->last_posted ? $this->escape($item->last_posted) : '—'; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ($this->pagination->pagesTotal > 1) : ?>
<div class="com-mokosuitecross-posts__pagination">
<?php echo $this->pagination->getListFooter(); ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
@@ -0,0 +1 @@
/* 01.08.05 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.07 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.08 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.09 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.10 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.11 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.12 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.13 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.14 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.15 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.16 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.17 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.19 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.20 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.21 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.22 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.23 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.24 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.25 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.26 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.27 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.28 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.29 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.30 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.31 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.32 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.33 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.34 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.35 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.36 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.37 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.38 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.39 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.40 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.41 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.43 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.44 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.45 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.46 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.47 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.49 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.50 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.51 — no schema changes */
@@ -0,0 +1 @@
/* 01.08.52 — no schema changes */
@@ -0,0 +1,100 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\AiGeneratorHelper;
class AiController extends BaseController
{
public function generate(): void
{
if (!Session::checkToken('get')) {
echo json_encode(['success' => false, 'error' => 'Invalid token']);
$this->app->close();
return;
}
$user = $this->app->getIdentity();
if (!$user->authorise('core.edit', 'com_mokosuitecross')) {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
$this->app->close();
return;
}
$articleId = $this->input->getInt('article_id', 0);
if ($articleId < 1) {
echo json_encode(['success' => false, 'error' => 'Missing article ID']);
$this->app->close();
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'title', 'introtext', 'catid']))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $articleId);
$db->setQuery($query);
$article = $db->loadObject();
if (!$article) {
echo json_encode(['success' => false, 'error' => 'Article not found']);
$this->app->close();
return;
}
$category = '';
$catQuery = $db->getQuery(true)
->select($db->quoteName('title'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('id') . ' = ' . (int) $article->catid);
$db->setQuery($catQuery);
$category = $db->loadResult() ?: '';
$tagQuery = $db->getQuery(true)
->select($db->quoteName('t.title'))
->from($db->quoteName('#__tags', 't'))
->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm') . ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id'))
->where($db->quoteName('m.content_item_id') . ' = ' . $articleId)
->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article'));
$db->setQuery($tagQuery);
$tags = $db->loadColumn() ?: [];
$introtext = strip_tags($article->introtext ?? '');
$introtext = mb_substr($introtext, 0, 500);
$params = \Joomla\CMS\Component\ComponentHelper::getParams('com_mokosuitecross');
$config = [
'ai_provider' => $params->get('ai_provider', 'none'),
'ai_api_key' => $params->get('ai_api_key', ''),
'ai_model' => $params->get('ai_model', ''),
'ai_tone' => $params->get('ai_tone', 'professional'),
];
$result = AiGeneratorHelper::generate($article->title, $introtext, $category, $tags, $config);
$this->app->setHeader('Content-Type', 'application/json; charset=utf-8');
echo json_encode($result);
$this->app->close();
}
}
@@ -0,0 +1,268 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoSuiteCross\Administrator\Table\PostTable;
/**
* Calendar controller -- provides AJAX endpoints for the visual post calendar.
*
* Endpoints:
* task=calendar.events -- GET JSON feed for FullCalendar (filtered by start/end)
* task=calendar.reschedule -- POST reschedule a post to a new date/time
*/
class CalendarController extends BaseController
{
/**
* Return posts as FullCalendar-compatible JSON events.
*
* Query params: start, end (ISO 8601 date range from FullCalendar).
*
* @return void
*/
public function events(): void
{
$app = $this->app;
$db = Factory::getDbo();
// ACL check
if (!$app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
return;
}
// FullCalendar sends start/end as ISO date strings
$start = $this->input->getString('start', '');
$end = $this->input->getString('end', '');
$query = $db->getQuery(true)
->select([
'p.' . $db->quoteName('id'),
'p.' . $db->quoteName('article_id'),
'p.' . $db->quoteName('service_id'),
'p.' . $db->quoteName('status'),
'p.' . $db->quoteName('scheduled_at'),
'p.' . $db->quoteName('posted_at'),
'p.' . $db->quoteName('created'),
'p.' . $db->quoteName('message'),
'a.' . $db->quoteName('title', 'article_title'),
's.' . $db->quoteName('title', 'service_title'),
's.' . $db->quoteName('service_type'),
])
->from($db->quoteName('#__mokosuitecross_posts', 'p'))
->leftJoin(
$db->quoteName('#__content', 'a')
. ' ON ' . $db->quoteName('a.id') . ' = ' . $db->quoteName('p.article_id')
)
->leftJoin(
$db->quoteName('#__mokosuitecross_services', 's')
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('p.service_id')
)
->order($db->quoteName('p.created') . ' DESC');
// Filter by date range when provided
if ($start !== '') {
$dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)';
$query->where($dateExpr . ' >= ' . $db->quote($start));
}
if ($end !== '') {
$dateExpr = 'COALESCE(p.scheduled_at, p.posted_at, p.created)';
$query->where($dateExpr . ' <= ' . $db->quote($end));
}
$db->setQuery($query);
$rows = $db->loadObjectList() ?: [];
// Map status to colour
$statusColors = [
'posted' => '#28a745',
'scheduled' => '#007bff',
'queued' => '#ffc107',
'failed' => '#dc3545',
'posting' => '#17a2b8',
];
$events = [];
foreach ($rows as $row) {
// Pick the best date for the calendar event
$eventDate = $row->scheduled_at ?: ($row->posted_at ?: $row->created);
// Skip rows with no usable date
if (empty($eventDate) || $eventDate === '0000-00-00 00:00:00') {
continue;
}
$title = ($row->article_title ?: 'Post #' . $row->id);
if ($row->service_title) {
$title .= ' - ' . $row->service_title;
}
$events[] = [
'id' => (int) $row->id,
'title' => $title,
'start' => $eventDate,
'color' => $statusColors[$row->status] ?? '#6c757d',
'url' => 'index.php?option=com_mokosuitecross&task=post.edit&id=' . (int) $row->id,
'extendedProps' => [
'status' => $row->status,
'service_type' => $row->service_type ?? '',
'article_id' => (int) $row->article_id,
'service_id' => (int) $row->service_id,
'message' => mb_substr($row->message ?? '', 0, 200),
],
];
}
$this->sendJsonResponse($events, 200);
}
/**
* Reschedule a post to a new date/time via drag-drop.
*
* POST params: post_id (int), new_date (ISO 8601 datetime string).
*
* @return void
*/
public function reschedule(): void
{
$app = $this->app;
// CSRF check
if (!Session::checkToken('post')) {
$this->sendJsonResponse(['error' => Text::_('JINVALID_TOKEN')], 403);
return;
}
// ACL check
if (!$app->getIdentity()->authorise('core.edit', 'com_mokosuitecross')) {
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
return;
}
$postId = $this->input->getInt('post_id', 0);
$newDate = $this->input->getString('new_date', '');
if ($postId < 1 || $newDate === '') {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
400
);
return;
}
// Validate the date format
try {
$dateObj = Factory::getDate($newDate);
} catch (\Exception $e) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
400
);
return;
}
// Load the post using Table bind/check/store pattern
$db = Factory::getDbo();
$table = new PostTable($db);
if (!$table->load($postId)) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
404
);
return;
}
// Only allow rescheduling of scheduled or queued posts
$allowedStatuses = ['scheduled', 'queued'];
if (!in_array($table->status, $allowedStatuses, true)) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
400
);
return;
}
// Update the post
$data = [
'scheduled_at' => $dateObj->toSql(),
'status' => 'scheduled',
'modified' => Factory::getDate()->toSql(),
];
if (!$table->bind($data) || !$table->check() || !$table->store()) {
$this->sendJsonResponse(
['error' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_ERROR')],
500
);
return;
}
// Log the reschedule
$log = (object) [
'post_id' => $postId,
'service_id' => (int) $table->service_id,
'level' => 'info',
'message' => sprintf('Post rescheduled to %s via calendar drag-drop', $dateObj->toSql()),
'context' => '{}',
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuitecross_logs', $log);
$this->sendJsonResponse(
[
'success' => true,
'message' => Text::_('COM_MOKOSUITECROSS_CALENDAR_RESCHEDULE_SUCCESS'),
],
200
);
}
/**
* Send a JSON response and close the application.
*
* @param array $data Response data
* @param int $httpCode HTTP status code
*
* @return void
*/
private function sendJsonResponse(array $data, int $httpCode): void
{
$app = $this->app;
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $httpCode);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$app->close();
}
}
@@ -56,7 +56,7 @@ class DispatchController extends BaseController
}
// ACL check — require core.manage on the component
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
if (!Factory::getApplication()->getIdentity()->authorise('mokosuitecross.dispatch', 'com_mokosuitecross')) {
$this->sendJsonResponse(['error' => 'Forbidden'], 403);
return;
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
class DisplayController extends BaseController
@@ -23,4 +24,13 @@ class DisplayController extends BaseController
* @var string
*/
protected $default_view = 'dashboard';
public function display($cachable = false, $urlparams = [])
{
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403);
}
return parent::display($cachable, $urlparams);
}
}
@@ -161,7 +161,7 @@ class PostsController extends AdminController
{
$this->checkToken('get');
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
if (!$this->app->getIdentity()->authorise('mokosuitecross.queue.export', 'com_mokosuitecross')) {
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
}
@@ -0,0 +1,86 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\PreviewHelper;
class PreviewController extends BaseController
{
public function render(): void
{
if (!Session::checkToken('get')) {
echo json_encode(['error' => 'Invalid token']);
$this->app->close();
return;
}
$articleId = $this->input->getInt('article_id', 0);
$platform = $this->input->getCmd('platform', 'twitter');
if ($articleId < 1) {
echo json_encode(['error' => 'Missing article ID']);
$this->app->close();
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $articleId);
$db->setQuery($query);
$article = $db->loadObject();
if (!$article) {
echo json_encode(['error' => 'Article not found']);
$this->app->close();
return;
}
$meta = CrossPostDispatcher::buildArticleMeta($article);
$title = $meta['{title}'] ?? '';
$text = $meta['{introtext}'] ?? '';
$url = $meta['{url}'] ?? '';
$imageUrl = $meta['{image}'] ?? '';
$authorName = $meta['{author}'] ?? '';
$supportedPlatforms = PreviewHelper::getSupportedPlatforms();
$html = '';
if ($platform === 'all') {
foreach ($supportedPlatforms as $p) {
$html .= '<div style="margin-bottom:20px;">'
. '<div style="font-weight:600;font-size:13px;color:#666;margin-bottom:6px;text-transform:uppercase;">' . htmlspecialchars(ucfirst($p)) . '</div>'
. PreviewHelper::render($p, $title, $text, $url, $imageUrl, $authorName)
. '</div>';
}
} else {
$html = PreviewHelper::render($platform, $title, $text, $url, $imageUrl, $authorName);
}
$this->app->setHeader('Content-Type', 'text/html; charset=utf-8');
echo $html;
$this->app->close();
}
}
@@ -31,7 +31,7 @@ class ServiceController extends FormController
{
$this->checkToken();
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitecross')) {
if (!$this->app->getIdentity()->authorise('mokosuitecross.services.manage', 'com_mokosuitecross')) {
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
}
@@ -21,4 +21,22 @@ class ServicesController extends AdminController
{
return parent::getModel($name, $prefix, $config);
}
public function publish(): void
{
if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403);
}
parent::publish();
}
public function delete(): void
{
if (!$this->app->getIdentity()->authorise('core.delete', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403);
}
parent::delete();
}
}
@@ -21,4 +21,22 @@ class TemplatesController extends AdminController
{
return parent::getModel($name, $prefix, $config);
}
public function publish(): void
{
if (!$this->app->getIdentity()->authorise('mokosuitecross.templates.manage', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403);
}
parent::publish();
}
public function delete(): void
{
if (!$this->app->getIdentity()->authorise('mokosuitecross.templates.manage', 'com_mokosuitecross')) {
throw new \Joomla\CMS\Access\Exception\NotAllowed(\Joomla\CMS\Language\Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403);
}
parent::delete();
}
}
@@ -0,0 +1,196 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Administrator\Helper;
defined('_JEXEC') or die;
class AiGeneratorHelper
{
public static function generate(string $title, string $introtext, string $category, array $tags, array $config): array
{
$provider = $config['ai_provider'] ?? 'none';
$apiKey = $config['ai_api_key'] ?? '';
$model = $config['ai_model'] ?? '';
$tone = $config['ai_tone'] ?? 'professional';
if ($provider === 'none' || $apiKey === '') {
return ['success' => false, 'error' => 'AI provider not configured or API key missing.'];
}
$prompt = self::buildPrompt($title, $introtext, $category, $tags, $tone);
$response = match ($provider) {
'claude' => self::callClaude($prompt, $apiKey, $model ?: 'claude-haiku-4-5'),
'openai' => self::callOpenAI($prompt, $apiKey, $model ?: 'gpt-4o-mini'),
default => '',
};
if ($response === '') {
return ['success' => false, 'error' => 'AI provider returned an empty response.'];
}
$parsed = self::parseResponse($response);
if ($parsed === null) {
return ['success' => false, 'error' => 'Could not parse AI response as JSON.'];
}
return ['success' => true, 'data' => $parsed];
}
private static function callClaude(string $prompt, string $apiKey, string $model): string
{
$payload = json_encode([
'model' => $model,
'max_tokens' => 500,
'messages' => [
['role' => 'user', 'content' => $prompt],
],
]);
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . $apiKey,
'anthropic-version: 2023-06-01',
],
]);
$response = curl_exec($ch);
if ($response === false) {
curl_close($ch);
return '';
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300) {
return '';
}
$data = json_decode($response, true);
return $data['content'][0]['text'] ?? '';
}
private static function callOpenAI(string $prompt, string $apiKey, string $model): string
{
$payload = json_encode([
'model' => $model,
'max_tokens' => 500,
'messages' => [
['role' => 'user', 'content' => $prompt],
],
]);
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
],
]);
$response = curl_exec($ch);
if ($response === false) {
curl_close($ch);
return '';
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300) {
return '';
}
$data = json_decode($response, true);
return $data['choices'][0]['message']['content'] ?? '';
}
private static function buildPrompt(string $title, string $introtext, string $category, array $tags, string $tone): string
{
$tagList = !empty($tags) ? implode(', ', $tags) : 'none';
$toneGuide = match ($tone) {
'casual' => 'Use a relaxed, conversational tone.',
'friendly' => 'Use a warm, approachable tone with enthusiasm.',
default => 'Use a professional, polished tone.',
};
return <<<PROMPT
Generate cross-post captions for this article. {$toneGuide}
Article title: {$title}
Content summary: {$introtext}
Category: {$category}
Tags: {$tagList}
Return ONLY a JSON object with these keys (no markdown, no explanation):
{
"social": "Facebook/LinkedIn post (max 200 chars, include a call to action)",
"short": "Twitter/Bluesky post (max 270 chars, punchy, include 1-2 relevant hashtags)",
"chat": "Telegram/Discord message (max 300 chars, conversational)",
"email_subject": "Email subject line (max 60 chars, compelling, no clickbait)"
}
Rules:
- Do not include the article URL (it is added automatically)
- Do not wrap the JSON in markdown code fences
- Respect the character limits strictly
- Each caption should be unique, not just a reformatted version of the others
PROMPT;
}
private static function parseResponse(string $response): ?array
{
$response = trim($response);
if (preg_match('/\{[\s\S]*\}/', $response, $matches)) {
$response = $matches[0];
}
$data = json_decode($response, true);
if (!\is_array($data)) {
return null;
}
$required = ['social', 'short', 'chat', 'email_subject'];
foreach ($required as $key) {
if (!isset($data[$key]) || !\is_string($data[$key])) {
return null;
}
}
return [
'social' => mb_substr($data['social'], 0, 500),
'short' => mb_substr($data['short'], 0, 280),
'chat' => mb_substr($data['chat'], 0, 500),
'email_subject' => mb_substr($data['email_subject'], 0, 120),
];
}
}
@@ -477,12 +477,16 @@ class CrossPostDispatcher
$url = $url . $separator . http_build_query($utmParams);
}
// Link shortening (#159) — shorten the final URL (with UTM if enabled)
$urlShort = LinkShortenerHelper::shorten($url);
return [
'{title}' => $titleText,
'{introtext}' => $introStripped,
'{fulltext}' => strip_tags(mb_substr($article->fulltext ?? '', 0, 500)),
'{url}' => $url,
'{url_raw}' => $urlRaw,
'{url_short}' => $urlShort,
'{image}' => $introImage,
'{category}' => $categoryName,
'{author}' => $authorName,
@@ -0,0 +1,172 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Administrator\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
/**
* Shortens URLs via Bitly, Rebrandly, or YOURLS.
*
* Returns the original URL on any failure so cross-posts are never broken.
*/
class LinkShortenerHelper
{
/**
* Shorten a URL using the configured provider.
*
* @param string $url The URL to shorten
*
* @return string Shortened URL, or the original on failure/disabled
*/
public static function shorten(string $url): string
{
$params = ComponentHelper::getParams('com_mokosuitecross');
$provider = $params->get('link_shortener', 'none');
if ($provider === 'none' || empty($url)) {
return $url;
}
$apiKey = $params->get('link_shortener_api_key', '');
switch ($provider) {
case 'bitly':
return self::shortenWithBitly($url, $apiKey);
case 'rebrandly':
return self::shortenWithRebrandly($url, $apiKey);
case 'yourls':
$apiUrl = $params->get('link_shortener_yourls_url', '');
$token = $params->get('link_shortener_yourls_token', '');
return self::shortenWithYourls($url, $apiUrl, $token);
default:
return $url;
}
}
/**
* Shorten via Bitly API v4.
*/
public static function shortenWithBitly(string $url, string $apiKey): string
{
if (empty($apiKey)) {
return $url;
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api-ssl.bitly.com/v4/shorten',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['long_url' => $url]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode < 200 || $httpCode >= 300) {
return $url;
}
$data = json_decode($response, true);
return $data['link'] ?? $url;
}
/**
* Shorten via Rebrandly API.
*/
public static function shortenWithRebrandly(string $url, string $apiKey, string $workspace = ''): string
{
if (empty($apiKey)) {
return $url;
}
$headers = [
'apikey: ' . $apiKey,
'Content-Type: application/json',
];
if (!empty($workspace)) {
$headers[] = 'workspace: ' . $workspace;
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => 'https://api.rebrandly.com/v1/links',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['destination' => $url]),
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode < 200 || $httpCode >= 300) {
return $url;
}
$data = json_decode($response, true);
$short = $data['shortUrl'] ?? '';
return !empty($short) ? 'https://' . ltrim($short, 'https://') : $url;
}
/**
* Shorten via YOURLS API (self-hosted).
*/
public static function shortenWithYourls(string $url, string $apiUrl, string $signatureToken): string
{
if (empty($apiUrl) || empty($signatureToken)) {
return $url;
}
$endpoint = rtrim($apiUrl, '/') . '?' . http_build_query([
'action' => 'shorturl',
'format' => 'json',
'signature' => $signatureToken,
'url' => $url,
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $endpoint,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $httpCode < 200 || $httpCode >= 300) {
return $url;
}
$data = json_decode($response, true);
return $data['shorturl'] ?? $url;
}
}
@@ -40,6 +40,7 @@ class MokoSuiteCrossHelper
'posts' => 'COM_MOKOSUITECROSS_SUBMENU_POSTS',
'services' => 'COM_MOKOSUITECROSS_SUBMENU_SERVICES',
'templates' => 'COM_MOKOSUITECROSS_SUBMENU_TEMPLATES',
'calendar' => 'COM_MOKOSUITECROSS_SUBMENU_CALENDAR',
'logs' => 'COM_MOKOSUITECROSS_SUBMENU_LOGS',
];
@@ -71,4 +72,26 @@ class MokoSuiteCrossHelper
);
}
}
/**
* Get a list of ACL actions for the component.
*
* @return \Joomla\CMS\Object\CMSObject
*/
public static function getActions(): \Joomla\CMS\Object\CMSObject
{
$user = \Joomla\CMS\Factory::getApplication()->getIdentity();
$result = new \Joomla\CMS\Object\CMSObject();
$actions = \Joomla\CMS\Access\Access::getActionsFromFile(
JPATH_ADMINISTRATOR . '/components/com_mokosuitecross/access.xml',
'/access/section[@name="component"]/'
);
foreach ($actions as $action) {
$result->set($action->name, $user->authorise($action->name, 'com_mokosuitecross'));
}
return $result;
}
}
@@ -0,0 +1,207 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage com_mokosuitecross
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Component\MokoSuiteCross\Administrator\Helper;
defined('_JEXEC') or die;
class PreviewHelper
{
public static function render(string $platform, string $title, string $text, string $url, string $imageUrl = '', string $authorName = ''): string
{
$title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$authorName = htmlspecialchars($authorName, ENT_QUOTES, 'UTF-8');
$imageHtml = '';
if (!empty($imageUrl)) {
$imageUrl = htmlspecialchars($imageUrl, ENT_QUOTES, 'UTF-8');
$imageHtml = '<img src="' . $imageUrl . '" alt="" style="width:100%;max-height:260px;object-fit:cover;border-radius:8px;margin:8px 0;">';
}
return match ($platform) {
'twitter' => self::renderTwitter($title, $text, $url, $imageHtml, $authorName),
'facebook' => self::renderFacebook($title, $text, $url, $imageHtml, $authorName),
'mastodon' => self::renderMastodon($title, $text, $url, $imageHtml, $authorName),
'linkedin' => self::renderLinkedIn($title, $text, $url, $imageHtml, $authorName),
'bluesky' => self::renderBluesky($title, $text, $url, $imageHtml, $authorName),
'telegram' => self::renderTelegram($title, $text, $url, $imageHtml),
default => self::renderGeneric($platform, $title, $text, $url, $imageHtml),
};
}
public static function getSupportedPlatforms(): array
{
return ['twitter', 'facebook', 'mastodon', 'linkedin', 'bluesky', 'telegram'];
}
private static function renderTwitter(string $title, string $text, string $url, string $imageHtml, string $author): string
{
$displayText = !empty($text) ? $text : $title;
$charCount = mb_strlen(strip_tags($displayText));
return <<<HTML
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:500px;border:1px solid #cfd9de;border-radius:16px;padding:12px 16px;background:#fff;">
<div style="display:flex;align-items:center;margin-bottom:8px;">
<div style="width:40px;height:40px;border-radius:50%;background:#1da1f2;margin-right:10px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;font-size:16px;">X</div>
<div>
<div style="font-weight:700;font-size:15px;color:#0f1419;">{$author}</div>
<div style="color:#536471;font-size:13px;">@username</div>
</div>
</div>
<div style="font-size:15px;line-height:1.4;color:#0f1419;margin-bottom:8px;">{$displayText}</div>
{$imageHtml}
<div style="margin-top:8px;padding:10px 12px;border:1px solid #cfd9de;border-radius:12px;background:#f7f9f9;">
<div style="font-size:13px;color:#536471;margin-bottom:2px;">yoursite.com</div>
<div style="font-size:15px;font-weight:600;color:#0f1419;">{$title}</div>
</div>
<div style="color:#536471;font-size:13px;margin-top:8px;text-align:right;">{$charCount}/280</div>
</div>
HTML;
}
private static function renderFacebook(string $title, string $text, string $url, string $imageHtml, string $author): string
{
$displayText = !empty($text) ? $text : $title;
return <<<HTML
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:500px;border:1px solid #dddfe2;border-radius:8px;background:#fff;overflow:hidden;">
<div style="padding:12px 16px;">
<div style="display:flex;align-items:center;margin-bottom:8px;">
<div style="width:40px;height:40px;border-radius:50%;background:#1877f2;margin-right:10px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;font-size:18px;">f</div>
<div>
<div style="font-weight:600;font-size:15px;color:#050505;">{$author}</div>
<div style="color:#65676b;font-size:13px;">Just now</div>
</div>
</div>
<div style="font-size:15px;line-height:1.4;color:#050505;">{$displayText}</div>
</div>
{$imageHtml}
<div style="padding:10px 16px;border-top:1px solid #dddfe2;background:#f0f2f5;">
<div style="font-size:12px;color:#65676b;text-transform:uppercase;">yoursite.com</div>
<div style="font-size:16px;font-weight:600;color:#050505;margin-top:2px;">{$title}</div>
</div>
</div>
HTML;
}
private static function renderMastodon(string $title, string $text, string $url, string $imageHtml, string $author): string
{
$displayText = !empty($text) ? $text : $title;
$charCount = mb_strlen(strip_tags($displayText));
return <<<HTML
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:500px;border:1px solid #c0cdd9;border-radius:8px;padding:14px 16px;background:#fff;">
<div style="display:flex;align-items:center;margin-bottom:10px;">
<div style="width:46px;height:46px;border-radius:8px;background:#6364ff;margin-right:10px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;font-size:20px;">M</div>
<div>
<div style="font-weight:700;font-size:15px;color:#1a1a2e;">{$author}</div>
<div style="color:#606984;font-size:13px;">@user@mastodon.social</div>
</div>
</div>
<div style="font-size:15px;line-height:1.5;color:#1a1a2e;">{$displayText}</div>
{$imageHtml}
<div style="margin-top:8px;padding:10px 12px;border:1px solid #c0cdd9;border-radius:8px;background:#f2f5f7;">
<div style="font-size:14px;font-weight:600;color:#1a1a2e;">{$title}</div>
<div style="font-size:12px;color:#606984;margin-top:2px;">yoursite.com</div>
</div>
<div style="color:#606984;font-size:13px;margin-top:8px;text-align:right;">{$charCount}/500</div>
</div>
HTML;
}
private static function renderLinkedIn(string $title, string $text, string $url, string $imageHtml, string $author): string
{
$displayText = !empty($text) ? $text : $title;
return <<<HTML
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:500px;border:1px solid #e0dfdc;border-radius:8px;background:#fff;overflow:hidden;">
<div style="padding:12px 16px;">
<div style="display:flex;align-items:center;margin-bottom:8px;">
<div style="width:48px;height:48px;border-radius:50%;background:#0a66c2;margin-right:10px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;font-size:18px;">in</div>
<div>
<div style="font-weight:600;font-size:14px;color:#191919;">{$author}</div>
<div style="color:#666;font-size:12px;">Just now</div>
</div>
</div>
<div style="font-size:14px;line-height:1.4;color:#191919;">{$displayText}</div>
</div>
{$imageHtml}
<div style="padding:8px 16px 12px;border-top:1px solid #e0dfdc;background:#f9fafb;">
<div style="font-size:14px;font-weight:600;color:#191919;">{$title}</div>
<div style="font-size:12px;color:#666;margin-top:2px;">yoursite.com</div>
</div>
</div>
HTML;
}
private static function renderBluesky(string $title, string $text, string $url, string $imageHtml, string $author): string
{
$displayText = !empty($text) ? $text : $title;
$charCount = mb_strlen(strip_tags($displayText));
return <<<HTML
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:500px;border:1px solid #d1d5db;border-radius:12px;padding:12px 16px;background:#fff;">
<div style="display:flex;align-items:center;margin-bottom:8px;">
<div style="width:42px;height:42px;border-radius:50%;background:#0085ff;margin-right:10px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;font-size:16px;">B</div>
<div>
<div style="font-weight:600;font-size:15px;color:#1e2937;">{$author}</div>
<div style="color:#6b7280;font-size:13px;">@user.bsky.social</div>
</div>
</div>
<div style="font-size:15px;line-height:1.4;color:#1e2937;">{$displayText}</div>
{$imageHtml}
<div style="margin-top:8px;padding:10px 12px;border:1px solid #d1d5db;border-radius:8px;background:#f9fafb;">
<div style="font-size:14px;font-weight:600;color:#1e2937;">{$title}</div>
<div style="font-size:12px;color:#6b7280;margin-top:2px;">yoursite.com</div>
</div>
<div style="color:#6b7280;font-size:13px;margin-top:8px;text-align:right;">{$charCount}/300</div>
</div>
HTML;
}
private static function renderTelegram(string $title, string $text, string $url, string $imageHtml): string
{
$displayText = !empty($text) ? $text : $title;
return <<<HTML
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:500px;background:#effdde;border-radius:12px;padding:10px 14px;margin-left:60px;">
{$imageHtml}
<div style="font-size:15px;line-height:1.4;color:#000;">{$displayText}</div>
<div style="margin-top:8px;padding:8px 12px;border-left:3px solid #4fae4e;background:#fff;border-radius:0 8px 8px 0;">
<div style="font-size:14px;font-weight:600;color:#000;">{$title}</div>
<div style="font-size:12px;color:#888;margin-top:2px;">yoursite.com</div>
</div>
<div style="color:#5fb452;font-size:11px;text-align:right;margin-top:4px;">Just now</div>
</div>
HTML;
}
private static function renderGeneric(string $platform, string $title, string $text, string $url, string $imageHtml): string
{
$platformLabel = htmlspecialchars(ucfirst($platform), ENT_QUOTES, 'UTF-8');
$displayText = !empty($text) ? $text : $title;
return <<<HTML
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:500px;border:1px solid #ddd;border-radius:8px;padding:12px 16px;background:#fff;">
<div style="font-weight:600;font-size:13px;color:#666;margin-bottom:8px;text-transform:uppercase;">{$platformLabel}</div>
<div style="font-size:15px;line-height:1.4;color:#333;">{$displayText}</div>
{$imageHtml}
<div style="margin-top:8px;padding:8px 12px;border:1px solid #ddd;border-radius:6px;background:#f9f9f9;">
<div style="font-size:14px;font-weight:600;color:#333;">{$title}</div>
<div style="font-size:12px;color:#999;">yoursite.com</div>
</div>
</div>
HTML;
}
}

Some files were not shown because too many files have changed in this diff Show More