Compare commits

..

356 Commits

Author SHA1 Message Date
5293589eea Release 3.0.0-beta.35.0.0 2017-06-13 17:05:50 +03:00
07f4c0bc1e Merge pull request #923 from mailpoet/fast_svn_publish
Use a faster SVN workflow in release publishing automation [MAILPOET-913]
2017-06-13 07:50:47 -04:00
7d9a072545 Merge pull request #928 from mailpoet/wp_mail_switch
Replaces SwiftMailer's mail() method with WP's PHPMailer [MAILPOET-926]
2017-06-13 14:10:07 +03:00
67ffcb5485 Clean up the SVN dir before checking out for faster operation [MAILPOET-913] 2017-06-13 12:22:32 +03:00
02098a3cf0 Merge pull request #888 from mailpoet/mp2tomp3migration
MP2 to MP3 migration [MAILPOET-946]
2017-06-12 19:41:46 +03:00
eab8065154 Remove the promise of migrating newsletters and statistics.
We will not be offering these options!
2017-06-12 17:36:04 +03:00
fb8ecef1c3 Merge pull request #932 from mailpoet/revert-931-minimum_php_version_fix
Revert "Updates minimum required "release version" from 3 to 30 [MAILPOET-944]"
2017-06-12 10:27:09 -04:00
14955e5022 Revert "Updates minimum required "release version" from 3 to 30"
This reverts commit e83c0ff0bd.
2017-06-12 10:20:01 -04:00
e83c0ff0bd Updates minimum required "release version" from 3 to 30 2017-06-12 14:52:44 +01:00
09db1aac22 Merge pull request #929 from mailpoet/mp_api_fix
Fixes confirmation emails not sent when adding subscriber in MP API [MAILPOET-942]
2017-06-12 14:22:05 +03:00
5fa1eb643a Remove tests that may fail because of the original database content 2017-06-12 13:13:17 +02:00
898913a517 Change: the users belonging to the wp_users segment must be imported as subscribed to the wp_users segment 2017-06-12 13:13:17 +02:00
3a4b364bcd Modify unit tests following previous changes 2017-06-12 13:13:16 +02:00
d373d10f6c Fixed: Segments with a null description were not imported 2017-06-12 13:13:15 +02:00
c07e1eff3c New: Redirect the user to the migration page if the import has started but is not finished to prevent him from using MailPoet with incomplete data. So he can resume the import where it left off. 2017-06-12 13:13:15 +02:00
f5e985baa4 Change: Move the progress bar JSON file to the wp-content/uploads/mailpoet directory and remove "mailpoet" from its filename 2017-06-12 13:13:14 +02:00
2c93a105cc Fixed: Migration script suggests displays "Upgrade completed!" text and allows the user to click the "Go to MailPoet" button while the migration is still being performed. 2017-06-12 13:13:14 +02:00
cbf0a7684f Fixed: If there are WP users that haven't been added as MP2 WP subscribers, they won't be present as MP3 subscribers after migration either. 2017-06-12 13:13:13 +02:00
2632feba31 Fixed: On Migration page, clicking on "Start upgrade No thanks, I'll skip and start from scratch." redirects to the same migration page. 2017-06-12 13:13:12 +02:00
c96bc755c7 Fixed: the progress bar goes over 100% and displays "NaN%" if the MP2 tables are empty. 2017-06-12 13:13:12 +02:00
bbad772d7a PHP Notice: Use of undefined constant IMPORT_TIMEOUT_IN_SECONDS 2017-06-12 13:13:11 +02:00
520a3c43be Changes for API 2017-06-12 13:13:10 +02:00
eb70df1466 The "Start upgrade" button can now resume the import. 2017-06-12 13:13:09 +02:00
5977b8b4bc Translations 2017-06-12 13:13:09 +02:00
2bb7d95e37 Remove the "Start upgrade" button when the upgrade is completed
Add the "Upgrade completed" message
Add the "Go to MailPoet" button
2017-06-12 13:13:08 +02:00
c64959dce0 Fixed: Segments number can be wrong 2017-06-12 13:13:08 +02:00
ead0792b32 Rename getDataToMigrate() to getDataToMigrateAndResetProgressBar() 2017-06-12 13:13:07 +02:00
7bd52d456c Make the Migration page accessible through admin.php?page=mailpoet-migration 2017-06-12 13:13:06 +02:00
8517896660 Rename imported_data_mapping table to mapping_to_external_entities 2017-06-12 13:13:06 +02:00
c78933f7c4 Numerous fixes following the code review:
H1 title should be "Welcome to MailPoet version 3!", as per wireframes
remove logo in top right of the page
remove the classes feature-section one-col to the parent
new styles for #logger
progress bar: ensure that the color and size for the font are the same as our current progress bar.
display the progress bar at 100% when the import of subscribers and lists is complete
move loadSQL() function in tests/_support/Helper/Database.php
remove CSS browser prefixes
use AMD module definition to embed the JavaScript
remove extra whitespace in JavaScript file
remove the redundant functions descriptions
rename objectPlugin to mailpoet_mp2_migrator
replace private $chunks_size by the constant CHUNK_SIZE
add the constant IMPORT_TIMEOUT_IN_SECONDS
replace Helpers::mysqlDate() by $datetime->formatTime(time(), \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT)
make the log messages translatable
fix PHPDoc
replace Env::$plugin_name by 'mailpoet' in the translation functions
use $snake_case for variable names, use lowercaseCamelCase for method names, use CamelCase for class names
define MP2 table names as constants
add spaces around ternary operators
use the models for MP3 entities, instead of counting via raw SQL queries
use \ORM::for_table('some_table')::count()
2017-06-12 13:13:05 +02:00
707d5efec1 Modify H1 title
Remove logo
Modify progress label font size
Modify progress bar style
2017-06-12 13:13:04 +02:00
acfb3aefba Use snake_case 2017-06-12 13:13:04 +02:00
cb6f4046a2 Removes utf8 decoding that breaks import 2017-06-12 13:13:03 +02:00
667aa91581 Adds character encoding 2017-06-12 13:13:02 +02:00
b7f7dc6728 Add unit tests for the MP2Migrator class
Add the method "invokeMethod" in the MailPoetTest class to be able to test private and protected methods
Move the "rowsCount" method in the Helpers class for reusability
Add the method "loadSQL" in the Helpers class
Store the mapping between MP2 user ID and MP3 subscriber ID
2017-06-12 13:13:02 +02:00
ad1e8feb23 QA fixes 2017-06-12 13:13:01 +02:00
486b382c88 Use API versioning 2017-06-12 13:13:00 +02:00
472be3b071 Import the MP2 users lists
Import the MP2 users
Add the table "imported_data_mapping"
Add the function Helpers::mysql_date
2017-06-12 13:13:00 +02:00
37a9fd9e1b Use Setting::getValue() and Setting::setValue() instead of get_option() and update_option() 2017-06-12 13:12:59 +02:00
c984ac7a66 New: Display the number of data to migrate
Several fixes following the code review:
- For styles we use Stylus. (http://stylus-lang.com/)
  In Stylus styles curly brackets and terminating semicolons are unnecessary. Indentation is sufficient to denote blocks of styles
  You can also use variables to avoid duplication (e.g. mentioning the same color multiple times)
  Vendor prefixes are not necessary. We use the Nib mixin for Stylus to handle those (http://tj.github.io/nib/)
  => DONE

- `admin.js` bundle is included on all admin pages, so we need to ensure that migration fires only on the migration page and nowhere else.
  You can even create a separate bundle only for migration files, as they won't be necessary on other admin pages.
  => DONE

- MP2MigratorAPI => MP2Migrator endpoint would be just as fine
  => DONE

- For storing migration files, you can use the `Env::$temp_path` path
  => DONE

- `proposeMigration()` the method name disagrees with the comment.
  Comment suggests it tests if migration can be or should be performed
  Method name suggests that it proposes doing the migration (to the user?) - not very clear
  => DONE: the new name is isMigrationNeeded()

  And not only does it test, it may also update the `mailpoet_migration_complete` option, which is confusing and is an unexpected side-effect.
  => DONE

  The migration class itself `MP2Migrator` shouldn't even care about _GET, _REQUEST or _POST arguments. It should only work with what is passed to it.
  => DONE

- In views, please make sure all human-friendly texts use WP's gettext functions for translations (e.g. __('text'))
=> DONE
2017-06-12 13:12:58 +02:00
a1ea56f505 Coding standard fixes 2017-06-12 13:12:58 +02:00
dd7f959731 Migration from Mailpoet 2 to Mailpoet 3 : phase 0
Interface
2017-06-12 13:12:57 +02:00
09f4f2e78a Fix a unit test [MAILPOET-942] 2017-06-12 11:38:01 +03:00
502250a1a3 Merge pull request #930 from mailpoet/import_update
Adjusts WP's email validation regex to JS's syntax [MAILPOET-943]
2017-06-12 11:10:38 +03:00
48e37f6797 Adjusts WP's email validation regex syntax to JS 2017-06-11 12:31:54 -04:00
420058a86d Optimizes tests
Adds new tests
2017-06-11 12:18:14 -04:00
6c777ca074 Reloads subscriber or else status and other fields populated at save
time are not returned
Schedules welcome notifications only for subscribers with "subscribed"
status
Extracts confirmation email sending/welcome notification scheduling into
separate functions for easy testing
2017-06-11 12:16:34 -04:00
a481debb77 Adds "html" body to the mailing method test message 2017-06-09 09:53:07 -04:00
c91f8ccc7a Replaces SwiftMailer's mail() method with WP's PHPMailer 2017-06-09 09:52:22 -04:00
6a2b5e28c2 Merge pull request #927 from mailpoet/fix-alc-buttons-settings
Fix ALC buttons settings when updating all buttons [MAILPOET-896]
2017-06-09 09:35:36 +03:00
bc51b6efc8 Add replaceAllButtonStyles test to button
[MAILPOET-896]
2017-06-09 07:24:26 +01:00
152edda03f Add unit test
[MAILPOET-896]
2017-06-08 16:58:30 +01:00
5029b73027 Merge pull request #924 from mailpoet/model_validator_fix
Fixes reference to $this in anonymous function on PHP 5.3 [MAILPOET-934]
2017-06-08 17:13:04 +03:00
35c25d3337 Updates length to support IPv4 mapped IPv6 addresses 2017-06-08 13:45:50 +01:00
ae25e95d51 Updates length of columns with IP addresses to 39 2017-06-08 13:45:50 +01:00
5b8d0c63a1 Fix a changelog link [MAILPOET-935] 2017-06-08 10:47:30 +01:00
94fb8c6096 Merge pull request #918 from mailpoet/beacon_update_with_premium_version
UTF-8 encodes beacon data
2017-06-08 12:38:18 +03:00
93ef1d0197 Fix ALC buttons settings when updating all buttons
[MAILPOET-896]
2017-06-08 10:19:24 +01:00
501d00b0cc Merge pull request #919 from mailpoet/fix-composer-cp-mac
Remove -d option from composer.json script
2017-06-08 12:15:18 +03:00
9af3dc1f9d Fixes reference to $this in anonymous function on PHP 5.3 2017-06-07 11:52:46 -04:00
3705ed7da0 Use a faster SVN workflow in release publishing automation [MAILPOET-913] 2017-06-07 17:46:10 +03:00
5b99e66d8d Merge pull request #922 from mailpoet/fix-tests-problems
Fix tests problems
2017-06-07 10:34:20 -04:00
cbedd5ff40 Bump up release version to 3.0.0-beta.34.0.0 2017-06-07 16:14:47 +03:00
bfcd6f10fc Merge pull request #921 from mailpoet/premium_launch
Add Premium features for the launch, Free-side
2017-06-07 14:54:21 +03:00
79362e9955 Update 'buy Premium only' link URL on the Premium page 2017-06-07 14:42:55 +03:00
fcf272b44a Fix lint error 2017-06-07 11:49:07 +01:00
b80683a9a1 Fix unit tests for PHPUnit v6
Codeception from version 2.3 up comes with PHPUnit v6 which changed
__construct behaviour. Our tests have to call parent __constructor in
order to work. The error was:
[PHPUnit\Framework\Exception] array_merge(): Argument #1 is not an array
2017-06-07 11:32:33 +01:00
5349f3a59a Use ssl while checking url
My internet provider tried to be helpfull and offeres content on
invalid urls. That is only behaviour for http:// scheme and not
for https:// scheme so I need to enforce ssl so that invalid url
fails even in my house.
2017-06-07 11:30:31 +01:00
95072a9ac5 Restore empty license key warnings [MAILPOET-933] 2017-06-07 12:39:45 +03:00
8c372b0909 Update support video, add a border to videos [MAILPOET-885] 2017-06-07 12:39:45 +03:00
580dd38b3a Rename methods for consistency and better readability [PREMIUM-9] 2017-06-07 12:39:44 +03:00
47d4e98aae Integrate installer with the Release API [PREMIUM-9] 2017-06-07 12:39:43 +03:00
7ebb7bac17 Add Premium installation/activation UI [PREMIUM-9] 2017-06-07 12:39:43 +03:00
6cbce2fc97 Replace Premium gif assets with mp4 videos [MAILPOET-885] 2017-06-07 12:39:41 +03:00
e8a950f32c Add Premium page images to plugin repository assets [MAILPOET-885] 2017-06-07 12:39:12 +03:00
4f722ecd8a Add hooks for actions row for standard and notification history newsletter types [PREMIUM-13] 2017-06-07 12:30:50 +03:00
478359f9ff Add minor improvements to stats [PREMIUM-13] 2017-06-07 12:30:48 +03:00
a1720a5cf1 Use videos instead of gifs on the Premium page [MAILPOET-885] 2017-06-07 12:30:47 +03:00
3f0ef3ded7 Replace Premium gif assets with mp4 videos [MAILPOET-885] 2017-06-07 12:30:47 +03:00
dcb25c1a6b Swap welcome emails and support images [MAILPOET-885] 2017-06-07 12:30:45 +03:00
c5dd575324 Add Premium page images to plugin repository assets [MAILPOET-885] 2017-06-07 12:30:45 +03:00
6eca26a4e2 Add UTM params to Premium page links [MAILPOET-885] 2017-06-07 12:30:43 +03:00
e10fa065bd Add Premium page [MAILPOET-885] 2017-06-07 12:30:42 +03:00
49673fabbd Does not display logo when MSS is active or preview is enabled
Adds additional unit tests and optimizes existing
2017-06-07 12:30:42 +03:00
1c1a210542 Adds MailPoet logo to newsletters in the free version 2017-06-07 12:30:41 +03:00
30277d92cd Updates action button 2017-06-07 12:30:41 +03:00
fb940065ea Fix a source value for Premium key in the worker [MAILPOET-890] 2017-06-07 12:30:40 +03:00
afa06342a5 Update link for the Premium page 2017-06-07 12:30:40 +03:00
03d2ff5f26 Make use of returned promises for parallel requests loading modal [MAILPOET-890] 2017-06-07 12:30:40 +03:00
ec71dff40d Change strings on the 2000 limit page for Premium [MAILPOET-888] 2017-06-07 12:30:39 +03:00
58faf64a5c Add a Premium page link to bounced subscribers listing [MAILPOET-887] 2017-06-07 12:30:39 +03:00
65ff14a81d Adds hook to modify newsletter actions 2017-06-07 12:30:38 +03:00
f7efe44f09 Fix ServicesChecker behavior, make MSS check stricter [MAILPOET-890] 2017-06-07 12:30:38 +03:00
cf22e81ae1 Updates exposed components 2017-06-07 12:30:37 +03:00
7aa0f21d11 Rework 'Send with...' tab UI, make a single license key field [MAILPOET-890] 2017-06-07 12:30:37 +03:00
2e31e3d37c Exposes components required for welcome notifications 2017-06-07 12:30:36 +03:00
3e988b7a56 Removes welcome notification creation component and routes
Updates welcome notification template
2017-06-07 12:30:36 +03:00
ce3eb06924 Prevents welcome emails from being created
Adds filter to modify available email types
2017-06-07 12:30:35 +03:00
a37ff8d398 Limits utf8 encoding to php_uname function only (Windows may use an
encoding other than ISO-8859-1 in non-English locales)
2017-06-06 18:16:04 -04:00
d0bdb1a47b Adds XML polyfill for utf8_encode/decode functions 2017-06-06 15:06:39 -04:00
855f2a55d4 Remove -d option from composer.json script
This flag is not present on mac and it makes this script
fail on MacOS.
2017-06-06 15:49:51 +01:00
fc7ec9bded UTF-8 encodes beacon data 2017-06-06 10:10:07 -04:00
028de860a2 Merge pull request #913 from mailpoet/sending_queue_update_on_newsletter_status_change
Prevents processing of sending queues when newsletter is paused [MAILPOET-900]
2017-06-06 15:00:21 +03:00
5af91d028d Merge pull request #916 from mailpoet/unsubscribe_fix
Fixes incorrect dependency that breaks unsubscribe link [MAILPOET-931]
2017-06-06 13:12:16 +03:00
a4bcf870bb Merge pull request #915 from mailpoet/beacon_update_with_premium_version
Adds premium version information to HS beacon [MAILPOET-930]
2017-06-06 09:49:09 +03:00
e06f2f5f0b References the correct class and removes unused dependency 2017-06-05 15:20:14 -04:00
c101645d93 Adds premium version information to HS beacon 2017-06-05 14:59:30 -04:00
b8904c2d51 Merge pull request #914 from mailpoet/subscriber_email_validation_logic_update
Uses WP's function to validate subscriber email address [MAILPOET-925]
2017-06-05 19:25:53 +03:00
099db4e1c8 Removes unused variable
Fixes typo in error message
2017-06-05 11:59:13 -04:00
cdf36ccb20 Trashes/restores/deletes (+ same bulk actions) children newsletters and
associations as per discussion on Slack:
https://mailpoet.slack.com/archives/C02MTKAJL/p1496427873491785
2017-06-05 11:36:04 -04:00
79b6ab1d15 Finishes incomplete test 2017-06-05 10:30:45 -04:00
95114774da Merge pull request #912 from mailpoet/sending_queue_and_post_notification_history_listing_update
Sending queue and post notification history listing update [MAILPOET-928]
2017-06-05 14:53:22 +03:00
7f566fb672 Adds client-side check for invalid characters in email addresses
Adds server-side validation of email addresses using WP's is_email()
2017-06-04 18:48:11 -04:00
d27968a215 Uses WP's is_email() to validate email addresses in Subscriber model 2017-06-04 18:19:37 -04:00
344990d59e Only processes queues when newsletter exists and is active/sending 2017-06-02 12:03:34 -04:00
ea831ef160 Prevents processing scheduled sending queues for inactive newsletters 2017-06-01 19:50:25 -04:00
8314b05fce Displays "not yet sent" as a sent date when post notification has not
yet been sent/being sent
2017-06-01 14:58:25 -04:00
fd33cc7068 Uses parent newsletter's subject when queue's rendered subject is not
available
2017-06-01 14:26:50 -04:00
92e4cc6a24 Sorts sending queue according to their creation date (oldest to newest) 2017-06-01 13:21:03 -04:00
dd4bebb570 Bumps up release version to Beta 33.1 and updates changelog 2017-05-30 14:27:29 -04:00
99aed2cb01 Merge pull request #908 from mailpoet/utf8_fix
Apply charset and collation only if they are specified [MAILPOET-924]
2017-05-30 14:25:33 -04:00
92616063ec Fix unit test for generating DB source name 2017-05-30 21:18:45 +03:00
c56b56f4aa Apply charset and collation only if they are specified 2017-05-30 21:05:01 +03:00
33d6533c64 Bumps up release version to Beta 33 and updates changelog 2017-05-30 09:30:12 -04:00
55d7a0dd01 Merge pull request #907 from mailpoet/welcome_email_scheduler_update
Schedules welcome notifications after subscription is confirmed [MAILPOET-907]
2017-05-30 14:02:48 +03:00
8b2ac99eda Merge pull request #903 from mailpoet/beacon_update
Adds server OS, web server information and cron ping response to HS beacon [MAILPOET-918]
2017-05-30 12:15:46 +03:00
dba21c68fd Schedules welcome notification upon subscription when subscription
confirmation is disabled
Schedules welcome notification upon subscription confirmation
Checks when 'REMOTE_ADDR' is not set
Adds unit tests
2017-05-29 22:04:47 -04:00
5b40652737 Merge pull request #906 from mailpoet/form_widget_rendering
Wrap form widget title as configured by the sidebar [MAILPOET-910]
2017-05-29 19:44:11 -04:00
7f0396747d Adds cron ping URL instead of ping response
Checks for existence of SERVER_SOFTWARE variable
2017-05-29 19:29:56 -04:00
e9dfff8e66 Wrap form widget title as configured by the sidebar 2017-05-29 18:12:51 +03:00
040c4da6c3 Merge pull request #904 from mailpoet/php_version_requirement_update
Requires PHP 5.3.3 in line with composer.json requirements [MAILPOET-921]
2017-05-29 13:15:13 +03:00
80a237504d Requires PHP 5.3.3 in line with composer.json requirements 2017-05-26 11:34:31 -04:00
4e2e09ea24 Adds server OS, web server information and cron ping response to HS
beacon
2017-05-25 15:55:33 -04:00
87b9fbdc16 Merge pull request #900 from mailpoet/utf8
Synchronize MailPoet DB connection charset with WordPress [MAILPOET-748]
2017-05-24 16:09:58 +03:00
a071a14eec Update only those queries, for which conversion will yield correct char
lengths
2017-05-24 15:45:12 +03:00
5ae006b10f Update plugin version [MAILPOET-748] 2017-05-24 15:45:12 +03:00
9d21ebd26e Fix a comment. UTF8MB4 is a superset of UTF8 2017-05-24 15:45:12 +03:00
fcff6de3c3 Skip conversion for charset utf8 -> utf8mb4 2017-05-24 15:45:12 +03:00
3d2168856d Fix unit tests for Env charsets and collations 2017-05-24 15:45:12 +03:00
a6eb1b06da Add connection charset sync with WP and convert existing data to it 2017-05-24 15:45:12 +03:00
21d0c3518e Merge pull request #901 from mailpoet/transifix
Bundle translations completed by 75%, remove PO files in build [MAILPOET-916]
2017-05-23 18:40:46 +03:00
3532a3c8e9 Bundle translations completed by 75%, remove PO files in build [MAILPOET-916] 2017-05-23 18:34:00 +03:00
79cba4cace Release 3.0.0-beta.32 2017-05-23 13:27:19 +03:00
a5dee8da12 Merge pull request #897 from mailpoet/third_party_subscription_methods
Enables subscriber email to be passed when subscribing to list(s) [MAILPOET-809]
2017-05-22 14:25:00 +03:00
3783384ea6 Add a test to ensure subscribers can be identified by their email
address via MPAPI
2017-05-22 13:49:01 +03:00
766c0dfcfc Enables subscriber email to be passed when subscribing to list(s)
List subscription methods return array with subscriber data
2017-05-19 09:51:29 -04:00
83e9de8e95 Merge pull request #887 from mailpoet/third_party_subscription_methods
Adds API methods for third-party plugins [MAILPOET-809]
2017-05-17 14:03:19 +03:00
0a512f6349 Uses the first matching namespace endpoint 2017-05-16 23:17:25 -04:00
a4c1095db7 Moves custom field extraction logic from CustomField model to Subscriber
model where it's used
2017-05-16 20:58:44 -04:00
87a6c7100e Uses real object's ID 2017-05-16 20:58:43 -04:00
fc51d5f98c Sends confirmation email and schedules welcome notification by default
Fixes a typo in text string
2017-05-16 20:58:43 -04:00
a1b3aaf1f8 Adds method to create subscriber 2017-05-16 20:58:43 -04:00
3a1bf88c22 Extracts some logic into resuable methods 2017-05-16 20:58:43 -04:00
bd39c34f03 Adds unit tests 2017-05-16 20:56:56 -04:00
73121c2ca5 Adds method to return all segments minus WP Users segment(s) 2017-05-16 20:56:56 -04:00
5e23fa4295 Adds method to subscribe to single or multiple lists 2017-05-16 20:56:56 -04:00
5e34bbf9d5 Adds method to return subscriber fields 2017-05-16 20:56:56 -04:00
cedd94550f Adds unit test for API entry point 2017-05-16 20:56:55 -04:00
8b13889c7a Adds one entry point for both JSON and MP APIs
Removes endpoints folder and moves versions to the root
JSON API folder
2017-05-16 20:56:55 -04:00
3c7ac5488a Adds MP API facade 2017-05-16 20:56:54 -04:00
398d7d3d80 Moves current API under JSON namespace 2017-05-16 20:56:54 -04:00
b727ba423e Fix a typo in readme.txt 2017-05-16 18:08:58 +03:00
45b9550293 Thank people in changelog 2017-05-16 17:56:25 +03:00
d2e520e2fd Add new translations info to the readme.txt 2017-05-16 17:39:30 +03:00
b9c3ae97cd Bump up release version to 3.0.0-beta.31 2017-05-16 17:23:58 +03:00
b90c0b173b Merge pull request #892 from mailpoet/premium_key_warnings_fix
Temporarily hide invalid key warnings when the license key isn't specified [MAILPOET-911]
2017-05-16 12:56:26 +03:00
f498f4df0c Merge pull request #894 from mailpoet/progress_bar_style
Change sending progress bar style [MAILPOET-753]
2017-05-15 22:20:00 -04:00
2f10f89fc5 Change sending progress bar style [MAILPOET-753] 2017-05-15 21:25:40 +03:00
a49f9d9c80 Merge pull request #893 from mailpoet/newsletter_hash_generation_fix
Fixes newsletter link hash generation logic [MAILPOET-912]
2017-05-15 20:45:54 +03:00
e71e23bbb5 Temporarily hide invalid key warnings when the license key isn't specified [MAILPOET-911] 2017-05-15 20:16:29 +03:00
adc86ef247 Increases hash length and random string size 2017-05-15 13:07:18 -04:00
765b2bad21 Merge pull request #891 from mailpoet/form_list_select_fix
Fix non-text form fields not being sent to server [MAILPOET-909]
2017-05-15 09:15:44 -04:00
2354cac719 Updates unit test 2017-05-15 08:55:32 -04:00
7f509f66ff Changes newsletter link hash generation function 2017-05-15 08:36:43 -04:00
d8ff251c71 Fix WP user first/last name being cleared after subscription management form submit 2017-05-15 10:22:47 +03:00
12979cc2c0 Merge pull request #883 from mailpoet/alc_term_limit
Remove the result limit when searching for post terms in ALC and Posts [MAILPOET-902]
2017-05-14 22:47:16 -04:00
e974c06a89 Fixes subscription management form not saving data 2017-05-14 22:41:16 -04:00
f2ceff8252 Removes unused method 2017-05-14 22:02:24 -04:00
cd5f3165c7 Uses queue ID when fetching newsletter link by hash 2017-05-14 22:02:19 -04:00
6e700b0cfa Moves newsletter hash generating logic into Security helper class
Updates Links class to use Security helper's hash generating method
2017-05-14 20:15:40 -04:00
5b41fc212c Merge pull request #890 from mailpoet/helpscout
Force showing name and email fields in HelpScout Beacon [MAILPOET-908]
2017-05-13 12:52:13 +03:00
2b7a5452b8 Fix non-text form fields not being sent to server [MAILPOET-909] 2017-05-11 18:12:57 +03:00
cfed133fb6 Limit the number of terms returned to 50 and force ordering by name 2017-05-11 13:00:42 +03:00
0beff9a090 Bump up the limit when searching for post terms in ALC and Posts 2017-05-11 12:45:15 +03:00
d6e707fb85 Force showing name and email fields in HelpScout Beacon [MAILPOET-908] 2017-05-11 12:22:07 +03:00
a3e8d47199 Bump up release version to 3.0.0-beta.30 2017-05-09 16:57:29 -04:00
cab3f3a96e Merge pull request #884 from mailpoet/premium_key_check
Add Premium key validation [PREMIUM-4]
2017-05-09 16:30:43 +03:00
5f0d4abe7f Temporarily hide Premium tab in settings [PREMIUM-4] 2017-05-09 16:01:42 +03:00
ff5f87eeca Rename processQueueLogic() method to processQueueStrategy() [PREMIUM-4] 2017-05-09 15:42:37 +03:00
e85b969e11 Rename initApi() to init() in workers [PREMIUM-4] 2017-05-09 09:12:20 +03:00
2eb98905b6 Encapsulate date formatting within the DateTime class [PREMIUM-4] 2017-05-09 08:54:12 +03:00
ac1274c6fd Merge pull request #889 from mailpoet/tinymce_lists_fix
Bring back list buttons to the TinyMCE toolbar [MAILPOET-906]
2017-05-08 14:14:52 -04:00
94f91afce1 Bring back list buttons to TinyMCE toolbar [MAILPOET-906] 2017-05-08 21:01:18 +03:00
73d5fb8cff Merge pull request #886 from mailpoet/strings_fix
Fixes form editor notification not displaying added/updated/removed custom field [MAILPOET-905]
2017-05-08 13:24:59 +03:00
90b2b46db4 Make key check method names consistent [PREMIUM-4] 2017-05-08 13:16:05 +03:00
f2bf61240a Extract a state building method from key check results processing [PREMIUM-4] 2017-05-08 08:01:51 +03:00
3f151fd235 Extract simple workers common code into a base class [PREMIUM-4] 2017-05-08 07:38:56 +03:00
7598363cae Fixes notification message not displaying dynamic value due to JS
encoding
2017-05-05 18:38:49 -04:00
4b1f216cd3 Use WP's date format instead of a hard-coded one [PREMIUM-4] 2017-05-05 18:57:15 +03:00
3d5f13a2b8 Fix code style [PREMIUM-4] 2017-05-05 18:41:19 +03:00
98eab956e9 Rename checkAPIKey to checkMSSKey (MailPoet Sending Service) [PREMIUM-4] 2017-05-05 18:12:48 +03:00
a7260cba3d Make the Premium key check stricter, split a unit test into more granular ones [PREMIUM-4] 2017-05-05 18:09:00 +03:00
787e022382 Rename license key constants and vars, optimize error generation [PREMIUM-4] 2017-05-05 18:04:52 +03:00
d8e1c76155 Remove a leftover hook from Free after the key field removal from Premium [PREMIUM-4] 2017-05-05 17:18:56 +03:00
3cb08e3c09 Rename MSS check methods to better distinguish them from Premium ones [PREMIUM-4] 2017-05-04 09:36:38 +03:00
0474985866 Add unit tests [PREMIUM-4] 2017-05-04 09:25:34 +03:00
8d15ef6d06 Refine license key check UI [PREMIUM-4] 2017-05-04 09:15:21 +03:00
0fbc7fb7eb Add Premium key validation [PREMIUM-4] 2017-05-03 12:20:13 +03:00
1379bdbbeb Bump up release version to 3.0.0-beta.29 2017-05-02 18:10:20 +03:00
64d3e659a4 Merge pull request #879 from mailpoet/newsletter_model_update
Newsletter model update [MAILPOET-830]
2017-05-02 16:19:20 +03:00
19458546a0 Updates unit tests 2017-05-02 08:41:51 -04:00
bba7460423 Merge pull request #882 from mailpoet/conflict_resolver_update
Resolves script conflicts in WP's admin footer [MAILPOET-901]
2017-05-02 13:37:20 +03:00
956fdd5cff Improve a deletion test to handle multiple queues, fix comments [MAILPOET-830] 2017-05-02 09:07:38 +03:00
a0289775cb Trashes/restores multiple associated queues when newsletter is
trashed/restored
2017-05-01 20:15:41 -04:00
4c785902bc Resolves script conflicts in WP's admin footer 2017-05-01 11:57:48 -04:00
e29ae4d7c9 Fixes indentation 2017-05-01 09:31:52 -04:00
1ea915017a Fixes unit test 2017-05-01 09:26:36 -04:00
6441c781a5 Moves relations to the top of the model and delete/save/restore/trash methods close to each other for easy navigation 2017-05-01 09:26:35 -04:00
589c54e205 Checks if associated queue exists before trashing/deleting/restoring it 2017-05-01 09:26:35 -04:00
e10b99eaac Deletes all sending queue and segment associations when newsletters are bulk deleted 2017-05-01 09:26:35 -04:00
0316f3ea3e Restores all sending queue associations when newsletters are bulk restored 2017-05-01 09:26:35 -04:00
166fef899f Trashes all sending queue associations when newsletters are bulk trashed 2017-05-01 09:26:35 -04:00
4e850408fc Restores sending queue association when newsletter is restored 2017-05-01 09:26:35 -04:00
6e2494831c Trashes sending queue association when newsletter is trashed 2017-05-01 09:26:35 -04:00
38a7d8f80a Deletes queue and segment associations when deleting newsletter 2017-05-01 09:26:34 -04:00
abfebc8643 Merge pull request #880 from mailpoet/subscriber_shortcode_fix
Returns shortcode's default value when subscriber's first/last name is blank [MAILPOET-899]
2017-05-01 11:28:54 +03:00
40a3487d3d Remove a redundant condition, fix a typo in a test name [MAILPOET-880] 2017-05-01 11:20:33 +03:00
a93865e594 Merge pull request #874 from mailpoet/trashed_segments_fix
Exclude trashed segments from subscriber listing filter and 'not in list' count [MAILPOET-893]
2017-04-30 20:09:23 -04:00
4e76286b44 Returns shortcode's default value when subscriber's first or last name is empty 2017-04-28 09:49:00 -04:00
fbe57e96c6 Merge pull request #878 from mailpoet/contribution_rules
Update rules/guidelines for contributing to this codebase [MAILPOET-898]
2017-04-27 11:22:10 -04:00
950bfb04d6 Update README.md with more new commands 2017-04-27 17:56:36 +03:00
6d43b7b6a9 Update rules/guidelines for contributing to this codebase 2017-04-27 17:51:50 +03:00
e1991deafd Merge pull request #876 from mailpoet/manage_subscription_shortcode_fix
Depreciates and removes certain link shortcodes [MAILPOET-895]
2017-04-27 15:51:26 +03:00
2f1b31aeb2 Adds missing anchor closing tag 2017-04-27 08:41:05 -04:00
ca29eefd7f Merge pull request #877 from mailpoet/form_placement_update
Removes HTML method from form placement [MAILPOET-897]
2017-04-27 15:40:55 +03:00
1421407a23 Merge pull request #875 from mailpoet/smtp_hooks
Adds filter to SMTP transport agent [MAILPOET-889]
2017-04-27 14:43:40 +03:00
36e4bf468d Merge pull request #871 from mailpoet/editor_marionette
Upgrade Marionette version in newsletter editor [MAILPOET-892]
2017-04-27 11:19:46 +03:00
5cd3917f4d Removes HTML method from form placement 2017-04-26 19:38:19 -04:00
586470e8f9 Updates unit tests 2017-04-26 17:58:45 -04:00
b02e9f5ab3 Depreciates and removes server-side rendering of subscription_unsubscribe,
subscription_manage and newsletter_view_in_browser
2017-04-26 17:47:52 -04:00
4a538e677d Adds filter to SMTP transport agent 2017-04-26 09:55:57 -04:00
cc2fdbe5be Merge pull request #864 from mailpoet/campaign_stats
Add detailed stats page support in Free, change stats style [PREMIUM-1] [MAILPOET-877]
2017-04-26 14:30:51 +03:00
3833688115 Exclude trashed segments from subscriber listing filter and 'not in list' count [MAILPOET-893] 2017-04-26 11:23:16 +03:00
1639741e55 Bump up release version to 3.0.0-beta.28 2017-04-25 19:00:18 +03:00
ab0d573a66 Merge pull request #873 from mailpoet/import_results_fix
Keeps track of the number of updated/created subscribers over multiple server requests [MAILPOET-894]
2017-04-25 18:26:38 +03:00
26c582b19f Keeps track of the number of updated/created subscribers over multiple
server requests
2017-04-25 11:00:52 -04:00
3bc53f9f09 Remove Premium-only styles from Free, cleanup styles [PREMIUM-1] 2017-04-25 12:46:05 +03:00
bb220baf6a Add names to constants, rename vars for clarity [MAILPOET-877] 2017-04-25 12:46:04 +03:00
121a78f42a Update an open rate improvement KB link URL [MAILPOET-877] 2017-04-25 12:46:04 +03:00
4257aa634e Don't show green box and KB link in stats for welcome emails [MAILPOET-877] 2017-04-25 12:46:03 +03:00
95ff83557f Add a green box in stats for recently sent newsletters, add help KB link [MAILPOET-877] 2017-04-25 12:46:03 +03:00
e9070de9c4 Add badges to stats in a newsletter listing, change stats style [PREMIUM-1] [MAILPOET-877] 2017-04-25 12:45:52 +03:00
72aa087411 Localize formatting to 1 decimal [PREMIUM-1] 2017-04-25 12:45:50 +03:00
fbc0a3ad8d Add detailed stats page support in Free [PREMIUM-1] 2017-04-25 12:45:49 +03:00
afedc409f5 Merge pull request #872 from mailpoet/newsletter_id_number_format_fix
Adds intval filter and fixes issue with number format applied on IDs >= 1000 [MAILPOET-891]
2017-04-25 12:27:17 +03:00
0360f16dc8 Merge pull request #865 from mailpoet/api_versioning
Adds versioning to our public API [MAILPOET-881]
2017-04-25 12:07:17 +03:00
f4800dbbae Removes namescape format enforcement 2017-04-24 20:52:01 -04:00
15ddc8454e Adds intval filter and fixes issue with number format applied on IDs
>=1000
2017-04-24 19:25:17 -04:00
f8df4de711 Merge pull request #868 from mailpoet/bounce_doc_link
Add a link to the bounce article in advanced settings [MAILPOET-868]
2017-04-24 19:11:03 -04:00
a0cb18e1a1 Merge pull request #869 from mailpoet/html_notices_fix
Wrap notices containing HTML in a paragraph, upgrade notice classes [MAILPOET-733]
2017-04-24 16:09:50 -04:00
509ec7d3d3 Fix posts block settings to not use deprecated CompositeView 2017-04-24 18:00:23 +03:00
aa2416f353 Simplify settings views to use methods defined in base settings view 2017-04-24 18:00:23 +03:00
167a605658 Switch Container block view to use methods defined by base view 2017-04-24 18:00:23 +03:00
592f11bd5f Fix App activation calls, fix block insertion animations 2017-04-24 18:00:23 +03:00
92b128039a Fix displaying placeholder message on empty containers 2017-04-24 18:00:23 +03:00
5efe611b2d Remove obsolete comments, fix ALC settings 2017-04-24 18:00:23 +03:00
477e2737b1 Fix sorting of social icons in settings 2017-04-24 18:00:23 +03:00
dc8bacc27d Fix drag&drop 2017-04-24 18:00:23 +03:00
0b8c787cda Partially migrate newsletter editor to Marionette 3.x from 2.x 2017-04-24 18:00:23 +03:00
4f5c464659 Merge pull request #866 from mailpoet/list_unsubsribe_url
List unsubsribe url [MAILPOET-797]
2017-04-24 14:58:06 +03:00
4f432645b1 Merge pull request #870 from mailpoet/welcome_page_update
Update the Welcome page [MAILPOET-884]
2017-04-24 13:23:39 +03:00
5fa7930896 Redefines how endpoint namespaces are set
Updates error response to terminate connection only on AJAX requests
Optimizes and cleans up code based on code revew comments
2017-04-20 22:34:18 -04:00
f9efd536d9 Update the Welcome page [MAILPOET-884] 2017-04-20 21:00:09 +03:00
6a65ff5e5d Removes default version
Updates all AJAX requests to include api version
Requires namespaces to have version
Clean up code
2017-04-19 23:34:40 -04:00
b549f83422 Updates form subscription class to use the main API class instead of calling directly API endpoint
Modifies forms to pass api_version
Modifies forms to pass store form-specific values (e.g., form_id, email) inside a separate data array
2017-04-19 15:38:16 -04:00
a9c80c031f Adds version support to public API 2017-04-19 15:38:16 -04:00
405bea3049 Upgrade notice classes from deprecated ones [MAILPOET-733] 2017-04-19 17:11:42 +03:00
6954acd0b3 Wrap notice messages containing HTML in a paragraph to avoid broken styles [MAILPOET-733] 2017-04-19 17:11:14 +03:00
efd15d5d18 Add a link to the bounce article in advanced settings [MAILPOET-868] 2017-04-19 10:34:30 +03:00
6566622167 Bumps up release version to 3.0.0-beta.27
Updates changelog
Updates readme.txt based changes proposed by Kim
Cleans up language in readme.txt
2017-04-18 20:05:24 -04:00
8157780b68 removing uneeded code and moving the url generation to proper class 2017-04-18 21:12:41 +02:00
975546915e Merge pull request #867 from mailpoet/requirements_checker_update
Adds requirement check for ZIP and XML PHP extensions [MAILPOET-874]
2017-04-18 10:46:42 +03:00
319d591662 Merge pull request #863 from mailpoet/editor_tinymce
TinyMCE fixes [MAILPOET-880] [MAILPOET-829] [MAILPOET-862]
2017-04-17 09:13:35 +03:00
1dd6c91529 Updates missing requirements language 2017-04-16 23:54:10 -04:00
c4f0426775 Adds checker for XML and ZIP extensions 2017-04-16 21:10:15 -04:00
53f5a122bd Merge pull request #861 from mailpoet/post_filters
Fix post filters for custom post types in ALC [MAILPOET-838]
2017-04-16 19:52:01 -04:00
a7142ed21b modified SendingQueue Tests to ensure it passes the correct unsubscribe URL to Mailer 2017-04-15 22:14:40 +00:00
771a1bfc44 Adding List-Unsubscribe to header of newsletters 2017-04-15 21:21:28 +00:00
53169bba78 Merge pull request #860 from mailpoet/import_fixes
Fixes SQL errors & next button not working on step 2 of import [MAILPOET-876] [MAILPOET-766] [MAILPOET-879] [MAILPOET-828]
2017-04-14 09:03:59 +03:00
e3b8c1836b Adds additional new and existing subscribers to the test method to
ensure that data between new subscribers does not mix
2017-04-13 15:57:04 -04:00
a4b091dc32 Extends test condition to check for all new subscriber column data 2017-04-13 10:50:36 -04:00
448c9ddaa8 Fixes custom column names not being automatically matched on step 2 of
import
2017-04-13 10:10:00 -04:00
ac574acf8e Merge branch 'import_fixes' of mailpoet:mailpoet/mailpoet into import_fixes 2017-04-13 09:41:46 -04:00
aa15b9420a Replaces redundant search with one-time lookup 2017-04-13 09:28:54 -04:00
2b7f5c321e Merge pull request #862 from mailpoet/remote_images
Fixes Image block to update image dimensions when image src changes [MAILPOET-762]
2017-04-13 13:51:47 +03:00
bee9bfcfcc Fix data being mixed up when splitting subscribers, remove excessive arguments [MAILPOET-828] 2017-04-13 11:08:20 +03:00
b7d73dcfaa Updates unit test 2017-04-12 09:49:42 -04:00
5b4fa4ea2b Fixes custom fields not being updated or causing integrity constraint
error: https://mailpoet.atlassian.net/browse/MAILPOET-828
2017-04-12 09:40:15 -04:00
12e5fe77de Perform caret positioning only on TinyMCE activation click 2017-04-12 14:37:47 +03:00
2dca10c539 Fix cursor positioning when activating TinyMCE on click [MAILPOET-880] 2017-04-12 14:07:19 +03:00
ceba5b3d0b Fix pasting from text editors and word processors [MAILPOET-829] 2017-04-12 13:17:45 +03:00
c05cf3cad4 Update TinyMCE for fixed triple-click behavior [MAILPOET-862] 2017-04-12 13:14:27 +03:00
d6f5a39829 Simplifies subscriber splitting code and adds comments 2017-04-11 22:12:50 -04:00
30d67508cb Fixes Image block to update image dimensions when image src changes 2017-04-11 19:38:57 +03:00
63b8d892f7 Update changelog entries to use asterisks for list items instead of
dashes
2017-04-11 16:05:53 +03:00
10137d8551 Bump up release version to 3.0.0-beta.26 2017-04-11 15:46:32 +03:00
9ef74e0951 Stops execution when there are no subscriber columns to update 2017-04-10 21:41:37 -04:00
89ff93958f Removes subscriber object modification logic from the splitSubscribersData() method
Uses 2 separate objects with its own data for existing and new subscribers
Extends only new subscribers' object when it is missing required fields
2017-04-10 21:41:21 -04:00
8d870e85eb Switch to get_bloginfo() from bloginfo() to prevent output 2017-04-10 19:44:32 +03:00
0cdb426712 Fix ALC filtering for custom taxonomies and post types 2017-04-10 19:23:19 +03:00
b9f7a5673f Removes lefover test code 2017-04-10 11:32:38 -04:00
7ffbf6c378 Updates code style and adds wp_user_id column to the list of columns
that should be ignore when updating existing subscribers
2017-04-09 22:05:02 -04:00
3a9c006cf9 Prevents overwriting existing subscribers' status (and other required fields) unless
the import object contains data for those fields
2017-04-09 22:04:56 -04:00
a9edb383b4 Fixes next button not appearing when list is first unselected and then
selected back
2017-04-09 21:49:24 -04:00
ec23a73edb Merge pull request #859 from mailpoet/trailing_br_rendering_fix
Fix last <br/> removal cutting off text when rendering a text block [MAILPOET-856]
2017-04-06 21:57:07 -04:00
10a164ee0c Merge pull request #858 from mailpoet/customizer_fix
Rename a 'method' field in a form widget so it doesn't break the WP interactive customizer [MAILPOET-851]
2017-04-06 21:55:24 -04:00
37fcd5699b Fix last <br/> removal cutting off text when rendering a text block [MAILPOET-856] 2017-04-06 10:07:11 +03:00
66d969cc2f Merge pull request #857 from mailpoet/settings-css-update
Removes sending method's heading line-height [MAILPOET-873]
2017-04-05 18:51:09 +03:00
9d358f74dd Rename a 'method' field in a form widget so it doesn't break the WP interactive customizer [MAILPOET-851] 2017-04-05 18:35:13 +03:00
57e00e3097 Removes sending method's heading line-height 2017-04-05 10:45:31 -04:00
53afbea6ec Bump up release version to 3.0.0-beta.25 2017-04-04 18:22:44 +03:00
2c2c0b3db4 Merge pull request #856 from mailpoet/sending_limit_enforcement_fix
Fixes sending limit not being enforced [MAILPOET-872]
2017-04-04 17:07:51 +03:00
e235ee66eb Adds regression unit test 2017-04-04 09:59:06 -04:00
0ef430567b Fixes sending limit not being enforced when email frequency limit is
changed to a lesser value OR when it is changed while sending is in
progress
2017-04-04 09:43:27 -04:00
74aef73f75 Merge pull request #855 from mailpoet/php53-fix
Fixes reference to self in anonymous function [MAILPOET-871]
2017-03-31 21:31:20 +03:00
99eb72428f Fixes reference to self in anonymous function 2017-03-31 12:51:58 -04:00
065b160155 Merge pull request #854 from mailpoet/subscriber_listing_performance
Improve performance of a subscriber listing on MySQL 5.5 and lower [MAILPOET-867]
2017-03-30 09:51:47 -04:00
6811d8e38d Improve performance of a subscriber listing on MySQL 5.5 and lower [MAILPOET-867] 2017-03-30 13:12:53 +03:00
5f75efddf1 Updates changelog and bumps version to 3.0.0-beta.24 2017-03-28 14:19:11 -04:00
822a7ac5f5 Merge pull request #852 from mailpoet/translation_string_escaping_fix
Escapes quotation marks in translation results [MAILPOET-864]
2017-03-28 19:28:57 +03:00
06e1ac9bb5 Escapes translations for output in HTML attributes 2017-03-28 12:02:29 -04:00
a3530c3367 Escapes translations for JS output 2017-03-28 11:17:00 -04:00
ec35bfb2d4 Reverts back previous code 2017-03-28 11:16:09 -04:00
ed3e46bebb Merge pull request #853 from mailpoet/tracking_code_update
Makes tracking image transparent and fixes CSS rule parsing logic [MAILPOET-827]
2017-03-28 11:36:54 +03:00
87b270482b Fixes rules with colons (e.g, background-image: url(http://....);) from
being incorrectly parsed
2017-03-26 18:45:07 -04:00
d22ba55858 Outputs transparent gif instead of red color 2017-03-26 18:45:07 -04:00
835f25cc82 Fixes unit test that fails on the last Saturday of the month 2017-03-25 02:24:38 -04:00
11944283b0 Escapes quotation marks in translation results 2017-03-24 20:38:04 -04:00
dc704a92de Merge pull request #851 from mailpoet/import_language_update
Updates example import paste data (textbox hint) [MAILPOET-863]
2017-03-24 18:01:12 +02:00
dca1e9e1a7 Merge pull request #850 from mailpoet/manage_subscription_descr
Add details to the Manage Subscription description [MAILPOET-853]
2017-03-24 17:56:27 +02:00
00781be077 Updates example import paste data 2017-03-23 13:27:48 -04:00
ac80148f5b Merge pull request #844 from mailpoet/ga_tracking
JS hooks & GA tracking support [PREMIUM-2]
2017-03-23 14:58:58 +02:00
ff36833270 Add a hook for reinstalling Premium [PREMIUM-2] 2017-03-23 15:27:15 +03:00
612c7d76a0 Add details to the Manage Subscription description [MAILPOET-853] 2017-03-22 13:19:26 +03:00
32097b4512 Test that Premium hooks are executed [PREMIUM-2] 2017-03-21 21:36:36 +03:00
d686f75222 Swap JS actions with filters for robustness & testability, get rid of URL key hashing [PREMIUM-2] 2017-03-21 13:34:47 +03:00
bcc01df0b8 Merge pull request #849 from mailpoet/transifex_upload
Upload translation files to Transifex via publish command [MAILPOET-855]
2017-03-20 19:40:19 +02:00
ee12f4d304 Move a hook to be always executed after rebasing [PREMIUM-2] 2017-03-20 16:23:16 +03:00
16c1607850 Refactor links processing: isolate core logic for easier substitution [PREMIUM-2] 2017-03-20 12:09:58 +03:00
e2864e2243 Add hooks for GA tracking feature [PREMIUM-2] 2017-03-20 12:09:58 +03:00
16dc81150d Execute mailpoet_initialized hook earlier after setup [PREMIUM-2]
This is done because Router can seize the request and prevent subsequent actions like Premium hooking from being run, so hooks didn't work in cron daemon.
2017-03-20 12:08:40 +03:00
ed4d3d52ed Add hooks for Premium translations and scripts [PREMIUM-2] 2017-03-20 12:08:40 +03:00
37a6a74b6e Extract React libraries to a separate chunk and expose them globally [PREMIUM-2] 2017-03-20 12:08:40 +03:00
136a531047 Rename newsletters 3rd step hook [PREMIUM-2] 2017-03-20 12:08:40 +03:00
bef0097f5b Add front-end WP-style hooks support for Premium [PREMIUM-2] 2017-03-20 12:08:39 +03:00
a0d2be50e8 Upload translation files to Transifex via publish command [MAILPOET-855] 2017-03-16 15:11:38 +03:00
287 changed files with 9618 additions and 2848 deletions

43
.gitignore vendored
View File

@ -1,21 +1,22 @@
.DS_Store
TODO
composer.phar
/vendor
tests/_output/*
tests/acceptance.suite.yml
tests/_support/_generated/*
node_modules
.env
npm-debug.log
!tasks/**
/views/cache/**
temp
.idea
mailpoet.zip
tests/javascript/testBundles
assets/css/*.css
assets/js/*.js
.vagrant
lang
.mp_svn
.DS_Store
TODO
composer.phar
/vendor
tests/_output/*
tests/acceptance.suite.yml
tests/_support/_generated/*
node_modules
.env
npm-debug.log
!tasks/**
/views/cache/**
temp
.idea
mailpoet.zip
tests/javascript/testBundles
assets/css/*.css
assets/js/*.js
.vagrant
lang
.mp_svn
/nbproject/

View File

@ -6,4 +6,4 @@ source_file = lang/mailpoet.pot
file_filter = lang/mailpoet-<lang>.po
source_lang = en_US
type = PO
minimum_perc = 100
minimum_perc = 75

View File

@ -5,30 +5,32 @@
- CamelCase for classes.
- camelCase for methods.
- snake_case for variables and class properties.
- Max line length at 80 chars.
- Classes can be no longer than 100 LOC.
- Methods can be no longer than 5 LOC.
- Pass no more than 4 parameters/hash keys into a method.
- Composition over Inheritance.
- Comments are a code smell.
- Routes can instantiate only one object.
- Comments are a code smell. If you need to use a comment - see if same idea can be achieved by more clearly expressing code.
- Require other classes with 'use' at the beginning of the class file.
- Do not specify 'public' if method is public, it's implicit.
- Always use guard clauses.
- Ensure compatibility with PHP 5.3 and newer versions.
- Cover your code in tests.
Recommendations:
- Max line length at 80 chars.
- Keep classes under 100 LOC.
- Keep methods under 10 LOC.
- Pass no more than 4 parameters/hash keys into a method.
- Keep Pull Requests small, under 100 LOC changed.
## Git flow.
- Do not commit to master.
- Open a short-living feature branch.
- Open a pull request.
- Add close #issue in pull request description.
- Add Jira issue reference in the title of the Pull Request.
- Work on the pull request.
- Wait for confirmation before merging to master.
- No one will accept a pull request that doesn't have 100% test coverage.
- Wait for review and confirmation from another developer before merging to master.
- Commit title no more than 80 chars, empty line after.
- Commit description as long as you want, 80 chars wrap.
- Keep the GitHub open issues count at less than 10.
## Issues creation.
- Issues are managed on Jira.
- Discuss issues on public Slack chats, discuss code in pull requests.
- Organize features on Trello.
- Open a small github issue only when it has been discussed.
- Open a small Jira issue only when it has been discussed.

View File

@ -46,11 +46,21 @@ $ ./do compile:all
$ ./do test:unit
```
- JS tests (using Mocha):
```sh
$ ./do test:javascript
```
- Debug tests:
```sh
$ ./do test:debug
```
- Code linters and quality checkers:
```sh
$ ./do qa
```
# CSS
- [Stylus](https://learnboost.github.io/stylus/)
- [Nib extension](http://tj.github.io/nib/)
@ -109,6 +119,7 @@ Once javascript is compiled with `./do compile:javascript`, your module will be
```php
__()
_n()
_x()
```
```html
@ -144,17 +155,23 @@ Finally , a `WP_TRANSIFEX_API_TOKEN` environment variable should be initialized
# Publish
Before you run a publishing command, you need to:
The `publish` command currently does the following:
* Pushes translations POT file to Transifex;
* Publishes the release in SVN.
Before you run it, you need to:
1. Ensure there is an up-to-date local copy of MailPoet SVN repository in `.mp_svn` directory by running `./do svn:checkout`.
2. Have all your features merged in Git `master`, your `mailpoet.php` and `readme.txt` tagged with a new version.
3. Run `./build.sh` to produce a `mailpoet.zip` distributable archive.
Everything's ready? Then run `./do svn:publish`.
Everything's ready? Then run `./do publish`.
If the job goes fine, you'll get a message like this:
```
Go to '.mp_svn' and run 'svn ci -m "Release 3.0.0-beta.9"' to publish the
release
Run 'svn copy ...' to tag the release
```
It's quite literal: you can review the changes to be pushed and if you're satisfied, run the suggested command to finish the release publishing process.
If you're confident, execute `./do svn:publish --force` and your release will be published to the remote SVN repository without manual intervention (automatically). For easier authentication you might want to set `WP_SVN_USERNAME` and `WP_SVN_PASSWORD` environment variables.
If you're confident, execute `./do publish --force` and your release will be published to the remote SVN repository without manual intervention (automatically). For easier authentication you might want to set `WP_SVN_USERNAME` and `WP_SVN_PASSWORD` environment variables.

View File

@ -104,10 +104,24 @@ class RoboFile extends \Robo\Tasks {
);
}
function pushpot() {
return $this->collectionBuilder()
->addCode(array($this, 'txinit'))
->taskExec('tx push -s')
->run();
}
function packtranslations() {
return $this->collectionBuilder()
->addCode(array($this, 'txinit'))
->taskExec('./tasks/pack_translations.sh')
->run();
}
function txinit() {
// Define WP_TRANSIFEX_API_TOKEN env. variable
$this->loadEnv();
return $this->_exec('./tasks/pack_translations.sh');
return $this->_exec('./tasks/transifex_init.sh');
}
function testUnit($opts=['file' => null, 'xml' => false]) {
@ -199,7 +213,26 @@ class RoboFile extends \Robo\Tasks {
}
function svnCheckout() {
return $this->_exec('svn co https://plugins.svn.wordpress.org/mailpoet/ .mp_svn');
$svn_dir = ".mp_svn";
$collection = $this->collectionBuilder();
// Clean up the SVN dir for faster shallow checkout
if(file_exists($svn_dir)) {
$collection->taskExecStack()
->exec('rm -rf ' . $svn_dir);
}
$collection->taskFileSystemStack()
->mkdir($svn_dir);
return $collection->taskExecStack()
->stopOnFail()
->dir($svn_dir)
->exec('svn co https://plugins.svn.wordpress.org/mailpoet/ -N .')
->exec('svn up trunk')
->exec('svn up assets')
->run();
}
function svnPublish($opts = ['force' => false]) {
@ -279,15 +312,15 @@ class RoboFile extends \Robo\Tasks {
// Remove files from SVN repo that have already been removed locally
->exec("svn st | grep ^! | awk '$awkCmd' | xargs $xargsFlag svn rm")
// Recursively add files to SVN that haven't been added yet
->exec("svn add --force * --auto-props --parents --depth infinity -q")
// Tag the release
->exec("svn cp trunk tags/$plugin_version");
->exec("svn add --force * --auto-props --parents --depth infinity -q");
$result = $collection->run();
if($result->wasSuccessful()) {
// Run or suggest release command depending on a flag
$repo_url = "https://plugins.svn.wordpress.org/$plugin_dist_name";
$release_cmd = "svn ci -m \"Release $plugin_version\"";
$tag_cmd = "svn copy $repo_url/trunk $repo_url/tags/$plugin_version -m \"Tag $plugin_version\"";
if(!empty($opts['force'])) {
$svn_login = getenv('WP_SVN_USERNAME');
$svn_password = getenv('WP_SVN_PASSWORD');
@ -300,17 +333,30 @@ class RoboFile extends \Robo\Tasks {
->stopOnFail()
->dir($svn_dir)
->exec($release_cmd)
->exec($tag_cmd)
->run();
} else {
$this->yell(
"Go to '$svn_dir' and run '$release_cmd' to publish the release"
);
$this->yell(
"Run '$tag_cmd' to tag the release"
);
}
}
return $result;
}
public function publish($opts = ['force' => false]) {
return $this->collectionBuilder()
->addCode(array($this, 'pushpot'))
->addCode(function () use ($opts) {
return $this->svnPublish($opts);
})
->run();
}
protected function loadEnv() {
$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();

View File

@ -22,3 +22,5 @@
@require 'progress_bar'
@require 'subscribers'
@require 'mp2migrator'

View File

@ -1,3 +1,53 @@
$excellent-badge-color = #2993ab
$good-badge-color = #f0b849
$bad-badge-color = #d54e21
$green-badge-color = #55bd56
#newsletters_container
h2.nav-tab-wrapper
margin-bottom: 1rem
margin-bottom: 1rem
.mailpoet_stats_text
font-size: 14px
font-weight: 600;
.mailpoet_stat
&_excellent
color: $excellent-badge-color
&_good
color: $good-badge-color
&_bad
color: $bad-badge-color
&_hidden
display: none
&_link_small
text-decoration: underline !important
font-size: 0.75rem
.mailpoet_badge
padding: 4px 6px 3px 6px
color: #FFFFFF
margin-right: 4px
text-transform: uppercase
font-size: 0.5625rem
font-weight: 500
border-radius: 3px
letter-spacing: 1px
vertical-align: middle
&_excellent
background: $excellent-badge-color
&_good
background: $good-badge-color
&_bad
background: $bad-badge-color
&_green
background: $green-badge-color

View File

@ -0,0 +1,33 @@
#logger
width: 100%
height: 300px
background-color: transparent
border: 0
border-top: 1px #aba9a9 solid
padding: 2px
overflow: scroll
resize: both
font-size: 0.85em
margin-top: 20px
#progressbar
width: 50%
background-color: #d8d8d8
border-radius: 5px
progressbar_color = #fecf23
progressbar_gradient_to_color = #fd9215
.ui-progressbar .ui-progressbar-value
height: 100%
background-color: progressbar_color
background-image: linear-gradient(to bottom, progressbar_color, progressbar_gradient_to_color)
border-radius: 3px
box-shadow: 0 1px 0 rgba(255,255,255,0.5) inset
border 0
.mailpoet_progress_label
font-size: 15px
.error_msg
color: #f00

View File

@ -4,6 +4,7 @@
padding: 0
width: 100%
margin: 0
margin-bottom: 10px
border-radius: 5px
position: relative
@ -25,5 +26,5 @@
.mailpoet_progress_complete
.mailpoet_progress_bar
background-color: #fecf23
background-image: linear-gradient(top, #fecf23, #fd9215)
background-color: hsla(191, 78%, 80%, 1)
background-image: linear-gradient(top, hsla(191, 78%, 80%, 1), hsla(191, 76%, 67%, 1))

View File

@ -21,7 +21,6 @@
h3
text-align center
height 54px
line-height 54px
font-size 1.5em
.mailpoet_description
font-size 14px
@ -39,6 +38,7 @@
font-weight bold
.mailpoet_active
.mailpoet_status
background-color #088b00
span
visibility visible
#mailpoet_mta_activate
@ -52,6 +52,15 @@
.button-secondary
margin 0 -6px -4px 0
// premium key
.mailpoet_key
&_valid
&::before
content ' '
&_invalid
&::before
content: ' '
// responsive
@media screen and (max-width: 782px)
.form-table th

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -5,6 +5,7 @@ define('ajax', ['mailpoet', 'jquery', 'underscore'], function(MailPoet, jQuery,
options: {},
defaults: {
url: null,
api_version: null,
endpoint: null,
action: null,
token: null,
@ -30,6 +31,7 @@ define('ajax', ['mailpoet', 'jquery', 'underscore'], function(MailPoet, jQuery,
getParams: function() {
return {
action: 'mailpoet',
api_version: this.options.api_version,
token: this.options.token,
endpoint: this.options.endpoint,
method: this.options.action,

View File

@ -64,6 +64,7 @@ define(
this.setState({ loading: true });
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint,
action: 'get',
data: {
@ -112,6 +113,7 @@ define(
}
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint,
action: 'save',
data: item

View File

@ -97,6 +97,7 @@ const item_actions = [
label: MailPoet.I18n.t('duplicate'),
onClick: function(item, refresh) {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'forms',
action: 'duplicate',
data: {
@ -125,6 +126,7 @@ const item_actions = [
const FormList = React.createClass({
createForm() {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'forms',
action: 'create'
}).done((response) => {

View File

@ -198,6 +198,17 @@ const ListingItem = React.createClass({
const ListingItems = React.createClass({
render: function() {
if (this.props.items.length === 0) {
let message;
if (this.props.loading === true) {
message = (this.props.messages.onLoadingItems
&& this.props.messages.onLoadingItems(this.props.group))
|| MailPoet.I18n.t('loadingItems');
} else {
message = (this.props.messages.onNoItemsFound
&& this.props.messages.onNoItemsFound(this.props.group))
|| MailPoet.I18n.t('noItemsFound');
}
return (
<tbody>
<tr className="no-items">
@ -207,11 +218,7 @@ const ListingItems = React.createClass({
+ (this.props.is_selectable ? 1 : 0)
}
className="colspanchange">
{
(this.props.loading === true)
? MailPoet.I18n.t('loadingItems')
: MailPoet.I18n.t('noItemsFound')
}
{message}
</td>
</tr>
</tbody>
@ -445,6 +452,7 @@ const Listing = React.createClass({
this.clearSelection();
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint,
action: 'listing',
data: {
@ -495,6 +503,7 @@ const Listing = React.createClass({
});
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint,
action: 'restore',
data: {
@ -522,6 +531,7 @@ const Listing = React.createClass({
});
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint,
action: 'trash',
data: {
@ -549,6 +559,7 @@ const Listing = React.createClass({
});
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint,
action: 'delete',
data: {
@ -611,6 +622,7 @@ const Listing = React.createClass({
}
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: this.props.endpoint,
action: 'bulkAction',
data: data
@ -788,6 +800,12 @@ const Listing = React.createClass({
groups = false;
}
// messages
let messages = {};
if (this.props.messages !== undefined) {
messages = this.props.messages;
}
return (
<div>
{ groups }
@ -841,6 +859,7 @@ const Listing = React.createClass({
count={ this.state.count }
limit={ this.state.limit }
item_actions={ item_actions }
messages={ messages }
items={ items } />
<tfoot>

View File

@ -0,0 +1,187 @@
define('mp2migrator', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
'use strict';
MailPoet.MP2Migrator = {
fatal_error: '',
is_logging: false,
startLogger: function () {
MailPoet.MP2Migrator.is_logging = true;
clearTimeout(MailPoet.MP2Migrator.displayLogs_timeout);
clearTimeout(MailPoet.MP2Migrator.updateProgressbar_timeout);
clearTimeout(MailPoet.MP2Migrator.update_wordpress_info_timeout);
setTimeout(MailPoet.MP2Migrator.updateDisplay, 1000)
},
stopLogger: function () {
MailPoet.MP2Migrator.is_logging = false;
},
updateDisplay: function () {
MailPoet.MP2Migrator.displayLogs();
MailPoet.MP2Migrator.updateProgressbar();
},
displayLogs: function () {
jQuery.ajax({
url: mailpoet_mp2_migrator.log_file_url,
cache: false
}).done(function (result) {
jQuery("#logger").html('');
result.split("\n").forEach(function (row) {
if(row.substr(0, 7) === '[ERROR]' || row.substr(0, 9) === '[WARNING]' || row === MailPoet.I18n.t('import_stopped_by_user')) {
row = '<span class="error_msg">' + row + '</span>'; // Mark the errors in red
}
// Test if the import is complete
else if(row === MailPoet.I18n.t('import_complete')) {
jQuery('#import-actions').hide();
jQuery('#upgrade-completed').show();
}
jQuery("#logger").append(row + "<br />\n");
});
jQuery("#logger").append('<span class="error_msg">' + MailPoet.MP2Migrator.fatal_error + '</span>' + "<br />\n");
}).always(function () {
if(MailPoet.MP2Migrator.is_logging) {
MailPoet.MP2Migrator.displayLogs_timeout = setTimeout(MailPoet.MP2Migrator.displayLogs, 1000);
}
});
},
updateProgressbar: function () {
jQuery.ajax({
url: mailpoet_mp2_migrator.progress_url,
cache: false,
dataType: 'json'
}).always(function (result) {
// Move the progress bar
var progress = 0;
if((result.total !== undefined) && (Number(result.total) !== 0)) {
progress = Math.round(Number(result.current) / Number(result.total) * 100);
}
jQuery('#progressbar').progressbar('option', 'value', progress);
jQuery('#progresslabel').html(progress + '%');
if(Number(result.current !== 0)) {
jQuery('#skip-import').hide();
}
if(MailPoet.MP2Migrator.is_logging) {
MailPoet.MP2Migrator.updateProgressbar_timeout = setTimeout(MailPoet.MP2Migrator.updateProgressbar, 1000);
}
});
},
startImport: function () {
MailPoet.MP2Migrator.fatal_error = '';
// Start displaying the logs
MailPoet.MP2Migrator.startLogger();
// Disable the import button
MailPoet.MP2Migrator.import_button_label = jQuery('#import').val();
jQuery('#import').val(MailPoet.I18n.t('importing')).attr('disabled', 'disabled');
// Hide the Skip button
jQuery('#skip-import').hide();
// Show the stop button
jQuery('#stop-import').show();
// Run the import
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'MP2Migrator',
action: 'import',
data: {
}
}).always(function () {
MailPoet.MP2Migrator.stopLogger();
MailPoet.MP2Migrator.updateDisplay(); // Get the latest information after the import was stopped
MailPoet.MP2Migrator.reactivateImportButton();
}).done(function (response) {
if(response) {
MailPoet.MP2Migrator.fatal_error = response.data;
}
}).fail(function (response) {
if(response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function (error) {
return error.message;
}),
{scroll: true}
);
}
});
return false;
},
reactivateImportButton: function () {
jQuery('#import').val(MailPoet.MP2Migrator.import_button_label).removeAttr('disabled');
jQuery('#stop-import').hide();
},
stopImport: function () {
jQuery('#stop-import').attr('disabled', 'disabled');
// Stop the import
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'MP2Migrator',
action: 'stopImport',
data: {
}
}).always(function () {
jQuery('#stop-import').removeAttr('disabled'); // Enable the button
MailPoet.MP2Migrator.reactivateImportButton();
MailPoet.MP2Migrator.updateDisplay(); // Get the latest information after the import was stopped
});
MailPoet.MP2Migrator.stopLogger();
return false;
},
skipImport: function () {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'MP2Migrator',
action: 'skipImport',
data: {
}
}).done(function () {
MailPoet.MP2Migrator.gotoWelcomePage();
});
return false;
},
gotoWelcomePage: function () {
window.location.href = 'admin.php?page=mailpoet-welcome';
return false;
}
};
/**
* Actions to run when the DOM is ready
*/
jQuery(function () {
jQuery('#progressbar').progressbar({value: 0});
// Import button
jQuery('#import').click(function() {
MailPoet.MP2Migrator.startImport();
});
// Stop import button
jQuery('#stop-import').click(function() {
MailPoet.MP2Migrator.stopImport();
});
// Skip import link
jQuery('#skip-import').click(function() {
MailPoet.MP2Migrator.skipImport();
});
// Go to welcome page
jQuery('#goto-welcome').click(function() {
MailPoet.MP2Migrator.gotoWelcomePage();
});
// Update the display
MailPoet.MP2Migrator.updateDisplay();
});
});

View File

@ -1,21 +1,14 @@
define([
'backbone',
'backbone.marionette',
'backbone.radio',
'jquery',
'underscore',
'handlebars',
'handlebars_helpers'
], function(Backbone, Marionette, jQuery, _, Handlebars) {
], function(Backbone, Marionette, Radio, jQuery, _, Handlebars) {
var app = new Marionette.Application(), AppView;
// Decoupled communication between application components
app.getChannel = function(channel) {
if (channel === undefined) return app.channel;
return Radio.channel(channel);
};
AppView = Marionette.LayoutView.extend({
var AppView = Marionette.View.extend({
el: '#mailpoet_editor',
regions: {
stylesRegion: '#mailpoet_editor_styles',
@ -26,10 +19,23 @@ define([
},
});
app.on('start', function(options) {
app._appView = new AppView();
var EditorApplication = Marionette.Application.extend({
region: '#mailpoet_editor',
onStart: function() {
this._appView = new AppView();
this.showView(this._appView);
},
getChannel: function(channel) {
if (channel === undefined) channel = 'global';
return Radio.channel(channel);
}
});
var app = new EditorApplication();
window.EditorApplication = app;
return app;
});

View File

@ -71,6 +71,7 @@ define([
markerWidth = '',
markerHeight = '',
containerOffset = element.offset(),
viewCollection = that.getCollection(),
marker, targetModel, targetView, targetElement,
topOffset, leftOffset, isLastBlockInsertion,
$targetBlock, margin;
@ -80,19 +81,19 @@ define([
element.find('.mailpoet_drop_marker').remove();
// Allow empty collections to handle their own drop marking
if (view.model.get('blocks').isEmpty()) return;
if (viewCollection.isEmpty()) return;
if (view.collection.length === 0) {
if (viewCollection.length === 0) {
targetElement = element.find(view.childViewContainer);
topOffset = targetElement.offset().top - element.offset().top;
leftOffset = targetElement.offset().left - element.offset().left;
markerWidth = targetElement.width();
markerHeight = targetElement.height();
} else {
isLastBlockInsertion = view.collection.length === dropPosition.index;
targetModel = isLastBlockInsertion ? view.collection.at(dropPosition.index - 1) : view.collection.at(dropPosition.index);
isLastBlockInsertion = that.getCollection().length === dropPosition.index;
targetModel = isLastBlockInsertion ? viewCollection.at(dropPosition.index - 1) : viewCollection.at(dropPosition.index);
targetView = view.children.findByModel(targetModel);
targetView = that.getChildren().findByModel(targetModel);
targetElement = targetView.$el;
topOffset = targetElement.offset().top - containerOffset.top;
@ -135,10 +136,10 @@ define([
if (dropPosition.index === 0) {
marker.addClass('mailpoet_drop_marker_first');
}
if (view.collection.length - 1 === dropPosition.index) {
if (viewCollection.length - 1 === dropPosition.index) {
marker.addClass('mailpoet_drop_marker_last');
}
if (dropPosition.index > 0 && view.collection.length - 1 > dropPosition.index) {
if (dropPosition.index > 0 && viewCollection.length - 1 > dropPosition.index) {
marker.addClass('mailpoet_drop_marker_middle');
}
marker.addClass('mailpoet_drop_marker_' + dropPosition.position);
@ -147,9 +148,9 @@ define([
// compensated for to position marker right in the middle of two
// blocks
if (dropPosition.position === 'before') {
$targetBlock = view.children.findByModel(view.collection.at(dropPosition.index-1)).$el;
$targetBlock = that.getChildren().findByModel(viewCollection.at(dropPosition.index-1)).$el;
} else {
$targetBlock = view.children.findByModel(view.collection.at(dropPosition.index)).$el;
$targetBlock = that.getChildren().findByModel(viewCollection.at(dropPosition.index)).$el;
}
margin = $targetBlock.outerHeight(true) - $targetBlock.outerHeight();
@ -182,6 +183,7 @@ define([
view.model.get('blocks').length
),
droppableModel = event.draggable.getDropModel(),
viewCollection = that.getCollection(),
droppedView, droppedModel, index, tempCollection, tempCollection2;
if (dropPosition === undefined) return;
@ -196,22 +198,22 @@ define([
orientation: 'vertical',
});
tempCollection.get('blocks').add(droppableModel);
view.collection.add(tempCollection, {at: index});
viewCollection.add(tempCollection, {at: index});
} else {
view.collection.add(droppableModel, {at: index});
viewCollection.add(droppableModel, {at: index});
}
droppedView = view.children.findByModel(droppableModel);
droppedView = that.getChildren().findByModel(droppableModel);
} else {
// Special insertion by replacing target block with collection
// and inserting dropModel into that
var tempModel = view.collection.at(dropPosition.index);
var tempModel = viewCollection.at(dropPosition.index);
tempCollection = new (EditorApplication.getBlockTypeModel('container'))({
orientation: (view.model.get('orientation') === 'vertical') ? 'horizontal' : 'vertical',
});
view.collection.remove(tempModel);
viewCollection.remove(tempModel);
if (tempCollection.get('orientation') === 'horizontal') {
if (dropPosition.position === 'before') {
@ -242,10 +244,10 @@ define([
tempCollection.get('blocks').add(droppableModel);
}
}
view.collection.add(tempCollection, {at: dropPosition.index});
viewCollection.add(tempCollection, {at: dropPosition.index});
// Call post add actions
droppedView = view.children.findByModel(tempCollection).children.findByModel(droppableModel);
droppedView = that.getChildren().findByModel(tempCollection).children.findByModel(droppableModel);
}
// Call post add actions
@ -290,7 +292,7 @@ define([
unsafe = !!unsafe;
if (this.view.collection.length === 0) {
if (this.getCollection().length === 0) {
return {
insertionType: 'normal',
index: 0,
@ -327,7 +329,7 @@ define([
index = indexAndPosition.index;
}
if (!unsafe && orientation === 'vertical' && insertionType === 'special' && this.view.collection.at(index).get('orientation') === 'horizontal') {
if (!unsafe && orientation === 'vertical' && insertionType === 'special' && this.getCollection().at(index).get('orientation') === 'horizontal') {
// Prevent placing horizontal container in another horizontal container,
// which would allow breaking the column limit.
// Switch that to normal insertion
@ -356,7 +358,7 @@ define([
var index = this._computeCellIndex(eventX, eventY),
// TODO: Handle case when there are no children, container is empty
targetView = this.view.children.findByModel(this.view.collection.at(index)),
targetView = this.getChildren().findByModel(this.getCollection().at(index)),
orientation = this.view.model.get('orientation'),
element = targetView.$el,
eventOffset, closeOffset, elementDimension;
@ -391,7 +393,7 @@ define([
_computeCellIndex: function(eventX, eventY) {
var orientation = this.view.model.get('orientation'),
eventOffset = (orientation === 'vertical') ? eventY : eventX,
resultView = this.view.children.find(function(view) {
resultView = this.getChildren().find(function(view) {
var element = view.$el,
closeOffset, farOffset;
@ -414,15 +416,24 @@ define([
_canAcceptNormalInsertion: function() {
var orientation = this.view.model.get('orientation'),
depth = this.view.renderOptions.depth,
childCount = this.view.children.length;
childCount = this.getChildren().length;
// Note that depth is zero indexed. Root container has depth=0
return orientation === 'vertical' || (orientation === 'horizontal' && depth === 1 && childCount < this.options.columnLimit);
},
_canAcceptSpecialInsertion: function() {
var orientation = this.view.model.get('orientation'),
depth = this.view.renderOptions.depth,
childCount = this.view.children.length;
childCount = this.getChildren().length;
return depth === 0 || (depth === 1 && orientation === 'horizontal' && childCount <= this.options.columnLimit);
},
getCollectionView: function() {
return this.view.getChildView('blocks');
},
getChildren: function() {
return this.getCollectionView().children;
},
getCollection: function() {
return this.getCollectionView().collection;
}
});
});

View File

@ -60,10 +60,17 @@ define([
editor.on('click', function(e) {
editor.focus();
if (that._isActivationClick) {
editor.selection.setRng(
tinymce.dom.RangeUtils.getCaretRangeFromPoint(e.clientX, e.clientY, editor.getDoc())
);
that._isActivationClick = false;
}
});
editor.on('focus', function(e) {
that.view.triggerMethod('text:editor:focus');
that._isActivationClick = true;
});
editor.on('blur', function(e) {

View File

@ -129,6 +129,12 @@ define([
Module.AutomatedLatestContentBlockView = base.BlockView.extend({
className: "mailpoet_block mailpoet_automated_latest_content_block mailpoet_droppable_block",
initialize: function() {
function replaceButtonStylesHandler(data) {
this.model.set({"readMoreButton": data});
}
App.getChannel().on("replaceAllButtonStyles", replaceButtonStylesHandler.bind(this));
},
getTemplate: function() { return templates.automatedLatestContentBlock; },
regions: {
toolsRegion: '.mailpoet_tools',
@ -151,8 +157,8 @@ define([
emptyContainerMessage: MailPoet.I18n.t('noPostsToDisplay'),
};
this.toolsView = new Module.AutomatedLatestContentBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
this.postsRegion.show(new ContainerView({ model: this.model.get('_container'), renderOptions: renderOptions }));
this.showChildView('toolsRegion', this.toolsView);
this.showChildView('postsRegion', new ContainerView({ model: this.model.get('_container'), renderOptions: renderOptions }));
},
});
@ -189,11 +195,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
};
},
onRender: function() {
var that = this;
@ -377,7 +378,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('automatedLatestContent', {
blockModel: Module.AutomatedLatestContentBlockModel,
blockView: Module.AutomatedLatestContentBlockView,
@ -390,7 +391,7 @@ define([
});
});
App.on('start', function() {
App.on('start', function(App, options) {
App._ALCSupervisor = new Module.ALCSupervisor();
App._ALCSupervisor.refresh();
});

View File

@ -17,7 +17,7 @@ define([
"use strict";
var Module = {},
AugmentedView = Marionette.LayoutView.extend({});
AugmentedView = Marionette.View.extend({});
Module.BlockModel = SuperModel.extend({
stale: [], // Attributes to be removed upon saving
@ -82,7 +82,7 @@ define([
},
HighlightEditingBehavior: {},
},
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
viewCid: this.cid,
@ -125,6 +125,12 @@ define([
return this.model.clone();
}.bind(this);
},
disableDragging: function() {
this.$el.addClass('mailpoet_ignore_drag');
},
enableDragging: function() {
this.$el.removeClass('mailpoet_ignore_drag');
},
showBlock: function() {
if (this._isFirstRender) {
this.transitionIn();
@ -193,7 +199,7 @@ define([
this.on('hideTools', this.hideDeletionConfirmation, this);
this.on('showSettings', this.changeSettings);
},
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
viewCid: this.cid,
@ -217,7 +223,7 @@ define([
},
});
Module.BlockSettingsView = Marionette.LayoutView.extend({
Module.BlockSettingsView = Marionette.View.extend({
className: 'mailpoet_editor_settings',
behaviors: {
ColorPickerBehavior: {},
@ -240,6 +246,11 @@ define([
MailPoet.Modal.panel(panelParams);
}
},
templateContext: function() {
return {
model: this.model.toJSON()
};
},
close: function(event) {
this.destroy();
},
@ -271,7 +282,7 @@ define([
},
});
Module.WidgetView = Marionette.ItemView.extend({
Module.WidgetView = Marionette.View.extend({
className: 'mailpoet_widget mailpoet_droppable_block mailpoet_droppable_widget',
behaviors: {
DraggableBehavior: {

View File

@ -56,7 +56,7 @@ define([
},
onRender: function() {
this.toolsView = new Module.ButtonBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
this.showChildView('toolsRegion', this.toolsView);
},
});
@ -98,12 +98,11 @@ define([
"click .mailpoet_done_editing": "close",
};
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
templateContext: function() {
return _.extend({}, base.BlockView.prototype.templateContext.apply(this, arguments), {
availableStyles: App.getAvailableStyles().toJSON(),
renderOptions: this.renderOptions,
};
});
},
applyToAll: function() {
App.getChannel().trigger('replaceAllButtonStyles', _.pick(this.model.toJSON(), 'styles', 'type'));
@ -133,7 +132,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('button', {
blockModel: Module.ButtonBlockModel,
blockView: Module.ButtonBlockView,

View File

@ -74,29 +74,43 @@ define([
},
});
Module.ContainerBlockView = Marionette.CompositeView.extend({
regionClass: Marionette.Region,
Module.ContainerBlocksView = Marionette.CollectionView.extend({
className: 'mailpoet_container',
childView: function(model) {
return App.getBlockTypeView(model.get('type'));
},
childViewOptions: function() {
var newRenderOptions = _.clone(this.renderOptions);
if (newRenderOptions.depth !== undefined) {
newRenderOptions.depth += 1;
}
return {
renderOptions: newRenderOptions
};
},
emptyView: function() { return Module.ContainerBlockEmptyView; },
emptyViewOptions: function() { return { renderOptions: this.renderOptions }; },
initialize: function(options) {
this.renderOptions = options.renderOptions;
}
});
Module.ContainerBlockView = base.BlockView.extend({
regions: _.extend({}, base.BlockView.prototype.regions, {
blocks: {
el: '> .mailpoet_container',
replaceElement: true
},
}),
className: 'mailpoet_block mailpoet_container_block mailpoet_droppable_block mailpoet_droppable_layout_block',
getTemplate: function() { return templates.containerBlock; },
childViewContainer: '> .mailpoet_container',
getEmptyView: function() { return Module.ContainerBlockEmptyView; },
emptyViewOptions: function() { return { renderOptions: this.renderOptions }; },
modelEvents: {
'change': 'render',
'delete': 'deleteBlock',
},
events: {
"mouseenter": "showTools",
"mouseleave": "hideTools",
events: _.extend({}, base.BlockView.prototype.events, {
"click .mailpoet_newsletter_layer_selector": "toggleEditingLayer",
},
regions: {
toolsRegion: '> .mailpoet_tools',
},
}),
ui: {
tools: '> .mailpoet_tools'
},
behaviors: {
behaviors: _.extend({}, base.BlockView.prototype.behaviors, {
ContainerDropZoneBehavior: {},
DraggableBehavior: {
cloneOriginal: true,
@ -125,8 +139,7 @@ define([
return view.renderOptions.depth === 1;
},
},
HighlightEditingBehavior: {}
},
}),
onDragSubstituteBy: function() {
// For two and three column layouts display their respective widgets,
// otherwise always default to one column layout widget
@ -137,39 +150,12 @@ define([
return Module.OneColumnContainerWidgetView;
},
constructor: function() {
// Set the block collection to be handled by this view as well
arguments[0].collection = arguments[0].model.get('blocks');
Marionette.CompositeView.apply(this, arguments);
this.$el.addClass('mailpoet_editor_view_' + this.cid);
},
initialize: function(options) {
base.BlockView.prototype.initialize.apply(this, arguments);
this.renderOptions = _.defaults(options.renderOptions || {}, {});
this.on('dom:refresh', this.showBlock, this);
this._isFirstRender = true;
},
// Determines which view type should be used for a child
getChildView: function(model) {
// TODO: If type does not have a type registered, use a generic one
return App.getBlockTypeView(model.get('type'));
},
childViewOptions: function() {
var newRenderOptions = _.clone(this.renderOptions);
if (newRenderOptions.depth !== undefined) {
newRenderOptions.depth += 1;
}
return {
renderOptions: newRenderOptions
};
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
viewCid: this.cid,
};
},
onRender: function() {
this._rebuildRegions();
this.toolsView = new Module.ContainerBlockToolsView({
model: this.model,
tools: {
@ -179,10 +165,15 @@ define([
layerSelector: false,
},
});
this.toolsRegion.show(this.toolsView);
},
onBeforeDestroy: function() {
this.regionManager.destroy();
this.showChildView('toolsRegion', this.toolsView);
this.showChildView('blocks', new Module.ContainerBlocksView({
collection: this.model.get('blocks'),
renderOptions: this.renderOptions
}));
// TODO: Look for a better way to do this than here
// Sets child container orientation HTML class here, as child CollectionView won't have access to model and will overwrite existing region element instead
this.$('> .mailpoet_container').attr('class', 'mailpoet_container mailpoet_container_' + this.model.get('orientation'));
},
showTools: function() {
if (this.renderOptions.depth === 1 && !this.$el.hasClass('mailpoet_container_layer_active')) {
@ -222,76 +213,14 @@ define([
}
event.stopPropagation();
},
_buildRegions: function(regions) {
var that = this;
var defaults = {
regionClass: this.getOption('regionClass'),
parentEl: function() { return that.$el; }
};
return this.regionManager.addRegions(regions, defaults);
},
_rebuildRegions: function() {
if (this.regionManager === undefined) {
this.regionManager = new Marionette.RegionManager();
}
this.regionManager.destroy();
_.extend(this, this._buildRegions(this.regions));
},
getDropFunc: function() {
return function() {
return this.model.clone();
}.bind(this);
},
showBlock: function() {
if (this._isFirstRender) {
this.transitionIn();
this._isFirstRender = false;
}
},
deleteBlock: function() {
this.transitionOut().done(function() {
this.model.destroy();
}.bind(this));
},
transitionIn: function() {
return this._transition('slideDown', 'fadeIn', 'easeIn');
},
transitionOut: function() {
return this._transition('slideUp', 'fadeOut', 'easeOut');
},
_transition: function(slideDirection, fadeDirection, easing) {
var promise = jQuery.Deferred();
this.$el.velocity(
slideDirection,
{
duration: 250,
easing: easing,
complete: function() {
promise.resolve();
}.bind(this),
}
).velocity(
fadeDirection,
{
duration: 250,
easing: easing,
queue: false, // Do not enqueue, trigger animation in parallel
}
);
return promise;
},
});
Module.ContainerBlockEmptyView = Marionette.ItemView.extend({
Module.ContainerBlockEmptyView = Marionette.View.extend({
getTemplate: function() { return templates.containerEmpty; },
initialize: function(options) {
this.renderOptions = _.defaults(options.renderOptions || {}, {});
},
templateHelpers: function() {
templateContext: function() {
return {
isRoot: this.renderOptions.depth === 0,
emptyContainerMessage: this.renderOptions.emptyContainerMessage || '',
@ -322,12 +251,12 @@ define([
});
},
onRender: function() {
this.columnsSettingsRegion.show(this._columnsSettingsView);
this.showChildView('columnsSettingsRegion', this._columnsSettingsView);
},
});
Module.ContainerBlockColumnsSettingsView = Marionette.CollectionView.extend({
getChildView: function() { return Module.ContainerBlockColumnSettingsView; },
childView: function() { return Module.ContainerBlockColumnSettingsView; },
childViewOptions: function(model, index) {
return {
columnIndex: index,
@ -335,12 +264,12 @@ define([
},
});
Module.ContainerBlockColumnSettingsView = Marionette.ItemView.extend({
Module.ContainerBlockColumnSettingsView = Marionette.View.extend({
getTemplate: function() { return templates.containerBlockColumnSettings; },
initialize: function(options) {
this.columnNumber = (options.columnIndex || 0) + 1;
},
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
columnNumber: this.columnNumber,
@ -405,7 +334,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('container', {
blockModel: Module.ContainerBlockModel,
blockView: Module.ContainerBlockView,

View File

@ -59,14 +59,14 @@ define([
this.listenTo(this.model, 'change:src change:styles.block.backgroundColor change:styles.block.borderStyle change:styles.block.borderWidth change:styles.block.borderColor applyToAll', this.render);
this.listenTo(this.model, 'change:styles.block.padding', this.changePadding);
},
templateHelpers: function() {
templateContext: function() {
return _.extend({
totalHeight: parseInt(this.model.get('styles.block.padding'), 10)*2 + parseInt(this.model.get('styles.block.borderWidth')) + 'px',
}, base.BlockView.prototype.templateHelpers.apply(this));
}, base.BlockView.prototype.templateContext.apply(this));
},
onRender: function() {
this.toolsView = new Module.DividerBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
this.showChildView('toolsRegion', this.toolsView);
},
onBeforeDestroy: function() {
App.getChannel().off('replaceAllDividers', this._replaceDividerHandler);
@ -104,12 +104,11 @@ define([
'change:styles.block.borderColor': 'repaintDividerStyleOptions',
};
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
templateContext: function() {
return _.extend({}, base.BlockView.prototype.templateContext.apply(this, arguments), {
availableStyles: App.getAvailableStyles().toJSON(),
renderOptions: this.renderOptions,
};
});
},
changeStyle: function(event) {
var style = jQuery(event.currentTarget).data('style');
@ -140,7 +139,7 @@ define([
}
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('divider', {
blockModel: Module.DividerBlockModel,
blockView: Module.DividerBlockView,

View File

@ -55,7 +55,7 @@ define([
onDragSubstituteBy: function() { return Module.FooterWidgetView; },
onRender: function() {
this.toolsView = new Module.FooterBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
this.showChildView('toolsRegion', this.toolsView);
},
onTextEditorChange: function(newContent) {
this.model.set('text', newContent);
@ -68,12 +68,6 @@ define([
this.enableDragging();
this.enableShowingTools();
},
disableDragging: function() {
this.$('.mailpoet_content').addClass('mailpoet_ignore_drag');
},
enableDragging: function() {
this.$('.mailpoet_content').removeClass('mailpoet_ignore_drag');
},
});
Module.FooterBlockToolsView = base.BlockToolsView.extend({
@ -96,11 +90,10 @@ define([
"click .mailpoet_done_editing": "close",
};
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
templateContext: function() {
return _.extend({}, base.BlockView.prototype.templateContext.apply(this, arguments), {
availableStyles: App.getAvailableStyles().toJSON(),
};
});
},
});
@ -116,7 +109,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('footer', {
blockModel: Module.FooterBlockModel,
blockView: Module.FooterBlockView,

View File

@ -55,7 +55,7 @@ define([
onDragSubstituteBy: function() { return Module.HeaderWidgetView; },
onRender: function() {
this.toolsView = new Module.HeaderBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
this.showChildView('toolsRegion', this.toolsView);
},
onTextEditorChange: function(newContent) {
this.model.set('text', newContent);
@ -68,12 +68,6 @@ define([
this.enableDragging();
this.enableShowingTools();
},
disableDragging: function() {
this.$('.mailpoet_content').addClass('mailpoet_ignore_drag');
},
enableDragging: function() {
this.$('.mailpoet_content').removeClass('mailpoet_ignore_drag');
},
});
Module.HeaderBlockToolsView = base.BlockToolsView.extend({
@ -96,11 +90,10 @@ define([
"click .mailpoet_done_editing": "close",
};
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
templateContext: function() {
return _.extend({}, base.BlockView.prototype.templateContext.apply(this, arguments), {
availableStyles: App.getAvailableStyles().toJSON(),
};
});
},
});
@ -116,7 +109,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('header', {
blockModel: Module.HeaderBlockModel,
blockView: Module.HeaderBlockView,

View File

@ -36,17 +36,17 @@ define([
className: "mailpoet_block mailpoet_image_block mailpoet_droppable_block",
getTemplate: function() { return templates.imageBlock; },
onDragSubstituteBy: function() { return Module.ImageWidgetView; },
templateHelpers: function() {
templateContext: function() {
return _.extend({
imageMissingSrc: App.getConfig().get('urls.imageMissing'),
}, base.BlockView.prototype.templateHelpers.apply(this));
}, base.BlockView.prototype.templateContext.apply(this));
},
behaviors: _.extend({}, base.BlockView.prototype.behaviors, {
ShowSettingsBehavior: {},
}),
onRender: function() {
this.toolsView = new Module.ImageBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
this.showChildView('toolsRegion', this.toolsView);
if (this.model.get('fullWidth')) {
this.$el.addClass('mailpoet_full_image');
@ -65,7 +65,7 @@ define([
events: function() {
return {
"input .mailpoet_field_image_link": _.partial(this.changeField, "link"),
"input .mailpoet_field_image_address": _.partial(this.changeField, "src"),
"input .mailpoet_field_image_address": 'changeAddress',
"input .mailpoet_field_image_alt_text": _.partial(this.changeField, "alt"),
"change .mailpoet_field_image_full_width": _.partial(this.changeBoolCheckboxField, "fullWidth"),
"change .mailpoet_field_image_alignment": _.partial(this.changeField, "styles.block.textAlign"),
@ -327,6 +327,20 @@ define([
this._mediaManager.open();
},
changeAddress: function(event) {
var src = jQuery(event.target).val();
var image = new Image();
image.onload = function() {
this.model.set({
src: src,
width: image.naturalWidth + 'px',
height: image.naturalHeight + 'px'
});
}.bind(this);
image.src = src;
},
onBeforeDestroy: function() {
base.BlockSettingsView.prototype.onBeforeDestroy.apply(this, arguments);
if (typeof this._mediaManager === 'object') {
@ -351,7 +365,7 @@ define([
});
Module.ImageWidgetView = ImageWidgetView;
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('image', {
blockModel: Module.ImageBlockModel,
blockView: Module.ImageBlockView,

View File

@ -166,8 +166,8 @@ define([
this.model.reply('blockView', this.notifyAboutSelf, this);
},
onRender: function() {
if (!this.toolsRegion.hasView()) {
this.toolsRegion.show(this.toolsView);
if (!this.getRegion('toolsRegion').hasView()) {
this.showChildView('toolsRegion', this.toolsView);
}
this.trigger('showSettings');
@ -177,7 +177,7 @@ define([
disableDragAndDrop: true,
emptyContainerMessage: MailPoet.I18n.t('noPostsToDisplay'),
};
this.postsRegion.show(new ContainerView({ model: this.model.get('_transformedPosts'), renderOptions: renderOptions }));
this.showChildView('postsRegion', new ContainerView({ model: this.model.get('_transformedPosts'), renderOptions: renderOptions }));
},
notifyAboutSelf: function() {
return this;
@ -202,7 +202,7 @@ define([
'click .mailpoet_settings_posts_show_post_selection': 'switchToPostSelection',
'click .mailpoet_settings_posts_insert_selected': 'insertPosts',
},
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
};
@ -259,16 +259,24 @@ define([
},
});
var PostSelectionSettingsView = Marionette.CompositeView.extend({
getTemplate: function() { return templates.postSelectionPostsBlockSettings; },
getChildView: function() { return SinglePostSelectionSettingsView; },
childViewContainer: '.mailpoet_post_selection_container',
getEmptyView: function() { return EmptyPostSelectionSettingsView; },
var PostsSelectionCollectionView = Marionette.CollectionView.extend({
childView: function() { return SinglePostSelectionSettingsView; },
emptyView: function() { return EmptyPostSelectionSettingsView; },
childViewOptions: function() {
return {
blockModel: this.model,
blockModel: this.blockModel,
};
},
initialize: function(options) {
this.blockModel = options.blockModel;
},
});
var PostSelectionSettingsView = Marionette.View.extend({
getTemplate: function() { return templates.postSelectionPostsBlockSettings; },
regions: {
posts: '.mailpoet_post_selection_container',
},
events: function() {
return {
'change .mailpoet_settings_posts_content_type': _.partial(this.changeField, 'contentType'),
@ -276,21 +284,19 @@ define([
'input .mailpoet_posts_search_term': _.partial(this.changeField, 'search'),
};
},
constructor: function() {
// Set the block collection to be handled by this view as well
arguments[0].collection = arguments[0].model.get('_availablePosts');
Marionette.CompositeView.apply(this, arguments);
},
onRender: function() {
// Dynamically update available post types
CommunicationComponent.getPostTypes().done(_.bind(this._updateContentTypes, this));
var postsView = new PostsSelectionCollectionView({
collection: this.model.get('_availablePosts'),
blockModel: this.model
});
this.showChildView('posts', postsView);
},
onAttach: function() {
var that = this;
// Dynamically update available post types
//CommunicationComponent.getPostTypes().done(_.bind(this._updateContentTypes, this));
this.$('.mailpoet_posts_categories_and_tags').select2({
multiple: true,
allowClear: true,
@ -372,18 +378,18 @@ define([
},
});
var EmptyPostSelectionSettingsView = Marionette.ItemView.extend({
var EmptyPostSelectionSettingsView = Marionette.View.extend({
getTemplate: function() { return templates.emptyPostPostsBlockSettings; },
});
var SinglePostSelectionSettingsView = Marionette.ItemView.extend({
var SinglePostSelectionSettingsView = Marionette.View.extend({
getTemplate: function() { return templates.singlePostPostsBlockSettings; },
events: function() {
return {
'change .mailpoet_select_post_checkbox': 'postSelectionChange',
};
},
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
index: this._index,
@ -428,7 +434,7 @@ define([
"change .mailpoet_posts_sort_by": _.partial(this.changeField, "sortBy"),
};
},
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
};
@ -520,7 +526,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('posts', {
blockModel: Module.PostsBlockModel,
blockView: Module.PostsBlockView,

View File

@ -17,6 +17,7 @@ define([
base = BaseBlock,
SocialBlockSettingsIconSelectorView,
SocialBlockSettingsIconView,
SocialBlockSettingsIconCollectionView,
SocialBlockSettingsStylesView;
Module.SocialIconModel = SuperModel.extend({
@ -82,13 +83,13 @@ define([
},
});
var SocialIconView = Marionette.ItemView.extend({
var SocialIconView = Marionette.View.extend({
tagName: 'span',
getTemplate: function() { return templates.socialIconBlock; },
modelEvents: {
'change': 'render',
},
templateHelpers: function() {
templateContext: function() {
var allIconSets = App.getAvailableStyles().get('socialIconSets');
return {
model: this.model.toJSON(),
@ -98,150 +99,29 @@ define([
},
});
Module.SocialBlockView = Marionette.CompositeView.extend({
regionClass: Marionette.Region,
Module.SocialIconCollectionView = Marionette.CollectionView.extend({
childView: SocialIconView,
});
Module.SocialBlockView = base.BlockView.extend({
className: 'mailpoet_block mailpoet_social_block mailpoet_droppable_block',
getTemplate: function() { return templates.socialBlock; },
childViewContainer: '.mailpoet_social',
modelEvents: {
'change': 'render',
'delete': 'deleteBlock',
},
events: {
"mouseover": "showTools",
"mouseout": "hideTools",
},
regions: {
toolsRegion: '> .mailpoet_tools',
},
regions: _.extend({}, base.BlockView.prototype.regions, {
icons: '.mailpoet_social'
}),
ui: {
tools: '> .mailpoet_tools'
},
behaviors: {
DraggableBehavior: {
cloneOriginal: true,
hideOriginal: true,
onDrop: function(options) {
// After a clone of model has been dropped, cleanup
// and destroy self
options.dragBehavior.view.model.destroy();
},
onDragSubstituteBy: function(behavior) {
var WidgetView, node;
// When block is being dragged, display the widget icon instead.
// This will create an instance of block's widget view and
// use it's rendered DOM element instead of the content block
if (_.isFunction(behavior.view.onDragSubstituteBy)) {
WidgetView = new (behavior.view.onDragSubstituteBy())();
WidgetView.render();
node = WidgetView.$el.get(0).cloneNode(true);
WidgetView.destroy();
return node;
}
},
},
HighlightEditingBehavior: {},
behaviors: _.extend({}, base.BlockView.prototype.behaviors, {
ShowSettingsBehavior: {},
},
}),
onDragSubstituteBy: function() { return Module.SocialWidgetView; },
constructor: function() {
// Set the block collection to be handled by this view as well
arguments[0].collection = arguments[0].model.get('icons');
Marionette.CompositeView.apply(this, arguments);
},
initialize: function() {
this.on('showSettings', this.showSettings, this);
this.on('dom:refresh', this.showBlock, this);
this._isFirstRender = true;
},
// Determines which view type should be used for a child
childView: SocialIconView,
templateHelpers: function() {
return {
model: this.model.toJSON(),
viewCid: this.cid,
};
},
onRender: function() {
this._rebuildRegions();
this.toolsView = new Module.SocialBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
},
onBeforeDestroy: function() {
this.regionManager.destroy();
},
showTools: function(_event) {
this.$(this.ui.tools).addClass('mailpoet_display_tools');
_event.stopPropagation();
},
hideTools: function(_event) {
this.$(this.ui.tools).removeClass('mailpoet_display_tools');
_event.stopPropagation();
},
showSettings: function(options) {
this.toolsView.triggerMethod('showSettings', options);
},
getDropFunc: function() {
return function() {
return this.model.clone();
}.bind(this);
},
_buildRegions: function(regions) {
var that = this;
var defaults = {
regionClass: this.getOption('regionClass'),
parentEl: function() { return that.$el; }
};
return this.regionManager.addRegions(regions, defaults);
},
_rebuildRegions: function() {
if (this.regionManager === undefined) {
this.regionManager = new Marionette.RegionManager();
}
this.regionManager.destroy();
_.extend(this, this._buildRegions(this.regions));
},
showBlock: function() {
if (this._isFirstRender) {
this.transitionIn();
this._isFirstRender = false;
}
},
deleteBlock: function() {
this.transitionOut().done(function() {
this.model.destroy();
}.bind(this));
},
transitionIn: function() {
return this._transition('slideDown', 'fadeIn', 'easeIn');
},
transitionOut: function() {
return this._transition('slideUp', 'fadeOut', 'easeOut');
},
_transition: function(slideDirection, fadeDirection, easing) {
var promise = jQuery.Deferred();
this.$el.velocity(
slideDirection,
{
duration: 250,
easing: easing,
complete: function() {
promise.resolve();
}.bind(this),
}
).velocity(
fadeDirection,
{
duration: 250,
easing: easing,
queue: false, // Do not enqueue, trigger animation in parallel
}
);
return promise;
this.showChildView('toolsRegion', this.toolsView);
this.showChildView('icons', new Module.SocialIconCollectionView({
collection: this.model.get('icons')
}))
},
});
@ -268,13 +148,13 @@ define([
this._stylesView = new SocialBlockSettingsStylesView({ model: this.model });
},
onRender: function() {
this.iconRegion.show(this._iconSelectorView);
this.stylesRegion.show(this._stylesView);
this.showChildView('iconRegion', this._iconSelectorView);
this.showChildView('stylesRegion', this._stylesView);
}
});
// Single icon settings view, used by the selector view
SocialBlockSettingsIconView = Marionette.ItemView.extend({
SocialBlockSettingsIconView = Marionette.View.extend({
getTemplate: function() { return templates.socialSettingsIcon; },
events: function() {
return {
@ -294,17 +174,16 @@ define([
this.$('.mailpoet_social_icon_image').attr('alt', this.model.get('text'));
},
},
templateHelpers: function() {
templateContext: function() {
var icons = App.getConfig().get('socialIcons'),
// Construct icon type list of format [{iconType: 'type', title: 'Title'}, ...]
availableIconTypes = _.map(_.keys(icons.attributes), function(key) { return { iconType: key, title: icons.get(key).get('title') }; }),
allIconSets = App.getAvailableStyles().get('socialIconSets');
return {
model: this.model.toJSON(),
return _.extend({}, base.BlockView.prototype.templateContext.apply(this, arguments), {
iconTypes: availableIconTypes,
currentType: icons.get(this.model.get('iconType')).toJSON(),
allIconSets: allIconSets.toJSON(),
};
});
},
deleteIcon: function() {
this.model.destroy();
@ -321,34 +200,41 @@ define([
},
});
// Select icons section container view
SocialBlockSettingsIconSelectorView = Marionette.CompositeView.extend({
getTemplate: function() { return templates.socialSettingsIconSelector; },
childView: SocialBlockSettingsIconView,
SocialBlockSettingsIconCollectionView = Marionette.CollectionView.extend({
behaviors: {
SortableBehavior: {
items: '> div',
},
},
childViewContainer: '#mailpoet_social_icon_selector_contents',
childView: SocialBlockSettingsIconView,
});
// Select icons section container view
SocialBlockSettingsIconSelectorView = Marionette.View.extend({
getTemplate: function() { return templates.socialSettingsIconSelector; },
regions: {
'icons': '#mailpoet_social_icon_selector_contents'
},
events: {
'click .mailpoet_add_social_icon': 'addSocialIcon',
},
modelEvents: {
'change:iconSet': 'render',
},
behaviors: {
SortableBehavior: {
items: '#mailpoet_social_icon_selector_contents > div',
},
},
constructor: function() {
// Set the icon collection to be handled by this view as well
arguments[0].collection = arguments[0].model.get('icons');
Marionette.CompositeView.apply(this, arguments);
},
addSocialIcon: function() {
// Add a social icon with default values
this.collection.add({});
this.model.get('icons').add({});
},
onRender: function() {
this.showChildView('icons', new SocialBlockSettingsIconCollectionView({
collection: this.model.get('icons')
}));
}
});
SocialBlockSettingsStylesView = Marionette.ItemView.extend({
SocialBlockSettingsStylesView = Marionette.View.extend({
getTemplate: function() { return templates.socialSettingsStyles; },
modelEvents: {
'change': 'render',
@ -359,7 +245,7 @@ define([
initialize: function() {
this.listenTo(this.model.get('icons'), 'add remove change', this.render);
},
templateHelpers: function() {
templateContext: function() {
var allIconSets = App.getAvailableStyles().get('socialIconSets');
return {
activeSet: this.model.get('iconSet'),
@ -411,7 +297,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('social', {
blockModel: Module.SocialBlockModel,
blockView: Module.SocialBlockView,

View File

@ -50,7 +50,7 @@ define([
},
onRender: function() {
this.toolsView = new Module.SpacerBlockToolsView({ model: this.model });
this.toolsRegion.show(this.toolsView);
this.showChildView('toolsRegion', this.toolsView);
},
changeHeight: function() {
this.$('.mailpoet_spacer').css('height', this.model.get('styles.block.height'));
@ -87,7 +87,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('spacer', {
blockModel: Module.SpacerBlockModel,
blockView: Module.SpacerBlockView,

View File

@ -32,7 +32,7 @@ define([
validElements: "p[class|style],span[class|style],a[href|class|title|target|style],h1[class|style],h2[class|style],h3[class|style],ol[class|style],ul[class|style],li[class|style],strong[class|style],em[class|style],strike,br,blockquote[class|style],table[class|style],tr[class|style],th[class|style],td[class|style]",
invalidElements: "script",
blockFormats: 'Heading 1=h1;Heading 2=h2;Heading 3=h3;Paragraph=p',
plugins: "link code textcolor colorpicker mailpoet_shortcodes",
plugins: "link lists code textcolor colorpicker mailpoet_shortcodes paste",
configurationFilter: function(originalSettings) {
return _.extend({}, originalSettings, {
mailpoet_shortcodes: App.getConfig().get('shortcodes').toJSON(),
@ -58,7 +58,7 @@ define([
settings: false,
},
});
this.toolsRegion.show(this.toolsView);
this.showChildView('toolsRegion', this.toolsView);
},
onTextEditorChange: function(newContent) {
this.model.set('text', newContent);
@ -71,13 +71,6 @@ define([
this.enableDragging();
this.enableShowingTools();
},
disableDragging: function() {
this.$('.mailpoet_content').addClass('mailpoet_ignore_drag');
},
enableDragging: function() {
this.$('.mailpoet_content').removeClass('mailpoet_ignore_drag');
},
});
Module.TextBlockToolsView = base.BlockToolsView.extend({
@ -100,7 +93,7 @@ define([
},
});
App.on('before:start', function() {
App.on('before:start', function(App, options) {
App.registerBlockType('text', {
blockModel: Module.TextBlockModel,
blockView: Module.TextBlockView,

View File

@ -9,6 +9,7 @@ define([
Module._query = function(args) {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'automatedLatestContent',
action: args.action,
data: args.options || {}
@ -81,6 +82,7 @@ define([
Module.saveNewsletter = function(options) {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'save',
data: options || {}
@ -89,13 +91,14 @@ define([
Module.previewNewsletter = function(options) {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'sendPreview',
data: options || {}
});
};
App.on('start', function(options) {
App.on('start', function(App, options) {
// Prefetch post types
Module.getPostTypes();
});

View File

@ -24,7 +24,7 @@ define([
return Module._config;
};
App.on('before:start', function(options) {
App.on('before:start', function(App, options) {
// Expose config methods globally
App.getConfig = Module.getConfig;
App.setConfig = Module.setConfig;

View File

@ -67,7 +67,7 @@ define([
return _.filter(blocks, predicate);
};
App.on('before:start', function(options) {
App.on('before:start', function(App, options) {
// Expose block methods globally
App.registerBlockType = Module.registerBlockType;
App.getBlockTypeModel = Module.getBlockTypeModel;
@ -80,7 +80,7 @@ define([
Module.newsletter = new Module.NewsletterModel(_.omit(_.clone(options.newsletter), ['body']));
});
App.on('start', function(options) {
App.on('start', function(App, options) {
var body = options.newsletter.body;
var content = (_.has(body, 'content')) ? body.content : {};
@ -97,7 +97,7 @@ define([
renderOptions: { depth: 0 },
});
App._appView.contentRegion.show(App._contentContainerView);
App._appView.showChildView('contentRegion', App._contentContainerView);
});

View File

@ -10,9 +10,9 @@ define([
var Module = {};
Module.HeadingView = Marionette.ItemView.extend({
Module.HeadingView = Marionette.View.extend({
getTemplate: function() { return templates.heading; },
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
};
@ -28,8 +28,8 @@ define([
},
});
App.on('start', function(options) {
App._appView.headingRegion.show(new Module.HeadingView({ model: App.getNewsletter() }));
App.on('start', function(App, options) {
App._appView.showChildView('headingRegion', new Module.HeadingView({ model: App.getNewsletter() }));
});
return Module;

View File

@ -109,6 +109,7 @@ define([
});
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletterTemplates',
action: 'save',
data: data
@ -142,7 +143,7 @@ define([
});
};
Module.SaveView = Marionette.LayoutView.extend({
Module.SaveView = Marionette.View.extend({
getTemplate: function() { return templates.save; },
events: {
'click .mailpoet_save_button': 'save',
@ -340,7 +341,7 @@ define([
}
};
App.on('before:start', function(options) {
App.on('before:start', function(App, options) {
App.save = Module.saveAndProvidePromise;
App.getChannel().on('autoSave', Module.autoSave);
@ -349,9 +350,9 @@ define([
App.getChannel().on('save', function(saveResult) { App.save(saveResult); });
});
App.on('start', function(options) {
App.on('start', function(App, options) {
var saveView = new Module.SaveView();
App._appView.bottomRegion.show(saveView);
App._appView.showChildView('bottomRegion', saveView);
});
return Module;

View File

@ -52,7 +52,7 @@ define([
Module.registerLayoutWidget = function(widget) { return Module._layoutWidgets.add(widget); };
Module.getLayoutWidgets = function() { return Module._layoutWidgets; };
var SidebarView = Marionette.LayoutView.extend({
var SidebarView = Marionette.View.extend({
getTemplate: function() { return templates.sidebar; },
regions: {
contentRegion: '.mailpoet_content_region',
@ -96,17 +96,17 @@ define([
.on('scroll', this.updateHorizontalScroll.bind(this));
},
onRender: function() {
this.contentRegion.show(new Module.SidebarWidgetsView({
collection: App.getWidgets(),
}));
this.layoutRegion.show(new Module.SidebarLayoutWidgetsView({
collection: App.getLayoutWidgets(),
}));
this.stylesRegion.show(new Module.SidebarStylesView({
this.showChildView('contentRegion', new Module.SidebarWidgetsView(
App.getWidgets()
));
this.showChildView('layoutRegion', new Module.SidebarLayoutWidgetsView(
App.getLayoutWidgets()
));
this.showChildView('stylesRegion', new Module.SidebarStylesView({
model: App.getGlobalStyles(),
availableStyles: App.getAvailableStyles(),
}));
this.previewRegion.show(new Module.SidebarPreviewView());
this.showChildView('previewRegion', new Module.SidebarPreviewView());
},
updateHorizontalScroll: function() {
// Fixes the sidebar so that on narrower screens the horizontal
@ -136,15 +136,31 @@ define([
},
});
/**
* Draggable widget collection view
*/
Module.SidebarWidgetsCollectionView = Marionette.CollectionView.extend({
childView: function(item) { return item.get('widgetView'); }
});
/**
* Responsible for rendering draggable content widgets
*/
Module.SidebarWidgetsView = Marionette.CompositeView.extend({
Module.SidebarWidgetsView = Marionette.View.extend({
getTemplate: function() { return templates.sidebarContent; },
getChildView: function(model) {
return model.get('widgetView');
regions: {
widgets: '.mailpoet_region_content'
},
childViewContainer: '.mailpoet_region_content',
initialize: function(widgets) {
this.widgets = widgets;
},
onRender: function() {
this.showChildView('widgets', new Module.SidebarWidgetsCollectionView({
collection: this.widgets
}));
}
});
/**
@ -153,10 +169,11 @@ define([
Module.SidebarLayoutWidgetsView = Module.SidebarWidgetsView.extend({
getTemplate: function() { return templates.sidebarLayout; },
});
/**
* Responsible for managing global styles
*/
Module.SidebarStylesView = Marionette.LayoutView.extend({
Module.SidebarStylesView = Marionette.View.extend({
getTemplate: function() { return templates.sidebarStyles; },
behaviors: {
ColorPickerBehavior: {},
@ -199,7 +216,7 @@ define([
"change #mailpoet_background_color": _.partial(this.changeColorField, 'body.backgroundColor'),
};
},
templateHelpers: function() {
templateContext: function() {
return {
model: this.model.toJSON(),
availableStyles: this.availableStyles.toJSON(),
@ -220,7 +237,7 @@ define([
},
});
Module.SidebarPreviewView = Marionette.LayoutView.extend({
Module.SidebarPreviewView = Marionette.View.extend({
getTemplate: function() { return templates.sidebarPreview; },
events: {
'click .mailpoet_show_preview': 'showPreview',
@ -243,6 +260,7 @@ define([
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'showPreview',
data: json,
@ -318,14 +336,14 @@ define([
},
});
Module.NewsletterPreviewView = Marionette.ItemView.extend({
Module.NewsletterPreviewView = Marionette.View.extend({
getTemplate: function() { return templates.newsletterPreview; },
initialize: function(options) {
this.previewUrl = options.previewUrl;
this.width = App.getConfig().get('newsletterPreview.width');
this.height = App.getConfig().get('newsletterPreview.height')
},
templateHelpers: function() {
templateContext: function() {
return {
previewUrl: this.previewUrl,
width: this.width,
@ -334,18 +352,18 @@ define([
}
});
App.on('before:start', function(options) {
App.on('before:start', function(App, options) {
App.registerWidget = Module.registerWidget;
App.getWidgets = Module.getWidgets;
App.registerLayoutWidget = Module.registerLayoutWidget;
App.getLayoutWidgets = Module.getLayoutWidgets;
});
App.on('start', function(options) {
App.on('start', function(App, options) {
var stylesModel = App.getGlobalStyles(),
sidebarView = new SidebarView();
App._appView.sidebarRegion.show(sidebarView);
App._appView.showChildView('sidebarRegion', sidebarView);
});
return Module;

View File

@ -46,11 +46,14 @@ define([
},
});
Module.StylesView = Marionette.ItemView.extend({
Module.StylesView = Marionette.View.extend({
getTemplate: function() { return templates.styles; },
modelEvents: {
'change': 'render',
},
serializeData: function() {
return this.model.toJSON();
}
});
Module._globalStyles = new SuperModel();
@ -65,7 +68,7 @@ define([
return App.getConfig().get('availableStyles');
};
App.on('before:start', function(options) {
App.on('before:start', function(App, options) {
// Expose style methods to global application
App.getGlobalStyles = Module.getGlobalStyles;
App.setGlobalStyles = Module.setGlobalStyles;
@ -76,9 +79,9 @@ define([
this.setGlobalStyles(globalStyles);
});
App.on('start', function(options) {
App.on('start', function(App, options) {
var stylesView = new Module.StylesView({ model: App.getGlobalStyles() });
App._appView.stylesRegion.show(stylesView);
App._appView.showChildView('stylesRegion', stylesView);
});
return Module;

View File

@ -11,13 +11,13 @@
/*jshint unused:false */
/*global tinymce:true */
tinymce.PluginManager.add('mailpoet_shortcodes', function(editor, url) {
var appendLabelAndClose = function(text) {
editor.insertContent('[' + text + ']');
var appendLabelAndClose = function(shortcode) {
editor.insertContent(shortcode);
editor.windowManager.close();
},
generateOnClickFunc = function(id) {
generateOnClickFunc = function(shortcode) {
return function() {
appendLabelAndClose(id);
appendLabelAndClose(shortcode);
};
};

View File

@ -0,0 +1,37 @@
import React from 'react'
import classNames from 'classnames'
import ReactTooltip from 'react-tooltip'
class Badge extends React.Component {
render() {
const badgeClasses = classNames(
'mailpoet_badge',
this.props.type ? `mailpoet_badge_${this.props.type}` : ''
);
const tooltip = this.props.tooltip ? this.props.tooltip.replace(/\n/g, '<br />') : false;
// tooltip ID must be unique, defaults to tooltip text
const tooltipId = this.props.tooltipId || tooltip;
return (
<span>
<span
className={badgeClasses}
data-tip={tooltip}
data-for={tooltipId}
>
{this.props.name}
</span>
{ tooltip && (
<ReactTooltip
place="right"
multiline={true}
id={tooltipId}
/>
) }
</span>
);
}
}
export default Badge;

View File

@ -0,0 +1,105 @@
import MailPoet from 'mailpoet'
import React from 'react'
import Badge from './badge.jsx'
const badges = {
excellent: {
name: MailPoet.I18n.t('excellentBadgeName'),
tooltipTitle: MailPoet.I18n.t('excellentBadgeTooltip')
},
good: {
name: MailPoet.I18n.t('goodBadgeName'),
tooltipTitle: MailPoet.I18n.t('goodBadgeTooltip')
},
bad: {
name: MailPoet.I18n.t('badBadgeName'),
tooltipTitle: MailPoet.I18n.t('badBadgeTooltip')
}
};
const stats = {
opened: {
badgeRanges: [30, 10, 0],
badgeTypes: [
'excellent',
'good',
'bad'
],
tooltipText: MailPoet.I18n.t('openedStatTooltip'),
},
clicked: {
badgeRanges: [3, 1, 0],
badgeTypes: [
'excellent',
'good',
'bad'
],
tooltipText: MailPoet.I18n.t('clickedStatTooltip')
},
unsubscribed: {
badgeRanges: [3, 1, 0],
badgeTypes: [
'bad',
'good',
'excellent'
],
tooltipText: MailPoet.I18n.t('unsubscribedStatTooltip')
},
};
class StatsBadge extends React.Component {
getBadgeType(stat, rate) {
const len = stat.badgeRanges.length;
for (var i = 0; i < len; i++) {
if (rate > stat.badgeRanges[i]) {
return stat.badgeTypes[i];
}
}
// rate must be zero at this point
return stat.badgeTypes[len - 1];
}
render() {
const stat = stats[this.props.stat] || null;
if (!stat) {
return null;
}
const rate = this.props.rate;
if (rate < 0 || rate > 100) {
return null;
}
const badgeType = this.getBadgeType(stat, rate);
const badge = badges[badgeType] || null;
if (!badge) {
return null;
}
const tooltipText = `${badge.tooltipTitle}\n\n${stat.tooltipText}`;
const tooltipId = this.props.tooltipId || null;
const content = (
<Badge
type={badgeType}
name={badge.name}
tooltip={tooltipText}
tooltipId={tooltipId}
/>
);
if (this.props.headline) {
return (
<div>
<span className={`mailpoet_stat_${badgeType}`}>
{this.props.headline}
</span> {content}
</div>
);
}
return content;
}
}
export default StatsBadge;

View File

@ -1,13 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ReactStringReplace from 'react-string-replace'
import { Link } from 'react-router'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
import moment from 'moment'
import jQuery from 'jquery'
import Hooks from 'wp-js-hooks'
import StatsBadge from 'newsletters/badges/stats.jsx'
const _QueueMixin = {
pauseSending: function(newsletter) {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'sendingQueue',
action: 'pause',
data: {
@ -27,6 +32,7 @@ const _QueueMixin = {
},
resumeSending: function(newsletter) {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'sendingQueue',
action: 'resume',
data: {
@ -138,40 +144,162 @@ const _QueueMixin = {
};
const _StatisticsMixin = {
renderStatistics: function(newsletter) {
if (
newsletter.statistics
&& newsletter.queue
&& newsletter.queue.status !== 'scheduled'
) {
const total_sent = ~~(newsletter.queue.count_processed);
let percentage_clicked = 0;
let percentage_opened = 0;
let percentage_unsubscribed = 0;
if (total_sent > 0) {
percentage_clicked = Math.round(
(~~(newsletter.statistics.clicked) * 100) / total_sent
);
percentage_opened = Math.round(
(~~(newsletter.statistics.opened) * 100) / total_sent
);
percentage_unsubscribed = Math.round(
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
);
}
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
renderStatistics: function(newsletter, is_sent, current_time) {
if (is_sent === undefined) {
// condition for standard and post notification listings
is_sent = newsletter.statistics
&& newsletter.queue
&& newsletter.queue.status !== 'scheduled'
}
if (!is_sent) {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
let params = {};
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
// welcome emails provide explicit total_sent value
const total_sent = ~~(newsletter.total_sent || newsletter.queue.count_processed);
let percentage_clicked = 0;
let percentage_opened = 0;
let percentage_unsubscribed = 0;
if (total_sent > 0) {
percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent;
percentage_opened = (newsletter.statistics.opened * 100) / total_sent;
percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent;
}
// format to 1 decimal place
const percentage_clicked_display = MailPoet.Num.toLocaleFixed(percentage_clicked, 1);
const percentage_opened_display = MailPoet.Num.toLocaleFixed(percentage_opened, 1);
const percentage_unsubscribed_display = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1);
let show_stats_timeout,
newsletter_date,
sent_hours_ago,
too_early_for_stats,
show_kb_link;
if (current_time !== undefined) {
// standard emails and post notifications:
// display green box for newsletters that were just sent
show_stats_timeout = 6; // in hours
newsletter_date = newsletter.queue.scheduled_at || newsletter.queue.created_at;
sent_hours_ago = moment(current_time).diff(moment(newsletter_date), 'hours');
too_early_for_stats = sent_hours_ago < show_stats_timeout;
show_kb_link = true;
} else {
// welcome emails: no green box and KB link
too_early_for_stats = false;
show_kb_link = false;
}
const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/191-how-to-improve-my-open-and-click-rates';
// thresholds to display badges
const min_newsletters_sent = 20;
const min_newsletter_opens = 5;
let content;
if (total_sent >= min_newsletters_sent
&& newsletter.statistics.opened >= min_newsletter_opens
&& !too_early_for_stats
) {
// display stats with badges
content = (
<div className="mailpoet_stats_text">
<div>
<span>{ percentage_opened_display }% </span>
<StatsBadge
stat="opened"
rate={percentage_opened}
tooltipId={`opened-${newsletter.id}`}
/>
</div>
<div>
<span>{ percentage_clicked_display }% </span>
<StatsBadge
stat="clicked"
rate={percentage_clicked}
tooltipId={`clicked-${newsletter.id}`}
/>
</div>
<div>
<span className="mailpoet_stat_hidden">{ percentage_unsubscribed_display }%</span>
</div>
</div>
);
} else {
// display simple stats
content = (
<div>
<span className="mailpoet_stats_text">
{ percentage_opened_display }%,
{ " " }
{ percentage_clicked_display }%
<span className="mailpoet_stat_hidden">
, { percentage_unsubscribed_display }%
</span>
</span>
{ too_early_for_stats && (
<div className="mailpoet_badge mailpoet_badge_green">
{MailPoet.I18n.t('checkBackInHours')
.replace('%$1d', show_stats_timeout - sent_hours_ago)}
</div>
) }
</div>
);
}
// thresholds to display bad open rate help
const max_percentage_opened = 5;
const min_sent_hours_ago = 24;
const min_total_sent = 10;
let after_content;
if (show_kb_link
&& percentage_opened < max_percentage_opened
&& sent_hours_ago >= min_sent_hours_ago
&& total_sent >= min_total_sent
) {
// help link for bad open rate
after_content = (
<div>
<a
href={improveStatsKBLink}
target="_blank"
className="mailpoet_stat_link_small"
>
{MailPoet.I18n.t('improveThisLinkText')}
</a>
</div>
);
}
if (total_sent > 0 && params.link) {
// wrap content in a link
return (
<div>
<Link
key={ `stats-${newsletter.id}` }
to={ params.link }
>
{content}
</Link>
{after_content}
</div>
);
}
return (
<div>
{content}
{after_content}
</div>
);
}
}
@ -225,6 +353,7 @@ const _MailerMixin = {
},
resumeMailerSending() {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'mailer',
action: 'resumeSending'
}).done(function() {

View File

@ -129,6 +129,7 @@ const newsletter_actions = [
label: MailPoet.I18n.t('duplicate'),
onClick: function(newsletter, refresh) {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'duplicate',
data: {
@ -164,6 +165,7 @@ const NewsletterListNotification = React.createClass({
e.persist();
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'setStatus',
data: {

View File

@ -3,6 +3,7 @@ import { Router, Link } from 'react-router'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import Hooks from 'wp-js-hooks'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
@ -40,7 +41,7 @@ const columns = [
}
];
const newsletter_actions = [
let newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
@ -53,8 +54,15 @@ const newsletter_actions = [
}
];
newsletter_actions = Hooks.applyFilters('mailpoet_newsletters_listings_notification_history_actions', newsletter_actions);
const NewsletterListNotificationHistory = React.createClass({
mixins: [ QueueMixin, StatisticsMixin, MailerMixin ],
renderSentDate: function(newsletter) {
return (newsletter.queue.status === 'completed')
? ( <abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr> )
: MailPoet.I18n.t('notSentYet')
},
renderItem: function(newsletter, actions, meta) {
const rowClasses = classNames(
'manage-column',
@ -75,7 +83,7 @@ const NewsletterListNotificationHistory = React.createClass({
<a
href={ newsletter.preview_url }
target="_blank"
>{ newsletter.queue.newsletter_rendered_subject }</a>
>{ newsletter.queue.newsletter_rendered_subject || newsletter.subject }</a>
</strong>
{ actions }
</td>
@ -87,11 +95,11 @@ const NewsletterListNotificationHistory = React.createClass({
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
{ this.renderStatistics(newsletter, undefined, meta.current_time) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
{ this.renderSentDate(newsletter) }
</td>
</div>
);

View File

@ -3,6 +3,7 @@ import { Router, Link } from 'react-router'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import Hooks from 'wp-js-hooks'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
@ -98,7 +99,7 @@ const bulk_actions = [
}
];
const newsletter_actions = [
let newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
@ -124,6 +125,7 @@ const newsletter_actions = [
label: MailPoet.I18n.t('duplicate'),
onClick: function(newsletter, refresh) {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'duplicate',
data: {
@ -151,6 +153,8 @@ const newsletter_actions = [
}
];
newsletter_actions = Hooks.applyFilters('mailpoet_newsletters_listings_standard_actions', newsletter_actions);
const NewsletterListStandard = React.createClass({
mixins: [ QueueMixin, StatisticsMixin, MailerMixin ],
renderItem: function(newsletter, actions, meta) {
@ -183,7 +187,7 @@ const NewsletterListStandard = React.createClass({
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
{ this.renderStatistics(newsletter, undefined, meta.current_time) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>

View File

@ -5,12 +5,13 @@ import { createHashHistory } from 'history'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import { MailerMixin } from 'newsletters/listings/mixins.jsx'
import { StatisticsMixin, MailerMixin } from 'newsletters/listings/mixins.jsx'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import _ from 'underscore'
import Hooks from 'wp-js-hooks'
const mailpoet_roles = window.mailpoet_roles || {};
const mailpoet_segments = window.mailpoet_segments || {};
@ -100,7 +101,7 @@ const bulk_actions = [
}
];
const newsletter_actions = [
let newsletter_actions = [
{
name: 'view',
link: function(newsletter) {
@ -121,46 +122,22 @@ const newsletter_actions = [
);
}
},
{
name: 'duplicate',
label: MailPoet.I18n.t('duplicate'),
onClick: function(newsletter, refresh) {
return MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'duplicate',
data: {
id: newsletter.id
}
}).done((response) => {
MailPoet.Notice.success(
(MailPoet.I18n.t('newsletterDuplicated')).replace(
'%$1s', response.data.subject
)
);
refresh();
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
{ scroll: true }
);
}
});
}
},
{
name: 'trash'
}
];
newsletter_actions = Hooks.applyFilters('mailpoet_newsletters_listings_welcome_notification_actions', newsletter_actions);
const NewsletterListWelcome = React.createClass({
mixins: [ MailerMixin ],
mixins: [ StatisticsMixin, MailerMixin ],
updateStatus: function(e) {
// make the event persist so that we can still override the selected value
// in the ajax callback
e.persist();
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'setStatus',
data: {
@ -274,35 +251,6 @@ const NewsletterListWelcome = React.createClass({
</span>
);
},
renderStatistics: function(newsletter) {
if (mailpoet_tracking_enabled === false) {
return;
}
if (newsletter.total_sent > 0 && newsletter.statistics) {
const total_sent = ~~(newsletter.total_sent);
const percentage_clicked = Math.round(
(~~(newsletter.statistics.clicked) * 100) / total_sent
);
const percentage_opened = Math.round(
(~~(newsletter.statistics.opened) * 100) / total_sent
);
const percentage_unsubscribed = Math.round(
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
);
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
},
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
@ -329,7 +277,10 @@ const NewsletterListWelcome = React.createClass({
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
{ this.renderStatistics(
newsletter,
newsletter.total_sent > 0 && newsletter.statistics
) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>

View File

@ -2,13 +2,13 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import Hooks from 'wp-js-hooks'
import NewsletterTypes from 'newsletters/types.jsx'
import NewsletterTemplates from 'newsletters/templates.jsx'
import NewsletterSend from 'newsletters/send.jsx'
import NewsletterTypeStandard from 'newsletters/types/standard.jsx'
import NewsletterTypeWelcome from 'newsletters/types/welcome/welcome.jsx'
import NewsletterTypeNotification from 'newsletters/types/notification/notification.jsx'
import NewsletterListStandard from 'newsletters/listings/standard.jsx'
@ -27,6 +27,9 @@ const App = React.createClass({
const container = document.getElementById('newsletters_container');
if(container) {
let extra_routes = [];
extra_routes = Hooks.applyFilters('mailpoet_newsletters_before_router', extra_routes);
const mailpoet_listing = ReactDOM.render((
<Router history={ history }>
<Route path="/" component={ App }>
@ -40,12 +43,13 @@ if(container) {
<Route path="new" component={ NewsletterTypes } />
{/* New newsletter: types */}
<Route path="new/standard" component={ NewsletterTypeStandard } />
<Route path="new/welcome" component={ NewsletterTypeWelcome } />
<Route path="new/notification" component={ NewsletterTypeNotification } />
{/* Template selection */}
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
{/* Sending options */}
<Route path="send/:id" component={ NewsletterSend } />
{/* Extra routes */}
{ extra_routes.map(rt => <Route key={rt.path} path={rt.path} component={rt.component} />) }
</Route>
</Router>
), container);

View File

@ -64,6 +64,7 @@ define(
this.setState({ loading: true });
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'get',
data: {
@ -97,6 +98,7 @@ define(
case 'notification':
case 'welcome':
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'setStatus',
data: {
@ -120,6 +122,7 @@ define(
}).fail(this._showError);
default:
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'sendingQueue',
action: 'add',
data: {
@ -184,6 +187,7 @@ define(
);
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'save',
data: newsletterData,

View File

@ -1,11 +1,13 @@
define(
[
'mailpoet',
'wp-js-hooks',
'newsletters/types/notification/scheduling.jsx',
'underscore'
],
function(
MailPoet,
Hooks,
Scheduling,
_
) {
@ -35,8 +37,9 @@ define(
tip: MailPoet.I18n.t('segmentsTip'),
type: 'selection',
placeholder: MailPoet.I18n.t('selectSegmentPlaceholder'),
id: "mailpoet_segments",
endpoint: "segments",
id: 'mailpoet_segments',
api_version: window.mailpoet_api_version,
endpoint: 'segments',
multiple: true,
filter: function(segment) {
return !!(!segment.deleted_at);
@ -101,6 +104,8 @@ define(
}
];
fields = Hooks.applyFilters('mailpoet_newsletters_3rd_step_fields', fields);
return {
getFields: function(newsletter) {
return fields;

View File

@ -4,6 +4,7 @@ define(
'jquery',
'underscore',
'mailpoet',
'wp-js-hooks',
'form/fields/checkbox.jsx',
'form/fields/select.jsx',
'form/fields/text.jsx',
@ -13,6 +14,7 @@ define(
jQuery,
_,
MailPoet,
Hooks,
Checkbox,
Select,
Text
@ -339,8 +341,9 @@ define(
tip: MailPoet.I18n.t('segmentsTip'),
type: 'selection',
placeholder: MailPoet.I18n.t('selectSegmentPlaceholder'),
id: "mailpoet_segments",
endpoint: "segments",
id: 'mailpoet_segments',
api_version: window.mailpoet_api_version,
endpoint: 'segments',
multiple: true,
filter: function(segment) {
return !!(!segment.deleted_at);
@ -411,6 +414,8 @@ define(
}
];
fields = Hooks.applyFilters('mailpoet_newsletters_3rd_step_fields', fields);
return {
getFields: function(newsletter) {
return fields;

View File

@ -1,10 +1,12 @@
define(
[
'mailpoet',
'wp-js-hooks',
'newsletters/types/welcome/scheduling.jsx'
],
function(
MailPoet,
Hooks,
Scheduling
) {
@ -71,6 +73,8 @@ define(
}
];
fields = Hooks.applyFilters('mailpoet_newsletters_3rd_step_fields', fields);
return {
getFields: function(newsletter) {
return fields;
@ -83,4 +87,3 @@ define(
};
}
);

View File

@ -27,6 +27,7 @@ define(
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletterTemplates',
action: 'save',
data: template
@ -97,6 +98,7 @@ define(
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletterTemplates',
action: 'getAll',
}).always(() => {
@ -130,6 +132,7 @@ define(
}
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'save',
data: {
@ -158,6 +161,7 @@ define(
)
) {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletterTemplates',
action: 'delete',
data: {

View File

@ -2,12 +2,14 @@ define(
[
'react',
'mailpoet',
'wp-js-hooks',
'react-router',
'newsletters/breadcrumb.jsx'
],
function(
React,
MailPoet,
Hooks,
Router,
Breadcrumb
) {
@ -22,6 +24,7 @@ define(
},
createNewsletter: function(type) {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'create',
data: {
@ -40,6 +43,49 @@ define(
});
},
render: function() {
var types = [
{
'id': 'standard',
'title': MailPoet.I18n.t('regularNewsletterTypeTitle'),
'description': MailPoet.I18n.t('regularNewsletterTypeDescription'),
'action': function() {
return (
<a className="button button-primary" onClick={ this.createNewsletter.bind(null, 'standard') }>
{MailPoet.I18n.t('create')}
</a>
)
}.bind(this)()
},
{
'id': 'welcome',
'title': MailPoet.I18n.t('welcomeNewsletterTypeTitle'),
'description': MailPoet.I18n.t('welcomeNewsletterTypeDescription'),
'action': function() {
return (
<div>
<a href="?page=mailpoet-premium" target="_blank">
{MailPoet.I18n.t('getPremiumVersion')}
</a>
</div>
)
}()
},
{
'id': 'notification',
'title': MailPoet.I18n.t('postNotificationNewsletterTypeTitle'),
'description': MailPoet.I18n.t('postNotificationNewsletterTypeDescription'),
'action': function() {
return (
<a className="button button-primary" onClick={ this.setupNewsletter.bind(null, 'notification') }>
{MailPoet.I18n.t('setUp')}
</a>
)
}.bind(this)()
}
];
types = Hooks.applyFilters('mailpoet_newsletters_types', types, this);
return (
<div>
<h1>{MailPoet.I18n.t('pickCampaignType')}</h1>
@ -47,65 +93,24 @@ define(
<Breadcrumb step="type" />
<ul className="mailpoet_boxes clearfix">
<li data-type="standard">
<div className="mailpoet_thumbnail"></div>
{types.map(function(type, index) {
return (
<li key={index} data-type={type.id}>
<div>
<div className="mailpoet_thumbnail"></div>
<div className="mailpoet_description">
<h3>{MailPoet.I18n.t('regularNewsletterTypeTitle')}</h3>
<p>
{MailPoet.I18n.t('regularNewsletterTypeDescription')}
</p>
</div>
<div className="mailpoet_description">
<h3>{type.title}</h3>
<p>{type.description}</p>
</div>
<div className="mailpoet_actions">
<a
className="button button-primary"
onClick={ this.createNewsletter.bind(null, 'standard') }
>
{MailPoet.I18n.t('create')}
</a>
</div>
</li>
<li data-type="welcome">
<div className="mailpoet_thumbnail"></div>
<div className="mailpoet_description">
<h3>{MailPoet.I18n.t('welcomeNewsletterTypeTitle')}</h3>
<p>
{MailPoet.I18n.t('welcomeNewsletterTypeDescription')}
</p>
</div>
<div className="mailpoet_actions">
<a
className="button button-primary"
onClick={ this.setupNewsletter.bind(null, 'welcome') }
>
{MailPoet.I18n.t('setUp')}
</a>
</div>
</li>
<li data-type="notification">
<div className="mailpoet_thumbnail"></div>
<div className="mailpoet_description">
<h3>{MailPoet.I18n.t('postNotificationNewsletterTypeTitle')}</h3>
<p>
{MailPoet.I18n.t('postNotificationsNewsletterTypeDescription')}
</p>
</div>
<div className="mailpoet_actions">
<a
className="button button-primary"
onClick={ this.setupNewsletter.bind(null, 'notification') }
>
{MailPoet.I18n.t('setUp')}
</a>
</div>
</li>
<div className="mailpoet_actions">
{type.action}
</div>
</div>
</li>
)
}, this)}
</ul>
</div>
);

View File

@ -44,6 +44,7 @@ define(
},
handleNext: function() {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'create',
data: _.extend({}, this.state, {

View File

@ -22,6 +22,7 @@ define(
componentDidMount: function() {
// No options for this type, create a newsletter upon mounting
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'create',
data: {

View File

@ -104,6 +104,7 @@ const WelcomeScheduling = React.createClass({
},
handleNext: function() {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'newsletters',
action: 'create',
data: {

View File

@ -1,102 +0,0 @@
define(
[
'underscore',
'react',
'react-router',
'mailpoet',
'newsletters/types/welcome/scheduling.jsx',
'newsletters/breadcrumb.jsx'
],
function(
_,
React,
Router,
MailPoet,
Scheduling,
Breadcrumb
) {
var field = {
name: 'options',
label: 'Event',
type: 'reactComponent',
component: Scheduling,
};
var availableSegments = window.mailpoet_segments || {},
defaultSegment = 1;
if (_.size(availableSegments) > 0) {
defaultSegment = _.first(availableSegments).id;
}
var NewsletterWelcome = React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
options: {
event: 'segment',
segment: defaultSegment,
role: 'subscriber',
afterTimeNumber: 1,
afterTimeType: 'immediate',
}
};
},
handleValueChange: function(event) {
var state = this.state;
state[event.target.name] = event.target.value;
this.setState(state);
},
handleNext: function() {
MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'create',
data: _.extend({}, this.state, {
type: 'welcome',
subject: MailPoet.I18n.t('draftNewsletterTitle'),
})
}).done((response) => {
this.showTemplateSelection(response.data.id);
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
{ scroll: true }
);
}
});
},
showTemplateSelection: function(newsletterId) {
this.context.router.push(`/template/${newsletterId}`);
},
render: function() {
return (
<div>
<h1>{MailPoet.I18n.t('welcomeNewsletterTypeTitle')}</h1>
<Breadcrumb step="type" />
<h3>{MailPoet.I18n.t('selectEventToSendWelcomeEmail')}</h3>
<Scheduling
item={this.state}
field={field}
onValueChange={this.handleValueChange} />
<p className="submit">
<input
className="button button-primary"
type="button"
onClick={ this.handleNext }
value={MailPoet.I18n.t('next')} />
</p>
</div>
);
},
});
return NewsletterWelcome;
}
);

View File

@ -102,22 +102,11 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
'setMessage', this.options.message
);
},
isHTML: function(str) {
var a = document.createElement('div');
a.innerHTML = str;
for (var c = a.childNodes, i = c.length; i--;) {
if (c[i].nodeType == 1) return true;
}
return false;
},
setMessage: function(message) {
message = this.formatMessage(message);
// if it's not an html message
// let's sugar coat the message with a fancy <p>
if (this.isHTML(message) === false) {
message = '<p>'+message+'</p>';
}
message = '<p>'+message+'</p>';
// set message
return this.element.html(message);
},
@ -153,13 +142,13 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// set class name
switch (this.options.type) {
case 'success':
this.element.addClass('updated');
this.element.addClass('notice notice-success');
break;
case 'system':
this.element.addClass('update-nag');
this.element.addClass('notice notice-warning');
break;
case 'error':
this.element.addClass('error');
this.element.addClass('notice notice-error');
break;
}
@ -199,7 +188,7 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// single id
jQuery('[data-id="' + all + '"]').trigger('close');
} else {
jQuery('.mailpoet_notice.updated:not([id]), .mailpoet_notice.error:not([id])')
jQuery('.mailpoet_notice.notice-success:not([id]), .mailpoet_notice.notice-error:not([id])')
.trigger('close');
}
},

21
assets/js/src/num.js Normal file
View File

@ -0,0 +1,21 @@
define('num',
[
'mailpoet'
], function(
MailPoet
) {
'use strict';
MailPoet.Num = {
toLocaleFixed: function (num, precision) {
precision = precision || 0;
var factor = Math.pow(10, precision);
return (Math.round(num * factor) / factor)
.toLocaleString(
undefined,
{minimumFractionDigits: precision, maximumFractionDigits: precision}
);
}
};
});

View File

@ -31,7 +31,7 @@ function(
});
form.parsley().on('form:submit', function(parsley) {
var data = form.serializeObject() || {};
var form_data = form.serializeObject() || {};
// check if we're on the same domain
if(isSameDomain(MailPoetForm.ajax_url) === false) {
// non ajax post request
@ -40,10 +40,11 @@ function(
// ajax request
MailPoet.Ajax.post({
url: MailPoetForm.ajax_url,
token: data.token,
token: form_data.token,
api_version: form_data.api_version,
endpoint: 'subscribers',
action: 'subscribe',
data: data
data: form_data.data
}).fail(function(response) {
form.find('.mailpoet_validate_error').html(
response.errors.map(function(error) {

View File

@ -112,6 +112,7 @@ const item_actions = [
label: MailPoet.I18n.t('duplicate'),
onClick: (item, refresh) => {
return MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'segments',
action: 'duplicate',
data: {
@ -153,6 +154,7 @@ const item_actions = [
onClick: function(item, refresh) {
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'segments',
action: 'synchronize'
}).done(function(response) {

View File

@ -15,6 +15,7 @@ define(
MailPoet.Router = new (Backbone.Router.extend({
routes: {
'': 'sendingMethodGroup', // the default tab is currently mta, needs its own method
'mta(/:group)': 'sendingMethodGroup',
'(:tab)': 'tabs',
},
@ -32,7 +33,7 @@ define(
if(group === null) {
// show sending methods
jQuery('.mailpoet_sending_methods').fadeIn();
jQuery('.mailpoet_sending_methods, .mailpoet_sending_methods_help').fadeIn();
} else {
// toggle SPF (hidden if the sending method is MailPoet)
jQuery('#mailpoet_mta_spf')[
@ -42,7 +43,7 @@ define(
]();
// hide sending methods
jQuery('.mailpoet_sending_methods').hide();
jQuery('.mailpoet_sending_methods, .mailpoet_sending_methods_help').hide();
// display selected sending method's settings
jQuery('.mailpoet_sending_method[data-group="'+ group +'"]').show();
@ -51,7 +52,7 @@ define(
},
tabs: function(tab, section) {
// set default tab
tab = tab || 'basics';
tab = tab || 'mta';
// reset all active tabs
jQuery('.nav-tab-wrapper a').removeClass('nav-tab-active');

View File

@ -60,7 +60,8 @@ define(
label: MailPoet.I18n.t('lists'),
type: 'selection',
placeholder: MailPoet.I18n.t('selectList'),
endpoint: "segments",
api_version: window.mailpoet_api_version,
endpoint: 'segments',
multiple: true,
selected: function(subscriber) {
if (Array.isArray(subscriber.subscriptions) === false) {

View File

@ -138,6 +138,7 @@ define(
}
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'ImportExport',
action: 'processExport',
data: JSON.stringify({

View File

@ -190,6 +190,7 @@ define(
mailChimpKeyVerifyButtonElement.click(function () {
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'importExport',
action: 'getMailChimpLists',
data: {
@ -225,6 +226,7 @@ define(
}
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'importExport',
action: 'getMailChimpSubscribers',
data: {
@ -321,6 +323,10 @@ define(
// is the email in 'mailto:email' format?
email = test[1].trim();
}
// test for valid characters using WP's rule (https://core.trac.wordpress.org/browser/tags/4.7.3/src/wp-includes/formatting.php#L2902)
if (!/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.\-@]+$/.test(email) ) {
return false;
}
return email;
};
@ -543,7 +549,7 @@ define(
MailPoet.Notice.error(MailPoet.I18n.t('segmentSelectionRequired'), {
static: true,
scroll: true,
id: 'segmentSelection',
id: 'notice_segmentSelection',
hideClose: true
});
}
@ -572,6 +578,7 @@ define(
var segmentDescription = jQuery('#new_segment_description').val().trim();
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'ImportExport',
action: 'addSegment',
data: {
@ -633,12 +640,12 @@ define(
columnId = 'email';
} else if (subscribers.header) {
var headerName = subscribers.header[i],
header_name_match = mailpoetColumns.map(function (el) {
return el.id;
headerNameMatch = mailpoetColumns.map(function (el) {
return el.name;
}).indexOf(headerName);
// set column type using header
if (header_name_match !== -1) {
columnId = headerName;
if (headerNameMatch !== -1) {
columnId = mailpoetColumns[headerNameMatch].id;
}// set column type using header name
else if (headerName) {
if (/first|first name|given name/i.test(headerName)) {
@ -648,11 +655,6 @@ define(
} else if (/status/i.test(headerName)) {
columnId = 'status';
}
/*else if (/subscribed|subscription/i.test(headerName)) {
columnId = 'confirmed_at';
} else if (/ip/i.test(headerName)) {
columnId = 'confirmed_ip';
}*/
}
}
// make sure the column id has not been previously selected
@ -734,6 +736,7 @@ define(
// save custom field
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'customFields',
action: 'save',
data: data
@ -995,6 +998,7 @@ define(
queue.add(function(queue) {
queue.pause();
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'ImportExport',
action: 'processImport',
data: JSON.stringify({
@ -1005,8 +1009,8 @@ define(
updateSubscribers: (jQuery(':radio[name="subscriber_update_option"]:checked').val() === 'yes') ? true : false
})
}).done(function(response) {
importResults.created = response.data.created;
importResults.updated = response.data.updated;
importResults.created += response.data.created;
importResults.updated += response.data.updated;
importResults.segments = response.data.segments;
importResults.added_to_segment_with_welcome_notification = response.data.added_to_segment_with_welcome_notification;
queue.run();

View File

@ -81,6 +81,22 @@ const messages = {
).replace('%$1d', count.toLocaleString());
}
MailPoet.Notice.success(message);
},
onNoItemsFound: (group) => {
if (group === 'bounced' && !mailpoet_premium_active) {
return (
<div>
<p>{MailPoet.I18n.t('bouncedSubscribersHelp')}</p>
<p>
<a href={ `admin.php?page=mailpoet-premium` } className="button-primary">
{MailPoet.I18n.t('bouncedSubscribersPremiumButtonText')}
</a>
</p>
</div>
);
}
// use default message
return false;
}
};
@ -91,6 +107,7 @@ const bulk_actions = [
onSelect: function() {
let field = {
id: 'move_to_segment',
api_version: window.mailpoet_api_version,
endpoint: 'segments',
filter: function(segment) {
return !!(
@ -122,6 +139,7 @@ const bulk_actions = [
onSelect: function() {
let field = {
id: 'add_to_segment',
api_version: window.mailpoet_api_version,
endpoint: 'segments',
filter: function(segment) {
return !!(
@ -153,6 +171,7 @@ const bulk_actions = [
onSelect: function() {
let field = {
id: 'remove_from_segment',
api_version: window.mailpoet_api_version,
endpoint: 'segments',
filter: function(segment) {
return !!(
@ -255,7 +274,7 @@ const SubscriberList = React.createClass({
case 'unsubscribed':
status = MailPoet.I18n.t('unsubscribed');
break;
case 'bounced':
status = MailPoet.I18n.t('bounced');
break;

View File

@ -35,6 +35,7 @@ cp -Rf vendor $plugin_name
cp -Rf views $plugin_name
rm -Rf $plugin_name/assets/css/src
rm -Rf $plugin_name/assets/js/src
rm -Rf $plugin_name/lang/*.po
# Remove extra files (docs, examples,...) from 3rd party extensions
echo '[BUILD] Removing obsolete files from vendor libraries'

View File

@ -15,7 +15,8 @@
"mtdowling/cron-expression": "^1.1",
"nesbot/carbon": "^1.21",
"soundasleep/html2text": "^0.3.4",
"sabberworm/php-css-parser": "^8.1"
"sabberworm/php-css-parser": "^8.1",
"symfony/polyfill-xml": "^1.3"
},
"require-dev": {
"codeception/codeception": "^2.2.9",
@ -36,7 +37,7 @@
}
},
"scripts": {
"post-update-cmd": "rm -rf vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility; cp -rpd vendor/wimg/php-compatibility vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility",
"post-install-cmd": "rm -rf vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility; cp -rpd vendor/wimg/php-compatibility vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility"
"post-update-cmd": "rm -rf vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility; cp -rp vendor/wimg/php-compatibility vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility",
"post-install-cmd": "rm -rf vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility; cp -rp vendor/wimg/php-compatibility vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/PHPCompatibility"
}
}

70
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "236065951cf739888fa21e0b8653c5fd",
"content-hash": "c8c2fdb08b7ab624890703dde640c5d1",
"packages": [
{
"name": "cerdic/css-tidy",
@ -306,7 +306,7 @@
"version": "0.3.4",
"source": {
"type": "git",
"url": "git@github.com:mailpoet/html2text.git",
"url": "https://github.com/mailpoet/html2text.git",
"reference": "a1f77b8f340c8425b746bef1d1040189e89be334"
},
"dist": {
@ -349,7 +349,8 @@
"text"
],
"support": {
"email": "support@jevon.org"
"email": "support@jevon.org",
"source": "https://github.com/mailpoet/html2text/tree/0.3.4"
},
"time": "2016-06-09T04:56:16+00:00"
},
@ -466,6 +467,64 @@
],
"time": "2016-11-14T01:06:16+00:00"
},
{
"name": "symfony/polyfill-xml",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-xml.git",
"reference": "64b6a864f18ab4fddad49f5025f805f6781dfabd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-xml/zipball/64b6a864f18ab4fddad49f5025f805f6781dfabd",
"reference": "64b6a864f18ab4fddad49f5025f805f6781dfabd",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-xml": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Xml\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for xml's utf8_encode and utf8_decode functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"time": "2016-11-14T01:06:16+00:00"
},
{
"name": "symfony/translation",
"version": "v2.8.18",
@ -571,7 +630,7 @@
{
"name": "Todd Burry",
"email": "todd@vanillaforums.com",
"role": "Developer"
"role": "developer"
}
],
"description": "A jQuery like html dom parser written in php.",
@ -2330,6 +2389,7 @@
"kint",
"php"
],
"abandoned": "kint-php/kint",
"time": "2017-01-15T14:23:43+00:00"
},
{
@ -2896,7 +2956,7 @@
"typo3"
],
"abandoned": true,
"time": "2016-05-12 11:58:38"
"time": "2016-05-12T11:58:38+00:00"
},
{
"name": "squizlabs/php_codesniffer",

View File

@ -1,177 +1,18 @@
<?php
namespace MailPoet\API;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
if(!defined('ABSPATH')) exit;
class API {
private $_endpoint;
private $_method;
private $_token;
private $_endpoint_namespaces = array();
private $_endpoint_class;
private $_data = array();
function __construct() {
$this->addEndpointNamespace(__NAMESPACE__ . "\\Endpoints");
static function JSON() {
return new \MailPoet\API\JSON\API();
}
function init() {
// Admin Security token
add_action(
'admin_head',
array($this, 'setToken')
);
// ajax (logged in users)
add_action(
'wp_ajax_mailpoet',
array($this, 'setupAjax')
);
// ajax (logged out users)
add_action(
'wp_ajax_nopriv_mailpoet',
array($this, 'setupAjax')
);
}
function setupAjax() {
do_action('mailpoet_api_setup', array($this));
$this->getRequestData($_POST);
if($this->checkToken() === false) {
$error_response = new ErrorResponse(
array(
Error::UNAUTHORIZED => __('Invalid request', 'mailpoet')
),
array(),
Response::STATUS_UNAUTHORIZED
);
$error_response->send();
static function MP($version) {
$api_class = sprintf('%s\MP\%s\API', __NAMESPACE__, $version);
if(class_exists($api_class)) {
return new $api_class();
}
$response = $this->processRoute();
$response->send();
throw new \Exception(__('Invalid API version.', 'mailpoet'));
}
function getRequestData($data) {
$this->_endpoint = isset($data['endpoint'])
? Helpers::underscoreToCamelCase(trim($data['endpoint']))
: null;
$this->_method = isset($data['method'])
? Helpers::underscoreToCamelCase(trim($data['method']))
: null;
$this->_token = isset($data['token'])
? trim($data['token'])
: null;
if(!$this->_endpoint || !$this->_method) {
// throw exception bad request
$error_response = new ErrorResponse(
array(
Error::BAD_REQUEST => __('Invalid request', 'mailpoet')
),
array(),
Response::STATUS_BAD_REQUEST
);
$error_response->send();
} else {
foreach($this->_endpoint_namespaces as $namespace) {
$class_name = $namespace . "\\" . ucfirst($this->_endpoint);
if(class_exists($class_name)) {
$this->_endpoint_class = $class_name;
}
}
$this->_data = isset($data['data'])
? stripslashes_deep($data['data'])
: array();
// remove reserved keywords from data
if(is_array($this->_data) && !empty($this->_data)) {
// filter out reserved keywords from data
$reserved_keywords = array(
'token',
'endpoint',
'method',
'mailpoet_redirect'
);
$this->_data = array_diff_key(
$this->_data,
array_flip($reserved_keywords)
);
}
}
}
function processRoute() {
try {
if(empty($this->_endpoint_class)) {
throw new \Exception('Invalid endpoint');
}
$endpoint = new $this->_endpoint_class();
// check the accessibility of the requested endpoint's action
// by default, an endpoint's action is considered "private"
$permissions = $endpoint->permissions;
if(
array_key_exists($this->_method, $permissions) === false
||
$permissions[$this->_method] !== Access::ALL
) {
if($this->checkPermissions() === false) {
$error_response = new ErrorResponse(
array(
Error::FORBIDDEN => __(
'You do not have the required permissions.',
'mailpoet'
)
),
array(),
Response::STATUS_FORBIDDEN
);
return $error_response;
}
}
$response = $endpoint->{$this->_method}($this->_data);
return $response;
} catch(\Exception $e) {
$error_response = new ErrorResponse(
array($e->getCode() => $e->getMessage())
);
return $error_response;
}
}
function checkPermissions() {
return current_user_can('manage_options');
}
function checkToken() {
return wp_verify_nonce($this->_token, 'mailpoet_token');
}
function setToken() {
$global = '<script type="text/javascript">';
$global .= 'var mailpoet_token = "';
$global .= Security::generateToken();
$global .= '";';
$global .= '</script>';
echo $global;
}
function addEndpointNamespace($namespace) {
$this->_endpoint_namespaces[] = $namespace;
}
function getEndpointNamespaces() {
return $this->_endpoint_namespaces;
}
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace MailPoet\API\Endpoints;
use Carbon\Carbon;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
use MailPoet\Services\Bridge;
if(!defined('ABSPATH')) exit;
class Services extends APIEndpoint {
public $bridge;
function __construct() {
$this->bridge = new Bridge();
}
function verifyMailPoetKey($data = array()) {
$key = isset($data['key']) ? trim($data['key']) : null;
if(!$key) {
return $this->badRequest(array(
APIError::BAD_REQUEST => __('Please specify a key.', 'mailpoet')
));
}
try {
$result = $this->bridge->checkKey($key);
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
));
}
$state = !empty($result['state']) ? $result['state'] : null;
$success_message = null;
if($state == Bridge::MAILPOET_KEY_VALID) {
$success_message = __('Your MailPoet API key is valid!', 'mailpoet');
} elseif($state == Bridge::MAILPOET_KEY_EXPIRING) {
$success_message = sprintf(
__('Your MailPoet key expires on %s!', 'mailpoet'),
Carbon::createFromTimestamp(strtotime($result['data']['expire_at']))
->format('Y-m-d')
);
}
if($success_message) {
return $this->successResponse(array('message' => $success_message));
}
switch($state) {
case Bridge::MAILPOET_KEY_INVALID:
$error = __('Your MailPoet key is invalid!', 'mailpoet');
break;
default:
$code = !empty($result['code']) ? $result['code'] : Bridge::CHECK_ERROR_UNKNOWN;
$error = sprintf(
__('Error validating API key, please try again later (code: %s)', 'mailpoet'),
$code
);
break;
}
return $this->errorResponse(array(APIError::BAD_REQUEST => $error));
}
}

200
lib/API/JSON/API.php Normal file
View File

@ -0,0 +1,200 @@
<?php
namespace MailPoet\API\JSON;
use MailPoet\Config\Env;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
use MailPoet\WP\Hooks;
if(!defined('ABSPATH')) exit;
class API {
private $_request_api_version;
private $_request_endpoint;
private $_request_method;
private $_request_token;
private $_request_endpoint_class;
private $_request_data = array();
private $_endpoint_namespaces = array();
private $_available_api_versions = array(
'v1'
);
const CURRENT_VERSION = 'v1';
function __construct() {
foreach($this->_available_api_versions as $available_api_version) {
$this->addEndpointNamespace(
sprintf('%s\%s', __NAMESPACE__, $available_api_version),
$available_api_version
);
}
}
function init() {
// admin security token and API version
add_action(
'admin_head',
array($this, 'setTokenAndAPIVersion')
);
// ajax (logged in users)
add_action(
'wp_ajax_mailpoet',
array($this, 'setupAjax')
);
// ajax (logged out users)
add_action(
'wp_ajax_nopriv_mailpoet',
array($this, 'setupAjax')
);
}
function setupAjax() {
Hooks::doAction('mailpoet_api_setup', array($this));
$this->setRequestData($_POST);
if($this->checkToken() === false) {
$error_message = __('Invalid API request.', 'mailpoet');
$error_response = $this->createErrorResponse(Error::UNAUTHORIZED, $error_message, Response::STATUS_UNAUTHORIZED);
return $error_response->send();
}
$response = $this->processRoute();
$response->send();
}
function setRequestData($data) {
$this->_request_api_version = !empty($data['api_version']) ? $data['api_version']: false;
$this->_request_endpoint = isset($data['endpoint'])
? Helpers::underscoreToCamelCase(trim($data['endpoint']))
: null;
// JS part of /wp-admin/customize.php does not like a 'method' field in a form widget
$method_param_name = isset($data['mailpoet_method']) ? 'mailpoet_method' : 'method';
$this->_request_method = isset($data[$method_param_name])
? Helpers::underscoreToCamelCase(trim($data[$method_param_name]))
: null;
$this->_request_token = isset($data['token'])
? trim($data['token'])
: null;
if(!$this->_request_endpoint || !$this->_request_method || !$this->_request_api_version) {
$error_message = __('Invalid API request.', 'mailpoet');
$error_response = $this->createErrorResponse(Error::BAD_REQUEST, $error_message, Response::STATUS_BAD_REQUEST);
return $error_response;
} else if(!empty($this->_endpoint_namespaces[$this->_request_api_version])) {
foreach($this->_endpoint_namespaces[$this->_request_api_version] as $namespace) {
$endpoint_class = sprintf(
'%s\%s',
$namespace,
ucfirst($this->_request_endpoint)
);
if(class_exists($endpoint_class)) {
$this->_request_endpoint_class = $endpoint_class;
break;
}
}
$this->_request_data = isset($data['data'])
? stripslashes_deep($data['data'])
: array();
// remove reserved keywords from data
if(is_array($this->_request_data) && !empty($this->_request_data)) {
// filter out reserved keywords from data
$reserved_keywords = array(
'token',
'endpoint',
'method',
'api_version',
'mailpoet_method', // alias of 'method'
'mailpoet_redirect'
);
$this->_request_data = array_diff_key(
$this->_request_data,
array_flip($reserved_keywords)
);
}
}
}
function processRoute() {
try {
if(empty($this->_request_endpoint_class)) {
throw new \Exception(__('Invalid API endpoint.', 'mailpoet'));
}
$endpoint = new $this->_request_endpoint_class();
// check the accessibility of the requested endpoint's action
// by default, an endpoint's action is considered "private"
$permissions = $endpoint->permissions;
if(array_key_exists($this->_request_method, $permissions) === false ||
$permissions[$this->_request_method] !== Access::ALL
) {
if($this->checkPermissions() === false) {
$error_message = __('You do not have the required permissions.', 'mailpoet');
$error_response = $this->createErrorResponse(Error::FORBIDDEN, $error_message, Response::STATUS_FORBIDDEN);
return $error_response;
}
}
$response = $endpoint->{$this->_request_method}($this->_request_data);
return $response;
} catch(\Exception $e) {
$error_message = $e->getMessage();
$error_response = $this->createErrorResponse(Error::BAD_REQUEST, $error_message, Response::STATUS_BAD_REQUEST);
return $error_response;
}
}
function checkPermissions() {
return current_user_can(Env::$required_permission);
}
function checkToken() {
return wp_verify_nonce($this->_request_token, 'mailpoet_token');
}
function setTokenAndAPIVersion() {
$global = '<script type="text/javascript">';
$global .= 'var mailpoet_token = "%s";';
$global .= 'var mailpoet_api_version = "%s";';
$global .= '</script>';
echo sprintf(
$global,
Security::generateToken(),
self::CURRENT_VERSION
);
}
function addEndpointNamespace($namespace, $version) {
if(!empty($this->_endpoint_namespaces[$version][$namespace])) return;
$this->_endpoint_namespaces[$version][] = $namespace;
}
function getEndpointNamespaces() {
return $this->_endpoint_namespaces;
}
function getRequestedEndpointClass() {
return $this->_request_endpoint_class;
}
function getRequestedAPIVersion() {
return $this->_request_api_version;
}
function createErrorResponse($error_type, $error_message, $response_status) {
$error_response = new ErrorResponse(
array(
$error_type => $error_message
),
array(),
$response_status
);
return $error_response;
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace MailPoet\API;
namespace MailPoet\API\JSON;
if(!defined('ABSPATH')) exit;

View File

@ -1,5 +1,5 @@
<?php
namespace MailPoet\API;
namespace MailPoet\API\JSON;
if(!defined('ABSPATH')) exit;

View File

@ -1,5 +1,5 @@
<?php
namespace MailPoet\API;
namespace MailPoet\API\JSON;
if(!defined('ABSPATH')) exit;

View File

@ -1,5 +1,5 @@
<?php
namespace MailPoet\API;
namespace MailPoet\API\JSON;
if(!defined('ABSPATH')) exit;

View File

@ -1,5 +1,5 @@
<?php
namespace MailPoet\API;
namespace MailPoet\API\JSON;
if(!defined('ABSPATH')) exit;

View File

@ -1,5 +1,5 @@
<?php
namespace MailPoet\API;
namespace MailPoet\API\JSON;
if(!defined('ABSPATH')) exit;

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\WP\Posts as WPPosts;
if(!defined('ABSPATH')) exit;
@ -27,17 +28,19 @@ class AutomatedLatestContent extends APIEndpoint {
function getTerms($data = array()) {
$taxonomies = (isset($data['taxonomies'])) ? $data['taxonomies'] : array();
$search = (isset($data['search'])) ? $data['search'] : '';
$limit = (isset($data['limit'])) ? (int)$data['limit'] : 10;
$limit = (isset($data['limit'])) ? (int)$data['limit'] : 50;
$page = (isset($data['page'])) ? (int)$data['page'] : 1;
return $this->successResponse(
get_terms(
$taxonomies,
WPPosts::getTerms(
array(
'taxonomy' => $taxonomies,
'hide_empty' => false,
'search' => $search,
'number' => $limit,
'offset' => $limit * ($page - 1)
'offset' => $limit * ($page - 1),
'orderby' => 'name',
'order' => 'ASC'
)
)
);

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Models\CustomField;
if(!defined('ABSPATH')) exit;

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Models\Form;
use MailPoet\Models\StatisticsForms;

View File

@ -1,6 +1,6 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Subscribers\ImportExport\Import\MailChimp;
use MailPoet\Models\Segment;

View File

@ -0,0 +1,64 @@
<?php
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
if(!defined('ABSPATH')) exit;
class MP2Migrator extends APIEndpoint {
public function __construct() {
$this->MP2Migrator = new \MailPoet\Config\MP2Migrator();
}
/**
* Import end point
*
* @param object $data
* @return object
*/
public function import($data) {
try {
$process = $this->MP2Migrator->import($data);
return $this->successResponse($process);
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
));
}
}
/**
* Stop import end point
*
* @param object $data
* @return object
*/
public function stopImport($data) {
try {
$process = $this->MP2Migrator->stopImport();
return $this->successResponse($process);
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
));
}
}
/**
* Skip import end point
*
* @param object $data
* @return object
*/
public function skipImport($data) {
try {
$process = $this->MP2Migrator->skipImport();
return $this->successResponse($process);
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
));
}
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Mailer\MailerLog;
if(!defined('ABSPATH')) exit;

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Models\NewsletterTemplate;

View File

@ -1,8 +1,8 @@
<?php
namespace MailPoet\API\Endpoints;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Listing;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Setting;
@ -15,6 +15,7 @@ use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Scheduler\Scheduler;
use MailPoet\Newsletter\Url as NewsletterUrl;
use MailPoet\WP\Hooks;
if(!defined('ABSPATH')) exit;
@ -29,12 +30,12 @@ class Newsletters extends APIEndpoint {
APIError::NOT_FOUND => __('This newsletter does not exist.', 'mailpoet')
));
} else {
return $this->successResponse(
$newsletter
$newsletter = $newsletter
->withSegments()
->withOptions()
->asArray()
);
->asArray();
$newsletter = Hooks::applyFilters('mailpoet_api_newsletters_get_after', $newsletter);
return $this->successResponse($newsletter);
}
}
@ -51,6 +52,8 @@ class Newsletters extends APIEndpoint {
unset($data['options']);
}
$data = Hooks::applyFilters('mailpoet_api_newsletters_save_before', $data);
$newsletter = Newsletter::createOrUpdate($data);
$errors = $newsletter->getErrors();
@ -107,6 +110,8 @@ class Newsletters extends APIEndpoint {
}
}
Hooks::doAction('mailpoet_api_newsletters_save_after', $newsletter);
return $this->successResponse($newsletter->asArray());
}
}
@ -204,6 +209,7 @@ class Newsletters extends APIEndpoint {
if(!empty($errors)) {
return $this->errorResponse($errors);
} else {
Hooks::doAction('mailpoet_api_newsletters_duplicate_after', $newsletter, $duplicate);
return $this->successResponse(
Newsletter::findOne($duplicate->id)->asArray(),
array('count' => 1)
@ -366,7 +372,8 @@ class Newsletters extends APIEndpoint {
'filters' => $listing_data['filters'],
'groups' => $listing_data['groups'],
'mta_log' => Setting::getValue('mta_log'),
'mta_method' => Setting::getValue('mta.method')
'mta_method' => Setting::getValue('mta.method'),
'current_time' => current_time('mysql')
));
}

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Models\Segment;
use MailPoet\Listing;

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\Newsletter;

View File

@ -0,0 +1,125 @@
<?php
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Config\Installer;
use MailPoet\Services\Bridge;
use MailPoet\Util\License\License;
use MailPoet\WP\DateTime;
if(!defined('ABSPATH')) exit;
class Services extends APIEndpoint {
public $bridge;
public $date_time;
function __construct() {
$this->bridge = new Bridge();
$this->date_time = new DateTime();
}
function checkMSSKey($data = array()) {
$key = isset($data['key']) ? trim($data['key']) : null;
if(!$key) {
return $this->badRequest(array(
APIError::BAD_REQUEST => __('Please specify a key.', 'mailpoet')
));
}
try {
$result = $this->bridge->checkMSSKey($key);
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
));
}
$state = !empty($result['state']) ? $result['state'] : null;
$success_message = null;
if($state == Bridge::MAILPOET_KEY_VALID) {
$success_message = __('Your MailPoet Sending Service key has been successfully validated.', 'mailpoet');
} elseif($state == Bridge::MAILPOET_KEY_EXPIRING) {
$success_message = sprintf(
__('Your MailPoet Sending Service key expires on %s!', 'mailpoet'),
$this->date_time->formatDate(strtotime($result['data']['expire_at']))
);
}
if($success_message) {
return $this->successResponse(array('message' => $success_message));
}
switch($state) {
case Bridge::MAILPOET_KEY_INVALID:
$error = __('Your MailPoet Sending Service key is invalid.', 'mailpoet');
break;
default:
$code = !empty($result['code']) ? $result['code'] : Bridge::CHECK_ERROR_UNKNOWN;
$error = sprintf(
__('Error validating MailPoet Sending Service key, please try again later (code: %s)', 'mailpoet'),
$code
);
break;
}
return $this->errorResponse(array(APIError::BAD_REQUEST => $error));
}
function checkPremiumKey($data = array()) {
$key = isset($data['key']) ? trim($data['key']) : null;
if(!$key) {
return $this->badRequest(array(
APIError::BAD_REQUEST => __('Please specify a key.', 'mailpoet')
));
}
try {
$result = $this->bridge->checkPremiumKey($key);
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
));
}
$state = !empty($result['state']) ? $result['state'] : null;
$success_message = null;
if($state == Bridge::PREMIUM_KEY_VALID) {
$success_message = __('Your Premium key has been successfully validated.', 'mailpoet');
} elseif($state == Bridge::PREMIUM_KEY_EXPIRING) {
$success_message = sprintf(
__('Your Premium key expires on %s.', 'mailpoet'),
$this->date_time->formatDate(strtotime($result['data']['expire_at']))
);
}
if($success_message) {
return $this->successResponse(
array('message' => $success_message),
Installer::getPremiumStatus()
);
}
switch($state) {
case Bridge::PREMIUM_KEY_INVALID:
$error = __('Your Premium key is invalid.', 'mailpoet');
break;
case Bridge::PREMIUM_KEY_ALREADY_USED:
$error = __('Your Premium key is already used on another site.', 'mailpoet');
break;
default:
$code = !empty($result['code']) ? $result['code'] : Bridge::CHECK_ERROR_UNKNOWN;
$error = sprintf(
__('Error validating Premium key, please try again later (code: %s)', 'mailpoet'),
$code
);
break;
}
return $this->errorResponse(array(APIError::BAD_REQUEST => $error));
}
}

View File

@ -1,7 +1,8 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\Models\Setting;
use MailPoet\Services\Bridge;
@ -16,20 +17,15 @@ class Settings extends APIEndpoint {
if(empty($settings)) {
return $this->badRequest(array(
APIError::BAD_REQUEST =>
__("You have not specified any settings to be saved.", 'mailpoet')
__('You have not specified any settings to be saved.', 'mailpoet')
));
} else {
foreach($settings as $name => $value) {
Setting::setValue($name, $value);
}
if(!empty($settings['mta']['mailpoet_api_key'])
&& Bridge::isMPSendingServiceEnabled()
) {
$bridge = new Bridge();
$result = $bridge->checkKey($settings['mta']['mailpoet_api_key']);
$bridge->updateSubscriberCount($result);
}
$bridge = new Bridge();
$bridge->onSettingsSave($settings);
return $this->successResponse(Setting::getAll());
}
}
}
}

View File

@ -1,7 +1,9 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Config\Activator;
use MailPoet\WP\Hooks;
if(!defined('ABSPATH')) exit;
@ -11,6 +13,7 @@ class Setup extends APIEndpoint {
$activator = new Activator();
$activator->deactivate();
$activator->activate();
Hooks::doAction('mailpoet_setup_reset');
return $this->successResponse();
} catch(\Exception $e) {
return $this->errorResponse(array(

View File

@ -1,8 +1,8 @@
<?php
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
use MailPoet\API\Access as APIAccess;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Error as APIError;
use MailPoet\API\JSON\Access as APIAccess;
use MailPoet\Listing;
use MailPoet\Models\Subscriber;

143
lib/API/MP/v1/API.php Normal file
View File

@ -0,0 +1,143 @@
<?php
namespace MailPoet\API\MP\v1;
use MailPoet\Models\CustomField;
use MailPoet\Models\Segment;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Newsletter\Scheduler\Scheduler;
if(!defined('ABSPATH')) exit;
class API {
function getSubscriberFields() {
$data = array(
array(
'id' => 'email',
'name' => __('Email', 'mailpoet')
),
array(
'id' => 'first_name',
'name' => __('First name', 'mailpoet')
),
array(
'id' => 'last_name',
'name' => __('Last name', 'mailpoet')
)
);
$custom_fields = CustomField::selectMany(array('id', 'name'))->findMany();
foreach($custom_fields as $custom_field) {
$data[] = array(
'id' => 'cf_' . $custom_field->id,
'name' => $custom_field->name
);
}
return $data;
}
function subscribeToList($subscriber_id, $segment_id) {
return $this->subscribeToLists($subscriber_id, array($segment_id));
}
function subscribeToLists($subscriber_id, array $segments_ids) {
$subscriber = Subscriber::findOne($subscriber_id);
// throw exception when subscriber does not exist
if(!$subscriber) {
throw new \Exception(__('This subscriber does not exist.', 'mailpoet'));
}
// throw exception when none of the segments exist
$found_segments = Segment::whereIn('id', $segments_ids)->findMany();
if(!$found_segments) {
throw new \Exception(__('These lists do not exist.', 'mailpoet'));
}
// throw exception when trying to subscribe to a WP Users segment
$found_segments_ids = array();
foreach($found_segments as $found_segment) {
if($found_segment->type === Segment::TYPE_WP_USERS) {
throw new \Exception(__(sprintf("Can't subscribe to a WordPress Users list with ID %d.", $found_segment->id), 'mailpoet'));
}
$found_segments_ids[] = $found_segment->id;
}
// throw an exception when one or more segments do not exist
if(count($found_segments_ids) !== count($segments_ids)) {
$missing_ids = array_values(array_diff($segments_ids, $found_segments_ids));
throw new \Exception(__(sprintf('Lists with ID %s do not exist.', implode(', ', $missing_ids)), 'mailpoet'));
}
SubscriberSegment::subscribeToSegments($subscriber, $found_segments_ids);
return $subscriber->withCustomFields()->withSubscriptions()->asArray();
}
function getLists() {
return Segment::whereNotEqual('type', Segment::TYPE_WP_USERS)->findArray();
}
function addSubscriber(array $subscriber, $segments = array(), $options = array()) {
$send_confirmation_email = (isset($options['send_confirmation_email']) && $options['send_confirmation_email'] === false) ? false : true;
$schedule_welcome_email = (isset($options['schedule_welcome_email']) && $options['schedule_welcome_email'] === false) ? false : true;
// throw exception when subscriber email is missing
if(empty($subscriber['email'])) {
throw new \Exception(
__('Subscriber email address is required.', 'mailpoet')
);
}
// throw exception when subscriber already exists
if(Subscriber::findOne($subscriber['email'])) {
throw new \Exception(
__('This subscriber already exists.', 'mailpoet')
);
}
// separate data into default and custom fields
list($default_fields, $custom_fields) = Subscriber::extractCustomFieldsFromFromObject($subscriber);
// if some required default fields are missing, set their values
$default_fields = Subscriber::setRequiredFieldsDefaultValues($default_fields);
// add subscriber
$new_subscriber = Subscriber::create();
$new_subscriber->hydrate($default_fields);
$new_subscriber->save();
if($new_subscriber->getErrors() !== false) {
throw new \Exception(
__(sprintf('Failed to add subscriber: %s', strtolower(implode(', ', $new_subscriber->getErrors()))), 'mailpoet')
);
}
if(!empty($custom_fields)) {
$new_subscriber->saveCustomFields($custom_fields);
}
// reload subscriber to get the saved status/created|updated|delete dates/other fields
$new_subscriber = Subscriber::findOne($new_subscriber->id);
// subscribe to segments and optionally: 1) send confirmation email, 2) schedule welcome email(s)
if(!empty($segments)) {
$this->subscribeToLists($new_subscriber->id, $segments);
// send confirmation email
if($send_confirmation_email && $new_subscriber->status === Subscriber::STATUS_UNCONFIRMED) {
$this->_sendConfirmationEmail($new_subscriber);
}
// schedule welcome email(s)
if($schedule_welcome_email && $new_subscriber->status === Subscriber::STATUS_SUBSCRIBED) {
$this->_scheduleWelcomeNotification($new_subscriber, $segments);
}
}
return $new_subscriber->withCustomFields()->withSubscriptions()->asArray();
}
protected function _sendConfirmationEmail(Subscriber $subscriber) {
return $subscriber->sendConfirmationEmail();
}
protected function _scheduleWelcomeNotification(Subscriber $subscriber, array $segments) {
return Scheduler::scheduleSubscriberWelcomeNotification($subscriber->id, $segments);
}
}

View File

@ -34,12 +34,23 @@ class Changelog {
$version = Setting::getValue('version', null);
$redirect_url = null;
if($version === null) {
// new install
$redirect_url = admin_url('admin.php?page=mailpoet-welcome');
} else if($version !== Env::$version) {
// update
$redirect_url = admin_url('admin.php?page=mailpoet-update');
$mp2_migrator = new MP2Migrator();
if(!in_array($_GET['page'], array('mailpoet-migration', 'mailpoet-settings')) && $mp2_migrator->isMigrationStartedAndNotCompleted()) {
// Force the redirection if the migration has started but is not completed
$redirect_url = admin_url('admin.php?page=mailpoet-migration');
} else {
if($version === null) {
// new install
if($mp2_migrator->isMigrationNeeded()) {
// Migration from MP2
$redirect_url = admin_url('admin.php?page=mailpoet-migration');
} else {
$redirect_url = admin_url('admin.php?page=mailpoet-welcome');
}
} else if($version !== Env::$version) {
// update
$redirect_url = admin_url('admin.php?page=mailpoet-update');
}
}
if($redirect_url !== null) {

View File

@ -33,6 +33,15 @@ class Database {
'TIME_ZONE = "' . Env::$db_timezone_offset . '"',
'sql_mode=(SELECT REPLACE(@@sql_mode,"ONLY_FULL_GROUP_BY",""))',
);
if(!empty(Env::$db_charset)) {
$character_set = 'NAMES ' . Env::$db_charset;
if(!empty(Env::$db_collation)) {
$character_set .= ' COLLATE ' . Env::$db_collation;
}
$driver_options[] = $character_set;
}
$current_options = ORM::for_table("")
->raw_query('SELECT @@session.wait_timeout as wait_timeout')
->findOne();
@ -68,6 +77,7 @@ class Database {
$statistics_opens = Env::$db_prefix . 'statistics_opens';
$statistics_unsubscribes = Env::$db_prefix . 'statistics_unsubscribes';
$statistics_forms = Env::$db_prefix . 'statistics_forms';
$mapping_to_external_entities = Env::$db_prefix . 'mapping_to_external_entities';
define('MP_SETTINGS_TABLE', $settings);
define('MP_SEGMENTS_TABLE', $segments);
@ -89,6 +99,7 @@ class Database {
define('MP_STATISTICS_OPENS_TABLE', $statistics_opens);
define('MP_STATISTICS_UNSUBSCRIBES_TABLE', $statistics_unsubscribes);
define('MP_STATISTICS_FORMS_TABLE', $statistics_forms);
define('MP_MAPPING_TO_EXTERNAL_ENTITIES_TABLE', $mapping_to_external_entities);
}
}
}
}

View File

@ -28,6 +28,8 @@ class Env {
static $db_username;
static $db_password;
static $db_charset;
static $db_collation;
static $db_charset_collate;
static $db_timezone_offset;
static $required_permission = 'manage_options';
@ -62,12 +64,14 @@ class Env {
self::$db_name = DB_NAME;
self::$db_username = DB_USER;
self::$db_password = DB_PASSWORD;
self::$db_charset = $wpdb->get_charset_collate();
self::$db_source_name = self::dbSourceName(self::$db_host, self::$db_socket, self::$db_port);
self::$db_charset = $wpdb->charset;
self::$db_collation = $wpdb->collate;
self::$db_charset_collate = $wpdb->get_charset_collate();
self::$db_source_name = self::dbSourceName(self::$db_host, self::$db_socket, self::$db_port, self::$db_charset);
self::$db_timezone_offset = self::getDbTimezoneOffset();
}
private static function dbSourceName($host, $socket, $port) {
private static function dbSourceName($host, $socket, $port, $charset) {
$source_name = array(
(!$socket) ? 'mysql:host=' : 'mysql:unix_socket=',
$host,
@ -78,6 +82,9 @@ class Env {
'dbname=',
DB_NAME
);
if(!empty($charset)) {
$source_name[] = ';charset=' . $charset;
}
return implode('', $source_name);
}
@ -90,4 +97,4 @@ class Env {
$mins -= $hrs * 60;
return sprintf('%+03d:%02d', $hrs * $sgn, $mins);
}
}
}

View File

@ -75,6 +75,7 @@ class Initializer {
try {
$this->maybeDbUpdate();
$this->setupRenderer();
$this->setupInstaller();
$this->setupLocalizer();
$this->setupMenu();
$this->setupAnalytics();
@ -85,6 +86,7 @@ class Initializer {
$this->setupConflictResolver();
$this->plugin_initialized = true;
do_action('mailpoet_initialized', MAILPOET_VERSION);
} catch(\Exception $e) {
$this->handleFailedInitialization($e);
}
@ -97,10 +99,9 @@ class Initializer {
}
try {
$this->setupAPI();
$this->setupJSONAPI();
$this->setupRouter();
$this->setupPages();
do_action('mailpoet_initialized', MAILPOET_VERSION);
} catch(\Exception $e) {
$this->handleFailedInitialization($e);
}
@ -136,6 +137,13 @@ class Initializer {
$this->renderer = new Renderer($caching, $debugging);
}
function setupInstaller() {
$installer = new Installer(
Installer::PREMIUM_PLUGIN_SLUG
);
$installer->init();
}
function setupLocalizer() {
$localizer = new Localizer($this->renderer);
$localizer->init();
@ -171,9 +179,8 @@ class Initializer {
$hooks->init();
}
function setupAPI() {
$api = new API\API();
$api->init();
function setupJSONAPI() {
API\API::JSON()->init();
}
function setupRouter() {

116
lib/Config/Installer.php Normal file
View File

@ -0,0 +1,116 @@
<?php
namespace MailPoet\Config;
use MailPoet\Models\Setting;
use MailPoet\Services\Bridge;
use MailPoet\Services\Release\API;
use MailPoet\Util\License\License;
if(!defined('ABSPATH')) exit;
class Installer {
const PREMIUM_PLUGIN_SLUG = 'mailpoet-premium';
private $slug;
function __construct($slug) {
$this->slug = $slug;
}
function init() {
add_filter('plugins_api', array($this, 'getPluginInformation'), 10, 3);
}
function getPluginInformation($data, $action = '', $args = null) {
if($action === 'plugin_information'
&& isset($args->slug)
&& $args->slug === $this->slug
) {
$data = $this->retrievePluginInformation();
}
return $data;
}
static function getPremiumStatus() {
$slug = self::PREMIUM_PLUGIN_SLUG;
$premium_plugin_active = License::getLicense();
$premium_plugin_installed = $premium_plugin_active || self::isPluginInstalled($slug);
$premium_install_url = $premium_plugin_installed ? '' : self::getPluginInstallationUrl($slug);
$premium_activate_url = $premium_plugin_active ? '' : self::getPluginActivationUrl($slug);
return compact(
'premium_plugin_active',
'premium_plugin_installed',
'premium_install_url',
'premium_activate_url'
);
}
static function isPluginInstalled($slug) {
$installed_plugin = self::getInstalledPlugin($slug);
return !empty($installed_plugin);
}
static function getPluginInstallationUrl($slug) {
$install_url = add_query_arg(
array(
'action' => 'install-plugin',
'plugin' => $slug,
'_wpnonce' => wp_create_nonce('install-plugin_' . $slug),
),
self_admin_url('update.php')
);
return $install_url;
}
static function getPluginActivationUrl($slug) {
$plugin_file = self::getPluginFile($slug);
if(empty($plugin_file)) {
return false;
}
$activate_url = add_query_arg(
array(
'action' => 'activate',
'plugin' => $plugin_file,
'_wpnonce' => wp_create_nonce('activate-plugin_' . $plugin_file),
),
self_admin_url('plugins.php')
);
return $activate_url;
}
private static function getInstalledPlugin($slug) {
$installed_plugin = array();
if(is_dir(WP_PLUGIN_DIR . '/' . $slug)) {
$installed_plugin = get_plugins('/' . $slug);
}
return $installed_plugin;
}
private static function getPluginFile($slug) {
$plugin_file = false;
$installed_plugin = self::getInstalledPlugin($slug);
if(!empty($installed_plugin)) {
$plugin_file = $slug . '/' . key($installed_plugin);
}
return $plugin_file;
}
function retrievePluginInformation() {
$key = Setting::getValue(Bridge::PREMIUM_KEY_SETTING_NAME);
$api = new API($key);
$info = $api->getPluginInformation($this->slug);
$info = $this->formatInformation($info);
return $info;
}
private function formatInformation($info) {
// cast sections object to array for WP to understand
if(isset($info->sections)) {
$info->sections = (array)$info->sections;
}
return $info;
}
}

742
lib/Config/MP2Migrator.php Normal file
View File

@ -0,0 +1,742 @@
<?php
namespace MailPoet\Config;
use MailPoet\Util\ProgressBar;
use MailPoet\Models\Setting;
use MailPoet\Models\Segment;
use MailPoet\Models\Subscriber;
use MailPoet\Models\CustomField;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Models\SubscriberCustomField;
use MailPoet\Models\MappingToExternalEntities;
use MailPoet\Config\Activator;
if(!defined('ABSPATH')) exit;
class MP2Migrator {
const IMPORT_TIMEOUT_IN_SECONDS = 7200; // Timeout = 2 hours
const CHUNK_SIZE = 10; // To import the data by batch
private $log_file;
public $log_file_url;
public $progressbar;
private $segments_mapping = array(); // Mapping between old and new segment IDs
private $wp_users_segment;
public function __construct() {
$this->defineMP2Tables();
$log_filename = 'mp2migration.log';
$this->log_file = Env::$temp_path . '/' . $log_filename;
$this->log_file_url = Env::$temp_url . '/' . $log_filename;
$this->progressbar = new ProgressBar('mp2migration');
}
private function defineMP2Tables() {
global $wpdb;
if(!defined('MP2_CAMPAIGN_TABLE')) {
define('MP2_CAMPAIGN_TABLE', $wpdb->prefix . 'wysija_campaign');
}
if(!defined('MP2_CUSTOM_FIELD_TABLE')) {
define('MP2_CUSTOM_FIELD_TABLE', $wpdb->prefix . 'wysija_custom_field');
}
if(!defined('MP2_EMAIL_TABLE')) {
define('MP2_EMAIL_TABLE', $wpdb->prefix . 'wysija_email');
}
if(!defined('MP2_FORM_TABLE')) {
define('MP2_FORM_TABLE', $wpdb->prefix . 'wysija_form');
}
if(!defined('MP2_LIST_TABLE')) {
define('MP2_LIST_TABLE', $wpdb->prefix . 'wysija_list');
}
if(!defined('MP2_USER_TABLE')) {
define('MP2_USER_TABLE', $wpdb->prefix . 'wysija_user');
}
if(!defined('MP2_USER_LIST_TABLE')) {
define('MP2_USER_LIST_TABLE', $wpdb->prefix . 'wysija_user_list');
}
}
/**
* Test if the migration is already started but is not completed
*
* @return boolean
*/
public function isMigrationStartedAndNotCompleted() {
return Setting::getValue('mailpoet_migration_started', false) && !Setting::getValue('mailpoet_migration_complete', false);
}
/**
* Test if the migration is needed
*
* @return boolean
*/
public function isMigrationNeeded() {
if(Setting::getValue('mailpoet_migration_complete')) {
return false;
} else {
return $this->tableExists(MP2_CAMPAIGN_TABLE); // Check if the MailPoet 2 tables exist
}
}
/**
* Store the "Skip import" choice
*
*/
public function skipImport() {
Setting::setValue('mailpoet_migration_complete', true);
}
/**
* Test if a table exists
*
* @param string $table Table name
* @return boolean
*/
private function tableExists($table) {
global $wpdb;
try {
$sql = "SHOW TABLES LIKE '{$table}'";
$result = $wpdb->query($sql);
return !empty($result);
} catch (Exception $e) {
// Do nothing
}
return false;
}
/**
* Initialize the migration page
*
*/
public function init() {
if(!Setting::getValue('mailpoet_migration_started', false)) {
$this->emptyLog();
$this->progressbar->setTotalCount(0);
}
$this->enqueueScripts();
}
/**
* Register the JavaScript for the admin area.
*
*/
private function enqueueScripts() {
wp_enqueue_script('jquery-ui-progressbar');
}
/**
* Write a message in the log file
*
* @param string $message
*/
private function log($message) {
file_put_contents($this->log_file, "$message\n", FILE_APPEND);
}
/**
* Import the data from MailPoet 2
*
* @return string Result
*/
public function import() {
set_time_limit(self::IMPORT_TIMEOUT_IN_SECONDS);
ob_start();
$datetime = new \MailPoet\WP\DateTime();
$this->log(sprintf('=== ' . __('START IMPORT', 'mailpoet') . ' %s ===', $datetime->formatTime(time(), \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT)));
Setting::setValue('import_stopped', false); // Reset the stop import action
if(!Setting::getValue('mailpoet_migration_started', false)) {
$this->eraseMP3Data();
Setting::setValue('mailpoet_migration_started', true);
$this->displayDataToMigrate();
}
$this->importSegments();
$this->importCustomFields();
$this->importSubscribers();
if(!$this->importStopped()) {
Setting::setValue('mailpoet_migration_complete', true);
$this->log(__('IMPORT COMPLETE', 'mailpoet'));
}
$this->log(sprintf('=== ' . __('END IMPORT', 'mailpoet') . ' %s ===', $datetime->formatTime(time(), \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT)));
$result = ob_get_contents();
ob_clean();
return $result;
}
/**
* Empty the log file
*
*/
private function emptyLog() {
file_put_contents($this->log_file, '');
}
/**
* Erase all the MailPoet 3 data
*
*/
private function eraseMP3Data() {
Activator::deactivate();
Activator::activate();
$this->deleteSegments();
$this->resetMigrationCounters();
$this->log(__("MailPoet data erased", 'mailpoet'));
}
/**
* Reset the migration counters
*
*/
private function resetMigrationCounters() {
Setting::setValue('last_imported_user_id', 0);
Setting::setValue('last_imported_list_id', 0);
}
/**
* Delete the existing segments except the wp_users segment
*
*/
private function deleteSegments() {
global $wpdb;
$table = MP_SEGMENTS_TABLE;
$wpdb->query("DELETE FROM {$table} WHERE type != '" . Segment::TYPE_WP_USERS . "'");
}
/**
* Stop the import
*
*/
public function stopImport() {
Setting::setValue('import_stopped', true);
$this->log(__('IMPORT STOPPED BY USER', 'mailpoet'));
}
/**
* Test if the import must stop
*
* @return boolean Import must stop or not
*/
private function importStopped() {
return Setting::getValue('import_stopped', false);
}
/**
* Display the number of data to migrate
*
*/
private function displayDataToMigrate() {
$data = $this->getDataToMigrateAndResetProgressBar();
$this->log($data);
}
/**
* Get the data to migrate
*
* @return string Data to migrate
*/
private function getDataToMigrateAndResetProgressBar() {
$result = '';
$total_count = 0;
$this->progressbar->setTotalCount(0);
$result .= __('MailPoet 2 data found:', 'mailpoet') . "\n";
// User Lists
$users_lists_count = \ORM::for_table(MP2_LIST_TABLE)->count();
$total_count += $users_lists_count;
$result .= sprintf(_n('%d subscribers list', '%d subscribers lists', $users_lists_count, 'mailpoet'), $users_lists_count) . "\n";
// Users
$users_count = \ORM::for_table(MP2_USER_TABLE)->count();
$total_count += $users_count;
$result .= sprintf(_n('%d subscriber', '%d subscribers', $users_count, 'mailpoet'), $users_count) . "\n";
// TODO to reactivate during the next phases
/*
// Emails
$emails_count = \ORM::for_table(MP2_EMAIL_TABLE)->count();
$total_count += $emails_count;
$result .= sprintf(_n('%d newsletter', '%d newsletters', $emails_count, 'mailpoet'), $emails_count) . "\n";
// Forms
$forms_count = \ORM::for_table(MP2_FORM_TABLE)->count();
$total_count += $forms_count;
$result .= sprintf(_n('%d form', '%d forms', $forms_count, 'mailpoet'), $forms_count) . "\n";
*/
$this->progressbar->setTotalCount($total_count);
return $result;
}
/**
* Import the subscribers segments
*
*/
private function importSegments() {
$imported_segments_count = 0;
if($this->importStopped()) {
$this->segments_mapping = $this->getImportedMapping('segments');
return;
}
$this->log(__("Importing segments...", 'mailpoet'));
do {
if($this->importStopped()) {
break;
}
$lists = $this->getLists(self::CHUNK_SIZE);
$lists_count = count($lists);
if(is_array($lists)) {
foreach($lists as $list) {
$segment = $this->importSegment($list);
if(!empty($segment)) {
$imported_segments_count++;
}
}
}
$this->progressbar->incrementCurrentCount($lists_count);
} while(($lists != null) && ($lists_count > 0));
$this->segments_mapping = $this->getImportedMapping('segments');
$this->log(sprintf(_n("%d segment imported", "%d segments imported", $imported_segments_count, 'mailpoet'), $imported_segments_count));
}
/**
* Get the Mailpoet 2 users lists
*
* @global object $wpdb
* @param int $limit Number of users max
* @return array Users Lists
*/
private function getLists($limit) {
global $wpdb;
$lists = array();
$last_id = Setting::getValue('last_imported_list_id', 0);
$table = MP2_LIST_TABLE;
$sql = "
SELECT l.list_id, l.name, l.description, l.is_enabled, l.created_at
FROM `$table` l
WHERE l.list_id > '$last_id'
ORDER BY l.list_id
LIMIT $limit
";
$lists = $wpdb->get_results($sql, ARRAY_A);
return $lists;
}
/**
* Import a segment
*
* @param array $list_data List data
* @return Segment
*/
private function importSegment($list_data) {
$datetime = new \MailPoet\WP\DateTime();
if($list_data['is_enabled']) {
$segment = Segment::createOrUpdate(array(
'name' => $list_data['name'],
'type' => 'default',
'description' => !empty($list_data['description']) ? $list_data['description'] : '',
'created_at' => $datetime->formatTime($list_data['created_at'], \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT),
));
} else {
$segment = Segment::getWPSegment();
}
if(!empty($segment)) {
// Map the segment with its old ID
$mapping = new MappingToExternalEntities();
$mapping->create(array(
'old_id' => $list_data['list_id'],
'type' => 'segments',
'new_id' => $segment->id,
'created_at' => $datetime->formatTime(time(), \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT),
));
}
Setting::setValue('last_imported_list_id', $list_data['list_id']);
return $segment;
}
/**
* Import the custom fields
*
*/
private function importCustomFields() {
$imported_custom_fields_count = 0;
if($this->importStopped()) {
return;
}
$this->log(__("Importing custom fields...", 'mailpoet'));
$custom_fields = $this->getCustomFields();
foreach($custom_fields as $custom_field) {
$result = $this->importCustomField($custom_field);
if(!empty($result)) {
$imported_custom_fields_count++;
}
}
$this->log(sprintf(_n("%d custom field imported", "%d custom fields imported", $imported_custom_fields_count, 'mailpoet'), $imported_custom_fields_count));
}
/**
* Get the Mailpoet 2 custom fields
*
* @global object $wpdb
* @return array Custom fields
*/
private function getCustomFields() {
global $wpdb;
$custom_fields = array();
$table = MP2_CUSTOM_FIELD_TABLE;
$sql = "
SELECT cf.id, cf.name, cf.type, cf.required, cf.settings
FROM `$table` cf
";
$custom_fields = $wpdb->get_results($sql, ARRAY_A);
return $custom_fields;
}
/**
* Import a custom field
*
* @param array $custom_field MP2 custom field
* @return CustomField
*/
private function importCustomField($custom_field) {
$data = array(
'id' => $custom_field['id'],
'name' => $custom_field['name'],
'type' => $this->mapCustomFieldType($custom_field['type']),
'params' => $this->mapCustomFieldParams($custom_field),
);
$custom_field = new CustomField();
$custom_field->createOrUpdate($data);
return $custom_field;
}
/**
* Map the MailPoet 2 custom field type with the MailPoet custom field type
*
* @param string $mp2_type MP2 custom field type
* @return string MP3 custom field type
*/
private function mapCustomFieldType($mp2_type) {
$type = '';
switch($mp2_type) {
case 'input':
$type = 'text';
break;
default:
$type = $mp2_type;
}
return $type;
}
/**
* Map the MailPoet 2 custom field settings with the MailPoet custom field params
*
* @param array $custom_field MP2 custom field
* @return string serialized MP3 custom field params
*/
private function mapCustomFieldParams($custom_field) {
$params = unserialize($custom_field['settings']);
$params['label'] = $custom_field['name'];
if(isset($params['validate'])) {
$params['validate'] = $this->mapCustomFieldValidateValue($params['validate']);
}
if(isset($params['date_order'])) { // Convert the date_order field
$params['date_format'] = strtoupper($params['date_order']);
unset($params['date_order']);
}
return $params;
}
/**
* Map the validate value
*
* @param string $mp2_value MP2 value
* @return string MP3 value
*/
private function mapCustomFieldValidateValue($mp2_value) {
$value = '';
switch($mp2_value) {
case 'onlyLetterSp':
case 'onlyLetterNumber':
$value = 'alphanum';
break;
case 'onlyNumberSp':
$value = 'number';
break;
case 'phone':
$value = 'phone';
break;
}
return $value;
}
/**
* Import the subscribers
*
*/
private function importSubscribers() {
$imported_subscribers_count = 0;
if($this->importStopped()) {
return;
}
$this->log(__("Importing subscribers...", 'mailpoet'));
$this->wp_users_segment = Segment::getWPSegment();
do {
if($this->importStopped()) {
break;
}
$users = $this->getUsers(self::CHUNK_SIZE);
$users_count = count($users);
if(is_array($users)) {
foreach($users as $user) {
$subscriber = $this->importSubscriber($user);
if(!empty($subscriber)) {
$imported_subscribers_count++;
$this->importSubscriberSegments($subscriber, $user['user_id']);
$this->importSubscriberCustomFields($subscriber, $user);
}
}
}
$this->progressbar->incrementCurrentCount($users_count);
} while(($users != null) && ($users_count > 0));
$this->log(sprintf(_n("%d subscriber imported", "%d subscribers imported", $imported_subscribers_count, 'mailpoet'), $imported_subscribers_count));
}
/**
* Get the Mailpoet 2 users
*
* @global object $wpdb
* @param int $limit Number of users max
* @return array Users
*/
private function getUsers($limit) {
global $wpdb;
$users = array();
$last_id = Setting::getValue('last_imported_user_id', 0);
$table = MP2_USER_TABLE;
$sql = "
SELECT u.*
FROM `$table` u
WHERE u.user_id > '$last_id'
ORDER BY u.user_id
LIMIT $limit
";
$users = $wpdb->get_results($sql, ARRAY_A);
return $users;
}
/**
* Import a subscriber
*
* @param array $user_data User data
* @return Subscriber
*/
private function importSubscriber($user_data) {
$datetime = new \MailPoet\WP\DateTime();
$subscriber = Subscriber::createOrUpdate(array(
'wp_user_id' => !empty($user_data['wpuser_id']) ? $user_data['wpuser_id'] : null,
'email' => $user_data['email'],
'first_name' => $user_data['firstname'],
'last_name' => $user_data['lastname'],
'status' => $this->mapUserStatus($user_data['status']),
'created_at' => $datetime->formatTime($user_data['created_at'], \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT),
'subscribed_ip' => !empty($user_data['ip']) ? $user_data['ip'] : null,
'confirmed_ip' => !empty($user_data['confirmed_ip']) ? $user_data['confirmed_ip'] : null,
'confirmed_at' => !empty($user_data['confirmed_at']) ? $datetime->formatTime($user_data['confirmed_at'], \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT) : null,
));
Setting::setValue('last_imported_user_id', $user_data['user_id']);
if(!empty($subscriber)) {
// Map the subscriber with its old ID
$mapping = new MappingToExternalEntities();
$mapping->create(array(
'old_id' => $user_data['user_id'],
'type' => 'subscribers',
'new_id' => $subscriber->id,
'created_at' => $datetime->formatTime(time(), \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT),
));
}
return $subscriber;
}
/**
* Map the MailPoet 2 user status with MailPoet 3
*
* @param int $mp2_user_status MP2 user status
* @return string MP3 user status
*/
private function mapUserStatus($mp2_user_status) {
switch($mp2_user_status) {
case 1:
$status = 'subscribed';
break;
case -1:
$status = 'unsubscribed';
break;
case 0:
default:
$status = 'unconfirmed';
}
return $status;
}
/**
* Import the segments for a subscriber
*
* @param Subscriber $subscriber MP3 subscriber
* @param int $user_id MP2 user ID
*/
private function importSubscriberSegments($subscriber, $user_id) {
$user_lists = $this->getUserLists($user_id);
foreach($user_lists as $user_list) {
$this->importSubscriberSegment($subscriber->id, $user_list);
}
}
/**
* Get the lists for a user
*
* @global object $wpdb
* @param int $user_id User ID
* @return array Users Lists
*/
private function getUserLists($user_id) {
global $wpdb;
$user_lists = array();
$table = MP2_USER_LIST_TABLE;
$sql = "
SELECT ul.list_id, ul.sub_date, ul.unsub_date
FROM `$table` ul
WHERE ul.user_id = '$user_id'
";
$user_lists = $wpdb->get_results($sql, ARRAY_A);
return $user_lists;
}
/**
* Import a subscriber segment
*
* @param int $subscriber_id
* @param array $user_list
* @return SubscriberSegment
*/
private function importSubscriberSegment($subscriber_id, $user_list) {
$subscriber_segment = null;
$datetime = new \MailPoet\WP\DateTime();
if(isset($this->segments_mapping[$user_list['list_id']])) {
$segment_id = $this->segments_mapping[$user_list['list_id']];
$status = (($segment_id == $this->wp_users_segment->id) || empty($user_list['unsub_date'])) ? 'subscribed' : 'unsubscribed'; // the users belonging to the wp_users segment are always subscribed
$data = array(
'subscriber_id' => $subscriber_id,
'segment_id' => $segment_id,
'status' => $status,
'created_at' => $datetime->formatTime($user_list['sub_date'], \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT),
'updated_at' => !empty($user_list['unsub_date']) ? $datetime->formatTime($user_list['unsub_date'], \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT) : null,
);
$subscriber_segment = new SubscriberSegment();
$subscriber_segment->createOrUpdate($data);
}
return $subscriber_segment;
}
/**
* Import the custom fields values for a subscriber
*
* @param Subscriber $subscriber MP3 subscriber
* @param array $user MP2 user
*/
private function importSubscriberCustomFields($subscriber, $user) {
$imported_custom_fields = $this->getImportedCustomFields();
foreach($imported_custom_fields as $custom_field) {
$custom_field_column = 'cf_' . $custom_field['id'];
if(isset($custom_field_column)) {
$this->importSubscriberCustomField($subscriber->id, $custom_field, $user[$custom_field_column]);
}
}
}
/**
* Get the imported custom fields
*
* @global object $wpdb
* @return array Imported custom fields
*
*/
private function getImportedCustomFields() {
global $wpdb;
$table = MP_CUSTOM_FIELDS_TABLE;
$sql = "
SELECT cf.id, cf.name, cf.type
FROM `$table` cf
";
$custom_fields = $wpdb->get_results($sql, ARRAY_A);
return $custom_fields;
}
/**
* Import a subscriber custom field
*
* @param int $subscriber_id Subscriber ID
* @param int $custom_field Custom field
* @param string $custom_field_value Custom field value
* @return SubscriberCustomField
*/
private function importSubscriberCustomField($subscriber_id, $custom_field, $custom_field_value) {
if($custom_field['type'] == 'date') {
$datetime = new \MailPoet\WP\DateTime();
$value = $datetime->formatTime($custom_field_value, \MailPoet\WP\DateTime::DEFAULT_DATE_TIME_FORMAT); // Convert the date field
} else {
$value = $custom_field_value;
}
$data = array(
'subscriber_id' => $subscriber_id,
'custom_field_id' => $custom_field['id'],
'value' => isset($value) ? $value : '',
);
$subscriber_custom_field = new SubscriberCustomField();
$subscriber_custom_field->createOrUpdate($data);
return $subscriber_custom_field;
}
/**
* Get the mapping between the MP2 and the imported MP3 IDs
*
* @param string $model Model (segment,...)
* @return array Mapping
*/
public function getImportedMapping($model) {
$mappings = array();
$mapping_relations = MappingToExternalEntities::where('type', $model)->findArray();
foreach($mapping_relations as $relation) {
$mappings[$relation['old_id']] = $relation['new_id'];
}
return $mappings;
}
}

View File

@ -15,6 +15,7 @@ use MailPoet\Settings\Hosts;
use MailPoet\Settings\Pages;
use MailPoet\Subscribers\ImportExport\ImportExportFactory;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\Util\License\License;
use MailPoet\WP\DateTime;
use MailPoet\WP\Notice as WPNotice;
use MailPoet\WP\Readme;
@ -28,6 +29,7 @@ class Menu {
$subscribers_feature = new SubscribersFeature();
$this->subscribers_over_limit = $subscribers_feature->check();
$this->checkMailPoetAPIKey();
$this->checkPremiumKey();
}
function init() {
@ -163,6 +165,20 @@ class Menu {
'settings'
)
);
// Only show this page in menu if the Premium plugin is not activated
add_submenu_page(
License::getLicense() ? true : $main_page_slug,
$this->setPageTitle(__('Premium', 'mailpoet')),
__('Premium', 'mailpoet'),
Env::$required_permission,
'mailpoet-premium',
array(
$this,
'premium'
)
);
add_submenu_page(
'admin.php?page=mailpoet-subscribers',
$this->setPageTitle(__('Import', 'mailpoet')),
@ -199,6 +215,18 @@ class Menu {
)
);
add_submenu_page(
true,
$this->setPageTitle(__('Migration', 'mailpoet')),
'',
Env::$required_permission,
'mailpoet-migration',
array(
$this,
'migration'
)
);
add_submenu_page(
true,
$this->setPageTitle(__('Update', 'mailpoet')),
@ -263,6 +291,16 @@ class Menu {
$this->displayPage('welcome.html', $data);
}
function migration() {
$mp2_migrator = new MP2Migrator();
$mp2_migrator->init();
$data = array(
'log_file_url' => $mp2_migrator->log_file_url,
'progress_url' => $mp2_migrator->progressbar->url,
);
$this->displayPage('mp2migration.html', $data);
}
function update() {
global $wp;
$current_url = home_url(add_query_arg($wp->query_string, $wp->request));
@ -297,17 +335,34 @@ class Menu {
$this->displayPage('update.html', $data);
}
function premium() {
$data = array(
'subscriber_count' => Subscriber::getTotalSubscribers(),
'sub_menu' => 'mailpoet-newsletters'
);
$this->displayPage('premium.html', $data);
}
function settings() {
if($this->subscribers_over_limit) return $this->displaySubscriberLimitExceededTemplate();
$settings = Setting::getAll();
$flags = $this->_getFlags();
// force MSS key check even if the method isn't active
$checker = new ServicesChecker();
$mp_api_key_valid = $checker->isMailPoetAPIKeyValid(false, true);
$data = array(
'settings' => $settings,
'segments' => Segment::getSegmentsWithSubscriberCount(),
'cron_trigger' => CronTrigger::getAvailableMethods(),
'total_subscribers' => Subscriber::getTotalSubscribers(),
'premium_plugin_active' => License::getLicense(),
'premium_key_valid' => !empty($this->premium_key_valid),
'mss_key_valid' => !empty($mp_api_key_valid),
'pages' => Pages::getAll(),
'flags' => $flags,
'current_user' => wp_get_current_user(),
@ -317,6 +372,8 @@ class Menu {
)
);
$data = array_merge($data, Installer::getPremiumStatus());
$this->displayPage('settings.html', $data);
}
@ -369,6 +426,8 @@ class Menu {
$data['date_formats'] = Block\Date::getDateFormats();
$data['month_names'] = Block\Date::getMonthNames();
$data['premium_plugin_active'] = License::getLicense();
$this->displayPage('subscribers/subscribers.html', $data);
}
@ -434,6 +493,7 @@ class Menu {
wp_enqueue_media();
wp_enqueue_script('tinymce-wplink', includes_url('js/tinymce/plugins/wplink/plugin.js'));
wp_enqueue_style('editor', includes_url('css/editor.css'));
$this->displayPage('newsletter/editor.html', $data);
}
@ -521,7 +581,16 @@ class Menu {
$show_notices = isset($_REQUEST['page'])
&& stripos($_REQUEST['page'], 'mailpoet-newsletters') === false;
$checker = $checker ?: new ServicesChecker();
$this->mp_api_key_valid = $checker->checkMailPoetAPIKeyValid($show_notices);
$this->mp_api_key_valid = $checker->isMailPoetAPIKeyValid($show_notices);
}
}
function checkPremiumKey(ServicesChecker $checker = null) {
if(self::isOnMailPoetAdminPage()) {
$show_notices = isset($_REQUEST['page'])
&& stripos($_REQUEST['page'], 'mailpoet-newsletters') === false;
$checker = $checker ?: new ServicesChecker();
$this->premium_key_valid = $checker->isPremiumKeyValid($show_notices);
}
}

Some files were not shown because too many files have changed in this diff Show More