Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add constrain to check if plugin can be published. #13696

Open
wants to merge 30 commits into
base: 5.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
542d376
Fixcs
rohitp19 Dec 28, 2023
1432937
Fixed return type when error and throw exception
rohitp19 Dec 28, 2023
c91fcac
Added Custom validator on Plugin publised so that we can check if we …
rohitp19 Dec 28, 2023
fc7ff3b
Do not publish plugin if plugin is in unplished state before and now …
rohitp19 Dec 28, 2023
4768bc8
Reverted unwanted changes
rohitp19 Dec 28, 2023
2c7cfb3
Delete keys access token and refresh token when credentials changes s…
rohitp19 Jan 2, 2024
4bf0d0f
Changed the validator implemenation with validator option suggested b…
rohitp19 Jan 2, 2024
936ab4b
Added Test cases
rohitp19 Jan 2, 2024
052115b
Added Validation test case
rohitp19 Jan 2, 2024
964b04b
Moved plugin unpublish check before sending an event
rohitp19 Jan 4, 2024
4a0b57e
Mock the HTTP client for all integrations in the test env
fedys Jan 5, 2024
6f69180
Fixed Review comments
rohitp19 Jan 8, 2024
33819ad
Added test cases for validator
rohitp19 Jan 15, 2024
5e5e1c9
Created language translation string
rohitp19 Jan 15, 2024
38701ef
Added Test cases
rohitp19 Jan 15, 2024
c812488
Removed Unnecessary variable
rohitp19 Jan 15, 2024
70b5c04
Added basic Unit test case in APIErrorException
rohitp19 Jan 15, 2024
903686c
Fix method name
rohitp19 Jan 15, 2024
ee1bde2
Merge branch '5.x' into add_plugin_can_publish
rohitpavaskar Apr 26, 2024
f245d4a
PHPSTAN
rohitp19 Apr 29, 2024
c8485ce
Merge branch 'add_plugin_can_publish' of https://github.com/rohitpava…
rohitp19 Apr 29, 2024
66f4823
Fix test cases
rohitp19 Apr 30, 2024
cc7787b
Fix PHPSTAN
rohitp19 Apr 30, 2024
9ea6997
Update app/bundles/PluginBundle/Translations/en_US/validators.ini
rohitpavaskar May 15, 2024
19ad87b
Update app/bundles/PluginBundle/Tests/Form/Constraint/CanPublishValid…
rohitpavaskar May 15, 2024
f469083
Update app/bundles/PluginBundle/Event/PluginIsPublishedEvent.php
rohitpavaskar May 15, 2024
43649cb
Update app/bundles/PluginBundle/PluginEvents.php
rohitpavaskar May 15, 2024
87982a1
Review changes
rohitp19 May 15, 2024
f415f28
Merge branch 'add_plugin_can_publish' of https://github.com/rohitpava…
rohitp19 May 15, 2024
74fabfc
Merge branch '5.x' into add_plugin_can_publish
rohitpavaskar May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/bundles/PluginBundle/Config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
'mautic.factory',
],
],
'mautic.plugin.validator.can_publish_validator' => [
'class' => Mautic\PluginBundle\Form\Constraint\CanPublishValidator::class,
'arguments' => [
'event_dispatcher',
],
'tag' => 'validator.constraint_validator',
],
rohitpavaskar marked this conversation as resolved.
Show resolved Hide resolved
],
'facades' => [
'mautic.plugin.facade.reload' => [
Expand Down
41 changes: 38 additions & 3 deletions app/bundles/PluginBundle/Controller/PluginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@
$leadFields = $pluginModel->getLeadFields();
$companyFields = $pluginModel->getCompanyFields();
/** @var AbstractIntegration $integrationObject */
$entity = $integrationObject->getIntegrationSettings();

$form = $this->createForm(
$entity = $integrationObject->getIntegrationSettings();
$existingPublishedState = $entity->getIsPublished();
$form = $this->createForm(
DetailsType::class,
$entity,
[
Expand Down Expand Up @@ -203,6 +203,9 @@
$keys[$secretKey] = $currentKeys[$secretKey];
}
}
$keys = $this->removeAuthData($keys, $currentKeys, $integrationObject);
$integrationObject->encryptAndSetApiKeys($keys, $entity);

$integrationObject->encryptAndSetApiKeys($keys, $entity);
}

Expand Down Expand Up @@ -254,6 +257,9 @@
$mauticLogger->info('Dispatching integration config save event.');
if ($dispatcher->hasListeners(PluginEvents::PLUGIN_ON_INTEGRATION_CONFIG_SAVE)) {
$mauticLogger->info('Event dispatcher has integration config save listeners.');
if (!$valid && !$existingPublishedState) {
$integrationObject->getIntegrationSettings()->setIsPublished(false);

Check warning on line 261 in app/bundles/PluginBundle/Controller/PluginController.php

View check run for this annotation

Codecov / codecov/patch

app/bundles/PluginBundle/Controller/PluginController.php#L260-L261

Added lines #L260 - L261 were not covered by tests
}
$event = new PluginIntegrationEvent($integrationObject);

$dispatcher->dispatch($event, PluginEvents::PLUGIN_ON_INTEGRATION_CONFIG_SAVE);
Expand Down Expand Up @@ -432,4 +438,33 @@
]
);
}

/**
* @param array <string,mixed> $keys
* @param array <string,mixed> $currentKeys
*
* @return array <string,mixed>
*
* @phpstan-ignore-next-line
rohitpavaskar marked this conversation as resolved.
Show resolved Hide resolved
*/
private function removeAuthData(array $keys, array $currentKeys, AbstractIntegration $integrationObject): array
{
$resetTokens = false;
$secretKeys = array_unique(array_merge($integrationObject->getSecretKeys(), [$integrationObject->getClientIdKey()]));

foreach ($secretKeys as $secretKey) {
if (($keys[$secretKey] ?? null) !== ($currentKeys[$secretKey] ?? null)) {
$resetTokens = true;
break;
}
}

if (!$resetTokens) {
return $keys;

Check warning on line 463 in app/bundles/PluginBundle/Controller/PluginController.php

View check run for this annotation

Codecov / codecov/patch

app/bundles/PluginBundle/Controller/PluginController.php#L463

Added line #L463 was not covered by tests
}

$keysToRemove = array_unique(array_merge($integrationObject->getRefreshTokenKeys(), [$integrationObject->getAuthTokenKey()]));

return array_diff_key($keys, array_flip($keysToRemove));
}
}
47 changes: 47 additions & 0 deletions app/bundles/PluginBundle/Event/PluginIsPublishedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Mautic\PluginBundle\Event;

class PluginIsPublishedEvent extends \Symfony\Contracts\EventDispatcher\Event
{
private string $message;
private bool $canPublish;

public function __construct(private int $value, private string $integrationName)
{
$this->canPublish = true;
$this->message = '';
}
rohitpavaskar marked this conversation as resolved.
Show resolved Hide resolved

public function getValue(): int
{
return $this->value;
}

public function getMessage(): string
{
return $this->message;
}

public function setMessage(string $message): void
{
$this->message = $message;
}

public function isCanPublish(): bool
{
return $this->canPublish;
}

public function setCanPublish(bool $canPublish): void
{
$this->canPublish = $canPublish;
}

public function getIntegrationName(): string
{
return $this->integrationName;
}
}
14 changes: 14 additions & 0 deletions app/bundles/PluginBundle/Exception/ApiErrorException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class ApiErrorException extends \Exception

private ?Lead $contact = null;

private string $shortMessage = '';

/**
* @param string $message
* @param int $code
Expand Down Expand Up @@ -56,4 +58,16 @@ public function setContact(Lead $contact)

return $this;
}

public function getShortMessage(): string
{
return $this->shortMessage;
}

public function setShortMessage(string $shortMessage): ApiErrorException
{
$this->shortMessage = $shortMessage;

return $this;
}
}
19 changes: 19 additions & 0 deletions app/bundles/PluginBundle/Form/Constraint/CanPublish.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Mautic\PluginBundle\Form\Constraint;

use Symfony\Component\Validator\Constraint;

class CanPublish extends Constraint
{
public string $message = 'mautic.lead_list.not_allowed_plugin_publish';

public string $integrationName;

public function getDefaultOption(): string
{
return 'integrationName';
}
}
35 changes: 35 additions & 0 deletions app/bundles/PluginBundle/Form/Constraint/CanPublishValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Mautic\PluginBundle\Form\Constraint;

use Mautic\PluginBundle\Event\PluginIsPublishedEvent;
use Mautic\PluginBundle\PluginEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class CanPublishValidator extends ConstraintValidator
{
public function __construct(private EventDispatcherInterface $eventDispatcher)
{
}

public function validate($value, Constraint $constraint): void
{
if (1 !== $value) {
return;
}
if (!$constraint instanceof CanPublish) {
throw new \Symfony\Component\Validator\Exception\UnexpectedTypeException($constraint, CanPublish::class);
}
$event = new PluginIsPublishedEvent($value, $constraint->integrationName);
$event = $this->eventDispatcher->dispatch($event, PluginEvents::PLUGIN_IS_PUBLISHED_STATE_CHANGING);

if (!$event->isCanPublish()) {
$this->context->buildViolation($event->getMessage())
->addViolation();
}
}
}
7 changes: 6 additions & 1 deletion app/bundles/PluginBundle/Form/Type/DetailsType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Mautic\CoreBundle\Form\Type\StandAloneButtonType;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Form\Constraint\CanPublish;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
Expand All @@ -22,7 +23,11 @@ class DetailsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('isPublished', YesNoButtonGroupType::class);
$builder->add('isPublished', YesNoButtonGroupType::class, [
'constraints' => [
new CanPublish($options['integration'] ?? ''),
],
]);

/** @var AbstractIntegration $integrationObject */
$integrationObject = $options['integration_object'];
Expand Down
2 changes: 1 addition & 1 deletion app/bundles/PluginBundle/Form/Type/FeatureSettingsType.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
$settings = [
'silence_exceptions' => false,
'feature_settings' => $data,
'ignore_field_cache' => (1 == $page && 'POST' !== $_SERVER['REQUEST_METHOD']) ? true : false,
'ignore_field_cache' => 1 == $page && 'POST' !== ($_SERVER['REQUEST_METHOD'] ?? null),
];

try {
Expand Down
37 changes: 27 additions & 10 deletions app/bundles/PluginBundle/Integration/AbstractIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@

protected array $persistIntegrationEntities = [];

protected array $commandParameters = [];
protected array $commandParameters = [];
private \Closure $clientFactory;

public function __construct(
protected EventDispatcherInterface $dispatcher,
Expand All @@ -119,6 +120,12 @@
$this->cache = $cacheStorageHelper->getCache($this->getName());
$this->session = (!defined('IN_MAUTIC_CONSOLE')) ? $session : null;
$this->request = (!defined('IN_MAUTIC_CONSOLE')) ? $requestStack->getCurrentRequest() : null;

$this->setClientFactory(fn (array $options): \GuzzleHttp\Client => new Client([
'handler' => HandlerStack::create(new CurlHandler([

Check warning on line 125 in app/bundles/PluginBundle/Integration/AbstractIntegration.php

View check run for this annotation

Codecov / codecov/patch

app/bundles/PluginBundle/Integration/AbstractIntegration.php#L125

Added line #L125 was not covered by tests
'options' => $options,
])),
]));
}

public function setCommandParameters(array $params): void
Expand Down Expand Up @@ -795,12 +802,16 @@
break;
}
} catch (\GuzzleHttp\Exception\RequestException $exception) {
return [
'error' => [
'message' => $exception->getResponse()->getBody()->getContents(),
'code' => $exception->getCode(),
],
];
if (!empty($settings['return_raw'])) {
return $exception->getResponse();

Check warning on line 806 in app/bundles/PluginBundle/Integration/AbstractIntegration.php

View check run for this annotation

Codecov / codecov/patch

app/bundles/PluginBundle/Integration/AbstractIntegration.php#L805-L806

Added lines #L805 - L806 were not covered by tests
} else {
return [
'error' => [
'message' => $exception->getResponse()->getBody()->getContents(),
'code' => $exception->getCode(),

Check warning on line 811 in app/bundles/PluginBundle/Integration/AbstractIntegration.php

View check run for this annotation

Codecov / codecov/patch

app/bundles/PluginBundle/Integration/AbstractIntegration.php#L810-L811

Added lines #L810 - L811 were not covered by tests
],
];
}
}
if (empty($settings['ignore_event_dispatch'])) {
$event->setResponse($result);
Expand Down Expand Up @@ -2435,6 +2446,14 @@
return $records;
}

/**
* @param callable(mixed[]): Client $clientFactory
*/
public function setClientFactory(callable $clientFactory): void
{
$this->clientFactory = \Closure::fromCallable($clientFactory);
}

/**
* Because so many integrations extend this class and mautic.http.client is not in the
* constructor at the time of writing, let's just create a new client here. In addition,
Expand All @@ -2444,8 +2463,6 @@
*/
protected function makeHttpClient(array $options): Client
{
return new Client(['handler' => HandlerStack::create(new CurlHandler([
'options' => $options,
]))]);
return ($this->clientFactory)($options);

Check warning on line 2466 in app/bundles/PluginBundle/Integration/AbstractIntegration.php

View check run for this annotation

Codecov / codecov/patch

app/bundles/PluginBundle/Integration/AbstractIntegration.php#L2466

Added line #L2466 was not covered by tests
}
}
10 changes: 10 additions & 0 deletions app/bundles/PluginBundle/MauticPluginBundle.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
<?php

declare(strict_types=1);

namespace Mautic\PluginBundle;

use Mautic\PluginBundle\Tests\DependencyInjection\Compiler\TestPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class MauticPluginBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
if ('test' === $container->getParameter('kernel.environment')) {
$container->addCompilerPass(new TestPass());
}
}
}
9 changes: 9 additions & 0 deletions app/bundles/PluginBundle/PluginEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,13 @@ final class PluginEvents
* @var string
*/
public const ON_PLUGIN_INSTALL = 'mautic.plugin.on_plugin_install';

/**
* The mautic.plugin.is_published_state_changing event is dispatched when a user tries to changes plugin is published state.
rohitpavaskar marked this conversation as resolved.
Show resolved Hide resolved
*
* The event listener receives a Mautic\PluginBundle\Event\PluginPublishedEvent instance.
*
* @var string
*/
public const PLUGIN_IS_PUBLISHED_STATE_CHANGING= 'mautic.plugin.is_published_state_changing';
}
50 changes: 50 additions & 0 deletions app/bundles/PluginBundle/Tests/Controller/PluginControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Mautic\PluginBundle\Tests\Controller;

use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;

class PluginControllerTest extends MauticMysqlTestCase
{
public function testConfigurePluginSuccessValidation(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/plugins/config/Twilio');
$form = $crawler->filter('form')->form();

$form->setValues([
'integration_details' => [
'isPublished' => 0,
'apiKeys' => [
'username' => 'valid_username',
'password' => 'valid_password',
],
],
]);

$this->client->submit($form);
Assert::assertTrue($this->client->getResponse()->isOk());
}

public function testConfigurePluginValidationError(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/plugins/config/Twilio');
$form = $crawler->filter('form')->form();

$form->setValues([
'integration_details' => [
'isPublished' => 1,
'apiKeys' => [
'username' => '',
'password' => 'bbb',
],
],
]);

$crawler = $this->client->submit($form);
Assert::assertStringContainsString('A value is required.', $crawler->filter('#integration_details_apiKeys div')->html());
}
}