From fe331c34dd54fd65a7ec85ff8ce6a24d57264ea0 Mon Sep 17 00:00:00 2001 From: objec Date: Wed, 29 Apr 2026 18:25:21 +0400 Subject: [PATCH] Mailchimp Bugfixes - Update README.md to reflect information about routes - Add registerAllNamespaces() function to correctly load plugin namespaces - center text in modal title - Properly decrypt the api key - Refactor getAllLists to getLists - Naming simplification of strings when mailchimp_ is redundant or unnecessary - Do not attempt to decrypt a plaintext api_key pasted into the form - Register namespaces early on in system init Signed-off-by: objec --- app/Config/Events.php | 5 +++ app/Libraries/Plugins/PluginManager.php | 23 +++++++++++ .../Controllers/MailchimpController.php | 8 ++-- .../Libraries/MailchimpConnector.php | 11 +---- .../Libraries/MailchimpLibrary.php | 21 +++++----- .../MailchimpPlugin/MailchimpPlugin.php | 32 +++++++++++---- app/Plugins/MailchimpPlugin/Views/config.php | 34 ++++++++-------- .../MailchimpPlugin/Views/customer_tab.php | 40 +++++++++---------- app/Plugins/README.md | 38 ++++++++++++++++++ app/Views/plugins/manage.php | 2 +- 10 files changed, 142 insertions(+), 72 deletions(-) diff --git a/app/Config/Events.php b/app/Config/Events.php index 26fe93841..23f8271ff 100644 --- a/app/Config/Events.php +++ b/app/Config/Events.php @@ -8,6 +8,7 @@ use CodeIgniter\HotReloader\HotReloader; use App\Events\Db_log; use App\Events\Load_config; use App\Events\Method; +use App\Libraries\Plugins\PluginManager; /* * -------------------------------------------------------------------- @@ -25,6 +26,10 @@ use App\Events\Method; * Example: * Events::on('create', [$myInstance, 'myMethod']); */ +Events::on('pre_system', static function (): void { + PluginManager::registerAllNamespaces(); +}); + Events::on('pre_system', static function (): void { if (ENVIRONMENT !== 'testing') { if (ini_get('zlib.output_compression')) { diff --git a/app/Libraries/Plugins/PluginManager.php b/app/Libraries/Plugins/PluginManager.php index a91ab0026..49b0c75fb 100644 --- a/app/Libraries/Plugins/PluginManager.php +++ b/app/Libraries/Plugins/PluginManager.php @@ -186,6 +186,29 @@ class PluginManager return $this->configModel->setValue("{$pluginId}_{$key}", $value); } + /** + * Registers PSR-4 namespaces for all plugin directories without touching the DB. + * Call this early (pre_system) so CI4's module route discovery can find each + * plugin's Config/Routes.php before the router runs. + */ + public static function registerAllNamespaces(): void + { + $pluginsPath = APPPATH . 'Plugins'; + if (!is_dir($pluginsPath)) { + return; + } + + $loader = Services::autoloader(); + foreach (glob($pluginsPath . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: [] as $dir) { + $name = basename($dir); + $namespace = "App\\Plugins\\{$name}"; + if (!in_array($namespace, self::$registeredNamespaces, true)) { + $loader->addNamespace($namespace, $dir . DIRECTORY_SEPARATOR); + self::$registeredNamespaces[] = $namespace; + } + } + } + public static function resetStatic(): void { self::$discovered = false; diff --git a/app/Plugins/MailchimpPlugin/Controllers/MailchimpController.php b/app/Plugins/MailchimpPlugin/Controllers/MailchimpController.php index 7a3879d6a..1bbd64011 100644 --- a/app/Plugins/MailchimpPlugin/Controllers/MailchimpController.php +++ b/app/Plugins/MailchimpPlugin/Controllers/MailchimpController.php @@ -16,13 +16,13 @@ class MailchimpController extends Secure_Controller */ public function postCheckMailchimpApiKey(): ResponseInterface { - $lists = $this->getAllMailchimpLists($this->request->getPost('mailchimp_api_key')); + $lists = $this->getAllMailchimpLists($this->request->getPost('api_key')); $success = count($lists) > 0; return $this->response->setJSON([ - 'success' => $success, - 'message' => lang('MailchimpPlugin.mailchimp_key_' . ($success ? '' : 'un') . 'successfully'), - 'mailchimp_lists' => $lists + 'success' => $success, + 'message' => lang('MailchimpPlugin.key_' . ($success ? '' : 'un') . 'successfully'), + 'lists' => $lists ]); } diff --git a/app/Plugins/MailchimpPlugin/Libraries/MailchimpConnector.php b/app/Plugins/MailchimpPlugin/Libraries/MailchimpConnector.php index f093e7c02..2cfed101c 100644 --- a/app/Plugins/MailchimpPlugin/Libraries/MailchimpConnector.php +++ b/app/Plugins/MailchimpPlugin/Libraries/MailchimpConnector.php @@ -2,9 +2,6 @@ namespace App\Plugins\MailchimpPlugin\Libraries; -use CodeIgniter\Encryption\EncrypterInterface; -use Config\Services; - /** * MailChimp API v3 REST client Connector * @@ -13,8 +10,6 @@ use Config\Services; * Inspired by the work of: * - Rajitha Bandara: https://github.com/rajitha-bandara/ci-mailchimp-v3-rest-client * - Stefan Ashwell: https://github.com/stef686/codeigniter-mailchimp-api-v3 - * - * @property encrypterinterface encrypter */ class MailchimpConnector { @@ -23,11 +18,7 @@ class MailchimpConnector public function __construct(string $apiKey) { - $mailchimpApiKey = !empty($apiKey) ? $apiKey : ''; - - if (!empty($mailchimpApiKey)) { - $this->apiKey = Services::encrypter()->decrypt($mailchimpApiKey); - } + $this->apiKey = $apiKey; if (!empty($this->apiKey)) { // Replace with correct datacenter obtained from the last part of the api key diff --git a/app/Plugins/MailchimpPlugin/Libraries/MailchimpLibrary.php b/app/Plugins/MailchimpPlugin/Libraries/MailchimpLibrary.php index bbe0f8d0b..306506634 100644 --- a/app/Plugins/MailchimpPlugin/Libraries/MailchimpLibrary.php +++ b/app/Plugins/MailchimpPlugin/Libraries/MailchimpLibrary.php @@ -37,7 +37,7 @@ class MailchimpLibrary * @return array|bool * @see http://developer.mailchimp.com/documentation/mailchimp/reference/lists/#read-get_lists */ - public function getAllLists(array $parameters = ['fields' => 'lists.id,lists.name,lists.stats.member_count,lists.stats.merge_field_count']): bool|array + public function getLists(array $parameters = ['fields' => 'lists.id,lists.name,lists.stats.member_count,lists.stats.merge_field_count']): bool|array { return $this->connector->call('/lists', 'GET', $parameters); } @@ -229,14 +229,11 @@ class MailchimpLibrary public function synchronizeSubscription(array $customerData): bool { try { - if (!$this->subscribeCustomer($customerData)) { - throw new Exception("Customer ID {$customerData['person_id']}"); - } + return $this->subscribeCustomer($customerData); } catch (Exception $e) { log_message('error', "Failed to sync customer to Mailchimp: {$e->getMessage()}"); + return false; } - - return false; } private function subscribeCustomer(array $customerData): bool @@ -284,7 +281,7 @@ class MailchimpLibrary $mailchimpInfo = $this->getMemberInfo($listId, $customerData->email); if ($mailchimpInfo !== false) { - $mailchimpData['mailchimp_info'] = $mailchimpInfo; + $mailchimpData['mailchimpActivity'] = $mailchimpInfo; $mailchimpData['subscriptionStatusOptions'] = $this->getSubscriptionStatusOptionViewData(); @@ -315,11 +312,11 @@ class MailchimpLibrary ++$total; } - $mailchimpData['mailchimp_activity']['total'] = $total; - $mailchimpData['mailchimp_activity']['open'] = $open; - $mailchimpData['mailchimp_activity']['unopen'] = $unopen; - $mailchimpData['mailchimp_activity']['click'] = $click; - $mailchimpData['mailchimp_activity']['last_open'] = $lastOpen; + $mailchimpData['mailchimpActivity']['total'] = $total; + $mailchimpData['mailchimpActivity']['open'] = $open; + $mailchimpData['mailchimpActivity']['unopen'] = $unopen; + $mailchimpData['mailchimpActivity']['click'] = $click; + $mailchimpData['mailchimpActivity']['last_open'] = $lastOpen; } } diff --git a/app/Plugins/MailchimpPlugin/MailchimpPlugin.php b/app/Plugins/MailchimpPlugin/MailchimpPlugin.php index 754486363..598f156fa 100644 --- a/app/Plugins/MailchimpPlugin/MailchimpPlugin.php +++ b/app/Plugins/MailchimpPlugin/MailchimpPlugin.php @@ -5,6 +5,7 @@ namespace App\Plugins\MailchimpPlugin; use App\Libraries\Plugins\BasePlugin; use App\Plugins\MailchimpPlugin\Libraries\MailchimpLibrary; use CodeIgniter\Events\Events; +use Config\Services; use stdClass; /** @@ -77,11 +78,23 @@ class MailchimpPlugin extends BasePlugin public function getSettings(): array { + $encryptedKey = $this->getSetting('api_key', ''); + $apiKey = ''; + + if (!empty($encryptedKey)) { + try { + $apiKey = Services::encrypter()->decrypt($encryptedKey); + } catch (\Exception $e) { + // Key stored as plaintext (pre-encryption migration) — use as-is + $apiKey = $encryptedKey; + } + } + return [ - 'api_key' => $this->getSetting('api_key', ''), - 'list_id' => $this->getSetting('list_id', ''), + 'api_key' => $apiKey, + 'list_id' => $this->getSetting('list_id', ''), 'sync_on_save' => $this->getSetting('sync_on_save', '1'), - 'enabled' => $this->getSetting('enabled', '0'), + 'enabled' => $this->getSetting('enabled', '0'), ]; } @@ -90,7 +103,10 @@ class MailchimpPlugin extends BasePlugin $normalized = []; if (array_key_exists('api_key', $settings)) { - $normalized['api_key'] = (string)$settings['api_key']; + $rawKey = (string)$settings['api_key']; + $normalized['api_key'] = !empty($rawKey) + ? Services::encrypter()->encrypt($rawKey) + : ''; } if (array_key_exists('list_id', $settings)) { @@ -126,7 +142,7 @@ class MailchimpPlugin extends BasePlugin { log_message('debug', "Customer_deleted event received for ID: {$customer->person_id}"); - $this->mailchimpLibrary->deleteSubscription(); + $this->mailchimpLibrary->deleteSubscription($customer); } private function shouldSyncOnSave(): bool @@ -140,7 +156,7 @@ class MailchimpPlugin extends BasePlugin $apiKey = $this->getSetting('api_key'); if (empty($apiKey)) { - return ['success' => false, 'message' => lang('mailchimp_api_key_required')]; + return ['success' => false, 'message' => lang('api_key_required')]; } $result = $this->mailchimpLibrary->getLists(); @@ -148,12 +164,12 @@ class MailchimpPlugin extends BasePlugin if ($result && isset($result['lists'])) { return [ 'success' => true, - 'message' => lang('mailchimp_key_successfully'), + 'message' => lang('key_successfully'), 'lists' => $result['lists'] ]; } - return ['success' => false, 'message' => lang('mailchimp_key_unsuccessfully')]; + return ['success' => false, 'message' => lang('key_unsuccessfully')]; } } diff --git a/app/Plugins/MailchimpPlugin/Views/config.php b/app/Plugins/MailchimpPlugin/Views/config.php index 1c04849a4..ddefd29f6 100644 --- a/app/Plugins/MailchimpPlugin/Views/config.php +++ b/app/Plugins/MailchimpPlugin/Views/config.php @@ -5,24 +5,24 @@ */ ?> - 'mailchimp_config_form', 'enctype' => 'multipart/form-data', 'class' => 'form-horizontal']) ?> + 'config_form', 'enctype' => 'multipart/form-data', 'class' => 'form-horizontal']) ?>
-
    +
      - 'control-label col-xs-2']) ?> + 'control-label col-xs-2']) ?>
      'mailchimp_api_key', - 'id' => 'mailchimp_api_key', + 'name' => 'api_key', + 'id' => 'api_key', 'class' => 'form-control input-sm', 'value' => esc($settings['api_key'] ?? '') ]) ?> @@ -38,17 +38,17 @@
      - 'control-label col-xs-2']) ?> + 'control-label col-xs-2']) ?>
      @@ -68,9 +68,9 @@ diff --git a/app/Plugins/MailchimpPlugin/Views/customer_tab.php b/app/Plugins/MailchimpPlugin/Views/customer_tab.php index f0763f0aa..4c17a61d1 100644 --- a/app/Plugins/MailchimpPlugin/Views/customer_tab.php +++ b/app/Plugins/MailchimpPlugin/Views/customer_tab.php @@ -8,32 +8,32 @@ ?> -
      +
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_status', 'class' => 'form-control input-sm'] + ['id' => 'status', 'class' => 'form-control input-sm'] ) ?>
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      - +
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_member_rating', + 'name' => 'member_rating', 'class' => 'form-control input-sm', 'value' => $mailchimpData['member_rating'], 'disabled' => '' @@ -42,10 +42,10 @@
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_activity_total', + 'name' => 'activity_total', 'class' => 'form-control input-sm', 'value' => $mailchimpActivity['total'], 'disabled' => '' @@ -54,10 +54,10 @@
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_activity_last_open', + 'name' => 'activity_last_open', 'class' => 'form-control input-sm', 'value' => $mailchimpActivity['last_open'], 'disabled' => '' @@ -66,10 +66,10 @@
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_activity_open', + 'name' => 'activity_open', 'class' => 'form-control input-sm', 'value' => $mailchimpActivity['open'], 'disabled' => '' @@ -78,10 +78,10 @@
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_activity_click', + 'name' => 'activity_click', 'class' => 'form-control input-sm', 'value' => $mailchimpActivity['click'], 'disabled' => '' @@ -90,10 +90,10 @@
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_activity_unopen', + 'name' => 'activity_unopen', 'class' => 'form-control input-sm', 'value' => $mailchimpActivity['unopen'], 'disabled' => '' @@ -102,10 +102,10 @@
      - 'control-label col-xs-3']) ?> + 'control-label col-xs-3']) ?>
      'mailchimp_email_client', + 'name' => 'email_client', 'class' => 'form-control input-sm', 'value' => $mailchimpData['email_client'], 'disabled' => '' diff --git a/app/Plugins/README.md b/app/Plugins/README.md index 53657c161..f3246ec03 100644 --- a/app/Plugins/README.md +++ b/app/Plugins/README.md @@ -190,6 +190,8 @@ For plugins that only need to listen to events without complex UI or database ta ```text app/Plugins/ └── ExamplePlugin/ # Plugin directory (self-contained) + ├── Config/ # Plugin-specific routing (optional) + │ └── Routes.php ├── Language/ # Plugin-specific translations (self-contained) │ ├── en/ │ │ └── ExamplePlugin.php @@ -267,6 +269,8 @@ For plugins that need database tables, controllers, models, and views: ```text app/Plugins/ └── ExamplePlugin/ # Plugin directory + ├── Config/ # Plugin-specific routing + │ └── Routes.php ├── Controllers/ # Plugin controllers │ └── ExampleController.php ├── Language/ # Plugin translations (self-contained) @@ -369,6 +373,39 @@ class ExamplePlugin extends BasePlugin } ``` +## Plugin Routes + +Plugins can define their own routes in a `Config/Routes.php` file. Routes are *NOT* auto-loaded by the framework when the plugin directory is discovered. + +### Defining Plugin Routes + +Create `app/Plugins/ExamplePlugin/Config/Routes.php`: + +```php +post('plugins/example/action', '\App\Plugins\ExamplePlugin\Controllers\ExampleController::postAction'); +$routes->get('plugins/example/dashboard', '\App\Plugins\ExamplePlugin\Controllers\ExampleController::getDashboard'); +``` + +### Route Naming Convention + +Use a consistent naming scheme for plugin routes: +- Prefix routes with `plugins/` followed by plugin identifier +- Examples: `plugins/mailchimp/checkApiKey`, `plugins/example/sync` + +### Full Qualified Class Names + +Always use fully qualified controller names: +- `\App\Plugins\ExamplePlugin\Controllers\ExampleController::methodName` + +This ensures routes work correctly regardless of autoloader state. + ## Internationalization (Language Files) Plugins can include their own language files, making them completely self-contained. This allows plugins to provide translations without modifying core language files. @@ -442,6 +479,7 @@ Settings are prefixed with the plugin ID (e.g., `example_api_key`) and stored in |---------------------------------------------------------------|-----------------------------------------------------------| | `app/Plugins/ExamplePlugin.php` | `App\Plugins` | | `app/Plugins/ExamplePlugin/ExamplePlugin.php` | `App\Plugins\ExamplePlugin\ExamplePlugin` | +| `app/Plugins/ExamplePlugin/Config/Routes.php` | *(Route file - no namespace)* | | `app/Plugins/ExamplePlugin/Models/ExampleModel.php` | `App\Plugins\ExamplePlugin\Models\ExampleModel` | | `app/Plugins/ExamplePlugin/Controllers/ExampleController.php` | `App\Plugins\ExamplePlugin\Controllers\ExampleController` | | `app/Plugins/ExamplePlugin/Libraries/ApiClient.php` | `App\Plugins\ExamplePlugin\Libraries\ApiClient` | diff --git a/app/Views/plugins/manage.php b/app/Views/plugins/manage.php index 81352499e..b22cb24e0 100644 --- a/app/Views/plugins/manage.php +++ b/app/Views/plugins/manage.php @@ -58,7 +58,7 @@