Нет описания

AutomatedLatestContent.php 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <?php
  2. namespace MailPoet\Newsletter;
  3. if (!defined('ABSPATH')) exit;
  4. use DateTimeInterface;
  5. use MailPoet\Logging\LoggerFactory;
  6. use MailPoet\Newsletter\Editor\Transformer;
  7. use MailPoet\WP\Functions as WPFunctions;
  8. class AutomatedLatestContent {
  9. const DEFAULT_POSTS_PER_PAGE = 10;
  10. /** @var LoggerFactory */
  11. private $loggerFactory;
  12. /** @var int|false */
  13. private $newsletterId;
  14. /** @var WPFunctions */
  15. private $wp;
  16. public function __construct(
  17. LoggerFactory $loggerFactory,
  18. WPFunctions $wp
  19. ) {
  20. $this->loggerFactory = $loggerFactory;
  21. $this->wp = $wp;
  22. }
  23. public function filterOutSentPosts(string $where): string {
  24. $sentPostsQuery = 'SELECT ' . MP_NEWSLETTER_POSTS_TABLE . '.post_id FROM '
  25. . MP_NEWSLETTER_POSTS_TABLE . ' WHERE '
  26. . MP_NEWSLETTER_POSTS_TABLE . ".newsletter_id='" . $this->newsletterId . "'";
  27. $wherePostUnsent = 'ID NOT IN (' . $sentPostsQuery . ')';
  28. if (!empty($where)) $wherePostUnsent = ' AND ' . $wherePostUnsent;
  29. return $where . $wherePostUnsent;
  30. }
  31. public function ensureConsistentQueryType(\WP_Query $query) {
  32. // Queries with taxonomies are autodetected as 'is_archive=true' and 'is_home=false'
  33. // while queries without them end up being 'is_archive=false' and 'is_home=true'.
  34. // This is to fix that by always enforcing constistent behavior.
  35. $query->is_archive = true; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  36. $query->is_home = false; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  37. }
  38. public function getPosts($args, $postsToExclude = [], $newsletterId = false, $newerThanTimestamp = false) {
  39. $this->newsletterId = $newsletterId;
  40. // Get posts as logged out user, so private posts hidden by other plugins (e.g. UAM) are also excluded
  41. $currentUserId = $this->wp->getCurrentUserId();
  42. $this->wp->wpSetCurrentUser(0);
  43. $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->addInfo(
  44. 'loading automated latest content',
  45. ['args' => $args, 'posts_to_exclude' => $postsToExclude, 'newsletter_id' => $newsletterId, 'newer_than_timestamp' => $newerThanTimestamp]
  46. );
  47. $postsPerPage = (!empty($args['amount']) && (int)$args['amount'] > 0)
  48. ? (int)$args['amount']
  49. : self::DEFAULT_POSTS_PER_PAGE;
  50. $parameters = [
  51. 'posts_per_page' => $postsPerPage,
  52. 'post_type' => (isset($args['contentType'])) ? $args['contentType'] : 'post',
  53. 'post_status' => (isset($args['postStatus'])) ? $args['postStatus'] : 'publish',
  54. 'orderby' => 'date',
  55. 'order' => ($args['sortBy'] === 'newest') ? 'DESC' : 'ASC',
  56. ];
  57. if (!empty($args['offset']) && (int)$args['offset'] > 0) {
  58. $parameters['offset'] = (int)$args['offset'];
  59. }
  60. if (isset($args['search'])) {
  61. $parameters['s'] = $args['search'];
  62. }
  63. if (isset($args['posts']) && is_array($args['posts'])) {
  64. $parameters['post__in'] = $args['posts'];
  65. $parameters['posts_per_page'] = -1; // Get all posts with matching IDs
  66. }
  67. if (!empty($postsToExclude)) {
  68. $parameters['post__not_in'] = $postsToExclude;
  69. }
  70. $parameters['tax_query'] = $this->constructTaxonomiesQuery($args);
  71. // WP posts with the type attachment have always post_status `inherit`
  72. if ($parameters['post_type'] === 'attachment' && $parameters['post_status'] === 'publish') {
  73. $parameters['post_status'] = 'inherit';
  74. }
  75. // This enables using posts query filters for get_posts, where by default
  76. // it is disabled.
  77. // However, it also enables other plugins and themes to hook in and alter
  78. // the query.
  79. $parameters['suppress_filters'] = false;
  80. if ($newerThanTimestamp instanceof DateTimeInterface) {
  81. $parameters['date_query'] = [
  82. [
  83. 'column' => 'post_date',
  84. 'after' => $newerThanTimestamp->format('Y-m-d H:i:s'),
  85. ],
  86. ];
  87. }
  88. // set low priority to execute 'ensureConstistentQueryType' before any other filter
  89. $filterPriority = defined('PHP_INT_MIN') ? constant('PHP_INT_MIN') : ~PHP_INT_MAX;
  90. $this->wp->addAction('pre_get_posts', [$this, 'ensureConsistentQueryType'], $filterPriority);
  91. $this->_attachSentPostsFilter($newsletterId);
  92. $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->addInfo(
  93. 'getting automated latest content',
  94. ['parameters' => $parameters]
  95. );
  96. $posts = $this->wp->getPosts($parameters);
  97. $this->logPosts($posts);
  98. $this->wp->removeAction('pre_get_posts', [$this, 'ensureConsistentQueryType'], $filterPriority);
  99. $this->_detachSentPostsFilter($newsletterId);
  100. $this->wp->wpSetCurrentUser($currentUserId);
  101. return $posts;
  102. }
  103. public function transformPosts($args, $posts) {
  104. $transformer = new Transformer($args);
  105. return $transformer->transform($posts);
  106. }
  107. public function constructTaxonomiesQuery($args) {
  108. $taxonomiesQuery = [];
  109. if (isset($args['terms']) && is_array($args['terms'])) {
  110. $taxonomies = [];
  111. // Categorize terms based on their taxonomies
  112. foreach ($args['terms'] as $term) {
  113. $taxonomy = $term['taxonomy'];
  114. if (!isset($taxonomies[$taxonomy])) {
  115. $taxonomies[$taxonomy] = [];
  116. }
  117. $taxonomies[$taxonomy][] = $term['id'];
  118. }
  119. foreach ($taxonomies as $taxonomy => $terms) {
  120. if (!empty($terms)) {
  121. $tax = [
  122. 'taxonomy' => $taxonomy,
  123. 'field' => 'id',
  124. 'terms' => $terms,
  125. ];
  126. if ($args['inclusionType'] === 'exclude') $tax['operator'] = 'NOT IN';
  127. $taxonomiesQuery[] = $tax;
  128. }
  129. }
  130. if (!empty($taxonomiesQuery)) {
  131. // With exclusion we want to use 'AND', because we want posts that
  132. // don't have excluded tags/categories. But with inclusion we want to
  133. // use 'OR', because we want posts that have any of the included
  134. // tags/categories
  135. $taxonomiesQuery['relation'] = ($args['inclusionType'] === 'exclude') ? 'AND' : 'OR';
  136. }
  137. }
  138. // make $taxonomies_query nested to avoid conflicts with plugins that use taxonomies
  139. return empty($taxonomiesQuery) ? [] : [$taxonomiesQuery];
  140. }
  141. private function _attachSentPostsFilter($newsletterId) {
  142. if ($newsletterId > 0) {
  143. $this->wp->addAction('posts_where', [$this, 'filterOutSentPosts']);
  144. }
  145. }
  146. private function _detachSentPostsFilter($newsletterId) {
  147. if ($newsletterId > 0) {
  148. $this->wp->removeAction('posts_where', [$this, 'filterOutSentPosts']);
  149. }
  150. }
  151. private function logPosts(array $posts) {
  152. $postsToLog = [];
  153. foreach ($posts as $post) {
  154. $postsToLog[] = [
  155. 'id' => $post->ID,
  156. 'post_date' => $post->post_date, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  157. ];
  158. }
  159. $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->addInfo(
  160. 'automated latest content loaded posts',
  161. ['posts' => $postsToLog]
  162. );
  163. }
  164. }