PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-newsletter / src / class-freshly-pressed.php

class-freshly-pressed.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3, at jetpack_vendor/automattic/jetpack-newsletter/src/class-freshly-pressed.php

213 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Fetches the posts currently featured on WordPress.com's Freshly Pressed.
4 *
5 * @package automattic/jetpack-newsletter
6 */
7
8 declare( strict_types = 1 );
9
10 namespace Automattic\Jetpack\Newsletter;
11
12 use Automattic\Jetpack\Status\Host;
13
14 /**
15 * Read the WordPress.com Freshly Pressed feed: straight from the database on
16 * Simple sites, and over the public API everywhere else.
17 */
18 class Freshly_Pressed {
19 /**
20 * Where the cached list of posts is stored.
21 *
22 * @var string
23 */
24 const TRANSIENT_KEY = 'jetpack_newsletter_freshly_pressed';
25
26 /**
27 * How long a successful response is cached for, in seconds.
28 *
29 * @var int
30 */
31 const CACHE_TTL = HOUR_IN_SECONDS;
32
33 /**
34 * How long a failed response is cached for, in seconds.
35 *
36 * Shorter than a success so an outage recovers quickly, but long enough that
37 * it doesn't become an outgoing request on every dashboard load.
38 *
39 * @var int
40 */
41 const FAILURE_CACHE_TTL = 5 * MINUTE_IN_SECONDS;
42
43 /**
44 * The WordPress.com endpoint listing the Freshly Pressed posts.
45 *
46 * @var string
47 */
48 const API_URL = 'https://public-api.wordpress.com/rest/v1.1/freshly-pressed';
49
50 /**
51 * How many posts to show.
52 *
53 * @var int
54 */
55 const POST_COUNT = 10;
56
57 /**
58 * Get the posts currently featured on Freshly Pressed.
59 *
60 * @since 0.15.0
61 *
62 * @return array[] {
63 * @type string $title The post title, as returned by the API.
64 * @type string $permalink A WordPress.com Reader link to the post.
65 * @type int $blog_id The ID of the site the post belongs to.
66 * @type int $post_id The ID of the post.
67 * }
68 */
69 public static function get_posts(): array {
70 $cached = get_transient( self::TRANSIENT_KEY );
71 if ( is_array( $cached ) ) {
72 return $cached;
73 }
74
75 $posts = ( new Host() )->is_wpcom_simple()
76 ? self::query_wpcom_posts()
77 : self::fetch_posts_from_api();
78
79 // A null means the source failed; an empty array means it had nothing to
80 // show. Only the former is worth retrying soon.
81 set_transient(
82 self::TRANSIENT_KEY,
83 $posts ?? array(),
84 null === $posts ? self::FAILURE_CACHE_TTL : self::CACHE_TTL
85 );
86
87 return $posts ?? array();
88 }
89
90 /**
91 * Read the featured posts straight from the wpcom database.
92 *
93 * `FreshlyPressed::query()` only caches for logged-out visitors, so this is a
94 * real query every time and the caller's transient is what keeps it off the
95 * dashboard's critical path.
96 *
97 * @since 0.15.0
98 *
99 * @return array[]|null The posts, or null if the wpcom plugin isn't available.
100 */
101 private static function query_wpcom_posts(): ?array {
102 if ( ! class_exists( 'FreshlyPressed' ) ) {
103 $plugin = WP_CONTENT_DIR . '/plugins/freshly-pressed.php';
104 if ( ! file_exists( $plugin ) ) {
105 return null;
106 }
107 require_once $plugin;
108 }
109
110 // @phan-suppress-next-line PhanUndeclaredClassMethod -- wpcom-only class, guarded by the class_exists above. Remove once FreshlyPressed is added to wpcom's stub-defs.php and the regenerated stubs land.
111 $result = \FreshlyPressed::get_available_posts( array( 'number' => self::POST_COUNT ) );
112
113 if ( ! isset( $result['posts'] ) || ! is_array( $result['posts'] ) ) {
114 return null;
115 }
116
117 $posts = array();
118 foreach ( $result['posts'] as $featured ) {
119 if ( empty( $featured['blog_id'] ) || empty( $featured['post_id'] ) ) {
120 continue;
121 }
122
123 // A post can be unpublished or deleted after being featured.
124 $post = get_blog_post( $featured['blog_id'], $featured['post_id'] );
125 if ( empty( $post ) ) {
126 continue;
127 }
128
129 // An untitled post would render as a link with nothing to click.
130 $title = trim( (string) $post->post_title );
131 if ( '' === $title ) {
132 continue;
133 }
134
135 $posts[] = self::format_post(
136 (int) $featured['blog_id'],
137 (int) $featured['post_id'],
138 $title
139 );
140 }
141
142 return $posts;
143 }
144
145 /**
146 * Fetch the featured posts from the WordPress.com public API.
147 *
148 * @since 0.15.0
149 *
150 * @return array[]|null The posts, or null if the request failed.
151 */
152 private static function fetch_posts_from_api(): ?array {
153 $response = wp_remote_get(
154 add_query_arg(
155 array(
156 'number' => self::POST_COUNT,
157 // Without this the API returns the full content of every post.
158 'fields' => 'ID,site_ID,title',
159 ),
160 self::API_URL
161 ),
162 array( 'timeout' => 5 )
163 );
164
165 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
166 return null;
167 }
168
169 $body = json_decode( wp_remote_retrieve_body( $response ), true );
170 if ( ! isset( $body['posts'] ) || ! is_array( $body['posts'] ) ) {
171 return null;
172 }
173
174 $posts = array();
175 foreach ( $body['posts'] as $post ) {
176 // An untitled post would render as a link with nothing to click.
177 $title = trim( (string) ( $post['title'] ?? '' ) );
178 if ( empty( $post['ID'] ) || empty( $post['site_ID'] ) || '' === $title ) {
179 continue;
180 }
181
182 $posts[] = self::format_post( (int) $post['site_ID'], (int) $post['ID'], $title );
183 }
184
185 return $posts;
186 }
187
188 /**
189 * Shape one post the way the widget expects it.
190 *
191 * @since 0.15.0
192 *
193 * @param int $blog_id The ID of the site the post belongs to.
194 * @param int $post_id The ID of the post.
195 * @param string $title The post title.
196 * @return array The formatted post.
197 */
198 private static function format_post( int $blog_id, int $post_id, string $title ): array {
199 return array(
200 'title' => $title,
201 'permalink' => add_query_arg(
202 array(
203 'algo' => 'freshly-pressed',
204 'ref' => 'dashboard_widget',
205 ),
206 sprintf( 'https://wordpress.com/reader/blogs/%d/posts/%d', $blog_id, $post_id )
207 ),
208 'blog_id' => $blog_id,
209 'post_id' => $post_id,
210 );
211 }
212 }
213