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.
This commit is contained in:
Jonathan Miller
2026-06-21 11:03:09 -05:00
parent b9b0c88ad5
commit 28db9a67b6
4 changed files with 529 additions and 0 deletions
@@ -0,0 +1,132 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_pinterest
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\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'];
}
}
@@ -0,0 +1,130 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_reddit
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\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'];
}
}
@@ -0,0 +1,133 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_sendgrid
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\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'];
}
}
@@ -0,0 +1,134 @@
<?php
/**
* @package MokoSuiteCross
* @subpackage plg_mokosuitecross_tiktok
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\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'];
}
}