PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 14.6.1
Jetpack – WP Security, Backup, Speed, & Growth v14.6.1
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / _inc / lib / class.media-summary.php
jetpack / _inc / lib Last commit date
admin-pages 1 year ago core-api 1 year ago debugger 1 year ago markdown 1 year ago class-jetpack-ai-helper.php 1 year ago class-jetpack-blog-stats-helper.php 1 year ago class-jetpack-currencies.php 2 years ago class-jetpack-instagram-gallery-helper.php 2 years ago class-jetpack-mapbox-helper.php 3 years ago class-jetpack-podcast-feed-locator.php 1 year ago class-jetpack-podcast-helper.php 1 year ago class-jetpack-recommendations.php 2 years ago class-jetpack-top-posts-helper.php 1 year ago class.color.php 2 years ago class.core-rest-api-endpoints.php 1 year ago class.jetpack-automatic-install-skin.php 2 years ago class.jetpack-iframe-embed.php 3 years ago class.jetpack-password-checker.php 2 years ago class.jetpack-search-performance-logger.php 4 years ago class.media-extractor.php 2 years ago class.media-summary.php 1 year ago class.media.php 2 years ago components.php 3 years ago debugger.php 2 years ago functions.wp-notify.php 1 year ago icalendar-reader.php 1 year ago markdown.php 3 years ago plans.php 4 years ago plugins.php 4 years ago tonesque.php 2 years ago widgets.php 2 years ago
class.media-summary.php
464 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Provides media summary of a post.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Image_CDN\Image_CDN_Core;
9
10 /**
11 * Class Jetpack_Media_Summary
12 *
13 * Priority: embed [video] > gallery > image > text
14 */
15 class Jetpack_Media_Summary {
16
17 /**
18 * Media cache.
19 *
20 * @var array
21 */
22 private static $cache = array();
23
24 /**
25 * Get media summary for a post.
26 *
27 * @param ?int $post_id Post ID.
28 * @param int $blog_id Blog ID, if applicable.
29 * @param array $args {
30 * Optional. An array of arguments.
31 * @type int $max_words Maximum number of words.
32 * @type int $max_chars Maximum number of characters.
33 * }
34 *
35 * @return array|mixed|void
36 */
37 public static function get( ?int $post_id, int $blog_id = 0, array $args = array() ) {
38 $post_id = (int) $post_id;
39 $blog_id = (int) $blog_id;
40
41 $defaults = array(
42 'max_words' => 16,
43 'max_chars' => 256,
44 );
45 $args = wp_parse_args( $args, $defaults );
46
47 $switched = false;
48 if ( ! empty( $blog_id ) && get_current_blog_id() !== $blog_id && function_exists( 'switch_to_blog' ) ) {
49 switch_to_blog( $blog_id );
50 $switched = true;
51 } else {
52 $blog_id = get_current_blog_id();
53 }
54
55 $cache_key = "{$blog_id}_{$post_id}_{$args['max_words']}_{$args['max_chars']}";
56 if ( isset( self::$cache[ $cache_key ] ) ) {
57 if ( $switched ) {
58 restore_current_blog();
59 }
60 return self::$cache[ $cache_key ];
61 }
62
63 if ( ! class_exists( 'Jetpack_Media_Meta_Extractor' ) ) {
64 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media-extractor.php';
65 }
66
67 $post = get_post( $post_id );
68 $permalink = get_permalink( $post_id );
69
70 $return = array(
71 'type' => 'standard',
72 'permalink' => $permalink,
73 'image' => '',
74 'excerpt' => '',
75 'word_count' => 0,
76 'secure' => array(
77 'image' => '',
78 ),
79 'count' => array(
80 'image' => 0,
81 'video' => 0,
82 'word' => 0,
83 'link' => 0,
84 ),
85 );
86
87 if ( $post instanceof WP_Post && empty( $post->post_password ) ) {
88 $return['excerpt'] = self::get_excerpt( $post->post_content, $post->post_excerpt, $args['max_words'], $args['max_chars'], $post );
89 $return['count']['word'] = self::get_word_count( $post->post_content );
90 $return['count']['word_remaining'] = self::get_word_remaining_count( $post->post_content, $return['excerpt'] );
91 $return['count']['link'] = self::get_link_count( $post->post_content );
92 }
93
94 $extract = Jetpack_Media_Meta_Extractor::extract( $blog_id, $post_id, Jetpack_Media_Meta_Extractor::ALL );
95
96 if ( empty( $extract['has'] ) ) {
97 if ( $switched ) {
98 restore_current_blog();
99 }
100 self::$cache[ $cache_key ] = $return;
101 return $return;
102 }
103
104 // Prioritize [some] video embeds.
105 if ( ! empty( $extract['has']['shortcode'] ) ) {
106 foreach ( $extract['shortcode'] as $type => $data ) {
107 switch ( $type ) {
108 case 'videopress':
109 case 'wpvideo':
110 if ( 0 === $return['count']['video'] ) {
111 // If there is no id on the video, then let's just skip this.
112 if ( ! isset( $data['id'][0] ) ) {
113 break;
114 }
115
116 $guid = $data['id'][0];
117 $video_info = videopress_get_video_details( $guid );
118
119 // Only add the video tags if the guid returns a valid videopress object.
120 if ( $video_info instanceof stdClass ) {
121 // Continue early if we can't find a Video slug.
122 if ( empty( $video_info->files->std->mp4 ) ) {
123 break;
124 }
125
126 $url = sprintf(
127 'https://videos.files.wordpress.com/%1$s/%2$s',
128 $guid,
129 $video_info->files->std->mp4
130 );
131
132 $thumbnail = $video_info->poster;
133 if ( ! empty( $thumbnail ) ) {
134 $return['image'] = $thumbnail;
135 $return['secure']['image'] = $thumbnail;
136 }
137
138 $return['type'] = 'video';
139 $return['video'] = esc_url_raw( $url );
140 $return['video_type'] = 'video/mp4';
141 $return['secure']['video'] = $return['video'];
142 }
143 }
144 ++$return['count']['video'];
145 break;
146 case 'youtube':
147 if ( 0 === $return['count']['video'] ) {
148 if ( ! isset( $extract['shortcode']['youtube']['id'][0] ) ) {
149 break;
150 }
151 $return['type'] = 'video';
152 $return['video'] = esc_url_raw( 'http://www.youtube.com/watch?feature=player_embedded&v=' . $extract['shortcode']['youtube']['id'][0] );
153 $return['image'] = self::get_video_poster( 'youtube', $extract['shortcode']['youtube']['id'][0] );
154 $return['secure']['video'] = self::https( $return['video'] );
155 $return['secure']['image'] = self::https( $return['image'] );
156 }
157 ++$return['count']['video'];
158 break;
159 case 'vimeo':
160 if ( 0 === $return['count']['video'] ) {
161 $return['type'] = 'video';
162 $return['video'] = esc_url_raw( 'http://vimeo.com/' . $extract['shortcode']['vimeo']['id'][0] );
163 $return['secure']['video'] = self::https( $return['video'] );
164
165 $poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
166 if ( ! empty( $poster_image ) ) {
167 $return['image'] = $poster_image;
168 $poster_url_parts = wp_parse_url( $poster_image );
169 $return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
170 }
171 }
172 ++$return['count']['video'];
173 break;
174 }
175 }
176 }
177
178 if ( ! empty( $extract['has']['embed'] ) ) {
179 foreach ( $extract['embed']['url'] as $embed ) {
180 if ( preg_match( '/((youtube|vimeo|dailymotion)\.com|youtu.be)/', $embed ) ) {
181 if ( 0 === $return['count']['video'] ) {
182 $return['type'] = 'video';
183 $return['video'] = 'http://' . $embed;
184 $return['secure']['video'] = self::https( $return['video'] );
185 if ( str_contains( $embed, 'youtube' ) ) {
186 $return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
187 $return['secure']['image'] = self::https( $return['image'] );
188 } elseif ( str_contains( $embed, 'youtu.be' ) ) {
189 $youtube_id = jetpack_get_youtube_id( $return['video'] );
190 $return['video'] = 'http://youtube.com/watch?v=' . $youtube_id . '&feature=youtu.be';
191 $return['secure']['video'] = self::https( $return['video'] );
192 $return['image'] = self::get_video_poster( 'youtube', jetpack_get_youtube_id( $return['video'] ) );
193 $return['secure']['image'] = self::https( $return['image'] );
194 } elseif ( str_contains( $embed, 'vimeo' ) ) {
195 $poster_image = get_post_meta( $post_id, 'vimeo_poster_image', true );
196 if ( ! empty( $poster_image ) ) {
197 $return['image'] = $poster_image;
198 $poster_url_parts = wp_parse_url( $poster_image );
199 $return['secure']['image'] = 'https://secure-a.vimeocdn.com' . $poster_url_parts['path'];
200 }
201 } elseif ( str_contains( $embed, 'dailymotion' ) ) {
202 $return['image'] = str_replace( 'dailymotion.com/video/', 'dailymotion.com/thumbnail/video/', $embed );
203 $return['image'] = wp_parse_url( $return['image'], PHP_URL_SCHEME ) === null ? 'http://' . $return['image'] : $return['image'];
204 $return['secure']['image'] = self::https( $return['image'] );
205 }
206 }
207 ++$return['count']['video'];
208 }
209 }
210 }
211
212 // Do we really want to make the video the primary focus of the post?
213 if ( 'video' === $return['type'] ) {
214 $content = wpautop( wp_strip_all_tags( $post->post_content ) );
215 $paragraphs = explode( '</p>', $content );
216 $number_of_paragraphs = 0;
217
218 foreach ( $paragraphs as $i => $paragraph ) {
219 // Don't include blank lines as a paragraph.
220 if ( '' === trim( $paragraph ) ) {
221 unset( $paragraphs[ $i ] );
222 continue;
223 }
224 ++$number_of_paragraphs;
225 }
226
227 $number_of_paragraphs = $number_of_paragraphs - $return['count']['video']; // subtract amount for videos.
228
229 // More than 2 paragraph? The video is not the primary focus so we can do some more analysis.
230 if ( $number_of_paragraphs > 2 ) {
231 $return['type'] = 'standard';
232 }
233 }
234
235 // If we don't have any prioritized embed...
236 if ( 'standard' === $return['type'] ) {
237 if ( ( ! empty( $extract['has']['gallery'] ) || ! empty( $extract['shortcode']['gallery']['count'] ) ) && ! empty( $extract['image'] ) ) {
238 // ... Then we prioritize galleries first (multiple images returned)
239 $return['type'] = 'gallery';
240 $return['images'] = $extract['image'];
241 foreach ( $return['images'] as $image ) {
242 $return['secure']['images'][] = array( 'url' => self::ssl_img( $image['url'] ) );
243 ++$return['count']['image'];
244 }
245 } elseif ( ! empty( $extract['has']['image'] ) ) {
246 // ... Or we try and select a single image that would make sense.
247 $content = wpautop( wp_strip_all_tags( $post->post_content ) );
248 $paragraphs = explode( '</p>', $content );
249 $number_of_paragraphs = 0;
250
251 foreach ( $paragraphs as $i => $paragraph ) {
252 // Don't include 'actual' captions as a paragraph.
253 if ( str_contains( $paragraph, '[caption' ) ) {
254 unset( $paragraphs[ $i ] );
255 continue;
256 }
257 // Don't include blank lines as a paragraph.
258 if ( '' === trim( $paragraph ) ) {
259 unset( $paragraphs[ $i ] );
260 continue;
261 }
262 ++$number_of_paragraphs;
263 }
264
265 $return['image'] = $extract['image'][0]['url'];
266 $return['secure']['image'] = self::ssl_img( $return['image'] );
267 ++$return['count']['image'];
268
269 if ( $number_of_paragraphs <= 2 && is_countable( $extract['image'] ) && 1 === count( $extract['image'] ) ) {
270 // If we have lots of text or images, let's not treat it as an image post, but return its first image.
271 $return['type'] = 'image';
272 }
273 }
274 }
275
276 if ( $switched ) {
277 restore_current_blog();
278 }
279
280 /**
281 * Allow a theme or plugin to inspect and ultimately change the media summary.
282 *
283 * @since 4.4.0
284 *
285 * @param array $data The calculated media summary data.
286 * @param int $post_id The id of the post this data applies to.
287 */
288 $return = apply_filters( 'jetpack_media_summary_output', $return, $post_id );
289
290 self::$cache[ $cache_key ] = $return;
291
292 return $return;
293 }
294
295 /**
296 * Converts http to https://
297 *
298 * @param string $str URL.
299 *
300 * @return string URL.
301 */
302 public static function https( $str ) {
303 return str_replace( 'http://', 'https://', $str );
304 }
305
306 /**
307 * Returns a Photonized version of the URL.
308 *
309 * @param string $url URL.
310 *
311 * @return string URL.
312 */
313 public static function ssl_img( $url ) {
314 if ( str_contains( $url, 'files.wordpress.com' ) ) {
315 return self::https( $url );
316 } else {
317 return self::https( Image_CDN_Core::cdn_url( $url ) );
318 }
319 }
320
321 /**
322 * Get the video poster.
323 *
324 * @param string $type Video service.
325 * @param string $id Video ID for the service.
326 *
327 * @return string URL of image thumbnail for the video.
328 */
329 public static function get_video_poster( $type, $id ) {
330 if ( 'videopress' === $type ) {
331 if ( function_exists( 'video_get_highest_resolution_image_url' ) ) {
332 return video_get_highest_resolution_image_url( $id );
333 } elseif ( class_exists( 'VideoPress_Video' ) ) {
334 $video = new VideoPress_Video( $id );
335 return $video->poster_frame_uri;
336 }
337 } elseif ( 'youtube' === $type ) {
338 return 'http://img.youtube.com/vi/' . $id . '/0.jpg';
339 }
340 }
341
342 /**
343 * Clean text of shortcodes and tags.
344 *
345 * @param string $text Dirty text.
346 *
347 * @return string Clean text.
348 */
349 public static function clean_text( $text ) {
350 return trim(
351 preg_replace(
352 '/[\s]+/',
353 ' ',
354 preg_replace(
355 '@https?://[\S]+@',
356 '',
357 strip_shortcodes(
358 wp_strip_all_tags(
359 $text
360 )
361 )
362 )
363 )
364 );
365 }
366
367 /**
368 * Retrieve an excerpt for the post summary.
369 *
370 * This function works around a suspected problem with Core. If resolved, this function should be simplified.
371 *
372 * @link https://github.com/Automattic/jetpack/pull/8510
373 * @link https://core.trac.wordpress.org/ticket/42814
374 *
375 * @param string $post_content The post's content.
376 * @param string $post_excerpt The post's excerpt. Empty if none was explicitly set.
377 * @param int $max_words Maximum number of words for the excerpt. Used on wp.com. Default 16.
378 * @param int $max_chars Maximum characters in the excerpt. Used on wp.com. Default 256.
379 * @param WP_Post $requested_post The post object.
380 * @return string Post excerpt.
381 **/
382 public static function get_excerpt( $post_content, $post_excerpt, $max_words = 16, $max_chars = 256, $requested_post = null ) {
383 global $post;
384 $original_post = $post; // Saving the global for later use.
385 if ( empty( $post_excerpt ) && function_exists( 'wpcom_enhanced_excerpt_extract_excerpt' ) ) {
386 return self::clean_text(
387 wpcom_enhanced_excerpt_extract_excerpt(
388 array(
389 'text' => $post_content,
390 'excerpt_only' => true,
391 'show_read_more' => false,
392 'max_words' => $max_words,
393 'max_chars' => $max_chars,
394 'read_more_threshold' => 25,
395 )
396 )
397 );
398 } elseif ( $requested_post instanceof WP_Post ) {
399 // @todo Refactor to not need to override the global.
400 // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
401 $post = $requested_post; // setup_postdata does not set the global.
402 setup_postdata( $post );
403 /** This filter is documented in core/src/wp-includes/post-template.php */
404 $post_excerpt = apply_filters( 'get_the_excerpt', $post_excerpt, $post );
405 // phpcs:ignore: WordPress.WP.GlobalVariablesOverride.Prohibited
406 $post = $original_post; // wp_reset_postdata uses the $post global.
407 wp_reset_postdata();
408 return self::clean_text( $post_excerpt );
409 }
410 return '';
411 }
412
413 /**
414 * Split a string into an array of words.
415 *
416 * @param string $text Post content or excerpt.
417 *
418 * @return array Array of words.
419 */
420 public static function split_content_in_words( $text ) {
421 $words = preg_split( '/[\s!?;,.]+/', $text, -1, PREG_SPLIT_NO_EMPTY );
422
423 // Return an empty array if the split above fails.
424 return $words ? $words : array();
425 }
426
427 /**
428 * Get the word count.
429 *
430 * @param string $post_content Post content.
431 *
432 * @return int Word count.
433 */
434 public static function get_word_count( $post_content ) {
435 return (int) count( self::split_content_in_words( self::clean_text( $post_content ) ) );
436 }
437
438 /**
439 * Get remainder word count (after the excerpt).
440 *
441 * @param string $post_content Post content.
442 * @param string $excerpt_content Excerpt content.
443 *
444 * @return int Number of words after the excerpt.
445 */
446 public static function get_word_remaining_count( $post_content, $excerpt_content ) {
447 $content_word_count = count( self::split_content_in_words( self::clean_text( $post_content ) ) );
448 $excerpt_word_count = count( self::split_content_in_words( self::clean_text( $excerpt_content ) ) );
449
450 return (int) $content_word_count - $excerpt_word_count;
451 }
452
453 /**
454 * Counts the number of links in a post.
455 *
456 * @param string $post_content Post content.
457 *
458 * @return false|int Number of links.
459 */
460 public static function get_link_count( $post_content ) {
461 return preg_match_all( '/\<a[\> ]/', $post_content, $matches );
462 }
463 }
464