diff --git a/composer.json b/composer.json index d6a928b..da4eed0 100755 --- a/composer.json +++ b/composer.json @@ -1,55 +1,57 @@ { - "name": "contaoacademy/contao-zammad-nc-api-bundle", - "description": "API Anbindung zum Ticket System Zammad über ein Gateway des Notification-Center", - "type": "contao-bundle", - "keywords": [ - "contao", - "zammad", - "notification-center" - ], - "homepage": "https://contao-academy.de", - "authors": [ - { - "name": "Contao Academy", - "homepage": "https://contao-academy.de" - } - ], - "license": "LGPL-3.0-or-later", - "autoload": { - "psr-4": { - "Contaoacademy\\ZammadNCApiBundle\\": "src/" - }, - "classmap": [ - "src/Resources/contao/" + "name": "contaoacademy/contao-zammad-nc-api-bundle", + "description": "API Anbindung zum Ticket System Zammad über ein Gateway des Notification-Center", + "type": "contao-bundle", + "keywords": [ + "contao", + "zammad", + "notification-center" ], - "exclude-from-classmap": [ - "src/Resources/contao/config/", - "src/Resources/contao/dca/", - "src/Resources/contao/languages/", - "src/Resources/contao/templates/" - ] - }, - "require": { - "php": "^7.1 || ^8.0", - "contao/core-bundle": "^4.4", - "terminal42/notification_center": "^1.5" - }, - "config": { - "preferred-install": "dist" - }, - "replace": { - "contao-legacy/contao-zammad-nc-api-bundle": "self.version" - }, - "extra": { - "contao": { - "sources": { - "": "system/modules/contao-zammad-nc-api-bundle" - } + "homepage": "https://contao-academy.de", + "authors": [ + { + "name": "Contao Academy", + "homepage": "https://contao-academy.de", + "role": "Owner" + }, + { + "name": "Fritz Michael Gschwantner", + "email": "fmg@inspiredminds.at", + "role": "Developer" + } + ], + "license": "LGPL-3.0-or-later", + "autoload": { + "psr-4": { + "ContaoAcademy\\ZammadNCApiBundle\\": "src/" + } + }, + "require": { + "php": ">=8.1", + "codefog/contao-haste": "^4.25 || ^5.1", + "contao/core-bundle": "^4.13 || ^5.3", + "terminal42/notification_center": "^2.0", + "symfony/config": "^5.4 || ^6.4", + "symfony/dependency-injection": "^5.4 || ^6.4", + "symfony/http-client": "^5.4 || ^6.4", + "symfony/http-kernel": "^5.4 || ^6.4" }, - "contao-manager-plugin": "Contaoacademy\\ZammadNCApiBundle\\ContaoManager\\Plugin" - }, - "support": { - "issues": "https://github.com/contaoacademy/contao-zammad-nc-api-bundle/issues", - "source": "https://github.com/contaoacademy/contao-zammad-nc-api-bundle" - } + "extra": { + "contao-manager-plugin": "ContaoAcademy\\ZammadNCApiBundle\\ContaoManager\\Plugin" + }, + "support": { + "issues": "https://github.com/contaoacademy/contao-zammad-nc-api-bundle/issues", + "source": "https://github.com/contaoacademy/contao-zammad-nc-api-bundle" + }, + "require-dev": { + "contao/easy-coding-standard": "^6.0", + "contao/rector": "^1.0" + }, + "config": { + "allow-plugins": { + "contao-components/installer": true, + "php-http/discovery": false, + "dealerdirect/phpcodesniffer-composer-installer": true + } + } } diff --git a/config/services.yaml b/config/services.yaml new file mode 100644 index 0000000..e2c2350 --- /dev/null +++ b/config/services.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + ContaoAcademy\ZammadNCApiBundle\: + resource: ../src/ + exclude: ../src/{ContaoManager,DependencyInjection} diff --git a/contao/dca/tl_nc_gateway.php b/contao/dca/tl_nc_gateway.php new file mode 100644 index 0000000..a460940 --- /dev/null +++ b/contao/dca/tl_nc_gateway.php @@ -0,0 +1,73 @@ + 'text', + 'eval' => [ + 'tl_class' => 'w50', + 'maxlength' => 255, + 'rgxp' => 'httpurl', + 'mandatory' => true, + ], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''], +]; + +$GLOBALS['TL_DCA']['tl_nc_gateway']['fields']['zammadAuthType'] = [ + 'inputType' => 'select', + 'eval' => [ + 'tl_class' => 'clr w50', + 'submitOnChange' => true, + 'includeBlankOption' => true, + 'mandatory' => true, + ], + 'options' => ['basic', 'token'], + 'reference' => &$GLOBALS['TL_LANG']['tl_nc_gateway']['zammadAuthTypes'], + 'exclude' => true, + 'sql' => ['type' => 'string', 'length' => 8, 'default' => ''], +]; + +$GLOBALS['TL_DCA']['tl_nc_gateway']['fields']['zammadToken'] = [ + 'inputType' => 'text', + 'eval' => [ + 'tl_class' => 'w50', + 'maxlength' => 255, + 'mandatory' => true, + ], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''], +]; + +$GLOBALS['TL_DCA']['tl_nc_gateway']['fields']['zammadUser'] = [ + 'inputType' => 'text', + 'eval' => [ + 'tl_class' => 'w50', + 'maxlength' => 255, + 'mandatory' => true, + ], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''], +]; + +$GLOBALS['TL_DCA']['tl_nc_gateway']['fields']['zammadPassword'] = [ + 'inputType' => 'text', + 'eval' => [ + 'tl_class' => 'w50', + 'maxlength' => 255, + 'mandatory' => true, + ], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''], +]; diff --git a/contao/dca/tl_nc_message.php b/contao/dca/tl_nc_message.php new file mode 100644 index 0000000..00a0c4c --- /dev/null +++ b/contao/dca/tl_nc_message.php @@ -0,0 +1,58 @@ + 'text', + 'eval' => ['mandatory' => true, 'maxlength' => 255, 'tl_class' => 'clr w50'], + 'exclude' => true, + 'nc_context' => TokenContext::Email, + 'sql' => ['type' => 'string', 'length' => 255, 'default' => ''], +]; + +$GLOBALS['TL_DCA']['tl_nc_message']['fields']['zammad_params'] = [ + 'exclude' => true, + 'inputType' => 'keyValueWizard', + 'eval' => ['tl_class' => 'clr'], + 'sql' => ['type' => 'blob', 'notnull' => false], +]; + +$GLOBALS['TL_DCA']['tl_nc_message']['fields']['zammad_title'] = [ + 'exclude' => true, + 'inputType' => 'text', + 'default' => '', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 64, 'mandatory' => true], + 'nc_context' => TokenContext::Text, + 'sql' => ['type' => 'string', 'length' => 64, 'default' => ''], +]; + +$GLOBALS['TL_DCA']['tl_nc_message']['fields']['zammad_group'] = [ + 'exclude' => true, + 'inputType' => 'text', + 'default' => '', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 64, 'mandatory' => true], + 'nc_context' => TokenContext::Text, + 'sql' => ['type' => 'string', 'length' => 64, 'default' => ''], +]; + +$GLOBALS['TL_DCA']['tl_nc_message']['fields']['zammad_body'] = [ + 'exclude' => true, + 'inputType' => 'textarea', + 'default' => '', + 'eval' => ['mandatory' => true, 'tl_class' => 'clr'], + 'nc_context' => TokenContext::Html, + 'sql' => ['type' => 'text', 'length' => MySQLPlatform::LENGTH_LIMIT_TEXT, 'notnull' => false], +]; + +$GLOBALS['TL_DCA']['tl_nc_message']['palettes']['zammad'] = '{title_legend},title,gateway;{zammad_customer_legend},zammad_email,zammad_params;{zammad_ticket_legend},zammad_title,zammad_group,zammad_body;{publish_legend},published'; diff --git a/contao/languages/de/tl_nc_gateway.php b/contao/languages/de/tl_nc_gateway.php new file mode 100644 index 0000000..5dca1c8 --- /dev/null +++ b/contao/languages/de/tl_nc_gateway.php @@ -0,0 +1,23 @@ +sets([__DIR__.'/vendor/contao/easy-coding-standard/config/contao.php']); + + $ecsConfig->paths([ + __DIR__.'/src', + __DIR__.'/contao', + ]); + + $ecsConfig->ruleWithConfiguration(HeaderCommentFixer::class, [ + 'header' => "This file is part of the Contao Zammad Gateway extension.\n\n(c) Contao Academy\n\n@license LGPL-3.0-or-later", + ]); + + $ecsConfig->parallel(); + $ecsConfig->lineEnding("\n"); + $ecsConfig->cacheDirectory(sys_get_temp_dir().'/ecs_default_cache'); +}; diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..bdff68d --- /dev/null +++ b/rector.php @@ -0,0 +1,17 @@ +sets([__DIR__.'/vendor/contao/rector/config/contao.php']); + + $rectorConfig->paths([ + __DIR__.'/src', + __DIR__.'/contao', + ]); + + $rectorConfig->parallel(); + $rectorConfig->cacheDirectory(sys_get_temp_dir().'/rector_cache'); +}; diff --git a/src/Config/ZammadMessageConfig.php b/src/Config/ZammadMessageConfig.php new file mode 100644 index 0000000..1e58833 --- /dev/null +++ b/src/Config/ZammadMessageConfig.php @@ -0,0 +1,43 @@ +getString('email'); + } + + public function getParameters(): array + { + return $this->get('parameters', []); + } + + public function getTitle(): string + { + return $this->getString('title'); + } + + public function getGroup(): string + { + return $this->getString('group'); + } + + public function getBody(): string + { + return $this->getString('body'); + } +} diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php index 84b8dc0..d5b79c0 100755 --- a/src/ContaoManager/Plugin.php +++ b/src/ContaoManager/Plugin.php @@ -1,32 +1,31 @@ setLoadAfter(['Contao\CoreBundle\ContaoCoreBundle', 'notification_center']) - ->setReplace(['contao-zammad-nc-api-bundle']), + BundleConfig::create(ZammadNCApiBundle::class) + ->setLoadAfter([ContaoCoreBundle::class, Terminal42NotificationCenterBundle::class]), ]; } - - - public function getRouteCollection( LoaderResolverInterface $resolver, KernelInterface $kernel ) { - return $resolver - ->resolve( __DIR__ . '/../Resources/config/routing.yml' ) - ->load( __DIR__ . '/../Resources/config/routing.yml' ); - } -} \ No newline at end of file +} diff --git a/src/ContaoacademyZammadNCApiBundle.php b/src/ContaoacademyZammadNCApiBundle.php deleted file mode 100755 index 1b19d5c..0000000 --- a/src/ContaoacademyZammadNCApiBundle.php +++ /dev/null @@ -1,8 +0,0 @@ -load('services.yaml') + ; + } +} diff --git a/src/Exception/ZammadGatewayException.php b/src/Exception/ZammadGatewayException.php new file mode 100644 index 0000000..08fc6e7 --- /dev/null +++ b/src/Exception/ZammadGatewayException.php @@ -0,0 +1,17 @@ +strRequest = \Config::get('zammadHost'); - $this->strGroup = $objMessage->zammad_group ?: 'Users'; - $this->arrHttpHeader[] = 'Content-Type: application/json'; - - $objCurl = curl_init(); - $this->authentication( $objCurl ); - curl_setopt( $objCurl, CURLOPT_HTTPHEADER, $this->arrHttpHeader ); - curl_setopt( $objCurl, CURLOPT_RETURNTRANSFER, true ); - $this->createUser( $arrTokens, $objCurl ); - $this->createTicket( $arrTokens, $objCurl ); - curl_close( $objCurl ); - } - - - protected function authentication( &$objCurl ) { - - switch ( \Config::get('zammadAuthType') ) { - - case 'basic': - - $this->strPassword = \Config::get('zammadPassword'); - $this->strUsername = \Config::get('zammadUser'); - curl_setopt( $objCurl, CURLOPT_USERPWD, $this->strUsername . ":" . $this->strPassword ); - - break; - - case 'token': - - $this->strToken = \Config::get('zammadToken'); - $this->arrHttpHeader[] = 'Authorization: Token token=' . $this->strToken; - - break; - } - } - - - protected function createTicket( $arrTokens, $objCurl ) { - - $strRequest = $this->strRequest . '/api/v1/tickets'; - - $arrRequest = [ - 'title' => $arrTokens['form_subject'], - 'group' => $this->strGroup, - 'article' => [ - 'subject' => $arrTokens['form_subject'], - 'body' => $this->collectBodyData( $arrTokens ), - 'type' => 'web', - 'internal' => false, - 'to' => $arrTokens['form_email'] - ], - 'customer' => $arrTokens['form_email'], - 'note' => $arrTokens['form_subject'] - ]; - - $arrCustomHeader = $this->arrHttpHeader; // get standard header - $arrCustomHeader[] = 'X-On-Behalf-Of:' . $arrTokens['form_email']; - - curl_setopt( $objCurl, CURLOPT_HTTPHEADER, $arrCustomHeader ); - curl_setopt( $objCurl, CURLOPT_URL, $strRequest ); - curl_setopt( $objCurl, CURLOPT_POST, 1 ); - curl_setopt( $objCurl, CURLOPT_POSTFIELDS, json_encode( $arrRequest, 512 ) ); - - $objResponse = curl_exec( $objCurl ); - - \System::log( $objResponse, __METHOD__, TL_GENERAL ); - } - - - protected function createUser( $arrTokens, $objCurl ) { - - $strRequest = $this->strRequest . '/api/v1/users/search'; - curl_setopt( $objCurl, CURLOPT_URL, $strRequest . '?query=' . $arrTokens['form_email'] . '&limit=1' ); - $strUserResult = curl_exec( $objCurl ); - $arrResults = json_decode( $strUserResult, 512 ); - - if ( empty( $arrResults ) ) { - - $arrRequest = []; - $strRequest = $this->strRequest . '/api/v1/users'; - - foreach ( $this->arrApiFields as $strFieldname ) { - - $arrRequest[ $strFieldname ] = $arrTokens[ 'form_' . $strFieldname ] ?: ''; - } - - curl_setopt( $objCurl, CURLOPT_URL, $strRequest ); - curl_setopt( $objCurl, CURLOPT_POST, 1 ); - curl_setopt( $objCurl, CURLOPT_POSTFIELDS, json_encode( $arrRequest, 512 ) ); - $objResponse = curl_exec( $objCurl ); - - \System::log( $objResponse, __METHOD__, TL_GENERAL ); - } - } - - - protected function collectBodyData( $arrTokens ) { - - global $objPage; - - $strBody = $arrTokens['form_body'] . PHP_EOL . PHP_EOL; - - foreach ( $arrTokens as $strName => $strValue ) { - - if ( strpos( $strName, 'form_' ) === false ) { - - continue; - } - - $strName = substr( $strName, 5 ); - - if ( in_array( $strName, $this->arrApiFields ) ) { - - continue; - } - - if ( in_array( $strName, [ 'body', 'subject' ] ) ) { - - continue; - } - - $strName = ucfirst( $strName ); - $strBody .= $strName . ': ' . $strValue . PHP_EOL; - } - - $strBody .= 'Alias: ' . $objPage->alias; - - return $strBody; - } -} \ No newline at end of file diff --git a/src/Gateway/ZammadGateway.php b/src/Gateway/ZammadGateway.php new file mode 100644 index 0000000..d61b024 --- /dev/null +++ b/src/Gateway/ZammadGateway.php @@ -0,0 +1,188 @@ +withStamp($this->createZammadMessageStamp($parcel)) + ->seal() + ; + } + + public function sendParcel(Parcel $parcel): Receipt + { + try { + $client = $this->createClientForGateway($parcel->getStamp(GatewayConfigStamp::class)->gatewayConfig); + $config = $parcel->getStamp(ZammadMessageStamp::class)->zammadMessageConfig; + $customerId = $this->getCustomerId($client, $config); + + // Create the ticket + $response = $client->request( + 'POST', + '/api/v1/tickets', + [ + 'json' => [ + 'customer_id' => $customerId, + 'title' => $config->getTitle(), + 'group' => $config->getGroup(), + 'article' => [ + 'subject' => $config->getTitle(), + 'body' => $config->getBody(), + 'type' => 'web', + 'internal' => false, + ], + ], + 'headers' => [ + 'X-On-Behalf-Of' => $customerId, + ], + ], + ); + + $result = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + + $this->contaoGeneralLogger->info(sprintf('Zammad ticket #%s ("%s") created.', $result['id'], $result['title'])); + + return Receipt::createForSuccessfulDelivery($parcel); + } catch (\Throwable $e) { + if ($this->kernel->isDebug()) { + throw $e; + } + + $this->contaoErrorLogger->error('Zammad API Request failed: '.$e->getMessage(), ['exception' => $e]); + + return Receipt::createForUnsuccessfulDelivery( + $parcel, + CouldNotDeliverParcelException::becauseOfGatewayException(self::NAME, exception: $e), + ); + } + } + + private function createZammadMessageStamp(Parcel $parcel): ZammadMessageStamp + { + $this->contaoFramework->initialize(); + + $messageConfig = $parcel->getMessageConfig(); + $tokens = $parcel->getStamp(TokenCollectionStamp::class)->tokenCollection->forSimpleTokenParser(); + $email = Util::recursiveReplaceTokensAndTags($messageConfig->getString('zammad_email'), $tokens); + + if (!$email || !Validator::isEmail($email)) { + throw new ZammadGatewayException('Invalid email address "'.$email.'" given.'); + } + + if (!$title = Util::recursiveReplaceTokensAndTags($messageConfig->getString('zammad_title'), $tokens)) { + throw new ZammadGatewayException('No title given!'); + } + + if (!$group = Util::recursiveReplaceTokensAndTags($messageConfig->getString('zammad_group'), $tokens)) { + throw new ZammadGatewayException('No group given!'); + } + + $parameters = []; + + foreach (StringUtil::deserialize($messageConfig->getString('zammad_params'), true) as $param) { + $key = Util::recursiveReplaceTokensAndTags((string) $param['key'], $tokens); + $value = Util::recursiveReplaceTokensAndTags((string) $param['value'], $tokens); + $parameters[$key] = $value; + } + + return ZammadMessageStamp::fromArray([ + 'email' => $email, + 'parameters' => $parameters, + 'title' => $title, + 'group' => $group, + 'body' => Util::recursiveReplaceTokensAndTags($messageConfig->getString('zammad_body'), $tokens), + ]); + } + + private function createClientForGateway(GatewayConfig $gatewayConfig): HttpClientInterface + { + $options = ['base_uri' => $gatewayConfig->getString('zammadHost')]; + + match ($gatewayConfig->getString('zammadAuthType')) { + 'basic' => $options['auth_basic'] = [$gatewayConfig->getString('zammadUser'), $gatewayConfig->getString('zammadPassword')], + 'token' => $options['auth_bearer'] = $gatewayConfig->getString('zammadToken'), + default => throw new ZammadGatewayException('Invalid authentication type given.'), + }; + + return $this->client->withOptions($options); + } + + private function getCustomerId(HttpClientInterface $client, ZammadMessageConfig $config): string + { + // Search for the customer first + $response = $client->request( + 'GET', + '/api/v1/users/search', + [ + 'query' => [ + 'query' => $config->getEmail(), + 'limit' => 1, + ], + ], + ); + + if ($result = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR)) { + // Customer already exists + return (string) $result[0]['id']; + } + + // Create the customer + $parameters = $config->getParameters(); + $parameters['email'] = $config->getEmail(); + + $response = $client->request('POST', '/api/v1/users', ['json' => $parameters]); + + $result = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR); + + $this->contaoGeneralLogger->info(sprintf('Zammad customer #%s (%s) created.', $result['id'], $result['email'])); + + return (string) $result['id']; + } +} diff --git a/src/Migration/ZammadGatewayMigration.php b/src/Migration/ZammadGatewayMigration.php new file mode 100644 index 0000000..0f8cd6e --- /dev/null +++ b/src/Migration/ZammadGatewayMigration.php @@ -0,0 +1,75 @@ +getLegacyConfigs()) { + return false; + } + + $schemaManager = $this->db->createSchemaManager(); + + if (!$schemaManager->tablesExist(['tl_nc_gateway'])) { + return false; + } + + $columns = array_map(static fn (Column $column): string => $column->getName(), $schemaManager->listTableColumns('tl_nc_gateway')); + + if (array_diff(self::$fields, $columns)) { + return false; + } + + return (bool) $this->db->fetchOne("SELECT TRUE FROM tl_nc_gateway WHERE type = ? AND zammadAuthType = ''", [ZammadGateway::NAME]); + } + + public function run(): MigrationResult + { + $this->db->update('tl_nc_gateway', $this->getLegacyConfigs(), ['zammadAuthType' => '']); + + return $this->createResult(true); + } + + private function getLegacyConfigs(): array + { + $this->contaoFramework->initialize(); + + $configs = []; + + foreach (self::$fields as $field) { + if ($value = Config::get($field)) { + $configs[$field] = $value; + } + } + + return $configs; + } +} diff --git a/src/Parcel/Stamp/ZammadMessageStamp.php b/src/Parcel/Stamp/ZammadMessageStamp.php new file mode 100644 index 0000000..1ef603e --- /dev/null +++ b/src/Parcel/Stamp/ZammadMessageStamp.php @@ -0,0 +1,30 @@ +zammadMessageConfig); + } + + public static function fromArray(array $data): StampInterface + { + return new self(ZammadMessageConfig::fromArray($data)); + } +} diff --git a/src/Resources/config/routing.yml b/src/Resources/config/routing.yml deleted file mode 100755 index e69de29..0000000 diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml deleted file mode 100755 index e69de29..0000000 diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php deleted file mode 100755 index 0473c5b..0000000 --- a/src/Resources/contao/config/config.php +++ /dev/null @@ -1,3 +0,0 @@ - &$GLOBALS['TL_LANG']['tl_nc_message']['zammad_group'], - 'exclude' => true, - 'inputType' => 'text', - 'default' => '', - 'eval' => array('tl_class'=>'w50'), - 'sql' => "varchar(64) NOT NULL default ''" -]; \ No newline at end of file diff --git a/src/Resources/contao/dca/tl_settings.php b/src/Resources/contao/dca/tl_settings.php deleted file mode 100755 index 9ea98a8..0000000 --- a/src/Resources/contao/dca/tl_settings.php +++ /dev/null @@ -1,51 +0,0 @@ - &$GLOBALS['TL_LANG']['tl_settings']['zammadAuthType'], - 'inputType' => 'radio', - 'eval' => [ - 'tl_class' => 'clr', - 'submitOnChange' => true, - ], - 'options' => [ 'basic', 'token' ], - 'reference' => &$GLOBALS['TL_LANG']['tl_settings']['reference']['zammadAuthType'], - 'exclude' => true -]; - -$GLOBALS['TL_DCA']['tl_settings']['fields']['zammadToken'] = [ - 'label' => &$GLOBALS['TL_LANG']['tl_settings']['zammadToken'], - 'inputType' => 'text', - 'eval' => [ - 'tl_class' => 'w50' - ] -]; - -$GLOBALS['TL_DCA']['tl_settings']['fields']['zammadHost'] = [ - 'label' => &$GLOBALS['TL_LANG']['tl_settings']['zammadHost'], - 'inputType' => 'text', - 'eval' => [ - 'tl_class' => 'w50' - ] -]; - -$GLOBALS['TL_DCA']['tl_settings']['fields']['zammadUser'] = [ - 'label' => &$GLOBALS['TL_LANG']['tl_settings']['zammadUser'], - 'inputType' => 'text', - 'eval' => [ - 'tl_class' => 'w50' - ] -]; - -$GLOBALS['TL_DCA']['tl_settings']['fields']['zammadPassword'] = [ - 'label' => &$GLOBALS['TL_LANG']['tl_settings']['zammadPassword'], - 'inputType' => 'text', - 'eval' => [ - 'tl_class' => 'w50' - ] -]; \ No newline at end of file diff --git a/src/Resources/contao/languages/de/tl_nc_gateway.php b/src/Resources/contao/languages/de/tl_nc_gateway.php deleted file mode 100644 index 33f56d5..0000000 --- a/src/Resources/contao/languages/de/tl_nc_gateway.php +++ /dev/null @@ -1,3 +0,0 @@ -recursiveReplaceTokensAndTags($text, $tokens, $textFlags); + } + + return StringUtil::recursiveReplaceTokensAndTags($text, $tokens, $textFlags); + } +} diff --git a/src/ZammadNCApiBundle.php b/src/ZammadNCApiBundle.php new file mode 100644 index 0000000..f740a70 --- /dev/null +++ b/src/ZammadNCApiBundle.php @@ -0,0 +1,23 @@ +