diff --git a/config/services.yaml b/config/services.yaml index e09434af..a859befc 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -15,6 +15,7 @@ parameters: router.request_context.host: '%env(default:domain:APP_PUBLIC_HOST)%' security_advisories_db_dir: '%env(resolve:SECURITY_ADVISORIES_DB_DIR)%' security_advisories_db_repo: 'https://github.com/FriendsOfPHP/security-advisories.git' + instance_id_file: '%kernel.project_dir%/var/instance-id' services: # default configuration for services in *this* file @@ -25,6 +26,7 @@ services: $distsDir: '%dists_dir%' $resetPasswordTokenTtl: 86400 # 24h Symfony\Component\HttpFoundation\Session\Session $session: '@session' + $instanceIdFile: '%instance_id_file%' # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name diff --git a/config/services_test.yaml b/config/services_test.yaml index 49208858..8e7616d0 100644 --- a/config/services_test.yaml +++ b/config/services_test.yaml @@ -3,6 +3,7 @@ parameters: repo_dir: '%kernel.project_dir%/tests/Resources' security_advisories_db_dir: '%kernel.project_dir%/tests/Resources/fixtures/security/security-advisories' security_advisories_db_repo: 'bogus' + instance_id_file: '%kernel.cache_dir%/test-instance-id' services: Buddy\Repman\Service\Downloader: diff --git a/src/Command/CreateAdminCommand.php b/src/Command/CreateAdminCommand.php index 69450204..05bb2bef 100644 --- a/src/Command/CreateAdminCommand.php +++ b/src/Command/CreateAdminCommand.php @@ -4,22 +4,28 @@ namespace Buddy\Repman\Command; +use Buddy\Repman\Message\Admin\ChangeConfig; use Buddy\Repman\Message\User\CreateUser; +use Buddy\Repman\Service\Telemetry; use Ramsey\Uuid\Uuid; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Messenger\MessageBusInterface; final class CreateAdminCommand extends Command { private MessageBusInterface $bus; + private Telemetry $telemetry; - public function __construct(MessageBusInterface $bus) + public function __construct(MessageBusInterface $bus, Telemetry $telemetry) { $this->bus = $bus; + $this->telemetry = $telemetry; + parent::__construct(); } @@ -53,6 +59,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int ['ROLE_ADMIN'] )); + if (!$this->telemetry->isInstanceIdPresent()) { + $question = new ConfirmationQuestion( + "Allow for sending anonymous usage statistic? [{$this->telemetry->docsUrl()}] (y/n)", + true + ); + $answer = $this + ->getHelper('question') + ->ask($input, $output, $question); + + $this->bus->dispatch(new ChangeConfig([ + 'telemetry' => $answer ? 'enabled' : 'disabled', + ])); + + $this->telemetry->generateInstanceId(); + } + $output->writeln(sprintf('Created admin user with id: %s', $id)); return 0; diff --git a/src/Controller/Admin/ConfigController.php b/src/Controller/Admin/ConfigController.php index f722a2ee..c40e5975 100644 --- a/src/Controller/Admin/ConfigController.php +++ b/src/Controller/Admin/ConfigController.php @@ -7,6 +7,7 @@ use Buddy\Repman\Form\Type\Admin\ConfigType; use Buddy\Repman\Message\Admin\ChangeConfig; use Buddy\Repman\Service\Config; +use Buddy\Repman\Service\Telemetry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -15,10 +16,12 @@ final class ConfigController extends AbstractController { private Config $config; + private Telemetry $telemetry; - public function __construct(Config $config) + public function __construct(Config $config, Telemetry $telemetry) { $this->config = $config; + $this->telemetry = $telemetry; } /** @@ -39,4 +42,30 @@ public function edit(Request $request): Response 'form' => $form->createView(), ]); } + + /** + * @Route("/admin/config/telemetry", name="admin_config_telemetry_enable", methods={"POST"}) + */ + public function enableTelemetry(Request $request): Response + { + $this->telemetry->generateInstanceId(); + $this->dispatchMessage(new ChangeConfig([ + 'telemetry' => 'enable', + ])); + + return $this->redirectToRoute('index'); + } + + /** + * @Route("/admin/config/telemetry", name="admin_config_telemetry_disable", methods={"DELETE"}) + */ + public function disableTelemetry(Request $request): Response + { + $this->telemetry->generateInstanceId(); + $this->dispatchMessage(new ChangeConfig([ + 'telemetry' => 'disable', + ])); + + return $this->redirectToRoute('index'); + } } diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php index 56390014..b96516a3 100644 --- a/src/Controller/IndexController.php +++ b/src/Controller/IndexController.php @@ -4,17 +4,30 @@ namespace Buddy\Repman\Controller; +use Buddy\Repman\Service\Telemetry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; final class IndexController extends AbstractController { + private Telemetry $telemetry; + + public function __construct(Telemetry $telemetry) + { + $this->telemetry = $telemetry; + } + /** * @Route(path="/", name="index", methods={"GET"}) */ public function index(): Response { - return $this->render('index.html.twig'); + $showTelemetryPrompt = !$this->telemetry->isInstanceIdPresent(); + + return $this->render('index.html.twig', [ + 'showTelemetryPrompt' => $showTelemetryPrompt, + 'telemetryDocsUrl' => $this->telemetry->docsUrl(), + ]); } } diff --git a/src/Form/Type/Admin/ConfigType.php b/src/Form/Type/Admin/ConfigType.php index 79c8afb7..7e863e3f 100644 --- a/src/Form/Type/Admin/ConfigType.php +++ b/src/Form/Type/Admin/ConfigType.php @@ -45,6 +45,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'data-style' => 'btn-secondary', ], ]) + ->add('telemetry', ChoiceType::class, [ + 'choices' => [ + 'enabled' => 'enabled', + 'disabled' => 'disabled', + ], + 'help' => 'Enable collecting and sending anonymous usage data', + 'attr' => [ + 'class' => 'form-control selectpicker', + 'data-style' => 'btn-secondary', + ], + ]) ->add('save', SubmitType::class, ['label' => 'Save']) ; } diff --git a/src/Migrations/Version20200716105216.php b/src/Migrations/Version20200716105216.php new file mode 100644 index 00000000..e0b6e632 --- /dev/null +++ b/src/Migrations/Version20200716105216.php @@ -0,0 +1,35 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql("INSERT INTO config (key, value) VALUES ('telemetry', 'disabled')"); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql("DELETE FROM config WHERE key = 'telemetry'"); + } +} diff --git a/src/Service/Telemetry.php b/src/Service/Telemetry.php new file mode 100644 index 00000000..650bddaa --- /dev/null +++ b/src/Service/Telemetry.php @@ -0,0 +1,34 @@ +instanceIdFile = $instanceIdFile; + } + + public function docsUrl(): string + { + return 'https://repman.io/docs/telemetry'; + } + + public function generateInstanceId(): void + { + if (!$this->isInstanceIdPresent()) { + \file_put_contents($this->instanceIdFile, Uuid::uuid4()); + } + } + + public function isInstanceIdPresent(): bool + { + return \file_exists($this->instanceIdFile); + } +} diff --git a/templates/admin/config/edit.html.twig b/templates/admin/config/edit.html.twig index 814125da..7c072ea1 100644 --- a/templates/admin/config/edit.html.twig +++ b/templates/admin/config/edit.html.twig @@ -4,7 +4,7 @@ {% block content %}
-
+
{{ form(form) }}
diff --git a/templates/component/telemetryPrompt.html.twig b/templates/component/telemetryPrompt.html.twig new file mode 100644 index 00000000..4bce7cfa --- /dev/null +++ b/templates/component/telemetryPrompt.html.twig @@ -0,0 +1,17 @@ +
+

Telemetry

+

+ Help us improve Repman by enabling sending anonymous usage statistic + (more info). +

+
+
+ + +
+
+ + +
+
+
diff --git a/templates/index.html.twig b/templates/index.html.twig index 3891a462..93602652 100644 --- a/templates/index.html.twig +++ b/templates/index.html.twig @@ -3,9 +3,12 @@ {% block page %}
- {% include 'component/flash.html.twig' %} + {% if is_granted('ROLE_ADMIN') and showTelemetryPrompt %} + {% include 'component/telemetryPrompt.html.twig' %} + {% endif %} +
diff --git a/tests/Functional/Command/CreateAdminCommandTest.php b/tests/Functional/Command/CreateAdminCommandTest.php index a18e8800..475b624d 100644 --- a/tests/Functional/Command/CreateAdminCommandTest.php +++ b/tests/Functional/Command/CreateAdminCommandTest.php @@ -6,13 +6,16 @@ use Buddy\Repman\Command\CreateAdminCommand; use Buddy\Repman\Tests\Functional\FunctionalTestCase; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; final class CreateAdminCommandTest extends FunctionalTestCase { public function testCreateAdmin(): void { - $commandTester = new CommandTester($this->container()->get(CreateAdminCommand::class)); + $command = $this->container()->get(CreateAdminCommand::class); + $command->setApplication(new Application()); + $commandTester = new CommandTester($command); $commandTester->execute([ 'email' => 'test@buddy.works', 'password' => 'password', diff --git a/tests/Functional/Controller/Admin/ConfigControllerTest.php b/tests/Functional/Controller/Admin/ConfigControllerTest.php index 904c8683..f0dc6e69 100644 --- a/tests/Functional/Controller/Admin/ConfigControllerTest.php +++ b/tests/Functional/Controller/Admin/ConfigControllerTest.php @@ -96,4 +96,40 @@ public function testToggleAuthenticationOptions(): void $this->lastResponseBody() ); } + + public function testEnableTelemetry(): void + { + $prompt = 'Help us improve Repman by enabling sending anonymous usage statistic'; + $instanceIdFile = $this->container()->getParameter('instance_id_file'); + @unlink($instanceIdFile); + $this->client->request('GET', $this->urlTo('index')); + self::assertStringContainsString($prompt, $this->lastResponseBody()); + + $this->client->request('POST', $this->urlTo('admin_config_telemetry_enable')); + + self::assertTrue($this->client->getResponse()->isRedirect($this->urlTo('index'))); + $this->client->followRedirect(); + + self::assertStringNotContainsString($prompt, $this->lastResponseBody()); + self::assertFileExists($instanceIdFile); + @unlink($instanceIdFile); + } + + public function testDisableTelemetry(): void + { + $prompt = 'Help us improve Repman by enabling sending anonymous usage statistic'; + $instanceIdFile = $this->container()->getParameter('instance_id_file'); + @unlink($instanceIdFile); + $this->client->request('GET', $this->urlTo('index')); + self::assertStringContainsString($prompt, $this->lastResponseBody()); + + $this->client->request('DELETE', $this->urlTo('admin_config_telemetry_enable')); + + self::assertTrue($this->client->getResponse()->isRedirect($this->urlTo('index'))); + $this->client->followRedirect(); + + self::assertStringNotContainsString($prompt, $this->lastResponseBody()); + self::assertFileExists($instanceIdFile); + @unlink($instanceIdFile); + } }