Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93b28a851e | |||
| cb28cb12cd | |||
| d8376d6cdf | |||
| 543bd2b464 | |||
| 32bb72d12d | |||
| 4f92b4e508 | |||
| 53bf4a3187 | |||
| 87267d8e80 | |||
| 11e50c54bb | |||
| 377ae2d39e | |||
| a60ba86b19 | |||
| 71a102028d | |||
| 8858c81f87 | |||
| 5b1fb1584e | |||
| e6328a1e8d | |||
| 8582a3eac5 |
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.05.02
|
||||
# VERSION: 01.06.09
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
## [01.05.00] --- 2026-06-28
|
||||
|
||||
|
||||
<!-- VERSION: 01.05.02 -->
|
||||
<!-- VERSION: 01.06.09 -->
|
||||
|
||||
All notable changes to MokoSuiteOpenGraph will be documented in this file.
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
|
||||
VERSION: 01.05.02
|
||||
VERSION: 01.06.09
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Community expectations and enforcement guidelines
|
||||
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
DEFGROUP: mokoconsulting-tech.Template-Joomla
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla
|
||||
VERSION: 01.05.02
|
||||
VERSION: 01.06.09
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
|
||||
-->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoSuiteOpenGraph
|
||||
|
||||
<!-- VERSION: 01.05.02 -->
|
||||
<!-- VERSION: 01.06.09 -->
|
||||
|
||||
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 6 and higher.
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 01.05.02
|
||||
VERSION: 01.06.09
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
* @package MokoSuiteOpenGraph
|
||||
* @subpackage com_mokoog
|
||||
* @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
|
||||
-->
|
||||
<access component="com_mokoog">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" />
|
||||
<action name="core.create" title="JACTION_CREATE" />
|
||||
<action name="core.delete" title="JACTION_DELETE" />
|
||||
<action name="core.edit" title="JACTION_EDIT" />
|
||||
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
||||
<action name="mokoog.batch" title="COM_MOKOOG_ACTION_BATCH" description="COM_MOKOOG_ACTION_BATCH_DESC" />
|
||||
<action name="mokoog.import" title="COM_MOKOOG_ACTION_IMPORT" description="COM_MOKOOG_ACTION_IMPORT_DESC" />
|
||||
</section>
|
||||
</access>
|
||||
@@ -31,10 +31,14 @@ class JsonapiView extends BaseApiView
|
||||
'og_description',
|
||||
'og_image',
|
||||
'og_type',
|
||||
'og_video',
|
||||
'seo_title',
|
||||
'meta_description',
|
||||
'robots',
|
||||
'canonical_url',
|
||||
'event_data',
|
||||
'recipe_data',
|
||||
'custom_schema',
|
||||
'language',
|
||||
'published',
|
||||
'created',
|
||||
@@ -54,10 +58,14 @@ class JsonapiView extends BaseApiView
|
||||
'og_description',
|
||||
'og_image',
|
||||
'og_type',
|
||||
'og_video',
|
||||
'seo_title',
|
||||
'meta_description',
|
||||
'robots',
|
||||
'canonical_url',
|
||||
'event_data',
|
||||
'recipe_data',
|
||||
'custom_schema',
|
||||
'language',
|
||||
'published',
|
||||
'created',
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
* @package MokoSuiteOpenGraph
|
||||
* @subpackage com_mokoog
|
||||
* @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
|
||||
-->
|
||||
<config>
|
||||
<fieldset name="general">
|
||||
<field
|
||||
type="note"
|
||||
label="COM_MOKOOG_CONFIG_NOTE_LABEL"
|
||||
description="COM_MOKOOG_CONFIG_NOTE_DESC"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
name="permissions"
|
||||
label="JCONFIG_PERMISSIONS_LABEL"
|
||||
description="JCONFIG_PERMISSIONS_DESC"
|
||||
>
|
||||
<field
|
||||
name="rules"
|
||||
type="rules"
|
||||
label="JCONFIG_PERMISSIONS_LABEL"
|
||||
class="inputbox"
|
||||
validate="rules"
|
||||
filter="rules"
|
||||
component="com_mokoog"
|
||||
section="component"
|
||||
/>
|
||||
</fieldset>
|
||||
</config>
|
||||
@@ -16,13 +16,15 @@
|
||||
name="content_type"
|
||||
type="text"
|
||||
label="COM_MOKOOG_FIELD_CONTENT_TYPE"
|
||||
readonly="true"
|
||||
description="COM_MOKOOG_FIELD_CONTENT_TYPE_DESC"
|
||||
required="true"
|
||||
/>
|
||||
<field
|
||||
name="content_id"
|
||||
type="number"
|
||||
label="COM_MOKOOG_FIELD_CONTENT_ID"
|
||||
readonly="true"
|
||||
description="COM_MOKOOG_FIELD_CONTENT_ID_DESC"
|
||||
required="true"
|
||||
/>
|
||||
<field
|
||||
name="og_title"
|
||||
@@ -77,37 +79,45 @@
|
||||
<option value="1">JPUBLISHED</option>
|
||||
<option value="0">JUNPUBLISHED</option>
|
||||
</field>
|
||||
<field
|
||||
name="language"
|
||||
type="contentlanguage"
|
||||
label="JFIELD_LANGUAGE_LABEL"
|
||||
default="*"
|
||||
>
|
||||
<option value="*">JALL</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
<fieldset name="seo" label="SEO Meta Tags">
|
||||
<fieldset name="seo" label="COM_MOKOOG_FIELDSET_SEO">
|
||||
<field
|
||||
name="seo_title"
|
||||
type="text"
|
||||
label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE"
|
||||
description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC"
|
||||
label="COM_MOKOOG_FIELD_SEO_TITLE"
|
||||
description="COM_MOKOOG_FIELD_SEO_TITLE_DESC"
|
||||
filter="string"
|
||||
maxlength="255"
|
||||
maxlength="70"
|
||||
/>
|
||||
<field
|
||||
name="meta_description"
|
||||
type="textarea"
|
||||
label="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION"
|
||||
description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
||||
label="COM_MOKOOG_FIELD_META_DESCRIPTION"
|
||||
description="COM_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
||||
filter="string"
|
||||
rows="3"
|
||||
maxlength="255"
|
||||
maxlength="200"
|
||||
/>
|
||||
<field
|
||||
name="robots"
|
||||
type="text"
|
||||
label="PLG_CONTENT_MOKOOG_FIELD_ROBOTS"
|
||||
description="PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC"
|
||||
label="COM_MOKOOG_FIELD_ROBOTS"
|
||||
description="COM_MOKOOG_FIELD_ROBOTS_DESC"
|
||||
filter="string"
|
||||
/>
|
||||
<field
|
||||
name="canonical_url"
|
||||
type="url"
|
||||
label="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL"
|
||||
description="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC"
|
||||
label="COM_MOKOOG_FIELD_CANONICAL_URL"
|
||||
description="COM_MOKOOG_FIELD_CANONICAL_URL_DESC"
|
||||
filter="url"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
@@ -66,3 +66,27 @@ COM_MOKOOG_COVERAGE_ARTICLES="%d of %d articles have OG tags"
|
||||
COM_MOKOOG_COVERAGE_MISSING_TITLE="%d tags missing custom title"
|
||||
COM_MOKOOG_COVERAGE_MISSING_DESC="%d tags missing custom description"
|
||||
COM_MOKOOG_COVERAGE_MISSING_IMAGE="%d tags missing custom image"
|
||||
|
||||
; Single-tag edit form
|
||||
COM_MOKOOG_TAG_NEW="MokoSuiteOpenGraph - New OG Tag"
|
||||
COM_MOKOOG_TAG_EDIT="MokoSuiteOpenGraph - Edit OG Tag"
|
||||
COM_MOKOOG_TAB_DETAILS="Details"
|
||||
COM_MOKOOG_FIELDSET_SEO="SEO Meta Tags"
|
||||
COM_MOKOOG_FIELD_CONTENT_TYPE_DESC="The content type this OG tag applies to (e.g. com_content, menu, com_content.category)."
|
||||
COM_MOKOOG_FIELD_CONTENT_ID_DESC="The ID of the content item this OG tag applies to."
|
||||
COM_MOKOOG_FIELD_SEO_TITLE="SEO Title"
|
||||
COM_MOKOOG_FIELD_SEO_TITLE_DESC="Overrides the page <title> tag (max 70 characters)."
|
||||
COM_MOKOOG_FIELD_META_DESCRIPTION="Meta Description"
|
||||
COM_MOKOOG_FIELD_META_DESCRIPTION_DESC="Overrides the page meta description (max 200 characters)."
|
||||
COM_MOKOOG_FIELD_ROBOTS="Robots"
|
||||
COM_MOKOOG_FIELD_ROBOTS_DESC="Per-page robots directive, e.g. noindex, nofollow."
|
||||
COM_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
||||
COM_MOKOOG_FIELD_CANONICAL_URL_DESC="Overrides the canonical URL for this content item (http/https only)."
|
||||
|
||||
; ACL actions (access.xml) and component options (config.xml)
|
||||
COM_MOKOOG_ACTION_BATCH="Batch Generate OG Tags"
|
||||
COM_MOKOOG_ACTION_BATCH_DESC="Allows users in this group to run batch OG tag generation."
|
||||
COM_MOKOOG_ACTION_IMPORT="Import / Export OG Tags"
|
||||
COM_MOKOOG_ACTION_IMPORT_DESC="Allows users in this group to import and export OG tags via CSV."
|
||||
COM_MOKOOG_CONFIG_NOTE_LABEL="Where are the settings?"
|
||||
COM_MOKOOG_CONFIG_NOTE_DESC="Open Graph and SEO settings are configured in the System - MokoSuiteOpenGraph plugin (Extensions → Plugins). This screen manages component permissions only."
|
||||
|
||||
@@ -66,3 +66,27 @@ COM_MOKOOG_COVERAGE_ARTICLES="%d of %d articles have OG tags"
|
||||
COM_MOKOOG_COVERAGE_MISSING_TITLE="%d tags missing custom title"
|
||||
COM_MOKOOG_COVERAGE_MISSING_DESC="%d tags missing custom description"
|
||||
COM_MOKOOG_COVERAGE_MISSING_IMAGE="%d tags missing custom image"
|
||||
|
||||
; Single-tag edit form
|
||||
COM_MOKOOG_TAG_NEW="MokoSuiteOpenGraph - New OG Tag"
|
||||
COM_MOKOOG_TAG_EDIT="MokoSuiteOpenGraph - Edit OG Tag"
|
||||
COM_MOKOOG_TAB_DETAILS="Details"
|
||||
COM_MOKOOG_FIELDSET_SEO="SEO Meta Tags"
|
||||
COM_MOKOOG_FIELD_CONTENT_TYPE_DESC="The content type this OG tag applies to (e.g. com_content, menu, com_content.category)."
|
||||
COM_MOKOOG_FIELD_CONTENT_ID_DESC="The ID of the content item this OG tag applies to."
|
||||
COM_MOKOOG_FIELD_SEO_TITLE="SEO Title"
|
||||
COM_MOKOOG_FIELD_SEO_TITLE_DESC="Overrides the page <title> tag (max 70 characters)."
|
||||
COM_MOKOOG_FIELD_META_DESCRIPTION="Meta Description"
|
||||
COM_MOKOOG_FIELD_META_DESCRIPTION_DESC="Overrides the page meta description (max 200 characters)."
|
||||
COM_MOKOOG_FIELD_ROBOTS="Robots"
|
||||
COM_MOKOOG_FIELD_ROBOTS_DESC="Per-page robots directive, e.g. noindex, nofollow."
|
||||
COM_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
||||
COM_MOKOOG_FIELD_CANONICAL_URL_DESC="Overrides the canonical URL for this content item (http/https only)."
|
||||
|
||||
; ACL actions (access.xml) and component options (config.xml)
|
||||
COM_MOKOOG_ACTION_BATCH="Batch Generate OG Tags"
|
||||
COM_MOKOOG_ACTION_BATCH_DESC="Allows users in this group to run batch OG tag generation."
|
||||
COM_MOKOOG_ACTION_IMPORT="Import / Export OG Tags"
|
||||
COM_MOKOOG_ACTION_IMPORT_DESC="Allows users in this group to import and export OG tags via CSV."
|
||||
COM_MOKOOG_CONFIG_NOTE_LABEL="Where are the settings?"
|
||||
COM_MOKOOG_CONFIG_NOTE_DESC="Open Graph and SEO settings are configured in the System - MokoSuiteOpenGraph plugin (Extensions → Plugins). This screen manages component permissions only."
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>com_mokoog</name>
|
||||
<version>01.05.02</version>
|
||||
<version>01.06.09</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -50,6 +50,7 @@
|
||||
<folder>View</folder>
|
||||
</files>
|
||||
<files folder="tmpl">
|
||||
<folder>tag</folder>
|
||||
<folder>tags</folder>
|
||||
</files>
|
||||
<files folder="sql">
|
||||
@@ -63,6 +64,11 @@
|
||||
</files>
|
||||
<files folder="language">
|
||||
<folder>en-GB</folder>
|
||||
<folder>en-US</folder>
|
||||
</files>
|
||||
<files>
|
||||
<filename>access.xml</filename>
|
||||
<filename>config.xml</filename>
|
||||
</files>
|
||||
<menu img="class:bookmark">COM_MOKOOG</menu>
|
||||
<submenu>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.02 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.03 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.04 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.05 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.06 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.07 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.08 — no schema changes */
|
||||
@@ -0,0 +1 @@
|
||||
/* 01.06.09 — no schema changes */
|
||||
@@ -29,7 +29,10 @@ class BatchController extends BaseController
|
||||
{
|
||||
Session::checkToken('get') || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
||||
|
||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
||||
$identity = Factory::getApplication()->getIdentity();
|
||||
|
||||
if (!$identity->authorise('mokoog.batch', 'com_mokoog')
|
||||
&& !$identity->authorise('core.create', 'com_mokoog')) {
|
||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||
}
|
||||
|
||||
@@ -62,7 +65,10 @@ class BatchController extends BaseController
|
||||
{
|
||||
Session::checkToken('get') || throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
||||
|
||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
||||
$identity = Factory::getApplication()->getIdentity();
|
||||
|
||||
if (!$identity->authorise('mokoog.batch', 'com_mokoog')
|
||||
&& !$identity->authorise('core.create', 'com_mokoog')) {
|
||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ class ImportExportController extends BaseController
|
||||
$db->quoteName('t.robots'),
|
||||
$db->quoteName('t.canonical_url'),
|
||||
$db->quoteName('t.language'),
|
||||
$db->quoteName('t.og_video'),
|
||||
$db->quoteName('t.event_data'),
|
||||
$db->quoteName('t.recipe_data'),
|
||||
$db->quoteName('t.custom_schema'),
|
||||
])
|
||||
->from($db->quoteName('#__mokoog_tags', 't'))
|
||||
->leftJoin(
|
||||
@@ -84,7 +88,7 @@ class ImportExportController extends BaseController
|
||||
'content_type', 'content_id', 'article_title',
|
||||
'og_title', 'og_description', 'og_image', 'og_type',
|
||||
'seo_title', 'meta_description', 'robots', 'canonical_url',
|
||||
'language',
|
||||
'language', 'og_video', 'event_data', 'recipe_data', 'custom_schema',
|
||||
]);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
@@ -106,7 +110,8 @@ class ImportExportController extends BaseController
|
||||
|
||||
$identity = Factory::getApplication()->getIdentity();
|
||||
|
||||
if (!$identity->authorise('core.create', 'com_mokoog') || !$identity->authorise('core.edit', 'com_mokoog')) {
|
||||
if (!$identity->authorise('mokoog.import', 'com_mokoog')
|
||||
&& !($identity->authorise('core.create', 'com_mokoog') && $identity->authorise('core.edit', 'com_mokoog'))) {
|
||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
||||
}
|
||||
|
||||
@@ -187,6 +192,10 @@ class ImportExportController extends BaseController
|
||||
$robots = trim($row[9] ?? '');
|
||||
$canonicalUrl = trim($row[10] ?? '');
|
||||
$language = trim($row[11] ?? '*');
|
||||
$ogVideo = $this->sanitizeUrl($row[12] ?? '');
|
||||
$eventData = $this->validateJsonField($row[13] ?? '');
|
||||
$recipeData = $this->validateJsonField($row[14] ?? '');
|
||||
$customSchema = $this->validateJsonField($row[15] ?? '');
|
||||
|
||||
// Validate language tag format (e.g., 'en-GB', '*')
|
||||
if ($language !== '*' && !preg_match('/^[a-z]{2,3}-[A-Z]{2}$/', $language)) {
|
||||
@@ -229,6 +238,10 @@ class ImportExportController extends BaseController
|
||||
'robots' => $robots,
|
||||
'canonical_url' => $canonicalUrl,
|
||||
'language' => $language,
|
||||
'og_video' => $ogVideo,
|
||||
'event_data' => $eventData,
|
||||
'recipe_data' => $recipeData,
|
||||
'custom_schema' => $customSchema,
|
||||
'published' => 1,
|
||||
'modified' => $now,
|
||||
];
|
||||
@@ -252,4 +265,45 @@ class ImportExportController extends BaseController
|
||||
);
|
||||
$app->redirect('index.php?option=com_mokoog&view=tags');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a JSON field — returns trimmed JSON only if it is an object/array.
|
||||
*
|
||||
* Scalars and invalid JSON are dropped to '' so an import can never inject a
|
||||
* payload that crashes the frontend JSON-LD renderer.
|
||||
*
|
||||
* @param string $value Raw CSV cell value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function validateJsonField(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if ($value === '' || !\is_array(json_decode($value, true))) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a URL to only allow http/https schemes.
|
||||
*
|
||||
* @param string $url Raw CSV cell value
|
||||
*
|
||||
* @return string Sanitized URL or empty string
|
||||
*/
|
||||
private function sanitizeUrl(string $url): string
|
||||
{
|
||||
$url = trim($url);
|
||||
|
||||
if ($url === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
|
||||
|
||||
return \in_array($scheme, ['http', 'https'], true) ? $url : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteOpenGraph
|
||||
* @subpackage com_mokoog
|
||||
* @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
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoOG\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\FormController;
|
||||
|
||||
/**
|
||||
* Controller for a single OG tag record.
|
||||
*
|
||||
* Provides the standard add/edit/save/apply/cancel tasks via FormController,
|
||||
* backed by the existing TagModel (AdminModel) and TagTable.
|
||||
*/
|
||||
class TagController extends FormController
|
||||
{
|
||||
/**
|
||||
* The list view to redirect to after save/cancel.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $view_list = 'tags';
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<html><body bgcolor="#FFFFFF"></body></html>
|
||||
@@ -1 +0,0 @@
|
||||
<html><body bgcolor="#FFFFFF"></body></html>
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteOpenGraph
|
||||
* @subpackage com_mokoog
|
||||
* @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
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoOG\Administrator\View\Tag;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
/**
|
||||
* Edit view for a single OG tag record.
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
/**
|
||||
* The edit form.
|
||||
*
|
||||
* @var \Joomla\CMS\Form\Form
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
/**
|
||||
* The item being edited.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* Display the view.
|
||||
*
|
||||
* @param string $tpl Template name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$this->form = $this->get('Form');
|
||||
$this->item = $this->get('Item');
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the edit toolbar.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
Factory::getApplication()->getInput()->set('hidemainmenu', true);
|
||||
|
||||
$isNew = empty($this->item->id);
|
||||
|
||||
ToolbarHelper::title(
|
||||
Text::_($isNew ? 'COM_MOKOOG_TAG_NEW' : 'COM_MOKOOG_TAG_EDIT'),
|
||||
'bookmark'
|
||||
);
|
||||
|
||||
ToolbarHelper::apply('tag.apply');
|
||||
ToolbarHelper::save('tag.save');
|
||||
ToolbarHelper::cancel('tag.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE');
|
||||
}
|
||||
}
|
||||
@@ -81,8 +81,11 @@ class HtmlView extends BaseHtmlView
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark');
|
||||
ToolbarHelper::addNew('tag.add');
|
||||
ToolbarHelper::editList('tag.edit');
|
||||
ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false);
|
||||
ToolbarHelper::custom('importexport.export', 'download', '', 'COM_MOKOOG_TOOLBAR_EXPORT', false);
|
||||
ToolbarHelper::custom('mokoog.showimport', 'upload', '', 'COM_MOKOOG_TOOLBAR_IMPORT', false);
|
||||
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete');
|
||||
ToolbarHelper::preferences('com_mokoog');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteOpenGraph
|
||||
* @subpackage com_mokoog
|
||||
* @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
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\HTML\HTMLHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
/** @var \Joomla\Component\MokoOG\Administrator\View\Tag\HtmlView $this */
|
||||
|
||||
HTMLHelper::_('behavior.formvalidator');
|
||||
?>
|
||||
<form action="<?php echo Route::_('index.php?option=com_mokoog&view=tag&layout=edit&id=' . (int) ($this->item->id ?? 0)); ?>"
|
||||
method="post" name="adminForm" id="adminForm" class="form-validate" aria-label="<?php echo $this->escape(Text::_('COM_MOKOOG_TAG_EDIT')); ?>">
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<?php echo HTMLHelper::_('uitab.startTabSet', 'mokoogTab', ['active' => 'details']); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'mokoogTab', 'details', Text::_('COM_MOKOOG_TAB_DETAILS')); ?>
|
||||
<?php echo $this->form->renderFieldset('details'); ?>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.addTab', 'mokoogTab', 'seo', Text::_('COM_MOKOOG_FIELDSET_SEO')); ?>
|
||||
<?php echo $this->form->renderFieldset('seo'); ?>
|
||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||
|
||||
<?php echo HTMLHelper::_('uitab.endTabSet'); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="task" value="">
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</form>
|
||||
@@ -85,7 +85,9 @@ $token = Session::getFormToken();
|
||||
<?php echo (int) $item->content_id; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo $this->escape($item->og_title ?: '(' . Text::_('COM_MOKOOG_AUTO_GENERATED') . ')'); ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokoog&task=tag.edit&id=' . (int) $item->id); ?>" title="<?php echo Text::_('JACTION_EDIT'); ?>">
|
||||
<?php echo $this->escape($item->og_title ?: '(' . Text::_('COM_MOKOOG_AUTO_GENERATED') . ')'); ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($item->og_image) : ?>
|
||||
@@ -171,6 +173,23 @@ $token = Session::getFormToken();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Import -->
|
||||
<div id="mokoog-import-panel" style="display:none;" class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h4><?php echo Text::_('COM_MOKOOG_TOOLBAR_IMPORT'); ?></h4>
|
||||
<form action="<?php echo Route::_('index.php?option=com_mokoog&task=importexport.import'); ?>" method="post" enctype="multipart/form-data" class="mt-2">
|
||||
<div class="mb-2">
|
||||
<input type="file" name="jform[csv_file]" accept=".csv" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="icon-upload" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOOG_TOOLBAR_IMPORT'); ?>
|
||||
</button>
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Intercept the batch.generate toolbar button
|
||||
@@ -180,6 +199,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
mokoogBatchGenerate();
|
||||
return;
|
||||
}
|
||||
if (task === 'mokoog.showimport') {
|
||||
var ip = document.getElementById('mokoog-import-panel');
|
||||
if (ip) {
|
||||
ip.style.display = (ip.style.display === 'none' ? 'block' : 'none');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (origSubmitbutton) {
|
||||
origSubmitbutton(task);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteOpenGraph</name>
|
||||
<version>01.05.02</version>
|
||||
<version>01.06.09</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -322,7 +322,14 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
$json = trim($json);
|
||||
|
||||
if ($json === '' || json_decode($json) === null) {
|
||||
if ($json === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Only accept JSON objects/arrays. Scalars (42, "x", true) decode to a
|
||||
// non-null value but would crash the frontend renderer when treated as
|
||||
// an array (writing $decoded['@context'] onto a scalar is a fatal error).
|
||||
if (!\is_array(json_decode($json, true))) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteOpenGraph</name>
|
||||
<version>01.05.02</version>
|
||||
<version>01.06.09</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -139,7 +139,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
}
|
||||
|
||||
// og:locale from current language
|
||||
$langTag = Factory::getLanguage()->getTag();
|
||||
$langTag = $this->getApplication()->getLanguage()->getTag();
|
||||
$ogLocale = str_replace('-', '_', $langTag);
|
||||
$doc->setMetaData('og:locale', $ogLocale, 'property');
|
||||
|
||||
@@ -358,7 +358,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
if (!empty($customSchema)) {
|
||||
$decoded = json_decode($customSchema, true);
|
||||
|
||||
if ($decoded) {
|
||||
// Guard against scalar/invalid payloads — only arrays/objects are
|
||||
// valid JSON-LD. Writing an array offset onto a scalar is fatal.
|
||||
if (\is_array($decoded) && $decoded !== []) {
|
||||
if (empty($decoded['@context'])) {
|
||||
$decoded['@context'] = 'https://schema.org';
|
||||
}
|
||||
@@ -474,7 +476,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
->where($db->quoteName('content_type') . ' = ' . $db->quote($option))
|
||||
->where($db->quoteName('content_id') . ' = ' . (int) $id)
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where('(' . $db->quoteName('language') . ' = ' . $db->quote(Factory::getLanguage()->getTag())
|
||||
->where('(' . $db->quoteName('language') . ' = ' . $db->quote($this->getApplication()->getLanguage()->getTag())
|
||||
. ' OR ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ')')
|
||||
->order('CASE WHEN ' . $db->quoteName('language') . ' = ' . $db->quote('*') . ' THEN 1 ELSE 0 END ASC');
|
||||
|
||||
@@ -494,7 +496,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
private function loadOgDataByType(string $contentType, int $contentId): ?object
|
||||
{
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$lang = Factory::getLanguage()->getTag();
|
||||
$lang = $this->getApplication()->getLanguage()->getTag();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
@@ -521,7 +523,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
private function loadOgDataByMenu(int $menuId): ?object
|
||||
{
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$lang = Factory::getLanguage()->getTag();
|
||||
$lang = $this->getApplication()->getLanguage()->getTag();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
@@ -670,7 +672,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
if (isset($cache[$id])) {
|
||||
// array_key_exists (not isset) so a negative lookup (null) is also cached
|
||||
// and not re-queried on every call within the request.
|
||||
if (\array_key_exists($id, $cache)) {
|
||||
return $cache[$id];
|
||||
}
|
||||
|
||||
@@ -702,8 +706,15 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
private function getArticleDate(int $id, string $field): string
|
||||
{
|
||||
$article = $this->loadArticle($id);
|
||||
$value = $article->$field ?? '';
|
||||
|
||||
return $article->$field ?? '';
|
||||
// Skip zero/empty dates — emitting "0000-00-00 00:00:00" as
|
||||
// article:published_time/modified_time produces invalid metadata.
|
||||
if ($value === '' || str_starts_with($value, '0000-00-00')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -820,6 +831,10 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
*/
|
||||
public function onContentAfterSaveRebuildSitemap(Event $event): void
|
||||
{
|
||||
// Opportunistic maintenance on content save: prune stale generated images
|
||||
// so the generated-image cache cannot grow without bound.
|
||||
ImageHelper::pruneOldFiles();
|
||||
|
||||
if (!$this->params->get('sitemap_enabled', 0)) {
|
||||
return;
|
||||
}
|
||||
@@ -858,6 +873,14 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
// Require article-edit capability — this triggers outbound paid AI calls,
|
||||
// so it must not be reachable by every authenticated back-end user.
|
||||
if (!$app->getIdentity()->authorise('core.edit', 'com_content')
|
||||
&& !$app->getIdentity()->authorise('core.create', 'com_content')) {
|
||||
$event->setArgument('result', ['Forbidden — insufficient permissions']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->params->get('ai_enabled', 0)) {
|
||||
$event->setArgument('result', ['AI generation is not enabled']);
|
||||
return;
|
||||
@@ -902,6 +925,9 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
$http = \Joomla\CMS\Http\HttpFactory::getHttp();
|
||||
|
||||
// Cap how long a hung provider can block the admin request.
|
||||
$timeout = 20;
|
||||
|
||||
if ($provider === 'claude') {
|
||||
$response = $http->post(
|
||||
'https://api.anthropic.com/v1/messages',
|
||||
@@ -914,9 +940,14 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
'Content-Type' => 'application/json',
|
||||
'x-api-key' => $apiKey,
|
||||
'anthropic-version' => '2023-06-01',
|
||||
]
|
||||
],
|
||||
$timeout
|
||||
);
|
||||
|
||||
if ((int) $response->code !== 200) {
|
||||
throw new \RuntimeException('Claude API request failed (HTTP ' . (int) $response->code . ')');
|
||||
}
|
||||
|
||||
$data = json_decode($response->body, true);
|
||||
|
||||
return trim($data['content'][0]['text'] ?? '');
|
||||
@@ -932,9 +963,14 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
||||
[
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $apiKey,
|
||||
]
|
||||
],
|
||||
$timeout
|
||||
);
|
||||
|
||||
if ((int) $response->code !== 200) {
|
||||
throw new \RuntimeException('OpenAI API request failed (HTTP ' . (int) $response->code . ')');
|
||||
}
|
||||
|
||||
$data = json_decode($response->body, true);
|
||||
|
||||
return trim($data['choices'][0]['message']['content'] ?? '');
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteOpenGraph
|
||||
* @subpackage plg_system_mokoog
|
||||
* @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
|
||||
*/
|
||||
|
||||
namespace Joomla\Plugin\System\MokoOG\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Filesystem\Folder;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
class ImageGenerator
|
||||
{
|
||||
private const WIDTH = 1200;
|
||||
private const HEIGHT = 630;
|
||||
private const OUTPUT_DIR = 'images/mokoog/generated';
|
||||
|
||||
/**
|
||||
* Generate an OG image with title text overlaid on a template background.
|
||||
*
|
||||
* @param string $title Article title to overlay
|
||||
* @param string $templateImage Path to template/background image relative to JPATH_ROOT
|
||||
* @param string $fontFile Absolute path to TTF font file
|
||||
* @param int $fontSize Font size in points (default 42)
|
||||
* @param array $fontColor RGB array [r, g, b] (default white)
|
||||
* @param int $quality JPEG quality (default 90)
|
||||
*
|
||||
* @return string Path to generated image relative to JPATH_ROOT, or empty on failure
|
||||
*/
|
||||
public static function generate(
|
||||
string $title,
|
||||
string $templateImage,
|
||||
string $fontFile = '',
|
||||
int $fontSize = 42,
|
||||
array $fontColor = [255, 255, 255],
|
||||
int $quality = 90
|
||||
): string {
|
||||
if (!\extension_loaded('gd')) {
|
||||
Log::add('MokoOG ImageGenerator: GD extension is not loaded. Image generation disabled.', Log::WARNING, 'mokoog');
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$templateAbs = JPATH_ROOT . '/' . ltrim($templateImage, '/');
|
||||
|
||||
if (!is_file($templateAbs)) {
|
||||
Log::add('MokoOG ImageGenerator: Template image not found: ' . $templateImage, Log::WARNING, 'mokoog');
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$fontFile || !is_file($fontFile)) {
|
||||
Log::add('MokoOG ImageGenerator: TTF font file not found: ' . ($fontFile ?: '(not configured)'), Log::WARNING, 'mokoog');
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR;
|
||||
|
||||
if (!is_dir($outputDir) && !Folder::create($outputDir)) {
|
||||
Log::add('MokoOG ImageGenerator: Cannot create output directory: ' . self::OUTPUT_DIR, Log::WARNING, 'mokoog');
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$hash = md5($title . $templateImage . $fontSize);
|
||||
$outputName = 'overlay_' . $hash . '.jpg';
|
||||
$outputPath = $outputDir . '/' . $outputName;
|
||||
$outputRel = self::OUTPUT_DIR . '/' . $outputName;
|
||||
|
||||
// Skip if already generated
|
||||
if (is_file($outputPath)) {
|
||||
return $outputRel;
|
||||
}
|
||||
|
||||
// Load template image
|
||||
$imageInfo = getimagesize($templateAbs);
|
||||
|
||||
if (!$imageInfo) {
|
||||
Log::add('MokoOG ImageGenerator: Cannot read image dimensions: ' . $templateImage, Log::WARNING, 'mokoog');
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$source = match ($imageInfo[2]) {
|
||||
IMAGETYPE_JPEG => imagecreatefromjpeg($templateAbs),
|
||||
IMAGETYPE_PNG => imagecreatefrompng($templateAbs),
|
||||
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? imagecreatefromwebp($templateAbs) : false,
|
||||
default => false,
|
||||
};
|
||||
|
||||
if (!$source) {
|
||||
Log::add('MokoOG ImageGenerator: Failed to load image (unsupported type or corrupt): ' . $templateImage, Log::WARNING, 'mokoog');
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create output canvas at target dimensions
|
||||
$canvas = imagecreatetruecolor(self::WIDTH, self::HEIGHT);
|
||||
|
||||
imagecopyresampled(
|
||||
$canvas,
|
||||
$source,
|
||||
0, 0, 0, 0,
|
||||
self::WIDTH, self::HEIGHT,
|
||||
$imageInfo[0], $imageInfo[1]
|
||||
);
|
||||
|
||||
imagedestroy($source);
|
||||
|
||||
// Semi-transparent overlay for text readability
|
||||
$overlay = imagecolorallocatealpha($canvas, 0, 0, 0, 64);
|
||||
imagefilledrectangle($canvas, 0, (int) (self::HEIGHT * 0.55), self::WIDTH, self::HEIGHT, $overlay);
|
||||
|
||||
// Render title text with word wrapping
|
||||
$textColor = imagecolorallocate($canvas, $fontColor[0], $fontColor[1], $fontColor[2]);
|
||||
$wrappedTitle = self::wrapText($title, $fontFile, $fontSize, (int) (self::WIDTH * 0.85));
|
||||
$textX = (int) (self::WIDTH * 0.075);
|
||||
$textY = (int) (self::HEIGHT * 0.72);
|
||||
|
||||
imagettftext($canvas, $fontSize, 0, $textX, $textY, $textColor, $fontFile, $wrappedTitle);
|
||||
|
||||
// Save
|
||||
imagejpeg($canvas, $outputPath, $quality);
|
||||
imagedestroy($canvas);
|
||||
|
||||
return $outputRel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text to fit within a maximum pixel width.
|
||||
*
|
||||
* @param string $text Text to wrap
|
||||
* @param string $fontFile Path to TTF font
|
||||
* @param int $fontSize Font size in points
|
||||
* @param int $maxWidth Maximum width in pixels
|
||||
*
|
||||
* @return string Wrapped text with newlines
|
||||
*/
|
||||
private static function wrapText(string $text, string $fontFile, int $fontSize, int $maxWidth): string
|
||||
{
|
||||
$words = explode(' ', $text);
|
||||
$lines = [];
|
||||
$line = '';
|
||||
|
||||
foreach ($words as $word) {
|
||||
$testLine = $line ? $line . ' ' . $word : $word;
|
||||
$bbox = imagettfbbox($fontSize, 0, $fontFile, $testLine);
|
||||
$lineWidth = abs($bbox[4] - $bbox[0]);
|
||||
|
||||
if ($lineWidth > $maxWidth && $line !== '') {
|
||||
$lines[] = $line;
|
||||
$line = $word;
|
||||
} else {
|
||||
$line = $testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if ($line !== '') {
|
||||
$lines[] = $line;
|
||||
}
|
||||
|
||||
// Limit to 3 lines, truncate last line if needed
|
||||
if (\count($lines) > 3) {
|
||||
$lines = \array_slice($lines, 0, 3);
|
||||
|
||||
if (mb_strlen($lines[2]) > 3) {
|
||||
$lines[2] = mb_substr($lines[2], 0, -3) . '...';
|
||||
} else {
|
||||
$lines[2] .= '...';
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ namespace Joomla\Plugin\System\MokoOG\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Filesystem\File;
|
||||
use Joomla\CMS\Filesystem\Folder;
|
||||
use Joomla\Filesystem\File;
|
||||
use Joomla\Filesystem\Folder;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
class ImageHelper
|
||||
@@ -300,6 +300,39 @@ class ImageHelper
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune generated images older than the given age, to bound disk usage.
|
||||
*
|
||||
* The generated-image cache is never otherwise cleaned, so without this it
|
||||
* grows unbounded over time.
|
||||
*
|
||||
* @param int $maxAgeDays Delete generated files older than this (default 30)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function pruneOldFiles(int $maxAgeDays = 30): void
|
||||
{
|
||||
$dir = JPATH_ROOT . '/' . self::OUTPUT_DIR;
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cutoff = time() - ($maxAgeDays * 86400);
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()
|
||||
&& $file->getFilename() !== 'index.html'
|
||||
&& $file->getMTime() < $cutoff) {
|
||||
File::delete($file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an image meets minimum OG size requirements.
|
||||
*
|
||||
|
||||
@@ -142,23 +142,6 @@ class JsonLdBuilder
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Organization schema from site configuration.
|
||||
*
|
||||
* @param string $siteName Site name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function buildOrganization(string $siteName): array
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Organization',
|
||||
'name' => $siteName,
|
||||
'url' => Uri::root(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Product schema for a MokoSuiteShop product.
|
||||
*
|
||||
|
||||
@@ -37,12 +37,20 @@ class SitemapBuilder
|
||||
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
|
||||
// Only include content the public (guest, user id 0) can view — never
|
||||
// leak registered/special-access articles into the public sitemap.
|
||||
$publicLevels = array_map('intval', \Joomla\CMS\Access\Access::getAuthorisedViewLevels(0));
|
||||
|
||||
// Get all published articles
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['a.id', 'a.alias', 'a.catid', 'a.modified', 'a.language']))
|
||||
->from($db->quoteName('#__content', 'a'))
|
||||
->where($db->quoteName('a.state') . ' = 1');
|
||||
|
||||
if (!empty($publicLevels)) {
|
||||
$query->where($db->quoteName('a.access') . ' IN (' . implode(',', $publicLevels) . ')');
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
$articles = $db->loadObjectList();
|
||||
|
||||
@@ -104,7 +112,19 @@ class SitemapBuilder
|
||||
public static function writeToFile(string $xml): bool
|
||||
{
|
||||
$path = JPATH_ROOT . '/sitemap.xml';
|
||||
$tmp = $path . '.' . uniqid('tmp', true);
|
||||
|
||||
return (bool) file_put_contents($path, $xml);
|
||||
if (file_put_contents($tmp, $xml) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Atomic replace so concurrent saves never expose a half-written sitemap.
|
||||
if (!@rename($tmp, $path)) {
|
||||
@unlink($tmp);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteOpenGraph</name>
|
||||
<version>01.05.02</version>
|
||||
<version>01.06.09</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteOpenGraph</name>
|
||||
<packagename>mokoog</packagename>
|
||||
<version>01.05.02</version>
|
||||
<version>01.06.09</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -31,7 +31,7 @@
|
||||
</languages>
|
||||
|
||||
<updateservers>
|
||||
<server type="extension" name="MokoSuiteOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/updates.xml</server>
|
||||
<server type="extension" name="MokoSuiteOpenGraph Updates">https://git.mokoconsulting.tech/api/packages/MokoConsulting/generic/MokoSuiteOpenGraph/latest/updates.xml</server>
|
||||
</updateservers>
|
||||
<dlid prefix="dlid=" suffix=""/>
|
||||
<blockChildUninstall>true</blockChildUninstall>
|
||||
|
||||
Reference in New Issue
Block a user