├── .gitignore
├── src
├── Resources
│ ├── docs
│ │ ├── contao-trigger-ex1.png
│ │ ├── installing.md
│ │ └── extending.md
│ ├── contao
│ │ ├── languages
│ │ │ ├── de
│ │ │ │ ├── default.php
│ │ │ │ ├── tl_nc_notification.php
│ │ │ │ ├── tl_eblick_trigger_log.php
│ │ │ │ └── tl_eblick_trigger.php
│ │ │ └── en
│ │ │ │ ├── default.php
│ │ │ │ ├── tl_nc_notification.php
│ │ │ │ ├── tl_eblick_trigger_log.php
│ │ │ │ └── tl_eblick_trigger.php
│ │ ├── config
│ │ │ └── config.php
│ │ └── dca
│ │ │ ├── tl_eblick_trigger_log.php
│ │ │ └── tl_eblick_trigger.php
│ ├── public
│ │ ├── img
│ │ │ ├── simulate.svg
│ │ │ ├── reset.svg
│ │ │ ├── log.svg
│ │ │ ├── running.svg
│ │ │ ├── waiting.svg
│ │ │ ├── error.svg
│ │ │ ├── paused.svg
│ │ │ └── automation.svg
│ │ └── css
│ │ │ └── backend.css
│ └── config
│ │ ├── services.yml
│ │ └── listener.yml
├── Execution
│ ├── ExecutionException.php
│ ├── ExecutionContextFactory.php
│ ├── ExecutionLog.php
│ └── ExecutionContext.php
├── EventListener
│ ├── DataContainer
│ │ ├── Common.php
│ │ ├── ExecutionLog.php
│ │ ├── NotificationAction.php
│ │ ├── TableCondition.php
│ │ └── Trigger.php
│ └── TriggerListener.php
├── DataContainer
│ ├── Definition.php
│ └── DataContainerComponentInterface.php
├── EBlickContaoTriggerBundle.php
├── Component
│ ├── Action
│ │ ├── ActionInterface.php
│ │ └── NotificationAction.php
│ ├── Condition
│ │ ├── ConditionInterface.php
│ │ ├── TimeCondition.php
│ │ └── TableCondition.php
│ └── ComponentManager.php
├── ContaoManager
│ └── Plugin.php
├── DependencyInjection
│ ├── EBlickContaoTriggerExtension.php
│ └── Compiler
│ │ └── AddComponentsCompilerPass.php
└── ExpressionLanguage
│ └── RowDataCompiler.php
├── tools
└── ecs
│ ├── composer.json
│ ├── config
│ └── default.php
│ └── composer.lock
├── phpunit.xml.dist
├── tests
├── Execution
│ ├── ExecutionContextFactoryTest.php
│ ├── ExecutionContextTest.php
│ └── ExecutionLogTest.php
├── EBlickContaoTriggerBundleTest.php
├── ContaoManager
│ └── PluginTest.php
├── EventListener
│ ├── TriggerListenerTest.php
│ └── DataContainer
│ │ ├── TableConditionTest.php
│ │ └── NotificationActionTest.php
├── DependencyInjection
│ ├── EBlickContaoTriggerExtensionTest.php
│ └── Compiler
│ │ └── AddComponentsCompilerPassTest.php
├── ExpressionLanguage
│ └── RowDataCompilerTest.php
└── Component
│ ├── ComponentManagerTest.php
│ ├── Condition
│ ├── TableConditionTest.php
│ └── TimeConditionTest.php
│ └── Action
│ └── NotificationActionTest.php
├── README.md
├── composer.json
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | /vendor
3 | /composer.lock
4 | /.phpunit.result.cache
5 | /tools/*/vendor
6 |
--------------------------------------------------------------------------------
/src/Resources/docs/contao-trigger-ex1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eBlick/contao-trigger/HEAD/src/Resources/docs/contao-trigger-ex1.png
--------------------------------------------------------------------------------
/tools/ecs/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "contao/easy-coding-standard": "^5.0"
4 | },
5 | "config": {
6 | "allow-plugins": {
7 | "dealerdirect/phpcodesniffer-composer-installer": true
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Execution/ExecutionException.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | ./tests/
10 |
11 |
12 |
13 |
14 | ./src/
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/DataContainer/Definition.php:
--------------------------------------------------------------------------------
1 | executionLog);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Resources/public/img/simulate.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/src/EBlickContaoTriggerBundle.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(
22 | new AddComponentsCompilerPass()
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Resources/public/img/reset.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/src/Component/Action/ActionInterface.php:
--------------------------------------------------------------------------------
1 | createMock(ExecutionLog::class));
23 | $parameters = new \stdClass();
24 | $parameters->id = 4;
25 |
26 | $context = $factory->createExecutionContext($parameters, 1000);
27 |
28 | self::assertInstanceOf(ExecutionContext::class, $context);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/EventListener/DataContainer/ExecutionLog.php:
--------------------------------------------------------------------------------
1 | [%s]',
23 | $GLOBALS['TL_LANG']['tl_eblick_trigger_log']['simulated'][0]
24 | ) : '';
25 |
26 | return sprintf(
27 | '%s (\'%s\' . \'%s\')%s',
28 | Date::parse(Config::get('datimFormat'), $row['tstamp']),
29 | $row['origin'],
30 | $row['originId'],
31 | $simulated
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tools/ecs/config/default.php:
--------------------------------------------------------------------------------
1 | sets([__DIR__.'/../vendor/contao/easy-coding-standard/config/contao.php']);
12 | $ecsConfig->parallel();
13 |
14 | $ecsConfig->ruleWithConfiguration(HeaderCommentFixer::class, [
15 | 'header' => "@copyright eBlick Medienberatung\n@license LGPL-3.0+\n@link https://github.com/eBlick/contao-trigger",
16 | ]);
17 |
18 | $ecsConfig->ruleWithConfiguration(
19 | PhpUnitTestCaseStaticMethodCallsFixer::class,
20 | ['call_type' => 'self']
21 | );
22 |
23 | $parameters = $ecsConfig->parameters();
24 | $parameters->set(Option::CACHE_DIRECTORY, sys_get_temp_dir().'/ecs_cache');
25 | };
26 |
--------------------------------------------------------------------------------
/tests/EBlickContaoTriggerBundleTest.php:
--------------------------------------------------------------------------------
1 | createMock(ContainerBuilder::class);
23 | $containerBuilder
24 | ->expects(self::once())
25 | ->method('addCompilerPass')
26 | ->with(new AddComponentsCompilerPass())
27 | ;
28 |
29 | $bundle = new EBlickContaoTriggerBundle();
30 | $bundle->build($containerBuilder);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/ContaoManager/Plugin.php:
--------------------------------------------------------------------------------
1 | setLoadAfter(
26 | [
27 | ContaoCoreBundle::class,
28 | 'notification-center',
29 | ]
30 | ),
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/DependencyInjection/EBlickContaoTriggerExtension.php:
--------------------------------------------------------------------------------
1 | load($file);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Resources/public/img/log.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/tests/ContaoManager/PluginTest.php:
--------------------------------------------------------------------------------
1 | getBundles($this->createMock(ParserInterface::class));
26 |
27 | /** @var BundleConfig $config */
28 | $config = $bundles[0];
29 |
30 | self::assertCount(1, $bundles);
31 | self::assertInstanceOf(BundleConfig::class, $config);
32 | self::assertEquals(EBlickContaoTriggerBundle::class, $config->getName());
33 | self::assertEquals([ContaoCoreBundle::class, 'notification-center'], $config->getLoadAfter());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Resources/docs/installing.md:
--------------------------------------------------------------------------------
1 | Installation
2 | ------------
3 |
4 | #### Step 1: Download the Bundle
5 |
6 | Open a command console, enter your project directory and execute the
7 | following command to download the latest stable version of this bundle:
8 |
9 | ```console
10 | $ composer require eblick/contao-trigger
11 | ```
12 |
13 |
14 | #### Step 2: Enable the Bundle
15 |
16 | **Skip this point if you are using a *Managed Edition* of Contao.**
17 |
18 | Enable the bundle by adding it to the list of registered bundles
19 | in the `app/AppKernel.php` file of your project:
20 |
21 | ```php
22 | null, 'myOtherValue' => null]
34 | */
35 | public function getDataPrototype(int $triggerId): array;
36 | }
37 |
--------------------------------------------------------------------------------
/src/ExpressionLanguage/RowDataCompiler.php:
--------------------------------------------------------------------------------
1 | compile($expression, $dataMapping);
34 |
35 | // evaluate in callback
36 | return
37 | static fn ($rowData): bool => true === @eval('return '.$compiledExpression.';');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Resources/public/img/running.svg:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/src/Resources/public/img/waiting.svg:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/AddComponentsCompilerPass.php:
--------------------------------------------------------------------------------
1 | container = $container;
26 | $this->componentManager = $container->getDefinition('eblick_contao_trigger.component.component_manager');
27 |
28 | $this->addToManager('eblick_contao_trigger.condition', 'addCondition');
29 | $this->addToManager('eblick_contao_trigger.action', 'addAction');
30 | }
31 |
32 | private function addToManager(string $tagName, string $method): void
33 | {
34 | foreach ($this->container->findTaggedServiceIds($tagName) as $id => $tags) {
35 | foreach ($tags as $attributes) {
36 | $this->componentManager->addMethodCall($method, [new Reference($id), $attributes['alias']]);
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Execution/ExecutionLog.php:
--------------------------------------------------------------------------------
1 | connection->executeQuery($query, $params)->fetchAllAssociative() as $result) {
34 | $logs[$result['originId']] = $result;
35 | }
36 |
37 | return $logs;
38 | }
39 |
40 | public function addLog(int $triggerId, int $originId, string $origin, bool $simulated): void
41 | {
42 | if (!$origin) {
43 | throw new \InvalidArgumentException(sprintf('Origin can\'t be empty in trigger %s!', $triggerId));
44 | }
45 |
46 | $this->connection
47 | ->executeQuery(
48 | 'INSERT INTO tl_eblick_trigger_log SET pid=?, tstamp=?, originId=?, origin=?, simulated=?',
49 | [$triggerId, time(), $originId, $origin, $simulated]
50 | )
51 | ;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Resources/public/img/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/Resources/public/img/paused.svg:
--------------------------------------------------------------------------------
1 |
2 |
13 |
--------------------------------------------------------------------------------
/src/Resources/public/img/automation.svg:
--------------------------------------------------------------------------------
1 |
2 |
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | contao-trigger
2 | ==============
3 | This bundle adds an extensible **condition ⇒ action** framework to
4 | Contao OpenSource CMS. The condition checking is processed on a regular
5 | basis via a cron job. If one or more actions are executed a respective
6 | entry gets created in the trigger log.
7 |
8 | 
9 |
10 | Components
11 | ----------
12 |
13 | By default the following components are available:
14 |
15 | - **Conditions**
16 | - **Table Records**: Executes an action at most once for each of a
17 | selected table's rows if:
18 |
19 | *A)* a *custom expression* based on the table's columns is met
20 | > category == 'things' and sum_total - coupon 2 > 100
21 |
22 | *B)* a field containing datetime information matches a given
23 | *time constraint* (e.g. 7 days later / 15 minutes before). When
24 | using the latter, the execution time can be overwritten (e.g. 3
25 | days in advance, but at 6pm).
26 |
27 | - **Point in Time**: Executes an action as soon as a given point in
28 | time is reached. This allows basic scheduling.
29 |
30 | - **Actions**
31 | - **Notification Action**: Allows to send a custom notification via
32 | `terminal42\notification-center` (must be installed individually).
33 | The available simple tokens are based on the selected condition
34 | and are displayed in the backend.
35 |
36 |
37 |
38 | Installation
39 | ------------
40 | - [Installation / Setup](src/Resources/docs/installing.md)
41 |
42 | Extending the framework
43 | -----------------------
44 | - [Adding conditions & actions](src/Resources/docs/extending.md)
--------------------------------------------------------------------------------
/tests/EventListener/TriggerListenerTest.php:
--------------------------------------------------------------------------------
1 | createMock(Result::class);
27 | $result
28 | ->expects(self::once())
29 | ->method('fetchAllAssociative')
30 | ->willReturn([])
31 | ;
32 |
33 | $connection = $this->createMock(Connection::class);
34 | $connection
35 | ->expects(self::once())
36 | ->method('executeQuery')
37 | ->with('SELECT * FROM tl_eblick_trigger WHERE enabled = 1 && error IS NULL')
38 | ->willReturn($result)
39 | ;
40 |
41 | $listener = new TriggerListener(
42 | $this->createMock(ComponentManager::class),
43 | $connection,
44 | $this->createMock(LoggerInterface::class),
45 | $this->createMock(ExecutionContextFactory::class),
46 | $this->createMock(RequestStack::class)
47 | );
48 |
49 | \define('TL_MODE', 'FE');
50 | $listener->onExecute();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Resources/contao/config/config.php:
--------------------------------------------------------------------------------
1 | [
19 | 'eblick_trigger' => [
20 | 'tables' => ['tl_eblick_trigger', 'tl_eblick_trigger_log'],
21 | 'execute' => ['eblick_contao_trigger.listener.datacontainer.trigger', 'onExecute'],
22 | 'simulate' => ['eblick_contao_trigger.listener.datacontainer.trigger', 'onSimulate'],
23 | 'reset' => ['eblick_contao_trigger.listener.datacontainer.trigger', 'onReset'],
24 | ],
25 | ],
26 | ]
27 | );
28 |
29 | // Generic Notification Center action
30 | $GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE']['eblick_trigger'] = [
31 | // Type
32 | 'eblick_notification_action' => [
33 | 'recipients' => ['admin_email', 'data_*'],
34 | 'email_subject' => ['data_*', 'trigger_id', 'trigger_title', 'trigger_startTime', 'admin_email'],
35 | 'email_text' => ['data_*', 'trigger_id', 'trigger_title', 'trigger_startTime', 'admin_email'],
36 | 'email_html' => ['data_*', 'trigger_id', 'trigger_title', 'trigger_startTime', 'admin_email'],
37 | 'email_sender_name' => ['data_*', 'admin_email'],
38 | 'email_sender_address' => ['data_*', 'admin_email'],
39 | 'email_recipient_cc' => ['data_*', 'admin_email'],
40 | 'email_recipient_bcc' => ['data_*', 'admin_email'],
41 | 'email_replyTo' => ['data_*', 'admin_email'],
42 | ],
43 | ];
44 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/EBlickContaoTriggerExtensionTest.php:
--------------------------------------------------------------------------------
1 | load([], $container);
25 |
26 | $definitions = [
27 | 'eblick_contao_trigger.execution.row_data_compiler',
28 | 'eblick_contao_trigger.execution.log',
29 | 'eblick_contao_trigger.execution.context_factory',
30 | 'eblick_contao_trigger.component.component_manager',
31 |
32 | 'eblick_contao_trigger.component.table_condition',
33 | 'eblick_contao_trigger.component.notification_action',
34 |
35 | 'eblick_contao_trigger.listener.trigger',
36 |
37 | 'eblick_contao_trigger.listener.datacontainer.trigger',
38 | 'eblick_contao_trigger.listener.datacontainer.trigger_log',
39 | 'eblick_contao_trigger.listener.datacontainer.table_condition',
40 | 'eblick_contao_trigger.listener.datacontainer.notification_action',
41 | ];
42 |
43 | foreach ($definitions as $definition) {
44 | self::assertTrue($container->hasDefinition($definition));
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/ExpressionLanguage/RowDataCompilerTest.php:
--------------------------------------------------------------------------------
1 | compileRowExpression($expression, $columnNames);
27 |
28 | self::assertFalse($closure(['a' => 1, 'b' => 2, 'c' => 3])); // 1 + 2 == 5 - 3
29 | self::assertTrue($closure(['a' => 1, 'b' => 2, 'c' => 2])); // 1 + 2 == 5 - 2
30 | }
31 |
32 | public function testCompileRowExpressionWithSyntaxError(): void
33 | {
34 | $compiler = new RowDataCompiler();
35 |
36 | $expression = 'a + b =x= (5 - c)';
37 | $columnNames = ['a', 'b', 'c'];
38 |
39 | $this->expectException(SyntaxError::class);
40 | $compiler->compileRowExpression($expression, $columnNames);
41 | }
42 |
43 | public function testCompileRowExpressionWithMissingColumnNames(): void
44 | {
45 | $compiler = new RowDataCompiler();
46 |
47 | $expression = 'a + b == (5 - c)';
48 | $columnNames = ['a', 'b'];
49 |
50 | $this->expectException(SyntaxError::class);
51 | $compiler->compileRowExpression($expression, $columnNames);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Resources/config/services.yml:
--------------------------------------------------------------------------------
1 | services:
2 | _instanceof:
3 | Contao\CoreBundle\Framework\FrameworkAwareInterface:
4 | calls:
5 | - ['setFramework', ['@contao.framework']]
6 |
7 | Symfony\Component\DependencyInjection\ContainerAwareInterface:
8 | calls:
9 | - ['setContainer', ['@service_container']]
10 |
11 | # execution environment
12 | eblick_contao_trigger.execution.row_data_compiler:
13 | class: EBlick\ContaoTrigger\ExpressionLanguage\RowDataCompiler
14 |
15 | eblick_contao_trigger.execution.log:
16 | class: EBlick\ContaoTrigger\Execution\ExecutionLog
17 | arguments:
18 | - '@database_connection'
19 |
20 | eblick_contao_trigger.execution.context_factory:
21 | class: EBlick\ContaoTrigger\Execution\ExecutionContextFactory
22 | arguments:
23 | - '@eblick_contao_trigger.execution.log'
24 |
25 | # condition and action components
26 | eblick_contao_trigger.component.component_manager:
27 | class: EBlick\ContaoTrigger\Component\ComponentManager
28 |
29 | eblick_contao_trigger.component.table_condition:
30 | class: EBlick\ContaoTrigger\Component\Condition\TableCondition
31 | arguments:
32 | - '@database_connection'
33 | - '@eblick_contao_trigger.execution.row_data_compiler'
34 | - '@contao.framework'
35 | tags:
36 | - { name: eblick_contao_trigger.condition, alias: table }
37 |
38 | eblick_contao_trigger.component.time_condition:
39 | class: EBlick\ContaoTrigger\Component\Condition\TimeCondition
40 | arguments:
41 | - '@database_connection'
42 | tags:
43 | - { name: eblick_contao_trigger.condition, alias: time }
44 |
45 | eblick_contao_trigger.component.notification_action:
46 | class: EBlick\ContaoTrigger\Component\Action\NotificationAction
47 | arguments:
48 | - '@contao.framework'
49 | tags:
50 | - { name: eblick_contao_trigger.action, alias: notification }
51 |
--------------------------------------------------------------------------------
/tests/EventListener/DataContainer/TableConditionTest.php:
--------------------------------------------------------------------------------
1 | createMock(Table::class);
26 | $table1
27 | ->expects(self::once())
28 | ->method('getName')
29 | ->willReturn('tl_eblick_trigger')
30 | ;
31 |
32 | $table2 = $this->createMock(Table::class);
33 | $table2
34 | ->expects(self::once())
35 | ->method('getName')
36 | ->willReturn('testTable2')
37 | ;
38 |
39 | $schemaManager = $this->createMock(AbstractSchemaManager::class);
40 | $schemaManager
41 | ->expects(self::once())
42 | ->method('listTables')
43 | ->willReturn([$table1, $table2])
44 | ;
45 |
46 | $connection = $this->createMock(Connection::class);
47 | $connection
48 | ->expects(self::once())
49 | ->method('createSchemaManager')
50 | ->willReturn($schemaManager)
51 | ;
52 |
53 | $condition = new TableCondition(
54 | $connection,
55 | $this->createMock(RowDataCompiler::class),
56 | $this->createMock(ContaoFramework::class)
57 | );
58 |
59 | self::assertEquals(['testTable2' => 'testTable2'], $condition->onGetTables());
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Component/ComponentManager.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | private array $conditions = [];
22 |
23 | /**
24 | * @var array
25 | */
26 | private array $actions = [];
27 |
28 | /**
29 | * Register a condition.
30 | */
31 | public function addCondition(ConditionInterface $condition, string $alias): void
32 | {
33 | $this->conditions[$alias] = $condition;
34 | }
35 |
36 | /**
37 | * Register an action.
38 | */
39 | public function addAction(ActionInterface $action, string $name): void
40 | {
41 | $this->actions[$name] = $action;
42 | }
43 |
44 | /**
45 | * Returns an array of all registered condition names.
46 | *
47 | * @return list
48 | */
49 | public function getConditionNames(): array
50 | {
51 | return array_keys($this->conditions);
52 | }
53 |
54 | /**
55 | * Get a certain condition.
56 | */
57 | public function getCondition(string $name): ConditionInterface|null
58 | {
59 | return $this->conditions[$name] ?? null;
60 | }
61 |
62 | /**
63 | * Returns an array of all registered action names.
64 | *
65 | * @return list
66 | */
67 | public function getActionNames(): array
68 | {
69 | return array_keys($this->actions);
70 | }
71 |
72 | /**
73 | * Get a certain action.
74 | */
75 | public function getAction(string $name): ActionInterface|null
76 | {
77 | return $this->actions[$name] ?? null;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Resources/public/css/backend.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Trigger Framework Bundle for Contao Open Source CMS
3 | *
4 | * @copyright Copyright (c) 2018, eBlick Medienberatung
5 | * @license LGPL-3.0+
6 | * @link https://github.com/eBlick/contao-trigger
7 | *
8 | * @author Moritz Vondano
9 | */
10 |
11 | #tl_navigation .group-automation {
12 | background: url(../img/automation.svg) no-repeat 4px 1px;
13 | background-size: 15px 15px;
14 | }
15 |
16 | .w25 {
17 | width: calc(25% - 30px);
18 | float: left;
19 | height: 80px;
20 | min-width: 100px;
21 | }
22 |
23 | .w16 {
24 | width: calc(16.66% - 31px);
25 | float: left;
26 | height: 80px;
27 | min-width: 100px;
28 | }
29 |
30 | .wizard.w25 .tl_text, .wizard.w25 .tl_select,
31 | .wizard.w16 .tl_text, .wizard.w16 .tl_select {
32 | max-width: calc(100% - 24px);
33 | }
34 |
35 | .trigger-error {
36 | color: #cc3333;
37 | }
38 |
39 | .trigger-error span {
40 | display: block;
41 | background-color: #ffeded;
42 | margin: 5px;
43 | padding: 10px;
44 | border-radius: 5px;
45 | }
46 |
47 | .trigger-list .title {
48 | display: block;
49 | font-weight: bold;
50 | margin-bottom: 8px;
51 | }
52 |
53 | .trigger-list .icon {
54 | float: left;
55 | width: 25px;
56 | height: 25px;
57 | margin-right: 6px;
58 | background-size: 100%;
59 | }
60 |
61 | .trigger-list.trigger-state-paused .icon {
62 | background-image: url(../img/paused.svg);
63 | }
64 |
65 | .trigger-list.trigger-state-waiting .icon {
66 | background-image: url(../img/waiting.svg);
67 | }
68 |
69 | .trigger-list.trigger-state-running .icon {
70 | background-image: url(../img/running.svg);
71 | }
72 |
73 | .trigger-list.trigger-state-error .icon {
74 | background-image: url(../img/error.svg);
75 | }
76 |
77 | .trigger-list.trigger-state-error .title {
78 | color: #cc3333;
79 | }
80 |
81 | .trigger-list .type {
82 | margin-bottom: 4px;
83 | }
84 |
85 | .trigger-list .type span, .trigger-simulated {
86 | color: #999;
87 | }
88 |
--------------------------------------------------------------------------------
/src/Execution/ExecutionContext.php:
--------------------------------------------------------------------------------
1 | triggerParameters;
28 | }
29 |
30 | /**
31 | * Returns the start time of execution as a timestamp.
32 | */
33 | public function getStartTime(): int
34 | {
35 | return $this->startTime;
36 | }
37 |
38 | /**
39 | * Returns an array of log entries associated with this trigger with keys being the origin ids and values a
40 | * parameter object of all columns.
41 | */
42 | public function getLog(string $origin = 'tl_eblick_trigger'): array
43 | {
44 | return $this->executionLog->getLog((int) $this->triggerParameters->id, $origin);
45 | }
46 |
47 | /**
48 | * Returns an array of log entries associated with this trigger with keys being the origin ids and values a
49 | * parameter object of all columns.
50 | */
51 | public function getAllLogs(): array
52 | {
53 | return $this->executionLog->getLog((int) $this->triggerParameters->id);
54 | }
55 |
56 | /**
57 | * Adds a new log entry for this trigger.
58 | */
59 | public function addLog(int $originId, string $origin = 'tl_eblick_trigger', bool $simulated = false): void
60 | {
61 | $this->executionLog->addLog((int) $this->triggerParameters->id, $originId, $origin, $simulated);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Resources/config/listener.yml:
--------------------------------------------------------------------------------
1 | services:
2 | # trigger execution
3 | eblick_contao_trigger.listener.trigger:
4 | class: 'EBlick\ContaoTrigger\EventListener\TriggerListener'
5 | arguments:
6 | - '@eblick_contao_trigger.component.component_manager'
7 | - '@database_connection'
8 | - '@logger'
9 | - '@eblick_contao_trigger.execution.context_factory'
10 | - '@request_stack'
11 | public: true
12 | tags:
13 | - { name: 'contao.cronjob', interval: 'minutely', method: 'onExecute'}
14 |
15 | # data container
16 | eblick_contao_trigger.listener.datacontainer.common:
17 | class: 'EBlick\ContaoTrigger\EventListener\DataContainer\Common'
18 | tags:
19 | - { name: 'contao.callback', table: 'tl_eblick_trigger', target: 'config.onload', method: 'addBackendCss'}
20 | - { name: 'contao.callback', table: 'tl_eblick_trigger_log', target: 'config.onload', method: 'addBackendCss'}
21 |
22 | eblick_contao_trigger.listener.datacontainer.trigger:
23 | class: 'EBlick\ContaoTrigger\EventListener\DataContainer\Trigger'
24 | arguments:
25 | - '@eblick_contao_trigger.component.component_manager'
26 | - '@database_connection'
27 | - '@eblick_contao_trigger.listener.trigger'
28 | - '@contao.framework'
29 | tags:
30 | - { name: 'contao.hook', hook: 'loadDataContainer', method: 'onImportDefinitions'}
31 | public: true
32 |
33 | eblick_contao_trigger.listener.datacontainer.trigger_log:
34 | class: 'EBlick\ContaoTrigger\EventListener\DataContainer\ExecutionLog'
35 | public: true
36 |
37 | eblick_contao_trigger.listener.datacontainer.table_condition:
38 | class: 'EBlick\ContaoTrigger\EventListener\DataContainer\TableCondition'
39 | arguments:
40 | - '@database_connection'
41 | - '@eblick_contao_trigger.execution.row_data_compiler'
42 | - '@contao.framework'
43 | public: true
44 |
45 | eblick_contao_trigger.listener.datacontainer.notification_action:
46 | class: 'EBlick\ContaoTrigger\EventListener\DataContainer\NotificationAction'
47 | arguments:
48 | - '@eblick_contao_trigger.component.component_manager'
49 | - '@database_connection'
50 | public: true
51 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eblick/contao-trigger",
3 | "type": "contao-bundle",
4 | "description": "Time and condition based trigger framework for notifications and other things inside Contao Open Source CMS",
5 | "keywords": [
6 | "contao",
7 | "trigger",
8 | "time",
9 | "table",
10 | "automation",
11 | "notifications"
12 | ],
13 | "license": "LGPL-3.0",
14 | "authors": [
15 | {
16 | "name": "eBlick Medienberatung",
17 | "homepage": "https://eblick-medienberatung.de"
18 | },
19 | {
20 | "name": "Moritz Vondano",
21 | "homepage": "https://github.com/m-vo",
22 | "role": "developer"
23 | }
24 | ],
25 | "require": {
26 | "php": ">=8.1",
27 | "contao/core-bundle": "^4.13 || ^5.1",
28 | "symfony/expression-language": "^5.4 || ^6.0",
29 | "symfony/stopwatch": "^5.4 || ^6.0",
30 | "symfony/lock": "^5.4 || ^6.0",
31 | "doctrine/dbal": "^3.3"
32 | },
33 | "require-dev": {
34 | "bamarni/composer-bin-plugin": "^1.4",
35 | "contao/manager-plugin": "^2.0",
36 | "terminal42/notification_center": "^1.7",
37 | "phpunit/phpunit": "^8.4",
38 | "contao/test-case": "^4.2"
39 | },
40 | "suggest": {
41 | "terminal42/notification_center": "^1.7"
42 | },
43 | "autoload": {
44 | "psr-4": {
45 | "EBlick\\ContaoTrigger\\": "src/",
46 | "EBlick\\ContaoTrigger\\Test\\": "tests/"
47 | }
48 | },
49 | "extra": {
50 | "contao-manager-plugin": "EBlick\\ContaoTrigger\\ContaoManager\\Plugin",
51 | "bamarni-bin": {
52 | "bin-links": false,
53 | "target-directory": "tools"
54 | }
55 | },
56 | "scripts": {
57 | "cs": [
58 | "tools/ecs/vendor/bin/ecs check src tests --config tools/ecs/config/default.php --fix --ansi"
59 | ],
60 | "tests": [
61 | "vendor/bin/phpunit --colors=always"
62 | ]
63 | },
64 | "config": {
65 | "allow-plugins": {
66 | "bamarni/composer-bin-plugin": true,
67 | "contao-components/installer": true,
68 | "contao/manager-plugin": true,
69 | "contao-community-alliance/composer-plugin": true,
70 | "php-http/discovery": false
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/Component/ComponentManagerTest.php:
--------------------------------------------------------------------------------
1 | createMock(ConditionInterface::class);
25 | $manager->addCondition($condition, 'testCondition');
26 |
27 | self::assertEquals($condition, $manager->getCondition('testCondition'));
28 | }
29 |
30 | public function testAddAndGetAction(): void
31 | {
32 | $manager = new ComponentManager();
33 |
34 | $action = $this->createMock(ActionInterface::class);
35 | $manager->addAction($action, 'testAction');
36 |
37 | self::assertEquals($action, $manager->getAction('testAction'));
38 | }
39 |
40 | public function testGetConditionNames(): void
41 | {
42 | $manager = new ComponentManager();
43 |
44 | $condition1 = $this->createMock(ConditionInterface::class);
45 | $condition2 = $this->createMock(ConditionInterface::class);
46 | $manager->addCondition($condition1, 'testCondition1');
47 | $manager->addCondition($condition2, 'testCondition2');
48 |
49 | self::assertEquals(['testCondition1', 'testCondition2'], $manager->getConditionNames());
50 | }
51 |
52 | public function testGetActionNames(): void
53 | {
54 | $manager = new ComponentManager();
55 |
56 | $action1 = $this->createMock(ActionInterface::class);
57 | $action2 = $this->createMock(ActionInterface::class);
58 | $manager->addAction($action1, 'testAction1');
59 | $manager->addAction($action2, 'testAction2');
60 |
61 | self::assertEquals(['testAction1', 'testAction2'], $manager->getActionNames());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/EventListener/DataContainer/NotificationAction.php:
--------------------------------------------------------------------------------
1 | connection
31 | ->executeQuery('SELECT condition_type FROM tl_eblick_trigger WHERE id = ?', [$dc->id])
32 | ->fetchOne()
33 | ;
34 |
35 | if ($conditionType && $condition = $this->componentManager->getCondition($conditionType)) {
36 | $tokens .= ', '.implode(
37 | ', ',
38 | array_map(
39 | static fn ($v) => '##data_'.$v.'##',
40 | array_keys($condition->getDataPrototype((int) $dc->id))
41 | )
42 | );
43 | }
44 |
45 | return sprintf(
46 | '%s
%s
',
47 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action_notification_tokens'],
48 | $tokens
49 | );
50 | }
51 |
52 | public function getNotificationChoices(): array
53 | {
54 | if (!class_exists(Notification::class)) {
55 | return [];
56 | }
57 |
58 | return $this->connection
59 | ->executeQuery(
60 | "SELECT id, title FROM tl_nc_notification WHERE type='eblick_notification_action' ORDER BY title"
61 | )
62 | ->fetchAllKeyValue()
63 | ;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/Compiler/AddComponentsCompilerPassTest.php:
--------------------------------------------------------------------------------
1 | createMock(Definition::class);
25 | $componentManager
26 | ->expects(self::exactly(2))
27 | ->method('addMethodCall')
28 | ->withConsecutive(
29 | ['addCondition', [new Reference('testTag1'), 'testAlias1']],
30 | ['addAction', [new Reference('testTag2'), 'testAlias2']]
31 | )
32 | ;
33 |
34 | $container = $this->createMock(ContainerBuilder::class);
35 | $container
36 | ->expects(self::once())
37 | ->method('getDefinition')
38 | ->with('eblick_contao_trigger.component.component_manager')
39 | ->willReturn($componentManager)
40 | ;
41 |
42 | $container
43 | ->expects(self::exactly(2))
44 | ->method('findTaggedServiceIds')
45 | ->withConsecutive(['eblick_contao_trigger.condition'], ['eblick_contao_trigger.action'])
46 | ->willReturnOnConsecutiveCalls(
47 | ['testTag1' => [['alias' => 'testAlias1']]],
48 | ['testTag2' => [['alias' => 'testAlias2']]]
49 | )
50 | ;
51 |
52 | $compilerPass = new AddComponentsCompilerPass();
53 | $compilerPass->process($container);
54 |
55 | self::assertTrue(method_exists(ComponentManager::class, 'addCondition'));
56 | self::assertTrue(method_exists(ComponentManager::class, 'addAction'));
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Component/Condition/TimeCondition.php:
--------------------------------------------------------------------------------
1 | getLog())) {
28 | return;
29 | }
30 |
31 | $trigger = $context->getParameters();
32 |
33 | $execute = $this->connection
34 | ->executeQuery(
35 | 'SELECT cnd_time_executionTime <> 0 && NOW() >= FROM_UNIXTIME(cnd_time_executionTime) FROM tl_eblick_trigger WHERE id=?',
36 | [$trigger->id]
37 | )
38 | ->fetchOne()
39 | ;
40 |
41 | if ($execute) {
42 | $fireCallback(
43 | ['selectedTime' => $trigger->cnd_time_executionTime]
44 | );
45 | }
46 | }
47 |
48 | public function getDataPrototype(int $triggerId): array
49 | {
50 | return array_fill_keys(['selectedTime'], null);
51 | }
52 |
53 | public function getDataContainerDefinition(): Definition
54 | {
55 | $palette = 'cnd_time_executionTime';
56 |
57 | $fields = [
58 | 'cnd_time_executionTime' => [
59 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_time_executionTime'],
60 | 'exclude' => true,
61 | 'inputType' => 'text',
62 | 'eval' => [
63 | 'mandatory' => true,
64 | 'rgxp' => 'datim',
65 | 'datepicker' => true,
66 | 'tl_class' => 'w25 wizard',
67 | ],
68 | 'sql' => 'INT(10) NULL',
69 | ],
70 | ];
71 |
72 | return new Definition($fields, $palette);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/Execution/ExecutionContextTest.php:
--------------------------------------------------------------------------------
1 | expectException(\InvalidArgumentException::class);
22 | new ExecutionContext(
23 | new \stdClass(),
24 | 1234,
25 | $this->createMock(ExecutionLog::class)
26 | );
27 | }
28 |
29 | public function testInstantiationWithId(): void
30 | {
31 | $parameters = new \stdClass();
32 | $parameters->id = 12;
33 |
34 | $obj = new ExecutionContext(
35 | $parameters,
36 | 1234,
37 | $this->createMock(ExecutionLog::class)
38 | );
39 | self::assertInstanceOf(ExecutionContext::class, $obj);
40 | }
41 |
42 | public function testGetParametersAndStartTime(): void
43 | {
44 | $parameters = new \stdClass();
45 | $parameters->id = 12;
46 | $parameters->someValue = 'meow';
47 |
48 | $context = new ExecutionContext(
49 | $parameters,
50 | 12345,
51 | $this->createMock(ExecutionLog::class)
52 | );
53 |
54 | self::assertEquals($context->getParameters()->id, 12);
55 | self::assertEquals($context->getParameters()->someValue, 'meow');
56 | self::assertEquals($context->getStartTime(), 12345);
57 | }
58 |
59 | public function testGetLog(): void
60 | {
61 | $parameters = new \stdClass();
62 | $parameters->id = 5;
63 |
64 | $data = [2 => [['id' => 4, 'pid' => 5, 'tstamp' => 1234, 'origin' => 'tl_someTable']]];
65 | $log = $this->createMock(ExecutionLog::class);
66 | $log
67 | ->expects(self::once())
68 | ->method('getLog')
69 | ->with(5, 'tl_someTable')
70 | ->willReturn($data)
71 | ;
72 |
73 | $context = new ExecutionContext($parameters, 15000, $log);
74 | self::assertEquals($data, $context->getLog('tl_someTable'));
75 | }
76 |
77 | public function testAddLog(): void
78 | {
79 | $parameters = new \stdClass();
80 | $parameters->id = 5;
81 |
82 | $log = $this->createMock(ExecutionLog::class);
83 | $log
84 | ->expects(self::once())
85 | ->method('addLog')
86 | ->with(5, 61, 'tl_someTable', false)
87 | ;
88 |
89 | $context = new ExecutionContext($parameters, 15000, $log);
90 | $context->addLog(61, 'tl_someTable', false);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Resources/contao/dca/tl_eblick_trigger_log.php:
--------------------------------------------------------------------------------
1 | [
15 | 'dataContainer' => 'Table',
16 | 'ptable' => 'tl_eblick_trigger',
17 | 'enableVersioning' => false,
18 | 'notEditable' => true,
19 | 'closed' => true,
20 | 'sql' => [
21 | 'keys' => [
22 | 'id' => 'primary',
23 | 'pid,origin,originId' => 'index',
24 | ],
25 | ],
26 | ],
27 |
28 | // List
29 | 'list' => [
30 | 'sorting' => [
31 | 'mode' => 1,
32 | 'fields' => ['tstamp'],
33 | 'flag' => 5,
34 | 'panelLayout' => 'limit',
35 | ],
36 | 'label' => [
37 | 'fields' => ['tstamp'],
38 | 'label_callback' => [
39 | 'eblick_contao_trigger.listener.datacontainer.trigger_log',
40 | 'onGenerateLabel',
41 | ],
42 | ],
43 | 'global_operations' => [],
44 | 'operations' => [
45 | 'show' => [
46 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger_log']['show'],
47 | 'href' => 'act=show',
48 | 'icon' => 'show.svg',
49 | ],
50 | 'delete' => [
51 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger_log']['delete'],
52 | 'href' => 'act=delete',
53 | 'icon' => 'delete.svg',
54 | 'attributes' => 'onclick="if(!confirm(\''
55 | .($GLOBALS['TL_LANG']['tl_eblick_trigger_log']['deleteConfirm'] ?? null)
56 | .'\'))return false;Backend.getScrollOffset()"',
57 | ],
58 | ],
59 | ],
60 |
61 | // Fields
62 | 'fields' => [
63 | 'id' => [
64 | 'sql' => 'int(10) unsigned NOT NULL auto_increment',
65 | ],
66 | 'pid' => [
67 | 'foreignKey' => 'tl_eblick_trigger.title',
68 | 'sql' => "int(10) unsigned NOT NULL default '0'",
69 | 'relation' => ['type' => 'belongsTo', 'load' => 'lazy'],
70 | ],
71 | 'tstamp' => [
72 | 'sql' => "int(10) unsigned NOT NULL default '0'",
73 | ],
74 | 'origin' => [
75 | 'sql' => "varchar(64) NOT NULL default 'tl_eblick_trigger'",
76 | ],
77 | 'originId' => [
78 | 'sql' => "int(10) unsigned NOT NULL default '0'",
79 | ],
80 | 'simulated' => [
81 | 'default' => false,
82 | 'inputType' => 'checkbox', // needed for details to show yes/no
83 | 'eval' => [
84 | 'isBoolean' => true,
85 | ],
86 | 'sql' => "char(1) NOT NULL default '0'",
87 | ],
88 | ],
89 | ];
90 |
--------------------------------------------------------------------------------
/tests/Component/Condition/TableConditionTest.php:
--------------------------------------------------------------------------------
1 | createMock(Connection::class),
28 | $this->createMock(RowDataCompiler::class)
29 | );
30 |
31 | $definition = $obj->getDataContainerDefinition();
32 | self::assertInstanceOf(Definition::class, $definition);
33 |
34 | self::assertCount(2, $definition->selectors);
35 | self::assertCount(2, $definition->subPalettes);
36 | self::assertCount(8, $definition->fields);
37 | self::assertSame('cnd_table_src,cnd_table_timed,cnd_table_expression', $definition->palette);
38 | }
39 |
40 | public function testGetDataPrototype(): void
41 | {
42 | $column1 = $this->createMock(Column::class);
43 | $column1
44 | ->expects(self::once())
45 | ->method('getName')
46 | ->willReturn('testCol1')
47 | ;
48 |
49 | $column2 = $this->createMock(Column::class);
50 | $column2
51 | ->expects(self::once())
52 | ->method('getName')
53 | ->willReturn('testCol2')
54 | ;
55 |
56 | $column3 = $this->createMock(Column::class);
57 | $column3
58 | ->expects(self::once())
59 | ->method('getName')
60 | ->willReturn('testCol3')
61 | ;
62 |
63 | $columns = [$column1, $column2, $column3];
64 |
65 | $schemaManager = $this->createMock(AbstractSchemaManager::class);
66 | $schemaManager
67 | ->expects(self::once())
68 | ->method('listTableColumns')
69 | ->with('testTable')
70 | ->willReturn($columns)
71 | ;
72 |
73 | $result = $this->createMock(Result::class);
74 | $result
75 | ->method('fetchOne')
76 | ->willReturn('testTable')
77 | ;
78 |
79 | $connection = $this->createMock(Connection::class);
80 | $connection
81 | ->expects(self::once())
82 | ->method('executeQuery')
83 | ->with('SELECT cnd_table_src FROM tl_eblick_trigger WHERE id = ?', [123])
84 | ->willReturn($result)
85 | ;
86 |
87 | $connection
88 | ->method('createSchemaManager')
89 | ->willReturn($schemaManager)
90 | ;
91 |
92 | $condition = new TableCondition(
93 | $connection,
94 | $this->createMock(RowDataCompiler::class)
95 | );
96 |
97 | $result = $condition->getDataPrototype(123);
98 |
99 | self::assertEquals($result, ['testCol1' => null, 'testCol2' => null, 'testCol3' => null]);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/tests/EventListener/DataContainer/NotificationActionTest.php:
--------------------------------------------------------------------------------
1 | createMock(Result::class);
28 | $result
29 | ->expects(self::once())
30 | ->method('fetchOne')
31 | ->willReturn('testCondition')
32 | ;
33 |
34 | $connection = $this->createMock(Connection::class);
35 | $connection
36 | ->expects(self::once())
37 | ->method('executeQuery')
38 | ->with('SELECT condition_type FROM tl_eblick_trigger WHERE id = ?', [9])
39 | ->willReturn($result)
40 | ;
41 |
42 | $condition = $this->createMock(ConditionInterface::class);
43 | $condition
44 | ->expects(self::once())
45 | ->method('getDataPrototype')
46 | ->with(9)
47 | ->willReturn(['testColumn1' => null, 'testColumn2' => null])
48 | ;
49 |
50 | $componentManager = $this->createMock(ComponentManager::class);
51 | $componentManager
52 | ->expects(self::once())
53 | ->method('getCondition')
54 | ->with('testCondition')
55 | ->willReturn($condition)
56 | ;
57 |
58 | $action = new NotificationAction($componentManager, $connection);
59 |
60 | $dc = $this->createMock(DataContainer::class);
61 | $dc
62 | ->method('__get')
63 | ->with('id')
64 | ->willReturn(9)
65 | ;
66 |
67 | self::assertStringContainsString(
68 | '##trigger_id##, ##trigger_title##, ##trigger_startTime##, ##data_testColumn1##, ##data_testColumn2##',
69 | $action->onGetTokenList($dc)
70 | );
71 | }
72 |
73 | public function testGetNotificationChoices(): void
74 | {
75 | $data = [4 => 'firstTitle', 63 => 'secondTitle'];
76 |
77 | $result = $this->createMock(Result::class);
78 | $result
79 | ->expects(self::once())
80 | ->method('fetchAllKeyValue')
81 | ->willReturn($data)
82 | ;
83 |
84 | $connection = $this->createMock(Connection::class);
85 | $connection
86 | ->expects(self::once())
87 | ->method('executeQuery')
88 | ->with("SELECT id, title FROM tl_nc_notification WHERE type='eblick_notification_action' ORDER BY title")
89 | ->willReturn($result)
90 | ;
91 |
92 | $action = new NotificationAction(
93 | $this->createMock(ComponentManager::class),
94 | $connection
95 | );
96 |
97 | self::assertEquals($data, $action->getNotificationChoices());
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Component/Action/NotificationAction.php:
--------------------------------------------------------------------------------
1 | framework->initialize();
30 |
31 | /** @var Model $notificationModel */
32 | $notificationModel = $this->framework->getAdapter(Notification::class);
33 |
34 | if (!class_exists(Notification::class)) {
35 | throw new ExecutionException('Notification Center not found! This extension is needed in order to run this trigger.');
36 | }
37 |
38 | $trigger = $context->getParameters();
39 |
40 | /** @noinspection StaticInvocationViaThisInspection */
41 | $objNotification = $notificationModel->findByPk($trigger->act_notification_entity);
42 |
43 | if (null !== $objNotification) {
44 | $processed = array_merge(
45 | [
46 | 'trigger_id' => $trigger->id,
47 | 'trigger_title' => $trigger->title,
48 | 'trigger_startTime' => $context->getStartTime(),
49 | ],
50 | $this->prepareData($data)
51 | );
52 | /** @var Notification $objNotification */
53 | $objNotification->send($processed);
54 |
55 | return true;
56 | }
57 |
58 | return false;
59 | }
60 |
61 | public function getDataContainerDefinition(): Definition
62 | {
63 | $palette = 'act_notification_entity,act_notification_tokenList';
64 |
65 | $fields = [
66 | 'act_notification_entity' => [
67 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['action_notification_entity'],
68 | 'exclude' => true,
69 | 'inputType' => 'select',
70 | 'options_callback' => [
71 | 'eblick_contao_trigger.listener.datacontainer.notification_action',
72 | 'getNotificationChoices',
73 | ],
74 | 'eval' => [
75 | 'mandatory' => true,
76 | 'includeBlankOption' => true,
77 | 'chosen' => true,
78 | 'tl_class' => 'w50',
79 | ],
80 | 'sql' => "int(10) unsigned NOT NULL default '0'",
81 | ],
82 | 'act_notification_tokenList' => [
83 | 'exclude' => true,
84 | 'input_field_callback' => [
85 | 'eblick_contao_trigger.listener.datacontainer.notification_action',
86 | 'onGetTokenList',
87 | ],
88 | ],
89 | ];
90 |
91 | return new Definition($fields, $palette);
92 | }
93 |
94 | /**
95 | * Prefix keys with 'data_'.
96 | */
97 | private function prepareData(array $rawData): array
98 | {
99 | $data = [];
100 |
101 | foreach ($rawData as $k => $v) {
102 | $data['data_'.$k] = $v;
103 | }
104 |
105 | return $data;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/Component/Action/NotificationActionTest.php:
--------------------------------------------------------------------------------
1 | id = 6;
28 | $parameters->title = 'testTrigger1';
29 | $parameters->act_notification_entity = 24;
30 |
31 | $context = new ExecutionContext(
32 | $parameters,
33 | 159800,
34 | $this->createMock(ExecutionLog::class)
35 | );
36 |
37 | $data = [];
38 |
39 | $preparedData = [
40 | 'trigger_id' => 6,
41 | 'trigger_title' => 'testTrigger1',
42 | 'trigger_startTime' => 159800,
43 | ];
44 |
45 | $action = $this->getMockedAction($preparedData, 24);
46 | self::assertTrue($action->fire($context, $data));
47 | }
48 |
49 | public function testFireWithCustomData(): void
50 | {
51 | $parameters = new \stdClass();
52 |
53 | $parameters->id = 11;
54 | $parameters->title = 'testTrigger2';
55 | $parameters->act_notification_entity = 24;
56 |
57 | $context = new ExecutionContext(
58 | $parameters,
59 | 123456,
60 | $this->createMock(ExecutionLog::class)
61 | );
62 |
63 | $data = [
64 | 'some_value' => 4,
65 | 'other_value' => 'yes',
66 | ];
67 |
68 | $preparedData = [
69 | 'trigger_id' => 11,
70 | 'trigger_title' => 'testTrigger2',
71 | 'trigger_startTime' => 123456,
72 | 'data_some_value' => 4,
73 | 'data_other_value' => 'yes',
74 | ];
75 |
76 | $action = $this->getMockedAction($preparedData, 24);
77 | self::assertTrue($action->fire($context, $data));
78 | }
79 |
80 | public function testGetDataContainerDefinition(): void
81 | {
82 | $obj = new NotificationAction($this->createMock(ContaoFramework::class));
83 |
84 | $definition = $obj->getDataContainerDefinition();
85 | self::assertInstanceOf(Definition::class, $definition);
86 |
87 | self::assertCount(0, $definition->selectors);
88 | self::assertCount(0, $definition->subPalettes);
89 | self::assertCount(2, $definition->fields);
90 | self::assertSame('act_notification_entity,act_notification_tokenList', $definition->palette);
91 | }
92 |
93 | private function getMockedAction($preparedData, $notificationId): NotificationAction
94 | {
95 | $notification = $this->mockAdapter(['send']);
96 | $notification
97 | ->expects(self::once())
98 | ->method('send')
99 | ->with($preparedData)
100 | ->willReturn(true)
101 | ;
102 |
103 | $notificationAdapter = $this->mockAdapter(['findByPk']);
104 | $notificationAdapter
105 | ->expects(self::once())
106 | ->method('findByPk')
107 | ->with($notificationId)
108 | ->willReturn($notification)
109 | ;
110 |
111 | $framework = $this->mockContaoFramework([Notification::class => $notificationAdapter]);
112 |
113 | return new NotificationAction($framework);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/tests/Execution/ExecutionLogTest.php:
--------------------------------------------------------------------------------
1 | createMock(Result::class);
23 | $result
24 | ->expects(self::once())
25 | ->method('fetchAllAssociative')
26 | ->willReturn([
27 | ['id' => 1, 'pid' => 12, 'tstamp' => 1234, 'origin' => 'tl_someTable', 'originId' => 4, 'simulated' => ''],
28 | ['id' => 2, 'pid' => 12, 'tstamp' => 2345, 'origin' => 'tl_otherTable', 'originId' => 7, 'simulated' => ''],
29 | ])
30 | ;
31 |
32 | $connection = $this->createMock(Connection::class);
33 | $connection
34 | ->expects(self::once())
35 | ->method('executeQuery')
36 | ->with('SELECT * FROM tl_eblick_trigger_log WHERE pid=?', [12])
37 | ->willReturn($result)
38 | ;
39 |
40 | $log = new ExecutionLog($connection);
41 |
42 | $expected = [
43 | 4 => ['id' => 1, 'pid' => 12, 'tstamp' => 1234, 'origin' => 'tl_someTable', 'originId' => 4, 'simulated' => ''],
44 | 7 => ['id' => 2, 'pid' => 12, 'tstamp' => 2345, 'origin' => 'tl_otherTable', 'originId' => 7, 'simulated' => ''],
45 | ];
46 |
47 | self::assertEquals($expected, $log->getLog(12));
48 | }
49 |
50 | public function testGetLogWithOrigin(): void
51 | {
52 | $result = $this->createMock(Result::class);
53 | $result
54 | ->expects(self::once())
55 | ->method('fetchAllAssociative')
56 | ->willReturn([
57 | ['id' => 1, 'pid' => 12, 'tstamp' => 1234, 'origin' => 'tl_someTable', 'originId' => 4, 'simulated' => ''],
58 | ['id' => 2, 'pid' => 12, 'tstamp' => 2345, 'origin' => 'tl_someTable', 'originId' => 7, 'simulated' => ''],
59 | ])
60 | ;
61 |
62 | $connection = $this->createMock(Connection::class);
63 | $connection
64 | ->expects(self::once())
65 | ->method('executeQuery')
66 | ->with('SELECT * FROM tl_eblick_trigger_log WHERE pid=? AND origin =?', [12, 'tl_someTable'])
67 | ->willReturn($result)
68 | ;
69 |
70 | $log = new ExecutionLog($connection);
71 |
72 | $expected = [
73 | 4 => ['id' => 1, 'pid' => 12, 'tstamp' => 1234, 'origin' => 'tl_someTable', 'originId' => 4, 'simulated' => ''],
74 | 7 => ['id' => 2, 'pid' => 12, 'tstamp' => 2345, 'origin' => 'tl_someTable', 'originId' => 7, 'simulated' => ''],
75 | ];
76 |
77 | self::assertEquals($expected, $log->getLog(12, 'tl_someTable'));
78 | }
79 |
80 | public function testAddLogWithoutOriginFails(): void
81 | {
82 | $log = new ExecutionLog($this->createMock(Connection::class));
83 |
84 | $this->expectException(\InvalidArgumentException::class);
85 | $log->addLog(12, 5, '', false);
86 | }
87 |
88 | public function testAddLogWithOrigin(): void
89 | {
90 | $connection = $this->createMock(Connection::class);
91 | $connection
92 | ->expects(self::once())
93 | ->method('executeQuery')
94 | ->with(
95 | 'INSERT INTO tl_eblick_trigger_log SET pid=?, tstamp=?, originId=?, origin=?, simulated=?',
96 | self::anything()
97 | )
98 | ;
99 |
100 | $log = new ExecutionLog($connection);
101 | $log->addLog(12, 5, 'tl_someTable', false);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/Component/Condition/TimeConditionTest.php:
--------------------------------------------------------------------------------
1 | assertSame(['selectedTime' => 12345], $data);
26 |
27 | throw new \Exception('callback called');
28 | };
29 |
30 | [$condition, $context] = $this->getTimeCondition('1');
31 |
32 | $this->expectExceptionMessage('callback called');
33 |
34 | /** @var TimeCondition $condition */
35 | /** @var ExecutionContext $context */
36 | $condition->evaluate($context, $callback);
37 | }
38 |
39 | public function testEvaluateWontFire(): void
40 | {
41 | $callback = static function ($data): void {
42 | // expect not to be called
43 | throw new \Exception('callback called');
44 | };
45 |
46 | [$condition, $context] = $this->getTimeCondition('0');
47 |
48 | /** @var TimeCondition $condition */
49 | /** @var ExecutionContext $context */
50 | $condition->evaluate($context, $callback);
51 | }
52 |
53 | public function testGetDataContainerDefinition(): void
54 | {
55 | $obj = new TimeCondition(
56 | $this->createMock(Connection::class)
57 | );
58 |
59 | $definition = $obj->getDataContainerDefinition();
60 | self::assertInstanceOf(Definition::class, $definition);
61 |
62 | self::assertCount(0, $definition->selectors);
63 | self::assertCount(0, $definition->subPalettes);
64 | self::assertCount(1, $definition->fields);
65 | self::assertSame('cnd_time_executionTime', $definition->palette);
66 | }
67 |
68 | public function testGetDataPrototype(): void
69 | {
70 | $condition = new TimeCondition(
71 | $this->createMock(Connection::class)
72 | );
73 |
74 | $result = $condition->getDataPrototype(123);
75 | self::assertEquals($result, ['selectedTime' => null]);
76 | }
77 |
78 | private function getTimeCondition($execute): array
79 | {
80 | $parameters = new \stdClass();
81 | $parameters->id = 6;
82 | $parameters->cnd_time_executionTime = 12345;
83 |
84 | $context = $this->createMock(ExecutionContext::class);
85 | $context
86 | ->expects(self::once())
87 | ->method('getLog')
88 | ->with()
89 | ->willReturn([])
90 | ;
91 | $context
92 | ->expects(self::once())
93 | ->method('getParameters')
94 | ->with()
95 | ->willReturn($parameters)
96 | ;
97 |
98 | $result = $this->createMock(Result::class);
99 | $result
100 | ->expects(self::once())
101 | ->method('fetchOne')
102 | ->willReturn($execute)
103 | ;
104 |
105 | $connection = $this->createMock(Connection::class);
106 | $connection
107 | ->expects(self::once())
108 | ->method('executeQuery')
109 | ->with(
110 | 'SELECT cnd_time_executionTime <> 0 && NOW() >= FROM_UNIXTIME(cnd_time_executionTime) FROM tl_eblick_trigger WHERE id=?',
111 | [6]
112 | )
113 | ->willReturn($result)
114 | ;
115 |
116 | return [new TimeCondition($connection), $context];
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Resources/contao/languages/en/tl_eblick_trigger.php:
--------------------------------------------------------------------------------
1 | Symfony Expression Language. The database columns are available as variables, e.g.: \'sum >= 4 and published\''];
47 |
48 | // time condition
49 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['condition']['time'] = ['Point in Time'];
50 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_time_executionTime'] = ['Date/Time', 'Action will be executed, when this moment in time is reached.'];
51 |
52 | // notification action
53 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action']['notification'] = ['Notification'];
54 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action_notification_entity'] = ['Notification', 'Choose a trigger notification from the notification center.'];
55 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action_notification_tokens'] = 'Available tokens';
56 |
--------------------------------------------------------------------------------
/src/Resources/docs/extending.md:
--------------------------------------------------------------------------------
1 | Adding conditions & actions
2 | ---------------------------
3 |
4 | Adding your own conditions and actions is pretty straightforward. Each
5 | condition can fire multiple actions on evaluation. This allows to
6 | effectively process large bits of data that should be checked against
7 | the same condition (e.g. rows of a table).
8 |
9 | Each condition can add custom data to the execution flow. To do so you
10 | must specify how the data you're adding looks like via the function
11 | `getDataProtoype()`. Note that only a flat structure is supported, yet.
12 |
13 | If an action returns `true` a trigger log entry is made. Your condition
14 | must check for the log entries and decide if actions should be fired
15 | (again) or not. To do so you can use the functions inside the
16 | `ExecutionContext` that is passed on evaluation. Each log entry is
17 | associated with the trigger, an `origin` value (most likely the source
18 | table or `tl_eblick_trigger` by default) and a `originId` to identify
19 | individual records or iterations.
20 |
21 | Throw an `ExecutionException` (or any other appropriate exception) if
22 | you want or need to stop processing and give a reason. The trigger will
23 | then be in an error mode until resolved in the backend and the exception
24 | message will be shown.
25 |
26 |
27 |
28 | #### Adding a custom Condition
29 |
30 | - Create a new service class that implements the
31 | `EBlick\ContaoTrigger\Component\Condition\ConditionInterface`.
32 | ```php
33 | 10, 'bar' => 'abc']
44 | $fireCallback($myCustomData);
45 | }
46 |
47 | public function getDataPrototype(int $triggerId) : array {
48 | return ['foo' => null, 'bar' => null]
49 | }
50 |
51 | }
52 |
53 | // ...
54 | ```
55 |
56 | - See the interface's doc blocks for further information.
57 | - Tag your service:
58 | ```yml
59 | somewhere.custom.my_condition:
60 | class: SomeWhere\Custom\MyCondition
61 | tags:
62 | - { name: eblick_contao_trigger.condition, alias: mycondition }
63 | ```
64 |
65 | - Make sure to add a language file for your condition's name:
66 | ```php
67 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['condition']['mycondition'] = ['My Condition'];
68 | ```
69 |
70 |
71 |
72 | #### Adding a custom Action
73 | - Create a new service class that implements the
74 | `EBlick\ContaoTrigger\Component\Action\ActionInterface`.
75 | ```php
76 |
127 | [
128 | // ...
129 | 'inputType' => 'text',
130 | 'sql' => "int(10) unsigned NOT NULL default '0'"
131 | ]
132 | ];
133 |
134 | return new Definition($fields, $palette);
135 | }
136 |
137 | }
138 |
139 | // ...
140 | ```
141 |
142 | - In a definition you can specify fields, palettes, selectors and
143 | sub palettes like you would in a regular Contao dca container. This
144 | definition will than get merged as a subgroup into the
145 | `tl_eblick_trigger` dca and displayed as a selectable option. To avoid
146 | collisions it is suggested to prefix your fields with `act_myaction_`
147 | for actions and `cnd_mycondition_` for conditions.
148 |
--------------------------------------------------------------------------------
/src/Resources/contao/languages/de/tl_eblick_trigger.php:
--------------------------------------------------------------------------------
1 | Symfony Expression Language. Die Tabellen-Spalten sind als Variablen verfügbar, z.B.: \'sum >= 4 and published\''];
47 |
48 | // time condition
49 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['condition']['time'] = ['Zeitpunkt'];
50 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_time_executionTime'] = ['Datum/Uhrzeit', 'Aktion wird ausgelöst, sobald der Zeitpunkt eingetreten ist.'];
51 |
52 | // notification action
53 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action']['notification'] = ['Benachrichtigung via Notification Center'];
54 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action_notification_entity'] = ['Benachrichtigung', 'Wählen Sie eine zuvor angelegte Benachrichtigung des Typs `Trigger Benachrichtigung` aus dem Notification Center.'];
55 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action_notification_tokens'] = 'Verfügbare Simple-Tokens';
56 |
--------------------------------------------------------------------------------
/src/EventListener/DataContainer/TableCondition.php:
--------------------------------------------------------------------------------
1 | schemaManager = $this->connection->createSchemaManager();
34 | }
35 |
36 | /**
37 | * Returns a list of available tables.
38 | */
39 | public function onGetTables(): array
40 | {
41 | $tables = array_map(
42 | static fn (Table $table): string => $table->getName(),
43 | $this->schemaManager->listTables()
44 | );
45 |
46 | // exclude tables
47 | $excludedTables = [
48 | // trigger framework
49 | 'tl_eblick_trigger',
50 | 'tl_eblick_trigger_log',
51 |
52 | // contao core
53 | 'tl_cron',
54 | 'tl_log',
55 | 'tl_remember_me',
56 | 'tl_search',
57 | 'tl_search_index',
58 | 'tl_undo',
59 | 'tl_version',
60 | ];
61 |
62 | $tables = array_diff(
63 | array_values($tables),
64 | $excludedTables
65 | );
66 |
67 | // key equals value
68 | return array_combine($tables, $tables);
69 | }
70 |
71 | /**
72 | * Returns a list of table columns that can contain time information.
73 | */
74 | public function onGetTimeColumns(DataContainer $dc): array
75 | {
76 | $table = $this->connection
77 | ->executeQuery('SELECT cnd_table_src FROM tl_eblick_trigger WHERE id = ?', [$dc->id])
78 | ->fetchOne()
79 | ;
80 |
81 | if (!$table || !$this->schemaManager->tablesExist([$table])) {
82 | return [];
83 | }
84 |
85 | $columns = [];
86 |
87 | // try to resolve column names
88 | $this->framework->initialize();
89 |
90 | /** @var Controller $controller */
91 | $controller = $this->framework->getAdapter(Controller::class);
92 | /** @noinspection StaticInvocationViaThisInspection */
93 | $controller->loadLanguageFile($table);
94 |
95 | foreach ($this->schemaManager->listTableColumns($table) as $column) {
96 | if ($this->canBeDateTimeColumn($column)) {
97 | $columns[$column->getName()] = $this->buildFieldLabel($table, $column->getName());
98 | }
99 | }
100 |
101 | return $columns;
102 | }
103 |
104 | public function onValidateExpression($var, DataContainer $dc): string
105 | {
106 | if (!$var) {
107 | return $var;
108 | }
109 |
110 | $columns = [];
111 |
112 | foreach ($this->schemaManager->listTableColumns($dc->activeRecord->cnd_table_src) as $column) {
113 | $columns[] = $column->getName();
114 | }
115 |
116 | // throws syntax error if invalid
117 | $this->rowDataCompiler->compileRowExpression($var, $columns);
118 |
119 | return $var;
120 | }
121 |
122 | private function canBeDateTimeColumn(Column $column): bool
123 | {
124 | $type = $column->getType();
125 |
126 | return match (true) {
127 | $type instanceof StringType => 10 === $column->getLength(),
128 | $type instanceof IntegerType => !\in_array($column->getName(), ['id', 'pid'], true)
129 | && (!$column->getLength() || $column->getLength() >= 10),
130 | $type instanceof DateTimeType, $type instanceof DateType, $type instanceof TimeType => true,
131 | default => false,
132 | };
133 | }
134 |
135 | private function buildFieldLabel(string $table, string $field): string
136 | {
137 | if (
138 | !\array_key_exists($table, $GLOBALS['TL_LANG'])
139 | || !\array_key_exists($field, $GLOBALS['TL_LANG'][$table])
140 | || !$GLOBALS['TL_LANG'][$table][$field]
141 | ) {
142 | return $field;
143 | }
144 | $label = \is_array(
145 | $GLOBALS['TL_LANG'][$table][$field]
146 | ) ? $GLOBALS['TL_LANG'][$table][$field][0] : $GLOBALS['TL_LANG'][$table][$field];
147 |
148 | return sprintf('%s (%s)', $label, $field);
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/EventListener/TriggerListener.php:
--------------------------------------------------------------------------------
1 | executionTimer = new Stopwatch();
31 | }
32 |
33 | /**
34 | * Run all triggers - this should be called periodically to make time constraints work.
35 | */
36 | public function onExecute(): void
37 | {
38 | $triggers = $this->connection
39 | ->executeQuery(
40 | 'SELECT * FROM tl_eblick_trigger '.
41 | 'WHERE enabled = 1 && error IS NULL'
42 | )
43 | ->fetchAllAssociative()
44 | ;
45 |
46 | if (!$triggers) {
47 | return;
48 | }
49 |
50 | $factory = new LockFactory(new FlockStore());
51 | $lock = $factory->createLock('trigger-execution', 60);
52 |
53 | $lock->acquire();
54 |
55 | // execute triggers
56 | try {
57 | foreach ($triggers as $trigger) {
58 | $this->execute((object) $trigger);
59 | $lock->refresh();
60 | }
61 | } finally {
62 | $lock->release();
63 | }
64 | }
65 |
66 | /**
67 | * Simulate a trigger and write logs without executing actions.
68 | */
69 | public function onSimulate(int $triggerId): void
70 | {
71 | $trigger = $this->connection
72 | ->executeQuery(
73 | 'SELECT * FROM tl_eblick_trigger WHERE id = ?',
74 | [$triggerId]
75 | )
76 | ->fetchAssociative()
77 | ;
78 |
79 | $this->execute((object) $trigger, true);
80 | }
81 |
82 | /**
83 | * Execute a single trigger.
84 | */
85 | private function execute(\stdClass $trigger, bool $simulated = false): void
86 | {
87 | $condition = $this->componentManager->getCondition($trigger->condition_type);
88 | $action = $this->componentManager->getAction($trigger->action_type);
89 |
90 | if (!$condition || !$action) {
91 | return;
92 | }
93 |
94 | // setup execution data environment
95 | $this->executionTimer->reset();
96 | $this->executionTimer->start('trigger-'.$trigger->id);
97 | $startTime = time();
98 |
99 | $executionContext = $this->executionContextFactory->createExecutionContext($trigger, $startTime);
100 | $dataPrototype = $condition->getDataPrototype((int) $trigger->id);
101 |
102 | $fireCallback =
103 | function (array $data = [], int $originId = 0, string $origin = 'tl_eblick_trigger') use ($simulated, $action, $executionContext, $dataPrototype): void {
104 | // fire action or skip if simulating
105 | if ($simulated || $action->fire($executionContext, $this->filterData($dataPrototype, $data))) {
106 | $executionContext->addLog($originId, $origin, $simulated);
107 | }
108 | };
109 |
110 | // condition evaluation
111 | try {
112 | $condition->evaluate($executionContext, $fireCallback);
113 | } catch (\Exception $e) {
114 | $this->connection->executeQuery(
115 | 'UPDATE tl_eblick_trigger SET error = ? WHERE id = ?',
116 | [$e->getMessage(), $trigger->id]
117 | );
118 |
119 | if ($e instanceof ExecutionException) {
120 | $this->logger->warning(
121 | sprintf('An error occurred during execution of trigger %s.', $trigger->id),
122 | ['exception' => $e, 'contao' => new ContaoContext(__METHOD__, ContaoContext::ERROR)]
123 | );
124 | } else {
125 | $this->logger->critical(
126 | sprintf('An unexpected exception occurred during execution of trigger %s.', $trigger->id),
127 | ['exception' => $e, 'contao' => new ContaoContext(__METHOD__, ContaoContext::ERROR)]
128 | );
129 | }
130 | }
131 |
132 | // update trigger meta data
133 | $stopwatchEvent = $this->executionTimer->stop('trigger-'.$trigger->id);
134 | $this->connection->executeQuery(
135 | 'UPDATE tl_eblick_trigger SET lastRun = ?, lastDuration = ? WHERE id = ?',
136 | [$executionContext->getStartTime(), (int) $stopwatchEvent->getDuration(), $trigger->id]
137 | );
138 | }
139 |
140 | /**
141 | * Constrain processing data to what has been set in the respective prototype.
142 | */
143 | private function filterData(array $dataPrototype, array $data): array
144 | {
145 | // filter by keys (currently does not support nesting)
146 | return array_filter(
147 | $data,
148 | static fn ($v) => \array_key_exists($v, $dataPrototype),
149 | ARRAY_FILTER_USE_KEY
150 | );
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/EventListener/DataContainer/Trigger.php:
--------------------------------------------------------------------------------
1 | componentManager->getConditionNames() as $conditionName) {
43 | $condition = $this->componentManager->getCondition($conditionName);
44 |
45 | if ($condition instanceof DataContainerComponentInterface) {
46 | $this->importComponent(
47 | 'condition',
48 | $conditionName,
49 | $condition->getDataContainerDefinition()
50 | );
51 | }
52 | }
53 |
54 | foreach ($this->componentManager->getActionNames() as $actionName) {
55 | $action = $this->componentManager->getAction($actionName);
56 |
57 | if ($action instanceof DataContainerComponentInterface) {
58 | $this->importComponent(
59 | 'action',
60 | $actionName,
61 | $action->getDataContainerDefinition()
62 | );
63 | }
64 | }
65 | }
66 |
67 | public function onGetError(DataContainer $dc): string
68 | {
69 | if (!$dc->activeRecord->error) {
70 | return '';
71 | }
72 |
73 | return sprintf(
74 | '%s
%s
%s',
75 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['error'][0],
76 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['error'][1],
77 | nl2br($dc->activeRecord->error)
78 | );
79 | }
80 |
81 | public function onResetError(DataContainer $dc): void
82 | {
83 | $this->connection->executeQuery(
84 | 'UPDATE tl_eblick_trigger SET error = NULL WHERE id =?',
85 | [$dc->id]
86 | );
87 | }
88 |
89 | public function onGenerateLabel(array $row): string
90 | {
91 | if ($row['error']) {
92 | $state = 'error';
93 | } elseif ($row['enabled']) {
94 | $state = $row['lastRun'] ? 'running' : 'waiting';
95 | } else {
96 | $state = 'paused';
97 | }
98 |
99 | $this->framework->initialize();
100 | $datimFormat = $this->framework->getAdapter(Config::class)->get('datimFormat');
101 |
102 | $lastRun = $row['lastRun'] ?
103 | Date::parse($datimFormat, $row['lastRun']).' ('.($row['lastDuration'] / 1000).'s)' : '[…]';
104 |
105 | $numRuns = (int) $this->connection
106 | ->executeQuery('SELECT COUNT(*) FROM tl_eblick_trigger_log WHERE pid =?', [$row['id']])
107 | ->fetchOne()
108 | ;
109 |
110 | return sprintf(
111 | '%s'.
112 | '
%s → %s (%s)
%s
',
113 | $state,
114 | $row['title'],
115 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['condition'][$row['condition_type']][0],
116 | $GLOBALS['TL_LANG']['tl_eblick_trigger']['action'][$row['action_type']][0],
117 | $numRuns,
118 | $lastRun
119 | );
120 | }
121 |
122 | public function onShowSimulateButton(array $row, string $href, string|null $label, string $title, string $icon, string $attributes): string
123 | {
124 | // only show for disabled triggers
125 | if ($row['enabled']) {
126 | return '';
127 | }
128 |
129 | return sprintf(
130 | '%s ',
131 | Backend::addToUrl($href.'&id='.$row['id']),
132 | StringUtil::specialchars($title),
133 | $attributes,
134 | Image::getHtml($icon, $label)
135 | );
136 | }
137 |
138 | public function onExecute(): void
139 | {
140 | $this->triggerSystem->onExecute();
141 | $this->redirectBack();
142 | }
143 |
144 | public function onSimulate(DataContainer $dc): void
145 | {
146 | $this->triggerSystem->onSimulate((int) $dc->id);
147 | $this->redirectBack();
148 | }
149 |
150 | public function onReset(DataContainer $dc): void
151 | {
152 | $this->connection->executeQuery(
153 | 'DELETE FROM tl_eblick_trigger_log WHERE pid =?',
154 | [$dc->id]
155 | );
156 | $this->connection->executeQuery(
157 | 'UPDATE tl_eblick_trigger SET lastDuration = 0, lastRun = 0 WHERE id =?',
158 | [$dc->id]
159 | );
160 |
161 | $this->redirectBack();
162 | }
163 |
164 | /**
165 | * Merge datacontainer properties as a sub component.
166 | */
167 | private function importComponent(string $componentType, string $componentName, Definition $definition): void
168 | {
169 | $node = &$GLOBALS['TL_DCA']['tl_eblick_trigger'];
170 |
171 | // add component to respective component selector
172 | $node['fields'][$componentType.'_type']['options'][] = $componentName;
173 |
174 | // add palettes (as a sub palette), fields, selectors and sub palettes
175 | $node['subpalettes'][$componentType.'_type_'.$componentName] = $definition->palette;
176 |
177 | foreach ($definition->fields as $fieldName => $field) {
178 | $node['fields'][$fieldName] = $field;
179 | }
180 |
181 | foreach ($definition->selectors as $selector) {
182 | $node['palettes']['__selector__'][] = $selector;
183 | }
184 |
185 | foreach ($definition->subPalettes as $subPaletteName => $subPalette) {
186 | $node['subpalettes'][$subPaletteName] = $subPalette;
187 | }
188 | }
189 |
190 | /**
191 | * Redirect to current listing after action.
192 | */
193 | private function redirectBack(): void
194 | {
195 | $this->framework->initialize();
196 |
197 | /** @var Controller $controller */
198 | $controller = $this->framework->getAdapter(Controller::class);
199 | /** @noinspection StaticInvocationViaThisInspection */
200 | $controller->redirect($controller->addToUrl(null, true, ['key']));
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/src/Resources/contao/dca/tl_eblick_trigger.php:
--------------------------------------------------------------------------------
1 | [
15 | 'dataContainer' => 'Table',
16 | 'ctable' => ['tl_eblick_trigger_log'],
17 | 'switchToEdit' => true,
18 | 'enableVersioning' => true,
19 | 'sql' => [
20 | 'keys' => [
21 | 'id' => 'primary',
22 | 'enabled' => 'index',
23 | 'condition_type' => 'index',
24 | 'action_type' => 'index',
25 | ],
26 | ],
27 | 'onsubmit_callback' => [
28 | [
29 | 'eblick_contao_trigger.listener.datacontainer.trigger',
30 | 'onResetError',
31 | ],
32 | ],
33 | ],
34 |
35 | // List
36 | 'list' => [
37 | 'sorting' => [
38 | 'mode' => 2,
39 | 'fields' => ['title'],
40 | 'flag' => 1,
41 | 'panelLayout' => 'sort,search,limit',
42 | ],
43 | 'label' => [
44 | 'fields' => ['title'],
45 | 'label_callback' => [
46 | 'eblick_contao_trigger.listener.datacontainer.trigger',
47 | 'onGenerateLabel',
48 | ],
49 | ],
50 | 'global_operations' => [
51 | 'execute' => [
52 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['execute'],
53 | 'href' => 'key=execute',
54 | 'class' => 'header_icon',
55 | 'icon' => 'sync.svg',
56 | ],
57 | ],
58 | 'operations' => [
59 | 'show' => [
60 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['show'],
61 | 'href' => 'act=show',
62 | 'icon' => 'show.svg',
63 | ],
64 | 'edit' => [
65 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['edit'],
66 | 'href' => 'act=edit',
67 | 'icon' => 'edit.svg',
68 | ],
69 | 'log' => [
70 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['log'],
71 | 'href' => 'table=tl_eblick_trigger_log',
72 | 'icon' => 'bundles/eblickcontaotrigger/img/log.svg',
73 | ],
74 | 'simulate' => [
75 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['simulate'],
76 | 'href' => 'key=simulate',
77 | 'icon' => 'bundles/eblickcontaotrigger/img/simulate.svg',
78 | 'button_callback' => [
79 | 'eblick_contao_trigger.listener.datacontainer.trigger',
80 | 'onShowSimulateButton',
81 | ],
82 | 'attributes' => 'onclick="if(!confirm(\''
83 | .($GLOBALS['TL_LANG']['tl_eblick_trigger']['simulateConfirm'] ?? null)
84 | .'\'))return false;Backend.getScrollOffset()"',
85 | ],
86 | 'reset' => [
87 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['reset'],
88 | 'href' => 'key=reset',
89 | 'icon' => 'bundles/eblickcontaotrigger/img/reset.svg',
90 | 'attributes' => 'onclick="if(!confirm(\''
91 | .($GLOBALS['TL_LANG']['tl_eblick_trigger']['resetConfirm'] ?? null)
92 | .'\'))return false;Backend.getScrollOffset()"',
93 | ],
94 | 'delete' => [
95 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['delete'],
96 | 'href' => 'act=delete',
97 | 'icon' => 'delete.svg',
98 | 'attributes' => 'onclick="if(!confirm(\''.($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null)
99 | .'\'))return false;Backend.getScrollOffset()"',
100 | ],
101 | ],
102 | ],
103 |
104 | // Select
105 | 'select' => [
106 | 'buttons_callback' => [],
107 | ],
108 |
109 | // Edit
110 | 'edit' => [
111 | 'buttons_callback' => [],
112 | ],
113 |
114 | // Palettes
115 | 'palettes' => [
116 | '__selector__' => ['condition_type', 'action_type'],
117 | 'default' => '{meta_legend},error,title;'.
118 | '{condition_legend},condition_type;'.
119 | '{action_legend},action_type;'.
120 | '{system_legend},enabled;',
121 | ],
122 |
123 | // Subpalettes
124 | 'subpalettes' => [
125 | 'condition_type_table' => 'condition_table_srcField',
126 | ],
127 |
128 | // Fields
129 | 'fields' => [
130 | 'id' => [
131 | 'sql' => 'int(10) unsigned NOT NULL auto_increment',
132 | ],
133 | 'tstamp' => [
134 | 'sql' => "int(10) unsigned NOT NULL default '0'",
135 | ],
136 | 'title' => [
137 | 'exclude' => true,
138 | 'inputType' => 'text',
139 | 'eval' => [
140 | 'mandatory' => true,
141 | 'maxlength' => 255,
142 | ],
143 | 'sql' => "varchar(255) NOT NULL default ''",
144 | ],
145 | 'error' => [
146 | 'exclude' => true,
147 | 'input_field_callback' => [
148 | 'eblick_contao_trigger.listener.datacontainer.trigger',
149 | 'onGetError',
150 | ],
151 | 'sql' => 'TEXT NULL default NULL',
152 | ],
153 | 'enabled' => [
154 | 'exclude' => true,
155 | 'default' => false,
156 | 'inputType' => 'checkbox',
157 | 'eval' => [
158 | 'isBoolean' => true,
159 | ],
160 | 'sql' => "char(1) NOT NULL default '0'",
161 | ],
162 | 'condition_type' => [
163 | 'exclude' => true,
164 | 'inputType' => 'select',
165 | 'reference' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['condition'],
166 | 'eval' => [
167 | 'mandatory' => true,
168 | 'includeBlankOption' => true,
169 | 'submitOnChange' => true,
170 | 'tl_class' => 'w50',
171 | ],
172 | 'sql' => "varchar(64) NOT NULL default ''",
173 | ],
174 | 'action_type' => [
175 | 'exclude' => true,
176 | 'inputType' => 'select',
177 | 'reference' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['action'],
178 | 'eval' => [
179 | 'mandatory' => true,
180 | 'includeBlankOption' => true,
181 | 'submitOnChange' => true,
182 | 'tl_class' => 'w50',
183 | ],
184 | 'sql' => "varchar(64) NOT NULL default ''",
185 | ],
186 | 'lastRun' => [
187 | 'eval' => ['rgxp' => 'datim'],
188 | 'exclude' => true,
189 | 'sql' => "int(10) unsigned NOT NULL default '0'",
190 | ],
191 | 'lastDuration' => [
192 | 'sql' => "int(10) unsigned NOT NULL default '0'",
193 | 'exclude' => true,
194 | ],
195 | ],
196 | ];
197 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
167 |
168 | ____________________________________________________________________________
169 |
170 |
171 | Icons used in the backend made by
172 |
173 | - `Pixelmeetup`
174 | - `Roundicons`
175 | - `Google`
176 |
177 | from `Flaticon` .
178 |
--------------------------------------------------------------------------------
/src/Component/Condition/TableCondition.php:
--------------------------------------------------------------------------------
1 | schemaManager = $this->connection->createSchemaManager();
33 | }
34 |
35 | public function evaluate(ExecutionContext $context, \Closure $fireCallback): void
36 | {
37 | $trigger = $context->getParameters();
38 |
39 | // build query
40 | $log = $context->getLog($trigger->cnd_table_src);
41 | [$query, $params, $types] = $this->buildQuery($trigger, $log);
42 |
43 | // create expression callback
44 | $expressionCallback = $this->buildExpressionCallback($trigger->cnd_table_expression, $trigger->cnd_table_src);
45 |
46 | // execute query
47 | try {
48 | $affectedEntities = $this->connection
49 | ->executeQuery($query, $params, $types)
50 | ->fetchAllAssociative()
51 | ;
52 | } catch (Exception $e) {
53 | throw new ExecutionException($e->getMessage(), 0, $e);
54 | }
55 |
56 | foreach ($affectedEntities as $entity) {
57 | // check expression
58 | if ($expressionCallback && !$expressionCallback($entity)) {
59 | continue;
60 | }
61 |
62 | // fire action
63 | $fireCallback($entity, (int) $entity['id'], $trigger->cnd_table_src);
64 | }
65 | }
66 |
67 | public function getDataPrototype(int $triggerId): array
68 | {
69 | $srcTable = $this->connection
70 | ->executeQuery('SELECT cnd_table_src FROM tl_eblick_trigger WHERE id = ?', [$triggerId])
71 | ->fetchOne()
72 | ;
73 |
74 | return
75 | array_fill_keys(
76 | $this->getColumnNames($srcTable),
77 | null
78 | );
79 | }
80 |
81 | public function getDataContainerDefinition(): Definition
82 | {
83 | $palette = 'cnd_table_src,cnd_table_timed,cnd_table_expression';
84 |
85 | $selectors = [
86 | 'cnd_table_timed',
87 | 'cnd_table_overwriteExecutionTime',
88 | ];
89 |
90 | $subPalettes = [
91 | 'cnd_table_timed' => 'cnd_table_timeColumn,cnd_table_timeOffset,cnd_table_timeOffsetUnit,cnd_table_overwriteExecutionTime',
92 | 'cnd_table_overwriteExecutionTime' => 'cnd_table_executionTime',
93 | ];
94 |
95 | $fields = [
96 | 'cnd_table_src' => [
97 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_src'],
98 | 'exclude' => true,
99 | 'inputType' => 'select',
100 | 'options_callback' => [
101 | 'eblick_contao_trigger.listener.datacontainer.table_condition',
102 | 'onGetTables',
103 | ],
104 | 'eval' => [
105 | 'mandatory' => true,
106 | 'includeBlankOption' => true,
107 | 'submitOnChange' => true,
108 | 'tl_class' => 'w50',
109 | ],
110 | 'sql' => "varchar(64) NOT NULL default ''",
111 | ],
112 | 'cnd_table_timed' => [
113 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_timed'],
114 | 'exclude' => true,
115 | 'inputType' => 'checkbox',
116 | 'eval' => ['submitOnChange' => true, 'tl_class' => 'clr m12'],
117 | 'sql' => "char(1) NOT NULL default ''",
118 | ],
119 | 'cnd_table_timeColumn' => [
120 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_timeColumn'],
121 | 'exclude' => true,
122 | 'inputType' => 'select',
123 | 'options_callback' => [
124 | 'eblick_contao_trigger.listener.datacontainer.table_condition',
125 | 'onGetTimeColumns',
126 | ],
127 | 'eval' => [
128 | 'mandatory' => true,
129 | 'includeBlankOption' => true,
130 | 'tl_class' => 'w50',
131 | ],
132 | 'sql' => "varchar(64) NOT NULL default ''",
133 | ],
134 | 'cnd_table_timeOffset' => [
135 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_timeOffset'],
136 | 'exclude' => true,
137 | 'inputType' => 'text',
138 | 'eval' => [
139 | 'rgxp' => 'digit',
140 | 'nospace' => true,
141 | 'mandatory' => true,
142 | 'decodeEntities' => true,
143 | 'tl_class' => 'w16',
144 | ],
145 | 'sql' => 'int(10) NULL',
146 | ],
147 | 'cnd_table_timeOffsetUnit' => [
148 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_timeOffsetUnit'],
149 | 'exclude' => true,
150 | 'inputType' => 'select',
151 | 'options' => ['MINUTE', 'HOUR', 'DAY'],
152 | 'reference' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_timeOffsetUnit'],
153 | 'eval' => [
154 | 'mandatory' => true,
155 | 'tl_class' => 'w16',
156 | ],
157 | 'sql' => 'varchar(6) NULL',
158 | ],
159 | 'cnd_table_overwriteExecutionTime' => [
160 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_overwriteExecutionTime'],
161 | 'exclude' => true,
162 | 'inputType' => 'checkbox',
163 | 'eval' => ['submitOnChange' => true, 'tl_class' => 'clr m12'],
164 | 'sql' => "char(1) NOT NULL default ''",
165 | ],
166 | 'cnd_table_executionTime' => [
167 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_executionTime'],
168 | 'exclude' => true,
169 | 'inputType' => 'text',
170 | 'eval' => [
171 | 'mandatory' => true,
172 | 'rgxp' => 'time',
173 | 'datepicker' => true,
174 | 'tl_class' => 'w25 wizard',
175 | ],
176 | 'sql' => 'INT(10) NULL',
177 | ],
178 | 'cnd_table_expression' => [
179 | 'label' => &$GLOBALS['TL_LANG']['tl_eblick_trigger']['cnd_table_expression'],
180 | 'exclude' => true,
181 | 'inputType' => 'text',
182 | 'eval' => [
183 | 'decodeEntities' => true,
184 | ],
185 | 'save_callback' => [
186 | [
187 | 'eblick_contao_trigger.listener.datacontainer.table_condition',
188 | 'onValidateExpression',
189 | ],
190 | ],
191 | 'sql' => "varchar(255) NOT NULL default ''",
192 | ],
193 | ];
194 |
195 | return new Definition($fields, $palette, $selectors, $subPalettes);
196 | }
197 |
198 | /**
199 | * Get column names of source table.
200 | */
201 | private function getColumnNames(string $srcTable): array
202 | {
203 | return array_map(
204 | static fn (Column $column): string => $column->getName(),
205 | $this->schemaManager->listTableColumns($srcTable)
206 | );
207 | }
208 |
209 | /**
210 | * Build SQL query to select entities of the selected source table.
211 | */
212 | private function buildQuery(\stdClass $trigger, array $log): array
213 | {
214 | $logIds = !empty($log) ? array_keys($log) : [-1];
215 |
216 | $query = 'SELECT * FROM '.$this->connection->quoteIdentifier($trigger->cnd_table_src).' WHERE TRUE';
217 | $params = [];
218 | $types = [];
219 |
220 | // log condition
221 | if (!empty($log)) {
222 | $query .= ' AND id NOT IN (?)';
223 | $params[] = $logIds;
224 | $types[] = ArrayParameterType::INTEGER;
225 | }
226 |
227 | // time condition
228 | if ($trigger->cnd_table_timed) {
229 | // injection prevention
230 | if (
231 | !\in_array(
232 | $timeUnit = $trigger->cnd_table_timeOffsetUnit,
233 | ['MINUTE', 'HOUR', 'DAY'],
234 | true
235 | )
236 | ) {
237 | throw new ExecutionException(sprintf('Invalid time offset "%s"!', $trigger->cnd_table_timeOffsetUnit));
238 | }
239 | $timeColumn = $this->connection->quoteIdentifier(
240 | $trigger->cnd_table_timeColumn
241 | );
242 |
243 | // offset condition
244 | $timeComparisonSql =
245 | 'DATE_ADD('.
246 | // allow datetime/date/timestamp fields with fallback to Contao's string timestamps
247 | 'IFNULL(DATE_ADD('.$timeColumn.', INTERVAL 0 SECOND), FROM_UNIXTIME('.$timeColumn.')),'.
248 | 'INTERVAL ? '.$timeUnit.
249 | ')';
250 |
251 | $params[] = $trigger->cnd_table_timeOffset;
252 | $types[] = ParameterType::INTEGER;
253 |
254 | // overwrite time portion: extract date + concatenate time
255 | if ($trigger->cnd_table_overwriteExecutionTime) {
256 | $timeComparisonSql = 'CONCAT(DATE('.$timeComparisonSql.'), ?)';
257 |
258 | $params[] = ' '.date('H:m:s', (int) $trigger->cnd_table_executionTime);
259 | $types[] = ParameterType::STRING;
260 | }
261 |
262 | // compose
263 | $query .= sprintf(' AND NOW() >= %s', $timeComparisonSql);
264 | }
265 |
266 | return [$query, $params, $types];
267 | }
268 |
269 | private function buildExpressionCallback(string $expression, string $srcTable): \Closure|null
270 | {
271 | if (!$expression) {
272 | return null;
273 | }
274 |
275 | try {
276 | $expressionCallback = $this->rowDataCompiler->compileRowExpression(
277 | $expression,
278 | $this->getColumnNames($srcTable)
279 | );
280 | } catch (SyntaxError $e) {
281 | throw new ExecutionException($e->getMessage(), 0, $e);
282 | }
283 |
284 | return $expressionCallback;
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/tools/ecs/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "dfbce4e9ae46a0d37dd10d37b3deb4a2",
8 | "packages": [
9 | {
10 | "name": "contao/easy-coding-standard",
11 | "version": "5.4.2",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/contao/easy-coding-standard.git",
15 | "reference": "55d47f7b08e60199b32e9e9cbaffd5d1bd2bb620"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/contao/easy-coding-standard/zipball/55d47f7b08e60199b32e9e9cbaffd5d1bd2bb620",
20 | "reference": "55d47f7b08e60199b32e9e9cbaffd5d1bd2bb620",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "php": "^7.4 || ^8.0",
25 | "slevomat/coding-standard": "^7.0 || ^8.0",
26 | "symplify/easy-coding-standard": "^10.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^9.5"
30 | },
31 | "type": "library",
32 | "autoload": {
33 | "psr-4": {
34 | "Contao\\EasyCodingStandard\\": "src/"
35 | }
36 | },
37 | "notification-url": "https://packagist.org/downloads/",
38 | "license": [
39 | "LGPL-3.0-or-later"
40 | ],
41 | "authors": [
42 | {
43 | "name": "Leo Feyer",
44 | "homepage": "https://github.com/leofeyer"
45 | }
46 | ],
47 | "description": "EasyCodingStandard configurations for Contao",
48 | "support": {
49 | "issues": "https://github.com/contao/easy-coding-standard/issues",
50 | "source": "https://github.com/contao/easy-coding-standard/tree/5.4.2"
51 | },
52 | "funding": [
53 | {
54 | "url": "https://to.contao.org/donate",
55 | "type": "custom"
56 | }
57 | ],
58 | "time": "2023-01-06T10:46:16+00:00"
59 | },
60 | {
61 | "name": "dealerdirect/phpcodesniffer-composer-installer",
62 | "version": "v1.0.0",
63 | "source": {
64 | "type": "git",
65 | "url": "https://github.com/PHPCSStandards/composer-installer.git",
66 | "reference": "4be43904336affa5c2f70744a348312336afd0da"
67 | },
68 | "dist": {
69 | "type": "zip",
70 | "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da",
71 | "reference": "4be43904336affa5c2f70744a348312336afd0da",
72 | "shasum": ""
73 | },
74 | "require": {
75 | "composer-plugin-api": "^1.0 || ^2.0",
76 | "php": ">=5.4",
77 | "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0"
78 | },
79 | "require-dev": {
80 | "composer/composer": "*",
81 | "ext-json": "*",
82 | "ext-zip": "*",
83 | "php-parallel-lint/php-parallel-lint": "^1.3.1",
84 | "phpcompatibility/php-compatibility": "^9.0",
85 | "yoast/phpunit-polyfills": "^1.0"
86 | },
87 | "type": "composer-plugin",
88 | "extra": {
89 | "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
90 | },
91 | "autoload": {
92 | "psr-4": {
93 | "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
94 | }
95 | },
96 | "notification-url": "https://packagist.org/downloads/",
97 | "license": [
98 | "MIT"
99 | ],
100 | "authors": [
101 | {
102 | "name": "Franck Nijhof",
103 | "email": "franck.nijhof@dealerdirect.com",
104 | "homepage": "http://www.frenck.nl",
105 | "role": "Developer / IT Manager"
106 | },
107 | {
108 | "name": "Contributors",
109 | "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors"
110 | }
111 | ],
112 | "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
113 | "homepage": "http://www.dealerdirect.com",
114 | "keywords": [
115 | "PHPCodeSniffer",
116 | "PHP_CodeSniffer",
117 | "code quality",
118 | "codesniffer",
119 | "composer",
120 | "installer",
121 | "phpcbf",
122 | "phpcs",
123 | "plugin",
124 | "qa",
125 | "quality",
126 | "standard",
127 | "standards",
128 | "style guide",
129 | "stylecheck",
130 | "tests"
131 | ],
132 | "support": {
133 | "issues": "https://github.com/PHPCSStandards/composer-installer/issues",
134 | "source": "https://github.com/PHPCSStandards/composer-installer"
135 | },
136 | "time": "2023-01-05T11:28:13+00:00"
137 | },
138 | {
139 | "name": "phpstan/phpdoc-parser",
140 | "version": "1.15.3",
141 | "source": {
142 | "type": "git",
143 | "url": "https://github.com/phpstan/phpdoc-parser.git",
144 | "reference": "61800f71a5526081d1b5633766aa88341f1ade76"
145 | },
146 | "dist": {
147 | "type": "zip",
148 | "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76",
149 | "reference": "61800f71a5526081d1b5633766aa88341f1ade76",
150 | "shasum": ""
151 | },
152 | "require": {
153 | "php": "^7.2 || ^8.0"
154 | },
155 | "require-dev": {
156 | "php-parallel-lint/php-parallel-lint": "^1.2",
157 | "phpstan/extension-installer": "^1.0",
158 | "phpstan/phpstan": "^1.5",
159 | "phpstan/phpstan-phpunit": "^1.1",
160 | "phpstan/phpstan-strict-rules": "^1.0",
161 | "phpunit/phpunit": "^9.5",
162 | "symfony/process": "^5.2"
163 | },
164 | "type": "library",
165 | "autoload": {
166 | "psr-4": {
167 | "PHPStan\\PhpDocParser\\": [
168 | "src/"
169 | ]
170 | }
171 | },
172 | "notification-url": "https://packagist.org/downloads/",
173 | "license": [
174 | "MIT"
175 | ],
176 | "description": "PHPDoc parser with support for nullable, intersection and generic types",
177 | "support": {
178 | "issues": "https://github.com/phpstan/phpdoc-parser/issues",
179 | "source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3"
180 | },
181 | "time": "2022-12-20T20:56:55+00:00"
182 | },
183 | {
184 | "name": "slevomat/coding-standard",
185 | "version": "8.8.0",
186 | "source": {
187 | "type": "git",
188 | "url": "https://github.com/slevomat/coding-standard.git",
189 | "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89"
190 | },
191 | "dist": {
192 | "type": "zip",
193 | "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89",
194 | "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89",
195 | "shasum": ""
196 | },
197 | "require": {
198 | "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0",
199 | "php": "^7.2 || ^8.0",
200 | "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0",
201 | "squizlabs/php_codesniffer": "^3.7.1"
202 | },
203 | "require-dev": {
204 | "phing/phing": "2.17.4",
205 | "php-parallel-lint/php-parallel-lint": "1.3.2",
206 | "phpstan/phpstan": "1.4.10|1.9.6",
207 | "phpstan/phpstan-deprecation-rules": "1.1.1",
208 | "phpstan/phpstan-phpunit": "1.0.0|1.3.3",
209 | "phpstan/phpstan-strict-rules": "1.4.4",
210 | "phpunit/phpunit": "7.5.20|8.5.21|9.5.27"
211 | },
212 | "type": "phpcodesniffer-standard",
213 | "extra": {
214 | "branch-alias": {
215 | "dev-master": "8.x-dev"
216 | }
217 | },
218 | "autoload": {
219 | "psr-4": {
220 | "SlevomatCodingStandard\\": "SlevomatCodingStandard"
221 | }
222 | },
223 | "notification-url": "https://packagist.org/downloads/",
224 | "license": [
225 | "MIT"
226 | ],
227 | "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
228 | "keywords": [
229 | "dev",
230 | "phpcs"
231 | ],
232 | "support": {
233 | "issues": "https://github.com/slevomat/coding-standard/issues",
234 | "source": "https://github.com/slevomat/coding-standard/tree/8.8.0"
235 | },
236 | "funding": [
237 | {
238 | "url": "https://github.com/kukulich",
239 | "type": "github"
240 | },
241 | {
242 | "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard",
243 | "type": "tidelift"
244 | }
245 | ],
246 | "time": "2023-01-09T10:46:13+00:00"
247 | },
248 | {
249 | "name": "squizlabs/php_codesniffer",
250 | "version": "3.7.2",
251 | "source": {
252 | "type": "git",
253 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
254 | "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
255 | },
256 | "dist": {
257 | "type": "zip",
258 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
259 | "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
260 | "shasum": ""
261 | },
262 | "require": {
263 | "ext-simplexml": "*",
264 | "ext-tokenizer": "*",
265 | "ext-xmlwriter": "*",
266 | "php": ">=5.4.0"
267 | },
268 | "require-dev": {
269 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
270 | },
271 | "bin": [
272 | "bin/phpcs",
273 | "bin/phpcbf"
274 | ],
275 | "type": "library",
276 | "extra": {
277 | "branch-alias": {
278 | "dev-master": "3.x-dev"
279 | }
280 | },
281 | "notification-url": "https://packagist.org/downloads/",
282 | "license": [
283 | "BSD-3-Clause"
284 | ],
285 | "authors": [
286 | {
287 | "name": "Greg Sherwood",
288 | "role": "lead"
289 | }
290 | ],
291 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
292 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
293 | "keywords": [
294 | "phpcs",
295 | "standards",
296 | "static analysis"
297 | ],
298 | "support": {
299 | "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
300 | "source": "https://github.com/squizlabs/PHP_CodeSniffer",
301 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
302 | },
303 | "time": "2023-02-22T23:07:41+00:00"
304 | },
305 | {
306 | "name": "symplify/easy-coding-standard",
307 | "version": "10.3.3",
308 | "source": {
309 | "type": "git",
310 | "url": "https://github.com/symplify/easy-coding-standard.git",
311 | "reference": "c93878b3c052321231519b6540e227380f90be17"
312 | },
313 | "dist": {
314 | "type": "zip",
315 | "url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/c93878b3c052321231519b6540e227380f90be17",
316 | "reference": "c93878b3c052321231519b6540e227380f90be17",
317 | "shasum": ""
318 | },
319 | "require": {
320 | "php": ">=7.2"
321 | },
322 | "conflict": {
323 | "friendsofphp/php-cs-fixer": "<3.0",
324 | "squizlabs/php_codesniffer": "<3.6"
325 | },
326 | "bin": [
327 | "bin/ecs"
328 | ],
329 | "type": "library",
330 | "extra": {
331 | "branch-alias": {
332 | "dev-main": "10.3-dev"
333 | }
334 | },
335 | "autoload": {
336 | "files": [
337 | "bootstrap.php"
338 | ]
339 | },
340 | "notification-url": "https://packagist.org/downloads/",
341 | "license": [
342 | "MIT"
343 | ],
344 | "description": "Prefixed scoped version of ECS package",
345 | "support": {
346 | "source": "https://github.com/symplify/easy-coding-standard/tree/10.3.3"
347 | },
348 | "funding": [
349 | {
350 | "url": "https://www.paypal.me/rectorphp",
351 | "type": "custom"
352 | },
353 | {
354 | "url": "https://github.com/tomasvotruba",
355 | "type": "github"
356 | }
357 | ],
358 | "time": "2022-06-13T14:03:37+00:00"
359 | }
360 | ],
361 | "packages-dev": [],
362 | "aliases": [],
363 | "minimum-stability": "stable",
364 | "stability-flags": [],
365 | "prefer-stable": false,
366 | "prefer-lowest": false,
367 | "platform": [],
368 | "platform-dev": [],
369 | "plugin-api-version": "2.3.0"
370 | }
371 |
--------------------------------------------------------------------------------