From 28db9a67b632e5e83b77f9d00dd8fa8bcd74d113 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 11:03:09 -0500 Subject: [PATCH] fix: remove duplicate curl_setopt_array calls in 4 service plugins (#139) SendGrid and Reddit had a second curl_setopt_array that referenced an undefined $token variable, silently breaking auth. TikTok and Pinterest had identical duplicates (no variable bug but dead code). Removes the duplicate block from each plugin's publish() method. --- .../src/Extension/PinterestService.php | 132 +++++++++++++++++ .../src/Extension/RedditService.php | 130 +++++++++++++++++ .../src/Extension/SendgridService.php | 133 +++++++++++++++++ .../src/Extension/TiktokService.php | 134 ++++++++++++++++++ 4 files changed, 529 insertions(+) create mode 100644 source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php create mode 100644 source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php create mode 100644 source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php create mode 100644 source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php diff --git a/source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php b/source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php new file mode 100644 index 00000000..c612603a --- /dev/null +++ b/source/packages/plg_mokosuitecross_pinterest/src/Extension/PinterestService.php @@ -0,0 +1,132 @@ + + * @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\Plugin\MokoSuiteCross\Pinterest\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Pinterest service plugin for MokoSuiteCross. + * + * API: https://api.pinterest.com/v5/pins + */ +class PinterestService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +{ + public static function getSubscribedEvents(): array + { + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; + } + + public function onMokoSuiteCrossGetServices(&$services): void + { + $services[] = $this; + } + + public function getServiceType(): string { return 'pinterest'; } + public function getServiceName(): string { return 'Pinterest'; } + public function getMaxLength(): int { return 500; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + $boardId = $credentials['board_id'] ?? ''; + + if (empty($token) || empty($boardId)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or board ID']]; + } + + if (empty($media[0])) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Pinterest requires an image']]; + } + + $postData = json_encode([ + 'board_id' => $boardId, + 'title' => mb_substr(strip_tags($message), 0, 100), + 'description' => mb_substr($message, 0, 500), + 'media_source' => [ + 'source_type' => 'image_url', + 'url' => $media[0], + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://api.pinterest.com/v5/pins', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + + $curlError = curl_error($ch); + + curl_close($ch); + + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300) { + return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['valid' => false, 'message' => 'Missing access token', 'account_name' => '']; + } + + $ch = curl_init('https://api.pinterest.com/v5/user_account'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ]); + $response = curl_exec($ch); + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => '']; + } + curl_close($ch); + $data = json_decode($response, true) ?: []; + + if (!empty($data['username'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['username']]; + } + + return ['valid' => false, 'message' => 'Invalid token', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php b/source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php new file mode 100644 index 00000000..52dad886 --- /dev/null +++ b/source/packages/plg_mokosuitecross_reddit/src/Extension/RedditService.php @@ -0,0 +1,130 @@ + + * @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\Plugin\MokoSuiteCross\Reddit\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * Reddit service plugin for MokoSuiteCross. + * + * API: https://oauth.reddit.com/api/submit + */ +class RedditService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +{ + public static function getSubscribedEvents(): array + { + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; + } + + public function onMokoSuiteCrossGetServices(&$services): void + { + $services[] = $this; + } + + public function getServiceType(): string { return 'reddit'; } + public function getServiceName(): string { return 'Reddit'; } + public function getMaxLength(): int { return 300; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $accessToken = $credentials['access_token'] ?? ''; + $subreddit = $credentials['subreddit'] ?? ''; + + if (empty($accessToken) || empty($subreddit)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token or subreddit']]; + } + + $title = $params['title'] ?? mb_substr(strip_tags($message), 0, 300); + + $postData = http_build_query([ + 'sr' => $subreddit, + 'kind' => 'self', + 'title' => $title, + 'text' => $message, + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://oauth.reddit.com/api/submit', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $accessToken, + 'User-Agent: MokoSuiteCross/1.0', + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + + $curlError = curl_error($ch); + + curl_close($ch); + + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300) { + return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['valid' => false, 'message' => 'Missing access token', 'account_name' => '']; + } + + $ch = curl_init('https://oauth.reddit.com/api/v1/me'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'User-Agent: MokoSuiteCross/1.0'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ]); + $response = curl_exec($ch); + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => '']; + } + curl_close($ch); + $data = json_decode($response, true) ?: []; + + if (!empty($data['name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => 'u/' . $data['name']]; + } + + return ['valid' => false, 'message' => 'Invalid token', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php b/source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php new file mode 100644 index 00000000..e23ead41 --- /dev/null +++ b/source/packages/plg_mokosuitecross_sendgrid/src/Extension/SendgridService.php @@ -0,0 +1,133 @@ + + * @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\Plugin\MokoSuiteCross\Sendgrid\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * SendGrid service plugin for MokoSuiteCross. + * + * API: https://api.sendgrid.com/v3/marketing/singlesends + */ +class SendgridService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +{ + public static function getSubscribedEvents(): array + { + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; + } + + public function onMokoSuiteCrossGetServices(&$services): void + { + $services[] = $this; + } + + public function getServiceType(): string { return 'sendgrid'; } + public function getServiceName(): string { return 'SendGrid'; } + public function getMaxLength(): int { return 0; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $apiKey = $credentials['api_key'] ?? ''; + $listId = $credentials['list_id'] ?? ''; + $senderEmail = $credentials['sender_email'] ?? ''; + $senderName = $credentials['sender_name'] ?? 'Newsletter'; + + if (empty($apiKey) || empty($senderEmail)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing API key or sender email']]; + } + + $subject = $params['subject'] ?? mb_substr(strip_tags($message), 0, 150); + + $postData = json_encode([ + 'name' => $subject, + 'send_to' => !empty($listId) ? ['list_ids' => [$listId]] : ['all' => true], + 'email_config' => [ + 'subject' => $subject, + 'html_content' => $message, + 'sender_id' => null, + 'custom_unsubscribe_url' => '', + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://api.sendgrid.com/v3/marketing/singlesends', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + + $curlError = curl_error($ch); + + curl_close($ch); + + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300) { + return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $key = $credentials['api_key'] ?? ''; + + if (empty($key)) { + return ['valid' => false, 'message' => 'Missing API key', 'account_name' => '']; + } + + $ch = curl_init('https://api.sendgrid.com/v3/user/profile'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ]); + $response = curl_exec($ch); + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => '']; + } + curl_close($ch); + $data = json_decode($response, true) ?: []; + + if (!empty($data['first_name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['first_name'] . ' ' . ($data['last_name'] ?? '')]; + } + + return ['valid' => false, 'message' => 'Invalid API key', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image']; + } +} diff --git a/source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php b/source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php new file mode 100644 index 00000000..561509ee --- /dev/null +++ b/source/packages/plg_mokosuitecross_tiktok/src/Extension/TiktokService.php @@ -0,0 +1,134 @@ + + * @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\Plugin\MokoSuiteCross\Tiktok\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoSuiteCross\Administrator\Service\MokoSuiteCrossServiceInterface; +use Joomla\Event\SubscriberInterface; + +/** + * TikTok service plugin for MokoSuiteCross. + * + * API: https://open.tiktokapis.com/v2/post/publish/content/init/ + */ +class TiktokService extends CMSPlugin implements SubscriberInterface, MokoSuiteCrossServiceInterface +{ + public static function getSubscribedEvents(): array + { + return ['onMokoSuiteCrossGetServices' => 'onMokoSuiteCrossGetServices']; + } + + public function onMokoSuiteCrossGetServices(&$services): void + { + $services[] = $this; + } + + public function getServiceType(): string { return 'tiktok'; } + public function getServiceName(): string { return 'TikTok'; } + public function getMaxLength(): int { return 2200; } + public function supportsMedia(): bool { return true; } + + public function publish(string $message, array $media, array $credentials, array $params): array + { + $token = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Missing access token']]; + } + + if (empty($media[0])) { + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'TikTok requires a video or image']]; + } + + $postData = json_encode([ + 'post_info' => [ + 'title' => mb_substr(strip_tags($message), 0, 150), + 'description' => mb_substr($message, 0, 2200), + 'privacy_level' => 'SELF_ONLY', + 'disable_comment' => false, + ], + 'source_info' => [ + 'source' => 'PULL_FROM_URL', + 'video_url' => $media[0], + ], + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://open.tiktokapis.com/v2/post/publish/content/init/', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + + $curlError = curl_error($ch); + + curl_close($ch); + + return ['success' => false, 'platform_post_id' => '', 'response' => ['error' => 'Connection error: ' . $curlError]]; + + } + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $data = json_decode($response, true) ?: []; + + if ($httpCode >= 200 && $httpCode < 300) { + return ['success' => true, 'platform_post_id' => $data['id'] ?? $data['uri'] ?? '', 'response' => $data]; + } + + return ['success' => false, 'platform_post_id' => '', 'response' => $data]; + } + + public function validateCredentials(array $credentials): array + { + $token = $credentials['access_token'] ?? ''; + + if (empty($token)) { + return ['valid' => false, 'message' => 'Missing access token', 'account_name' => '']; + } + + $ch = curl_init('https://open.tiktokapis.com/v2/user/info/?fields=display_name,username'); + curl_setopt_array($ch, [ + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ]); + $response = curl_exec($ch); + if ($response === false) { + $curlError = curl_error($ch); + curl_close($ch); + return ['valid' => false, 'message' => 'Connection error: ' . $curlError, 'account_name' => '']; + } + curl_close($ch); + $data = json_decode($response, true) ?: []; + + if (!empty($data['data']['user']['display_name'])) { + return ['valid' => true, 'message' => 'Connected', 'account_name' => $data['data']['user']['display_name']]; + } + + return ['valid' => false, 'message' => 'Invalid token', 'account_name' => '']; + } + + public function getSupportedMediaTypes(): array + { + return ['image', 'video']; + } +}