PluginProbe ʕ •ᴥ•ʔ
WP Popular Posts / 7.4.1
WP Popular Posts v7.4.1
7.4.1 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 5.0.0 5.0.1 5.0.2 5.1.0 5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.2.0 6.2.1 6.3.0 6.3.1 6.3.2 6.3.3 6.3.4 6.4.0 6.4.1 6.4.2 7.0.0 7.0.1 7.1.0 7.2.0 7.3.0 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6 7.3.7 7.3.8 7.4.0 trunk 2.3.7 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.2 4.0.3 4.0.5 4.0.6
wordpress-popular-posts / src / Output.php
wordpress-popular-posts / src Last commit date
Activation 10 months ago Admin 5 days ago Block 5 days ago Compatibility 10 months ago Container 10 months ago Front 5 days ago Rest 10 months ago Shortcode 1 year ago Traits 4 years ago Widget 5 days ago Bootstrap.php 5 years ago Cache.php 10 months ago Helper.php 5 days ago I18N.php 5 years ago Image.php 10 months ago Output.php 5 days ago Query.php 5 days ago Settings.php 5 days ago Themer.php 10 months ago Translate.php 3 years ago Upgrader.php 10 months ago WordPressPopularPosts.php 10 months ago deprecated.php 4 years ago template-tags.php 5 days ago
Output.php
1160 lines
1 <?php
2 /**
3 * This class formats the HTML output of every popular posts listing.
4 *
5 *
6 * @package WordPressPopularPosts
7 * @author Hector Cabrera <me@cabrerahector.com>
8 */
9
10 namespace WordPressPopularPosts;
11
12 class Output {
13
14 /**
15 * Popular posts data.
16 *
17 * @since 4.0.0
18 * @var string
19 */
20 private $data;
21
22 /**
23 * HTML output.
24 *
25 * @since 4.0.0
26 * @var string
27 */
28 private $output;
29
30 /**
31 * Widget / shortcode settings.
32 *
33 * @since 4.0.0
34 * @var array
35 */
36 private $public_options = [];
37
38 /**
39 * Administrative settings.
40 *
41 * @since 2.3.3
42 * @var array
43 */
44 private $admin_options = [];
45
46 /**
47 * Default excerpt 'more' string.
48 *
49 * @since 4.2.1
50 * @var string
51 */
52 private $more;
53
54 /**
55 * Image object
56 *
57 * @since 4.0.2
58 * @var WordPressPopularPosts\Image
59 */
60 private $thumbnail;
61
62 /**
63 * Translate object.
64 *
65 * @var \WordPressPopularPosts\Translate $translate
66 * @access private
67 */
68 private $translate;
69
70 /**
71 * Themer object.
72 *
73 * @var \WordPressPopularPosts\Themer $themer
74 * @access private
75 */
76 private $themer;
77
78 /**
79 * WordPress Date format.
80 *
81 * @var string
82 * @access private
83 */
84 private $wp_date_format;
85
86 /**
87 * Constructor.
88 *
89 * @since 4.0.0
90 * @param array $public_options
91 * @param array $admin_options
92 * @param WordPressPopularPosts\Image $thumbnail
93 * @param WordPressPopularPosts\Translate $translate
94 * @param WordPressPopularPosts\Themer $themer
95 */
96 public function __construct(array $public_options, array $admin_options, Image $thumbnail, Translate $translate, Themer $themer)
97 {
98 $this->public_options = $public_options;
99 $this->admin_options = $admin_options;
100 $this->thumbnail = $thumbnail;
101 $this->translate = $translate;
102 $this->themer = $themer;
103
104 $this->more = '...';
105
106 $this->wp_date_format = get_option('date_format');
107
108 if ( ! $this->wp_date_format ) {
109 $this->wp_date_format = 'F j, Y';
110 }
111 }
112
113 /**
114 * Sets data.
115 *
116 * @since 5.0.0
117 * @param array
118 */
119 public function set_data(array $data = [])
120 {
121 $this->data = $data;
122 }
123
124 /**
125 * Sets public options.
126 *
127 * @since 5.0.0
128 * @param array
129 */
130 public function set_public_options(array $public_options = [])
131 {
132 $this->public_options = Helper::merge_array_r(
133 Settings::get('widget_options'),
134 $public_options
135 );
136 }
137
138 /**
139 * Output the HTML.
140 *
141 * @since 4.0.0
142 */
143 public function output()
144 {
145 echo $this->get_output(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- At this point everything has been escaped / sanitized already
146 }
147
148 /**
149 * Return the HTML.
150 *
151 * @since 4.0.0
152 * @return string
153 */
154 public function get_output()
155 {
156 $this->output = $this->get_clean_output();
157 $this->output = ( WP_DEBUG ? "\n" . '<!-- WP Popular Posts v' . WPP_VERSION . ( $this->admin_options['tools']['cache']['active'] ? ' - cached' : '' ) . ' -->' . "\n" : '' ) . $this->output;
158 return $this->output;
159 }
160
161 /**
162 * Returns a "clean" version of the HTML output.
163 *
164 * @since 7.3.7
165 * @return string
166 */
167 private function get_clean_output()
168 {
169 if ( $this->output ) {
170 // Attempt to close open tags
171 $this->output = force_balance_tags($this->output);
172
173 /**
174 * @ToDo
175 *
176 * Look into \Dom\HTMLDocument (PHP 8.4 apparently) to see
177 * if it's a good alternative to the code below.
178 */
179
180 if ( extension_loaded('mbstring') && function_exists('mb_encode_numericentity') ) {
181 $clean_html = '';
182 $html = trim($this->output);
183
184 // Process special characters
185 $html = htmlspecialchars_decode(mb_encode_numericentity(htmlentities($html, ENT_QUOTES, 'UTF-8'), [0x80, 0x10FFFF, 0, ~0], 'UTF-8'));
186
187 // Remove empty tags
188 $html = '<!DOCTYPE html><html><head><meta charset="UTF-8" /></head><body>' . $html . '</body></html>';
189
190 $dom = new \DOMDocument();
191 $dom->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
192 $xpath = new \DOMXPath($dom);
193
194 // Void/empty tags that should never be removed
195 $voids = ['br', 'div', 'hr', 'img', 'source', 'span'];
196 $voidPred = implode(' or ', array_map(function($n) { return "name() = '" . $n . "'"; }, $voids));
197
198 // Build XPath predicate
199 $query = "//*[not(normalize-space()) and not(*) and not({$voidPred})]";
200
201 // Iterate until no more removable nodes found (because removing can create new empty tags)
202 do {
203 $removed = false;
204 $nodes = $xpath->query($query);
205
206 if ( $nodes === null || $nodes->length === 0 ) {
207 break;
208 }
209
210 // Collect nodes first to avoid live-node issues
211 $toRemove = [];
212
213 foreach( $nodes as $node ) {
214 $toRemove[] = $node;
215 }
216
217 foreach( $toRemove as $node ) {
218 if ($node->parentNode) {
219 $node->parentNode->removeChild($node);
220 $removed = true;
221 }
222 }
223 } while( $removed );
224
225 $body = $dom->getElementsByTagName('body')->item(0);
226
227 if ( isset($body->childNodes) ) {
228 foreach( $body->childNodes as $node ) {
229 $clean_html .= $dom->saveHTML($node);
230 }
231 }
232
233 $this->output = trim($clean_html);
234 } else {
235 if ( defined('WP_DEBUG') && WP_DEBUG ) {
236 trigger_error('WP Popular Posts - looks like PHP\'s mbstring extension isn\'t enabled on this site. Please enable it for the plugin to be able to properly format your popular post list.', E_USER_WARNING);
237 }
238 }
239
240 // Remove excess line jumps
241 // @TODO
242 // Causes encoding artifacts in some languages
243 // See: https://wordpress.org/support/topic/encoding-issue-with-titles-and-excerpts-in-wpp-shortcode/
244 // Using the /u modifier may fix this, requires further testing
245 // $this->output = preg_replace('/\R+/', "\n", $this->output);
246
247 // Sanitize HTML
248 $this->output = Helper::sanitize_html($this->output, $this->public_options);
249 }
250
251 return $this->output;
252 }
253
254 /**
255 * Build the HTML output.
256 *
257 * @since 4.0.0
258 */
259 public function build_output()
260 {
261 // Got some posts, format 'em!
262 if ( ! empty($this->data) ) {
263
264 $this->output = '';
265
266 // Allow WP themers / coders access to raw data
267 // so they can build their own output
268 if ( has_filter('wpp_custom_html') ) {
269 $this->output .= apply_filters('wpp_custom_html', $this->data, $this->public_options);
270 return;
271 }
272
273 if (
274 isset($this->public_options['theme']['name'])
275 && $this->public_options['theme']['name']
276 ) {
277 $this->output .= '<div class="popular-posts-sr">';
278
279 if ( @file_exists(get_stylesheet_directory() . '/wordpress-popular-posts/themes/' . $this->public_options['theme']['name'] . '/style.css') ) {
280 $theme_stylesheet = get_stylesheet_directory() . '/wordpress-popular-posts/themes/' . $this->public_options['theme']['name'] . '/style.css';
281 } else {
282 $theme_stylesheet = $this->themer->get_theme($this->public_options['theme']['name'])['path'] . '/style.css';
283 }
284
285 $theme_css_rules = wp_strip_all_tags(file_get_contents($theme_stylesheet), true); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- We're loading a local file
286 $additional_styles = '';
287
288 if ( has_filter('wpp_additional_theme_styles') ) {
289 $additional_styles = wp_strip_all_tags(apply_filters('wpp_additional_theme_styles', '', $this->public_options['theme']['name']), true);
290
291 if ( $additional_styles ) {
292 $additional_styles = ' /* additional rules */ ' . $additional_styles;
293 }
294 }
295
296 $this->output .= '<style>' . $theme_css_rules . $additional_styles . '</style>';
297 }
298
299 // Opening HTML wrapper
300 $this->output .= $this->build_opening_wrapper();
301
302 // Format each post
303 $position = 0;
304
305 foreach( $this->data as $post_object ) {
306 $position++;
307 $this->output .= $this->render_post($post_object, $position);
308 }
309
310 // Closing HTML wrapper
311 $this->output .= $this->build_closing_wrapper();
312
313 if (
314 isset($this->public_options['theme']['name'])
315 && $this->public_options['theme']['name']
316 ) {
317 $this->output .= '</div>';
318 }
319
320 }
321 // Got nothing to show, give 'em the old "Sorry. No data so far." message!
322 else {
323 $this->output = apply_filters('wpp_no_data', '<p class="wpp-no-data">' . __('Sorry. No data so far.', 'wordpress-popular-posts') . '</p>');
324 }
325 }
326
327 /**
328 * Builds the opening HTML wrapper for the list.
329 *
330 * @since 7.3.3
331 * @return string
332 */
333 public function build_opening_wrapper()
334 {
335 // Custom opening HTML wrapper
336 if (
337 isset($this->public_options['markup']['custom_html'])
338 && $this->public_options['markup']['custom_html']
339 && isset($this->public_options['markup']['wpp-start'])
340 && isset($this->public_options['markup']['wpp-end'])
341 ){
342 return "\n" . htmlspecialchars_decode($this->public_options['markup']['wpp-start'], ENT_QUOTES) . "\n";
343 }
344
345 // Default opening HTML wrapper
346 $classes = 'wpp-list';
347
348 if ( $this->public_options['thumbnail']['active'] ) {
349 $classes .= ' wpp-list-with-thumbnails';
350 }
351
352 return "\n<ul class=\"{$classes}\">\n";
353 }
354
355 /**
356 * Builds the closing HTML wrapper for the list.
357 *
358 * @since 7.3.3
359 * @return string
360 */
361 public function build_closing_wrapper()
362 {
363 // Custom closing HTML wrapper
364 if (
365 isset($this->public_options['markup']['custom_html'])
366 && $this->public_options['markup']['custom_html']
367 && isset($this->public_options['markup']['wpp-start'])
368 && isset($this->public_options['markup']['wpp-end'])
369 ){
370 return "\n" . htmlspecialchars_decode($this->public_options['markup']['wpp-end'], ENT_QUOTES) . "\n";
371 }
372
373 // Default closing HTML wrapper
374 return '</ul>' . "\n";
375 }
376
377 /**
378 * Build the HTML markup for a single post.
379 *
380 * @since 4.0.0
381 * @access private
382 * @param object $post_object
383 * @param integer $position
384 * @return string
385 */
386 private function render_post(\stdClass $post_object, int $position = 1)
387 {
388 $is_single = $this->is_single();
389 $post = '';
390 $post_id = $post_object->id;
391 $trid = $this->translate->get_object_id(
392 $post_object->id,
393 get_post_type($post_object->id)
394 );
395
396 if ( $trid && $post_id != $trid ) {
397 $post_id = $trid;
398 }
399
400 $is_current_post = ( $is_single && ($is_single == $post_id || $is_single == $post_object->id) ) ? true : false;
401
402 // Permalink
403 $permalink = esc_url($this->get_permalink($post_object, $post_id));
404
405 // Post title (and title attribute)
406 $post_title_attr = esc_attr(wp_strip_all_tags($this->get_title($post_object, $post_id)));
407 $post_title = $this->get_title($post_object, $post_id);
408
409 if ( $this->public_options['shorten_title']['active'] ) {
410 $length = ( filter_var($this->public_options['shorten_title']['length'], FILTER_VALIDATE_INT) && $this->public_options['shorten_title']['length'] > 0 )
411 ? $this->public_options['shorten_title']['length']
412 : 25;
413
414 $more = $this->public_options['shorten_title']['words'] ? ' ' . $this->more : $this->more;
415 $more = apply_filters('wpp_title_more', $more);
416 $post_title = Helper::truncate($post_title, $length, $this->public_options['shorten_title']['words'], $more);
417 }
418
419 // Thumbnail
420 $post_thumbnail = $this->get_thumbnail($post_id);
421
422 // Post excerpt
423 $post_excerpt = $this->get_excerpt($post_object, $post_id);
424
425 // Post rating
426 $post_rating = $this->get_rating($post_object);
427
428 /**
429 * Post meta
430 */
431
432 // Post date
433 $post_date = $this->get_date($post_object);
434
435 // Post taxonomies
436 $post_taxonomies = $this->get_taxonomies($post_id);
437
438 // Post author
439 $post_author = $this->get_author($post_object, $post_id);
440
441 // Post views count
442 $post_views = $this->get_pageviews($post_object);
443
444 // Post comments count
445 $post_comments = $this->get_comments($post_object);
446
447 // Post meta
448 $meta_arr = $this->get_metadata(
449 $post_object,
450 $post_id,
451 $post_date,
452 $post_taxonomies,
453 $post_author,
454 $post_views,
455 $post_comments
456 );
457
458 if (
459 is_array($meta_arr)
460 && ! empty($meta_arr)
461 && 'views' == $this->public_options['order_by']
462 ) {
463 $keys = ['views', 'comments', 'author', 'date', 'taxonomy'];
464 $new_meta_arr = [];
465
466 foreach($keys as $key) {
467 if ( isset($meta_arr[$key])) {
468 $new_meta_arr[$key] = $meta_arr[$key];
469 }
470 }
471
472 if ( ! empty($new_meta_arr) ) {
473 $meta_arr = $new_meta_arr;
474 }
475 }
476
477 $post_meta_separator = esc_html(apply_filters('wpp_post_meta_separator', ' | '));
478 $post_meta = join($post_meta_separator, $meta_arr);
479
480 $prettify_numbers = apply_filters('wpp_prettify_numbers', true);
481
482 /**
483 * @ToDo
484 *
485 * Remove this filter, it's a typo and the correct one has been around since 6.3.4
486 *
487 * @since 7.3.7
488 */
489 if ( has_filter('wpp_pretiffy_numbers') ) {
490 $prettify_numbers = apply_filters('wpp_pretiffy_numbers', true);
491
492 if ( defined('WP_DEBUG') && WP_DEBUG ) {
493 trigger_error('WP Popular Posts - wpp_pretiffy_numbers has been deprecated. Please use wpp_prettify_numbers instead.', E_USER_WARNING);
494 }
495 }
496
497 // Build custom HTML output
498 if ( $this->public_options['markup']['custom_html'] ) {
499 $post_template = htmlspecialchars_decode($this->public_options['markup']['post-html'], ENT_QUOTES) . "\n";
500 }
501 // Build stock HTML output
502 else {
503 $wpp_post_classnames = apply_filters('wpp_post_class', ['{current_class}'], $post_id);
504
505 $post_template_classnames = ( is_array($wpp_post_classnames) && ! empty($wpp_post_classnames) )
506 ? implode(' ', array_map(fn($classname) => esc_attr($classname), $wpp_post_classnames))
507 : '';
508
509 $post_template_thumbnail = ( ! empty($post_thumbnail) )
510 ? '{thumb}'
511 : '';
512
513 $post_template_excerpt = ( ! empty($post_excerpt) )
514 ? ' <span class="wpp-excerpt">{excerpt}</span>'
515 : '';
516
517 $post_template_meta = ( ! empty($post_meta) )
518 ? ' <span class="wpp-meta post-stats">{stats}</span>'
519 : '';
520
521 $post_template_rating = ( ! empty($post_rating) )
522 ? ' <span class="wpp-rating">{rating}</span>'
523 : '';
524
525 $post_template = <<<EOD
526 <li class="$post_template_classnames">
527 $post_template_thumbnail
528 {title}
529 $post_template_excerpt
530 $post_template_meta
531 $post_template_rating
532 </li>
533 EOD;
534 }
535
536 $post = $this->format_content(
537 $post_template,
538 [
539 'id' => $post_id,
540 'is_current_post' => $is_current_post,
541 'title' => '<a href="' . $permalink . '" ' . ($post_title_attr !== $post_title ? 'title="' . $post_title_attr . '" ' : '' ) . 'class="wpp-post-title" target="' . esc_attr($this->admin_options['tools']['link']['target']) . '">' . $post_title . '</a>',
542 'title_attr' => $post_title_attr,
543 'summary' => $post_excerpt,
544 'stats' => $post_meta,
545 'img' => ( ! empty($post_thumbnail) ) ? '<a href="' . $permalink . '" ' . ($post_title_attr !== $post_title ? 'title="' . $post_title_attr . '" ' : '' ) . 'target="' . esc_attr($this->admin_options['tools']['link']['target']) . '">' . $post_thumbnail . '</a>' : '',
546 'img_no_link' => $post_thumbnail,
547 'url' => $permalink,
548 'text_title' => $post_title,
549 'taxonomy' => $post_taxonomies,
550 'taxonomy_copy' => $meta_arr['taxonomy'] ?? null,
551 'author' => ( ! empty($post_author) ) ? '<a href="' . esc_url(get_author_posts_url($post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid )) . '">' . esc_html($post_author) . '</a>' : '',
552 'author_copy' => $meta_arr['author'] ?? null,
553 'author_name' => esc_html($post_author),
554 'author_url' => ( ! empty($post_author) ) ? esc_url(get_author_posts_url($post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid)) : '',
555 'views' => ( $this->public_options['order_by'] == 'views' || $this->public_options['order_by'] == 'comments' ) ? ($prettify_numbers ? Helper::prettify_number($post_views) : number_format_i18n($post_views)) : ($prettify_numbers ? Helper::prettify_number($post_views, 2) : number_format_i18n($post_views, 2)),
556 'views_copy' => $meta_arr['views'] ?? null,
557 'comments' => $prettify_numbers ? Helper::prettify_number($post_comments) : number_format_i18n($post_comments),
558 'comments_copy' => $meta_arr['comments'] ?? null,
559 'date' => $post_date,
560 'date_copy' => $meta_arr['date'] ?? null,
561 'total_items' => count($this->data),
562 'item_position' => $position
563 ],
564 $this->public_options['rating']
565 );
566
567 return apply_filters(
568 'wpp_post',
569 $post,
570 $post_object,
571 $this->public_options
572 );
573 }
574
575 /**
576 * Return the processed post/page title.
577 *
578 * @since 3.0.0
579 * @access private
580 * @param object $post_object
581 * @param integer $post_id
582 * @return string
583 */
584 private function get_title(\stdClass $post_object, int $post_id)
585 {
586 $title = '';
587
588 if ( $post_object->id != $post_id ) {
589 $title = get_the_title($post_id);
590 } else {
591 $title = $post_object->title;
592 }
593
594 // Run the_title filter so core/plugin title hooks can
595 // be applied to the post title
596 $title = apply_filters('the_title', $title, $post_object->id);
597
598 return apply_filters('wpp_the_title', $title, $post_object->id, $post_id);
599 }
600
601 /**
602 * Return the permalink.
603 *
604 * @since 4.0.12
605 * @access private
606 * @param object $post_object
607 * @param integer $post_id
608 * @return string
609 */
610 private function get_permalink(\stdClass $post_object, int $post_id) {
611 if ( $post_object->id != $post_id ) {
612 return get_permalink($post_id);
613 }
614
615 return get_permalink($post_object->id);
616 }
617
618 /**
619 * Return the processed thumbnail.
620 *
621 * @since 3.0.0
622 * @access private
623 * @param int $post_id
624 * @return string
625 */
626 private function get_thumbnail(int $post_id)
627 {
628 $thumbnail = '';
629
630 if ( $this->public_options['thumbnail']['active'] ) {
631 $thumbnail = $this->thumbnail->get(
632 $post_id,
633 [
634 $this->public_options['thumbnail']['width'],
635 $this->public_options['thumbnail']['height']
636 ],
637 $this->admin_options['tools']['thumbnail']['source'],
638 $this->public_options['thumbnail']['crop'],
639 $this->public_options['thumbnail']['build']
640 );
641 }
642
643 return $thumbnail;
644 }
645
646 /**
647 * Return post excerpt.
648 *
649 * @since 3.0.0
650 * @access private
651 * @param object $post_object
652 * @param integer $post_id
653 * @return string
654 */
655 private function get_excerpt(\stdClass $post_object, int $post_id)
656 {
657 $excerpt = '';
658
659 if ( $this->public_options['post-excerpt']['active'] ) {
660
661 if ( $post_object->id != $post_id ) {
662 $the_post = get_post($post_id);
663
664 $excerpt = ( empty($the_post->post_excerpt) )
665 ? $the_post->post_content
666 : $the_post->post_excerpt;
667 }
668 else {
669 $excerpt = ( empty($post_object->post_excerpt) )
670 ? $post_object->post_content
671 : $post_object->post_excerpt;
672 }
673
674 // remove caption tags
675 $excerpt = preg_replace('/\[caption.*\[\/caption\]/', '', $excerpt);
676
677 // remove Flash objects
678 $excerpt = preg_replace("/<object[0-9 a-z_?*=\":\-\/\.#\,\\n\\r\\t]+/smi", '', $excerpt);
679
680 // remove iframes
681 $excerpt = preg_replace('/<iframe.*?\/iframe>/i', '', $excerpt);
682
683 // remove WP shortcodes
684 $excerpt = strip_shortcodes($excerpt);
685
686 // remove style/script tags
687 $excerpt = preg_replace('@<(script|style)[^>]*?>.*?</\\1>@si', '', $excerpt);
688
689 // remove blocks that are not appropriate for the excerpt
690 $excerpt = excerpt_remove_blocks($excerpt);
691
692 // remove HTML tags if requested
693 if ( $this->public_options['post-excerpt']['keep_format'] ) {
694 $excerpt = wp_kses(
695 $excerpt,
696 [
697 'a' => [
698 'href' => [],
699 'title' => []
700 ],
701 'em' => [],
702 'strong' => []
703 ]
704 );
705 } else {
706 $excerpt = wp_kses($excerpt, []);
707
708 // remove URLs, too
709 $excerpt = preg_replace('_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS', '', $excerpt);
710 }
711
712 // remove HTML comments
713 $excerpt = preg_replace('/<!--.*?-->/ms', '', $excerpt);
714
715 // remove extra whitespaces
716 $excerpt = preg_replace('/\s\s+/', ' ', $excerpt);
717
718 $excerpt = trim($excerpt);
719
720 }
721
722 // Balance tags, if needed
723 if ( '' !== $excerpt ) {
724
725 $more = $this->public_options['post-excerpt']['words'] ? ' ' . $this->more : $this->more;
726 $more = apply_filters('wpp_excerpt_more', $more);
727 $excerpt = Helper::truncate($excerpt, $this->public_options['post-excerpt']['length'], $this->public_options['post-excerpt']['words'], $more);
728
729 if ( $this->public_options['post-excerpt']['keep_format'] ) {
730 $excerpt = force_balance_tags($excerpt);
731 }
732 }
733
734 return $excerpt;
735 }
736
737 /**
738 * Return post rating.
739 *
740 * @since 3.0.0
741 * @access private
742 * @param object $post_object
743 * @return string
744 */
745 private function get_rating(\stdClass $post_object)
746 {
747 $rating = '';
748
749 if ( function_exists('the_ratings_results') && $this->public_options['rating'] ) {
750 $rating = the_ratings_results($post_object->id);
751 }
752
753 return $rating;
754 }
755
756 /**
757 * Get post date.
758 *
759 * @since 3.0.0
760 * @access private
761 * @param object $post_object
762 * @return string
763 */
764 private function get_date(\stdClass $post_object)
765 {
766 $date = '';
767
768 if ( $this->public_options['stats_tag']['date']['active'] ) {
769 if ( 'relative' == $this->public_options['stats_tag']['date']['format'] ) {
770 $date = sprintf(
771 __('%s ago', 'wordpress-popular-posts'),
772 human_time_diff(
773 strtotime($post_object->date),
774 Helper::timestamp()
775 )
776 );
777 } else {
778 $date = date_i18n(
779 ( 'wp_date_format' == $this->public_options['stats_tag']['date']['format'] ? $this->wp_date_format : $this->public_options['stats_tag']['date']['format'] ),
780 strtotime($post_object->date)
781 );
782 }
783 }
784
785 return apply_filters('wpp_the_date', $date, $post_object->id);
786 }
787
788 /**
789 * Get post taxonomies.
790 *
791 * @since 3.0.0
792 * @access private
793 * @param integer $post_id
794 * @return string
795 */
796 private function get_taxonomies(int $post_id)
797 {
798 $post_tax = '';
799
800 if (
801 (isset($this->public_options['stats_tag']['category']) && $this->public_options['stats_tag']['category'])
802 || $this->public_options['stats_tag']['taxonomy']['active']
803 ) {
804
805 $taxonomy = 'category';
806
807 if (
808 $this->public_options['stats_tag']['taxonomy']['active']
809 && ! empty($this->public_options['stats_tag']['taxonomy']['name'])
810 ) {
811 $taxonomy = $this->public_options['stats_tag']['taxonomy']['name'];
812 }
813
814 $terms = wp_get_post_terms($post_id, $taxonomy);
815
816 if ( ! is_wp_error($terms) ) {
817 // Usage: https://wordpress.stackexchange.com/a/46824
818 if ( has_filter('wpp_post_exclude_terms') ) {
819 $args = apply_filters('wpp_post_exclude_terms', []);
820 $terms = wp_list_filter($terms, $args, 'NOT');
821 }
822
823 $terms = apply_filters('wpp_post_terms', $terms);
824
825 if (
826 is_array($terms)
827 && ! empty($terms)
828 ) {
829 $taxonomy_separator = esc_html(apply_filters('wpp_taxonomy_separator', ', '));
830
831 // We're going to use the taxonomy slug as a CSS class so let's escape it just in case
832 $taxonomy = esc_attr($taxonomy);
833
834 foreach ($terms as $term) {
835 $term_link = get_term_link($term);
836
837 if ( is_wp_error($term_link) ) {
838 continue;
839 }
840
841 $term_link = esc_url($this->translate->url($term_link, $this->translate->get_current_language()));
842 $post_tax .= "<a href=\"{$term_link}\" class=\"wpp-taxonomy {$taxonomy} {$taxonomy}-{$term->term_id}\">" . esc_html($term->name) . '</a>' . $taxonomy_separator;
843 }
844 }
845 }
846
847 if ( '' != $post_tax ) {
848 $post_tax = rtrim($post_tax, $taxonomy_separator);
849 }
850
851 }
852
853 return $post_tax;
854 }
855
856 /**
857 * Get post author.
858 *
859 * @since 3.0.0
860 * @access private
861 * @param object $post_object
862 * @param integer $post_id
863 * @return string
864 */
865 private function get_author(\stdClass $post_object, int $post_id)
866 {
867 $author = ( $this->public_options['stats_tag']['author'] )
868 ? get_the_author_meta('display_name', $post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid)
869 : '';
870
871 return $author;
872 }
873
874 /**
875 * Return post views count.
876 *
877 * @since 3.0.0
878 * @access private
879 * @param object $post_object
880 * @return int|float
881 */
882 private function get_pageviews(\stdClass $post_object)
883 {
884 $pageviews = 0;
885
886 if (
887 (
888 $this->public_options['order_by'] == 'views'
889 || $this->public_options['order_by'] == 'avg'
890 || $this->public_options['stats_tag']['views']
891 )
892 && ( isset($post_object->pageviews) || isset($post_object->avg_views) )
893 ) {
894 $pageviews = ( $this->public_options['order_by'] == 'views' || $this->public_options['order_by'] == 'comments' )
895 ? $post_object->pageviews
896 : $post_object->avg_views;
897 }
898
899 return $pageviews;
900 }
901
902 /**
903 * Return post comment count.
904 *
905 * @since 3.0.0
906 * @access private
907 * @param object $post_object
908 * @return int
909 */
910 private function get_comments(\stdClass $post_object)
911 {
912 $comments = ( ( $this->public_options['order_by'] == 'comments' || $this->public_options['stats_tag']['comment_count'] ) && isset($post_object->comment_count) )
913 ? $post_object->comment_count
914 : 0;
915
916 return $comments;
917 }
918
919 /**
920 * Return post metadata.
921 *
922 * @since 3.0.0
923 * @access private
924 * @param object $post_object
925 * @param integer $post_id
926 * @return array
927 */
928 //private function get_metadata(\stdClass $post_object, $post_id)
929 private function get_metadata(\stdClass $post_object, int $post_id, string $date, string $post_tax, string $author, $pageviews, int $comments) /** @TODO: starting PHP 8.0 $pageviews can be declared as mixed $pageviews */
930 {
931 $stats = [];
932
933 $prettify_numbers = apply_filters('wpp_prettify_numbers', true);
934
935 /**
936 * @ToDo
937 *
938 * Remove this filter, it's a typo and the correct one has been around since 6.3.4
939 *
940 * @since 7.3.7
941 */
942 if ( has_filter('wpp_pretiffy_numbers') ) {
943 $prettify_numbers = apply_filters('wpp_pretiffy_numbers', true);
944
945 if ( defined('WP_DEBUG') && WP_DEBUG ) {
946 trigger_error('WP Popular Posts - wpp_pretiffy_numbers has been deprecated. Please use wpp_prettify_numbers instead..', E_USER_WARNING);
947 }
948 }
949
950 // comments
951 if ( $this->public_options['stats_tag']['comment_count'] ) {
952 $comments_text = sprintf(
953 _n('%s comment', '%s comments', $comments, 'wordpress-popular-posts'),
954 $prettify_numbers ? Helper::prettify_number($comments) : number_format_i18n($comments)
955 );
956
957 $stats['comments'] = '<span class="wpp-comments">' . $comments_text . '</span>';
958 }
959
960 // views
961 if ( $this->public_options['stats_tag']['views'] ) {
962 if ( $this->public_options['order_by'] == 'avg' ) {
963 $views_text = sprintf(
964 _n('%s view per day', '%s views per day', $pageviews, 'wordpress-popular-posts'),
965 $prettify_numbers ? Helper::prettify_number($pageviews, 2) : number_format_i18n($pageviews, (fmod($pageviews, 1) !== 0.0 ? 2 : 0))
966 );
967 }
968 else {
969 $views_text = sprintf(
970 _n('%s view', '%s views', $pageviews, 'wordpress-popular-posts'),
971 $prettify_numbers ? Helper::prettify_number($pageviews) : number_format_i18n($pageviews)
972 );
973 }
974
975 $stats['views'] = '<span class="wpp-views">' . $views_text . '</span>';
976 }
977
978 // author
979 if ( $this->public_options['stats_tag']['author'] ) {
980 $author_url = get_author_posts_url($post_object->uid != $post_id ? get_post_field('post_author', $post_id) : $post_object->uid);
981 $display_name = '<a href="' . esc_url($this->translate->url($author_url, $this->translate->get_current_language())) . '">' . esc_html($author) . '</a>';
982 $stats['author'] = '<span class="wpp-author">' . sprintf(__('by %s', 'wordpress-popular-posts'), $display_name) . '</span>';
983 }
984
985 // date
986 if ( $this->public_options['stats_tag']['date']['active'] ) {
987 $stats['date'] = '<span class="wpp-date">' . ( 'relative' == $this->public_options['stats_tag']['date']['format'] ? sprintf(__('posted %s', 'wordpress-popular-posts'), $date) : sprintf(__('posted on %s', 'wordpress-popular-posts'), $date) ) . '</span>';
988 }
989
990 // taxonomy
991 if ( ($this->public_options['stats_tag']['category'] || $this->public_options['stats_tag']['taxonomy']['active']) && $post_tax != '' ) {
992 $stats['taxonomy'] = '<span class="wpp-category">' . sprintf(__('under %s', 'wordpress-popular-posts'), $post_tax) . '</span>';
993 }
994
995 return $stats;
996 }
997
998 /**
999 * Parse content tags.
1000 *
1001 * @since 1.4.6
1002 * @access private
1003 * @param string HTML string with content tags
1004 * @param array Post data
1005 * @param bool Used to display post rating (if functionality is available)
1006 * @return string
1007 */
1008 private function format_content(string $string, array $data, bool $rating) {
1009
1010 if ( empty($string) || ( empty($data) || ! is_array($data) ) ) {
1011 return false;
1012 }
1013
1014 $params = [];
1015 $pattern = '/\{(pid|current_class|excerpt|summary|meta|stats|title|title_attr|image|thumb|thumb_img|thumb_url|rating|score|url|text_title|author|author_copy|author_name|author_url|taxonomy|taxonomy_copy|category|category_copy|views|views_copy|comments|comments_copy|date|date_copy|total_items|item_position)\}/i';
1016 preg_match_all($pattern, $string, $matches);
1017
1018 array_map('strtolower', $matches[0]);
1019
1020 if ( in_array('{pid}', $matches[0]) ) {
1021 $string = str_replace('{pid}', $data['id'], $string);
1022 }
1023
1024 if ( in_array('{current_class}', $matches[0]) ) {
1025 $string = str_replace('{current_class}', ( $data['is_current_post'] ? 'current' : '' ), $string);
1026 }
1027
1028 if ( in_array('{title}', $matches[0]) ) {
1029 $string = str_replace('{title}', $data['title'], $string);
1030 }
1031
1032 if ( in_array('{title_attr}', $matches[0]) ) {
1033 $string = str_replace('{title_attr}', $data['title_attr'], $string);
1034 }
1035
1036 if ( in_array('{meta}', $matches[0]) || in_array('{stats}', $matches[0]) ) {
1037 $string = str_replace(['{meta}', '{stats}'], $data['stats'], $string);
1038 }
1039
1040 if ( in_array('{excerpt}', $matches[0]) || in_array('{summary}', $matches[0]) ) {
1041 $string = str_replace(['{excerpt}', '{summary}'], $data['summary'], $string);
1042 }
1043
1044 if ( in_array('{image}', $matches[0]) || in_array('{thumb}', $matches[0]) ) {
1045 $string = str_replace(['{image}', '{thumb}'], $data['img'], $string);
1046 }
1047
1048 if ( in_array('{thumb_img}', $matches[0]) ) {
1049 $string = str_replace('{thumb_img}', $data['img_no_link'], $string);
1050 }
1051
1052 if ( in_array('{thumb_url}', $matches[0]) && ! empty($data['img_no_link']) ) {
1053 $dom = new \DOMDocument();
1054
1055 if ( $dom->loadHTML($data['img_no_link']) ) {
1056 $img_tag = $dom->getElementsByTagName('img');
1057
1058 if ( $img_tag->length ) {
1059 foreach( $img_tag as $node ) {
1060 if ( $node->hasAttribute('src') ) {
1061 $src = $node->getAttribute('src');
1062 $string = str_replace('{thumb_url}', $src, $string);
1063 }
1064 }
1065 }
1066 }
1067 }
1068
1069 // WP-PostRatings check
1070 if ( $rating ) {
1071 if ( function_exists('the_ratings_results') && in_array('{rating}', $matches[0]) ) {
1072 $string = str_replace('{rating}', the_ratings_results($data['id']), $string);
1073 }
1074
1075 if ( function_exists('expand_ratings_template') && in_array('{score}', $matches[0]) ) {
1076 $string = str_replace('{score}', expand_ratings_template('%RATINGS_SCORE%', $data['id']), $string);
1077 // removing the redundant plus sign
1078 $string = str_replace('+', '', $string);
1079 }
1080 }
1081
1082 if ( in_array('{url}', $matches[0]) ) {
1083 $string = str_replace('{url}', $data['url'], $string);
1084 }
1085
1086 if ( in_array('{text_title}', $matches[0]) ) {
1087 $string = str_replace('{text_title}', $data['text_title'], $string);
1088 }
1089
1090 if ( in_array('{author}', $matches[0]) ) {
1091 $string = str_replace('{author}', $data['author'], $string);
1092 }
1093
1094 if ( in_array('{author_copy}', $matches[0]) ) {
1095 $string = str_replace('{author_copy}', $data['author_copy'], $string);
1096 }
1097
1098 if ( in_array('{author_name}', $matches[0]) ) {
1099 $string = str_replace('{author_name}', $data['author_name'], $string);
1100 }
1101
1102 if ( in_array('{author_url}', $matches[0]) ) {
1103 $string = str_replace('{author_url}', $data['author_url'], $string);
1104 }
1105
1106 if ( in_array('{taxonomy}', $matches[0]) || in_array('{category}', $matches[0]) ) {
1107 $string = str_replace(['{taxonomy}', '{category}'], $data['taxonomy'], $string);
1108 }
1109
1110 if ( in_array('{taxonomy_copy}', $matches[0]) || in_array('{category_copy}', $matches[0]) ) {
1111 $string = str_replace(['{taxonomy_copy}', '{category_copy}'], $data['taxonomy_copy'], $string);
1112 }
1113
1114 if ( in_array('{views}', $matches[0]) ) {
1115 $string = str_replace('{views}', $data['views'], $string);
1116 }
1117
1118 if ( in_array('{views_copy}', $matches[0]) ) {
1119 $string = str_replace('{views_copy}', $data['views_copy'], $string);
1120 }
1121
1122 if ( in_array('{comments}', $matches[0]) ) {
1123 $string = str_replace('{comments}', $data['comments'], $string);
1124 }
1125
1126 if ( in_array('{comments_copy}', $matches[0]) ) {
1127 $string = str_replace('{comments_copy}', $data['comments_copy'], $string);
1128 }
1129
1130 if ( in_array('{date}', $matches[0]) ) {
1131 $string = str_replace('{date}', $data['date'], $string);
1132 }
1133
1134 if ( in_array('{date_copy}', $matches[0]) ) {
1135 $string = str_replace('{date_copy}', $data['date_copy'], $string);
1136 }
1137
1138 if ( in_array('{total_items}', $matches[0]) ) {
1139 $string = str_replace('{total_items}', $data['total_items'], $string);
1140 }
1141
1142 if ( in_array('{item_position}', $matches[0]) ) {
1143 $string = str_replace('{item_position}', $data['item_position'], $string);
1144 }
1145
1146 return apply_filters('wpp_parse_custom_content_tags', $string, $data['id']);
1147 }
1148
1149 /**
1150 * Checks whether we're currently seeing a single post/page/CPT.
1151 *
1152 * @since 5.0.0
1153 * @return int
1154 */
1155 public function is_single()
1156 {
1157 return apply_filters('wpp_is_single', Helper::is_single());
1158 }
1159 }
1160