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 <objecttothis@gmail.com>
This commit is contained in:
objec
2026-04-29 18:25:21 +04:00
parent 6630fb56f6
commit fe331c34dd
10 changed files with 142 additions and 72 deletions

View File

@@ -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')) {

View File

@@ -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;

View File

@@ -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
]);
}

View File

@@ -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 <dc> with correct datacenter obtained from the last part of the api key

View File

@@ -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;
}
}

View File

@@ -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')];
}
}

View File

@@ -5,24 +5,24 @@
*/
?>
<?= form_open('MailchimpPlugin/saveMailchimp/', ['id' => 'mailchimp_config_form', 'enctype' => 'multipart/form-data', 'class' => 'form-horizontal']) ?>
<?= form_open(site_url('plugins/saveConfig/mailchimp'), ['id' => 'config_form', 'enctype' => 'multipart/form-data', 'class' => 'form-horizontal']) ?>
<div id="config_wrapper">
<fieldset id="config_info">
<div id="required_fields_message"><?= lang('MailchimpPlugin.fields_required_message') ?></div>
<div id="plugins_header"><?= lang('MailchimpPlugin.configuration') ?></div>
<ul id="mailchimp_error_message_box" class="error_message_box"></ul>
<ul id="error_message_box" class="error_message_box"></ul>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.api_key'), 'mailchimp_api_key', ['class' => 'control-label col-xs-2']) ?>
<?= form_label(lang('MailchimpPlugin.api_key'), 'api_key', ['class' => 'control-label col-xs-2']) ?>
<div class="col-xs-4">
<div class="input-group">
<span class="input-group-addon input-sm">
<span class="glyphicon glyphicon-cloud"></span>
</span>
<?= form_input([
'name' => '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 @@
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.lists'), 'mailchimp_list_id', ['class' => 'control-label col-xs-2']) ?>
<?= form_label(lang('MailchimpPlugin.lists'), 'list_id', ['class' => 'control-label col-xs-2']) ?>
<div class="col-xs-4">
<div class="input-group">
<span class="input-group-addon input-sm">
<span class="glyphicon glyphicon-user"></span>
</span>
<?= form_dropdown(
'mailchimp_list_id',
'list_id',
esc($settings['lists'] ?? ''),
esc($settings['list_id'] ?? ''),
'id="mailchimp_list_id" class="form-control input-sm"'
'id="list_id" class="form-control input-sm"'
) ?>
</div>
</div>
@@ -68,9 +68,9 @@
<script type="text/javascript">
// Validation and submit handling
$(document).ready(function() {
$('#mailchimp_api_key').change(function() {
$.post("<?= "checkMailchimpApiKey" ?>", {
'mailchimp_api_key': $('#mailchimp_api_key').val()
$('#api_key').change(function() {
$.post("<?= site_url('plugins/mailchimp/checkApiKey') ?>", {
'api_key': $('#api_key').val()
},
function(response) {
$.notify({
@@ -78,17 +78,17 @@
}, {
type: response.success ? 'success' : 'danger'
});
$('#mailchimp_list_id').empty();
$.each(response.mailchimp_lists, function(val, text) {
$('#mailchimp_list_id').append(new Option(text, val));
$('#list_id').empty();
$.each(response.lists, function(val, text) {
$('#list_id').append(new Option(text, val));
});
$('#mailchimp_list_id').prop('selectedIndex', 0);
$('#list_id').prop('selectedIndex', 0);
},
'json'
);
});
$('#mailchimp_config_form').validate($.extend(form_support.handler, {
$('#config_form').validate($.extend(form_support.handler, {
submitHandler: function(form) {
$(form).ajaxSubmit({
success: function(response) {
@@ -102,7 +102,7 @@
});
},
errorLabelContainer: '#mailchimp_error_message_box'
errorLabelContainer: '#error_message_box'
}));
});
</script>

View File

@@ -8,32 +8,32 @@
?>
<div class="tab-pane" id="customer_mailchimp_info">
<div class="tab-pane" id="activity_info">
<fieldset>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.status'), 'mailchimp_status', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.status'), 'status', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_dropdown(
'mailchimp_status',
'status',
$subscriptionStatusOptions,
$mailchimpData['status'],
['id' => 'mailchimp_status', 'class' => 'form-control input-sm']
['id' => 'status', 'class' => 'form-control input-sm']
) ?>
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.vip'), 'mailchimp_vip', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.vip'), 'vip', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-1">
<?= form_checkbox('mailchimp_vip', 1, $mailchimpData['vip'] == 1) ?>
<?= form_checkbox('vip', 1, $mailchimpData['vip'] == 1) ?>
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.member_rating'), 'mailchimp_member_rating', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.member_rating'), 'member_rating', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_input([
'name' => 'mailchimp_member_rating',
'name' => 'member_rating',
'class' => 'form-control input-sm',
'value' => $mailchimpData['member_rating'],
'disabled' => ''
@@ -42,10 +42,10 @@
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.activity_total'), 'mailchimp_activity_total', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.activity_total'), 'activity_total', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_input([
'name' => 'mailchimp_activity_total',
'name' => 'activity_total',
'class' => 'form-control input-sm',
'value' => $mailchimpActivity['total'],
'disabled' => ''
@@ -54,10 +54,10 @@
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.activity_last_open'), 'mailchimp_activity_last_open', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.activity_last_open'), 'activity_last_open', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_input([
'name' => 'mailchimp_activity_last_open',
'name' => 'activity_last_open',
'class' => 'form-control input-sm',
'value' => $mailchimpActivity['last_open'],
'disabled' => ''
@@ -66,10 +66,10 @@
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.activity_open'), 'mailchimp_activity_open', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.activity_open'), 'activity_open', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_input([
'name' => 'mailchimp_activity_open',
'name' => 'activity_open',
'class' => 'form-control input-sm',
'value' => $mailchimpActivity['open'],
'disabled' => ''
@@ -78,10 +78,10 @@
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.activity_click'), 'mailchimp_activity_click', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.activity_click'), 'activity_click', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_input([
'name' => 'mailchimp_activity_click',
'name' => 'activity_click',
'class' => 'form-control input-sm',
'value' => $mailchimpActivity['click'],
'disabled' => ''
@@ -90,10 +90,10 @@
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.activity_unopen'), 'mailchimp_activity_unopen', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.activity_unopen'), 'activity_unopen', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_input([
'name' => 'mailchimp_activity_unopen',
'name' => 'activity_unopen',
'class' => 'form-control input-sm',
'value' => $mailchimpActivity['unopen'],
'disabled' => ''
@@ -102,10 +102,10 @@
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('MailchimpPlugin.email_client'), 'mailchimp_email_client', ['class' => 'control-label col-xs-3']) ?>
<?= form_label(lang('MailchimpPlugin.email_client'), 'email_client', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_input([
'name' => 'mailchimp_email_client',
'name' => 'email_client',
'class' => 'form-control input-sm',
'value' => $mailchimpData['email_client'],
'disabled' => ''

View File

@@ -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
<?php
use CodeIgniter\Router\RouteCollection;
/**
* @var RouteCollection $routes
*/
$routes->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` |

View File

@@ -58,7 +58,7 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title"><?= lang('Plugins.plugins') ?></h4>
<h4 class="modal-title text-center"><?= lang('Plugins.plugins') ?></h4>
</div>
<div class="modal-body" id="plugin-config-content"></div>
</div>