Skip to content

Commit

Permalink
Merge branch '2.x' into develop
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
#	composer.json
#	src/controllers/FormsController.php
  • Loading branch information
bencroker committed Jun 18, 2024
2 parents f6c4869 + 6350201 commit 60180dd
Show file tree
Hide file tree
Showing 17 changed files with 461 additions and 43 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Release Notes for Campaign

## 3.2.0 - Unreleased

### Added

- Added the ability to enforce spam prevention on front-end forms using Cloudflare Turnstile ([#447](https://github.com/putyourlightson/craft-campaign/issues/447)).
- Added the `resave/campaigns`, `resave/contacts` and `resave/mailing-lists` console commands ([#481](https://github.com/putyourlightson/craft-campaign/issues/481)).

## 3.1.4 - 2024-05-06

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "putyourlightson/craft-campaign",
"description": "Send and manage email campaigns, contacts and mailing lists.",
"version": "3.1.4",
"version": "3.2.0",
"type": "craft-plugin",
"homepage": "https://putyourlightson.com/plugins/campaign",
"license": "proprietary",
Expand Down
66 changes: 65 additions & 1 deletion src/Campaign.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

use Craft;
use craft\base\Plugin;
use craft\console\Controller as ConsoleController;
use craft\console\controllers\ResaveController;
use craft\controllers\PreviewController;
use craft\elements\User;
use craft\events\DefineConsoleActionsEvent;
use craft\events\DefineFieldLayoutFieldsEvent;
use craft\events\PluginEvent;
use craft\events\RebuildConfigEvent;
Expand Down Expand Up @@ -221,6 +224,10 @@ public function init(): void
$this->controllerMap = ['t' => TrackerController::class];
}

if (Craft::$app->getRequest()->getIsConsoleRequest()) {
$this->registerResaveCommands();
}

if (Craft::$app->getRequest()->getIsCpRequest()) {
$this->registerNativeFields();
$this->registerAssetBundles();
Expand Down Expand Up @@ -346,7 +353,7 @@ public function log(string $message, array $params = [], int $type = Logger::LEV
/**
* Returns whether the current user can edit contacts.
*/
public function userCanEditContacts(): bool
public function canUserEditContacts(): bool
{
/** @var User|null $currentUser */
$currentUser = Craft::$app->getUser()->getIdentity();
Expand Down Expand Up @@ -443,6 +450,7 @@ protected function getCpRoutes(): array
'campaign/settings/contact' => 'campaign/settings/edit-contact',
'campaign/settings/geoip' => 'campaign/settings/edit-geoip',
'campaign/settings/recaptcha' => 'campaign/settings/edit-recaptcha',
'campaign/settings/turnstile' => 'campaign/settings/edit-turnstile',
'campaign/settings/campaigntypes' => 'campaign/campaign-types/index',
'campaign/settings/campaigntypes/new' => 'campaign/campaign-types/edit',
'campaign/settings/campaigntypes/<campaignTypeId:\d+>' => 'campaign/campaign-types/edit',
Expand Down Expand Up @@ -846,4 +854,60 @@ function(RegisterUserPermissionsEvent $event) {
}
);
}

/**
* Registers resave commands.
*
* @since 2.16.0
*/
private function registerResaveCommands(): void
{
Event::on(ResaveController::class, ConsoleController::EVENT_DEFINE_ACTIONS,
function(DefineConsoleActionsEvent $event) {
$event->actions['campaigns'] = [
'action' => function(): int {
/** @var ResaveController $controller */
$controller = Craft::$app->controller;
$criteria = [];
if ($controller->type !== null) {
$criteria['campaignType'] = explode(',', $controller->type);
}
return $controller->resaveElements(CampaignElement::class, $criteria);
},
'options' => ['type'],
'helpSummary' => 'Re-saves Campaign campaigns.',
'optionsHelp' => [
'type' => 'The campaign type handle(s) of the campaigns to resave.',
],
];

$event->actions['mailing-lists'] = [
'action' => function(): int {
/** @var ResaveController $controller */
$controller = Craft::$app->controller;
$criteria = [];
if ($controller->type !== null) {
$criteria['mailingListType'] = explode(',', $controller->type);
}
return $controller->resaveElements(MailingListElement::class, $criteria);
},
'options' => ['type'],
'helpSummary' => 'Re-saves Campaign mailing lists.',
'optionsHelp' => [
'type' => 'The mailing lists type handle(s) of the mailing lists to resave.',
],
];

$event->actions['contacts'] = [
'action' => function(): int {
/** @var ResaveController $controller */
$controller = Craft::$app->controller;
return $controller->resaveElements(ContactElement::class);
},
'options' => [],
'helpSummary' => 'Re-saves Campaign contacts.',
];
}
);
}
}
28 changes: 23 additions & 5 deletions src/controllers/FormsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
namespace putyourlightson\campaign\controllers;

use Craft;
use craft\helpers\App;
use putyourlightson\campaign\base\BaseMessageController;
use putyourlightson\campaign\Campaign;
use putyourlightson\campaign\elements\ContactElement;
use putyourlightson\campaign\elements\MailingListElement;
use putyourlightson\campaign\helpers\RecaptchaHelper;
use putyourlightson\campaign\helpers\TurnstileHelper;
use yii\web\ForbiddenHttpException;
use yii\web\NotFoundHttpException;
use yii\web\Response;
Expand All @@ -30,6 +30,7 @@ public function actionSubscribe(): ?Response
{
$this->requirePostRequest();
$this->validateRecaptcha();
$this->validateTurnstile();

$mailingList = $this->getMailingListFromParams();
if ($mailingList === null) {
Expand All @@ -47,7 +48,7 @@ public function actionSubscribe(): ?Response
}

$message = $mailingList->mailingListType->subscribeVerificationRequired ? Craft::t('campaign', 'Thank you for subscribing to the mailing list. Please check your email for a verification link.') : Craft::t('campaign', 'You have successfully subscribed to the mailing list.');

if ($this->request->getAcceptsJson()) {
return $this->asSuccess($message);
}
Expand All @@ -70,6 +71,7 @@ public function actionUnsubscribe(): ?Response
{
$this->requirePostRequest();
$this->validateRecaptcha();
$this->validateTurnstile();

$mailingList = $this->getMailingListFromParams();
if ($mailingList === null) {
Expand Down Expand Up @@ -103,6 +105,7 @@ public function actionUpdateContact(): ?Response
{
$this->requirePostRequest();
$this->validateRecaptcha();
$this->validateTurnstile();

// Get verified contact
$contact = $this->getVerifiedContact();
Expand Down Expand Up @@ -249,22 +252,37 @@ private function getMailingListFromParams(): ?MailingListElement
}

/**
* Validates reCAPTCHA if enabled.
* Validates reCAPTCHA, if enabled.
*/
private function validateRecaptcha(): void
{
// Validate reCAPTCHA if enabled
if (Campaign::$plugin->settings->reCaptcha) {
$response = $this->request->getParam('g-recaptcha-response');

if ($response === null) {
throw new ForbiddenHttpException(App::parseEnv(Campaign::$plugin->settings->reCaptchaErrorMessage));
throw new ForbiddenHttpException(Campaign::$plugin->settings->getRecaptchaErrorMessage());
}

RecaptchaHelper::validateRecaptcha($response, $this->request->getRemoteIP());
}
}

/**
* Validates Turnstile, if enabled.
*/
private function validateTurnstile(): void
{
if (Campaign::$plugin->settings->turnstile) {
$response = $this->request->getParam('cf-turnstile-response');

if ($response === null) {
throw new ForbiddenHttpException(Campaign::$plugin->settings->getTurnstileErrorMessage());
}

TurnstileHelper::validate($response, $this->request->getRemoteIP());
}
}

/**
* Gets contact by CID, verified by UID.
*/
Expand Down
3 changes: 1 addition & 2 deletions src/controllers/SendoutsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Craft;
use craft\base\Element;
use craft\errors\SiteNotFoundException;
use craft\helpers\App;
use craft\helpers\Cp;
use craft\helpers\ElementHelper;
use craft\helpers\UrlHelper;
Expand Down Expand Up @@ -57,7 +56,7 @@ public function actionQueuePendingSendouts(): Response
} else {
// Verify API key
$key = $this->request->getParam('key');
$apiKey = App::parseEnv(Campaign::$plugin->settings->apiKey);
$apiKey = Campaign::$plugin->settings->getApiKey();

if ($key === null || empty($apiKey) || $key != $apiKey) {
throw new ForbiddenHttpException('Unauthorised access.');
Expand Down
40 changes: 40 additions & 0 deletions src/controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,23 @@ public function actionEditRecaptcha(SettingsModel $settings = null): Response
]);
}

/**
* Edit Turnstile settings.
*
* @param SettingsModel|null $settings The settings being edited, if there were any validation errors.
*/
public function actionEditTurnstile(SettingsModel $settings = null): Response
{
if ($settings === null) {
$settings = Campaign::$plugin->settings;
}

return $this->renderTemplate('campaign/_settings/turnstile', [
'settings' => $settings,
'config' => Craft::$app->getConfig()->getConfigFromFile('campaign'),
]);
}

/**
* Saves general settings.
*/
Expand Down Expand Up @@ -319,6 +336,29 @@ public function actionSaveRecaptcha(): ?Response
return $this->asSuccess(Craft::t('campaign', 'reCAPTCHA settings saved.'));
}

/**
* Saves Turnstile settings.
*/
public function actionSaveTurnstile(): ?Response
{
$this->requirePostRequest();

$settings = Campaign::$plugin->settings;

// Set the simple stuff
$settings->turnstile = $this->request->getBodyParam('turnstile', $settings->turnstile);
$settings->turnstileSiteKey = $this->request->getBodyParam('turnstileSiteKey', $settings->turnstileSiteKey);
$settings->turnstileSecretKey = $this->request->getBodyParam('turnstileSecretKey', $settings->turnstileSecretKey);
$settings->turnstileErrorMessage = $this->request->getBodyParam('turnstileErrorMessage', $settings->turnstileErrorMessage);

// Save it
if (!Craft::$app->getPlugins()->savePluginSettings(Campaign::$plugin, $settings->getAttributes())) {
return $this->asModelFailure($settings, Craft::t('campaign', 'Couldn’t save Turnstile settings.'), 'settings');
}

return $this->asSuccess(Craft::t('campaign', 'Turnstile settings saved.'));
}

/**
* Sends a test email.
*/
Expand Down
9 changes: 4 additions & 5 deletions src/controllers/WebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Aws\Sns\Message;
use Aws\Sns\MessageValidator;
use Craft;
use craft\helpers\App;
use craft\helpers\Json;
use craft\web\Controller;
use EllipticCurve\Ecdsa;
Expand Down Expand Up @@ -54,7 +53,7 @@ public function beforeAction($action): bool
{
// Verify API key
$key = $this->request->getParam('key');
$apiKey = App::parseEnv(Campaign::$plugin->settings->apiKey);
$apiKey = Campaign::$plugin->settings->getApiKey();

if ($key === null || empty($apiKey) || $key != $apiKey) {
throw new ForbiddenHttpException('Unauthorised access.');
Expand Down Expand Up @@ -409,7 +408,7 @@ private function isValidMailersendRequest(string $body): bool
return true;
}

$signingSecret = (string)App::parseEnv(Campaign::$plugin->settings->mailersendWebhookSigningSecret);
$signingSecret = Campaign::$plugin->settings->getMailersendWebhookSigningSecret();
$signature = $this->request->headers->get('Signature', '');
$hashedValue = hash_hmac('sha256', $body, $signingSecret);

Expand All @@ -425,7 +424,7 @@ private function isValidMailgunRequest(string $signature, string $timestamp, str
return true;
}

$signingKey = (string)App::parseEnv(Campaign::$plugin->settings->mailgunWebhookSigningKey);
$signingKey = Campaign::$plugin->settings->getMailgunWebhookSigningSecret();
$hashedValue = hash_hmac('sha256', $timestamp . $token, $signingKey);

return hash_equals($signature, $hashedValue);
Expand All @@ -442,7 +441,7 @@ private function isValidSendgridRequest(string $body): bool

$signature = $this->request->headers->get('X-Twilio-Email-Event-Webhook-Signature', '');
$timestamp = $this->request->headers->get('X-Twilio-Email-Event-Webhook-Timestamp', '');
$verificationKey = (string)App::parseEnv(Campaign::$plugin->settings->sendgridWebhookVerificationKey);
$verificationKey = Campaign::$plugin->settings->getSendgridWebhookSigningSecret();

// https://github.com/sendgrid/sendgrid-php/blob/9335dca98bc64456a72db73469d1dd67db72f6ea/lib/eventwebhook/EventWebhook.php#L23-L26
$publicKey = PublicKey::fromString($verificationKey);
Expand Down
2 changes: 1 addition & 1 deletion src/elements/ContactElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ protected function statusFieldHtml(): string
*/
public function canView(User $user): bool
{
if (!Campaign::$plugin->userCanEditContacts()) {
if (!Campaign::$plugin->canUserEditContacts()) {
return false;
}

Expand Down
3 changes: 1 addition & 2 deletions src/helpers/ContactActivityHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
namespace putyourlightson\campaign\helpers;

use Craft;
use craft\helpers\App;
use craft\helpers\Json;
use DateTime;
use DeviceDetector\DeviceDetector;
Expand Down Expand Up @@ -83,7 +82,7 @@ public static function getGeoIp(int $timeout = 5): ?array

try {
$ip = Craft::$app->getRequest()->getRemoteIP();
$apiKey = App::parseEnv(Campaign::$plugin->settings->ipstackApiKey);
$apiKey = Campaign::$plugin->settings->getIpstackApiKey();

/** @noinspection HttpUrlsUsage */
$response = $client->get('http://api.ipstack.com/' . $ip . '?access_key=' . $apiKey);
Expand Down
9 changes: 4 additions & 5 deletions src/helpers/RecaptchaHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
namespace putyourlightson\campaign\helpers;

use Craft;
use craft\helpers\App;
use craft\helpers\Json;
use GuzzleHttp\Exception\ConnectException;
use putyourlightson\campaign\Campaign;
Expand Down Expand Up @@ -39,24 +38,24 @@ public static function validateRecaptcha(string $recaptchaResponse, string $ip):
try {
$response = $client->post('https://www.google.com/recaptcha/api/siteverify', [
'form_params' => [
'secret' => App::parseEnv($settings->reCaptchaSecretKey),
'secret' => $settings->getRecaptchaSecretKey(),
'response' => $recaptchaResponse,
'remoteip' => $ip,
],
]);

if ($response->getStatusCode() == 200) {
if ($response->getStatusCode() === 200) {
$result = Json::decodeIfJson($response->getBody());
}
} catch (ConnectException) {
}

if (empty($result['success'])) {
throw new ForbiddenHttpException(App::parseEnv($settings->reCaptchaErrorMessage));
throw new ForbiddenHttpException($settings->reCaptchaErrorMessage);
}

if (!empty($result['action']) && $result['action'] != self::RECAPTCHA_ACTION) {
throw new ForbiddenHttpException(App::parseEnv($settings->reCaptchaErrorMessage));
throw new ForbiddenHttpException($settings->getRecaptchaErrorMessage());
}
}
}
Loading

0 comments on commit 60180dd

Please sign in to comment.