Compare commits

...

259 Commits

Author SHA1 Message Date
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
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
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
238 changed files with 6543 additions and 2648 deletions

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

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
.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

@ -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

@ -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

@ -151,8 +151,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 +189,6 @@ define([
"click .mailpoet_done_editing": "close",
};
},
templateHelpers: function() {
return {
model: this.model.toJSON(),
};
},
onRender: function() {
var that = this;
@ -377,7 +372,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 +385,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

@ -37,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);

View File

@ -341,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);

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 !!(

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

@ -1,178 +1,18 @@
<?php
namespace MailPoet\API;
use MailPoet\Util\Helpers;
use MailPoet\Util\Security;
use MailPoet\WP\Hooks;
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() {
Hooks::doAction('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();
}
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;
throw new \Exception(__('Invalid API version.', 'mailpoet'));
}
}

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

@ -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;
@ -372,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,19 +17,14 @@ 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,7 @@
<?php
namespace MailPoet\API\Endpoints;
namespace MailPoet\API\JSON\v1;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\JSON\Endpoint as APIEndpoint;
use MailPoet\Config\Activator;
use MailPoet\WP\Hooks;

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;

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

@ -0,0 +1,132 @@
<?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);
}
// 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) {
Scheduler::scheduleSubscriberWelcomeNotification($new_subscriber->id, $segments);
}
}
return $new_subscriber->withCustomFields()->withSubscriptions()->asArray();
}
}

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();

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);
}

View File

@ -75,6 +75,7 @@ class Initializer {
try {
$this->maybeDbUpdate();
$this->setupRenderer();
$this->setupInstaller();
$this->setupLocalizer();
$this->setupMenu();
$this->setupAnalytics();
@ -98,7 +99,7 @@ class Initializer {
}
try {
$this->setupAPI();
$this->setupJSONAPI();
$this->setupRouter();
$this->setupPages();
} catch(\Exception $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;
}
}

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')),
@ -297,17 +313,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 +350,8 @@ class Menu {
)
);
$data = array_merge($data, Installer::getPremiumStatus());
$this->displayPage('settings.html', $data);
}
@ -369,6 +404,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 +471,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 +559,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);
}
}

View File

@ -12,7 +12,7 @@ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
class Migrator {
function __construct() {
$this->prefix = Env::$db_prefix;
$this->charset = Env::$db_charset;
$this->charset_collate = Env::$db_charset_collate;
$this->models = array(
'segments',
'settings',
@ -300,7 +300,8 @@ class Migrator {
'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,',
'sent_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
@ -316,6 +317,7 @@ class Migrator {
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -329,6 +331,7 @@ class Migrator {
'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -342,6 +345,7 @@ class Migrator {
'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'KEY newsletter_id (newsletter_id),',
'KEY queue_id (queue_id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -365,7 +369,7 @@ class Migrator {
$sql = array();
$sql[] = "CREATE TABLE " . $table . " (";
$sql = array_merge($sql, $attributes);
$sql[] = ") " . $this->charset . ";";
$sql[] = ") " . $this->charset_collate . ";";
return implode("\n", $sql);
}

View File

@ -42,7 +42,7 @@ class Populator {
}
function up() {
global $wpdb;
$this->convertExistingDataToUTF8();
array_map(array($this, 'populate'), $this->models);
@ -308,4 +308,69 @@ class Populator {
)
);
}
/*
* MailPoet versions 3.0.0-beta.32 and older used the default MySQL connection
* character set, which usually defaults to latin1, but stored UTF-8 data.
* This method converts existing incorrectly stored data that uses the
* default character set, into a new character set that is used by WordPress.
*/
public function convertExistingDataToUTF8() {
global $wpdb;
if(!version_compare(get_option('mailpoet_db_version'), '3.0.0-beta.32', '<=')) {
// Data conversion should only be performed only once, when migrating from
// older version
return;
}
$source_charset = $wpdb->get_var('SELECT @@GLOBAL.character_set_connection');
$destination_charset = $wpdb->get_var('SELECT @@SESSION.character_set_connection');
if($source_charset === $destination_charset) return;
// UTF8MB4 is a superset of UTF8, thus a conversion is not necessary
if(substr($source_charset, 0, 4) === 'utf8' && $destination_charset === 'utf8mb4') return;
$tables = array(
'segments' => array('name', 'type', 'description'),
'settings' => array('name', 'value'),
'custom_fields' => array('name', 'type', 'params'),
'sending_queues' => array('type', 'newsletter_rendered_body', 'newsletter_rendered_subject', 'subscribers', 'status'),
'subscribers' => array('first_name', 'last_name', 'email', 'status', 'subscribed_ip', 'confirmed_ip', 'unconfirmed_data'),
'subscriber_segment' => array('status'),
'subscriber_custom_field' => array('value'),
'newsletters' => array('hash', 'subject', 'type', 'sender_address', 'sender_name', 'status', 'reply_to_address', 'reply_to_name', 'preheader', 'body'),
'newsletter_templates' => array('name', 'description', 'body', 'thumbnail'),
'newsletter_option_fields' => array('name', 'newsletter_type'),
'newsletter_option' => array('value'),
'newsletter_links' => array('url', 'hash'),
'forms' => array('name', 'body', 'settings', 'styles'),
);
foreach($tables as $table => $columns) {
$query = "UPDATE `%s` SET %s WHERE %s";
$columns_query = array();
$where_query = array();
foreach($columns as $column) {
$columns_query[] = sprintf(
'`%1$s` = @%1$s',
$column
);
$where_query[] = sprintf(
'char_length(%1$s) = length(@%1$s := convert(binary convert(%1$s using %2$s) using %3$s))',
$column,
$source_charset,
$destination_charset
);
}
$wpdb->query(sprintf(
$query,
$this->prefix . $table,
implode(', ', $columns_query),
implode(' AND ', $where_query)
));
}
}
}

View File

@ -31,6 +31,7 @@ class Renderer {
$this->setupDebug();
$this->setupTranslations();
$this->setupFunctions();
$this->setupFilters();
$this->setupHandlebars();
$this->setupHelpscout();
$this->setupGlobalVariables();
@ -45,6 +46,10 @@ class Renderer {
$this->renderer->addExtension(new Twig\Functions());
}
function setupFilters() {
$this->renderer->addExtension(new Twig\Filters());
}
function setupHandlebars() {
$this->renderer->addExtension(new Twig\Handlebars());
}

View File

@ -10,6 +10,8 @@ class RequirementsChecker {
const TEST_FOLDER_PERMISSIONS = 'TempAndCacheFolderCreation';
const TEST_PDO_EXTENSION = 'PDOExtension';
const TEST_MBSTRING_EXTENSION = 'MbstringExtension';
const TEST_XML_EXTENSION = 'XmlExtension';
const TEST_ZIP_EXTENSION = 'ZipExtension';
const TEST_VENDOR_SOURCE = 'VendorSource';
public $display_error_notice;
@ -45,6 +47,8 @@ class RequirementsChecker {
self::TEST_PDO_EXTENSION,
self::TEST_FOLDER_PERMISSIONS,
self::TEST_MBSTRING_EXTENSION,
self::TEST_XML_EXTENSION,
self::TEST_ZIP_EXTENSION,
self::TEST_VENDOR_SOURCE
);
$results = array();
@ -61,7 +65,7 @@ class RequirementsChecker {
);
if(!is_dir($paths['cache_path']) && !wp_mkdir_p($paths['cache_path'])) {
$error = Helpers::replaceLinkTags(
__('This plugin requires write permissions inside the /wp-content/uploads folder. Please read our [link]instructions[/link] on how to resolve this issue.', 'mailpoet'),
__('MailPoet requires write permissions inside the /wp-content/uploads folder. Please read our [link]instructions[/link] on how to resolve this issue.', 'mailpoet'),
'//beta.docs.mailpoet.com/article/152-minimum-requirements-for-mailpoet-3#folder_permissions'
);
return $this->processError($error);
@ -81,7 +85,7 @@ class RequirementsChecker {
function checkPDOExtension() {
if(extension_loaded('pdo') && extension_loaded('pdo_mysql')) return true;
$error = Helpers::replaceLinkTags(
__('This plugin requires the PDO_MYSQL PHP extension. Please read our [link]instructions[/link] on how to resolve this issue.', 'mailpoet'),
__('MailPoet requires a PDO_MYSQL PHP extension. Please read our [link]instructions[/link] on how to resolve this issue.', 'mailpoet'),
'//beta.docs.mailpoet.com/article/152-minimum-requirements-for-mailpoet-3#php_extension'
);
return $this->processError($error);
@ -94,6 +98,24 @@ class RequirementsChecker {
return true;
}
function checkXmlExtension() {
if(extension_loaded('xml')) return true;
$error = Helpers::replaceLinkTags(
__('MailPoet requires an XML PHP extension. Please read our [link]instructions[/link] on how to resolve this issue.', 'mailpoet'),
'//beta.docs.mailpoet.com/article/152-minimum-requirements-for-mailpoet-3#php_extension'
);
return $this->processError($error);
}
function checkZipExtension() {
if(extension_loaded('zip')) return true;
$error = Helpers::replaceLinkTags(
__('MailPoet requires a ZIP PHP extension. Please read our [link]instructions[/link] on how to resolve this issue.', 'mailpoet'),
'//beta.docs.mailpoet.com/article/152-minimum-requirements-for-mailpoet-3#php_extension'
);
return $this->processError($error);
}
function checkVendorSource() {
foreach($this->vendor_classes as $dependency) {
$dependency_path = $this->getDependencyPath($dependency);

View File

@ -5,45 +5,94 @@ use MailPoet\Models\Setting;
use MailPoet\Models\Subscriber;
use MailPoet\Services\Bridge;
use MailPoet\Util\Helpers;
use MailPoet\Util\License\License;
use MailPoet\WP\DateTime;
use MailPoet\WP\Notice as WPNotice;
if(!defined('ABSPATH')) exit;
class ServicesChecker {
function checkMailPoetAPIKeyValid($display_error_notice = true) {
if(!Bridge::isMPSendingServiceEnabled()) {
function isMailPoetAPIKeyValid($display_error_notice = true, $force_check = false) {
if(!$force_check && !Bridge::isMPSendingServiceEnabled()) {
return null;
}
$result = Setting::getValue(Bridge::API_KEY_STATE_SETTING_NAME);
if(empty($result['state']) || $result['state'] == Bridge::MAILPOET_KEY_VALID) {
return true;
}
$mss_key_specified = Bridge::isMSSKeySpecified();
$mss_key = Setting::getValue(Bridge::API_KEY_STATE_SETTING_NAME);
if($result['state'] == Bridge::MAILPOET_KEY_INVALID) {
$error = Helpers::replaceLinkTags(
__('All sending is currently paused! Your key to send with MailPoet is invalid. [link]Visit MailPoet.com to purchase a key[/link]', 'mailpoet'),
'https://account.mailpoet.com?s=' . Subscriber::getTotalSubscribers()
);
if(!$mss_key_specified
|| empty($mss_key['state'])
|| $mss_key['state'] == Bridge::MAILPOET_KEY_INVALID
) {
if($display_error_notice) {
$error = Helpers::replaceLinkTags(
__('All sending is currently paused! Your key to send with MailPoet is invalid. [link]Visit MailPoet.com to purchase a key[/link]', 'mailpoet'),
'https://account.mailpoet.com?s=' . Subscriber::getTotalSubscribers()
);
WPNotice::displayError($error);
}
return false;
} elseif($result['state'] == Bridge::MAILPOET_KEY_EXPIRING
&& !empty($result['data']['expire_at'])
} elseif($mss_key['state'] == Bridge::MAILPOET_KEY_EXPIRING
&& !empty($mss_key['data']['expire_at'])
) {
$date = date('Y-m-d', strtotime($result['data']['expire_at']));
$error = Helpers::replaceLinkTags(
__('Your newsletters are awesome! Don\'t forget to [link]upgrade your MailPoet email plan[/link] by %s to keep sending them to your subscribers.', 'mailpoet'),
'https://account.mailpoet.com?s=' . Subscriber::getTotalSubscribers()
);
$error = sprintf($error, $date);
if($display_error_notice) {
$date_time = new DateTime();
$date = $date_time->formatDate(strtotime($mss_key['data']['expire_at']));
$error = Helpers::replaceLinkTags(
__('Your newsletters are awesome! Don\'t forget to [link]upgrade your MailPoet email plan[/link] by %s to keep sending them to your subscribers.', 'mailpoet'),
'https://account.mailpoet.com?s=' . Subscriber::getTotalSubscribers()
);
$error = sprintf($error, $date);
WPNotice::displayWarning($error);
}
return true;
} elseif($mss_key['state'] == Bridge::MAILPOET_KEY_VALID) {
return true;
}
return true;
return false;
}
function isPremiumKeyValid($display_error_notice = true) {
$premium_key_specified = Bridge::isPremiumKeySpecified();
$premium_plugin_active = License::getLicense();
$premium_key = Setting::getValue(Bridge::PREMIUM_KEY_STATE_SETTING_NAME);
if(!$premium_plugin_active) {
$display_error_notice = false;
}
if(!$premium_key_specified
|| empty($premium_key['state'])
|| $premium_key['state'] === Bridge::PREMIUM_KEY_INVALID
|| $premium_key['state'] === Bridge::PREMIUM_KEY_ALREADY_USED
) {
if($display_error_notice) {
$error = Helpers::replaceLinkTags(
__('Warning! Your License Key is either invalid or expired. [link]Renew your License now[/link] to enjoy automatic updates and Premium support.', 'mailpoet'),
'https://account.mailpoet.com'
);
WPNotice::displayError($error);
}
return false;
} elseif($premium_key['state'] === Bridge::PREMIUM_KEY_EXPIRING
&& !empty($premium_key['data']['expire_at'])
) {
if($display_error_notice) {
$date_time = new DateTime();
$date = $date_time->formatDate(strtotime($premium_key['data']['expire_at']));
$error = Helpers::replaceLinkTags(
__('Your License Key is expiring! Don\'t forget to [link]renew your license[/link] by %s to keep enjoying automatic updates and Premium support.', 'mailpoet'),
'https://account.mailpoet.com'
);
$error = sprintf($error, $date);
WPNotice::displayWarning($error);
}
return true;
} elseif($premium_key['state'] === Bridge::PREMIUM_KEY_VALID) {
return true;
}
return false;
}
}

View File

@ -3,7 +3,8 @@ namespace MailPoet\Cron;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Cron\Workers\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-includes/pluggable.php');
@ -50,6 +51,7 @@ class Daemon {
$this->executeScheduleWorker();
$this->executeQueueWorker();
$this->executeSendingServiceKeyCheckWorker();
$this->executePremiumKeyCheckWorker();
$this->executeBounceWorker();
} catch(\Exception $e) {
// continue processing, no need to handle errors
@ -87,6 +89,11 @@ class Daemon {
return $worker->process();
}
function executePremiumKeyCheckWorker() {
$worker = new PremiumKeyCheckWorker($this->timer);
return $worker->process();
}
function executeBounceWorker() {
$bounce = new BounceWorker($this->timer);
return $bounce->process();

View File

@ -5,7 +5,8 @@ use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Cron\Workers\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
use MailPoet\Mailer\MailerLog;
use MailPoet\Services\Bridge;
@ -32,12 +33,17 @@ class WordPress {
// sending service key check
$sskeycheck_due_queues = SendingServiceKeyCheckWorker::getAllDueQueues();
$sskeycheck_future_queues = SendingServiceKeyCheckWorker::getFutureQueues();
// premium key check
$premium_key_specified = Bridge::isPremiumKeySpecified();
$premium_keycheck_due_queues = PremiumKeyCheckWorker::getAllDueQueues();
$premium_keycheck_future_queues = PremiumKeyCheckWorker::getFutureQueues();
// check requirements for each worker
$sending_queue_active = (($scheduled_queues || $running_queues) && !$sending_limit_reached && !$sending_is_paused);
$bounce_sync_active = ($mp_sending_enabled && ($bounce_due_queues || !$bounce_future_queues));
$sending_service_key_check_active = ($mp_sending_enabled && ($sskeycheck_due_queues || !$sskeycheck_future_queues));
$premium_key_check_active = ($premium_key_specified && ($premium_keycheck_due_queues || !$premium_keycheck_future_queues));
return ($sending_queue_active || $bounce_sync_active || $sending_service_key_check_active);
return ($sending_queue_active || $bounce_sync_active || $sending_service_key_check_active || $premium_key_check_active);
}
static function cleanup() {

View File

@ -1,7 +1,6 @@
<?php
namespace MailPoet\Cron\Workers;
use Carbon\Carbon;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\SendingQueue;
@ -12,7 +11,7 @@ use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class Bounce {
class Bounce extends SimpleWorker {
const TASK_TYPE = 'bounce';
const BATCH_SIZE = 100;
@ -20,66 +19,20 @@ class Bounce {
const BOUNCED_SOFT = 'soft';
const NOT_BOUNCED = null;
public $timer;
public $api;
function __construct($timer = false) {
$this->timer = ($timer) ? $timer : microtime(true);
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
}
function initApi() {
function init() {
if(!$this->api) {
$mailer_config = Mailer::getMailerConfig();
$this->api = new API($mailer_config['mailpoet_api_key']);
}
}
function process() {
if(!Bridge::isMPSendingServiceEnabled()) {
return false;
}
$this->initApi();
$scheduled_queues = self::getScheduledQueues();
$running_queues = self::getRunningQueues();
if(!$scheduled_queues && !$running_queues) {
self::scheduleBounceSync();
return false;
}
foreach($scheduled_queues as $i => $queue) {
$this->prepareBounceQueue($queue);
}
foreach($running_queues as $i => $queue) {
$this->processBounceQueue($queue);
}
return true;
function checkProcessingRequirements() {
return Bridge::isMPSendingServiceEnabled();
}
static function scheduleBounceSync() {
$already_scheduled = SendingQueue::where('type', self::TASK_TYPE)
->whereNull('deleted_at')
->where('status', SendingQueue::STATUS_SCHEDULED)
->findMany();
if($already_scheduled) {
return false;
}
$queue = SendingQueue::create();
$queue->type = self::TASK_TYPE;
$queue->status = SendingQueue::STATUS_SCHEDULED;
$queue->priority = SendingQueue::PRIORITY_LOW;
$queue->scheduled_at = self::getNextRunDate();
$queue->newsletter_id = 0;
$queue->save();
return $queue;
}
function prepareBounceQueue(SendingQueue $queue) {
function prepareQueue(SendingQueue $queue) {
$subscribers = Subscriber::select('id')
->whereNull('deleted_at')
->whereIn('status', array(
@ -101,16 +54,11 @@ class Bounce {
)
);
$queue->count_total = $queue->count_to_process = count($subscribers);
$queue->status = null;
$queue->save();
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
return true;
return parent::prepareQueue($queue);
}
function processBounceQueue(SendingQueue $queue) {
function processQueue(SendingQueue $queue) {
$queue->subscribers = $queue->getSubscribers();
if(empty($queue->subscribers['to_process'])) {
$queue->delete();
@ -157,39 +105,4 @@ class Bounce {
}
}
}
static function getNextRunDate() {
$date = Carbon::createFromTimestamp(current_time('timestamp'));
// Random day of the next week
$date->setISODate($date->format('o'), $date->format('W') + 1, mt_rand(1, 7));
$date->startOfDay();
return $date;
}
static function getScheduledQueues($future = false) {
$dateWhere = ($future) ? 'whereGt' : 'whereLte';
return SendingQueue::where('type', self::TASK_TYPE)
->$dateWhere('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->whereNull('deleted_at')
->where('status', SendingQueue::STATUS_SCHEDULED)
->findMany();
}
static function getRunningQueues() {
return SendingQueue::where('type', self::TASK_TYPE)
->whereLte('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->whereNull('deleted_at')
->whereNull('status')
->findMany();
}
static function getAllDueQueues() {
$scheduled_queues = self::getScheduledQueues();
$running_queues = self::getRunningQueues();
return array_merge((array)$scheduled_queues, (array)$running_queues);
}
static function getFutureQueues() {
return self::getScheduledQueues(true);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace MailPoet\Cron\Workers\KeyCheck;
use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\SimpleWorker;
use MailPoet\Models\SendingQueue;
use MailPoet\Services\Bridge;
if(!defined('ABSPATH')) exit;
abstract class KeyCheckWorker extends SimpleWorker {
const UNAVAILABLE_SERVICE_RESCHEDULE_TIMEOUT = 60;
public $bridge;
function init() {
if(!$this->bridge) {
$this->bridge = new Bridge();
}
}
function processQueueStrategy(SendingQueue $queue) {
try {
$result = $this->checkKey();
} catch (\Exception $e) {
$result = false;
}
if(empty($result['code']) || $result['code'] == Bridge::CHECK_ERROR_UNAVAILABLE) {
$this->reschedule($queue, self::UNAVAILABLE_SERVICE_RESCHEDULE_TIMEOUT);
return false;
}
return true;
}
abstract function checkKey();
}

View File

@ -0,0 +1,21 @@
<?php
namespace MailPoet\Cron\Workers\KeyCheck;
use MailPoet\Models\Setting;
use MailPoet\Services\Bridge;
if(!defined('ABSPATH')) exit;
class PremiumKeyCheck extends KeyCheckWorker {
const TASK_TYPE = 'premium_key_check';
function checkProcessingRequirements() {
return Bridge::isPremiumKeySpecified();
}
function checkKey() {
$premium_key = Setting::getValue(Bridge::PREMIUM_KEY_SETTING_NAME);
$result = $this->bridge->checkPremiumKey($premium_key);
return $result;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace MailPoet\Cron\Workers\KeyCheck;
use MailPoet\Mailer\Mailer;
use MailPoet\Services\Bridge;
if(!defined('ABSPATH')) exit;
class SendingServiceKeyCheck extends KeyCheckWorker {
const TASK_TYPE = 'sending_service_key_check';
function checkProcessingRequirements() {
return Bridge::isMPSendingServiceEnabled();
}
function checkKey() {
$mailer_config = Mailer::getMailerConfig();
$result = $this->bridge->checkMSSKey($mailer_config['mailpoet_api_key']);
$this->bridge->updateSubscriberCount($result);
return $result;
}
}

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