820 lines
22 KiB
PHP
820 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* @package MokoSuite
|
|
* @subpackage plg_system_mokosuite
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: Joomla.Plugin
|
|
* INGROUP: MokoSuite
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
|
|
* PATH: /src/packages/plg_system_mokosuite/Service/ContentSyncReceiver.php
|
|
* VERSION: 02.34.80
|
|
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
|
|
*/
|
|
|
|
namespace Moko\Plugin\Task\MokoSuiteSync\Service;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Log\Log;
|
|
|
|
/**
|
|
* Content Sync Receiver — applies incoming sync payload to the local site.
|
|
*
|
|
* Processes categories, articles, menu types, menu items, and modules
|
|
* from a JSON payload sent by a source MokoSuite site. Content is matched
|
|
* by alias (upsert pattern): existing content is updated, new content
|
|
* is inserted.
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
class ContentSyncReceiver
|
|
{
|
|
/**
|
|
* @var \Joomla\Database\DatabaseInterface
|
|
* @since 02.21.00
|
|
*/
|
|
private $db;
|
|
|
|
/**
|
|
* Warnings collected during sync.
|
|
*
|
|
* @var array
|
|
* @since 02.21.00
|
|
*/
|
|
private array $warnings = [];
|
|
|
|
/**
|
|
* Cache of resolved category paths → local IDs.
|
|
*
|
|
* @var array
|
|
* @since 02.21.00
|
|
*/
|
|
private array $catPathCache = [];
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
public function __construct($db = null)
|
|
{
|
|
$this->db = $db ?: Factory::getDbo();
|
|
}
|
|
|
|
/**
|
|
* Process an incoming sync payload.
|
|
*
|
|
* @param array $payload Decoded JSON payload from the source site
|
|
*
|
|
* @return array Result summary with per-type counts and warnings
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
public function receive(array $payload): array
|
|
{
|
|
if (empty($payload['mokosuite_sync']))
|
|
{
|
|
return ['status' => 'error', 'message' => 'Invalid payload — missing mokosuite_sync version'];
|
|
}
|
|
|
|
$this->warnings = [];
|
|
$results = [];
|
|
|
|
// Apply in dependency order
|
|
try
|
|
{
|
|
$results['categories'] = $this->applyCategories($payload['categories'] ?? []);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$results['categories'] = ['error' => $e->getMessage()];
|
|
$this->warnings[] = 'Categories failed: ' . $e->getMessage();
|
|
}
|
|
|
|
try
|
|
{
|
|
$results['articles'] = $this->applyArticles($payload['articles'] ?? []);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$results['articles'] = ['error' => $e->getMessage()];
|
|
$this->warnings[] = 'Articles failed: ' . $e->getMessage();
|
|
}
|
|
|
|
try
|
|
{
|
|
$results['menu_types'] = $this->applyMenuTypes($payload['menu_types'] ?? []);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$results['menu_types'] = ['error' => $e->getMessage()];
|
|
$this->warnings[] = 'Menu types failed: ' . $e->getMessage();
|
|
}
|
|
|
|
try
|
|
{
|
|
$results['menu_items'] = $this->applyMenuItems($payload['menu_items'] ?? []);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$results['menu_items'] = ['error' => $e->getMessage()];
|
|
$this->warnings[] = 'Menu items failed: ' . $e->getMessage();
|
|
}
|
|
|
|
try
|
|
{
|
|
$results['modules'] = $this->applyModules($payload['modules'] ?? []);
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$results['modules'] = ['error' => $e->getMessage()];
|
|
$this->warnings[] = 'Modules failed: ' . $e->getMessage();
|
|
}
|
|
|
|
Log::add(
|
|
sprintf('Content sync received from %s', $payload['source_site'] ?? 'unknown'),
|
|
Log::INFO,
|
|
'mokosuite'
|
|
);
|
|
|
|
return [
|
|
'status' => 'ok',
|
|
'message' => 'Sync applied',
|
|
'source_site' => $payload['source_site'] ?? '',
|
|
'results' => $results,
|
|
'warnings' => $this->warnings,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Apply categories — sorted by path depth (shallow first).
|
|
*
|
|
* @param array $categories Category data from payload
|
|
*
|
|
* @return array ['inserted' => N, 'updated' => N]
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function applyCategories(array $categories): array
|
|
{
|
|
$db = $this->db;
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
// Sort by path depth — parents before children
|
|
usort($categories, function ($a, $b) {
|
|
return substr_count($a['path'], '/') - substr_count($b['path'], '/');
|
|
});
|
|
|
|
foreach ($categories as $cat)
|
|
{
|
|
$alias = $cat['alias'] ?? '';
|
|
$path = $cat['path'] ?? $alias;
|
|
|
|
if (empty($alias) || !preg_match('/^[a-z0-9\-\/]+$/i', $path))
|
|
{
|
|
$this->warnings[] = 'Skipped category with invalid alias/path: ' . $alias;
|
|
continue;
|
|
}
|
|
|
|
// Resolve parent ID from path
|
|
$parentId = 1; // Root
|
|
$pathParts = explode('/', $path);
|
|
|
|
if (count($pathParts) > 1)
|
|
{
|
|
$parentPath = implode('/', array_slice($pathParts, 0, -1));
|
|
$parentId = $this->resolveCategoryPath($parentPath);
|
|
|
|
if ($parentId === 0)
|
|
{
|
|
$this->warnings[] = 'Parent category not found for: ' . $path;
|
|
$parentId = 1;
|
|
}
|
|
}
|
|
|
|
// Check if category exists
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__categories'))
|
|
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
|
|
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
|
|
->where($db->quoteName('parent_id') . ' = ' . (int) $parentId);
|
|
|
|
$db->setQuery($query);
|
|
$existingId = (int) $db->loadResult();
|
|
|
|
$now = Factory::getDate()->toSql();
|
|
|
|
if ($existingId)
|
|
{
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__categories'))
|
|
->set($db->quoteName('title') . ' = ' . $db->quote($cat['title']))
|
|
->set($db->quoteName('description') . ' = ' . $db->quote($cat['description'] ?? ''))
|
|
->set($db->quoteName('published') . ' = ' . (int) ($cat['published'] ?? 1))
|
|
->set($db->quoteName('access') . ' = ' . (int) ($cat['access'] ?? 1))
|
|
->set($db->quoteName('language') . ' = ' . $db->quote($cat['language'] ?? '*'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($cat['params'] ?? new \stdClass)))
|
|
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($cat['metadata'] ?? new \stdClass)))
|
|
->set($db->quoteName('modified_time') . ' = ' . $db->quote($now))
|
|
->where($db->quoteName('id') . ' = ' . $existingId);
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
$updated++;
|
|
|
|
$this->catPathCache[$path] = $existingId;
|
|
}
|
|
else
|
|
{
|
|
$obj = (object) [
|
|
'title' => $cat['title'],
|
|
'alias' => $alias,
|
|
'path' => $path,
|
|
'extension' => 'com_content',
|
|
'description' => $cat['description'] ?? '',
|
|
'published' => (int) ($cat['published'] ?? 1),
|
|
'access' => (int) ($cat['access'] ?? 1),
|
|
'language' => $cat['language'] ?? '*',
|
|
'params' => json_encode($cat['params'] ?? new \stdClass),
|
|
'metadata' => json_encode($cat['metadata'] ?? new \stdClass),
|
|
'parent_id' => $parentId,
|
|
'level' => count($pathParts),
|
|
'lft' => 0,
|
|
'rgt' => 0,
|
|
'created_time' => $now,
|
|
'modified_time' => $now,
|
|
];
|
|
|
|
$db->insertObject('#__categories', $obj, 'id');
|
|
$inserted++;
|
|
|
|
$this->catPathCache[$path] = (int) $obj->id;
|
|
|
|
// Rebuild category tree for this extension
|
|
$this->rebuildCategoryTree();
|
|
}
|
|
}
|
|
|
|
return ['inserted' => $inserted, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* Apply articles — resolve category by alias path, upsert by alias.
|
|
*
|
|
* @param array $articles Article data from payload
|
|
*
|
|
* @return array ['inserted' => N, 'updated' => N]
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function applyArticles(array $articles): array
|
|
{
|
|
$db = $this->db;
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
foreach ($articles as $article)
|
|
{
|
|
$alias = $article['alias'] ?? '';
|
|
|
|
if (empty($alias))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Resolve category
|
|
$catPath = $article['catid_alias_path'] ?? 'uncategorised';
|
|
$catId = $this->resolveCategoryPath($catPath);
|
|
|
|
if ($catId === 0)
|
|
{
|
|
$catId = 2; // Joomla's built-in Uncategorised
|
|
$this->warnings[] = 'Category "' . $catPath . '" not found for article "' . $alias . '" — assigned to Uncategorised';
|
|
}
|
|
|
|
// Check if article exists
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__content'))
|
|
->where($db->quoteName('alias') . ' = ' . $db->quote($alias));
|
|
|
|
$db->setQuery($query);
|
|
$existingId = (int) $db->loadResult();
|
|
|
|
$now = Factory::getDate()->toSql();
|
|
|
|
if ($existingId)
|
|
{
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__content'))
|
|
->set($db->quoteName('title') . ' = ' . $db->quote($article['title']))
|
|
->set($db->quoteName('introtext') . ' = ' . $db->quote($article['introtext'] ?? ''))
|
|
->set($db->quoteName('fulltext') . ' = ' . $db->quote($article['fulltext'] ?? ''))
|
|
->set($db->quoteName('state') . ' = ' . (int) ($article['state'] ?? 1))
|
|
->set($db->quoteName('catid') . ' = ' . $catId)
|
|
->set($db->quoteName('access') . ' = ' . (int) ($article['access'] ?? 1))
|
|
->set($db->quoteName('language') . ' = ' . $db->quote($article['language'] ?? '*'))
|
|
->set($db->quoteName('featured') . ' = ' . (int) ($article['featured'] ?? 0))
|
|
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($article['metadata'] ?? new \stdClass)))
|
|
->set($db->quoteName('attribs') . ' = ' . $db->quote(json_encode($article['attribs'] ?? new \stdClass)))
|
|
->set($db->quoteName('images') . ' = ' . $db->quote(json_encode($article['images'] ?? new \stdClass)))
|
|
->set($db->quoteName('urls') . ' = ' . $db->quote(json_encode($article['urls'] ?? new \stdClass)))
|
|
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
|
->where($db->quoteName('id') . ' = ' . $existingId);
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
$updated++;
|
|
}
|
|
else
|
|
{
|
|
$obj = (object) [
|
|
'title' => $article['title'],
|
|
'alias' => $alias,
|
|
'introtext' => $article['introtext'] ?? '',
|
|
'fulltext' => $article['fulltext'] ?? '',
|
|
'state' => (int) ($article['state'] ?? 1),
|
|
'catid' => $catId,
|
|
'access' => (int) ($article['access'] ?? 1),
|
|
'language' => $article['language'] ?? '*',
|
|
'featured' => (int) ($article['featured'] ?? 0),
|
|
'publish_up' => $article['publish_up'] ?? $now,
|
|
'publish_down' => $article['publish_down'],
|
|
'metadata' => json_encode($article['metadata'] ?? new \stdClass),
|
|
'attribs' => json_encode($article['attribs'] ?? new \stdClass),
|
|
'images' => json_encode($article['images'] ?? new \stdClass),
|
|
'urls' => json_encode($article['urls'] ?? new \stdClass),
|
|
'created' => $now,
|
|
'modified' => $now,
|
|
];
|
|
|
|
$db->insertObject('#__content', $obj, 'id');
|
|
$inserted++;
|
|
}
|
|
}
|
|
|
|
return ['inserted' => $inserted, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* Apply menu types — insert if not exists.
|
|
*
|
|
* @param array $menuTypes Menu type data from payload
|
|
*
|
|
* @return array ['inserted' => N, 'updated' => N]
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function applyMenuTypes(array $menuTypes): array
|
|
{
|
|
$db = $this->db;
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
foreach ($menuTypes as $mt)
|
|
{
|
|
$menutype = $mt['menutype'] ?? '';
|
|
|
|
if (empty($menutype))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__menu_types'))
|
|
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
|
|
|
|
$db->setQuery($query);
|
|
|
|
if ($db->loadResult())
|
|
{
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__menu_types'))
|
|
->set($db->quoteName('title') . ' = ' . $db->quote($mt['title'] ?? $menutype))
|
|
->set($db->quoteName('description') . ' = ' . $db->quote($mt['description'] ?? ''))
|
|
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
$updated++;
|
|
}
|
|
else
|
|
{
|
|
$obj = (object) [
|
|
'title' => $mt['title'] ?? $menutype,
|
|
'menutype' => $menutype,
|
|
'description' => $mt['description'] ?? '',
|
|
];
|
|
|
|
$db->insertObject('#__menu_types', $obj);
|
|
$inserted++;
|
|
}
|
|
}
|
|
|
|
return ['inserted' => $inserted, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* Apply menu items — resolve parent aliases and {catid:path} tokens.
|
|
*
|
|
* @param array $items Menu item data from payload
|
|
*
|
|
* @return array ['inserted' => N, 'updated' => N]
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function applyMenuItems(array $items): array
|
|
{
|
|
$db = $this->db;
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
// Sort: root items first, then children
|
|
usort($items, function ($a, $b) {
|
|
$aIsRoot = empty($a['parent_alias']);
|
|
$bIsRoot = empty($b['parent_alias']);
|
|
|
|
if ($aIsRoot && !$bIsRoot) return -1;
|
|
if (!$aIsRoot && $bIsRoot) return 1;
|
|
|
|
return 0;
|
|
});
|
|
|
|
// Resolve component IDs
|
|
$compQuery = $db->getQuery(true)
|
|
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
|
|
->from($db->quoteName('#__extensions'))
|
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
|
|
$db->setQuery($compQuery);
|
|
$compMap = [];
|
|
|
|
foreach ($db->loadAssocList() ?: [] as $c)
|
|
{
|
|
$compMap[$c['element']] = (int) $c['extension_id'];
|
|
}
|
|
|
|
foreach ($items as $item)
|
|
{
|
|
$alias = $item['alias'] ?? '';
|
|
$menutype = $item['menutype'] ?? '';
|
|
|
|
if (empty($alias) || empty($menutype))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Resolve parent
|
|
$parentId = 1; // Root menu item
|
|
|
|
if (!empty($item['parent_alias']))
|
|
{
|
|
$parentId = $this->resolveMenuAlias($menutype, $item['parent_alias']);
|
|
|
|
if ($parentId === 0)
|
|
{
|
|
$this->warnings[] = 'Parent menu item "' . $item['parent_alias'] . '" not found for "' . $alias . '"';
|
|
$parentId = 1;
|
|
}
|
|
}
|
|
|
|
// Resolve {catid:path} tokens in link
|
|
$link = $item['link'] ?? '';
|
|
|
|
if (preg_match_all('/\{catid:([^}]+)\}/', $link, $matches))
|
|
{
|
|
foreach ($matches[1] as $i => $catPath)
|
|
{
|
|
$localCatId = $this->resolveCategoryPath($catPath);
|
|
|
|
if ($localCatId > 0)
|
|
{
|
|
$link = str_replace($matches[0][$i], (string) $localCatId, $link);
|
|
}
|
|
else
|
|
{
|
|
$this->warnings[] = 'Could not resolve {catid:' . $catPath . '} in menu item "' . $alias . '"';
|
|
$link = str_replace($matches[0][$i], '0', $link);
|
|
}
|
|
}
|
|
}
|
|
|
|
$componentId = $compMap[$item['component_name'] ?? ''] ?? 0;
|
|
|
|
// Check if menu item exists
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__menu'))
|
|
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
|
|
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
|
|
->where($db->quoteName('client_id') . ' = 0');
|
|
|
|
$db->setQuery($query);
|
|
$existingId = (int) $db->loadResult();
|
|
|
|
if ($existingId)
|
|
{
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__menu'))
|
|
->set($db->quoteName('title') . ' = ' . $db->quote($item['title']))
|
|
->set($db->quoteName('link') . ' = ' . $db->quote($link))
|
|
->set($db->quoteName('type') . ' = ' . $db->quote($item['type'] ?? 'component'))
|
|
->set($db->quoteName('published') . ' = ' . (int) ($item['published'] ?? 1))
|
|
->set($db->quoteName('access') . ' = ' . (int) ($item['access'] ?? 1))
|
|
->set($db->quoteName('language') . ' = ' . $db->quote($item['language'] ?? '*'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($item['params'] ?? new \stdClass)))
|
|
->set($db->quoteName('parent_id') . ' = ' . $parentId)
|
|
->set($db->quoteName('component_id') . ' = ' . $componentId)
|
|
->set($db->quoteName('home') . ' = ' . (int) ($item['home'] ?? 0))
|
|
->where($db->quoteName('id') . ' = ' . $existingId);
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
$updated++;
|
|
}
|
|
else
|
|
{
|
|
$obj = (object) [
|
|
'menutype' => $menutype,
|
|
'title' => $item['title'],
|
|
'alias' => $alias,
|
|
'path' => $alias,
|
|
'link' => $link,
|
|
'type' => $item['type'] ?? 'component',
|
|
'published' => (int) ($item['published'] ?? 1),
|
|
'parent_id' => $parentId,
|
|
'level' => $parentId <= 1 ? 1 : 2,
|
|
'component_id' => $componentId,
|
|
'access' => (int) ($item['access'] ?? 1),
|
|
'language' => $item['language'] ?? '*',
|
|
'params' => json_encode($item['params'] ?? new \stdClass),
|
|
'home' => (int) ($item['home'] ?? 0),
|
|
'client_id' => 0,
|
|
'lft' => 0,
|
|
'rgt' => 0,
|
|
];
|
|
|
|
$db->insertObject('#__menu', $obj, 'id');
|
|
$inserted++;
|
|
}
|
|
}
|
|
|
|
// Rebuild menu tree for each affected menutype
|
|
$affectedMenuTypes = array_unique(array_column($items, 'menutype'));
|
|
|
|
foreach ($affectedMenuTypes as $mt)
|
|
{
|
|
$this->rebuildMenuTree($mt);
|
|
}
|
|
|
|
return ['inserted' => $inserted, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* Apply modules — upsert by title+position+client_id, rebuild menu assignments.
|
|
*
|
|
* @param array $modules Module data from payload
|
|
*
|
|
* @return array ['inserted' => N, 'updated' => N]
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function applyModules(array $modules): array
|
|
{
|
|
$db = $this->db;
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
foreach ($modules as $mod)
|
|
{
|
|
$title = $mod['title'] ?? '';
|
|
$position = $mod['position'] ?? '';
|
|
$clientId = (int) ($mod['client_id'] ?? 0);
|
|
|
|
if (empty($title))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check existence by title + position + client_id
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__modules'))
|
|
->where($db->quoteName('title') . ' = ' . $db->quote($title))
|
|
->where($db->quoteName('position') . ' = ' . $db->quote($position))
|
|
->where($db->quoteName('client_id') . ' = ' . $clientId);
|
|
|
|
$db->setQuery($query);
|
|
$existingId = (int) $db->loadResult();
|
|
|
|
if ($existingId)
|
|
{
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__modules'))
|
|
->set($db->quoteName('module') . ' = ' . $db->quote($mod['module'] ?? ''))
|
|
->set($db->quoteName('content') . ' = ' . $db->quote($mod['content'] ?? ''))
|
|
->set($db->quoteName('published') . ' = ' . (int) ($mod['published'] ?? 1))
|
|
->set($db->quoteName('access') . ' = ' . (int) ($mod['access'] ?? 1))
|
|
->set($db->quoteName('language') . ' = ' . $db->quote($mod['language'] ?? '*'))
|
|
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($mod['params'] ?? new \stdClass)))
|
|
->where($db->quoteName('id') . ' = ' . $existingId);
|
|
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
$updated++;
|
|
|
|
$moduleId = $existingId;
|
|
}
|
|
else
|
|
{
|
|
$obj = (object) [
|
|
'title' => $title,
|
|
'module' => $mod['module'] ?? '',
|
|
'position' => $position,
|
|
'content' => $mod['content'] ?? '',
|
|
'published' => (int) ($mod['published'] ?? 1),
|
|
'access' => (int) ($mod['access'] ?? 1),
|
|
'language' => $mod['language'] ?? '*',
|
|
'params' => json_encode($mod['params'] ?? new \stdClass),
|
|
'client_id' => $clientId,
|
|
'ordering' => 0,
|
|
];
|
|
|
|
$db->insertObject('#__modules', $obj, 'id');
|
|
$inserted++;
|
|
$moduleId = (int) $obj->id;
|
|
}
|
|
|
|
// Rebuild menu assignments
|
|
$query = $db->getQuery(true)
|
|
->delete($db->quoteName('#__modules_menu'))
|
|
->where($db->quoteName('moduleid') . ' = ' . $moduleId);
|
|
$db->setQuery($query);
|
|
$db->execute();
|
|
|
|
$assignment = $mod['menu_assignment'] ?? [];
|
|
$assignType = (int) ($assignment['assignment'] ?? 0);
|
|
$aliases = $assignment['menu_item_aliases'] ?? [];
|
|
|
|
if ($assignType === 0 || empty($aliases))
|
|
{
|
|
// All pages
|
|
$obj = (object) ['moduleid' => $moduleId, 'menuid' => 0];
|
|
$db->insertObject('#__modules_menu', $obj);
|
|
}
|
|
else
|
|
{
|
|
foreach ($aliases as $aliasRef)
|
|
{
|
|
// Format: "menutype:alias"
|
|
$parts = explode(':', $aliasRef, 2);
|
|
|
|
if (count($parts) !== 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$menuId = $this->resolveMenuAlias($parts[0], $parts[1]);
|
|
|
|
if ($menuId > 0)
|
|
{
|
|
$menuidValue = $assignType === -1 ? -$menuId : $menuId;
|
|
$obj = (object) ['moduleid' => $moduleId, 'menuid' => $menuidValue];
|
|
$db->insertObject('#__modules_menu', $obj);
|
|
}
|
|
else
|
|
{
|
|
$this->warnings[] = 'Module "' . $title . '": menu item "' . $aliasRef . '" not found';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ['inserted' => $inserted, 'updated' => $updated];
|
|
}
|
|
|
|
/**
|
|
* Resolve a category alias path to a local category ID.
|
|
*
|
|
* @param string $path Slash-delimited alias path (e.g. "blog/news")
|
|
*
|
|
* @return int Category ID, or 0 if not found
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function resolveCategoryPath(string $path): int
|
|
{
|
|
if (isset($this->catPathCache[$path]))
|
|
{
|
|
return $this->catPathCache[$path];
|
|
}
|
|
|
|
$db = $this->db;
|
|
$segments = explode('/', $path);
|
|
$parentId = 1;
|
|
|
|
foreach ($segments as $segment)
|
|
{
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__categories'))
|
|
->where($db->quoteName('alias') . ' = ' . $db->quote($segment))
|
|
->where($db->quoteName('parent_id') . ' = ' . $parentId)
|
|
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'));
|
|
|
|
$db->setQuery($query);
|
|
$id = (int) $db->loadResult();
|
|
|
|
if ($id === 0)
|
|
{
|
|
$this->catPathCache[$path] = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
$parentId = $id;
|
|
}
|
|
|
|
$this->catPathCache[$path] = $parentId;
|
|
|
|
return $parentId;
|
|
}
|
|
|
|
/**
|
|
* Resolve a menu item alias to a local menu ID.
|
|
*
|
|
* @param string $menutype Menu type key
|
|
* @param string $alias Menu item alias
|
|
*
|
|
* @return int Menu item ID, or 0 if not found
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function resolveMenuAlias(string $menutype, string $alias): int
|
|
{
|
|
$db = $this->db;
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('id'))
|
|
->from($db->quoteName('#__menu'))
|
|
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
|
|
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
|
|
->where($db->quoteName('client_id') . ' = 0');
|
|
|
|
$db->setQuery($query);
|
|
|
|
return (int) $db->loadResult();
|
|
}
|
|
|
|
/**
|
|
* Rebuild the nested set (lft/rgt) for the category tree.
|
|
*
|
|
* Uses Joomla's built-in Table rebuild method.
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function rebuildCategoryTree(): void
|
|
{
|
|
try
|
|
{
|
|
$table = \Joomla\CMS\Table\Table::getInstance('Category');
|
|
$table->rebuild();
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->warnings[] = 'Category tree rebuild failed: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rebuild the nested set (lft/rgt) for a menu type.
|
|
*
|
|
* @param string $menutype Menu type to rebuild
|
|
*
|
|
* @return void
|
|
*
|
|
* @since 02.21.00
|
|
*/
|
|
private function rebuildMenuTree(string $menutype): void
|
|
{
|
|
try
|
|
{
|
|
$table = \Joomla\CMS\Table\Table::getInstance('Menu');
|
|
$table->rebuild();
|
|
}
|
|
catch (\Throwable $e)
|
|
{
|
|
$this->warnings[] = 'Menu tree rebuild failed for "' . $menutype . '": ' . $e->getMessage();
|
|
}
|
|
}
|
|
}
|