diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml
index 881e09d..820bdad 100644
--- a/src/packages/plg_task_mokowaassync/forms/sync_params.xml
+++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml
@@ -1,8 +1,75 @@
diff --git a/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini
index 049f286..bb34177 100644
--- a/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini
+++ b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini
@@ -3,6 +3,35 @@
; SPDX-License-Identifier: GPL-3.0-or-later
PLG_TASK_MOKOWAASSYNC="Task - MokoWaaS Content Sync"
-PLG_TASK_MOKOWAASSYNC_DESC="Scheduled task to push content (articles, categories, menus, modules) to remote MokoWaaS sites."
+PLG_TASK_MOKOWAASSYNC_DESC="Scheduled task to sync content to a remote MokoWaaS site via the Joomla API. Each task instance syncs to one target."
PLG_TASK_MOKOWAASSYNC_SYNC_TITLE="MokoWaaS Content Sync"
-PLG_TASK_MOKOWAASSYNC_SYNC_DESC="Push site content to all configured sync targets. Targets are configured in the MokoWaaS system plugin settings."
+PLG_TASK_MOKOWAASSYNC_SYNC_DESC="Sync selected content types to a single remote target site via the Joomla REST API."
+
+; ===== Target fieldset =====
+PLG_TASK_MOKOWAASSYNC_FIELDSET_TARGET="Sync Target"
+PLG_TASK_MOKOWAASSYNC_TARGET_URL_LABEL="Target Site URL"
+PLG_TASK_MOKOWAASSYNC_TARGET_URL_DESC="Base URL of the remote Joomla site to sync to (e.g. https://demo.example.com)."
+PLG_TASK_MOKOWAASSYNC_API_TOKEN_LABEL="API Token"
+PLG_TASK_MOKOWAASSYNC_API_TOKEN_DESC="Joomla API token (Bearer token) for authenticating with the target site's REST API."
+PLG_TASK_MOKOWAASSYNC_API_USER_LABEL="API User"
+PLG_TASK_MOKOWAASSYNC_API_USER_DESC="Optional username on the target site. Used for logging purposes only."
+
+; ===== Content types fieldset =====
+PLG_TASK_MOKOWAASSYNC_FIELDSET_CONTENT="Content to Sync"
+PLG_TASK_MOKOWAASSYNC_SYNC_ARTICLES_LABEL="Articles"
+PLG_TASK_MOKOWAASSYNC_SYNC_ARTICLES_DESC="Sync articles (com_content). Deletes all articles on the target, then pushes exact copies from this site."
+PLG_TASK_MOKOWAASSYNC_SYNC_CATEGORIES_LABEL="Categories"
+PLG_TASK_MOKOWAASSYNC_SYNC_CATEGORIES_DESC="Sync content categories. Ensures category structure matches this site."
+PLG_TASK_MOKOWAASSYNC_SYNC_MENUS_LABEL="Menus"
+PLG_TASK_MOKOWAASSYNC_SYNC_MENUS_DESC="Sync menu items. Deletes all menu items on the target, then pushes exact copies from this site."
+PLG_TASK_MOKOWAASSYNC_SYNC_MODULES_LABEL="Modules"
+PLG_TASK_MOKOWAASSYNC_SYNC_MODULES_DESC="Sync site modules. Pushes module configuration and assignments."
+
+; ===== Files fieldset =====
+PLG_TASK_MOKOWAASSYNC_FIELDSET_FILES="Files to Sync"
+PLG_TASK_MOKOWAASSYNC_SYNC_IMAGES_LABEL="Images (/images/)"
+PLG_TASK_MOKOWAASSYNC_SYNC_IMAGES_DESC="Sync the /images/ directory to the target site."
+PLG_TASK_MOKOWAASSYNC_SYNC_FILES_LABEL="Files (/files/)"
+PLG_TASK_MOKOWAASSYNC_SYNC_FILES_DESC="Sync the /files/ directory to the target site."
+PLG_TASK_MOKOWAASSYNC_SYNC_MEDIA_LABEL="Media (/media/)"
+PLG_TASK_MOKOWAASSYNC_SYNC_MEDIA_DESC="Sync the /media/ directory to the target site. Be careful — this includes extension assets."
diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php
index 82015fe..8f452fa 100644
--- a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php
+++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php
@@ -20,11 +20,16 @@ use Joomla\Event\SubscriberInterface;
/**
* MokoWaaS Content Sync — Joomla Scheduled Task Plugin.
*
- * Pushes site content (articles, categories, menus, modules) to
- * configured remote MokoWaaS sites on a schedule. Sync targets are
- * read from the system plugin params (content_sync fieldset).
+ * Syncs selected content types to a single remote Joomla site via the
+ * REST API. Each task instance has its own target URL, API token, and
+ * content type toggles — so multiple targets can sync independently
+ * on different schedules.
*
- * @since 02.27.00
+ * Sync strategy: delete-then-push for articles and menus to avoid
+ * duplicates. Categories are upserted. Files are pushed via the
+ * MokoWaaS sync-receive endpoint.
+ *
+ * @since 02.30.00
*/
final class ContentSync extends CMSPlugin implements SubscriberInterface
{
@@ -48,117 +53,559 @@ final class ContentSync extends CMSPlugin implements SubscriberInterface
}
/**
- * Push content to all configured sync targets.
- *
- * Reads sync_targets from the MokoWaaS system plugin params, then
- * delegates to ContentSyncService. Task-level overrides (if any)
- * are merged on top.
+ * Sync content to the configured target.
*
* @param ExecuteTaskEvent $event The task event
*
* @return int Status::OK or Status::KNOCKOUT
*
- * @since 02.27.00
+ * @since 02.30.00
*/
private function syncContent(ExecuteTaskEvent $event): int
{
- $serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncService.php';
+ $params = $event->getArgument('params');
- if (!file_exists($serviceFile))
+ $targetUrl = rtrim($params->target_url ?? '', '/');
+ $apiToken = $params->api_token ?? '';
+
+ if (empty($targetUrl) || empty($apiToken))
{
- $this->logTask('ContentSyncService.php not found — is plg_system_mokowaas installed?');
+ $this->logTask('Sync target URL or API token not configured');
return Status::KNOCKOUT;
}
- require_once $serviceFile;
+ $errors = 0;
+ $synced = [];
+ $apiBase = $targetUrl . '/api/index.php/v1';
- // Read sync targets from the system plugin params
- $targets = $this->getSyncTargets();
-
- if (empty($targets))
+ // Sync content types based on toggles
+ if ((int) ($params->sync_categories ?? 0) === 1)
{
- $this->logTask('No sync targets configured in MokoWaaS system plugin');
+ $result = $this->syncCategories($apiBase, $apiToken);
+ $synced[] = 'categories:' . ($result ? 'ok' : 'fail');
+ if (!$result) $errors++;
+ }
+
+ if ((int) ($params->sync_articles ?? 0) === 1)
+ {
+ $result = $this->syncArticles($apiBase, $apiToken);
+ $synced[] = 'articles:' . ($result ? 'ok' : 'fail');
+ if (!$result) $errors++;
+ }
+
+ if ((int) ($params->sync_menus ?? 0) === 1)
+ {
+ $result = $this->syncMenus($apiBase, $apiToken);
+ $synced[] = 'menus:' . ($result ? 'ok' : 'fail');
+ if (!$result) $errors++;
+ }
+
+ if ((int) ($params->sync_modules ?? 0) === 1)
+ {
+ $result = $this->syncModules($apiBase, $apiToken);
+ $synced[] = 'modules:' . ($result ? 'ok' : 'fail');
+ if (!$result) $errors++;
+ }
+
+ // File sync via MokoWaaS endpoint
+ $healthToken = $this->getHealthToken();
+
+ if ((int) ($params->sync_images ?? 0) === 1)
+ {
+ $result = $this->syncDirectory('images', $targetUrl, $healthToken);
+ $synced[] = 'images:' . ($result ? 'ok' : 'fail');
+ if (!$result) $errors++;
+ }
+
+ if ((int) ($params->sync_files ?? 0) === 1)
+ {
+ $result = $this->syncDirectory('files', $targetUrl, $healthToken);
+ $synced[] = 'files:' . ($result ? 'ok' : 'fail');
+ if (!$result) $errors++;
+ }
+
+ if ((int) ($params->sync_media ?? 0) === 1)
+ {
+ $result = $this->syncDirectory('media', $targetUrl, $healthToken);
+ $synced[] = 'media:' . ($result ? 'ok' : 'fail');
+ if (!$result) $errors++;
+ }
+
+ $summary = implode(', ', $synced);
+
+ if (empty($synced))
+ {
+ $this->logTask('No content types selected for sync');
return Status::OK;
}
+ $this->logTask("Sync to {$targetUrl}: {$summary}");
+
+ return $errors > 0 && $errors === count($synced) ? Status::KNOCKOUT : Status::OK;
+ }
+
+ // ------------------------------------------------------------------
+ // Joomla API sync methods
+ // ------------------------------------------------------------------
+
+ /**
+ * Sync articles: delete all on target, then push from source.
+ *
+ * @param string $apiBase Target API base URL
+ * @param string $token API bearer token
+ *
+ * @return bool
+ *
+ * @since 02.30.00
+ */
+ private function syncArticles(string $apiBase, string $token): bool
+ {
try
{
- $service = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncService();
- $result = $service->syncAllTargets($targets);
+ // Delete all articles on target
+ $existing = $this->apiGet($apiBase . '/content/articles?page[limit]=0', $token);
- $targetResults = $result['targets'] ?? [];
- $okCount = 0;
- $errCount = 0;
-
- foreach ($targetResults as $tr)
+ if ($existing !== null && !empty($existing->data))
{
- if (($tr['status'] ?? '') === 'ok')
+ foreach ($existing->data as $article)
{
- $okCount++;
- }
- else
- {
- $errCount++;
- $this->logTask('Sync failed for ' . ($tr['target'] ?? 'unknown') . ': ' . ($tr['message'] ?? ''));
+ $this->apiDelete($apiBase . '/content/articles/' . $article->attributes->id, $token);
}
}
- $this->logTask(sprintf('Content sync completed — %d ok, %d failed of %d target(s)', $okCount, $errCount, count($targetResults)));
+ // Read all articles from source
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from($db->quoteName('#__content'))
+ ->where($db->quoteName('state') . ' >= 0');
+ $db->setQuery($query);
+ $articles = $db->loadObjectList();
- return $errCount > 0 && $okCount === 0 ? Status::KNOCKOUT : Status::OK;
+ // Push each article to target
+ foreach ($articles as $article)
+ {
+ $payload = [
+ 'title' => $article->title,
+ 'alias' => $article->alias,
+ 'articletext' => $article->introtext . ($article->fulltext ? '
' . $article->fulltext : ''),
+ 'catid' => $article->catid,
+ 'state' => $article->state,
+ 'language' => $article->language,
+ 'featured' => $article->featured,
+ 'metadesc' => $article->metadesc ?? '',
+ 'metakey' => $article->metakey ?? '',
+ ];
+
+ $this->apiPost($apiBase . '/content/articles', $token, $payload);
+ }
+
+ $this->logTask(sprintf('Synced %d articles', count($articles)));
+
+ return true;
}
catch (\Throwable $e)
{
- $this->logTask('Content sync failed: ' . $e->getMessage());
+ $this->logTask('Article sync failed: ' . $e->getMessage());
- return Status::KNOCKOUT;
+ return false;
}
}
/**
- * Read sync targets from the MokoWaaS system plugin configuration.
+ * Sync categories: push from source, creating or updating on target.
*
- * @return array Array of ['url' => ..., 'token' => ..., 'label' => ...]
+ * @param string $apiBase Target API base URL
+ * @param string $token API bearer token
*
- * @since 02.27.00
+ * @return bool
+ *
+ * @since 02.30.00
*/
- private function getSyncTargets(): array
+ private function syncCategories(string $apiBase, string $token): bool
{
try
{
- $db = Factory::getDbo();
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from($db->quoteName('#__categories'))
+ ->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
+ ->where($db->quoteName('published') . ' >= 0')
+ ->where($db->quoteName('id') . ' > 1')
+ ->order($db->quoteName('lft') . ' ASC');
+ $db->setQuery($query);
+ $categories = $db->loadObjectList();
+
+ foreach ($categories as $cat)
+ {
+ $payload = [
+ 'title' => $cat->title,
+ 'alias' => $cat->alias,
+ 'description' => $cat->description ?? '',
+ 'published' => $cat->published,
+ 'language' => $cat->language,
+ 'extension' => 'com_content',
+ ];
+
+ $this->apiPost($apiBase . '/content/categories', $token, $payload);
+ }
+
+ $this->logTask(sprintf('Synced %d categories', count($categories)));
+
+ return true;
+ }
+ catch (\Throwable $e)
+ {
+ $this->logTask('Category sync failed: ' . $e->getMessage());
+
+ return false;
+ }
+ }
+
+ /**
+ * Sync menus: delete all non-system items on target, then push from source.
+ *
+ * @param string $apiBase Target API base URL
+ * @param string $token API bearer token
+ *
+ * @return bool
+ *
+ * @since 02.30.00
+ */
+ private function syncMenus(string $apiBase, string $token): bool
+ {
+ try
+ {
+ // Delete existing menu items on target
+ $existing = $this->apiGet($apiBase . '/menus/site?page[limit]=0', $token);
+
+ if ($existing !== null && !empty($existing->data))
+ {
+ // Delete in reverse order (children first)
+ $items = array_reverse((array) $existing->data);
+
+ foreach ($items as $item)
+ {
+ $this->apiDelete($apiBase . '/menus/site/' . $item->attributes->id, $token);
+ }
+ }
+
+ // Read menu items from source (site menus only, ordered by lft for tree)
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from($db->quoteName('#__menu'))
+ ->where($db->quoteName('client_id') . ' = 0')
+ ->where($db->quoteName('id') . ' > 1')
+ ->where($db->quoteName('published') . ' >= 0')
+ ->order($db->quoteName('lft') . ' ASC');
+ $db->setQuery($query);
+ $items = $db->loadObjectList();
+
+ foreach ($items as $item)
+ {
+ $payload = [
+ 'title' => $item->title,
+ 'alias' => $item->alias,
+ 'menutype' => $item->menutype,
+ 'type' => $item->type,
+ 'link' => $item->link,
+ 'language' => $item->language,
+ 'published' => $item->published,
+ 'home' => $item->home,
+ ];
+
+ $this->apiPost($apiBase . '/menus/site', $token, $payload);
+ }
+
+ $this->logTask(sprintf('Synced %d menu items', count($items)));
+
+ return true;
+ }
+ catch (\Throwable $e)
+ {
+ $this->logTask('Menu sync failed: ' . $e->getMessage());
+
+ return false;
+ }
+ }
+
+ /**
+ * Sync modules: push from source.
+ *
+ * @param string $apiBase Target API base URL
+ * @param string $token API bearer token
+ *
+ * @return bool
+ *
+ * @since 02.30.00
+ */
+ private function syncModules(string $apiBase, string $token): bool
+ {
+ try
+ {
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from($db->quoteName('#__modules'))
+ ->where($db->quoteName('client_id') . ' = 0')
+ ->where($db->quoteName('published') . ' >= 0');
+ $db->setQuery($query);
+ $modules = $db->loadObjectList();
+
+ foreach ($modules as $mod)
+ {
+ $payload = [
+ 'title' => $mod->title,
+ 'module' => $mod->module,
+ 'position' => $mod->position,
+ 'params' => $mod->params,
+ 'language' => $mod->language,
+ 'published' => $mod->published,
+ ];
+
+ $this->apiPost($apiBase . '/modules/site', $token, $payload);
+ }
+
+ $this->logTask(sprintf('Synced %d modules', count($modules)));
+
+ return true;
+ }
+ catch (\Throwable $e)
+ {
+ $this->logTask('Module sync failed: ' . $e->getMessage());
+
+ return false;
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // File sync via MokoWaaS endpoint
+ // ------------------------------------------------------------------
+
+ /**
+ * Sync a directory to the target site via the MokoWaaS sync-receive endpoint.
+ *
+ * @param string $dir Directory name (images, files, media)
+ * @param string $targetUrl Target site base URL
+ * @param string $token MokoWaaS health token for auth
+ *
+ * @return bool
+ *
+ * @since 02.30.00
+ */
+ private function syncDirectory(string $dir, string $targetUrl, string $token): bool
+ {
+ try
+ {
+ $sourcePath = JPATH_ROOT . '/' . $dir;
+
+ if (!is_dir($sourcePath))
+ {
+ $this->logTask("Directory /{$dir}/ does not exist — skipping");
+
+ return true;
+ }
+
+ // Collect files
+ $files = [];
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($sourcePath, \RecursiveDirectoryIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ foreach ($iterator as $file)
+ {
+ if ($file->isFile())
+ {
+ $relativePath = str_replace('\\', '/', substr($file->getPathname(), strlen($sourcePath) + 1));
+ $files[] = [
+ 'path' => $dir . '/' . $relativePath,
+ 'content' => base64_encode(file_get_contents($file->getPathname())),
+ ];
+ }
+ }
+
+ if (empty($files))
+ {
+ return true;
+ }
+
+ // Send in batches of 50 files
+ $batches = array_chunk($files, 50);
+
+ foreach ($batches as $batch)
+ {
+ $payload = json_encode([
+ 'token' => $token,
+ 'files' => $batch,
+ ]);
+
+ $ch = curl_init($targetUrl . '/?mokowaas=syncreceive');
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 120);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ $response = curl_exec($ch);
+ $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode < 200 || $httpCode >= 300)
+ {
+ $this->logTask("File sync batch failed for /{$dir}/: HTTP {$httpCode}");
+
+ return false;
+ }
+ }
+
+ $this->logTask(sprintf('Synced %d files from /%s/', count($files), $dir));
+
+ return true;
+ }
+ catch (\Throwable $e)
+ {
+ $this->logTask("File sync failed for /{$dir}/: " . $e->getMessage());
+
+ return false;
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // HTTP helpers
+ // ------------------------------------------------------------------
+
+ /**
+ * GET request to the Joomla API.
+ *
+ * @param string $url Full API URL
+ * @param string $token Bearer token
+ *
+ * @return object|null Decoded JSON response
+ *
+ * @since 02.30.00
+ */
+ private function apiGet(string $url, string $token): ?object
+ {
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Authorization: Bearer ' . $token,
+ 'Accept: application/vnd.api+json',
+ ]);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ $response = curl_exec($ch);
+ $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode >= 200 && $httpCode < 300 && $response)
+ {
+ return json_decode($response);
+ }
+
+ return null;
+ }
+
+ /**
+ * POST request to the Joomla API.
+ *
+ * @param string $url Full API URL
+ * @param string $token Bearer token
+ * @param array $payload Data to send
+ *
+ * @return object|null Decoded JSON response
+ *
+ * @since 02.30.00
+ */
+ private function apiPost(string $url, string $token, array $payload): ?object
+ {
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Authorization: Bearer ' . $token,
+ 'Content-Type: application/json',
+ 'Accept: application/vnd.api+json',
+ ]);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ $response = curl_exec($ch);
+ $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($httpCode >= 200 && $httpCode < 300 && $response)
+ {
+ return json_decode($response);
+ }
+
+ return null;
+ }
+
+ /**
+ * DELETE request to the Joomla API.
+ *
+ * @param string $url Full API URL
+ * @param string $token Bearer token
+ *
+ * @return bool
+ *
+ * @since 02.30.00
+ */
+ private function apiDelete(string $url, string $token): bool
+ {
+ $ch = curl_init($url);
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Authorization: Bearer ' . $token,
+ 'Accept: application/vnd.api+json',
+ ]);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_exec($ch);
+ $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ return $httpCode >= 200 && $httpCode < 300;
+ }
+
+ // ------------------------------------------------------------------
+ // Helpers
+ // ------------------------------------------------------------------
+
+ /**
+ * Read the MokoWaaS health API token from the system plugin params.
+ *
+ * @return string
+ *
+ * @since 02.30.00
+ */
+ private function getHealthToken(): string
+ {
+ try
+ {
+ $db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'));
-
$db->setQuery($query);
$raw = $db->loadResult();
+ $params = json_decode($raw ?: '{}', true);
- if (empty($raw))
- {
- return [];
- }
-
- $params = json_decode($raw, true) ?: [];
- $targets = $params['sync_targets'] ?? [];
-
- if (is_string($targets))
- {
- $targets = json_decode($targets, true) ?: [];
- }
-
- return is_array($targets) ? $targets : [];
+ return $params['health_api_token'] ?? '';
}
catch (\Throwable $e)
{
- $this->logTask('Failed to read sync targets: ' . $e->getMessage());
-
- return [];
+ return '';
}
}
}