18 | "forin": false,
19 | // Tolerate `for in` loops without `hasOwnPrototype`.
20 | "indent": 4,
21 | // Specify indentation spacing
22 | "immed": true,
23 | "jquery": true,
24 | "latedef": true,
25 | "newcap": true,
26 | "noarg": true,
27 | // Prohibit use of `arguments.caller` and `arguments.callee`
28 | "noempty": true,
29 | // Prohibit use of empty blocks.
30 | "nonew": true,
31 | // Prohibit use of constructors for side-effects.
32 | "strict": true,
33 | // Require `use strict` pragma in every file.
34 | "undef": true,
35 | // Require all non-global variables be declared before they are used.
36 | "unused": true
37 | }
38 |
--------------------------------------------------------------------------------
/Resources/Private/Language/fr.locallang_module.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Newsletter
8 | Newsletter
9 |
10 |
11 | Newsletter
12 | Newsletter
13 |
14 |
15 | Newsletter control module. This module allows you to control the newsletter settings.
16 | Module de Newsletter. Ce module permet d'envoyer des pages en tant que Newsletter et de voir les statistiques d'envoi.
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/PlainConverter/Lynx.php:
--------------------------------------------------------------------------------
1 | injectBaseUrl($content, $baseUrl);
26 |
27 | file_put_contents($tmpFile, $contentWithBase);
28 |
29 | $cmd = escapeshellcmd(Tools::confParam('path_to_lynx')) . ' -force_html -dump ' . escapeshellarg($tmpFile);
30 | exec($cmd, $output);
31 | unlink($tmpFile);
32 | $plainText = implode("\n", $output);
33 |
34 | return $plainText;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/Unit/Utility/UriTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(Uri::isAbsolute($scheme . ':foo/bar'));
30 | $this->assertTrue(Uri::isAbsolute($scheme . '://foo/bar'));
31 | $this->assertFalse(Uri::isAbsolute('foo/bar/' . $scheme));
32 | }
33 |
34 | public function testFragment()
35 | {
36 | $this->assertTrue(Uri::isAbsolute('http:foo/bar#abc'));
37 | $this->assertTrue(Uri::isAbsolute('#abc'), 'fragment URL should be considered absolute URI because they msut not be modified at all');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecodev/newsletter",
3 | "type": "typo3-cms-extension",
4 | "description": "TYPO3 extension to send any pages as Newsletter and provide statistics on opened emails and clicked links.",
5 | "homepage": "https://github.com/ecodev/newsletter",
6 | "license": "GPL-2.0+",
7 | "keywords": [
8 | "TYPO3 CMS",
9 | "TYPO3",
10 | "newsletter",
11 | "email",
12 | "statistics"
13 | ],
14 | "require": {
15 | "php": ">=5.6.0",
16 | "ext-dom": "*",
17 | "ext-openssl": "*",
18 | "typo3/cms-backend": "^7.6 || ^8.7",
19 | "typo3/cms-core": "^7.6 || ^8.7",
20 | "typo3/cms-extbase": "^7.6 || ^8.7",
21 | "typo3/cms-fluid": "^7.6 || ^8.7",
22 | "typo3/cms-scheduler": "^7.6 || ^8.7"
23 | },
24 | "require-dev": {
25 | "friendsofphp/php-cs-fixer": "^2.1"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "Ecodev\\Newsletter\\": "Classes/"
30 | }
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "Ecodev\\Newsletter\\Tests\\": "Tests/"
35 | }
36 | },
37 | "extra": {
38 | "typo3/cms": {
39 | "cms-package-dir": "{$vendor-dir}/typo3/cms",
40 | "web-dir": "web"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Classes/Tca/EmailTca.php:
--------------------------------------------------------------------------------
1 | ';
27 | $result .= '';
28 | foreach ($keys as $key) {
29 | $result .= '| ' . $key . ' | ';
30 | }
31 | $result .= '
';
32 |
33 | $result .= '';
34 | foreach ($data as $value) {
35 | $result .= '| ' . $value . ' | ';
36 | }
37 | $result .= '
';
38 | $result .= '';
39 |
40 | return $result;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/RecipientList/AbstractArray.php:
--------------------------------------------------------------------------------
1 | data);
17 | next($this->data);
18 |
19 | if (is_array($r)) {
20 | if (!isset($r['plain_only'])) {
21 | $r['plain_only'] = $this->getPlainOnly();
22 | }
23 |
24 | return $r;
25 | }
26 |
27 | return false;
28 | }
29 |
30 | public function getCount()
31 | {
32 | return count($this->data);
33 | }
34 |
35 | public function getError()
36 | {
37 | if (!is_array($this->data)) {
38 | return 'Not an array';
39 | }
40 |
41 | if (count($this->data) == 0) {
42 | return 'No data fetched';
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var shell = require('gulp-shell');
3 |
4 | var paths = {
5 | js: [
6 | 'Resources/Public/JavaScript/**/*.js',
7 | '!Resources/Public/JavaScript/NVD3/*',
8 | '!Resources/Public/JavaScript/Override/*',
9 | ],
10 | php: [
11 | '**/*.php',
12 | '!3dparty/*',
13 | '!vendor/*',
14 | ],
15 | };
16 |
17 | gulp.task('default', ['composer', 'lint-js', 'lint-php'], function() {
18 | // place code for your default task here
19 | });
20 |
21 | gulp.task('lint-js', function() {
22 | var jshint = require('gulp-jshint');
23 | var jscs = require('gulp-jscs');
24 |
25 | return gulp.src(paths.js)
26 | .pipe(jscs({configPath: '.jscs.json'}))
27 | .pipe(jshint())
28 | .pipe(jshint.reporter());
29 | });
30 |
31 | gulp.task('lint-php', ['composer'], shell.task([
32 | './vendor/bin/php-cs-fixer fix --diff --verbose --dry-run'
33 | ]));
34 |
35 | gulp.task('composer', function() {
36 | var composer = require('gulp-composer');
37 | return composer('install', {});
38 | });
39 |
40 | gulp.task('watch', function() {
41 | gulp.watch(paths.js, ['lint-js']);
42 | gulp.watch(paths.php, ['lint-php']);
43 | });
44 |
45 |
--------------------------------------------------------------------------------
/Tests/Unit/Domain/Model/PlainConverter/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 | First title
11 | Second title
12 |
13 | Muffin cake tootsie roll chocolate cake cookie. Gummi bears sugar
14 | plum powder icing jujubes tart danish caramels. Cotton candy jujubes
15 | pudding chocolate ice cream sesame snaps. Souffle cookie marzipan
16 | chocolate cake croissant lemon drops.
17 | Icing souffle sugar plum ice cream marzipan. Jelly tart powder
18 | fruitcake oat cake halvah chocolate cake halvah icing. Gummies
19 | candy canes liquorice. Dragee pastry sesame snaps cake cake
20 | gingerbread.
21 |
22 |
23 | - item 1
24 | - item 2
25 | - item 3
26 |
27 |
28 |
29 | first link
30 | second link
31 | third identical link
32 | relative link
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Tests/Unit/Domain/Model/RecipientList/CsvListTest.php:
--------------------------------------------------------------------------------
1 | subject = new CsvList();
15 | }
16 |
17 | /**
18 | * @test
19 | */
20 | public function getCsvSeparatorReturnsInitialValueForString()
21 | {
22 | $this->assertSame(',', $this->subject->getCsvSeparator());
23 | }
24 |
25 | /**
26 | * @test
27 | */
28 | public function getCsvValuesReturnsInitialValueForString()
29 | {
30 | $this->assertSame('', $this->subject->getCsvValues());
31 | }
32 |
33 | /**
34 | * @test
35 | */
36 | public function setCsvValuesForStringSetsCsvValues()
37 | {
38 | $this->subject->setCsvValues('Conceived at T3CON10');
39 | $this->assertAttributeSame('Conceived at T3CON10', 'csvValues', $this->subject);
40 | }
41 |
42 | protected function prepareDataForEnumeration()
43 | {
44 | $values = file_get_contents(__DIR__ . '/data.csv');
45 |
46 | $this->subject->setCsvValues($values);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Resources/Public/Icons/tx_newsletter_domain_model_bounceaccount.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | email
8 |
9 |
10 | server
11 |
12 |
13 | protocol
14 |
15 |
16 | username
17 |
18 |
19 | password
20 |
21 |
22 | port
23 |
24 |
25 | fetchmail config
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Store/Recipient.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store');
5 |
6 | /**
7 | * A Store for the selectedRecipientList model using ExtDirect to communicate with the
8 | * server side extbase framework.
9 | */
10 | Ext.ux.Ecodev.Newsletter.Store.Recipient = (function () {
11 |
12 | var selectedRecipientListStore = null;
13 |
14 | var initialize = function () {
15 | if (selectedRecipientListStore === null) {
16 | selectedRecipientListStore = new Ext.data.DirectStore({
17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\Recipient',
18 | // Here the JsonReader will be configured by metadata sent by server-side, because the columns available not known in advance
19 | reader: new Ext.data.JsonReader(),
20 | api: {
21 | read: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.listRecipientAction,
22 | },
23 | paramOrder: {
24 | read: ['data', 'start', 'limit'],
25 | },
26 | });
27 | }
28 | };
29 |
30 | /**
31 | * Public API of this singleton.
32 | */
33 | return {
34 | initialize: initialize,
35 | };
36 | }());
37 | }());
38 |
--------------------------------------------------------------------------------
/Resources/Public/Icons/tx_newsletter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_email.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | startTime
8 |
9 |
10 | endTime
11 |
12 |
13 | recipientAddress
14 |
15 |
16 | recipientData
17 |
18 |
19 | opened
20 |
21 |
22 | bounced
23 |
24 |
25 | newsletter
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/RecipientList/BeUsers.php:
--------------------------------------------------------------------------------
1 | beUsers = $beUsers;
27 | }
28 |
29 | /**
30 | * Getter for beUsers
31 | *
32 | * @return string beUsers
33 | */
34 | public function getBeUsers()
35 | {
36 | return $this->beUsers;
37 | }
38 |
39 | /**
40 | * Returns the tablename to work with
41 | *
42 | * @return string
43 | */
44 | protected function getTableName()
45 | {
46 | return 'be_users';
47 | }
48 |
49 | public function init()
50 | {
51 | $config = explode(',', $this->getBeUsers());
52 | $config[] = -1;
53 | $config = array_filter($config);
54 |
55 | $this->data = Tools::getDatabaseConnection()->sql_query(
56 | 'SELECT email, realName, username, lang, admin FROM be_users
57 | WHERE uid IN (' . implode(',', $config) . ")
58 | AND email <> ''
59 | AND disable = 0
60 | AND tx_newsletter_bounce < 10");
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Classes/Domain/Repository/AbstractRepository.php:
--------------------------------------------------------------------------------
1 | getQuerySettings()->setRespectStoragePage(false);
22 |
23 | return $query;
24 | }
25 |
26 | /**
27 | * Override parent method to update the object and persist changes immediately. By commiting immediately
28 | * stay compatible with raw sql query via $TYPO3_DB.
29 | * TODO this method should be destroyed once "old code" is completely replaced with extbase concepts
30 | *
31 | * @param \TYPO3\CMS\Extbase\DomainObject\AbstractEntity $modifiedObject
32 | */
33 | public function update($modifiedObject)
34 | {
35 | parent::update($modifiedObject);
36 | $this->persistenceManager->persistAll();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Resources/Public/Styles/flashmessages.css:
--------------------------------------------------------------------------------
1 | @CHARSET "UTF-8";
2 |
3 | /**
4 | * FLASH-Messenge Boxes
5 | */
6 |
7 | .flashmessage h3 {
8 | background-repeat: no-repeat;
9 | padding-left: 20px;
10 | }
11 |
12 | div.flashmessage {
13 | padding-left: 20px;
14 | margin-top: 10px;
15 | border: 1px solid #CCC;
16 | -webkit-border-radius: 4px;
17 | -moz-border-radius: 4px;
18 | border-radius: 4px;
19 | background-color: white;
20 | box-shadow: 2px 2px 4px 0px #AEB6C1;
21 | }
22 |
23 | div.messenge-box h3 {
24 | margin-bottom: 10px;
25 | background-color: #FFFFFF;
26 | }
27 |
28 | div.messenge-box p {
29 | margin-left: 20px;
30 | padding-bottom: 5px;
31 | }
32 |
33 | .message-ok {
34 | color: #3b7826 !important;
35 | background-image: url("../Icons/ok.svg") !important;
36 | }
37 |
38 | .message-notice {
39 | color: #777 !important;
40 | background-image: url("../Icons/information.svg") !important;
41 | }
42 |
43 | .message-information {
44 | color: #4c73a1 !important;
45 | background-image: url("../Icons/information.svg") !important;
46 | }
47 |
48 | .message-ok {
49 | color: #3b7826 !important;
50 | background-image: url("../Icons/ok.svg") !important;
51 | }
52 |
53 | .message-warning {
54 | color: #9e7d4a !important;
55 | background-image: url("../Icons/warning.svg") !important;
56 | }
57 |
58 | .message-error {
59 | color: #aa0225 !important;
60 | background-image: url("../Icons/error.svg") !important;
61 | }
62 |
--------------------------------------------------------------------------------
/Resources/Public/Icons/tx_newsletter_domain_model_link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Classes/Domain/Repository/BounceAccountRepository.php:
--------------------------------------------------------------------------------
1 | get(ConfigurationManager::class);
22 | $storagePid = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'newsletter', 'storagePid');
23 |
24 | if ($storagePid['storagePid']) {
25 | $query->getQuerySettings()->setRespectStoragePage(true);
26 | }
27 |
28 | return $query;
29 | }
30 |
31 | /**
32 | * Returns the first BounceAccount or null if none at all
33 | *
34 | * @return BounceAccount
35 | */
36 | public function findFirst()
37 | {
38 | $query = $this->createQuery();
39 |
40 | $bounceAccount = $query->setLimit(1)
41 | ->execute()
42 | ->getFirst();
43 |
44 | return $bounceAccount;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Resources/Public/Icons/tx_newsletter_domain_model_newsletter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Statistics/Statistics.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Statistics');
5 |
6 | /**
7 | * @class Ext.ux.Ecodev.Newsletter.Statistics.Statistics
8 | * @namespace Ext.ux.Ecodev.Newsletter.Statistics
9 | * @extends Ext.Container
10 | *
11 | * Class for statistic container
12 | */
13 | Ext.ux.Ecodev.Newsletter.Statistics.Statistics = Ext.extend(Ext.Container, {
14 | initComponent: function () {
15 | var config = {
16 | layout: 'border',
17 | title: Ext.ux.Ecodev.Newsletter.Language.statistics_tab,
18 | items: [
19 | {
20 | split: true,
21 | region: 'north',
22 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.NewsletterListMenu',
23 | ref: 'newsletterListMenu',
24 | },
25 | {
26 | region: 'center',
27 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel',
28 | ref: 'statisticsPanel',
29 | },
30 | ],
31 | };
32 | Ext.apply(this, config);
33 | Ext.ux.Ecodev.Newsletter.Statistics.Statistics.superclass.initComponent.call(this);
34 | },
35 | });
36 |
37 | Ext.reg('Ext.ux.Ecodev.Newsletter.Statistics.Statistics', Ext.ux.Ecodev.Newsletter.Statistics.Statistics);
38 | }());
39 |
--------------------------------------------------------------------------------
/Tests/Unit/Domain/Model/PlainConverter/LynxTest.php:
--------------------------------------------------------------------------------
1 | subject = new Lynx();
21 | }
22 |
23 | protected function tearDown()
24 | {
25 | unset($this->subject);
26 | }
27 |
28 | private function canRunLynx()
29 | {
30 | $this->loadConfiguration();
31 |
32 | $cmd = escapeshellcmd(Tools::confParam('path_to_lynx')) . ' --help';
33 | exec($cmd, $output, $statusCode);
34 |
35 | return $statusCode == 0;
36 | }
37 |
38 | /**
39 | * @test
40 | */
41 | public function getUrlReturnsInitialValueForString()
42 | {
43 | if (!$this->canRunLynx()) {
44 | $this->markTestSkipped('The command "' . Tools::confParam('path_to_lynx') . '" is not available.');
45 | }
46 |
47 | $html = file_get_contents(__DIR__ . '/input.html');
48 | $expected = file_get_contents(__DIR__ . '/lynx.txt');
49 | $actual = $this->subject->getPlainText($html, 'http://my-domain.com');
50 | $this->assertSame($expected, $actual);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Classes/MVC/View/ExtDirectView.php:
--------------------------------------------------------------------------------
1 | renderArray();
20 | $this->controllerContext->getResponse()->setResult($result);
21 | $this->controllerContext->getResponse()->setSuccess(true);
22 | }
23 |
24 | /**
25 | * Assigns errors to the view and converts them to a format that Ext JS
26 | * understands.
27 | *
28 | * @param array $errors Errors e.g. from mapping results
29 | */
30 | public function assignErrors(array $errors)
31 | {
32 | $result = [];
33 | foreach ($errors as $argumentName => $argumentError) {
34 | foreach ($argumentError->getErrors() as $propertyName => $propertyError) {
35 | $message = '';
36 | foreach ($propertyError->getErrors() as $error) {
37 | $message .= $error->getMessage();
38 | }
39 | $result[$propertyName] = $message;
40 | }
41 | }
42 | $this->assign('value', [
43 | 'errors' => $result,
44 | 'success' => false,
45 | ]);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/Functional/AbstractFunctionalTestCase.php:
--------------------------------------------------------------------------------
1 | objectManager = GeneralUtility::makeInstance(ObjectManager::class);
26 |
27 | $this->importDataSet(__DIR__ . '/Fixtures/fixtures.xml');
28 | $this->authCode = md5(302 . 'recipient2@example.com');
29 | }
30 |
31 | /**
32 | * Assert that there is exactly 1 record in sys_log table containing
33 | * the exact text given in $details
34 | *
35 | * @param string $details
36 | */
37 | protected function assertRecipientListCallbackWasCalled($details)
38 | {
39 | $db = $this->getDatabaseConnection();
40 | $count = $db->exec_SELECTcountRows('*', 'sys_log', 'details = ' . $db->fullQuoteStr($details, 'sys_log'));
41 | $this->assertSame(1, $count, 'could not find exactly 1 log record containing "' . $details . '"');
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Classes/MVC/ExtDirect/TransactionResponse.php:
--------------------------------------------------------------------------------
1 | result = $result;
36 | }
37 |
38 | /**
39 | * Sette for success.
40 | *
41 | * @param bool $success The success of the called action
42 | */
43 | public function setSuccess($success)
44 | {
45 | $this->success = $success;
46 | }
47 |
48 | /**
49 | * Returns the result of the transaction.
50 | *
51 | * @return mixed The result
52 | */
53 | public function getResult()
54 | {
55 | return $this->result;
56 | }
57 |
58 | /**
59 | * Returns the state (success/fail) of the transaction.
60 | *
61 | * @return bool The success
62 | */
63 | public function getSuccess()
64 | {
65 | return $this->success;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Classes/Controller/ModuleController.php:
--------------------------------------------------------------------------------
1 | pageId = (int) GeneralUtility::_GP('id');
26 | }
27 |
28 | /**
29 | * index action for the module controller
30 | * This will render the HTML needed for ExtJS application
31 | */
32 | public function indexAction()
33 | {
34 | $pageType = '';
35 | $record = Tools::getDatabaseConnection()->exec_SELECTgetSingleRow('doktype', 'pages', 'uid =' . $this->pageId);
36 | if (!empty($record['doktype']) && $record['doktype'] == 254) {
37 | $pageType = 'folder';
38 | } elseif (!empty($record['doktype'])) {
39 | $pageType = 'page';
40 | }
41 |
42 | $configuration = [
43 | 'pageId' => $this->pageId,
44 | 'pageType' => $pageType,
45 | 'emailShowUrl' => UriBuilder::buildFrontendUri($this->pageId, 'Email', 'show'),
46 | ];
47 |
48 | $this->view->assign('configuration', $configuration);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/Unit/Utility/EmailParserTest.php:
--------------------------------------------------------------------------------
1 | parse($emailSource);
42 | $this->assertSame($expectedBounce, $parser->getBounceLevel(), 'Bounce level should be ' . var_export($expectedBounce, true) . ' for ' . $message);
43 | $this->assertSame($expectedAuthCode, $parser->getAuthCode(), 'AuthCode should be ' . var_export($expectedAuthCode, true) . ' for ' . $message);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Documentation/Developer/Index.rst:
--------------------------------------------------------------------------------
1 | .. ==================================================
2 | .. FOR YOUR INFORMATION
3 | .. --------------------------------------------------
4 | .. -*- coding: utf-8 -*- with BOM.
5 |
6 | .. include:: ../Includes.txt
7 |
8 |
9 | .. _developer:
10 |
11 | Developer Corner
12 | ================
13 |
14 | Target group: **Developers**
15 |
16 | This extension is developed on GitHub, if you wish to contribute to the
17 | `project`_ you are most welcome to participate. Be sure to contact the team
18 | before starting to work on significant modifications.
19 |
20 | .. _project: https://github.com/Ecodev/newsletter
21 |
22 | The following figure shows the original model of the extension, while it has
23 | changed overtime, it still gives a good idea of how it the code is structured:
24 |
25 |
26 | .. figure:: ../Images/model.png
27 | :alt: Original model of Newsletter
28 |
29 | .. _developer-hooks:
30 |
31 | Hooks
32 | -----
33 |
34 | There are only two hooks available: ``substituteMarkersHook`` and
35 | ``getConfiguredMailerHook``. See source code for details.
36 |
37 |
38 | If you need additional hooks, post your request to the
39 | projects `issues`_ page on GitHub with a detailed explanation of your use-case.
40 |
41 | .. _issues: https://github.com/Ecodev/newsletter/issues
42 |
43 | .. _developer-api:
44 |
45 | API
46 | ---
47 |
48 | There is currently no published API for this extension however there is a
49 | Doxyfile configuration in the Documentation folder. This file can be used to
50 | generate source documentation in combination with `Doxygen`_.
51 |
52 | .. _Doxygen: http://www.doxygen.org
53 |
--------------------------------------------------------------------------------
/Documentation/Index.rst:
--------------------------------------------------------------------------------
1 | .. ==================================================
2 | .. FOR YOUR INFORMATION
3 | .. --------------------------------------------------
4 | .. -*- coding: utf-8 -*- with BOM.
5 |
6 | .. include:: Includes.txt
7 |
8 | .. _start:
9 |
10 | ==========
11 | Newsletter
12 | ==========
13 |
14 | .. only:: html
15 |
16 | :Classification:
17 | newsletter
18 |
19 | :Language:
20 | en
21 |
22 | :Description:
23 | Manual covering TYPO3 extension Newsletter
24 |
25 | :Keywords:
26 | newsletter, email, scheduling, bulk email
27 |
28 | :Copyright:
29 | 2010-2016
30 |
31 | :Author:
32 | Ecodev
33 |
34 | :Email:
35 | contact@ecodev.ch
36 |
37 | :License:
38 | This document is published under the Open Publication License
39 | available from http://www.opencontent.org/openpub/
40 |
41 | :Support:
42 | If you need help with this extension, commercial support may be obtained
43 | by contacting `ecodev.ch `_.
44 |
45 | :Rendered:
46 | |today|
47 |
48 | The content of this document is related to TYPO3,
49 | a GNU/GPL CMS/Framework available from `www.typo3.org `_.
50 |
51 |
52 |
53 | **Table of Contents**
54 |
55 | .. toctree::
56 | :maxdepth: 3
57 | :titlesonly:
58 |
59 | Introduction/Index
60 | User/Index
61 | Administrator/Index
62 | Configuration/Index
63 | Configuration/Recipient_List_SQL_Examples
64 | Developer/Index
65 | KnownProblems/Index
66 | ToDoList/Index
67 | ChangeLog/Index
68 | Links
69 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/IncludeJsFileViewHelper.php:
--------------------------------------------------------------------------------
1 | -Tags
11 | *
12 | * = Examples =
13 | *
14 | *
15 | *
16 | *
17 | */
18 | class IncludeJsFileViewHelper extends AbstractViewHelper
19 | {
20 | /**
21 | * Calls addJsFile on the Instance of TYPO3\CMS\Core\Page\PageRenderer.
22 | *
23 | * @param string $name the file to include
24 | * @param string $extKey the extension, where the file is located
25 | * @param string $pathInsideExt the path to the file relative to the ext-folder
26 | */
27 | public function render($name = null, $extKey = null, $pathInsideExt = 'Resources/Public/JavaScript/')
28 | {
29 | if ($extKey == null) {
30 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey();
31 | }
32 | if (TYPO3_MODE === 'FE') {
33 | $extPath = ExtensionManagementUtility::extPath($extKey);
34 | $extRelPath = mb_substr($extPath, mb_strlen(PATH_site));
35 | } else {
36 | $extRelPath = ExtensionManagementUtility::extRelPath($extKey);
37 | }
38 | $this->pageRenderer->addJsFile($extRelPath . $pathInsideExt . $name);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Classes/Task/FetchBounces.php:
--------------------------------------------------------------------------------
1 | get(BounceAccountRepository::class);
40 | $bounceAccountCount = count($bounceAccountRepository->findAll());
41 |
42 | return LocalizationUtility::translate('task_fetch_bounce_additional_information', 'newsletter', [$bounceAccountCount]);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Classes/Tca/BounceAccountDataProvider.php:
--------------------------------------------------------------------------------
1 | getDecryptedFieldValue($field, $result['databaseRow'][$field]);
26 | }
27 | }
28 |
29 | return $result;
30 | }
31 |
32 | /**
33 | * Returns the decrypted field value if set.
34 | *
35 | * @param mixed $field
36 | * @param mixed $value
37 | *
38 | * @return string
39 | */
40 | private function getDecryptedFieldValue($field, $value)
41 | {
42 | $default = @$GLOBALS['TCA']['tx_newsletter_domain_model_bounceaccount']['columns'][$field]['config']['default'];
43 |
44 | // Set the value
45 | if (empty($value)) {
46 | if ($default) {
47 | $value = $default;
48 | }
49 | } elseif ($value != $default) {
50 | $value = Tools::decrypt($value);
51 | }
52 |
53 | return $value;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/IncludeCssFileViewHelper.php:
--------------------------------------------------------------------------------
1 | -Tags
11 | *
12 | * = Examples =
13 | *
14 | *
15 | *
16 | *
17 | */
18 | class IncludeCssFileViewHelper extends AbstractViewHelper
19 | {
20 | /**
21 | * Calls addCssFile on the Instance of TYPO3\CMS\Core\Page\PageRenderer.
22 | *
23 | * @param string $name the file to include
24 | * @param string $extKey the extension, where the file is located
25 | * @param string $pathInsideExt the path to the file relative to the ext-folder
26 | *
27 | * @return string the link
28 | */
29 | public function render($name = null, $extKey = null, $pathInsideExt = 'Resources/Public/Styles/')
30 | {
31 | if ($extKey === null) {
32 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey();
33 | }
34 |
35 | if (TYPO3_MODE === 'FE') {
36 | $extPath = ExtensionManagementUtility::extPath($extKey);
37 | $extRelPath = mb_substr($extPath, mb_strlen(PATH_site));
38 | } else {
39 | $extRelPath = ExtensionManagementUtility::extRelPath($extKey);
40 | }
41 |
42 | $this->pageRenderer->addCssFile($extRelPath . $pathInsideExt . $name);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/DirectFlashMessageDispatcher.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter');
5 | /**
6 | * This class fetches Ext.Direct events, fires a event if
7 | * new FlashMessages are available and removes the messages from the
8 | * Ext.Direct response event.
9 | *
10 | * Register your FlashMessage-processing ExtJS component like this:
11 | * Ext.ux.Ecodev.Newsletter.DirectFlashMessages.on('new',function(flashMessages) {
12 | * //do something with incoming FlashMessages
13 | * });
14 | *
15 | */
16 | Ext.ux.Ecodev.Newsletter.DirectFlashMessageDispatcher = (function () {
17 | /**
18 | * @class Ext.util.Observable
19 | */
20 | var directFlashMessages = new Ext.util.Observable();
21 | directFlashMessages.addEvents('new');
22 |
23 | var fetchRemoteMessages = function (event) {
24 | if (event.result && event.result.flashMessages) {
25 | var flashMessages = event.result.flashMessages;
26 | delete event.result.flashMessages;
27 | directFlashMessages.fireEvent('new', flashMessages);
28 | }
29 | };
30 |
31 | var initialize = function () {
32 | Ext.Direct.on('event', fetchRemoteMessages);
33 | };
34 |
35 | return Ext.apply(directFlashMessages, {
36 | initialize: initialize,
37 | addMessage: function (message) {
38 | this.fireEvent('new', [message]);
39 | },
40 | addMessages: function (messages) {
41 | this.fireEvent('new', messages);
42 | },
43 | });
44 | }());
45 | }());
46 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/RecipientList/FePages.php:
--------------------------------------------------------------------------------
1 | fePages = $fePages;
27 | }
28 |
29 | /**
30 | * Getter for fePages
31 | *
32 | * @return string fePages
33 | */
34 | public function getFePages()
35 | {
36 | return $this->fePages;
37 | }
38 |
39 | /**
40 | * Returns the tablename to work with
41 | *
42 | * @return string
43 | */
44 | protected function getTableName()
45 | {
46 | return 'fe_users';
47 | }
48 |
49 | public function init()
50 | {
51 | $config = explode(',', $this->getFePages());
52 | $config[] = -1;
53 | $config = array_filter($config);
54 |
55 | $this->data = Tools::getDatabaseConnection()->sql_query(
56 | 'SELECT DISTINCT email,name,address,telephone,fax,username,fe_users.title,zip,city,country,www,company,pages.title AS pages_title
57 | FROM pages
58 | INNER JOIN fe_users ON pages.uid = fe_users.pid
59 | WHERE pages.uid IN (' . implode(',', $config) . ")
60 | AND email != ''
61 | AND pages.deleted = 0
62 | AND pages.hidden = 0
63 | AND fe_users.disable = 0
64 | AND fe_users.deleted = 0
65 | AND tx_newsletter_bounce < 10");
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/RecipientList/FeGroups.php:
--------------------------------------------------------------------------------
1 | feGroups = $feGroups;
27 | }
28 |
29 | /**
30 | * Getter for feGroups
31 | *
32 | * @return string feGroups
33 | */
34 | public function getFeGroups()
35 | {
36 | return $this->feGroups;
37 | }
38 |
39 | /**
40 | * Returns the tablename to work with
41 | *
42 | * @return string
43 | */
44 | protected function getTableName()
45 | {
46 | return 'fe_users';
47 | }
48 |
49 | public function init()
50 | {
51 | $groups = explode(',', $this->getFeGroups());
52 | $groups[] = -1;
53 | $groups = array_filter($groups);
54 |
55 | $this->data = Tools::getDatabaseConnection()->sql_query(
56 | 'SELECT DISTINCT email,name,address,telephone,fax,username,fe_users.title,zip,city,country,www,company,fe_groups.title AS group_title
57 | FROM fe_groups, fe_users
58 | WHERE fe_groups.uid IN (' . implode(',', $groups) . ")
59 | AND FIND_IN_SET(fe_groups.uid, fe_users.usergroup)
60 | AND email != ''
61 | AND fe_groups.deleted = 0
62 | AND fe_groups.hidden = 0
63 | AND fe_users.disable = 0
64 | AND fe_users.deleted = 0
65 | AND tx_newsletter_bounce < 10");
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/IncludeExtOnReadyFromFileViewHelper.php:
--------------------------------------------------------------------------------
1 | -Tags
12 | *
13 | * = Examples =
14 | *
15 | *
16 | *
17 | *
18 | */
19 | class IncludeExtOnReadyFromFileViewHelper extends AbstractViewHelper
20 | {
21 | /**
22 | * Calls addJsFile on the Instance of TYPO3\CMS\Core\Page\PageRenderer.
23 | *
24 | * @param string $name the file to include
25 | * @param string $extKey the extension, where the file is located
26 | * @param string $pathInsideExt the path to the file relative to the ext-folder
27 | */
28 | public function render($name = 'extOnReady.js', $extKey = null, $pathInsideExt = 'Resources/Public/JavaScript/')
29 | {
30 | if ($extKey == null) {
31 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey();
32 | }
33 | $extPath = ExtensionManagementUtility::extPath($extKey);
34 |
35 | $filePath = $extPath . $pathInsideExt . $name;
36 |
37 | if (!file_exists($filePath)) {
38 | throw new Exception('File not found: ' . $filePath, 1264197781);
39 | }
40 |
41 | $fileContent = file_get_contents($extPath . $pathInsideExt . $name);
42 |
43 | $this->pageRenderer->addExtOnReadyCode($fileContent);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Resources/Private/Language/de.locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | email
8 | E-Mail
9 |
10 |
11 | server
12 | Server
13 |
14 |
15 | protocol
16 | Protokoll
17 |
18 |
19 | username
20 | Benutzername
21 |
22 |
23 | password
24 | Kennwort
25 |
26 |
27 | port
28 | Port
29 |
30 |
31 | fetchmail config
32 | fetchmail Konfigurations
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Statistics/StatisticsPanel.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Statistics');
5 |
6 | /**
7 | * @class Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel
8 | * @namespace Ext.ux.Ecodev.Newsletter.Statistics
9 | * @extends Ext.TabPanel
10 | *
11 | * Class for statistic tab panel
12 | */
13 | Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel = Ext.extend(Ext.TabPanel, {
14 | initComponent: function () {
15 |
16 | var config = {
17 | activeTab: 0,
18 | border: false,
19 | items: [
20 | {
21 | title: Ext.ux.Ecodev.Newsletter.Language.overview_tab,
22 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.OverviewTab',
23 | itemId: 'overviewTab',
24 | },
25 | {
26 | title: Ext.ux.Ecodev.Newsletter.Language.emails_tab,
27 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.EmailTab',
28 | itemId: 'emailTab',
29 | },
30 | {
31 | title: Ext.ux.Ecodev.Newsletter.Language.links_tab,
32 | xtype: 'Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.LinkTab',
33 | itemId: 'linkTab',
34 | },
35 | ],
36 | };
37 | Ext.apply(this, config);
38 | Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel.superclass.initComponent.call(this);
39 | },
40 |
41 | });
42 |
43 | Ext.reg('Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel', Ext.ux.Ecodev.Newsletter.Statistics.StatisticsPanel);
44 | }());
45 |
--------------------------------------------------------------------------------
/Resources/Private/Templates/Module/Index.html:
--------------------------------------------------------------------------------
1 | {namespace newsletter=Ecodev\Newsletter\ViewHelpers}
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Resources/Private/Language/de.locallang_csh_tx_newsletter_domain_model_email.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | startTime
8 | Start Zeit
9 |
10 |
11 | endTime
12 | End Zeit
13 |
14 |
15 | recipientAddress
16 | Empfänger Adresse
17 |
18 |
19 | recipientData
20 | Empfänger Daten
21 |
22 |
23 | opened
24 | geöffnet
25 |
26 |
27 | bounced
28 | zurückgekommen
29 |
30 |
31 | newsletter
32 | Newsletter
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Resources/Public/Icons/tx_newsletter_domain_model_recipientlist.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Tests/Unit/Domain/Model/RecipientList/SqlTest.php:
--------------------------------------------------------------------------------
1 | subject = new Sql();
15 | }
16 |
17 | /**
18 | * @test
19 | */
20 | public function getSqlStatementReturnsInitialValueForString()
21 | {
22 | $this->assertSame('', $this->subject->getSqlStatement());
23 | }
24 |
25 | /**
26 | * @test
27 | */
28 | public function setSqlStatementForStringSetsSqlStatement()
29 | {
30 | $this->subject->setSqlStatement('Conceived at T3CON10');
31 | $this->assertAttributeSame('Conceived at T3CON10', 'sqlStatement', $this->subject);
32 | }
33 |
34 | /**
35 | * @test
36 | */
37 | public function getSqlRegisterBounceReturnsInitialValueForString()
38 | {
39 | $this->assertSame('', $this->subject->getSqlRegisterBounce());
40 | }
41 |
42 | /**
43 | * @test
44 | */
45 | public function setSqlRegisterBounceForStringSetsSqlRegisterBounce()
46 | {
47 | $this->subject->setSqlRegisterBounce('Conceived at T3CON10');
48 | $this->assertAttributeSame('Conceived at T3CON10', 'sqlRegisterBounce', $this->subject);
49 | }
50 |
51 | /**
52 | * @test
53 | */
54 | public function getSqlRegisterClickReturnsInitialValueForString()
55 | {
56 | $this->assertSame('', $this->subject->getSqlRegisterClick());
57 | }
58 |
59 | /**
60 | * @test
61 | */
62 | public function setSqlRegisterClickForStringSetsSqlRegisterClick()
63 | {
64 | $this->subject->setSqlRegisterClick('Conceived at T3CON10');
65 | $this->assertAttributeSame('Conceived at T3CON10', 'sqlRegisterClick', $this->subject);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/Unit/Domain/Model/LinkTest.php:
--------------------------------------------------------------------------------
1 | subject = new Link();
21 | }
22 |
23 | protected function tearDown()
24 | {
25 | unset($this->subject);
26 | }
27 |
28 | /**
29 | * @test
30 | */
31 | public function getUrlReturnsInitialValueForString()
32 | {
33 | $this->assertSame('', $this->subject->getUrl());
34 | }
35 |
36 | /**
37 | * @test
38 | */
39 | public function setUrlForStringSetsUrl()
40 | {
41 | $this->subject->setUrl('Conceived at T3CON10');
42 | $this->assertAttributeSame('Conceived at T3CON10', 'url', $this->subject);
43 | }
44 |
45 | /**
46 | * @test
47 | */
48 | public function setNewsletterForNewsletterSetsNewsletter()
49 | {
50 | $newsletterFixture = new Newsletter();
51 | $this->subject->setNewsletter($newsletterFixture);
52 |
53 | $this->assertAttributeSame(
54 | $newsletterFixture, 'newsletter', $this->subject
55 | );
56 | }
57 |
58 | /**
59 | * @test
60 | */
61 | public function getOpenedCountReturnsInitialValueForInteger()
62 | {
63 | $this->assertSame(
64 | 0, $this->subject->getOpenedCount()
65 | );
66 | }
67 |
68 | /**
69 | * @test
70 | */
71 | public function setOpenedCountForIntegerSetsOpenedCount()
72 | {
73 | $this->subject->setOpenedCount(12);
74 |
75 | $this->assertAttributeSame(
76 | 12, 'openedCount', $this->subject
77 | );
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/Unit/Controller/BounceAccountControllerTest.php:
--------------------------------------------------------------------------------
1 | subject = $this->getMock(BounceAccountController::class, ['redirect', 'forward', 'addFlashMessage', 'translate', 'flushFlashMessages'], [], '', false);
23 | }
24 |
25 | protected function tearDown()
26 | {
27 | unset($this->subject);
28 | }
29 |
30 | /**
31 | * @test
32 | */
33 | public function listActionFetchesAllBounceAccountsFromRepositoryAndAssignsThemToView()
34 | {
35 | $allBounceAccounts = $this->getMock(ObjectStorage::class, [], [], '', false);
36 |
37 | $bounceAccountRepository = $this->getMock(BounceAccountRepository::class, ['findAll'], [], '', false);
38 | $bounceAccountRepository->expects($this->once())->method('findAll')->will($this->returnValue($allBounceAccounts));
39 | $this->inject($this->subject, 'bounceAccountRepository', $bounceAccountRepository);
40 |
41 | $view = $this->getMock(ExtDirectView::class, ['assign']);
42 | $view->expects($this->at(0))->method('assign')->with('total', count($allBounceAccounts));
43 | $view->expects($this->at(1))->method('assign')->with('data', $allBounceAccounts);
44 | $view->expects($this->at(2))->method('assign')->with('success', true);
45 | $this->inject($this->subject, 'view', $view);
46 |
47 | $this->subject->listAction();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Classes/Domain/Repository/RecipientListRepository.php:
--------------------------------------------------------------------------------
1 | get(ConfigurationManager::class);
22 | $storagePid = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'newsletter', 'storagePid');
23 |
24 | if ($storagePid['storagePid']) {
25 | $query->getQuerySettings()->setRespectStoragePage(true);
26 | }
27 |
28 | return $query;
29 | }
30 |
31 | /**
32 | * Returns a RecipientList already initialized, even if it is hidden
33 | *
34 | * @param int $uidRecipientlist
35 | *
36 | * @return RecipientList
37 | */
38 | public function findByUidInitialized($uidRecipientlist)
39 | {
40 | $query = $this->createQuery();
41 | $query->getQuerySettings()->setRespectSysLanguage(false);
42 | $query->getQuerySettings()->setRespectStoragePage(false);
43 | $query->getQuerySettings()->setIgnoreEnableFields(true); // because of this line hidden objects can be retrieved
44 | $recipientList = $query->matching(
45 | $query->equals('uid', $uidRecipientlist)
46 | )
47 | ->execute()
48 | ->getFirst();
49 |
50 | if ($recipientList) {
51 | $recipientList->init();
52 | }
53 |
54 | return $recipientList;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Resources/Private/Language/fr.locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | email
8 | Adresse email du compte de rebont qui sera inclut dans le champ "reply-to" des emails envoyés
9 |
10 |
11 | server
12 | Nom d'hôte du serveur email sur lequel récupéré les emails rebondit
13 |
14 |
15 | protocol
16 | Protocole du serveur email
17 |
18 |
19 | username
20 | Nom d'utilistateur du serveur email
21 |
22 |
23 | password
24 | Mot de passe du serveur email
25 |
26 |
27 | port
28 | Port du serveur email
29 |
30 |
31 | fetchmail config
32 | Configuration fetchmail
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Resources/Private/Language/fr.locallang_csh_tx_newsletter_domain_model_email.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | startTime
8 | Date de début d'envoi de l'email
9 |
10 |
11 | endTime
12 | Date de fin d'envoi de l'email
13 |
14 |
15 | recipientAddress
16 | Adresse email du destinataire
17 |
18 |
19 | recipientData
20 | Donnée du destinataire qui peuvent être utilisée dans le corp de l'email via des markers
21 |
22 |
23 | opened
24 | Si l'email a été ouvert
25 |
26 |
27 | bounced
28 | Si l'email a rebondit
29 |
30 |
31 | newsletter
32 | Newsletter à qui appartient cet email
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Configuration/TCA/tx_newsletter_domain_model_link.php:
--------------------------------------------------------------------------------
1 | [
5 | 'title' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link',
6 | 'label' => 'url',
7 | 'iconfile' => 'EXT:newsletter/Resources/Public/Icons/tx_newsletter_domain_model_link.svg',
8 | ],
9 | 'interface' => [
10 | 'showRecordFieldList' => 'url,opened_count,newsletter',
11 | ],
12 | 'types' => [
13 | '1' => ['showitem' => 'url,opened_count,newsletter'],
14 | ],
15 | 'palettes' => [
16 | '1' => ['showitem' => ''],
17 | ],
18 | 'columns' => [
19 | 'url' => [
20 | 'label' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link.url',
21 | 'config' => [
22 | 'type' => 'input',
23 | 'size' => 40,
24 | 'eval' => 'trim',
25 | 'readOnly' => true,
26 | ],
27 | ],
28 | 'opened_count' => [
29 | 'label' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link.opened_count',
30 | 'config' => [
31 | 'type' => 'input',
32 | 'size' => 4,
33 | 'eval' => 'int',
34 | 'readOnly' => true,
35 | ],
36 | ],
37 | 'newsletter' => [
38 | 'label' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_db.xlf:tx_newsletter_domain_model_link.newsletter',
39 | 'config' => [
40 | 'readOnly' => true,
41 | 'type' => 'inline',
42 | 'foreign_table' => 'tx_newsletter_domain_model_newsletter',
43 | 'minitems' => 0,
44 | 'maxitems' => 1,
45 | 'appearance' => [
46 | 'collapse' => 0,
47 | 'showSynchronizationLink' => 1,
48 | 'showPossibleLocalizationRecords' => 1,
49 | 'showAllLocalizationLink' => 1,
50 | ],
51 | ],
52 | ],
53 | ],
54 | ];
55 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Store/Link.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store');
5 |
6 | /**
7 | * A Store for the link model using ExtDirect to communicate with the
8 | * server side extbase framework.
9 | */
10 | Ext.ux.Ecodev.Newsletter.Store.Link = (function () {
11 |
12 | var linkStore = null;
13 |
14 | var initialize = function () {
15 | if (linkStore === null) {
16 | linkStore = new Ext.data.DirectStore({
17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\Link',
18 | reader: new Ext.data.JsonReader({
19 | totalProperty: 'total',
20 | successProperty: 'success',
21 | idProperty: '__identity',
22 | root: 'data',
23 | fields: [
24 | {name: '__identity', type: 'int'},
25 | {name: 'url', type: 'string'},
26 | {name: 'openedCount', type: 'int'},
27 | {name: 'openedPercentage', type: 'int'},
28 | ],
29 | }),
30 | writer: new Ext.data.JsonWriter({
31 | encode: false,
32 | writeAllFields: false,
33 | }),
34 | api: {
35 | read: Ext.ux.Ecodev.Newsletter.Remote.LinkController.listAction,
36 | update: Ext.ux.Ecodev.Newsletter.Remote.LinkController.updateAction,
37 | destroy: Ext.ux.Ecodev.Newsletter.Remote.LinkController.destroyAction,
38 | create: Ext.ux.Ecodev.Newsletter.Remote.LinkController.createAction,
39 | },
40 | paramOrder: {
41 | read: ['data', 'start', 'limit'],
42 | },
43 | });
44 | }
45 | };
46 |
47 | /**
48 | * Public API of this singleton.
49 | */
50 | return {
51 | initialize: initialize,
52 | };
53 | }());
54 | }());
55 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | dist: trusty
3 |
4 | addons:
5 | apt:
6 | packages:
7 | - mysql-server-5.6
8 | - mysql-client-core-5.6
9 | - mysql-client-5.6
10 |
11 | php:
12 | - 5.6
13 | - 7.0
14 | - 7.1
15 | - 7.2
16 |
17 | env:
18 | - TYPO3_VERSION=7.6.25
19 | - TYPO3_VERSION=master
20 |
21 | matrix:
22 | fast_finish: true
23 | allow_failures:
24 | - env: TYPO3_VERSION=master
25 |
26 | cache:
27 | directories:
28 | - vendor
29 | - $HOME/.composer/cache
30 |
31 | before_install:
32 | - nvm install 8
33 | - nvm use 8
34 | - curl -o- -L https://yarnpkg.com/install.sh | bash
35 | - export PATH=$HOME/.yarn/bin:$PATH
36 |
37 | before_script:
38 | - if [[ $TRAVIS_PHP_VERSION = '7.1' && $TYPO3_VERSION = '7.6.25' ]]; then PHPUNIT_FLAGS_UNIT="--coverage-clover=unit-tests-coverage.clover"; else PHPUNIT_FLAGS_UNIT=""; fi
39 | - if [[ $TRAVIS_PHP_VERSION = '7.1' && $TYPO3_VERSION = '7.6.25' ]]; then PHPUNIT_FLAGS_FUNCTIONAL="--coverage-clover=functional-tests-coverage.clover"; else PHPUNIT_FLAGS_FUNCTIONAL=""; fi
40 | - yarn install
41 | - composer install
42 | - cd ..
43 | - git clone --branch $TYPO3_VERSION --depth 1 https://github.com/TYPO3/TYPO3.CMS.git typo3_core
44 | - mv typo3_core/* .
45 | - composer install
46 | - mkdir -p uploads typo3temp typo3conf/ext
47 | - mv newsletter typo3conf/ext/
48 | - export typo3DatabaseName="typo3";
49 | - export typo3DatabaseHost="localhost";
50 | - export typo3DatabaseUsername="root";
51 | - export typo3DatabasePassword="";
52 |
53 | script:
54 | - cd typo3conf/ext/newsletter && ./node_modules/.bin/gulp && cd ../../../
55 | - ./bin/phpunit $PHPUNIT_FLAGS_UNIT --colors -c typo3conf/ext/newsletter/Tests/Build/UnitTests.xml
56 | - ./bin/phpunit $PHPUNIT_FLAGS_FUNCTIONAL --colors -c typo3conf/ext/newsletter/Tests/Build/FunctionalTests.xml
57 |
58 | after_script:
59 | - if [[ ! -z $PHPUNIT_FLAGS_UNIT ]]; then echo "Uploading code coverage results" && cp -R typo3conf/ext/newsletter/.git . && wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover unit-tests-coverage.clover && php ocular.phar code-coverage:upload --format=php-clover functional-tests-coverage.clover ; fi
60 |
--------------------------------------------------------------------------------
/.jscs.json:
--------------------------------------------------------------------------------
1 | {
2 | "requireCurlyBraces": [
3 | "if",
4 | "else",
5 | "for",
6 | "while",
7 | "do",
8 | "try",
9 | "catch"
10 | ],
11 | "requireSpaceAfterKeywords": [
12 | "if",
13 | "else",
14 | "for",
15 | "while",
16 | "do",
17 | "switch",
18 | "return",
19 | "try",
20 | "catch"
21 | ],
22 | "disallowSpaceBeforeBinaryOperators": [
23 | ","
24 | ],
25 | "disallowImplicitTypeConversion": [
26 | "string"
27 | ],
28 | "disallowKeywords": [
29 | "with"
30 | ],
31 | "disallowMultipleLineBreaks": true,
32 | "disallowKeywordsOnNewLine": [
33 | "else"
34 | ],
35 | "disallowMixedSpacesAndTabs": true,
36 | "disallowTrailingWhitespace": true,
37 | "requireLineFeedAtFileEnd": true,
38 | "requireSpacesInFunctionExpression": {
39 | "beforeOpeningCurlyBrace": true
40 | },
41 | "disallowSpacesInFunctionExpression": {
42 | "beforeOpeningRoundBrace": true
43 | },
44 | "disallowSpacesInsideArrayBrackets": true,
45 | "disallowSpacesInsideParentheses": true,
46 | "validateJSDoc": {
47 | "checkParamNames": true
48 | },
49 | "validateIndentation": 4,
50 | "requireSpaceBeforeBinaryOperators": [
51 | "?",
52 | "+",
53 | "-",
54 | "/",
55 | "*",
56 | "=",
57 | "==",
58 | "===",
59 | "!=",
60 | "!==",
61 | ">",
62 | ">=",
63 | "<",
64 | "<="
65 | ],
66 | "requireSpaceAfterBinaryOperators": [
67 | "?",
68 | "+",
69 | "-",
70 | "/",
71 | "*",
72 | "=",
73 | "==",
74 | "===",
75 | "!=",
76 | "!==",
77 | ">",
78 | ">=",
79 | "<",
80 | "<="
81 | ],
82 | "disallowSpaceAfterPrefixUnaryOperators": [
83 | "++",
84 | "--",
85 | "+",
86 | "-",
87 | "~",
88 | "!"
89 | ],
90 | "disallowSpaceBeforePostfixUnaryOperators": [
91 | "++",
92 | "--"
93 | ],
94 | "disallowSpaceAfterObjectKeys": true,
95 | "validateLineBreaks": "LF",
96 | "requireDotNotation": true,
97 | "requireSpacesInConditionalExpression": true,
98 | "disallowEmptyBlocks": true,
99 | "requireBlocksOnNewline": true,
100 | "requireSpaceBeforeBlockStatements": true
101 | }
102 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | DEPRECATED
2 | ==========
3 |
4 | This extension is **no longer maintained** and will never support TYPO3 9+ for the reasons explained in https://github.com/Ecodev/newsletter/issues/168.
5 |
6 | Users should migrate to another extension, or fork this project.
7 |
8 | Newsletter
9 | ==========
10 |
11 | |badge_travis| |badge_scrutinizer| |badge_coverage| |badge_downloads| |badge_version| |badge_license| |badge_gitter|
12 |
13 | .. |badge_travis| image:: https://travis-ci.org/Ecodev/newsletter.svg?branch=master
14 | :target: https://travis-ci.org/Ecodev/newsletter
15 |
16 | .. |badge_scrutinizer| image:: https://scrutinizer-ci.com/g/Ecodev/newsletter/badges/quality-score.png?b=master
17 | :target: https://scrutinizer-ci.com/g/Ecodev/newsletter
18 |
19 | .. |badge_coverage| image:: https://scrutinizer-ci.com/g/Ecodev/newsletter/badges/coverage.png?b=master
20 | :target: https://scrutinizer-ci.com/g/Ecodev/newsletter
21 |
22 | .. |badge_downloads| image:: https://poser.pugx.org/ecodev/newsletter/downloads
23 | :alt: Total Downloads
24 | :target: https://packagist.org/packages/ecodev/newsletter
25 |
26 | .. |badge_version| image:: https://poser.pugx.org/ecodev/newsletter/v/stable
27 | :alt: Latest Stable Version
28 | :target: https://packagist.org/packages/ecodev/newsletter
29 |
30 | .. |badge_license| image:: https://poser.pugx.org/ecodev/newsletter/license
31 | :alt: License
32 | :target: https://packagist.org/packages/ecodev/newsletter
33 |
34 | .. |badge_gitter| image:: https://badges.gitter.im/Ecodev/newsletter.svg
35 | :alt: Join the chat at https://gitter.im/Ecodev/newsletter
36 | :target: https://gitter.im/Ecodev/newsletter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
37 |
38 | .. image:: Documentation/Images/Overview_-_small.png
39 | :align: right
40 |
41 |
42 | A TYPO3 extension to send any pages as a newsletter to several recipients at once.
43 |
44 | Originally based on `TC Directmail`_ 2.0.2,
45 | the mailing engine was almost entirely rewritten but most features were preserved.
46 | We now use SwiftMailer (from TYPO3 core). And it aims to improve the user
47 | experience and works out of the box.
48 |
49 |
50 | Read more in ``Documentation`` folder.
51 |
52 | .. _TC Directmail: http://typo3.org/extensions/repository/view/tcdirectmail/current/
53 |
54 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/RecipientList/Html.php:
--------------------------------------------------------------------------------
1 | htmlUrl = $htmlUrl;
34 | }
35 |
36 | /**
37 | * Getter for htmlUrl
38 | *
39 | * @return string htmlUrl
40 | */
41 | public function getHtmlUrl()
42 | {
43 | return $this->htmlUrl;
44 | }
45 |
46 | /**
47 | * Setter for htmlFetchType
48 | *
49 | * @param string $htmlFetchType htmlFetchType
50 | */
51 | public function setHtmlFetchType($htmlFetchType)
52 | {
53 | $this->htmlFetchType = $htmlFetchType;
54 | }
55 |
56 | /**
57 | * Getter for htmlFetchType
58 | *
59 | * @return string htmlFetchType
60 | */
61 | public function getHtmlFetchType()
62 | {
63 | return $this->htmlFetchType;
64 | }
65 |
66 | public function init()
67 | {
68 | $this->data = [];
69 |
70 | $content = Tools::getUrl($this->getHtmlUrl());
71 |
72 | switch ($this->getHtmlFetchType()) {
73 | case 'mailto':
74 | preg_match_all('|]+href="mailto:([^"]+)"[^>]*>(.*)|Ui', $content, $fetched_data);
75 |
76 | foreach ($fetched_data[1] as $i => $email) {
77 | $this->data[] = ['email' => $email, 'name' => $fetched_data[2][$i]];
78 | }
79 | break;
80 | case 'regex':
81 | default:
82 | preg_match_all("|[\.a-z0-9!#$%&'*+-/=?^_`{\|}]+@[a-z0-9_-][\.a-z0-9_-]*\.[a-z]{2,}|i", $content, $fetched_data);
83 |
84 | foreach ($fetched_data[0] as $address) {
85 | $this->data[]['email'] = $address;
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Classes/Task/SendEmails.php:
--------------------------------------------------------------------------------
1 | get(NewsletterRepository::class);
42 |
43 | $newslettersToSend = $newsletterRepository->findAllReadyToSend();
44 | $newslettersBeingSent = $newsletterRepository->findAllBeingSent();
45 | $newslettersToSendCount = count($newslettersToSend);
46 | $newslettersBeingSentCount = count($newslettersBeingSent);
47 |
48 | $emailNotSentCount = 0;
49 | foreach ($newslettersToSend as $newsletter) {
50 | $emailNotSentCount += $newsletter->getEmailNotSentCount();
51 | }
52 | foreach ($newslettersBeingSent as $newsletter) {
53 | $emailNotSentCount += $newsletter->getEmailNotSentCount();
54 | }
55 |
56 | $emailsPerRound = Tools::confParam('mails_per_round');
57 |
58 | return LocalizationUtility::translate('task_send_emails_additional_information', 'newsletter', [$emailsPerRound, $emailNotSentCount, $newslettersToSendCount, $newslettersBeingSentCount]);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/ExtDirectProviderViewHelper.php:
--------------------------------------------------------------------------------
1 |
15 | *
16 | *
17 | */
18 | class ExtDirectProviderViewHelper extends AbstractViewHelper
19 | {
20 | /**
21 | * @var Api
22 | */
23 | protected $apiService;
24 |
25 | /**
26 | * @see Classes/Core/ViewHelper/\TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper#initializeArguments()
27 | */
28 | public function initializeArguments()
29 | {
30 | $objectManager = GeneralUtility::makeInstance(ObjectManager::class);
31 | $this->apiService = $objectManager->get(Api::class);
32 | }
33 |
34 | /**
35 | * Generates a Ext.Direct API descriptor and adds it to the pagerenderer.
36 | * Also calls Ext.Direct.addProvider() on itself (at js side).
37 | * The remote API is directly useable.
38 | *
39 | * @param string $name the name for the javascript variable
40 | * @param string $namespace the namespace the variable is placed
41 | * @param string $routeUrl you can specify a URL that acts as router
42 | */
43 | public function render($name = 'remoteDescriptor', $namespace = 'Ext.ux.Ecodev.Newsletter.Remote', $routeUrl = null)
44 | {
45 | if ($routeUrl === null) {
46 | $routeUrl = $this->controllerContext->getUriBuilder()->reset()->build() . '&Ecodev\\Newsletter\\ExtDirectRequest=1';
47 | }
48 |
49 | $api = $this->apiService->createApi($routeUrl, $namespace);
50 |
51 | // prepare output variable
52 | $jsCode = '';
53 | $descriptor = $namespace . '.' . $name;
54 | // build up the output
55 | $jsCode .= 'Ext.ns(\'' . $namespace . '\'); ' . "\n";
56 | $jsCode .= $descriptor . ' = ';
57 | $jsCode .= json_encode($api);
58 | $jsCode .= ";\n";
59 | $jsCode .= 'Ext.Direct.addProvider(' . $descriptor . ');' . "\n";
60 | // add the output to the pageRenderer
61 | $this->pageRenderer->addExtOnReadyCode($jsCode, true);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Classes/Controller/BounceAccountController.php:
--------------------------------------------------------------------------------
1 | bounceAccountRepository = $bounceAccountRepository;
29 | }
30 |
31 | /**
32 | * Displays all BounceAccounts
33 | *
34 | * @return string The rendered list view
35 | */
36 | public function listAction()
37 | {
38 | $bounceAccounts = $this->bounceAccountRepository->findAll();
39 |
40 | $this->view->setVariablesToRender(['total', 'data', 'success', 'flashMessages']);
41 | $this->view->setConfiguration([
42 | 'data' => [
43 | '_descendAll' => self::resolveJsonViewConfiguration(),
44 | ],
45 | ]);
46 |
47 | $this->addFlashMessage('Loaded BounceAccounts from Server side.', 'BounceAccounts loaded successfully', FlashMessage::NOTICE);
48 |
49 | $this->view->assign('total', $bounceAccounts->count());
50 | $this->view->assign('data', $bounceAccounts);
51 | $this->view->assign('success', true);
52 | $this->flushFlashMessages();
53 | }
54 |
55 | /**
56 | * Returns a configuration for the JsonView, that describes which fields should be rendered for
57 | * a BounceAccount record.
58 | *
59 | * @return array
60 | */
61 | public static function resolveJsonViewConfiguration()
62 | {
63 | return [
64 | '_exposeObjectIdentifier' => true,
65 | '_only' => [
66 | 'email',
67 | 'server',
68 | 'protocol',
69 | 'username',
70 | ],
71 | ];
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Tests/Unit/Domain/Model/RecipientList/AbstractRecipientList.php:
--------------------------------------------------------------------------------
1 | subject);
18 | }
19 |
20 | /**
21 | * @test
22 | */
23 | public function getTitleReturnsInitialValueForString()
24 | {
25 | $this->assertSame('', $this->subject->getTitle());
26 | }
27 |
28 | /**
29 | * @test
30 | */
31 | public function setTitleForStringSetsTitle()
32 | {
33 | $this->subject->setTitle('Conceived at T3CON10');
34 | $this->assertAttributeSame('Conceived at T3CON10', 'title', $this->subject);
35 | }
36 |
37 | /**
38 | * @test
39 | */
40 | public function getPlainOnlyReturnsInitialValueForBoolean()
41 | {
42 | $this->assertFalse(
43 | $this->subject->getPlainOnly()
44 | );
45 | $this->assertFalse(
46 | $this->subject->isPlainOnly()
47 | );
48 | }
49 |
50 | /**
51 | * @test
52 | */
53 | public function setPlainOnlyForBooleanSetsPlainOnly()
54 | {
55 | $this->subject->setPlainOnly(true);
56 |
57 | $this->assertAttributeSame(
58 | true, 'plainOnly', $this->subject
59 | );
60 | }
61 |
62 | /**
63 | * @test
64 | */
65 | public function getLangReturnsInitialValueForString()
66 | {
67 | $this->assertSame(0, $this->subject->getLang());
68 | }
69 |
70 | /**
71 | * @test
72 | */
73 | public function setLangForStringSetsLang()
74 | {
75 | $this->subject->setLang(123);
76 | $this->assertAttributeSame(123, 'lang', $this->subject);
77 | }
78 |
79 | /**
80 | * @test
81 | */
82 | public function getTypeReturnsInitialValueForString()
83 | {
84 | $this->assertSame('', $this->subject->getType());
85 | }
86 |
87 | /**
88 | * @test
89 | */
90 | public function setTypeForStringSetsType()
91 | {
92 | $this->subject->setType('Conceived at T3CON10');
93 | $this->assertAttributeSame('Conceived at T3CON10', 'type', $this->subject);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/LocalizationViewHelper.php:
--------------------------------------------------------------------------------
1 | controllerContext->getRequest()->getControllerExtensionKey();
25 | }
26 | $extPath = ExtensionManagementUtility::extPath($extKey);
27 |
28 | $localizations = [];
29 | foreach ($names as $name) {
30 | $filePath = $extPath . $pathInsideExt . $name;
31 | $localizations = array_merge($localizations, $this->getLocalizations($filePath));
32 | }
33 |
34 | $localizations = json_encode($localizations);
35 | $javascript = "Ext.ux.Ecodev.Newsletter.Language = $localizations;";
36 |
37 | $this->pageRenderer->addJsInlineCode($filePath, $javascript);
38 | }
39 |
40 | /**
41 | * Returns localization variables within an array
42 | *
43 | * @param string $filePath
44 | *
45 | * @throws Exception
46 | * @return array
47 | */
48 | protected function getLocalizations($filePath)
49 | {
50 | global $LANG;
51 | global $LOCAL_LANG;
52 |
53 | // Language inclusion
54 | $LANG->includeLLFile($filePath);
55 | if (!isset($LOCAL_LANG[$LANG->lang]) || empty($LOCAL_LANG[$LANG->lang])) {
56 | $lang = 'default';
57 | } else {
58 | $lang = $LANG->lang;
59 | }
60 |
61 | $result = [];
62 | foreach ($LOCAL_LANG[$lang] as $key => $value) {
63 | $target = $value[0]['target'];
64 |
65 | // Replace '.' in key because it would break JSON
66 | $key = str_replace('.', '_', $key);
67 | $result[$key] = $target;
68 | }
69 |
70 | return $result;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Tests/Functional/Utility/UriBuilderTest.php:
--------------------------------------------------------------------------------
1 | 1, 'b' => 'baz']);
16 | $expected = '/?tx_newsletter_p%5Ba%5D=1&tx_newsletter_p%5Bb%5D=baz&tx_newsletter_p%5Baction%5D=bar&tx_newsletter_p%5Bcontroller%5D=foo&type=1342671779';
17 | $this->assertSame($expected, $actual);
18 | }
19 |
20 | public function testBuildFrontendUri()
21 | {
22 | // Create an internal builder mock
23 | $mockBuilder = $this->getMock(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class, ['buildFrontendUri'], [], '', false);
24 | $mockBuilder->method('buildFrontendUri')->will($this->onConsecutiveCalls('url1?a=1', 'url2?a=1'));
25 |
26 | // Inject the mock
27 | $reflectionClass = new ReflectionClass(UriBuilder::class);
28 | $property = $reflectionClass->getProperty('uriBuilder');
29 | $property->setAccessible(true);
30 | $property->setValue([1 => $mockBuilder]);
31 |
32 | $actual1 = UriBuilder::buildFrontendUri(1, 'foo', 'bar', ['a' => 1, 'l' => 'original']);
33 | $expectedArguments = [
34 | 'tx_newsletter_p' => [
35 | 'a' => 1,
36 | 'action' => 'bar',
37 | 'controller' => 'foo',
38 | ],
39 | ];
40 | $this->assertSame($expectedArguments, $mockBuilder->getArguments(), 'the internal builder should have been given namespaced arguments without the special l parameter');
41 |
42 | $actual2 = UriBuilder::buildFrontendUri(1, 'foo', 'bar', ['baz' => 1]);
43 | $actual1bis = UriBuilder::buildFrontendUri(1, 'foo', 'bar', ['a' => 1, 'l' => 'bis']);
44 |
45 | $this->assertSame('url1?a=1&tx_newsletter_p%5Bl%5D=original', $actual1, 'should be able to build URL with special l parameter');
46 | $this->assertSame('url2?a=1', $actual2, 'should be able to build URL without special l parameter');
47 | $this->assertSame('url1?a=1&tx_newsletter_p%5Bl%5D=bis', $actual1bis, 'should be able to hit the cache to retrieve the first URL and complete new value of l parameter');
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/IncludeJsFolderViewHelper.php:
--------------------------------------------------------------------------------
1 | -Tags
12 | *
13 | * = Examples =
14 | *
15 | *
16 | *
17 | *
18 | */
19 | class IncludeJsFolderViewHelper extends AbstractViewHelper
20 | {
21 | /**
22 | * Calls addJsFile for each file in the given folder on the Instance of TYPO3\CMS\Core\Page\PageRenderer.
23 | *
24 | * @param string $name the file to include
25 | * @param string $extKey the extension, where the file is located
26 | * @param string $pathInsideExt the path to the file relative to the ext-folder
27 | * @param bool $recursive
28 | */
29 | public function render($name = null, $extKey = null, $pathInsideExt = 'Resources/Public/JavaScript/', $recursive = false)
30 | {
31 | if ($extKey == null) {
32 | $extKey = $this->controllerContext->getRequest()->getControllerExtensionKey();
33 | }
34 | $extPath = ExtensionManagementUtility::extPath($extKey);
35 | if (TYPO3_MODE === 'FE') {
36 | $extRelPath = mb_substr($extPath, mb_strlen(PATH_site));
37 | } else {
38 | $extRelPath = ExtensionManagementUtility::extRelPath($extKey);
39 | }
40 | $absFolderPath = $extPath . $pathInsideExt . $name;
41 | // $files will include all files relative to $pathInsideExt
42 | if ($recursive === false) {
43 | $files = GeneralUtility::getFilesInDir($absFolderPath);
44 | foreach ($files as $hash => $filename) {
45 | $files[$hash] = $name . $filename;
46 | }
47 | } else {
48 | $files = GeneralUtility::getAllFilesAndFoldersInPath([], $absFolderPath, '', 0, 99, '\\.svn');
49 | foreach ($files as $hash => $absPath) {
50 | $files[$hash] = str_replace($extPath . $pathInsideExt, '', $absPath);
51 | }
52 | }
53 | foreach ($files as $name) {
54 | $this->pageRenderer->addJsFile($extRelPath . $pathInsideExt . $name);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/ext_icon.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Store/Email.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store');
5 |
6 | /**
7 | * A Store for the email model using ExtDirect to communicate with the
8 | * server side extbase framework.
9 | */
10 | Ext.ux.Ecodev.Newsletter.Store.Email = (function () {
11 |
12 | var emailStore = null;
13 |
14 | var initialize = function () {
15 | if (emailStore === null) {
16 | emailStore = new Ext.data.DirectStore({
17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\Email',
18 | reader: new Ext.data.JsonReader({
19 | totalProperty: 'total',
20 | successProperty: 'success',
21 | idProperty: '__identity',
22 | root: 'data',
23 | fields: [
24 | {name: '__identity', type: 'int'},
25 | {name: 'recipientAddress', type: 'string'},
26 | {name: 'beginTime', type: 'date'},
27 | {name: 'endTime', type: 'date'},
28 | {name: 'openTime', type: 'date'},
29 | {name: 'bounceTime', type: 'date'},
30 | {name: 'authCode', type: 'string'},
31 | {name: 'recipientAddress', type: 'string'},
32 | {name: 'unsubscribed', type: 'boolean'},
33 | ],
34 | }),
35 | writer: new Ext.data.JsonWriter({
36 | encode: false,
37 | writeAllFields: false,
38 | }),
39 | api: {
40 | read: Ext.ux.Ecodev.Newsletter.Remote.EmailController.listAction,
41 | update: Ext.ux.Ecodev.Newsletter.Remote.EmailController.updateAction,
42 | destroy: Ext.ux.Ecodev.Newsletter.Remote.EmailController.destroyAction,
43 | create: Ext.ux.Ecodev.Newsletter.Remote.EmailController.createAction,
44 | },
45 | paramOrder: {
46 | read: ['data', 'start', 'limit'],
47 | },
48 | });
49 | }
50 | };
51 |
52 | /**
53 | * Public API of this singleton.
54 | */
55 | return {
56 | initialize: initialize,
57 | };
58 | }());
59 | }());
60 |
--------------------------------------------------------------------------------
/Classes/ViewHelpers/Be/ModuleContainerViewHelper.php:
--------------------------------------------------------------------------------
1 |
15 | * {namespace newsletter=Ecodev\Newsletter\ViewHelpers}
16 | * your additional viewHelpers inside
17 | *
18 | *
19 | * Output:
20 | * "your module content" wrapped with propper head & body tags.
21 | * Default backend CSS styles and JavaScript will be included
22 | *
23 | *
24 | * {namespace newsletter=Ecodev\Newsletter\ViewHelpers}
25 | * your module content
26 | *
27 | */
28 | class ModuleContainerViewHelper extends AbstractViewHelper
29 | {
30 | /**
31 | * Don't escape anything because we will render the entire page
32 | */
33 | protected $escapeOutput = false;
34 |
35 | /**
36 | * Renders start page with template.php and pageTitle.
37 | *
38 | * @param string $pageTitle title tag of the module. Not required by default, as BE modules are shown in a frame
39 | *
40 | * @return string
41 | * @see template
42 | * @see \TYPO3\CMS\Core\Page\PageRenderer
43 | */
44 | public function render($pageTitle = '')
45 | {
46 | $doc = $this->getDocInstance();
47 | $this->pageRenderer->backPath = '';
48 | $this->pageRenderer->loadExtJS();
49 |
50 | // From TYPO3 8.6.0 onward t3skin is located in core (see: https://forge.typo3.org/issues/79259).
51 | if (version_compare(TYPO3_version, '8.6.0', '>=')) {
52 | $this->pageRenderer->addCssFile('sysext/core/Resources/Public/ExtJs/xtheme-t3skin.css');
53 | } else {
54 | // Anything before 8.6.0 must still use the old t3skin EXT path.
55 | $this->pageRenderer->addCssFile('sysext/t3skin/extjs/xtheme-t3skin.css');
56 | }
57 |
58 | $this->renderChildren();
59 |
60 | $this->pageRenderer->enableCompressJavaScript();
61 | $this->pageRenderer->enableCompressCss();
62 | $this->pageRenderer->enableConcatenateFiles();
63 |
64 | $output = $doc->startPage($pageTitle);
65 | $output .= $this->pageRenderer->getBodyContent();
66 | $output .= $doc->endPage();
67 |
68 | return $output;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Classes/Update/Transaction.php:
--------------------------------------------------------------------------------
1 | sql_query('START TRANSACTION;');
31 | $results = self::transactDBQueries($queries);
32 | if ($results->getErrorMessage()) {
33 | $db->sql_query('ROLLBACK;');
34 | // Because we rolled back nothing was modified so we can safely reset the integrity state.
35 | $results->resetDataIntegrity();
36 |
37 | return $results;
38 | }
39 | $db->sql_query('COMMIT;');
40 | }
41 |
42 | return $results;
43 | }
44 |
45 | /**
46 | * Executes an array of database queries.
47 | *
48 | * @param string[] $queries
49 | * An array of SQL queries
50 | *
51 | * @return TransactionResult
52 | */
53 | private static function transactDBQueries(array $queries)
54 | {
55 | $results = new TransactionResult(count($queries));
56 | if (!empty($queries)) {
57 | $db = Tools::getDatabaseConnection();
58 | foreach ($queries as $query) {
59 | $res = $db->sql_query($query);
60 | $results->appendAffectedDataCount($db->sql_affected_rows());
61 | $error = $db->sql_error();
62 | if ($error) {
63 | $results->setErrorMessage($error);
64 | break;
65 | }
66 | $results->stepProcessed();
67 | }
68 | $db->sql_free_result($res);
69 | }
70 |
71 | return $results;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/ext_tables.php:
--------------------------------------------------------------------------------
1 | 'index',
16 | 'Newsletter' => 'list, listPlanned, create, statistics',
17 | 'Email' => 'list',
18 | 'Link' => 'list',
19 | 'BounceAccount' => 'list',
20 | 'RecipientList' => 'list, listRecipient',
21 | ],
22 | [
23 | 'access' => 'user,group',
24 | 'icon' => 'EXT:newsletter/Resources/Public/Icons/tx_newsletter.svg',
25 | 'labels' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang_module.xlf',
26 | ]
27 | );
28 | }
29 |
30 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_newsletter', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_newsletter.xlf');
31 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_newsletter');
32 |
33 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_bounceaccount', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_bounceaccount.xlf');
34 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_bounceaccount');
35 |
36 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_recipientlist', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_recipientlist.xlf');
37 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_recipientlist');
38 |
39 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_email', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_email.xlf');
40 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_email');
41 |
42 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr('tx_newsletter_domain_model_link', 'EXT:newsletter/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_link.xlf');
43 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::allowTableOnStandardPages('tx_newsletter_domain_model_link');
44 |
--------------------------------------------------------------------------------
/Tests/Functional/Fixtures/mailer/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Newsletter Title
5 |
6 |
7 |
8 |
9 | http://www.example.com this line should never be modified
10 | this link should be injected
11 | this custom link should be injected
12 | this link should be injected, and marker substituted
13 | mailto link should still work
14 | HTML entities encoded mailto link should still work
15 |
16 | ###unknown_field_will_be_kept###
17 | ###newsletter_view_url###
18 | ###newsletter_unsubscribe_url###
19 | ###email###
20 | ###my_custom_field###
21 |
22 | http://unknown_field_will_be_kept
23 | http://newsletter_view_url
24 | http://newsletter_unsubscribe_url
25 | http://email
26 | http://my_custom_field
27 |
28 | https://unknown_field_will_be_kept
29 | https://newsletter_view_url
30 | https://newsletter_unsubscribe_url
31 | https://email
32 | https://my_custom_field
33 |
34 | boolean_false=###:IF: boolean_false ###true-ish###:ENDIF:###
35 | boolean_true=###:IF: boolean_true ###true-ish###:ENDIF:###
36 | integer_false=###:IF: integer_false ###true-ish###:ENDIF:###
37 | integer_true=###:IF: integer_true ###true-ish###:ENDIF:###
38 | string_false=###:IF: string_false ###true-ish###:ENDIF:###
39 | string_true=###:IF: string_true ###true-ish###:ENDIF:###
40 |
41 | boolean_false=###:IF: boolean_false ###true-ish###:ELSE:###false-ish###:ENDIF:###
42 | boolean_true=###:IF: boolean_true ###true-ish###:ELSE:###false-ish###:ENDIF:###
43 | integer_false=###:IF: integer_false ###true-ish###:ELSE:###false-ish###:ENDIF:###
44 | integer_true=###:IF: integer_true ###true-ish###:ELSE:###false-ish###:ENDIF:###
45 | string_false=###:IF: string_false ###true-ish###:ELSE:###false-ish###:ENDIF:###
46 | string_true=###:IF: string_true ###true-ish###:ELSE:###false-ish###:ENDIF:###
47 |
48 | UTF-8: können 한국어 ✓ € ☺ ☹
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/ext_localconf.php:
--------------------------------------------------------------------------------
1 | ');
9 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptConstants('');
10 |
11 | // Register keys for CLI
12 | $TYPO3_CONF_VARS['SC_OPTIONS']['GLOBAL']['cliKeys']['newsletter_bounce'] = ['EXT:newsletter/cli/bounce.php', '_CLI_scheduler'];
13 |
14 | // Configure FE plugin element
15 | \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
16 | 'Ecodev.' . $_EXTKEY, 'p', [// list of controller
17 | 'Email' => 'show, opened',
18 | 'Link' => 'clicked',
19 | 'RecipientList' => 'unsubscribe, export',
20 | ], [// non-cacheable controller
21 | 'Email' => 'show, opened, unsubscribe',
22 | 'Link' => 'clicked',
23 | 'RecipientList' => 'export',
24 | ]
25 | );
26 |
27 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\Ecodev\Newsletter\Task\SendEmails::class] = [
28 | 'extension' => $_EXTKEY,
29 | 'title' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_send_emails_title',
30 | 'description' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_send_emails_description',
31 | ];
32 |
33 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\Ecodev\Newsletter\Task\FetchBounces::class] = [
34 | 'extension' => $_EXTKEY,
35 | 'title' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_fetch_bounces_title',
36 | 'description' => 'LLL:EXT:newsletter/Resources/Private/Language/locallang.xlf:task_fetch_bounces_description',
37 | ];
38 |
39 | // Configure TCA custom eval and hooks to manage on-the-fly (de)encryption from database
40 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][\Ecodev\Newsletter\Tca\BounceAccountTca::class] = 'EXT:' . $_EXTKEY . '/Classes/Tca/BounceAccountTca.php';
41 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord'][\Ecodev\Newsletter\Tca\BounceAccountDataProvider::class] = [
42 | 'depends' => [
43 | \TYPO3\CMS\Backend\Form\FormDataProvider\DatabaseEditRow::class,
44 | ],
45 | ];
46 |
47 | // Make a call to update
48 | if (TYPO3_MODE === 'BE') {
49 | $dispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class);
50 | $dispatcher->connect(\TYPO3\CMS\Extensionmanager\Utility\InstallUtility::class, 'afterExtensionInstall', \Ecodev\Newsletter\Update\Update::class, 'afterExtensionInstall');
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/Functional/Repository/EmailRepositoryTest.php:
--------------------------------------------------------------------------------
1 | emailRepository = $this->objectManager->get(EmailRepository::class);
21 | }
22 |
23 | public function testFindByAuthcode()
24 | {
25 | $email = $this->emailRepository->findByAuthcode($this->authCode);
26 | $this->assertNotNull($email);
27 | $this->assertSame(302, $email->getUid());
28 | }
29 |
30 | public function testGetCount()
31 | {
32 | $this->assertSame(0, $this->emailRepository->getCount(10));
33 | $this->assertSame(2, $this->emailRepository->getCount(30));
34 | }
35 |
36 | public function testFindAllByNewsletter()
37 | {
38 | $this->assertCount(0, $this->emailRepository->findAllByNewsletter(10, 0, 999));
39 |
40 | $emails = $this->emailRepository->findAllByNewsletter(30, 0, 999);
41 | $this->assertCount(2, $emails);
42 | $this->assertSame(301, $emails[0]->getUid());
43 | $this->assertSame(302, $emails[1]->getUid());
44 |
45 | $emails = $this->emailRepository->findAllByNewsletter(30, 1, 999);
46 | $this->assertCount(1, $emails);
47 | $this->assertSame(302, $emails[0]->getUid());
48 |
49 | $emails = $this->emailRepository->findAllByNewsletter(30, 2, 999);
50 | $this->assertCount(0, $emails);
51 |
52 | $emails = $this->emailRepository->findAllByNewsletter(30, 0, 1);
53 | $this->assertCount(1, $emails);
54 | $this->assertSame(301, $emails[0]->getUid());
55 |
56 | $emails = $this->emailRepository->findAllByNewsletter(30, 1, 1);
57 | $this->assertCount(1, $emails);
58 | $this->assertSame(302, $emails[0]->getUid());
59 |
60 | $emails = $this->emailRepository->findAllByNewsletter(30, 2, 1);
61 | $this->assertCount(0, $emails);
62 | }
63 |
64 | public function testRegisterOpen()
65 | {
66 | $this->emailRepository->registerOpen($this->authCode);
67 |
68 | $email = $this->emailRepository->findByUid(302);
69 | $this->assertTrue($email->isOpened(), 'email should be marked as opened');
70 | $this->assertRecipientListCallbackWasCalled('opened recipient2@example.com');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Documentation/Introduction/Index.rst:
--------------------------------------------------------------------------------
1 | .. ==================================================
2 | .. FOR YOUR INFORMATION
3 | .. --------------------------------------------------
4 | .. -*- coding: utf-8 -*- with BOM.
5 |
6 | .. include:: ../Includes.txt
7 |
8 |
9 | .. _introduction:
10 |
11 | Introduction
12 | ============
13 |
14 |
15 | .. _what-it-does:
16 |
17 | What does it do?
18 | ----------------
19 |
20 | A TYPO3 extension to send any pages as a newsletter to several recipients at
21 | once.
22 |
23 | Originally based on `TC Directmail`_ 2.0.2,
24 | the mailing engine was almost entirely rewritten but most features were
25 | preserved.We now use SwiftMailer (from TYPO3 core). And it aims to improve the
26 | user experience and works out of the box.
27 |
28 | .. _TC Directmail: http://typo3.org/extensions/repository/view/tcdirectmail/current/
29 |
30 | .. _how-it-compares:
31 |
32 | Comparison with TC Directmail 2.0.2
33 | -----------------------------------
34 |
35 | What's better
36 | ^^^^^^^^^^^^^
37 |
38 | - Use of `SwiftMailer`_
39 | - Brand new database structure allowing for much more size efficient
40 | storage
41 | - Two special markers available: :code:`###newsletter_view_url###` and
42 | :code:`###newsletter_unsubscribe_url###`
43 | - Better cleaning of javascript in email content
44 | - Plain text quality is improved
45 |
46 | What's worse
47 | ^^^^^^^^^^^^
48 |
49 | - Removed wizard for recipientlist generation
50 | - Only one recipient list per newsletter (workaround: send multiple
51 | newsletter or UNION via raw sql)
52 |
53 | .. _SwiftMailer: http://swiftmailer.org/
54 |
55 | .. _screenshots:
56 |
57 | Screenshots
58 | -----------
59 |
60 | Current status of newsletter
61 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 |
63 | .. figure:: ../Images/Newsletter_-_Status.png
64 | :alt: Status of Newsletter
65 |
66 | Settings for newsletter
67 | ^^^^^^^^^^^^^^^^^^^^^^^
68 |
69 | .. figure:: ../Images/Newsletter_-_Settings.png
70 | :alt: Settings for Newsletter
71 |
72 | Newsletter planning (and testing)
73 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74 |
75 | .. figure:: ../Images/Newsletter_-_Sending.png
76 | :alt: Newsletter planning and testing
77 |
78 | Statistics overview with charts for one newsletter
79 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 |
81 | .. figure:: ../Images/Statistics_-_Overview.png
82 | :alt: Statistics overview for one newsletter
83 |
84 | Statistics of all emails for one newsletter
85 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
86 |
87 | .. figure:: Images/Statistics_-_Emails.png
88 | :alt: Statistics of all emails for one newsletter
89 |
90 | Statistics of all links for one newsletter
91 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92 |
93 | .. figure:: ../Images/Statistics_-_Links.png
94 | :alt: Statistics of all links for one newsletter
95 |
--------------------------------------------------------------------------------
/ext_conf_template.txt:
--------------------------------------------------------------------------------
1 | # cat=basic; type=string; label=Default sender name: Specify default value for sender name. Can be overridden for each newsletter. If "user", the be_user owning the newsletter will be used. If blank, will default to the site name from $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']. Otherwise can be any string.
2 | sender_name = user
3 |
4 | # cat=basic; type=string; label=Default sender email: Specify default value for sender email. Can be overridden for each newsletter. If "user", the be_user owning the newsletter will be used. If blank, will default to generic settings for the site or the system. Otherwise must be a valid email address.
5 | sender_email = user
6 |
7 | # cat=basic; type=string; label=Notification email: Specify a valid address email to receive notification when recipients unsubscribe. If "user", the be_user owning the newsletter will be used. If blank, no notification will be sent. Otherwise must be a valid email address.
8 | notification_email = user
9 |
10 | # cat=basic; type=string; label=Domain: Base URL (scheme + domain + path) from which to fetch content and encode links with. Leave blank to use domain-records from the page tree.
11 | fetch_path =
12 |
13 | # cat=basic; type=string; label=Append parameters: String to append to URL's when fetching content. For example use &type= allows you to implement your newsletter with a special template. Or disable cache with &no_cache=1.
14 | append_url =
15 |
16 | # cat=basic; type=string; label=Path to Lynx CLI browser: This program is not required, but can be used by Newsletter to produce an acceptable plaintext conversion.
17 | path_to_lynx = /usr/bin/lynx
18 |
19 | # cat=basic; type=string; label=Path to fetchmail program: This a standard unix mail-retrieval program. It is used to collect bounced emails with.
20 | path_to_fetchmail = /usr/bin/fetchmail
21 |
22 | # cat=basic; type=boolean; label=Keep bounced emails on server: Checking this option will leave bounced emails on the server. Default behaviour is to *delete* bounce messages, once they have been processed.
23 | keep_messages = 0
24 |
25 | # cat=basic; type=boolean; label=Attach images: Leave this checked to enable attached, inline images. This will normally improve you viewers experience, but reduces the performance of the mailer, since it has to deliver much more (binary) data. Uncheck it to instead link to the images online.
26 | attach_images = 1
27 |
28 | # cat=basic; type=integer; label=Number of mailer per round: This can be used to limit the rate on which Newsletter will send out mails. You must set this if you wish to use the "Invoke mailer" button. If you specify 0 as value, it will send all mails in one go.
29 | mails_per_round = 100
30 |
31 | # cat=basic; type=string; label=Redirect page (pid|url) on unsubscribe: If this value is set, on unsubscribe the user will be redirected to the corresponding page.
32 | unsubscribe_redirect =
33 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Store/BounceAccount.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store');
5 |
6 | /**
7 | * A Store for the bounceAccount model using ExtDirect to communicate with the
8 | * server side extbase framework.
9 | */
10 | Ext.ux.Ecodev.Newsletter.Store.BounceAccount = (function () {
11 |
12 | var bounceAccountStore = null;
13 |
14 | var initialize = function () {
15 | if (bounceAccountStore === null) {
16 | bounceAccountStore = new Ext.data.DirectStore({
17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\BounceAccount',
18 | reader: new Ext.data.JsonReader({
19 | totalProperty: 'total',
20 | successProperty: 'success',
21 | idProperty: '__identity',
22 | root: 'data',
23 | fields: [
24 | {name: '__identity', type: 'int'},
25 | {name: 'email', type: 'string'},
26 | {name: 'server', type: 'string'},
27 | {name: 'protocol', type: 'string'},
28 | {name: 'username', type: 'string'},
29 | {
30 | name: 'fullName', convert: function (v, bounceAccount) {
31 | return String.format('{0} ({1}://{2}@{3})', bounceAccount.email, bounceAccount.protocol, bounceAccount.username, bounceAccount.server);
32 | },
33 | },
34 | ],
35 | }),
36 | writer: new Ext.data.JsonWriter({
37 | encode: false,
38 | writeAllFields: false,
39 | }),
40 | api: {
41 | read: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.listAction,
42 | update: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.updateAction,
43 | destroy: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.destroyAction,
44 | create: Ext.ux.Ecodev.Newsletter.Remote.BounceAccountController.createAction,
45 | },
46 | paramOrder: {
47 | read: [],
48 | update: ['data'],
49 | create: ['data'],
50 | destroy: ['data'],
51 | },
52 | autoLoad: true,
53 |
54 | });
55 | }
56 | };
57 |
58 | /**
59 | * Public API of this singleton.
60 | */
61 | return {
62 | initialize: initialize,
63 | };
64 | }());
65 | }());
66 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Store/RecipientList.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | Ext.ns('Ext.ux.Ecodev.Newsletter.Store');
5 |
6 | /**
7 | * A Store for the recipientList model using ExtDirect to communicate with the
8 | * server side extbase framework.
9 | */
10 | Ext.ux.Ecodev.Newsletter.Store.RecipientList = (function () {
11 |
12 | var recipientListStore = null;
13 |
14 | var initialize = function () {
15 | if (recipientListStore === null) {
16 | recipientListStore = new Ext.data.DirectStore({
17 | storeId: 'Ecodev\\Newsletter\\Domain\\Model\\RecipientList',
18 | reader: new Ext.data.JsonReader({
19 | totalProperty: 'total',
20 | successProperty: 'success',
21 | idProperty: '__identity',
22 | root: 'data',
23 | fields: [
24 | {name: '__identity', type: 'int'},
25 | {name: 'title', type: 'string'},
26 | {name: 'plainOnly', type: 'boolean'},
27 | {name: 'lang', type: 'string'},
28 | {name: 'type', type: 'string'},
29 | {name: 'count', type: 'integer'},
30 | {
31 | name: 'fullName', convert: function (v, recipientList) {
32 | return String.format('{0} ({1})', recipientList.title, recipientList.count);
33 | },
34 | },
35 | ],
36 | }),
37 | writer: new Ext.data.JsonWriter({
38 | encode: false,
39 | writeAllFields: false,
40 | }),
41 | api: {
42 | read: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.listAction,
43 | update: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.updateAction,
44 | destroy: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.destroyAction,
45 | create: Ext.ux.Ecodev.Newsletter.Remote.RecipientListController.createAction,
46 | },
47 | paramOrder: {
48 | read: [],
49 | update: ['data'],
50 | create: ['data'],
51 | destroy: ['data'],
52 | },
53 | autoLoad: true,
54 |
55 | });
56 | }
57 | };
58 | /**
59 | * Public API of this singleton.
60 | */
61 | return {
62 | initialize: initialize,
63 | };
64 | }());
65 | }());
66 |
--------------------------------------------------------------------------------
/Classes/MVC/ExtDirect/RequestBuilder.php:
--------------------------------------------------------------------------------
1 | objectManager = $objectManager;
35 | }
36 |
37 | /**
38 | * Injects the ConfigurationManager
39 | *
40 | * @param ConfigurationManager $configurationManager
41 | */
42 | public function injectConfigurationManager(ConfigurationManager $configurationManager)
43 | {
44 | $this->configurationManager = $configurationManager;
45 | }
46 |
47 | /**
48 | * Builds an Ext Direct request
49 | *
50 | * @return Request The built request
51 | */
52 | public function build()
53 | {
54 | $postArguments = $_POST;
55 | if (isset($postArguments['extAction'])) {
56 | throw new Exception('Form Post Request building is not yet implemented.', 1281379502);
57 | }
58 | $request = $this->buildJsonRequest();
59 |
60 | return $request;
61 | }
62 |
63 | /**
64 | * Builds a Json Ext Direct request by reading the transaction data from
65 | * standard input.
66 | *
67 | * @return Request The Ext Direct request object
68 | */
69 | protected function buildJsonRequest()
70 | {
71 | $transactionDatas = file_get_contents('php://input');
72 |
73 | if (($transactionDatas = json_decode($transactionDatas)) === null) {
74 | throw new Exception('The request is not a valid Ext Direct request', 1268490738);
75 | }
76 |
77 | if (!is_array($transactionDatas)) {
78 | $transactionDatas = [$transactionDatas];
79 | }
80 |
81 | /** @var Request $request */
82 | $request = $this->objectManager->get(Request::class);
83 | foreach ($transactionDatas as $transactionData) {
84 | $request->createAndAddTransaction(
85 | $transactionData->action, $transactionData->method, is_array($transactionData->data) ? $transactionData->data : [], $transactionData->tid
86 | );
87 | }
88 |
89 | return $request;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/RecipientList/GentleSql.php:
--------------------------------------------------------------------------------
1 | getTableName() table.
11 | */
12 | abstract class GentleSql extends Sql
13 | {
14 | /**
15 | * Returns the tablename to work with
16 | *
17 | * @return string
18 | */
19 | abstract protected function getTableName();
20 |
21 | /**
22 | * This increases the bounce-counter each time a mail has bounced.
23 | * Hard bounces count more that soft ones. After 2 hards or 10 softs the user will be disabled.
24 | * You should be able to reset then in the backend
25 | *
26 | * @param string $email the email address of the recipient
27 | * @param int $bounceLevel this is the level of the bounce
28 | *
29 | * @return bool success of the bounce-handling
30 | */
31 | public function registerBounce($email, $bounceLevel)
32 | {
33 | $db = Tools::getDatabaseConnection();
34 |
35 | $increment = 0;
36 | switch ($bounceLevel) {
37 | case EmailParser::NEWSLETTER_UNSUBSCRIBE:
38 | $increment = 10;
39 | break;
40 | case EmailParser::NEWSLETTER_HARDBOUNCE:
41 | $increment = 5;
42 | break;
43 | case EmailParser::NEWSLETTER_SOFTBOUNCE:
44 | $increment = 1;
45 | break;
46 | }
47 |
48 | if ($increment) {
49 | $db->sql_query('UPDATE ' . $this->getTableName() . "
50 | SET tx_newsletter_bounce = tx_newsletter_bounce + $increment
51 | WHERE email = '$email'");
52 |
53 | return $db->sql_affected_rows();
54 | }
55 |
56 | return false;
57 | }
58 |
59 | /**
60 | * This is a default action for registered clicks.
61 | * Here we just reset the bounce counter. If the user reads the mail, it must have succeded.
62 | * It can also be used for marketing or statistics purposes
63 | *
64 | * @param string $email the email address of the recipient
65 | */
66 | public function registerClick($email)
67 | {
68 | Tools::getDatabaseConnection()->sql_query('UPDATE ' . $this->getTableName() . "
69 | SET tx_newsletter_bounce = 0
70 | WHERE email = '$email'");
71 | }
72 |
73 | /**
74 | * Like the registerClick()-method, but just for embedded spy-image.
75 | *
76 | * @param string $email the email address of the recipient
77 | */
78 | public function registerOpen($email)
79 | {
80 | Tools::getDatabaseConnection()->sql_query('UPDATE ' . $this->getTableName() . "
81 | SET tx_newsletter_bounce = 0
82 | WHERE email = '$email'");
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Resources/Private/Language/locallang_csh_tx_newsletter_domain_model_newsletter.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | When the newsletter will start sending emails
8 |
9 |
10 | beginTime
11 |
12 |
13 | endTime
14 |
15 |
16 | 0-7 values to indicates when this newsletter will repeat
17 |
18 |
19 | Tool used to convert to plain text
20 |
21 |
22 | Whether this newsletter is for test purpose. If it is it will be ignored in statistics
23 |
24 |
25 | List of files to be attached (comma separated list
26 |
27 |
28 | The name of the newsletter sender
29 |
30 |
31 | The email of the newsletter sender
32 |
33 |
34 | The name of the newsletter Reply-to:
35 |
36 |
37 | The email of the newsletter Reply-to:
38 |
39 |
40 | injectOpenSpy
41 |
42 |
43 | injectLinksSpy
44 |
45 |
46 | bounceAccount
47 |
48 |
49 | recipientList
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Resources/Public/JavaScript/Override/DirectProxyPatch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Patch the DirectProxy to accept paramOrders for store delete, update and create requests.
3 | */
4 | Ext.override(Ext.data.DirectProxy, {
5 | doRequest: function (action, rs, params, reader, callback, scope, options) {
6 | var args = [],
7 | directFn = this.api[action] || this.directFn;
8 |
9 | if (Ext.isObject(this.paramOrder)) {
10 | paramOrder = this.paramOrder[action];
11 | if (typeof paramOrder == 'string') {
12 | paramOrder = this.paramOrder.split(/[\s,|]/);
13 | }
14 | } else {
15 | paramOrder = this.paramOrder;
16 | }
17 |
18 | paramsCreateUpdateDestroy = params.jsonData;
19 |
20 | switch (action) {
21 | case Ext.data.Api.actions.create:
22 | if (directFn.directCfg.method.len > 0) {
23 | if (paramOrder) {
24 | for (var i = 0, len = paramOrder.length; i < len; i++) {
25 | args.push(paramsCreateUpdateDestroy[paramOrder[i]]);
26 | }
27 | }
28 | }
29 | break;
30 | case Ext.data.Api.actions.read:
31 | // If the method has no parameters, ignore the paramOrder/paramsAsHash.
32 | if (directFn.directCfg.method.len > 0) {
33 | if (paramOrder) {
34 | for (var i = 0, len = paramOrder.length; i < len; i++) {
35 | args.push(params[paramOrder[i]]);
36 | }
37 | } else if (this.paramsAsHash) {
38 | args.push(params);
39 | }
40 | }
41 | break;
42 | case Ext.data.Api.actions.update:
43 | if (directFn.directCfg.method.len > 0) {
44 | if (paramOrder) {
45 | for (var i = 0, len = paramOrder.length; i < len; i++) {
46 | args.push(paramsCreateUpdateDestroy[paramOrder[i]]);
47 | }
48 | }
49 | }
50 | break;
51 | case Ext.data.Api.actions.destroy:
52 | if (directFn.directCfg.method.len > 0) {
53 | if (paramOrder) {
54 | for (var i = 0, len = paramOrder.length; i < len; i++) {
55 | args.push(paramsCreateUpdateDestroy[paramOrder[i]]);
56 | }
57 | }
58 | }
59 | break;
60 | }
61 |
62 | var trans = {
63 | params: params || {},
64 | request: {
65 | callback: callback,
66 | scope: scope,
67 | arg: options,
68 | },
69 | reader: reader,
70 | };
71 |
72 | args.push(this.createCallback(action, rs, trans), this);
73 | directFn.apply(window, args);
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/Classes/MVC/ExtDirect/Request.php:
--------------------------------------------------------------------------------
1 | objectManager = $objectManager;
49 | }
50 |
51 | /**
52 | * Creates an Ext Direct Transaction and adds it to the request instance.
53 | *
54 | * @param string $action The "action" – the "controller object name" in FLOW3 terms
55 | * @param string $method The "method" – the "action name" in FLOW3 terms
56 | * @param array $data Numeric array of arguments which are eventually passed to the FLOW3 action method
57 | * @param mixed $tid The ExtDirect transaction id
58 | */
59 | public function createAndAddTransaction($action, $method, array $data, $tid)
60 | {
61 | $transaction = $this->objectManager->get(Transaction::class, $this, $action, $method, $data, $tid);
62 | $this->transactions[] = $transaction;
63 | }
64 |
65 | /**
66 | * Getter for transactions.
67 | *
68 | * @return array
69 | */
70 | public function getTransactions()
71 | {
72 | return $this->transactions;
73 | }
74 |
75 | /**
76 | * Whether this request represents a form post or not.
77 | *
78 | * @return bool
79 | */
80 | public function isFormPost()
81 | {
82 | return $this->formPost;
83 | }
84 |
85 | /**
86 | * Marks this request as representing a form post or not.
87 | *
88 | * @param bool $formPost
89 | */
90 | public function setFormPost($formPost)
91 | {
92 | $this->formPost = $formPost;
93 | }
94 |
95 | /**
96 | * Whether this request represents a file upload or not.
97 | *
98 | * @return bool
99 | */
100 | public function isFileUpload()
101 | {
102 | return $this->fileUpload;
103 | }
104 |
105 | /**
106 | * Marks this request as representing a file upload or not.
107 | *
108 | * @param bool $fileUpload
109 | */
110 | public function setFileUpload($fileUpload)
111 | {
112 | $this->fileUpload = $fileUpload ? true : false;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Classes/Domain/Model/Link.php:
--------------------------------------------------------------------------------
1 | objectManager = GeneralUtility::makeInstance(ObjectManager::class);
51 | }
52 |
53 | /**
54 | * Setter for url
55 | *
56 | * @param string $url url
57 | */
58 | public function setUrl($url)
59 | {
60 | $this->url = $url;
61 | }
62 |
63 | /**
64 | * Getter for url
65 | *
66 | * @return string url
67 | */
68 | public function getUrl()
69 | {
70 | return $this->url;
71 | }
72 |
73 | /**
74 | * Setter for newsletter
75 | *
76 | * @param Newsletter $newsletter newsletter
77 | */
78 | public function setNewsletter(Newsletter $newsletter)
79 | {
80 | $this->newsletter = $newsletter;
81 | }
82 |
83 | /**
84 | * Getter for newsletter
85 | *
86 | * @return Newsletter newsletter
87 | */
88 | public function getNewsletter()
89 | {
90 | $newsletterRepository = $this->objectManager->get(NewsletterRepository::class);
91 |
92 | return $newsletterRepository->findByUid($this->newsletter);
93 | }
94 |
95 | /**
96 | * Setter for openedCount
97 | *
98 | * @param int $openedCount openedCount
99 | */
100 | public function setOpenedCount($openedCount)
101 | {
102 | $this->openedCount = $openedCount;
103 | }
104 |
105 | /**
106 | * Getter for openedCount
107 | *
108 | * @return int openedCount
109 | */
110 | public function getOpenedCount()
111 | {
112 | return $this->openedCount;
113 | }
114 |
115 | public function getOpenedPercentage()
116 | {
117 | $emailRepository = $this->objectManager->get(EmailRepository::class);
118 | $emailCount = $emailRepository->getCount($this->newsletter);
119 |
120 | if ($emailCount == 0) {
121 | return 0;
122 | }
123 |
124 | return round($this->getOpenedCount() * 100 / $emailCount, 2);
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Tests/Functional/ToolsTest.php:
--------------------------------------------------------------------------------
1 | importDataSet(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Fixtures/tt_content.xml');
19 |
20 | $db = $this->getDatabaseConnection();
21 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_newsletter', 'begin_time != 0 AND end_time != 0');
22 | $this->assertSame(0, $count);
23 |
24 | Tools::createAllSpool();
25 |
26 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_newsletter', 'begin_time != 0 AND end_time != 0');
27 | $this->assertSame(1, $count, 'newsletter should be marked as spooled');
28 |
29 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_email', 'newsletter = 20 AND begin_time = 0');
30 | $this->assertSame(2, $count, 'two emails must have been created but not sent yet');
31 |
32 | $lastInsertedEmail = $db->exec_SELECTgetSingleRow('*', 'tx_newsletter_domain_model_email', 'newsletter = 20 AND begin_time = 0');
33 | $this->assertNotSame(md5('0' . $lastInsertedEmail['recipient_address']), $lastInsertedEmail['auth_code'], 'the UID used in authCode must never be 0');
34 | $this->assertSame(md5($lastInsertedEmail['uid'] . $lastInsertedEmail['recipient_address']), $lastInsertedEmail['auth_code'], 'the UID used in authCode should be the real value');
35 |
36 | // Prepare a mock to always validate content
37 | /** @var Validator|\PHPUnit_Framework_MockObject_MockObject $mockValidator */
38 | $mockValidator = $this->getMock(Validator::class, ['validate'], [], '', false);
39 | $mockValidator->method('validate')->will($this->returnValue(
40 | [
41 | 'content' => 'some very interesting content link',
42 | 'errors' => [],
43 | 'warnings' => [],
44 | 'infos' => [],
45 | ]
46 | ));
47 |
48 | // Force email to NOT be sent
49 | global $TYPO3_CONF_VARS;
50 | $TYPO3_CONF_VARS['MAIL']['transport'] = 'Swift_NullTransport';
51 |
52 | /** @var NewsletterRepository $newsletterRepository */
53 | $newsletterRepository = $this->objectManager->get(NewsletterRepository::class);
54 | $newsletter = $newsletterRepository->findByUid(20);
55 | $newsletter->setValidator($mockValidator);
56 | Tools::runSpool($newsletter);
57 |
58 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_email', 'newsletter = 20 AND begin_time != 0 AND end_time != 0 AND recipient_data != ""');
59 | $this->assertSame(2, $count, 'should have sent two emails');
60 | $count = $db->exec_SELECTcountRows('*', 'tx_newsletter_domain_model_link', 'newsletter = 20');
61 | $this->assertSame(1, $count, 'should have on1 new link');
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/Unit/Domain/Model/RecipientList/CsvFileTest.php:
--------------------------------------------------------------------------------
1 | subject = new CsvFile();
15 | }
16 |
17 | /**
18 | * @test
19 | */
20 | public function getCsvSeparatorReturnsInitialValueForString()
21 | {
22 | $this->assertSame(',', $this->subject->getCsvSeparator());
23 | }
24 |
25 | /**
26 | * @test
27 | */
28 | public function setCsvSeparatorForStringSetsCsvSeparator()
29 | {
30 | $this->subject->setCsvSeparator('Conceived at T3CON10');
31 | $this->assertAttributeSame('Conceived at T3CON10', 'csvSeparator', $this->subject);
32 | }
33 |
34 | /**
35 | * @test
36 | */
37 | public function getCsvFieldsReturnsInitialValueForString()
38 | {
39 | $this->assertSame('', $this->subject->getCsvFields());
40 | }
41 |
42 | /**
43 | * @test
44 | */
45 | public function setCsvFieldsForStringSetsCsvFields()
46 | {
47 | $this->subject->setCsvFields('Conceived at T3CON10');
48 | $this->assertAttributeSame('Conceived at T3CON10', 'csvFields', $this->subject);
49 | }
50 |
51 | /**
52 | * @test
53 | */
54 | public function getCsvFilenameReturnsInitialValueForString()
55 | {
56 | $this->assertSame('', $this->subject->getCsvFilename());
57 | }
58 |
59 | /**
60 | * @test
61 | */
62 | public function setCsvFilenameForStringSetsCsvFilename()
63 | {
64 | $this->subject->setCsvFilename('Conceived at T3CON10');
65 | $this->assertAttributeSame('Conceived at T3CON10', 'csvFilename', $this->subject);
66 | }
67 |
68 | protected function prepareDataForEnumeration()
69 | {
70 | $this->subject = $this->getMock(CsvFile::class, ['getPathname'], [], '', false);
71 | $this->subject->expects($this->once())->method('getPathname')->will($this->returnValue(__DIR__));
72 | $this->subject->setCsvFilename('data.csv');
73 | }
74 |
75 | /**
76 | * @test
77 | */
78 | public function canEnumerateRecipients()
79 | {
80 | $this->prepareDataForEnumeration();
81 | $this->subject->setCsvFields('email,name,some_flags');
82 | $this->subject->init();
83 | $this->assertSame(2, $this->subject->getCount());
84 |
85 | $recipient1 = [
86 | 'email' => 'john@example.com',
87 | 'name' => 'John',
88 | 'some_flags' => '1',
89 | 'plain_only' => false,
90 | ];
91 |
92 | $recipient2 = [
93 | 'email' => 'bob@example.com',
94 | 'name' => 'Roger',
95 | 'some_flags' => '0',
96 | 'plain_only' => false,
97 | ];
98 |
99 | $this->assertSame($recipient1, $this->subject->getRecipient());
100 | $this->assertSame($recipient2, $this->subject->getRecipient());
101 | $this->assertFalse($this->subject->getRecipient());
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Classes/MVC/Controller/ExtDirectActionController.php:
--------------------------------------------------------------------------------
1 | persistenceManager = $persistenceManager;
32 | }
33 |
34 | /**
35 | * Initializes the View to be a \Ecodev\Newsletter\ExtDirect\View\ExtDirectView that renders json without Template Files.
36 | *
37 | * @param ViewInterface $view
38 | */
39 | public function initializeView(ViewInterface $view)
40 | {
41 | if ($this->request->getFormat() === 'extdirect') {
42 | $this->view = $this->objectManager->get(ExtDirectView::class);
43 | $this->view->setControllerContext($this->controllerContext);
44 | }
45 | }
46 |
47 | /**
48 | * Override parent method to render error message for ExtJS (in JSON).
49 | * Also append detail about what property failed to error message.
50 | */
51 | protected function errorAction()
52 | {
53 | $message = parent::errorAction();
54 |
55 | // Append detail of properties if available
56 | // Message layout is not optimal, but at least we avoid code duplication
57 | foreach ($this->argumentsMappingResults->getErrors() as $error) {
58 | if ($error instanceof PropertyError) {
59 | foreach ($error->getErrors() as $subError) {
60 | $message .= 'Error: ' . $subError->getMessage() . PHP_EOL;
61 | }
62 | }
63 | }
64 | if ($this->view instanceof JsonView) {
65 | $this->view->setVariablesToRender(['flashMessages', 'error', 'success']);
66 | $this->view->assign('flashMessages', $this->controllerContext->getFlashMessageQueue()->getAllMessagesAndFlush());
67 | $this->view->assign('error', $message);
68 | $this->view->assign('success', false);
69 | }
70 | }
71 |
72 | /**
73 | * Translate key
74 | *
75 | * @param string $key
76 | * @param array $args
77 | *
78 | * @return string
79 | */
80 | protected function translate($key, array $args = [])
81 | {
82 | return LocalizationUtility::translate($key, 'newsletter', $args);
83 | }
84 |
85 | /**
86 | * Flush flashMessages into view
87 | */
88 | protected function flushFlashMessages()
89 | {
90 | $this->view->assign('flashMessages', $this->controllerContext->getFlashMessageQueue()->getAllMessagesAndFlush());
91 | }
92 | }
93 |
--------------------------------------------------------------------------------