Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Api;
|
||||
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Mautic\MarketplaceBundle\Exception\ApiException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class Connection
|
||||
{
|
||||
public function __construct(
|
||||
private ClientInterface $httpClient,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function getPlugins(int $page, int $limit, string $query = ''): array
|
||||
{
|
||||
return $this->makeRequest("https://packagist.org/search.json?page={$page}&per_page={$limit}&type=mautic-plugin&q={$query}");
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function getPackage(string $pluginName): array
|
||||
{
|
||||
return $this->makeRequest("https://packagist.org/packages/{$pluginName}.json");
|
||||
}
|
||||
|
||||
public function makeRequest(string $url): array
|
||||
{
|
||||
$this->logger->debug('About to query the Packagist API: '.$url);
|
||||
|
||||
$request = new Request('GET', $url, $this->getHeaders());
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->send($request);
|
||||
} catch (GuzzleException $e) {
|
||||
throw new ApiException($e->getMessage(), $e->getCode());
|
||||
}
|
||||
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
if ($response->getStatusCode() >= 300) {
|
||||
throw new ApiException($body, $response->getStatusCode());
|
||||
}
|
||||
|
||||
$payload = json_decode($body, true);
|
||||
|
||||
$this->logger->debug('Successful Packagist API response', ['payload' => $payload]);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function getHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'Accept-Encoding' => 'gzip, deflate, br',
|
||||
'Connection' => 'keep-alive',
|
||||
'User-Agent' => 'Mautic Marketplace',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Collection;
|
||||
|
||||
use Mautic\MarketplaceBundle\DTO\Maintainer;
|
||||
use Mautic\MarketplaceBundle\Exception\RecordNotFoundException;
|
||||
|
||||
class MaintainerCollection implements \Iterator, \Countable, \ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @var Maintainer[]
|
||||
*/
|
||||
private array $records;
|
||||
|
||||
private int $position = 0;
|
||||
|
||||
/**
|
||||
* @param Maintainer[] $records
|
||||
*/
|
||||
public function __construct(array $records = [])
|
||||
{
|
||||
$this->records = array_values($records);
|
||||
}
|
||||
|
||||
public static function fromArray(array $array): MaintainerCollection
|
||||
{
|
||||
return new self(
|
||||
array_map(
|
||||
fn (array $record) => Maintainer::fromArray($record),
|
||||
$array
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function current(): Maintainer
|
||||
{
|
||||
return $this->records[$this->position];
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
++$this->position;
|
||||
}
|
||||
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->records[$this->position]);
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->records);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (is_null($offset)) {
|
||||
$this->records[] = $value;
|
||||
} else {
|
||||
$this->records[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->records[$offset]);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->records[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): Maintainer
|
||||
{
|
||||
if (isset($this->records[$offset])) {
|
||||
return $this->records[$offset];
|
||||
}
|
||||
|
||||
throw new RecordNotFoundException("Maintainer on offset {$offset} was not found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Collection;
|
||||
|
||||
use Mautic\MarketplaceBundle\DTO\PackageBase;
|
||||
use Mautic\MarketplaceBundle\Exception\RecordNotFoundException;
|
||||
|
||||
class PackageCollection implements \Iterator, \Countable, \ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @var PackageBase[]
|
||||
*/
|
||||
private array $records;
|
||||
|
||||
private int $position = 0;
|
||||
|
||||
/**
|
||||
* @param PackageBase[] $records
|
||||
*/
|
||||
public function __construct(array $records = [])
|
||||
{
|
||||
$this->records = array_values($records);
|
||||
}
|
||||
|
||||
public static function fromArray(array $array): PackageCollection
|
||||
{
|
||||
return new self(
|
||||
array_map(
|
||||
fn (array $record) => PackageBase::fromArray($record),
|
||||
$array
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function map(callable $callback): PackageCollection
|
||||
{
|
||||
return new self(array_map($callback, $this->records));
|
||||
}
|
||||
|
||||
public function filter(callable $callback): PackageCollection
|
||||
{
|
||||
return new self(array_values(array_filter($this->records, $callback)));
|
||||
}
|
||||
|
||||
public function current(): PackageBase
|
||||
{
|
||||
return $this->records[$this->position];
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
++$this->position;
|
||||
}
|
||||
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->records[$this->position]);
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->records);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (is_null($offset)) {
|
||||
$this->records[] = $value;
|
||||
} else {
|
||||
$this->records[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->records[$offset]);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->records[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): PackageBase
|
||||
{
|
||||
if (isset($this->records[$offset])) {
|
||||
return $this->records[$offset];
|
||||
}
|
||||
|
||||
throw new RecordNotFoundException("Package on offset {$offset} was not found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Collection;
|
||||
|
||||
use Mautic\MarketplaceBundle\DTO\Version;
|
||||
use Mautic\MarketplaceBundle\Exception\RecordNotFoundException;
|
||||
|
||||
class VersionCollection implements \Iterator, \Countable, \ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @var Version[]
|
||||
*/
|
||||
private array $records;
|
||||
|
||||
private int $position = 0;
|
||||
|
||||
/**
|
||||
* @param Version[] $records
|
||||
*/
|
||||
public function __construct(array $records = [])
|
||||
{
|
||||
$this->records = array_values($records);
|
||||
}
|
||||
|
||||
public static function fromArray(array $array): VersionCollection
|
||||
{
|
||||
return new self(
|
||||
array_map(
|
||||
fn (array $record) => Version::fromArray($record),
|
||||
$array
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function map(callable $callback): VersionCollection
|
||||
{
|
||||
return new self(array_map($callback, $this->records));
|
||||
}
|
||||
|
||||
public function sortByLatest(): VersionCollection
|
||||
{
|
||||
$records = $this->records;
|
||||
|
||||
usort(
|
||||
$records,
|
||||
fn (Version $versionA, Version $versionB) => $versionB->time->getTimestamp() - $versionA->time->getTimestamp()
|
||||
);
|
||||
|
||||
return new self($records);
|
||||
}
|
||||
|
||||
public function filter(callable $callback): VersionCollection
|
||||
{
|
||||
return new self(array_values(array_filter($this->records, $callback)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the latest stable version. If no stable version is found, returns the version with latest timestamp.
|
||||
*/
|
||||
public function findLatestStableVersionPackage(): ?Version
|
||||
{
|
||||
return $this->sortByLatest()->filter(fn (Version $version) => $version->isStable())->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the latest stable version. If no stable version is found, returns the version with latest timestamp.
|
||||
*/
|
||||
public function findLatestVersionPackage(): ?Version
|
||||
{
|
||||
return $this->sortByLatest()->first();
|
||||
}
|
||||
|
||||
public function current(): Version
|
||||
{
|
||||
return $this->records[$this->position];
|
||||
}
|
||||
|
||||
public function first(): ?Version
|
||||
{
|
||||
return $this->records[0] ?? null;
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
++$this->position;
|
||||
}
|
||||
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->records[$this->position]);
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->records);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
if (is_null($offset)) {
|
||||
$this->records[] = $value;
|
||||
} else {
|
||||
$this->records[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->records[$offset]);
|
||||
}
|
||||
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->records[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet($offset): Version
|
||||
{
|
||||
if (isset($this->records[$offset])) {
|
||||
return $this->records[$offset];
|
||||
}
|
||||
|
||||
throw new RecordNotFoundException("Version on offset {$offset} was not found");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ComposerHelper;
|
||||
use Mautic\MarketplaceBundle\Exception\ApiException;
|
||||
use Mautic\MarketplaceBundle\Model\PackageModel;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: InstallCommand::NAME,
|
||||
description: 'Installs a plugin that is available at Packagist.org'
|
||||
)]
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
public const NAME = 'mautic:marketplace:install';
|
||||
|
||||
public function __construct(
|
||||
private ComposerHelper $composer,
|
||||
private PackageModel $packageModel,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('package', InputArgument::REQUIRED, 'The Packagist package to install (e.g. mautic/example-plugin)');
|
||||
$this->addOption('dry-run', null, null, 'Simulate the installation of the package. Doesn\'t actually install it.');
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$packageName = $input->getArgument('package');
|
||||
$dryRun = true === $input->getOption('dry-run') ? true : false;
|
||||
|
||||
try {
|
||||
$package = $this->packageModel->getPackageDetail($packageName);
|
||||
} catch (ApiException $e) {
|
||||
if (404 === $e->getCode()) {
|
||||
throw new \InvalidArgumentException('Given package '.$packageName.' does not exist in Packagist. Please check the name for typos.');
|
||||
} else {
|
||||
throw new \Exception('Error while trying to get package details: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($package->packageBase->type) || 'mautic-plugin' !== $package->packageBase->type) {
|
||||
throw new \Exception('Package type is not mautic-plugin. Cannot install this plugin.');
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$output->writeLn('Note: dry-running this installation!');
|
||||
}
|
||||
|
||||
$output->writeln('Installing '.$input->getArgument('package').', this might take a while...');
|
||||
$result = $this->composer->install($input->getArgument('package'), $dryRun);
|
||||
|
||||
if (0 !== $result->exitCode) {
|
||||
$output->writeln('<error>Error while installing this plugin.</error>');
|
||||
|
||||
if ($result->output) {
|
||||
$output->writeln($result->output);
|
||||
} else {
|
||||
// If the output is empty then tell the user where to find more details.
|
||||
$output->writeln('Check the logs for more details or run again with the -vvv parameter.');
|
||||
}
|
||||
|
||||
return $result->exitCode;
|
||||
}
|
||||
|
||||
$output->writeln('All done! '.$input->getArgument('package').' has successfully been installed.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Command;
|
||||
|
||||
use Mautic\MarketplaceBundle\DTO\PackageBase;
|
||||
use Mautic\MarketplaceBundle\Service\PluginCollector;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
#[AsCommand(
|
||||
name: ListCommand::NAME,
|
||||
description: 'Lists plugins that are available at Packagist.org'
|
||||
)]
|
||||
class ListCommand extends Command
|
||||
{
|
||||
public const NAME = 'mautic:marketplace:list';
|
||||
|
||||
public function __construct(
|
||||
private PluginCollector $pluginCollector,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('page', 'p', InputOption::VALUE_OPTIONAL, 'Page number', 1);
|
||||
$this->addOption('limit', 'l', InputOption::VALUE_OPTIONAL, 'Packages per page', 15);
|
||||
$this->addOption('filter', 'f', InputOption::VALUE_OPTIONAL, 'Filter the packages', '');
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$stopwatch = new Stopwatch();
|
||||
$stopwatch->start('command');
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(['name', 'downloads', 'favers']);
|
||||
|
||||
$plugins = $this->pluginCollector->collectPackages($input->getOption('page'), $input->getOption('limit'), $input->getOption('filter'));
|
||||
|
||||
/** @var PackageBase $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$color = 'white';
|
||||
$delimiter = "\n ";
|
||||
$description = $plugin->description ? $delimiter.wordwrap($plugin->description, 50, $delimiter) : '';
|
||||
$table->addRow([
|
||||
"<fg={$color}>{$plugin->name}{$description}</>",
|
||||
"<fg={$color}>{$plugin->downloads}</>",
|
||||
"<fg={$color}>{$plugin->favers}</>",
|
||||
]);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
|
||||
$event = $stopwatch->stop('command');
|
||||
|
||||
$io->writeln("<fg=green>Total packages: {$this->pluginCollector->getTotal()}</>");
|
||||
$io->writeln("<fg=green>Execution time: {$event->getDuration()} ms</>");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ComposerHelper;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: RemoveCommand::NAME,
|
||||
description: 'Removes a plugin that is currently installed'
|
||||
)]
|
||||
class RemoveCommand extends Command
|
||||
{
|
||||
public const NAME = 'mautic:marketplace:remove';
|
||||
|
||||
public function __construct(
|
||||
private ComposerHelper $composer,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('package', InputArgument::REQUIRED, 'The Packagist package of the plugin to remove (e.g. mautic/example-plugin)');
|
||||
|
||||
parent::configure();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->writeln('Removing '.$input->getArgument('package').', this might take a while...');
|
||||
$packageVendorAndName = $input->getArgument('package');
|
||||
|
||||
// Just checking the package type so that the user doesn't accidentially removes a core package
|
||||
if (!in_array($packageVendorAndName, $this->composer->getMauticPluginPackages())) {
|
||||
$output->writeln('This package cannot be removed, it must be of type mautic-plugin');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$removeResult = $this->composer->remove($packageVendorAndName);
|
||||
|
||||
if (0 !== $removeResult->exitCode) {
|
||||
$message = 'Error while removing plugin through Composer: '.$removeResult->output;
|
||||
$this->logger->error($message);
|
||||
$output->writeLn($message);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$output->writeln($input->getArgument('package').' has successfully been removed.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Mautic\MarketplaceBundle\Service\RouteProvider;
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
RouteProvider::ROUTE_LIST => [
|
||||
'path' => '/marketplace/{page}',
|
||||
'controller' => 'Mautic\MarketplaceBundle\Controller\Package\ListController::listAction',
|
||||
'method' => 'GET|POST',
|
||||
'defaults' => ['page' => 1],
|
||||
],
|
||||
RouteProvider::ROUTE_DETAIL => [
|
||||
'path' => '/marketplace/detail/{vendor}/{package}',
|
||||
'controller' => 'Mautic\MarketplaceBundle\Controller\Package\DetailController::viewAction',
|
||||
'method' => 'GET',
|
||||
],
|
||||
RouteProvider::ROUTE_INSTALL => [
|
||||
'path' => '/marketplace/install/{vendor}/{package}',
|
||||
'controller' => 'Mautic\MarketplaceBundle\Controller\Package\InstallController::viewAction',
|
||||
'method' => 'GET|POST',
|
||||
],
|
||||
RouteProvider::ROUTE_REMOVE => [
|
||||
'path' => '/marketplace/remove/{vendor}/{package}',
|
||||
'controller' => 'Mautic\MarketplaceBundle\Controller\Package\RemoveController::viewAction',
|
||||
'method' => 'GET|POST',
|
||||
],
|
||||
RouteProvider::ROUTE_CLEAR_CACHE => [
|
||||
'path' => '/marketplace/clear/cache',
|
||||
'controller' => 'Mautic\MarketplaceBundle\Controller\CacheController::clearAction',
|
||||
'method' => 'GET',
|
||||
],
|
||||
],
|
||||
],
|
||||
'services' => [
|
||||
'permissions' => [
|
||||
'marketplace.permissions' => [
|
||||
'class' => Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
'marketplace.service.config',
|
||||
],
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'marketplace.api.connection' => [
|
||||
'class' => Mautic\MarketplaceBundle\Api\Connection::class,
|
||||
'arguments' => [
|
||||
'mautic.http.client',
|
||||
'monolog.logger.mautic',
|
||||
],
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
'marketplace.service.plugin_collector' => [
|
||||
'class' => Mautic\MarketplaceBundle\Service\PluginCollector::class,
|
||||
'arguments' => [
|
||||
'marketplace.api.connection',
|
||||
'marketplace.service.allowlist',
|
||||
],
|
||||
],
|
||||
'marketplace.service.route_provider' => [
|
||||
'class' => RouteProvider::class,
|
||||
'arguments' => ['router'],
|
||||
],
|
||||
'marketplace.service.config' => [
|
||||
'class' => Config::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
],
|
||||
'marketplace.service.allowlist' => [
|
||||
'class' => Mautic\MarketplaceBundle\Service\Allowlist::class,
|
||||
'arguments' => [
|
||||
'marketplace.service.config',
|
||||
'mautic.cache.provider',
|
||||
'mautic.http.client',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
// NOTE: when adding new parameters here, please add them to the developer documentation as well:
|
||||
'parameters' => [
|
||||
Config::MARKETPLACE_ENABLED => true,
|
||||
Config::MARKETPLACE_ALLOWLIST_URL => 'https://raw.githubusercontent.com/mautic/marketplace-allowlist/main/allowlist.json',
|
||||
Config::MARKETPLACE_ALLOWLIST_CACHE_TTL_SECONDS => 3600,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('Mautic\\MarketplaceBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->alias('marketplace.model.package', Mautic\MarketplaceBundle\Model\PackageModel::class);
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CacheHelper;
|
||||
use Mautic\CoreBundle\Helper\ComposerHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
public function __construct(
|
||||
private ComposerHelper $composer,
|
||||
private CacheHelper $cacheHelper,
|
||||
private LoggerInterface $logger,
|
||||
private Config $config,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
public function installPackageAction(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->config->marketplaceIsEnabled()) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.request.marketplace_disabled'),
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(MarketplacePermissions::CAN_INSTALL_PACKAGES)
|
||||
|| !$this->config->isComposerEnabled()) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.request.no_permissions'),
|
||||
], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
if (empty($data['vendor']) || empty($data['package'])) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.request.details.missing'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$packageName = $data['vendor'].'/'.$data['package'];
|
||||
|
||||
if ($this->composer->isInstalled($packageName)) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.install.already.installed'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$installResult = $this->composer->install($packageName);
|
||||
|
||||
if (Command::SUCCESS !== $installResult->exitCode) {
|
||||
return $this->installError(new \Exception($installResult->output));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->installError($e);
|
||||
}
|
||||
|
||||
// Note: do not do anything except returning a response after clearing the cache to prevent errors
|
||||
$clearCacheResult = $this->clearCacheOrReturnError();
|
||||
|
||||
if (null !== $clearCacheResult) {
|
||||
return $clearCacheResult;
|
||||
}
|
||||
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
|
||||
public function removePackageAction(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->config->marketplaceIsEnabled()) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.request.marketplace_disabled'),
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(MarketplacePermissions::CAN_REMOVE_PACKAGES)
|
||||
|| !$this->config->isComposerEnabled()) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.request.no_permissions'),
|
||||
], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
if (empty($data['vendor']) || empty($data['package'])) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.request.details.missing'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$packageName = $data['vendor'].'/'.$data['package'];
|
||||
|
||||
if (!$this->composer->isInstalled($packageName)) {
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.remove.not.installed'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$removeResult = $this->composer->remove($packageName);
|
||||
|
||||
if (0 !== $removeResult->exitCode) {
|
||||
return $this->removeError(new \Exception($removeResult->output));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->removeError($e);
|
||||
}
|
||||
|
||||
// Note: do not do anything except returning a response after clearing the cache to prevent errors
|
||||
$clearCacheResult = $this->clearCacheOrReturnError();
|
||||
|
||||
if (null !== $clearCacheResult) {
|
||||
return $clearCacheResult;
|
||||
}
|
||||
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
|
||||
private function clearCacheOrReturnError(): ?JsonResponse
|
||||
{
|
||||
try {
|
||||
$exitCode = $this->cacheHelper->clearSymfonyCache();
|
||||
|
||||
if (0 !== $exitCode) {
|
||||
$this->logger->error('Could not clear Mautic\'s cache. Please try again.');
|
||||
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.cache.clear.failed'),
|
||||
], 500);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Could not clear Mautic\'s cache. Details: '.$e->getMessage());
|
||||
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.cache.clear.failed'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function installError(\Exception $e): JsonResponse
|
||||
{
|
||||
$this->logger->error('Installation of plugin through Composer has failed: '.$e->getMessage());
|
||||
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.install.failed'),
|
||||
], 500);
|
||||
}
|
||||
|
||||
private function removeError(\Exception $e): JsonResponse
|
||||
{
|
||||
$this->logger->error('Error while removing package through Composer: '.$e->getMessage());
|
||||
|
||||
return $this->sendJsonResponse([
|
||||
'error' => $this->translator->trans('marketplace.package.remove.failed'),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Allowlist;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CacheController extends CommonController
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private Allowlist $allowlist,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
public function clearAction(): Response
|
||||
{
|
||||
if (!$this->config->marketplaceIsEnabled()) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(MarketplacePermissions::CAN_VIEW_PACKAGES)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->allowlist->clearCache();
|
||||
|
||||
return $this->forward(
|
||||
'Mautic\MarketplaceBundle\Controller\Package\ListController::listAction'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Controller\Package;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\ComposerHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\MarketplaceBundle\Exception\RecordNotFoundException;
|
||||
use Mautic\MarketplaceBundle\Model\PackageModel;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Mautic\MarketplaceBundle\Service\RouteProvider;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DetailController extends CommonController
|
||||
{
|
||||
public function __construct(
|
||||
private PackageModel $packageModel,
|
||||
private RouteProvider $routeProvider,
|
||||
private Config $config,
|
||||
private ComposerHelper $composer,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
public function viewAction(string $vendor, string $package): Response
|
||||
{
|
||||
if (!$this->config->marketplaceIsEnabled()) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(MarketplacePermissions::CAN_VIEW_PACKAGES)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$isInstalled = $this->composer->isInstalled("{$vendor}/{$package}");
|
||||
|
||||
try {
|
||||
$packageDetail = $this->packageModel->getPackageDetail("{$vendor}/{$package}");
|
||||
} catch (RecordNotFoundException $e) {
|
||||
return $this->notFound($e->getMessage());
|
||||
}
|
||||
|
||||
$security = $this->security;
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'returnUrl' => $this->routeProvider->buildListRoute(),
|
||||
'viewParameters' => [
|
||||
'packageDetail' => $packageDetail,
|
||||
'isInstalled' => $isInstalled,
|
||||
'isComposerEnabled' => $this->config->isComposerEnabled(),
|
||||
'security' => $security,
|
||||
],
|
||||
'contentTemplate' => '@Marketplace/Package/detail.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => 'package',
|
||||
'activeLink' => '#mautic_marketplace',
|
||||
'route' => $this->routeProvider->buildDetailRoute($vendor, $package),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Controller\Package;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\MarketplaceBundle\Model\PackageModel;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Mautic\MarketplaceBundle\Service\RouteProvider;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class InstallController extends CommonController
|
||||
{
|
||||
public function __construct(
|
||||
private PackageModel $packageModel,
|
||||
private RouteProvider $routeProvider,
|
||||
private Config $config,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
public function viewAction(string $vendor, string $package): Response
|
||||
{
|
||||
if (!$this->config->marketplaceIsEnabled()) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(MarketplacePermissions::CAN_INSTALL_PACKAGES)
|
||||
|| !$this->config->isComposerEnabled()) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'returnUrl' => $this->routeProvider->buildListRoute(),
|
||||
'viewParameters' => [
|
||||
'packageDetail' => $this->packageModel->getPackageDetail("{$vendor}/{$package}"),
|
||||
],
|
||||
'contentTemplate' => '@Marketplace/Package/install.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => 'package',
|
||||
'activeLink' => '#mautic_marketplace',
|
||||
'route' => $this->routeProvider->buildInstallRoute($vendor, $package),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Controller\Package;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Mautic\MarketplaceBundle\Service\PluginCollector;
|
||||
use Mautic\MarketplaceBundle\Service\RouteProvider;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ListController extends CommonController
|
||||
{
|
||||
public function __construct(
|
||||
private PluginCollector $pluginCollector,
|
||||
private RouteProvider $routeProvider,
|
||||
ManagerRegistry $doctrine,
|
||||
private Config $config,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
public function listAction(int $page = 1): Response
|
||||
{
|
||||
if (!$this->config->marketplaceIsEnabled()) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(MarketplacePermissions::CAN_VIEW_PACKAGES)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$request = $this->getCurrentRequest();
|
||||
$search = InputHelper::clean($request->get('search', ''));
|
||||
|
||||
$session = $request->getSession();
|
||||
if (empty($page)) {
|
||||
$page = $session->get('mautic.marketplace.package.page', 1);
|
||||
}
|
||||
|
||||
// set limits
|
||||
$limit = $session->get('mautic.marketplace.package.limit', $this->coreParametersHelper->get('default_pagelimit'));
|
||||
$route = $this->routeProvider->buildListRoute($page);
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'returnUrl' => $route,
|
||||
'viewParameters' => [
|
||||
'searchValue' => $search,
|
||||
'items' => $this->pluginCollector->collectPackages($page, $limit, $search),
|
||||
'count' => $this->pluginCollector->getTotal(),
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
|
||||
'isComposerEnabled' => $this->config->isComposerEnabled(),
|
||||
],
|
||||
'contentTemplate' => '@Marketplace/Package/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => 'package',
|
||||
'route' => $route,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Controller\Package;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\MarketplaceBundle\Model\PackageModel;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Mautic\MarketplaceBundle\Service\RouteProvider;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RemoveController extends CommonController
|
||||
{
|
||||
public function __construct(
|
||||
private PackageModel $packageModel,
|
||||
private RouteProvider $routeProvider,
|
||||
private Config $config,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
public function viewAction(string $vendor, string $package): Response
|
||||
{
|
||||
if (!$this->config->marketplaceIsEnabled()) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(MarketplacePermissions::CAN_REMOVE_PACKAGES)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'returnUrl' => $this->routeProvider->buildListRoute(),
|
||||
'viewParameters' => [
|
||||
'packageDetail' => $this->packageModel->getPackageDetail("{$vendor}/{$package}"),
|
||||
],
|
||||
'contentTemplate' => '@Marketplace/Package/remove.html.twig',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => 'package',
|
||||
'activeLink' => '#mautic_marketplace',
|
||||
'route' => $this->routeProvider->buildRemoveRoute($vendor, $package),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
use Mautic\MarketplaceBundle\Exception\RecordNotFoundException;
|
||||
|
||||
final class Allowlist
|
||||
{
|
||||
/**
|
||||
* @param AllowlistEntry[] $entries
|
||||
*/
|
||||
public function __construct(
|
||||
public array $entries,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $array
|
||||
*/
|
||||
public static function fromArray(array $array): Allowlist
|
||||
{
|
||||
return new self(
|
||||
array_map(fn (array $item) => AllowlistEntry::fromArray($item), $array['allowlist'] ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
public function findPackageByName(string $packageName): AllowlistEntry
|
||||
{
|
||||
foreach ($this->entries as $entry) {
|
||||
if ($entry->package === $packageName) {
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RecordNotFoundException("Package '$packageName' not found in allowlist.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
final class AllowlistEntry
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* Packagist package in the format vendor/package.
|
||||
*/
|
||||
public string $package,
|
||||
/**
|
||||
* Human readable name.
|
||||
*/
|
||||
public string $displayName,
|
||||
/**
|
||||
* Minimum Mautic version in semver format (e.g. 4.1.2).
|
||||
*/
|
||||
public ?string $minimumMauticVersion,
|
||||
/**
|
||||
* Maximum Mautic version in semver format (e.g. 4.1.2).
|
||||
*/
|
||||
public ?string $maximumMauticVersion,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $array
|
||||
*/
|
||||
public static function fromArray(array $array): AllowlistEntry
|
||||
{
|
||||
return new self(
|
||||
$array['package'],
|
||||
$array['display_name'] ?? '',
|
||||
$array['minimum_mautic_version'],
|
||||
$array['maximum_mautic_version']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'package' => $this->package,
|
||||
'display_name' => $this->displayName,
|
||||
'minimum_mautic_version' => $this->minimumMauticVersion,
|
||||
'maximum_mautic_version' => $this->maximumMauticVersion,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
final class ConsoleOutput
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* Console exit code. 0 when everything went fine, or an error code.
|
||||
*/
|
||||
public int $exitCode,
|
||||
public string $output,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
final class GitHubInfo
|
||||
{
|
||||
public function __construct(
|
||||
public int $stars,
|
||||
public int $watchers,
|
||||
public int $forks,
|
||||
public int $openIssues,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
final class Maintainer
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $avatar,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $array): Maintainer
|
||||
{
|
||||
return new self(
|
||||
$array['name'],
|
||||
$array['avatar_url']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
final class PackageBase
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* Original name in format "vendor/name".
|
||||
*/
|
||||
public string $name,
|
||||
public string $url,
|
||||
public string $repository,
|
||||
public string $description,
|
||||
public int $downloads,
|
||||
public int $favers,
|
||||
/**
|
||||
* E.g. mautic-plugin.
|
||||
*/
|
||||
public ?string $type,
|
||||
public ?string $displayName = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $array): self
|
||||
{
|
||||
return new self(
|
||||
$array['name'],
|
||||
$array['url'],
|
||||
$array['repository'],
|
||||
$array['description'],
|
||||
(int) $array['downloads'],
|
||||
(int) $array['favers'],
|
||||
$array['type'] ?? null,
|
||||
$array['display_name'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just an alias to getName(). Used in Mautic helpers.
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in Mautic helpers.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getPackageName(): string
|
||||
{
|
||||
[, $packageName] = explode('/', $this->name);
|
||||
|
||||
return $packageName;
|
||||
}
|
||||
|
||||
public function getHumanPackageName(): string
|
||||
{
|
||||
if ($this->displayName) {
|
||||
return $this->displayName;
|
||||
}
|
||||
|
||||
return utf8_ucwords(str_replace('-', ' ', $this->getPackageName()));
|
||||
}
|
||||
|
||||
public function getVendorName(): string
|
||||
{
|
||||
[$vendor] = explode('/', $this->name);
|
||||
|
||||
return $vendor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
use Mautic\MarketplaceBundle\Collection\MaintainerCollection;
|
||||
use Mautic\MarketplaceBundle\Collection\VersionCollection;
|
||||
|
||||
final class PackageDetail
|
||||
{
|
||||
public function __construct(
|
||||
public PackageBase $packageBase,
|
||||
public VersionCollection $versions,
|
||||
public MaintainerCollection $maintainers,
|
||||
public GitHubInfo $githubInfo,
|
||||
public int $monthlyDownloads,
|
||||
public int $dailyDownloads,
|
||||
public \DateTimeInterface $time,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $array): self
|
||||
{
|
||||
return new self(
|
||||
new PackageBase(
|
||||
$array['name'],
|
||||
"https://packagist.org/packages/{$array['name']}",
|
||||
$array['repository'],
|
||||
$array['description'],
|
||||
(int) $array['downloads']['total'],
|
||||
(int) $array['favers'],
|
||||
$array['type'] ?? null,
|
||||
$array['display_name'] ?? null
|
||||
),
|
||||
VersionCollection::fromArray($array['versions']),
|
||||
MaintainerCollection::fromArray($array['maintainers']),
|
||||
new GitHubInfo(
|
||||
$array['github_stars'],
|
||||
$array['github_watchers'],
|
||||
$array['github_forks'],
|
||||
$array['github_open_issues']
|
||||
),
|
||||
$array['downloads']['monthly'],
|
||||
$array['downloads']['daily'],
|
||||
new \DateTimeImmutable($array['time'])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DTO;
|
||||
|
||||
final class Version
|
||||
{
|
||||
public function __construct(
|
||||
public string $version,
|
||||
public array $license,
|
||||
public \DateTimeInterface $time,
|
||||
public string $homepage,
|
||||
public string $issues,
|
||||
public string $wiki,
|
||||
public array $require,
|
||||
public array $keywords,
|
||||
public ?string $type,
|
||||
public ?string $directoryName,
|
||||
public ?string $displayName,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $array): Version
|
||||
{
|
||||
return new self(
|
||||
$array['version'],
|
||||
$array['license'],
|
||||
new \DateTimeImmutable($array['time']),
|
||||
$array['homepage'],
|
||||
$array['support']['issues'] ?? '',
|
||||
$array['support']['wiki'] ?? '',
|
||||
$array['require'] ?? [],
|
||||
$array['keywords'] ?? [],
|
||||
$array['type'] ?? null,
|
||||
$array['extra']['install-directory-name'] ?? null,
|
||||
$array['extra']['display-name'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider a version stable if it is in SemVer fomrat "d.d.d".
|
||||
*/
|
||||
public function isStable(): bool
|
||||
{
|
||||
return 1 === preg_match('/^(\d+\.)?(\d+\.)?(\*|\d+)$/', $this->version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider a version pre-release if it is in fomrat "d.d.d-s".
|
||||
*/
|
||||
public function isPreRelease(): bool
|
||||
{
|
||||
return 1 === preg_match('#^(\d+\.)?(\d+\.)?(\d+)(-[a-z0-9]+)?$#i', $this->version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MarketplaceExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\MenuEvent;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Mautic\MarketplaceBundle\Service\RouteProvider;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
final class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::BUILD_MENU => ['onBuildMenu', 9999],
|
||||
];
|
||||
}
|
||||
|
||||
public function onBuildMenu(MenuEvent $event): void
|
||||
{
|
||||
if ('admin' !== $event->getType() || !$this->config->marketplaceIsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->addMenuItems(
|
||||
[
|
||||
'priority' => 81,
|
||||
'items' => [
|
||||
'marketplace.title' => [
|
||||
'id' => 'marketplace',
|
||||
'route' => RouteProvider::ROUTE_LIST,
|
||||
'access' => MarketplacePermissions::CAN_VIEW_PACKAGES,
|
||||
'parent' => 'mautic.core.integrations',
|
||||
'iconClass' => 'ri-shopping-bag-2-line',
|
||||
'priority' => 16,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Exception;
|
||||
|
||||
class ApiException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Exception;
|
||||
|
||||
class RecordNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle;
|
||||
|
||||
use Mautic\PluginBundle\Bundle\PluginBundleBase;
|
||||
|
||||
class MarketplaceBundle extends PluginBundleBase
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Model;
|
||||
|
||||
use Mautic\MarketplaceBundle\Api\Connection;
|
||||
use Mautic\MarketplaceBundle\DTO\PackageDetail;
|
||||
use Mautic\MarketplaceBundle\Service\Allowlist;
|
||||
|
||||
class PackageModel
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private Allowlist $allowlist,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPackageDetail(string $name): PackageDetail
|
||||
{
|
||||
$allowlist = $this->allowlist->getAllowList();
|
||||
$allowedPackage = $allowlist->findPackageByName($name);
|
||||
$payload = $this->connection->getPackage($name);
|
||||
|
||||
return PackageDetail::fromArray($payload['package'] + $allowedPackage->toArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<div class="panel mt-sm">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{% trans %}marketplace.package.latest.stable.version{% endtrans %}</div>
|
||||
</div>
|
||||
<table class="table table-hover mb-0">
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.version{% endtrans %}</th>
|
||||
<td>
|
||||
{% if not latestVersion %}
|
||||
<div class="text-danger">
|
||||
{% trans %}marketplace.latest.version.missing{% endtrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ packageDetail.packageBase.repository|escape }}/releases/tag/{{ latestVersion.version|escape }}" id="latest-version" target="_blank" rel="noopener noreferrer">
|
||||
<strong>{{ latestVersion.version }}</strong>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if latestVersion is not empty %}
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.version.release.date{% endtrans %}</th>
|
||||
<td title="{{ dateToText(latestVersion.time) }}">
|
||||
{{ dateToDate(latestVersion.time) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.license{% endtrans %}</th>
|
||||
<td>{{ latestVersion.license|join(', ')|escape }}</td>
|
||||
</tr>
|
||||
{% if latestVersion.homepage %}
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.homepage{% endtrans %}</th>
|
||||
<td>{{ latestVersion.homepage|escape }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>
|
||||
{% trans %}marketplace.package.required.packages{% endtrans %}
|
||||
({{ latestVersion.require|length }})
|
||||
</th>
|
||||
<td>{{ latestVersion.require|keys|join(', ')|escape }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{% trans %}marketplace.package.all.versions{% endtrans %}</div>
|
||||
</div>
|
||||
<table class="table table-hover mb-0">
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.version{% endtrans %}</th>
|
||||
<th>{% trans %}marketplace.package.version.release.date{% endtrans %}</th>
|
||||
</tr>
|
||||
{% for version in packageDetail.versions.sortByLatest() %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if version.isStable() or version.isPreRelease() %}
|
||||
<a href="{{ packageDetail.packageBase.repository|escape }}/releases/tag/{{ version.version|escape }}" target="_blank" rel="noopener noreferrer" >
|
||||
{% if version.isStable() %}
|
||||
<b>{{ version.version|escape }}</b>
|
||||
{% else %}
|
||||
{{ version.version|escape }}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<i>{{ version.version|escape }}</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td title="{{ dateToText(version.time) }}">
|
||||
{{ dateToFullConcat(version.time) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!-- GitHub -->
|
||||
<table class="table table-hover mb-0">
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.repository{% endtrans %}</th>
|
||||
<td>
|
||||
<a href="{{ packageDetail.packageBase.repository|escape }}" target="_blank" rel="noopener noreferrer" >
|
||||
{{ packageDetail.packageBase.name|escape }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.github.stars{% endtrans %}</th>
|
||||
<td>{{ packageDetail.githubInfo.stars|escape }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.github.watchers{% endtrans %}</th>
|
||||
<td>{{ packageDetail.githubInfo.watchers|escape }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.github.forks{% endtrans %}</th>
|
||||
<td>{{ packageDetail.githubInfo.forks|escape }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.github.open.issues{% endtrans %}</th>
|
||||
<td>{{ packageDetail.githubInfo.openIssues|escape }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="panel panel-default mt-sm">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{% trans %}marketplace.package.cli.install{% endtrans %}</div>
|
||||
</div>
|
||||
<div class="panel-body">{{ 'marketplace.package.cli.install.descr'|trans({
|
||||
'%vendor%' : packageDetail.packageBase.getVendorName(),
|
||||
'%package%' : packageDetail.packageBase.getPackageName(),
|
||||
})|purify }}</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- Packagist -->
|
||||
<table class="table table-hover mb-0">
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.repository{% endtrans %}</th>
|
||||
<td>
|
||||
<a href="{{ packageDetail.packageBase.url|escape }}" target="_blank" rel="noopener noreferrer" >
|
||||
{{ packageDetail.packageBase.name|escape }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.total.downloads{% endtrans %}</th>
|
||||
<td>{{ packageDetail.packageBase.downloads|escape }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.monthly.downloads{% endtrans %}</th>
|
||||
<td>{{ packageDetail.monthlyDownloads|escape }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.daily.downloads{% endtrans %}</th>
|
||||
<td>{{ packageDetail.dailyDownloads|escape }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}marketplace.package.create.date{% endtrans %}</th>
|
||||
<td title="{{ dateToText(packageDetail.time) }}">
|
||||
{{ dateToDate(packageDetail.time) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,243 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% block headerTitle %}{% endblock %}
|
||||
|
||||
{% set latestVersion = packageDetail.versions.findLatestStableVersionPackage() %}
|
||||
|
||||
{% if not latestVersion %}
|
||||
{% set latestVersion = packageDetail.versions.findLatestVersionPackage() %}
|
||||
{% endif %}
|
||||
|
||||
{% block preHeader %}
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: 'mautic.core.close_back'|trans({'%target%': 'mautic.marketplace.marketplace'|trans}),
|
||||
size: 'xs',
|
||||
variant: 'tertiary',
|
||||
icon: 'ri-arrow-left-line',
|
||||
href: path(constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_LIST')),
|
||||
attributes: {
|
||||
'data-toggle': 'ajax',
|
||||
'class': 'btn-back mb-lg'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{% set buttons = {} %}
|
||||
|
||||
{% if latestVersion and latestVersion.issues %}
|
||||
{% set buttons = buttons|merge({0: {
|
||||
'attr' : {
|
||||
'href' : latestVersion.issues,
|
||||
'target' : '_blank',
|
||||
'rel' : 'noopener noreferrer',
|
||||
'data-toggle' : '',
|
||||
},
|
||||
'btnText' : 'marketplace.package.issue.tracker'|trans,
|
||||
'iconClass' : 'ri-question-mark',
|
||||
'primary' : false,
|
||||
}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if latestVersion and latestVersion.wiki %}
|
||||
{% set buttons = buttons|merge({0:{
|
||||
'attr' : {
|
||||
'href' : latestVersion.wiki,
|
||||
'target' : '_blank',
|
||||
'rel' : 'noopener noreferrer',
|
||||
'data-toggle' : '',
|
||||
},
|
||||
'btnText' : 'marketplace.package.wiki'|trans,
|
||||
'iconClass' : 'ri-book-line',
|
||||
'primary' : false,
|
||||
}}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if security.isGranted(constant('Mautic\\MarketplaceBundle\\Security\\Permissions\\MarketplacePermissions::CAN_INSTALL_PACKAGES')) and not isInstalled and isComposerEnabled %}
|
||||
{% set installRoute = path(constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_INSTALL'),
|
||||
{
|
||||
'vendor' : packageDetail.packageBase.getVendorName(),
|
||||
'package' : packageDetail.packageBase.getPackageName(),
|
||||
}
|
||||
) %}
|
||||
|
||||
{% set buttons = buttons|merge({0:{
|
||||
'attr' : {
|
||||
'data-toggle' : 'ajaxmodal',
|
||||
'data-target' : '#InstallationInProgressModal',
|
||||
'href' : installRoute,
|
||||
},
|
||||
'btnText' : 'marketplace.package.install'|trans,
|
||||
'iconClass' : 'ri-download-line',
|
||||
'primary' : true,
|
||||
}}) %}
|
||||
|
||||
{% elseif security.isGranted(constant('Mautic\\MarketplaceBundle\\Security\\Permissions\\MarketplacePermissions::CAN_INSTALL_PACKAGES')) and isComposerEnabled %}
|
||||
{% set removeRoute = path(constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_REMOVE'),
|
||||
{'vendor' : packageDetail.packageBase.getVendorName(),
|
||||
'package' : packageDetail.packageBase.getPackageName(),
|
||||
}) %}
|
||||
|
||||
{% set buttons = buttons|merge({ 0: {
|
||||
'attr' : {
|
||||
'data-toggle' : 'ajaxmodal',
|
||||
'data-target' :'#RemovalInProgressModal',
|
||||
'href' : removeRoute,
|
||||
},
|
||||
'btnText' : 'marketplace.package.remove'|trans,
|
||||
'iconClass' : 'ri-delete-bin-line',
|
||||
'primary' : true,
|
||||
}}) %}
|
||||
{% endif %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'customButtons' : buttons
|
||||
}) -}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-9">
|
||||
<div class="marketplace-header {{ packageDetail.packageBase.type|default('')|purify }} bg-picture col-xs-12 jc-center pt-lg pb-lg">
|
||||
|
||||
<h1 class="fw-b fs-46">{{ packageDetail.packageBase.getHumanPackageName()|escape }}</h1>
|
||||
{% if packageDetail.packageBase.description %}
|
||||
<div class="text-muted mt-sm">{{ packageDetail.packageBase.description|purify }}</div>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<div class="d-flex gap-3 mt-sm">
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with {
|
||||
tags: [
|
||||
{
|
||||
type: 'read-only',
|
||||
color: 'warning',
|
||||
label: packageDetail.githubInfo.stars|escape,
|
||||
icon: 'ri-star-s-fill ri-lg',
|
||||
attributes: {
|
||||
'title': 'marketplace.package.github.stars'|trans,
|
||||
'data-toggle': 'tooltip',
|
||||
'size': 'md'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'read-only',
|
||||
color: 'warm-gray',
|
||||
label: packageDetail.packageBase.downloads|escape,
|
||||
icon: 'ri-download-line',
|
||||
attributes: {
|
||||
'title': 'marketplace.package.total.downloads'|trans,
|
||||
'data-toggle': 'tooltip',
|
||||
'size': 'md'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'read-only',
|
||||
color: 'warm-gray',
|
||||
label: packageDetail.packageBase.type == 'theme'
|
||||
? 'marketplace.package.type.theme'|trans
|
||||
: 'marketplace.package.type.plugin'|trans,
|
||||
icon: packageDetail.packageBase.type == 'theme'
|
||||
? 'ri-paint-brush-line marketplace-icon'
|
||||
: 'ri-plug-line',
|
||||
attributes: {
|
||||
'title': 'marketplace.package.type'|trans,
|
||||
'data-toggle': 'tooltip',
|
||||
'size': 'md'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'read-only',
|
||||
color: 'warm-gray',
|
||||
label: latestVersion is not empty
|
||||
? ('marketplace.package.last_updated'|trans ~ ' ' ~ dateToHumanized(latestVersion.time))
|
||||
: '',
|
||||
attributes: {
|
||||
'size': 'md'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{% include '@MauticCore/Helper/nav_tabs.html.twig' with {
|
||||
'tabs': [
|
||||
{
|
||||
'title': 'mautic.core.overview',
|
||||
'content': include('@bundles/MarketplaceBundle/Resources/views/Package/Details/details--tab_overview.html.twig')
|
||||
},
|
||||
{
|
||||
'title': 'mautic.core.details',
|
||||
'content': include('@bundles/MarketplaceBundle/Resources/views/Package/Details/details--tab_details.html.twig')
|
||||
}
|
||||
],
|
||||
'style': 'line'
|
||||
} %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 pb-lg">
|
||||
<!-- Maintainers -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans %}marketplace.package.maintainers{% endtrans %}</h3>
|
||||
</div>
|
||||
|
||||
{% for maintainer in packageDetail.maintainers %}
|
||||
<div class="box-layout">
|
||||
<div class="col-xs-3 va-m">
|
||||
<div class="panel-body">
|
||||
<span class="img-wrapper img-rounded">
|
||||
<img class="img" src="{{ maintainer.avatar|escape }}">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-9 va-t">
|
||||
<div class="panel-body">
|
||||
<h4 class="fw-sb mb-xs ellipsis">
|
||||
{{ maintainer.name|title|escape }}
|
||||
</h4>
|
||||
<a href="https://packagist.org/packages/{{ maintainer.name|escape }}" target="_blank" rel="noopener noreferrer">
|
||||
{{ 'marketplace.other.packages'|trans({'%name%' : maintainer.name}) }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{% include '@MauticCore/Helper/nav_tabs.html.twig' with {
|
||||
'tabs': [
|
||||
{
|
||||
'title': 'marketplace.package.github.info',
|
||||
'content': include('@bundles/MarketplaceBundle/Resources/views/Package/Details/details--tab_github.html.twig')
|
||||
},
|
||||
{
|
||||
'title': 'marketplace.package.packagist.info',
|
||||
'content': include('@bundles/MarketplaceBundle/Resources/views/Package/Details/details--tab_packagist.html.twig')
|
||||
}
|
||||
],
|
||||
'style': 'contained'
|
||||
} %}
|
||||
|
||||
</div>
|
||||
|
||||
{{- include('@MauticCore/Helper/modal.html.twig', {
|
||||
'id' : 'InstallationInProgressModal',
|
||||
'header' : 'Installing ' ~ packageDetail.packageBase.getHumanPackageName()|escape,
|
||||
'size' : 'md',
|
||||
'footerButtons' : false,
|
||||
}) -}}
|
||||
|
||||
{{- include('@MauticCore/Helper/modal.html.twig', {
|
||||
'id' : 'RemovalInProgressModal',
|
||||
'header' : 'Removing ' ~ packageDetail.packageBase.getHumanPackageName()|escape,
|
||||
'size' : 'md',
|
||||
'footerButtons' : false,
|
||||
}) -}}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="text-center" id="marketplace-installation-in-progress">
|
||||
<p>{{ 'marketplace.package.install.html.in.progress'|trans({'%packagename%' : packageDetail.packageBase.getHumanPackageName()})|purify }}</p>
|
||||
<div class="spinner">
|
||||
<i class="ri-loader-3-line ri-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: none" class="text-center" id="marketplace-installation-failed">
|
||||
<p>{{ 'marketplace.package.install.html.failed'|trans({'%packagename%' : packageDetail.packageBase.getHumanPackageName()})|purify }}</p>
|
||||
<textarea class="form-control" readonly id="marketplace-installation-failed-details"></textarea>
|
||||
</div>
|
||||
<div style="display: none" class="text-center" id="marketplace-installation-success">
|
||||
<p>{{ 'marketplace.package.install.html.success'|trans({'%packagename%' : packageDetail.packageBase.getHumanPackageName()})|purify }}</p>
|
||||
<p><a class="btn btn-primary" href="{{ path('mautic_plugin_reload') }}">{% trans %}marketplace.package.install.html.success.continue{% endtrans %}</a></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const installPackageResetView = () => {
|
||||
mQuery('#marketplace-installation-in-progress').show();
|
||||
mQuery('#marketplace-installation-success').hide();
|
||||
mQuery('#marketplace-installation-failed').hide();
|
||||
}
|
||||
|
||||
installPackageResetView();
|
||||
|
||||
Mautic.Marketplace.installPackage(
|
||||
'{{ packageDetail.packageBase.getVendorName()|escape }}',
|
||||
'{{ packageDetail.packageBase.getPackageName()|escape }}',
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
mQuery('#marketplace-installation-in-progress').hide();
|
||||
mQuery('#marketplace-installation-success').show();
|
||||
} else if (response.redirect) {
|
||||
window.location = response.redirect;
|
||||
}
|
||||
},
|
||||
(request, textStatus, errorThrown) => {
|
||||
let error;
|
||||
|
||||
try {
|
||||
const res = JSON.parse(request.responseText);
|
||||
if (res.error) {
|
||||
error = res.error;
|
||||
} else {
|
||||
error = res.errors[0].message ?? 'Unknown error';
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'An unknown error occurred. Please check the logs for more details.';
|
||||
console.error(request.responseText);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
mQuery('#marketplace-installation-in-progress').hide();
|
||||
mQuery('#marketplace-installation-failed').show();
|
||||
mQuery('#marketplace-installation-failed-details').text(error);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,146 @@
|
||||
{% set isIndex = tmpl == 'index' ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
{% block mauticContent %}Package{% endblock %}
|
||||
{% block headerTitle %}{{ 'marketplace.title'|trans|purify }}{% endblock %}
|
||||
{% block actions %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'customButtons' : {
|
||||
0: {
|
||||
'attr' : {
|
||||
'class' : 'btn btn-primary btn-nospin',
|
||||
'data-toggle' : 'ajax',
|
||||
'href' : path(constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_CLEAR_CACHE')),
|
||||
},
|
||||
'iconClass' : 'ri-refresh-line',
|
||||
'btnText' : 'marketplace.clear.cache',
|
||||
'tooltip' : 'marketplace.clear.cache.tooltip',
|
||||
},
|
||||
},
|
||||
}) -}}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
{% if isComposerEnabled %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{% trans %}marketplace.beta.warning{% endtrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
{% trans %}marketplace.composer.required{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="page-list-wrapper" class="panel panel-default">
|
||||
{{- include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue' : searchValue,
|
||||
'action' : currentRoute,
|
||||
}) -}}
|
||||
<div class="page-list">
|
||||
{{ block('listResults') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ block('listResults') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block listResults %}
|
||||
{% if items|length %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="marketplace-packages-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'checkall' : 'true',
|
||||
'target' : '#marketplace-packages-table',
|
||||
'langVar' : 'marketplace.package',
|
||||
'routeBase' : 'marketplace',
|
||||
}
|
||||
) -}}
|
||||
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'text' : 'mautic.core.name',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'text' : 'marketplace.vendor',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'text' : 'marketplace.downloads',
|
||||
}
|
||||
) -}}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'text' : 'marketplace.favers',
|
||||
}
|
||||
) -}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{{- include(
|
||||
'@MauticCore/Helper/list_actions.html.twig',
|
||||
{
|
||||
'item' : item,
|
||||
'customButtons' : {},
|
||||
}
|
||||
) -}}
|
||||
</td>
|
||||
<td class="package-name">
|
||||
<div>
|
||||
<a data-toggle="ajax" href="{{ path(
|
||||
constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_DETAIL'),
|
||||
{
|
||||
'vendor' : item.getVendorName()|escape,
|
||||
'package' : item.getPackageName()|escape,
|
||||
}
|
||||
) }}">
|
||||
{{ item.getHumanPackageName()|escape }}
|
||||
</a>
|
||||
</div>
|
||||
{{ include('@MauticCore/Helper/description--inline.html.twig', {
|
||||
'description': item.description
|
||||
}) }}
|
||||
</td>
|
||||
<td class="vendor-name">{{ item.getVendorName()|escape }}</td>
|
||||
<td class="downloads">{{ item.downloads|escape }}</td>
|
||||
<td class="favers">{{ item.favers|escape }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
|
||||
{{- include(
|
||||
'@MauticCore/Helper/pagination.html.twig',
|
||||
{
|
||||
'totalItems' : count,
|
||||
'page' : page,
|
||||
'limit' : limit,
|
||||
'baseUrl' : path(constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_LIST')),
|
||||
'sessionVar' : 'marketplace.package',
|
||||
'routeBase' : constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_LIST'),
|
||||
}
|
||||
) -}}
|
||||
</div>
|
||||
{% else %}
|
||||
{{- include('@MauticCore/Helper/noresults.html.twig', {'message' : 'marketplace.noresults.tip'}) -}}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,58 @@
|
||||
<div class="text-center" id="marketplace-removal-in-progress">
|
||||
<p>{{ 'marketplace.package.remove.html.in.progress'|trans({'%packagename%' : packageDetail.packageBase.getHumanPackageName()})|purify }}</p>
|
||||
<div class="spinner">
|
||||
<i class="ri-loader-3-line ri-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: none" class="text-center" id="marketplace-removal-failed">
|
||||
<p>{{ 'marketplace.package.remove.html.failed'|trans({'%packagename%' : packageDetail.packageBase.getHumanPackageName()})|purify }}</p>
|
||||
<textarea class="form-control" readonly id="marketplace-removal-failed-details"></textarea>
|
||||
</div>
|
||||
<div style="display: none" class="text-center" id="marketplace-removal-success">
|
||||
<p>{{ 'marketplace.package.remove.html.success'|trans({'%packagename%' : packageDetail.packageBase.getHumanPackageName()})|purify }}</p>
|
||||
<p><a class="btn btn-primary" href="{{ path(constant('Mautic\\MarketplaceBundle\\Service\\RouteProvider::ROUTE_LIST')) }}">
|
||||
{% trans %}marketplace.package.remove.html.success.continue{% endtrans %}</a></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const removePackageResetView = () => {
|
||||
mQuery('#marketplace-removal-in-progress').show();
|
||||
mQuery('#marketplace-removal-success').hide();
|
||||
mQuery('#marketplace-removal-failed').hide();
|
||||
}
|
||||
|
||||
removePackageResetView();
|
||||
|
||||
Mautic.Marketplace.removePackage(
|
||||
'{{ packageDetail.packageBase.getVendorName()|escape }}',
|
||||
'{{ packageDetail.packageBase.getPackageName()|escape }}',
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
mQuery('#marketplace-removal-in-progress').hide();
|
||||
mQuery('#marketplace-removal-success').show();
|
||||
} else if (response.redirect) {
|
||||
window.location = response.redirect;
|
||||
}
|
||||
},
|
||||
(request, textStatus, errorThrown) => {
|
||||
let error;
|
||||
|
||||
try {
|
||||
const res = JSON.parse(request.responseText);
|
||||
if (res.error) {
|
||||
error = res.error;
|
||||
} else {
|
||||
error = res.errors[0].message ?? 'Unknown error';
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'An unknown error occurred. Please check the logs for more details.';
|
||||
console.error(request.responseText);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
mQuery('#marketplace-removal-in-progress').hide();
|
||||
mQuery('#marketplace-removal-failed').show();
|
||||
mQuery('#marketplace-removal-failed-details').text(error);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class MarketplacePermissions extends AbstractPermissions
|
||||
{
|
||||
public const BASE = 'marketplace';
|
||||
|
||||
public const PACKAGES = 'packages';
|
||||
|
||||
public const CAN_VIEW_PACKAGES = self::BASE.':'.self::PACKAGES.':view';
|
||||
|
||||
public const CAN_INSTALL_PACKAGES = self::BASE.':'.self::PACKAGES.':create';
|
||||
|
||||
public const CAN_REMOVE_PACKAGES = self::BASE.':'.self::PACKAGES.':remove';
|
||||
|
||||
public function __construct(
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
private Config $config,
|
||||
) {
|
||||
parent::__construct($coreParametersHelper->all());
|
||||
}
|
||||
|
||||
public function definePermissions(): void
|
||||
{
|
||||
$this->addStandardPermissions(self::PACKAGES, false);
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->config->marketplaceIsEnabled();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::BASE;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields(self::BASE, self::PACKAGES, $builder, $data, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Service;
|
||||
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Mautic\CacheBundle\Cache\CacheProvider;
|
||||
use Mautic\MarketplaceBundle\DTO\Allowlist as DTOAllowlist;
|
||||
use Mautic\MarketplaceBundle\Exception\ApiException;
|
||||
|
||||
/**
|
||||
* Provides several helper functions to interact with Mautic's allowlist.
|
||||
*/
|
||||
class Allowlist
|
||||
{
|
||||
private const MARKETPLACE_ALLOWLIST_CACHE_KEY = 'marketplace_allowlist';
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private CacheProvider $cache,
|
||||
private ClientInterface $httpClient,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAllowList(): ?DTOAllowlist
|
||||
{
|
||||
$cache = $this->cache->getSimpleCache();
|
||||
$cachedAllowlistString = $cache->get(self::MARKETPLACE_ALLOWLIST_CACHE_KEY);
|
||||
|
||||
if (!empty($cachedAllowlistString)) {
|
||||
return $this->parseAllowlistJson($cachedAllowlistString);
|
||||
}
|
||||
|
||||
if (!empty($this->config->getAllowListUrl())) {
|
||||
$response = $this->httpClient->request('GET', $this->config->getAllowlistUrl());
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
if ($response->getStatusCode() >= 300) {
|
||||
throw new ApiException($body, $response->getStatusCode());
|
||||
}
|
||||
|
||||
// Cache the allowlist for the given amount of seconds (3600 by default).
|
||||
$cache->set(
|
||||
self::MARKETPLACE_ALLOWLIST_CACHE_KEY,
|
||||
$body,
|
||||
$this->config->getAllowlistCacheTtlSeconds()
|
||||
);
|
||||
|
||||
return $this->parseAllowlistJson($body);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cache->getSimpleCache()->delete(self::MARKETPLACE_ALLOWLIST_CACHE_KEY);
|
||||
}
|
||||
|
||||
private function parseAllowlistJson(string $payload): DTOAllowlist
|
||||
{
|
||||
return DTOAllowlist::fromArray(json_decode($payload, true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Service;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
class Config
|
||||
{
|
||||
public const MARKETPLACE_ENABLED = 'marketplace_enabled';
|
||||
|
||||
public const MARKETPLACE_ALLOWLIST_URL = 'marketplace_allowlist_url';
|
||||
|
||||
public const MARKETPLACE_ALLOWLIST_CACHE_TTL_SECONDS = 'marketplace_allowlist_cache_ttl_seconds';
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function marketplaceIsEnabled(): bool
|
||||
{
|
||||
return (bool) $this->coreParametersHelper->get(self::MARKETPLACE_ENABLED);
|
||||
}
|
||||
|
||||
public function getAllowlistUrl(): string
|
||||
{
|
||||
return $this->coreParametersHelper->get(self::MARKETPLACE_ALLOWLIST_URL);
|
||||
}
|
||||
|
||||
public function getAllowlistCacheTtlSeconds(): int
|
||||
{
|
||||
return (int) $this->coreParametersHelper->get(self::MARKETPLACE_ALLOWLIST_CACHE_TTL_SECONDS, 3600);
|
||||
}
|
||||
|
||||
public function isComposerEnabled(): bool
|
||||
{
|
||||
return $this->coreParametersHelper->get('composer_updates', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Service;
|
||||
|
||||
use Mautic\CoreBundle\Release\ThisRelease;
|
||||
use Mautic\MarketplaceBundle\Api\Connection;
|
||||
use Mautic\MarketplaceBundle\Collection\PackageCollection;
|
||||
use Mautic\MarketplaceBundle\DTO\AllowlistEntry;
|
||||
|
||||
class PluginCollector
|
||||
{
|
||||
/**
|
||||
* @var AllowlistEntry[]
|
||||
*/
|
||||
private array $allowlistedPackages = [];
|
||||
|
||||
private int $total = 0;
|
||||
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private Allowlist $allowlist,
|
||||
) {
|
||||
}
|
||||
|
||||
public function collectPackages(int $page, int $limit, string $query = ''): PackageCollection
|
||||
{
|
||||
$allowlist = $this->allowlist->getAllowList();
|
||||
|
||||
if (!empty($allowlist)) {
|
||||
$this->allowlistedPackages = $this->filterAllowlistedPackagesForCurrentMauticVersion($allowlist->entries);
|
||||
$payload = $this->getAllowlistedPackages($page, $limit);
|
||||
} else {
|
||||
$payload = $this->connection->getPlugins($page, $limit, $query);
|
||||
}
|
||||
|
||||
$this->total = (int) $payload['total'];
|
||||
|
||||
return PackageCollection::fromArray($payload['results']);
|
||||
}
|
||||
|
||||
public function getTotal(): int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AllowlistEntry[] $entries
|
||||
*
|
||||
* @return AllowlistEntry[]
|
||||
*/
|
||||
private function filterAllowlistedPackagesForCurrentMauticVersion(array $entries): array
|
||||
{
|
||||
$mauticVersion = ThisRelease::getMetadata()->getVersion();
|
||||
|
||||
return array_filter($entries, function (AllowlistEntry $entry) use ($mauticVersion): bool {
|
||||
if (
|
||||
!empty($entry->minimumMauticVersion)
|
||||
&& !version_compare($mauticVersion, $entry->minimumMauticVersion, '>=')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!empty($entry->maximumMauticVersion)
|
||||
&& !version_compare($mauticVersion, $entry->maximumMauticVersion, '<=')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* During the Marketplace beta period, we only want to show packages that are explicitly
|
||||
* allowlisted. This function only gets allowlisted packages from Packagist. Their API doesn't
|
||||
* support querying multiple packages at once, so we simply do a foreach loop.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function getAllowlistedPackages(int $page, int $limit): array
|
||||
{
|
||||
$total = count($this->allowlistedPackages);
|
||||
$results = [];
|
||||
|
||||
if (0 === $total) {
|
||||
return [
|
||||
'total' => 0,
|
||||
'results' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/** @var array<int, AllowlistEntry[]> $chunks */
|
||||
$chunks = array_chunk($this->allowlistedPackages, $limit);
|
||||
// Array keys start at 0 but page numbers start at 1
|
||||
$pageChunk = $page - 1;
|
||||
|
||||
foreach ($chunks[$pageChunk] as $entry) {
|
||||
if (count($results) >= $limit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload = $this->connection->getPlugins(1, 1, $entry->package);
|
||||
|
||||
if (isset($payload['results'][0])) {
|
||||
$results[] = $payload['results'][0] + $entry->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Service;
|
||||
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class RouteProvider
|
||||
{
|
||||
public const ROUTE_LIST = 'mautic_marketplace_list';
|
||||
|
||||
public const ROUTE_DETAIL = 'mautic_marketplace_detail';
|
||||
|
||||
public const ROUTE_INSTALL = 'mautic_marketplace_install';
|
||||
|
||||
public const ROUTE_REMOVE = 'mautic_marketplace_remove';
|
||||
|
||||
public const ROUTE_CLEAR_CACHE = 'mautic_marketplace_clear_cache';
|
||||
|
||||
public function __construct(
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildListRoute(int $page = 1): string
|
||||
{
|
||||
return $this->router->generate(static::ROUTE_LIST, ['page' => $page]);
|
||||
}
|
||||
|
||||
public function buildDetailRoute(string $vendor, string $package): string
|
||||
{
|
||||
return $this->router->generate(
|
||||
static::ROUTE_DETAIL,
|
||||
['vendor' => $vendor, 'package' => $package]
|
||||
);
|
||||
}
|
||||
|
||||
public function buildInstallRoute(string $vendor, string $package): string
|
||||
{
|
||||
return $this->router->generate(
|
||||
static::ROUTE_DETAIL,
|
||||
['vendor' => $vendor, 'package' => $package]
|
||||
);
|
||||
}
|
||||
|
||||
public function buildRemoveRoute(string $vendor, string $package): string
|
||||
{
|
||||
return $this->router->generate(
|
||||
static::ROUTE_REMOVE,
|
||||
['vendor' => $vendor, 'package' => $package]
|
||||
);
|
||||
}
|
||||
|
||||
public function buildClearCacheRoute(): string
|
||||
{
|
||||
return $this->router->generate(
|
||||
static::ROUTE_CLEAR_CACHE
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"allowlist": [
|
||||
{
|
||||
"package": "koco\/mautic-recaptcha-bundle",
|
||||
"display_name": "KocoCaptcha",
|
||||
"minimum_mautic_version": "4.1.0",
|
||||
"maximum_mautic_version": null
|
||||
},
|
||||
{
|
||||
"package": "maatoo\/mautic-referrals-bundle",
|
||||
"minimum_mautic_version": "4.1.0",
|
||||
"maximum_mautic_version": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
{
|
||||
"package": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"time": "2018-01-10T09:30:31+00:00",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "koco",
|
||||
"avatar_url": "https:\/\/www.gravatar.com\/avatar\/73ad638ac9373ee1580bf6fb59e7538f?d=identicon"
|
||||
}
|
||||
],
|
||||
"versions": {
|
||||
"dev-master": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "dev-master",
|
||||
"version_normalized": "dev-master",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "4855321ffa22e8c8435cd4a5ec45afec9857df84"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/4855321ffa22e8c8435cd4a5ec45afec9857df84",
|
||||
"reference": "4855321ffa22e8c8435cd4a5ec45afec9857df84",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/3.0.1",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2020-07-22T10:07:15+00:00",
|
||||
"default-branch": true,
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"dev-feature\/circleci-store_test_results": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "dev-feature\/circleci-store_test_results",
|
||||
"version_normalized": "dev-feature\/circleci-store_test_results",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "0cdbec1b2e7427dff64d57e80789f79128105dea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/0cdbec1b2e7427dff64d57e80789f79128105dea",
|
||||
"reference": "0cdbec1b2e7427dff64d57e80789f79128105dea",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/feature\/circleci-store_test_results",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-10-28T18:31:03+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"dev-feature\/refactor": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "dev-feature\/refactor",
|
||||
"version_normalized": "dev-feature\/refactor",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "702b25df5958f04cfdf1ccbfe34733082d379872"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/702b25df5958f04cfdf1ccbfe34733082d379872",
|
||||
"reference": "702b25df5958f04cfdf1ccbfe34733082d379872",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/feature\/refactor",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-09-14T13:32:44+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"dev-feature\/only-delete-new-leads": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "dev-feature\/only-delete-new-leads",
|
||||
"version_normalized": "dev-feature\/only-delete-new-leads",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "a37a878ba7b54226d1c162d7ca78258b408151fe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/a37a878ba7b54226d1c162d7ca78258b408151fe",
|
||||
"reference": "a37a878ba7b54226d1c162d7ca78258b408151fe",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/feature\/only-delete-new-leads",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-05-30T09:28:07+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"dev-feature\/delete-contact-on-invalid-form": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "dev-feature\/delete-contact-on-invalid-form",
|
||||
"version_normalized": "dev-feature\/delete-contact-on-invalid-form",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "1d97afa904cc69da2a40066a7ffe67d2cea0aa0f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/1d97afa904cc69da2a40066a7ffe67d2cea0aa0f",
|
||||
"reference": "1d97afa904cc69da2a40066a7ffe67d2cea0aa0f",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/feature\/delete-contact-on-invalid-form",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-05-25T14:09:40+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"3.0.1": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "3.0.1",
|
||||
"version_normalized": "3.0.1.0",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "4855321ffa22e8c8435cd4a5ec45afec9857df84"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/4855321ffa22e8c8435cd4a5ec45afec9857df84",
|
||||
"reference": "4855321ffa22e8c8435cd4a5ec45afec9857df84",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/3.0.1",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2020-07-22T10:07:15+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"3.0.0": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "3.0.0",
|
||||
"version_normalized": "3.0.0.0",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "971e5d29a6dfae8831b1ad478ebd80b2740e5f1d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/971e5d29a6dfae8831b1ad478ebd80b2740e5f1d",
|
||||
"reference": "971e5d29a6dfae8831b1ad478ebd80b2740e5f1d",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/master",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2020-04-20T14:45:33+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"1.1.3": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "1.1.3",
|
||||
"version_normalized": "1.1.3.0",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "6736156e10ad53ddf64c300b618fe83b2790c944"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/6736156e10ad53ddf64c300b618fe83b2790c944",
|
||||
"reference": "6736156e10ad53ddf64c300b618fe83b2790c944",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/master",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-10-02T15:45:41+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"1.1.2": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "1.1.2",
|
||||
"version_normalized": "1.1.2.0",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "ac6855b6aebb22f6a80aff2589088449c6a1feca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/ac6855b6aebb22f6a80aff2589088449c6a1feca",
|
||||
"reference": "ac6855b6aebb22f6a80aff2589088449c6a1feca",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/master",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-09-14T13:35:51+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"1.1.1": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "1.1.1",
|
||||
"version_normalized": "1.1.1.0",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "5d25d058fc246ff1ca9e196846443d113523af4a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/5d25d058fc246ff1ca9e196846443d113523af4a",
|
||||
"reference": "5d25d058fc246ff1ca9e196846443d113523af4a",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/master",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-09-14T12:03:24+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"1.1.0": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "1.1.0",
|
||||
"version_normalized": "1.1.0.0",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "2866cd4868ea5f44823b9fc341b68f0c7ae7d8c1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/2866cd4868ea5f44823b9fc341b68f0c7ae7d8c1",
|
||||
"reference": "2866cd4868ea5f44823b9fc341b68f0c7ae7d8c1",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/master",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-05-30T09:37:18+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"1.0.1": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "1.0.1",
|
||||
"version_normalized": "1.0.1.0",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "a29f1be60bf8c49a0b226712b700da29ab57a757"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/a29f1be60bf8c49a0b226712b700da29ab57a757",
|
||||
"reference": "a29f1be60bf8c49a0b226712b700da29ab57a757",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/master",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-02-19T13:28:13+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
},
|
||||
"1.0.0": {
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"keywords": [],
|
||||
"homepage": "",
|
||||
"version": "1.0.0",
|
||||
"version_normalized": "1.0.0.0",
|
||||
"license": [
|
||||
"GNU General Public License v3.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Konstantin Scheumann",
|
||||
"email": "info@konstantin.codes"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha.git",
|
||||
"reference": "950854e08227187423e2ee99b028c9d92b4c1ba0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https:\/\/api.github.com\/repos\/KonstantinCodes\/mautic-recaptcha\/zipball\/950854e08227187423e2ee99b028c9d92b4c1ba0",
|
||||
"reference": "950854e08227187423e2ee99b028c9d92b4c1ba0",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"support": {
|
||||
"source": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/tree\/master",
|
||||
"issues": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha\/issues"
|
||||
},
|
||||
"time": "2018-01-10T10:53:07+00:00",
|
||||
"require": {
|
||||
"mautic\/composer-plugin": "^1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "mautic-plugin",
|
||||
"repository": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha",
|
||||
"github_stars": 23,
|
||||
"github_watchers": 9,
|
||||
"github_forks": 17,
|
||||
"github_open_issues": 9,
|
||||
"language": "PHP",
|
||||
"dependents": 0,
|
||||
"suggesters": 0,
|
||||
"downloads": {
|
||||
"total": 2662,
|
||||
"monthly": 430,
|
||||
"daily": 6
|
||||
},
|
||||
"favers": 24
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "mautic\/mautic-saelos-bundle",
|
||||
"description": "",
|
||||
"url": "https:\/\/packagist.org\/packages\/mautic\/mautic-saelos-bundle",
|
||||
"repository": "https:\/\/github.com\/mautic\/mautic-saelos-bundle",
|
||||
"downloads": 10586,
|
||||
"favers": 11
|
||||
},
|
||||
{
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"url": "https:\/\/packagist.org\/packages\/koco\/mautic-recaptcha-bundle",
|
||||
"repository": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha",
|
||||
"downloads": 2012,
|
||||
"favers": 20
|
||||
},
|
||||
{
|
||||
"name": "monogramm\/mautic-ldap-auth-bundle",
|
||||
"description": "This plugin enables LDAP authentication for mautic.",
|
||||
"url": "https:\/\/packagist.org\/packages\/monogramm\/mautic-ldap-auth-bundle",
|
||||
"repository": "https:\/\/github.com\/Monogramm\/MauticLdapAuthBundle",
|
||||
"downloads": 307,
|
||||
"favers": 8
|
||||
},
|
||||
{
|
||||
"name": "maatoo\/mautic-referrals-bundle",
|
||||
"description": "This plugin enables referrals in mautic.",
|
||||
"url": "https:\/\/packagist.org\/packages\/maatoo\/mautic-referrals-bundle",
|
||||
"repository": "https:\/\/github.com\/maatoo-io\/MauticReferralsBundle",
|
||||
"downloads": 527,
|
||||
"favers": 5
|
||||
},
|
||||
{
|
||||
"name": "thedmsgroup\/mautic-do-not-contact-extras-bundle",
|
||||
"description": "Adds custom DNC list items to be added to standard Mautic DNC lists and creates phpne and sms channels",
|
||||
"url": "https:\/\/packagist.org\/packages\/thedmsgroup\/mautic-do-not-contact-extras-bundle",
|
||||
"repository": "https:\/\/github.com\/TheDMSGroup\/mautic-dnc-extras",
|
||||
"downloads": 532,
|
||||
"favers": 9
|
||||
}
|
||||
],
|
||||
"total": 58,
|
||||
"next": "https:\/\/packagist.org\/search.json?page=2\u0026type=mautic-plugin"
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Tests\Functional\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ComposerHelper;
|
||||
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
|
||||
use Mautic\MarketplaceBundle\Command\InstallCommand;
|
||||
use Mautic\MarketplaceBundle\DTO\ConsoleOutput;
|
||||
use Mautic\MarketplaceBundle\DTO\PackageDetail;
|
||||
use Mautic\MarketplaceBundle\Exception\ApiException;
|
||||
use Mautic\MarketplaceBundle\Model\PackageModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
final class InstallCommandTest extends AbstractMauticTestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&ComposerHelper
|
||||
*/
|
||||
private MockObject $composerHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject&PackageModel
|
||||
*/
|
||||
private MockObject $packageModel;
|
||||
|
||||
private string $packageName;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->composerHelper = $this->createMock(ComposerHelper::class);
|
||||
$this->packageModel = $this->createMock(PackageModel::class);
|
||||
$this->packageName = 'koco/mautic-recaptcha-bundle';
|
||||
}
|
||||
|
||||
public function testInstallCommand(): void
|
||||
{
|
||||
$this->packageModel->method('getPackageDetail')
|
||||
->with($this->packageName)
|
||||
->willReturn($this->getPackageDetail());
|
||||
|
||||
$this->composerHelper->method('install')
|
||||
->with($this->packageName)
|
||||
->willReturn(new ConsoleOutput(0, 'OK'));
|
||||
|
||||
$command = new InstallCommand($this->composerHelper, $this->packageModel);
|
||||
|
||||
$result = $this->testSymfonyCommand(
|
||||
'mautic:marketplace:install',
|
||||
['package' => $this->packageName],
|
||||
$command
|
||||
);
|
||||
|
||||
Assert::assertSame(0, $result->getStatusCode());
|
||||
}
|
||||
|
||||
public function testInstallCommandWithDryRun(): void
|
||||
{
|
||||
$this->packageModel->method('getPackageDetail')
|
||||
->with($this->packageName)
|
||||
->willReturn($this->getPackageDetail());
|
||||
|
||||
$this->composerHelper->method('install')
|
||||
->with($this->packageName)
|
||||
->willReturn(new ConsoleOutput(0, 'OK'));
|
||||
|
||||
$command = new InstallCommand($this->composerHelper, $this->packageModel);
|
||||
|
||||
$result = $this->testSymfonyCommand(
|
||||
'mautic:marketplace:install',
|
||||
['package' => $this->packageName, '--dry-run' => null],
|
||||
$command
|
||||
);
|
||||
|
||||
Assert::assertSame(0, $result->getStatusCode());
|
||||
Assert::assertStringContainsString('dry-running this installation', $result->getDisplay());
|
||||
}
|
||||
|
||||
public function testInstallCommandWithNonExistingPackage(): void
|
||||
{
|
||||
$packageName = 'mautic/non-existent-plugin';
|
||||
|
||||
$this->packageModel->method('getPackageDetail')
|
||||
->with($packageName)
|
||||
->willThrowException(new ApiException('Package not found', 404));
|
||||
|
||||
$command = new InstallCommand($this->composerHelper, $this->packageModel);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$this->testSymfonyCommand(
|
||||
'mautic:marketplace:install',
|
||||
['package' => $packageName],
|
||||
$command
|
||||
);
|
||||
}
|
||||
|
||||
public function testInstallCommandWithComposerNotAvailable(): void
|
||||
{
|
||||
$packageName = 'mautic/non-existent-plugin';
|
||||
|
||||
$this->packageModel->method('getPackageDetail')
|
||||
->with($packageName)
|
||||
->willThrowException(new ApiException('Internal Server Error', 500));
|
||||
|
||||
$command = new InstallCommand($this->composerHelper, $this->packageModel);
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
|
||||
$this->testSymfonyCommand(
|
||||
'mautic:marketplace:install',
|
||||
['package' => $packageName],
|
||||
$command
|
||||
);
|
||||
}
|
||||
|
||||
public function testInstallCommandWithWrongPackageType(): void
|
||||
{
|
||||
$packageName = 'mautic/package-with-wrong-type';
|
||||
$packageDetail = $this->getPackageDetail();
|
||||
$packageDetail->packageBase->type = 'non-existent-type';
|
||||
|
||||
$this->packageModel->method('getPackageDetail')
|
||||
->with($packageName)
|
||||
->willReturn($packageDetail);
|
||||
|
||||
$command = new InstallCommand($this->composerHelper, $this->packageModel);
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
|
||||
$this->testSymfonyCommand(
|
||||
'mautic:marketplace:install',
|
||||
['package' => $packageName],
|
||||
$command
|
||||
);
|
||||
}
|
||||
|
||||
public function testInstallCommandWithFailedComposerCommand(): void
|
||||
{
|
||||
$packageName = 'mautic/crash-package';
|
||||
|
||||
$this->composerHelper->method('install')
|
||||
->with($packageName)
|
||||
->willReturn(new ConsoleOutput(1, 'Something went wrong during the installation'));
|
||||
|
||||
$this->packageModel->method('getPackageDetail')
|
||||
->with($packageName)
|
||||
->willReturn($this->getPackageDetail());
|
||||
|
||||
$command = new InstallCommand($this->composerHelper, $this->packageModel);
|
||||
$result = $this->testSymfonyCommand(
|
||||
'mautic:marketplace:install',
|
||||
['package' => $packageName],
|
||||
$command
|
||||
);
|
||||
|
||||
Assert::assertSame(1, $result->getStatusCode());
|
||||
Assert::assertSame("Installing mautic/crash-package, this might take a while...\nError while installing this plugin.\nSomething went wrong during the installation\n", $result->getDisplay());
|
||||
}
|
||||
|
||||
private function getPackageDetail(): PackageDetail
|
||||
{
|
||||
$payload = json_decode(file_get_contents(__DIR__.'/../../ApiResponse/detail.json'), true);
|
||||
|
||||
return PackageDetail::fromArray($payload['package']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Tests\Functional\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
|
||||
use Mautic\MarketplaceBundle\Api\Connection;
|
||||
use Mautic\MarketplaceBundle\Command\ListCommand;
|
||||
use Mautic\MarketplaceBundle\DTO\Allowlist as DTOAllowlist;
|
||||
use Mautic\MarketplaceBundle\Service\Allowlist;
|
||||
use Mautic\MarketplaceBundle\Service\PluginCollector;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class ListCommandTest extends AbstractMauticTestCase
|
||||
{
|
||||
public function testCommand(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getPlugins')
|
||||
->willReturn(json_decode(file_get_contents(__DIR__.'/../../ApiResponse/list.json'), true));
|
||||
|
||||
$allowlist = $this->createMock(Allowlist::class);
|
||||
$allowlist->method('getAllowlist')->willReturn(null);
|
||||
|
||||
$pluginCollector = new PluginCollector($connection, $allowlist);
|
||||
$command = new ListCommand($pluginCollector);
|
||||
|
||||
$result = $this->testSymfonyCommand(
|
||||
ListCommand::NAME,
|
||||
[
|
||||
'--page' => 1,
|
||||
'--limit' => 5,
|
||||
'--filter' => 'mautic',
|
||||
],
|
||||
$command
|
||||
);
|
||||
|
||||
$expected = <<<EOF
|
||||
+--------------------------------------------------------+-----------+--------+
|
||||
| name | downloads | favers |
|
||||
+--------------------------------------------------------+-----------+--------+
|
||||
| mautic/mautic-saelos-bundle | 10586 | 11 |
|
||||
| koco/mautic-recaptcha-bundle | 2012 | 20 |
|
||||
| This plugin brings reCAPTCHA integration to | | |
|
||||
| mautic. | | |
|
||||
| monogramm/mautic-ldap-auth-bundle | 307 | 8 |
|
||||
| This plugin enables LDAP authentication for | | |
|
||||
| mautic. | | |
|
||||
| maatoo/mautic-referrals-bundle | 527 | 5 |
|
||||
| This plugin enables referrals in mautic. | | |
|
||||
| thedmsgroup/mautic-do-not-contact-extras-bundle | 532 | 9 |
|
||||
| Adds custom DNC list items to be added to standard | | |
|
||||
| Mautic DNC lists and creates phpne and sms | | |
|
||||
| channels | | |
|
||||
+--------------------------------------------------------+-----------+--------+
|
||||
Total packages: 58
|
||||
Execution time:
|
||||
EOF;
|
||||
|
||||
Assert::assertStringContainsString($expected, $result->getDisplay());
|
||||
Assert::assertSame(0, $result->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCommmandWithAllowlist(): void
|
||||
{
|
||||
$page = 1;
|
||||
$limit = 5;
|
||||
$query = 'mautic';
|
||||
|
||||
$plugin1 = <<<EOF
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "koco\/mautic-recaptcha-bundle",
|
||||
"description": "This plugin brings reCAPTCHA integration to mautic.",
|
||||
"url": "https:\/\/packagist.org\/packages\/koco\/mautic-recaptcha-bundle",
|
||||
"repository": "https:\/\/github.com\/KonstantinCodes\/mautic-recaptcha",
|
||||
"downloads": 2012,
|
||||
"favers": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF;
|
||||
|
||||
$plugin2 = <<<EOF
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "maatoo\/mautic-referrals-bundle",
|
||||
"description": "This plugin enables referrals in mautic.",
|
||||
"url": "https:\/\/packagist.org\/packages\/maatoo\/mautic-referrals-bundle",
|
||||
"repository": "https:\/\/github.com\/maatoo-io\/MauticReferralsBundle",
|
||||
"downloads": 527,
|
||||
"favers": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF;
|
||||
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$connection->expects($matcher)->method('getPlugins')->willReturnCallback(function (...$parameters) use ($matcher, $plugin1, $plugin2) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(1, $parameters[0]);
|
||||
$this->assertSame(1, $parameters[1]);
|
||||
$this->assertSame('koco/mautic-recaptcha-bundle', $parameters[2]);
|
||||
|
||||
return json_decode($plugin1, true);
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame(1, $parameters[0]);
|
||||
$this->assertSame(1, $parameters[1]);
|
||||
$this->assertSame('maatoo/mautic-referrals-bundle', $parameters[2]);
|
||||
|
||||
return json_decode($plugin2, true);
|
||||
}
|
||||
});
|
||||
|
||||
$allowlistPayload = DTOAllowlist::fromArray(json_decode(file_get_contents(__DIR__.'/../../ApiResponse/allowlist.json'), true));
|
||||
$allowlist = $this->createMock(Allowlist::class);
|
||||
$allowlist->method('getAllowList')->willReturn($allowlistPayload);
|
||||
|
||||
$pluginCollector = new PluginCollector($connection, $allowlist);
|
||||
$command = new ListCommand($pluginCollector);
|
||||
|
||||
$result = $this->testSymfonyCommand(
|
||||
ListCommand::NAME,
|
||||
[
|
||||
'--page' => $page,
|
||||
'--limit' => $limit,
|
||||
'--filter' => $query,
|
||||
],
|
||||
$command
|
||||
);
|
||||
|
||||
$expected = <<<EOF
|
||||
+-------------------------------------------------+-----------+--------+
|
||||
| name | downloads | favers |
|
||||
+-------------------------------------------------+-----------+--------+
|
||||
| koco/mautic-recaptcha-bundle | 2012 | 20 |
|
||||
| This plugin brings reCAPTCHA integration to | | |
|
||||
| mautic. | | |
|
||||
| maatoo/mautic-referrals-bundle | 527 | 5 |
|
||||
| This plugin enables referrals in mautic. | | |
|
||||
+-------------------------------------------------+-----------+--------+
|
||||
Total packages: 2
|
||||
Execution time:
|
||||
EOF;
|
||||
|
||||
Assert::assertStringContainsString($expected, $result->getDisplay());
|
||||
Assert::assertSame(0, $result->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Tests\Functional\Command;
|
||||
|
||||
use Mautic\CoreBundle\Helper\ComposerHelper;
|
||||
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
|
||||
use Mautic\MarketplaceBundle\Command\RemoveCommand;
|
||||
use Mautic\MarketplaceBundle\DTO\ConsoleOutput;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class RemoveCommandTest extends AbstractMauticTestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&LoggerInterface
|
||||
*/
|
||||
private MockObject $logger;
|
||||
|
||||
private string $packageName;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->packageName = 'koco/mautic-recaptcha-bundle';
|
||||
}
|
||||
|
||||
public function testRemoveCommand(): void
|
||||
{
|
||||
$composer = $this->createMock(ComposerHelper::class);
|
||||
$composer->method('remove')
|
||||
->with($this->packageName)
|
||||
->willReturn(new ConsoleOutput(0, 'OK'));
|
||||
$composer->method('getMauticPluginPackages')
|
||||
->willReturn(['koco/mautic-recaptcha-bundle']);
|
||||
$command = new RemoveCommand($composer, $this->logger);
|
||||
|
||||
$result = $this->testSymfonyCommand(
|
||||
'mautic:marketplace:remove',
|
||||
['package' => $this->packageName],
|
||||
$command
|
||||
);
|
||||
|
||||
Assert::assertSame(0, $result->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRemoveCommandWithInvalidPackageType(): void
|
||||
{
|
||||
$composer = $this->createMock(ComposerHelper::class);
|
||||
$composer->method('remove')
|
||||
->with($this->packageName)
|
||||
->willReturn(new ConsoleOutput(0, 'OK'));
|
||||
$composer->method('getMauticPluginPackages')
|
||||
->willReturn([]);
|
||||
$command = new RemoveCommand($composer, $this->logger);
|
||||
|
||||
$result = $this->testSymfonyCommand(
|
||||
'mautic:marketplace:remove',
|
||||
['package' => $this->packageName],
|
||||
$command
|
||||
);
|
||||
|
||||
Assert::assertSame(1, $result->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRemoveCommandWithComposerError(): void
|
||||
{
|
||||
$composer = $this->createMock(ComposerHelper::class);
|
||||
$composer->method('remove')
|
||||
->with($this->packageName)
|
||||
->willReturn(new ConsoleOutput(1, 'Error while removing package'));
|
||||
$composer->method('getMauticPluginPackages')
|
||||
->willReturn([]);
|
||||
$command = new RemoveCommand($composer, $this->logger);
|
||||
|
||||
$result = $this->testSymfonyCommand(
|
||||
'mautic:marketplace:remove',
|
||||
['package' => $this->packageName],
|
||||
$command
|
||||
);
|
||||
|
||||
Assert::assertSame(1, $result->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Tests\Functional\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CacheHelper;
|
||||
use Mautic\CoreBundle\Helper\ComposerHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\MarketplaceBundle\Controller\AjaxController;
|
||||
use Mautic\MarketplaceBundle\DTO\ConsoleOutput;
|
||||
use Mautic\MarketplaceBundle\Security\Permissions\MarketplacePermissions;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
final class AjaxControllerTest extends AbstractMauticTestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|CorePermissions
|
||||
*/
|
||||
private MockObject $security;
|
||||
|
||||
/**
|
||||
* @var MockObject|Config
|
||||
*/
|
||||
private MockObject $marketplaceConfig;
|
||||
|
||||
/**
|
||||
* @var MockObject|RequestStack
|
||||
*/
|
||||
private MockObject $requestStack;
|
||||
|
||||
public function testInstallPackageAction(): void
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [], '{"vendor":"mautic","package":"test-plugin-bundle"}');
|
||||
$controller = $this->generateController(false);
|
||||
|
||||
$this->marketplaceConfig->method('marketplaceIsEnabled')->willReturn(true);
|
||||
$this->marketplaceConfig->method('isComposerEnabled')->willReturn(true);
|
||||
$this->security->expects($this->any())
|
||||
->method('isGranted')
|
||||
->with(MarketplacePermissions::CAN_INSTALL_PACKAGES)
|
||||
->willReturn(true);
|
||||
|
||||
$response = $controller->installPackageAction($request);
|
||||
|
||||
Assert::assertSame('{"success":true}', $response->getContent());
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRemovePackageAction(): void
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [], '{"vendor":"mautic","package":"test-plugin-bundle"}');
|
||||
$controller = $this->generateController(true);
|
||||
|
||||
$this->marketplaceConfig->method('marketplaceIsEnabled')->willReturn(true);
|
||||
$this->marketplaceConfig->method('isComposerEnabled')->willReturn(true);
|
||||
$this->security->expects($this->any())
|
||||
->method('isGranted')
|
||||
->with(MarketplacePermissions::CAN_REMOVE_PACKAGES)
|
||||
->willReturn(true);
|
||||
|
||||
$response = $controller->removePackageAction($request);
|
||||
|
||||
Assert::assertSame('{"success":true}', $response->getContent());
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
private function generateController(bool $isPackageInstalled): AjaxController
|
||||
{
|
||||
$composer = $this->createMock(ComposerHelper::class);
|
||||
$composer->method('install')->willReturn(new ConsoleOutput(0, 'OK'));
|
||||
$composer->method('remove')->willReturn(new ConsoleOutput(0, 'OK'));
|
||||
$composer->method('isInstalled')->willReturn($isPackageInstalled);
|
||||
|
||||
$cacheHelper = $this->createMock(CacheHelper::class);
|
||||
$cacheHelper->method('clearSymfonyCache')->willReturn(0);
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$doctrine = $this->createMock(ManagerRegistry::class);
|
||||
$modelFactory = $this->createMock(ModelFactory::class);
|
||||
$userHelper = $this->createMock(UserHelper::class);
|
||||
$coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$translator = $this->createMock(Translator::class);
|
||||
$flashBag = $this->createMock(FlashBag::class);
|
||||
$this->requestStack = $this->createMock(RequestStack::class);
|
||||
$this->security = $this->createMock(CorePermissions::class);
|
||||
$this->marketplaceConfig = $this->createMock(Config::class);
|
||||
|
||||
$controller = new AjaxController(
|
||||
$composer,
|
||||
$cacheHelper,
|
||||
$logger,
|
||||
$this->marketplaceConfig,
|
||||
$doctrine,
|
||||
$modelFactory,
|
||||
$userHelper,
|
||||
$coreParametersHelper,
|
||||
$dispatcher,
|
||||
$translator,
|
||||
$flashBag,
|
||||
$this->requestStack,
|
||||
$this->security
|
||||
);
|
||||
$controller->setContainer(static::getContainer());
|
||||
|
||||
return $controller;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Tests\Functional\Controller;
|
||||
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Mautic\CoreBundle\Test\Guzzle\ClientMockTrait;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\MarketplaceBundle\Service\Allowlist;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
final class DetailControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use ClientMockTrait;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')]
|
||||
public function testMarketplaceDetailPage(string $requestedPackage, int $responseCode, string $foundPackageName, string $foundPackageDesc, string $latestVersion = ''): void
|
||||
{
|
||||
/** @var MockHandler $handlerStack */
|
||||
$handlerStack = $this->getClientMockHandler();
|
||||
$handlerStack->append(
|
||||
new Response(SymfonyResponse::HTTP_OK, [], file_get_contents(__DIR__.'/../../ApiResponse/allowlist.json')), // Getting Allow list from Github API.
|
||||
new Response(SymfonyResponse::HTTP_OK, [], file_get_contents(__DIR__.'/../../ApiResponse/detail.json')) // Getting package detail from Packagist API.
|
||||
);
|
||||
|
||||
/** @var Allowlist $allowlist */
|
||||
$allowlist = static::getContainer()->get('marketplace.service.allowlist');
|
||||
$allowlist->clearCache();
|
||||
|
||||
$this->client->request('GET', "s/marketplace/detail/{$requestedPackage}");
|
||||
|
||||
$responseContent = $this->client->getResponse()->getContent();
|
||||
|
||||
Assert::assertSame($responseCode, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
|
||||
Assert::assertStringContainsString($foundPackageDesc, $responseContent);
|
||||
Assert::assertStringContainsString($foundPackageName, $responseContent);
|
||||
Assert::assertStringContainsString($latestVersion, $responseContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array<string|int>>
|
||||
*/
|
||||
public static function dataProvider(): iterable
|
||||
{
|
||||
// Package that do not exist in the allowlist.
|
||||
yield [
|
||||
'mautic/unicorn',
|
||||
SymfonyResponse::HTTP_NOT_FOUND,
|
||||
'mautic/unicorn',
|
||||
'Package 'mautic/unicorn' not found in allowlist.',
|
||||
];
|
||||
|
||||
// Package that exists in the allowlist with display name.
|
||||
yield [
|
||||
'koco/mautic-recaptcha-bundle',
|
||||
SymfonyResponse::HTTP_OK,
|
||||
'KocoCaptcha',
|
||||
'This plugin brings reCAPTCHA integration to mautic.',
|
||||
'<a href="https://github.com/KonstantinCodes/mautic-recaptcha/releases/tag/3.0.1" id="latest-version" target="_blank" rel="noopener noreferrer">',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\MarketplaceBundle\Tests\Functional\Controller;
|
||||
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Mautic\CoreBundle\Test\Guzzle\ClientMockTrait;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\MarketplaceBundle\Service\Allowlist;
|
||||
use Mautic\MarketplaceBundle\Service\Config;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
final class ListControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use ClientMockTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
if ('testMarketplaceListTableWithNoAllowList' === $this->name()) {
|
||||
$this->configParams[Config::MARKETPLACE_ALLOWLIST_URL] = '0'; // Empty string results in null for some reason.
|
||||
}
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testMarketplaceListTableWithNoAllowList(): void
|
||||
{
|
||||
/** @var MockHandler $handlerStack */
|
||||
$handlerStack = $this->getClientMockHandler();
|
||||
$handlerStack->append(
|
||||
new Response(SymfonyResponse::HTTP_OK, [], file_get_contents(__DIR__.'/../../ApiResponse/list.json')) // Getting the package list from Packagist API.
|
||||
);
|
||||
|
||||
/** @var Allowlist $allowlist */
|
||||
$allowlist = static::getContainer()->get('marketplace.service.allowlist');
|
||||
$allowlist->clearCache();
|
||||
|
||||
$crawler = $this->client->request('GET', 's/marketplace');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'Mautic Saelos Bundle',
|
||||
'Mautic Recaptcha Bundle',
|
||||
'Mautic Ldap Auth Bundle',
|
||||
'Mautic Referrals Bundle',
|
||||
'Mautic Do Not Contact Extras Bundle',
|
||||
],
|
||||
array_map(
|
||||
fn (string $dirtyPackageName) => trim($dirtyPackageName),
|
||||
$crawler->filter('#marketplace-packages-table .package-name a')->extract(['_text'])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testMarketplaceListTableWithAllowList(): void
|
||||
{
|
||||
$mockResults = json_decode(file_get_contents(__DIR__.'/../../ApiResponse/list.json'), true)['results'];
|
||||
|
||||
/** @var MockHandler $handlerStack */
|
||||
$handlerStack = $this->getClientMockHandler();
|
||||
$handlerStack->append(
|
||||
new Response(SymfonyResponse::HTTP_OK, [], file_get_contents(__DIR__.'/../../ApiResponse/allowlist.json')), // Getting Allow list from Github API.
|
||||
new Response(SymfonyResponse::HTTP_OK, [], json_encode(['results' => [$mockResults[1]]])), // mautic-recaptcha-bundle
|
||||
new Response(SymfonyResponse::HTTP_OK, [], json_encode(['results' => [$mockResults[3]]])), // mautic-referrals-bundle
|
||||
);
|
||||
|
||||
/** @var Allowlist $allowlist */
|
||||
$allowlist = static::getContainer()->get('marketplace.service.allowlist');
|
||||
$allowlist->clearCache();
|
||||
|
||||
$crawler = $this->client->request('GET', 's/marketplace');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'KocoCaptcha',
|
||||
'Mautic Referrals Bundle',
|
||||
],
|
||||
array_map(
|
||||
fn (string $dirtyPackageName) => trim($dirtyPackageName),
|
||||
$crawler->filter('#marketplace-packages-table .package-name a')->extract(['_text'])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
mautic.marketplace.marketplace="Marketplace"
|
||||
marketplace.title="Marketplace <sup>BETA</sup>"
|
||||
marketplace.beta.warning="<strong>Heads up!</strong> This is a preview of the Mautic Marketplace. Some things might not work as expected yet. <a target='_blank' href='https://docs.mautic.org/en/marketplace'>[Read more]</a>"
|
||||
marketplace.composer.required="<strong>Heads up!</strong> Technical setup required to install or update plugins. <a target='_blank' href='https://mau.tc/switch-to-composer'>Send these instructions to a developer.</a>"
|
||||
mautic.marketplace.permissions.header="Marketplace Permissions"
|
||||
mautic.marketplace.permissions.packages="Packages - User has access to"
|
||||
marketplace.vendor="Vendor"
|
||||
marketplace.downloads="Downloads"
|
||||
marketplace.favers="Stars"
|
||||
marketplace.package.github.stars="Stars"
|
||||
marketplace.package.github.watchers="Watchers"
|
||||
marketplace.package.github.forks="Forks"
|
||||
marketplace.package.github.open.issues="Open issues"
|
||||
marketplace.package.dependents="Dependent packages"
|
||||
marketplace.package.suggesters="Suggested by other packages"
|
||||
marketplace.package.total.downloads="Total downloads"
|
||||
marketplace.package.monthly.downloads="Monthly downloads"
|
||||
marketplace.package.daily.downloads="Daily downloads"
|
||||
marketplace.package.version="Version"
|
||||
marketplace.package.homepage="Homepage"
|
||||
marketplace.package.create.date="Package created"
|
||||
marketplace.package.maintainers="Package maintainers"
|
||||
marketplace.package.license="License"
|
||||
marketplace.package.issue.tracker="Issue tracker"
|
||||
marketplace.package.wiki="Documentation"
|
||||
marketplace.package.version.release.date="Release date"
|
||||
marketplace.package.required.packages="Required packages"
|
||||
marketplace.package.keywords="Keywords"
|
||||
marketplace.other.packages="Other packages by %name%"
|
||||
marketplace.package.repository="Repository"
|
||||
marketplace.package.cli.install="Install via CLI"
|
||||
marketplace.package.cli.install.descr="Installing a plugin via Command Line Interface is recommended. To install this plugin, execute this command: <code>bin/console mautic:marketplace:install %vendor%/%package%</code>"
|
||||
marketplace.package.latest.stable.version="Latest Stable Version"
|
||||
marketplace.package.last_updated="Last updated"
|
||||
marketplace.package.all.versions="All Versions"
|
||||
marketplace.package.maintainers="Maintainers"
|
||||
marketplace.package.github.info="GitHub Info"
|
||||
marketplace.package.packagist.info="Packagist Info"
|
||||
marketplace.package.install="Install"
|
||||
marketplace.package.type="Type"
|
||||
marketplace.package.type.plugin="Plugin"
|
||||
marketplace.package.type.theme="Theme"
|
||||
marketplace.package.remove="Remove"
|
||||
marketplace.package.install.failed="Installation of the package has failed. Please check Mautic's logs for more details."
|
||||
marketplace.package.install.already.installed="This package is already installed on the system."
|
||||
marketplace.package.install.html.failed="Something went wrong while installing <strong>%packagename%</strong>. This is the error:"
|
||||
marketplace.package.install.html.in.progress="<strong>%packagename%</strong> is being installed. This might take a while..."
|
||||
marketplace.package.install.html.success="Successfully installed <strong>%packagename%</strong>!"
|
||||
marketplace.package.install.html.success.continue="Go to the plugin page to activate the plugin"
|
||||
marketplace.package.request.marketplace_disabled="The marketplace is disabled."
|
||||
marketplace.package.request.no_permissions="You don't have permission to perform this action."
|
||||
marketplace.package.request.details.missing="The package vendor or name has not been provided. Please try again."
|
||||
marketplace.package.cache.clear.failed="Couldn't refresh plugins list. Please ask a developer to check server permissions and try again."
|
||||
marketplace.package.remove.not.installed="The selected package is not currently installed and can therefore not be removed. Please try again with a different package name."
|
||||
marketplace.package.remove.failed="Removing the package has failed. Please check Mautic's logs for more details."
|
||||
marketplace.package.remove.html.failed="Something went wrong while removing <strong>%packagename%</strong>. This is the error:"
|
||||
marketplace.package.remove.html.in.progress="<strong>%packagename%</strong> is being removed. This might take a while..."
|
||||
marketplace.package.remove.html.success="Successfully removed <strong>%packagename%</strong>!"
|
||||
marketplace.package.remove.html.success.continue="Back to Marketplace overview"
|
||||
marketplace.latest.version.missing="Could not find any version of this package. Please try again later."
|
||||
marketplace.noresults.tip="There are no packages available for your version of Mautic. Try upgrading to a newer version."
|
||||
marketplace.clear.cache="Refresh plugins list"
|
||||
marketplace.clear.cache.tooltip="We save some data to make the Marketplace load faster. Click here to update this saved data."
|
||||
marketplace.package.details.close="Back to marketplace"
|
||||
Reference in New Issue
Block a user