diff --git a/assets/js/src/newsletter_editor/blocks/automatedLatestContent.js b/assets/js/src/newsletter_editor/blocks/automatedLatestContent.js index e86c8b0779..3ed52fa77b 100644 --- a/assets/js/src/newsletter_editor/blocks/automatedLatestContent.js +++ b/assets/js/src/newsletter_editor/blocks/automatedLatestContent.js @@ -9,10 +9,12 @@ define([ 'newsletter_editor/App', 'newsletter_editor/blocks/base', + 'newsletter_editor/blocks/button', + 'newsletter_editor/blocks/divider', 'newsletter_editor/components/wordpress', 'underscore', 'jquery', - ], function(App, BaseBlock, WordpressComponent, _, jQuery) { + ], function(App, BaseBlock, ButtonBlock, DividerBlock, WordpressComponent, _, jQuery) { "use strict"; @@ -249,7 +251,7 @@ define([ } }, showButtonSettings: function(event) { - var buttonModule = App.module('blocks.button'); + var buttonModule = ButtonBlock; (new buttonModule.ButtonBlockSettingsView({ model: this.model.get('readMoreButton'), renderOptions: { @@ -260,7 +262,7 @@ define([ })).render(); }, showDividerSettings: function(event) { - var dividerModule = App.module('blocks.divider'); + var dividerModule = DividerBlock; (new dividerModule.DividerBlockSettingsView({ model: this.model.get('divider'), renderOptions: { diff --git a/assets/js/src/newsletter_editor/blocks/posts.js b/assets/js/src/newsletter_editor/blocks/posts.js index 547a421e32..39e4525a76 100644 --- a/assets/js/src/newsletter_editor/blocks/posts.js +++ b/assets/js/src/newsletter_editor/blocks/posts.js @@ -20,7 +20,9 @@ define([ 'newsletter_editor/App', 'newsletter_editor/components/wordpress', 'newsletter_editor/blocks/base', - ], function(Backbone, Marionette, Radio, _, jQuery, MailPoet, App, WordpressComponent, BaseBlock) { + 'newsletter_editor/blocks/button', + 'newsletter_editor/blocks/divider', + ], function(Backbone, Marionette, Radio, _, jQuery, MailPoet, App, WordpressComponent, BaseBlock, ButtonBlock, DividerBlock) { "use strict"; @@ -115,6 +117,12 @@ define([ if (data.posts.length === 0) return; + WordpressComponent.getTransformedPosts(data).done(function(posts) { + console.log('Available posts fetched', arguments); + collection.add(posts, { at: index }); + }).fail(function() { + console.log('Posts fetchPosts error', arguments); + }); // TODO: Move query logic to new AJAX format //mailpoet_post_wpi('automated_latest_content.php', data, function(response) { //console.log('Available posts fetched', arguments); @@ -437,7 +445,7 @@ define([ }; }, showButtonSettings: function(event) { - var buttonModule = App.module('blocks.button'); + var buttonModule = ButtonBlock; (new buttonModule.ButtonBlockSettingsView({ model: this.model.get('readMoreButton'), renderOptions: { @@ -448,7 +456,7 @@ define([ })).render(); }, showDividerSettings: function(event) { - var dividerModule = App.module('blocks.divider'); + var dividerModule = DividerBlock; (new dividerModule.DividerBlockSettingsView({ model: this.model.get('divider'), renderOptions: { diff --git a/assets/js/src/newsletter_editor/components/wordpress.js b/assets/js/src/newsletter_editor/components/wordpress.js index f45ae1fd42..2b21cba21c 100644 --- a/assets/js/src/newsletter_editor/components/wordpress.js +++ b/assets/js/src/newsletter_editor/components/wordpress.js @@ -9,7 +9,8 @@ define([ var postTypesCache, taxonomiesCache = {}, termsCache = {}, - postsCache = {}; + postsCache = {}, + transformedPostsCache = {}; Module.getPostTypes = function() { if (!postTypesCache) { @@ -65,6 +66,19 @@ define([ return postsCache[key]; }; + Module.getTransformedPosts = function(options) { + var key = JSON.stringify(options); + if (!transformedPostsCache[key]) { + transformedPostsCache[key] = MailPoet.Ajax.post({ + endpoint: 'wordpress', + action: 'getTransformedPosts', + data: options || {}, + }); + } + + return transformedPostsCache[key]; + }; + App.on('start', function(options) { // Prefetch post types Module.getPostTypes(); diff --git a/lib/Newsletter/PostsTransformer.php b/lib/Newsletter/PostsTransformer.php new file mode 100644 index 0000000000..e26b0847f1 --- /dev/null +++ b/lib/Newsletter/PostsTransformer.php @@ -0,0 +1,357 @@ + $post) { + if ($title_list_only) { + $results[] = self::getPostTitle($post, $args); + } else { + $postJSON = self::postToEditorJson($post, $args); + + $results = array_merge($results, $postJSON); + + if ($use_divider && $index + 1 < $total_posts) { + $results[] = $args['divider']; + } + } + } + + if ($title_list_only && !empty($results)) { + $results = array( + array( + 'type' => 'text', + 'text' => '', + ), + ); + } + + return $results; + } + + private static function postToEditorJson($post, $args) { + if ($args['displayType'] === 'titleOnly') { + $content = ''; + } elseif ($args['displayType'] === 'excerpt') { + // get excerpt + if(!empty($post->post_excerpt)) { + $content = $post->post_excerpt; + } else { + // if excerpt is empty then try to find the "more" tag + $excerpts = explode('', $post->post_content); + if (count($excerpts) > 1) { + // separator was present + $content = $excerpts[0]; + } else { + // Separator not present, try to shorten long posts + $content = self::postContentToExcerpt($post->post_content, self::MAX_EXCERPT_LENGTH); + } + } + } else { + $content = $post->post_content; + } + + if (strlen($post->post_content) < strlen($content)) { + $hideReadMore = true; + } else { + $hideReadMore = false; + } + + $content = self::stripShortCodes($content); + + // remove wysija nl shortcode + $content = preg_replace('/\
(.*?)\<\/div>/','',$content); + + // convert embedded content if necessary + $content = self::convertEmbeddedContent($content); + + // convert h4 h5 h6 to h3 + $content = preg_replace('/<([\/])?h[456](.*?)>/', '<$1h3$2>', $content); + + if ($args['titlePosition'] === 'aboveBlock') { + $content = self::getPostTitle($post, $args) . $content; + } + + // convert currency signs + $content = str_replace(array('$', '€', '£', '¥'), array('$', '€', '£', '¥'), $content); + + // strip useless tags + $tags_not_being_stripped = array('', '

','','','','','','

','

','

','','
    ','
      ','
    1. ','
      '); + $content = strip_tags($content, implode('',$tags_not_being_stripped)); + + $content = wpautop($content); + + if (!$hideReadMore && $args['readMoreType'] === 'link') { + $content .= '

      ' . stripslashes($args['readMoreText']) . '

      '; + } + + // Append author and categories above and below contents + foreach (array('above', 'below') as $position) { + $position_field = $position . 'Text'; + if ($args['showCategories'] === $position_field || $args['showAuthor'] === $position_field) { + $text = ''; + + if ($args['showAuthor'] === $position_field) { + $text .= self::getPostAuthor($args['authorPrecededBy'], $post->post_author); + } + + if ($args['showCategories'] === $position_field) { + if (!empty($text)) $text .= '
      '; + $text .= self::getPostCategories($args['categoriesPrecededBy'], $post); + } + + if (!empty($text)) $text = '

      ' . $text . '

      '; + if ($position === 'above') $content = $text . $content; + else if ($position === 'below') $content .= $text; + } + } + + $root = pQuery::parseStr($content); + + // Step 1: Hoist image tags to root level, preserving order and inserting + // images before top ancestor + + foreach ($root->query('img') as $item) { + $top_ancestor = self::findTopAncestor($item); + $offset = $top_ancestor->index(); + + if ($item->hasParent('a')) { + $item = $item->parent; + } + + $item->changeParent($root, $offset); + } + + // Step 2: Perform transformation: turn special tags to their respective + // JSON objects, turn other root children into text blocks + + $structure = array(); + + // Prepend featured image if current post has one + if(in_array($args['displayType'], array('full', 'excerpt')) && has_post_thumbnail($post->ID)) { + $thumbnail_id = get_post_thumbnail_id($post->ID); + + // get attachment data (src, width, height) + $image_info = wp_get_attachment_image_src($thumbnail_id, 'single-post-thumbnail'); + + // get alt text + $alt_text = trim(strip_tags(get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true))); + if(strlen($alt_text) === 0) { + // if the alt text is empty then use the post title + $alt_text = trim(strip_tags($post->post_title)); + } + + $structure[] = array( + 'type' => 'image', + 'link' => '', + 'src' => $image_info[0], + 'alt' => $alt_text, + 'padded' => (bool)$args['imagePadded'], + 'width' => $image_info[1], + 'height' => $image_info[2], + 'styles' => array( + 'block' => array( + 'textAlign' => 'center', + ), + ), + ); + } + + foreach ($root->children as $item) { + if ($item->tag === 'img' || $item->tag === 'a' && $item->query('img')) { + $link = ''; + $image = $item; + if ($item->tag === 'a') { + $link = $item->getAttribute('href'); + $image = $item->children[0]; + } + + $structure[] = array( + 'type' => 'image', + 'link' => $link, + 'src' => $image->getAttribute('src'), + 'alt' => $image->getAttribute('alt'), + 'padded' => (bool)$args['imagePadded'], + 'width' => $image->getAttribute('width'), + 'height' => $image->getAttribute('height'), + 'styles' => array( + 'block' => array( + 'textAlign' => 'center', + ), + ), + ); + } else { + $structure[] = array( + 'type' => 'text', + 'text' => $item->toString(), + ); + } + } + + // Step 3: Merge neighboring text blocks into one, remove empty text blocks + + $updated_structure = array(); + $text_accumulator = ''; + foreach ($structure as $item) { + if ($item['type'] === 'text') { + $text_accumulator .= $item['text']; + } + if ($item['type'] !== 'text') { + if (!empty($text_accumulator)) { + $updated_structure[] = array( + 'type' => 'text', + 'text' => trim($text_accumulator), + ); + $text_accumulator = ''; + } + $updated_structure[] = $item; + } + } + + if (!empty($text_accumulator)) { + $updated_structure[] = array( + 'type' => 'text', + 'text' => trim($text_accumulator), + ); + } + + + if ($args['titlePosition'] === 'inTextBlock') { + // Attach title to the first text block + $text_block_index = null; + foreach ($updated_structure as $index => $block) { + if ($block['type'] === 'text') { + $text_block_index = $index; + break; + } + } + + $title = self::getPostTitle($post, $args); + if ($text_block_index === null) { + $updated_structure[] = array( + 'type' => 'text', + 'text' => $title, + ); + } else { + $updated_structure[$text_block_index]['text'] = $title . $updated_structure[$text_block_index]['text']; + } + } + + if (!$hideReadMore && $args['readMoreType'] === 'button') { + $button = $args['readMoreButton']; + $button['url'] = get_permalink($post->ID); + $updated_structure[] = $button; + } + + return $updated_structure; + } + + private static function findTopAncestor($item) { + while ($item->parent->parent !== null) { + $item = $item->parent; + } + return $item; + } + + private static function stripShortCodes($content) { + if(strlen(trim($content)) === 0) { + return ''; + } + // remove captions + $content = preg_replace("/\[caption.*?\](.*<\/a>)(.*?)\[\/caption\]/", '$1', $content); + + // remove other shortcodes + $content = preg_replace('/\[[^\[\]]*\]/', '', $content); + + return $content; + } + + private static function convertEmbeddedContent($content = '') { + // remove embedded video and replace with links + $content = preg_replace('#<\/iframe>#', ''.__('Click here to view media.').'', $content); + + // replace youtube links + $content = preg_replace('#http://www.youtube.com/embed/([a-zA-Z0-9_-]*)#Ui', 'http://www.youtube.com/watch?v=$1', $content); + + return $content; + } + + private static function getPostAuthor($preceded_by, $author_id) { + + if(!empty($author_id)) { + $author_name = get_the_author_meta('display_name', (int)$author_id); + + // check if the user specified a label to be displayed before the author's name + if(strlen(trim($preceded_by)) > 0) { + $author_name = stripslashes(trim($preceded_by)).' '.$author_name; + } + return $author_name; + } + + return ''; + } + + private static function getPostCategories($preceded_by, $post) { + $content = ''; + + // Get categories + $categories = wp_get_post_terms($post->ID, get_object_taxonomies($post->post_type), array('fields' => 'names')); + if(!empty($categories)) { + // check if the user specified a label to be displayed before the author's name + if(strlen(trim($preceded_by)) > 0) { + $content = stripslashes(trim($preceded_by)).' '; + } + + $content .= join(', ', $categories); + } + + return $content; + } + + private static function getPostTitle($post, $args) { + $title = $post->post_title; + + if ((bool)$args['titleIsLink']) { + $title = '' . $title . ''; + } + + if (in_array($args['titleFormat'], array('h1', 'h2', 'h3'))) { + $tag = $args['titleFormat']; + } elseif ($args['titleFormat'] === 'ul') { + $tag = 'li'; + } else { + $tag = 'h1'; + } + + $alignment = (in_array($args['titleAlignment'], array('left', 'right', 'center'))) ? $args['titleAlignment'] : 'left'; + + return '<' . $tag . ' style="text-align: ' . $alignment . '">' . $title . ''; + } + + /** + * make an excerpt with a certain number of words + * @param type $text + * @param type $num_words + * @param type $more + * @return type + */ + private static function postContentToExcerpt($text, $num_words = 8, $more = ' …'){ + return wp_trim_words($text, $num_words, $more); + } +} diff --git a/lib/Router/Wordpress.php b/lib/Router/Wordpress.php index c03ddcbe30..2b8f72ca0b 100644 --- a/lib/Router/Wordpress.php +++ b/lib/Router/Wordpress.php @@ -1,6 +1,8 @@ constructTaxonomiesQuery($args); + + wp_send_json(get_posts($parameters)); + } + + function getTransformedPosts($args) { + $parameters = array( + 'posts_per_page' => (isset($args['amount'])) ? (int)$args['amount'] : 10, + 'post_type' => (isset($args['contentType'])) ? $args['contentType'] : 'post', + 'orderby' => 'date', + 'order' => ($args['sortBy'] === 'newest') ? 'DESC' : 'ASC', + ); + + if (isset($args['posts']) && is_array($args['posts'])) { + $parameters['post__in'] = $args['posts']; + } + + $parameters['tax_query'] = $this->constructTaxonomiesQuery($args); + + $posts = get_posts($parameters); + + wp_send_json(PostsTransformer::transform($posts, $args)); + } + + private function constructTaxonomiesQuery($args) { + $taxonomies_query = array(); + + if (isset($args['terms']) && is_array($args['terms'])) { // Add filtering by tags and categories $tags = array(); $categories = array(); @@ -52,7 +81,6 @@ class Wordpress { else if ($term['taxonomy'] === 'post_tag') $tags[] = $term['id']; } - $taxonomies_query = array(); foreach (array('post_tag' => $tags, 'category' => $categories) as $taxonomy => $terms) { if (!empty($terms)) { $tax = array( @@ -69,10 +97,11 @@ class Wordpress { // With exclusion we want to use 'AND', because we want posts that don't have excluded tags/categories // But with inclusion we want to use 'OR', because we want posts that have any of the included tags/categories $taxonomies_query['relation'] = ($args['inclusionType'] === 'exclude') ? 'AND' : 'OR'; - $parameters['tax_query'] = $taxonomies_query; + + return $taxonomies_query; } } - wp_send_json(get_posts($parameters)); + return $taxonomies_query; } }