PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.11.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.11.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.7 2.6.8 2.6.5 2.6.4 2.6.3 2.6.2 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 trunk 1.0 1.0.1 1.0.2 1.0.3 1.1 1.1.1 1.1.2 1.2.0 2.0 2.1.0 2.1.1 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1
reviews-feed / class / sbr-functions.php
reviews-feed / class Last commit date
Common 1 week ago sbr-functions.php 1 week ago
sbr-functions.php
971 lines
1 <?php
2
3 use SmashBalloon\Reviews\Common\Admin\Blocks\SB_Reviews_Blocks;
4 use SmashBalloon\Reviews\Common\Util;
5
6 if (!defined('ABSPATH')) {
7 exit; // Exit if accessed directly
8 }
9
10 function sbr_json_encode($thing)
11 {
12 if (function_exists('wp_json_encode')) {
13 return wp_json_encode($thing);
14 } else {
15 return json_encode($thing);
16 }
17 }
18
19
20 /**
21 * Reviews Currect User Capability Check
22 *
23 * @since 1.0
24 */
25 function sbr_current_user_can($cap)
26 {
27 if ($cap === 'manage_reviews_feed_options') {
28 $cap = current_user_can('manage_reviews_feed_options') ? 'manage_reviews_feed_options' : 'manage_options';
29 }
30 $cap = apply_filters('sbr_settings_pages_capability', $cap);
31
32 return current_user_can($cap);
33 }
34
35
36 /**
37 * Get the settings in the database with defaults
38 *
39 * @return array
40 */
41 function sbr_get_database_settings()
42 {
43 global $sbr_settings;
44
45 $defaults = sbr_settings_defaults();
46
47 if ($sbr_settings === null) {
48 $sbr_settings = get_option('sbr_settings', []);
49 }
50
51 // Defensive: `sbr_settings` can arrive as a non-array (raw SQL edits,
52 // broken backup/restore, migration tooling that mangled serialization).
53 // Without this guard `array_merge()` fatals and takes the whole admin
54 // down. Normalizing to an array lets SMASH-1281's migration-recovery
55 // flow re-register + repopulate on the next page load.
56 if (!is_array($sbr_settings)) {
57 $sbr_settings = [];
58 }
59
60 return array_merge($defaults, $sbr_settings);
61 }
62
63 /**
64 * Get the settings default settins
65 *
66 * @return array
67 */
68 function sbr_settings_defaults()
69 {
70 return [
71 //Template
72 'feedTemplate' => 'default',
73 //Sources
74 'sources' => [],
75 //Layout Settings
76 'layout' => 'list',
77 'verticalSpacing' => 20,
78 'horizontalSpacing' => 10,
79 'contentAlignment' => 'left',
80 'contentLength' => 320,
81 'numPostDesktop' => 10,
82 'numPostTablet' => 8,
83 'numPostMobile' => 6,
84 'gridDesktopColumns' => 3,
85 'gridTabletColumns' => 2,
86 'gridMobileColumns' => 1,
87 'masonryDesktopColumns' => 3,
88 'masonryTabletColumns' => 2,
89 'masonryMobileColumns' => 1,
90 'carouselDesktopColumns' => 3,
91 'carouselTabletColumns' => 2,
92 'carouselMobileColumns' => 1,
93 'carouselDesktopRows' => 1,
94 'carouselTabletRows' => 1,
95 'carouselMobileRows' => 1,
96 'carouselLoopType' => 'infinity',
97 'carouselIntervalTime' => 5000,
98 'carouselShowArrows' => false,
99 'carouselShowPagination' => true,
100 'carouselEnableAutoplay' => true,
101
102 //Header
103 'showHeader' => true,
104 'headerContent' => ['heading', 'button', 'averagereview'],
105 'headerPadding' => [],
106 'headerMargin' => ['bottom' => 20],
107 //Heading
108 'headerHeadingContent' => 'Reviews',
109 'headingFont' => [
110 'weight' => 700,
111 'size' => 36,
112 'height' => '100%'
113 ],
114 'headingColor' => '#141B38',
115 'headerHeadingPadding' => [],
116 'headerHeadingMargin' => ['bottom' => 10],
117 //Button
118 'headerButtonLinkTo' => 'google',
119 'headerButtonIcon' => '',
120 'headerButtonExternalLink' => '',
121 'headerButtonFont' => [
122 'weight' => 600,
123 'size' => 14,
124 'height' => '22px'
125 ],
126 'headerButtonColor' => '#ffffff',
127 'headerButtonBg' => '#ED4944',
128 'headerButtonHoverColor' => '#ffffff',
129 'headerButtonHoverBg' => '#CC3F3A',
130 'headerButtonPadding' => [
131 'top' => 8,
132 'right' => 20,
133 'bottom' => 8,
134 'left' => 12,
135 ],
136 'headerButtonMargin' => [],
137 //AverageReview
138 'headerAvReviewFont' => [
139 'weight' => 600,
140 'size' => 20,
141 'height' => '1.5em'
142 ],
143 'headerAvSubtextReviewFont' => [
144 'weight' => 400,
145 'size' => 12,
146 'height' => '1.5em'
147 ],
148 'headerAvReviewIconColor' => '#ED4944',
149 'headerAvReviewColor' => '#141B38',
150 'headerAvReviewSubtextColor' => '#434960',
151 'headerAvReviewMargin' => '',
152 'headerAvReviewPadding' => '',
153
154 //Post Style
155 'postStyle' => 'regular',
156 'boxedBackgroundColor' => '#ffffff',
157 'boxedBoxShadow' => [],
158 'boxedBorderRadius' => [],
159 'postStroke' => [],
160 'postPadding' => [
161 'bottom' => 20
162 ],
163
164 'postElements' => ['author', 'rating', 'text', 'media'],
165 'ratingIconSize' => 'small',
166 'ratingIconColor' => '#ED4944',
167 'ratingIconPadding' => [],
168 'ratingIconMargin' => [
169 'top' => 15,
170 'bottom' => 15,
171 ],
172 'paragraphFont' => [
173 'weight' => 400,
174 'size' => 16,
175 'height' => '1.5em'
176 ],
177 'paragraphColor' => '#434960',
178 'paragraphPadding' => [],
179 'paragraphMargin' => [],
180
181 'authorContent' => ['name', 'image', 'date'],
182 'authorPadding' => [],
183 'authorMargin' => [],
184
185 'authorNameFont' => [
186 'weight' => 600,
187 'size' => 14,
188 'height' => '1.5em'
189 ],
190 'authorNameColor' => '#141B38',
191 'authorNamePadding' => [],
192 'authorNameMargin' => [],
193
194 'dateFont' => [
195 'weight' => 400,
196 'size' => 13,
197 'height' => '1.5em'
198 ],
199 'dateColor' => '#434960',
200
201 'dateFormat' => '1',
202 'dateCustomFormat' => '',
203 'dateBeforeText' => '',
204 'dateAfterText' => '',
205 'datePadding' => [],
206 'dateMargin' => [],
207 'authorImageBorderRadius' => 50,
208 'authorImageMargin' => [
209 'right' => 10
210 ],
211
212 'showLoadButton' => true,
213 'loadButtonText' => 'Load More',
214 'loadButtonFont' => [
215 'weight' => 600,
216 'size' => 16,
217 'height' => '1em'
218 ],
219 'loadButtonColor' => '#141B38',
220 'loadButtonHoverColor' => '#ffffff',
221 'loadButtonBg' => '#E6E6EB',
222 'loadButtonHoverBg' => '#FE544F',
223
224 'loadButtonPadding' => [
225 'top' => 15,
226 'bottom' => 15
227 ],
228 'loadButtonMargin' => [
229 'top' => 20
230 ],
231
232 //Filters
233 'includedStarFilters' => [],
234 'includeWords' => '',
235 'excludeWords' => '',
236 'filterByImage' => false,
237 'filterByVideos' => true,
238
239 //Sort
240 'sortByDateEnabled' => true,
241 'sortByDate' => 'latest',
242
243 'sortByRatingEnabled' => false,
244 'sortByRating' => '',
245
246 'sortRandomEnabled' => false,
247
248 //ColorScheme
249 'colorScheme' => 'inherit',
250
251
252 //Moderation Mode
253 'moderationEnabled' => false,
254 'moderationType' => 'allow',
255 'moderationAllowList' => [],
256 'moderationBlockList' => [],
257
258 //Translation
259 'localization' => 'default',
260 'trustpilotLanguage' => 'all',
261
262 //Filter By Length
263 'filterCharCountMin' => 0,
264 'filterCharCountMax' => '',
265
266 //Carousel Breakpoints
267 'carouselBreakpointDesktop' => 850,
268 'carouselBreakpointTablet' => 520
269 ];
270 }
271
272
273 function sbr_plugin_settings_defaults()
274 {
275 return [
276 'localization' => '',
277 'optimize_images' => true,
278 // Must match UsageTracking\Config::is_enabled()'s absent-key fallback, or the
279 // Advanced toggle renders ON for Free and any settings save persists consent.
280 'usagetracking' => Util::sbr_is_pro(),
281 'enqueue_js_in_header' => false,
282 'admin_error_notices' => true,
283 'feed_issue_reports' => true,
284 // SMASH-1756 — emit schema.org rich-snippet markup for feeds (global, default on).
285 'enableSchema' => true,
286 'translations' => [
287 'second' => __('second', 'reviews-feed'),
288 'seconds' => __('seconds', 'reviews-feed'),
289 'minute' => __('minute', 'reviews-feed'),
290 'minutes' => __('minutes', 'reviews-feed'),
291 'hour' => __('hour', 'reviews-feed'),
292 'hours' => __('hours', 'reviews-feed'),
293 'day' => __('day', 'reviews-feed'),
294 'days' => __('days', 'reviews-feed'),
295 'week' => __('week', 'reviews-feed'),
296 'weeks' => __('weeks', 'reviews-feed'),
297 'month' => __('month', 'reviews-feed'),
298 'months' => __('months', 'reviews-feed'),
299 'year' => __('year', 'reviews-feed'),
300 'years' => __('year', 'reviews-feed'),
301 'ago' => __('ago', 'reviews-feed'),
302 'writeReview' => __('Write a Review', 'reviews-feed'),
303 'reviewsHeader' => __('Over %s Reviews', 'reviews-feed'),
304 ]
305 ];
306 }
307 function sbr_activate($network_wide)
308 {
309 global $wp_roles;
310 $wp_roles->add_cap('administrator', 'manage_reviews_feed_options');
311 }
312
313 register_activation_hook(__FILE__, 'sby_activate');
314
315
316 function sbr_get_feed_template_part($part, $settings = array())
317 {
318 $file = '';
319
320 /**
321 * Whether or not to search for custom templates in theme folder
322 *
323 * @param boolean Setting from DB or shortcode to use custom templates
324 *
325 * @since 1.0
326 */
327 $settings_custom_templates = ! empty($settings['customtemplates']) && $settings['customtemplates'];
328 $using_custom_templates_in_theme = apply_filters('sbr_use_theme_templates', $settings_custom_templates);
329 $generic_path = trailingslashit(SBR_PLUGIN_DIR) . 'templates/frontend/';
330
331 //For Templates that are different Free Or Pro
332 $special_path = $generic_path . ( Util::sbr_is_pro() ? 'pro' : 'lite' ) . '/';
333
334 if ($using_custom_templates_in_theme) {
335 $custom_header_template = locate_template('sbr/header.php', false, false);
336 $custom_item_template = locate_template('sbr/item.php', false, false);
337 $custom_footer_template = locate_template('sbr/footer.php', false, false);
338 $custom_feed_template = locate_template('sbr/feed.php', false, false);
339 } else {
340 $custom_header_template = false;
341 $custom_item_template = false;
342 $custom_footer_template = false;
343 $custom_feed_template = false;
344 }
345
346 if ($part === 'header') {
347 if ($custom_header_template) {
348 $file = $custom_header_template;
349 } else {
350 #$file = $generic_path . 'header.php';
351 $file = $special_path . 'header.php';
352 }
353 } elseif ($part === 'item') {
354 if ($custom_item_template) {
355 $file = $custom_item_template;
356 } else {
357 $file = $generic_path . 'item.php';
358 }
359 } elseif ($part === 'footer') {
360 if ($custom_footer_template) {
361 $file = $custom_footer_template;
362 } else {
363 #$file = $generic_path . 'footer.php';
364 $file = $special_path . 'footer.php';
365 }
366 } elseif ($part === 'feed') {
367 if ($custom_feed_template) {
368 $file = $custom_feed_template;
369 } else {
370 #$file = $generic_path . 'feed.php';
371 $file = $special_path . 'feed.php';
372 }
373 } elseif ($part === 'post-elements/author') {
374 if ($custom_feed_template) {
375 $file = $custom_feed_template;
376 } else {
377 #$file = $generic_path . 'post-elements/author.php';
378 $file = $special_path . 'post-elements/author.php';
379 }
380 } elseif ($part === 'post-elements/media') {
381 if ($custom_feed_template) {
382 $file = $custom_feed_template;
383 } else {
384 #$file = $generic_path . 'post-elements/media.php';
385 $file = $special_path . 'post-elements/media.php';
386 }
387 } elseif ($part === 'post-elements/rating') {
388 if ($custom_feed_template) {
389 $file = $custom_feed_template;
390 } else {
391 $file = $generic_path . 'post-elements/rating.php';
392 }
393 } elseif ($part === 'post-elements/text') {
394 if ($custom_feed_template) {
395 $file = $custom_feed_template;
396 } else {
397 $file = $generic_path . 'post-elements/text.php';
398 }
399 }
400
401 return $file;
402 }
403
404 function sbr_container_id($feed_id)
405 {
406 return 'sb-reviews-container-' . $feed_id;
407 }
408
409 function sbr_scripts_enqueue($enqueue = false)
410 {
411 //Register the script to make it available
412 $assets_url = trailingslashit(SBR_PLUGIN_URL);
413 $settings = get_option('sbr_settings', []);
414 if (!is_array($settings)) {
415 $settings = [];
416 }
417 $min = !empty($_GET['sb_debug']) ? '' : '.min';
418
419 wp_enqueue_style(
420 'sbr_styles',
421 $assets_url . 'assets/css/sbr-styles' . $min . '.css',
422 [],
423 SBRVER
424 );
425
426 if (!empty($settings['enqueue_js_in_header'])) {
427 wp_enqueue_script(
428 'sbr_scripts',
429 $assets_url . 'assets/js/sbr-feed' . $min . '.js',
430 ['jquery'],
431 SBRVER,
432 false
433 );
434 } else {
435 wp_register_script(
436 'sbr_scripts',
437 $assets_url . 'assets/js/sbr-feed' . $min . '.js',
438 ['jquery'],
439 SBRVER,
440 true
441 );
442 }
443
444 $data = array(
445 'adminAjaxUrl' => admin_url('admin-ajax.php'),
446 // Translatable strings for JS-injected accessible names and SR announcements (WCAG 3.1.2).
447 'a11y' => array(
448 /* translators: %s: number of reviews just loaded. Announced to screen readers after Load More. */
449 'reviewLoaded' => __('%s review loaded.', 'reviews-feed'),
450 /* translators: %s: number of reviews just loaded. Announced to screen readers after Load More. */
451 'reviewsLoaded' => __('%s reviews loaded.', 'reviews-feed'),
452 'allReviewsShown' => __('All reviews shown.', 'reviews-feed'),
453 'photoViewer' => __('Review photo viewer', 'reviews-feed'),
454 'previousPhoto' => __('Previous photo', 'reviews-feed'),
455 'nextPhoto' => __('Next photo', 'reviews-feed'),
456 'closePhotoViewer' => __('Close photo viewer', 'reviews-feed'),
457 'close' => __('Close', 'reviews-feed'),
458 'previous' => __('Previous', 'reviews-feed'),
459 'next' => __('Next', 'reviews-feed'),
460 'reviewerPhoto' => __('Reviewer photo', 'reviews-feed'),
461 ),
462 );
463 //Pass option to JS file
464 wp_localize_script('sbr_scripts', 'sbrOptions', $data);
465
466 if ($enqueue || SB_Reviews_Blocks::is_gb_editor()) {
467 wp_enqueue_style('sbr_styles');
468 wp_enqueue_script('sbr_scripts');
469 }
470 }
471 add_action('wp_enqueue_scripts', 'sbr_scripts_enqueue', 2);
472
473 function sbr_esc_html_with_br($text)
474 {
475 return str_replace(array( '&lt;br /&gt;', '&lt;br&gt;' ), '<br>', esc_html(nl2br($text)));
476 }
477
478 /**
479 * Neutralize WordPress shortcodes in third-party review content before output.
480 *
481 * Review data imported from connected sources (Google, Yelp, Booking.com, EDD,
482 * etc.) is rendered inside the dynamic `sbr/sbr-feed-block`. WordPress runs
483 * `do_blocks()` on `the_content` at priority 9 and `do_shortcode()` at priority
484 * 11, so any shortcode left in the rendered block markup is expanded
485 * server-side — including a shortcode an unauthenticated visitor planted in a
486 * public review (e.g. a reviewer name or review body of `[gallery ids=1]`).
487 * The escaping helpers (`esc_html()`, `sbr_kses_review_text()`) deliberately leave the
488 * `[` and `]` characters untouched, so they do not stop this on their own.
489 *
490 * Encoding the square brackets to HEX HTML entities (`&#x5B;` / `&#x5D;`) keeps
491 * the literal text visible to the visitor (the browser renders them as `[` / `]`)
492 * while ensuring `do_shortcode()` can never match them. Apply this as the
493 * OUTERMOST wrapper around already-escaped output: `esc_html()` / `sbr_kses_review_text()`
494 * run first on the raw text (they leave `[` and `]` alone), then this encodes the
495 * brackets last.
496 *
497 * HEX, not decimal, is mandatory here: WordPress core's `do_shortcode()` ends by
498 * calling `unescape_invalid_shortcodes()`, which runs
499 * `str_replace( array( '&#91;', '&#93;' ), array( '[', ']' ), $content )` over the
500 * processed content. That reverses the DECIMAL entities `&#91;` / `&#93;` straight
501 * back to raw `[` / `]` — re-arming the very shortcode we just neutralized (the
502 * feed renders through `do_shortcode` via the `[reviews-feed]` shortcode and again
503 * through the block + `the_content` chain, so this fires in practice). It does NOT
504 * touch the hex forms `&#x5B;` / `&#x5D;`, so those survive intact. Using decimal
505 * here is self-defeating; see SMASH-1607 follow-up (CVE-2026-10724 regression).
506 *
507 * @since 2.6.5
508 *
509 * @see https://awesomemotive.atlassian.net/browse/SMASH-1607 (CVE-2026-10724)
510 *
511 * Non-string (e.g. null) or empty input is returned unchanged, so the type is
512 * intentionally permissive — callers pass already-escaped output, but the guard
513 * keeps a stray null/empty safe rather than coercing it.
514 *
515 * @param string|null $text Already-escaped output that may contain shortcode brackets.
516 * @return string|null The text with `[` and `]` encoded to hex HTML entities (`&#x5B;` / `&#x5D;`); the input unchanged if it isn't a non-empty string.
517 */
518 function sbr_neutralize_shortcodes($text)
519 {
520 if (! is_string($text) || $text === '') {
521 return $text;
522 }
523
524 // Hex entities (not decimal): WordPress core's unescape_invalid_shortcodes()
525 // str_replaces decimal &#91;/&#93; back to [/] inside do_shortcode(), which would
526 // re-arm the shortcode. Hex forms are not reversed. See docblock + SMASH-1607.
527 return str_replace(array( '[', ']' ), array( '&#x5B;', '&#x5D;' ), $text);
528 }
529
530 /**
531 * Allowlist for rendering a review body. Use this instead of wp_kses_post().
532 *
533 * SMASH-1795 — wp_kses_post() is the *post editor* allowlist and keeps `<img>` with
534 * its class/src/alt, which the feed script then re-parsed out of the alt. This permits
535 * WordPress's comment-formatting set instead: emphasis, lists, quotes, headings and
536 * `a[href|title|rel]`. Links are allowed because Woo/EDD bodies contain them and
537 * wp_kses() drops a disallowed protocol from the href. `img` never is, nor any
538 * attribute the front end reads back and re-parses.
539 *
540 * Runs on the READ path, at every review-text sink, so it also covers bodies already
541 * stored and writers that bypass the write-side filter — Woo and EDD pass
542 * `comment_content` straight into `$review['text']`. `nl2br()` output survives.
543 *
544 * @param string|null $text Raw review body.
545 * @return string Sanitised body, safe to echo.
546 */
547 function sbr_kses_review_text($text)
548 {
549 if (! is_string($text) || $text === '') {
550 return '';
551 }
552
553 // Resolve an emoji image to its alt BEFORE the allowlist drops the tag, so a 😀
554 // is shown rather than silently deleted. Server-side twin of stripEmojihtml()
555 // (assets/js/sbr-feed.js), resolved the same way — decode once, then escape — so
556 // a payload hidden in the alt lands as inert text here too.
557 // Never let a PCRE failure blank the review. preg_replace_callback() returns NULL
558 // on any engine error (backtrack/recursion limit, JIT stack) and `(string) null`
559 // is '' — which would silently empty the body instead of degrading to unresolved
560 // emoji markup. Same failure shape as the read-more blanking bug (ae11c55): the
561 // safe fallback is the text we already had.
562 //
563 // Measured, so the next reader doesn't have to re-derive it: the pattern is NOT
564 // quadratic on a `>`-less run. Six adversarial shapes — repeated `class=`,
565 // repeated `emoji`, unterminated quotes, stacked `<img` prefixes — at 4 KB to
566 // 36 KB all return in under 1.4ms with preg_last_error() == PREG_NO_ERROR on
567 // PHP 8.2 (pcre.backtrack_limit 1000000). The character classes exclude `>`, so
568 // the required literals gate progression and PCRE fails fast. Invalid UTF-8 is
569 // not a NULL route either: there is no /u modifier, so the match is byte-wise.
570 // The guard is here because the cast was unsafe in principle, not because a
571 // reachable payload was found.
572 $emoji_resolved = preg_replace_callback(
573 '#<img\b[^>]*\bclass\s*=\s*["\']?[^"\'>]*\bemoji\b[^"\'>]*["\']?[^>]*>#i',
574 static function ($match) {
575 if (preg_match('#\balt\s*=\s*("([^"]*)"|\'([^\']*)\'|([^\s>]+))#i', $match[0], $alt) !== 1) {
576 return '';
577 }
578 $value = $alt[2] ?? '';
579 if ($value === '') {
580 $value = $alt[3] ?? '';
581 }
582 if ($value === '') {
583 $value = $alt[4] ?? '';
584 }
585 return esc_html(html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
586 },
587 $text
588 );
589 if (is_string($emoji_resolved)) {
590 $text = $emoji_resolved;
591 }
592
593 /**
594 * Filter the tags allowed in a rendered review body.
595 *
596 * Intentionally narrow. Widening this re-opens SMASH-1795 if a tag that can
597 * carry an attribute the front end later reads back is added.
598 *
599 * MUST return an array. A non-array return is ignored — see the guard below.
600 *
601 * @param array<string,array<string,array<mixed>>> $tags Allowed tags in wp_kses() format.
602 */
603 $default = array(
604 'br' => array(),
605 'em' => array(),
606 'strong' => array(),
607 'b' => array(),
608 'i' => array(),
609 'p' => array(),
610 'span' => array(),
611 // Woo/EDD bodies are raw comment_content, so they legitimately carry the
612 // WP-comment markup below; a narrower list drops it from stored reviews.
613 // No `target`: permitting it without forcing rel="noopener" hands the opened
614 // page a window.opener handle. WP's comment allowlist omits it too.
615 'a' => array('href' => array(), 'title' => array(), 'rel' => array()),
616 'blockquote' => array('cite' => array()),
617 'q' => array('cite' => array()),
618 'cite' => array(),
619 'code' => array(),
620 'pre' => array(),
621 'del' => array(),
622 'ins' => array(),
623 'ul' => array(),
624 'ol' => array(),
625 'li' => array(),
626 's' => array(),
627 'strike' => array(),
628 'u' => array(),
629 'sub' => array(),
630 'sup' => array(),
631 'hr' => array(),
632 'abbr' => array('title' => array()),
633 'acronym' => array('title' => array()),
634 'h1' => array(),
635 'h2' => array(),
636 'h3' => array(),
637 'h4' => array(),
638 'h5' => array(),
639 'h6' => array(),
640 );
641
642 $allowed = apply_filters('sbr_allowed_review_text_tags', $default);
643
644 // wp_kses() reads a STRING second argument as a CONTEXT NAME, so a filter
645 // returning 'post' resolves $allowedposttags — img included — and re-opens this
646 // exact chain. Non-arrays fall back to the default rather than being trusted.
647 if (! is_array($allowed)) {
648 $allowed = $default;
649 }
650
651 return wp_kses($text, $allowed);
652 }
653
654
655
656
657 function sbr_get_fb_connection_urls($is_settings = false)
658 {
659 $urls = array();
660 $admin_url_state = $is_settings ?
661 admin_url('admin.php?page=sbr-settings') :
662 admin_url('admin.php?page=sbr');
663 $sb_admin_email = get_option('admin_email');
664 $nonce = wp_create_nonce('cff_con');
665 $sw_flag = !empty($_GET['sw-feed']) ? true : false;
666
667 // If the admin_url isn't returned correctly then use a fallback.
668 if (
669 $admin_url_state === '/wp-admin/admin.php?page=sbr'
670 || $admin_url_state === '/wp-admin/admin.php?page=sbr&tab=configuration'
671 ) {
672 $admin_url_state = "http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
673 }
674
675 $urls['page'] = [
676 'connect' => SBR_FB_CONNECT_URL,
677 'wordpress_user' => $sb_admin_email,
678 'v' => 'pro',
679 'vn' => SBRVER,
680 'cff_con' => $nonce,
681 'sw_feed' => $sw_flag
682 ];
683
684 $urls['stateURL'] = $admin_url_state;
685 return $urls;
686 }
687
688 function check_license_valid()
689 {
690 $sbr_settings = get_option('sbr_settings', []);
691 return isset($sbr_settings['license_key'])
692 && !empty($sbr_settings['license_key'])
693 && isset($sbr_settings['license_status'])
694 && !empty($sbr_settings['license_status'])
695 && $sbr_settings['license_status'] !== 'invalid';
696 }
697
698 function sbr_plugin_action_links($links)
699 {
700 $settings_link = check_license_valid() ? admin_url('admin.php?page=sbr-settings') : admin_url('admin.php?page=sbr');
701 $support_link = check_license_valid() ? admin_url('admin.php?page=sbr-support') : admin_url('admin.php?page=sbr');
702 $links = array_merge(
703 array(
704 '<a href="' . esc_url($settings_link) . '">' . __('Settings', 'reviews-feed') . '</a>'
705 ),
706 $links
707 );
708
709 if (!Util::sbr_is_pro()) {
710 $links = array_merge(
711 array(
712 '<a href="https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=plugins-page&utm_medium=upgrade-link&utm_content=UpgradeToPro" target="_blank" style="font-weight:bold; color: #50a56d;">' . __('Upgrade to Pro', 'reviews-feed') . '</a>'
713 ),
714 $links
715 );
716 } else {
717 $links = array_merge(
718 array(
719 '<a href="' . esc_url($support_link) . '">' . __('Support', 'reviews-feed') . '</a>'
720 ),
721 $links
722 );
723 }
724
725 return $links;
726 }
727 add_action('plugin_action_links_' . SBR_PLUGIN_BASENAME, 'sbr_plugin_action_links');
728
729
730 add_action('current_screen', 'sbr_check_current_screen');
731
732 function sbr_check_current_screen()
733 {
734 if (Util::currentPageIs('sbr')) {
735 add_action('admin_enqueue_scripts', 'dequeue_smash_plugins_style');
736 }
737 }
738
739 function dequeue_smash_plugins_style()
740 {
741 wp_dequeue_style('cff_custom_wp_admin_css');
742 wp_deregister_style('cff_custom_wp_admin_css');
743
744 wp_dequeue_style('feed-global-style');
745 wp_deregister_style('feed-global-style');
746
747 wp_dequeue_style('sb_instagram_admin_css');
748 wp_deregister_style('sb_instagram_admin_css');
749
750 wp_dequeue_style('ctf_admin_styles');
751 wp_deregister_style('ctf_admin_styles');
752 }
753
754 function sbr_custom_menu()
755 {
756 if (Util::sbr_is_pro() === false) {
757 $cap = current_user_can('manage_reviews_feed_options') ? 'manage_reviews_feed_options' : 'manage_options';
758 $cap = apply_filters('sbr_settings_pages_capability', $cap);
759 add_submenu_page(
760 'sbr',
761 __('Upgrade to Pro', 'reviews-feed'),
762 __('<div class="sb-pro-upgradelink-bg"></div><strong class="sb-pro-upgradelink">Upgrade to Pro</strong>', 'reviews-feed'),
763 $cap,
764 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=menu-link&utm_medium=upgrade-link&utm_content=UpgradeToPro',
765 ''
766 );
767 }
768 }
769
770 add_action('admin_menu', 'sbr_custom_menu', 40);
771
772
773 function sbr_text_domain()
774 {
775 load_plugin_textdomain('reviews-feed', false, dirname(SBR_PLUGIN_BASENAME) . '/languages');
776 }
777 add_action('init', 'sbr_text_domain');
778
779 function sbr_get_current_time()
780 {
781 $current_time = time();
782
783 // where to do tests
784 //$current_time = strtotime( 'November 25, 2020' );
785
786 return $current_time;
787 }
788
789
790 function sbr_recursive_parse_args($args, $defaults)
791 {
792 $new_args = (array) $defaults;
793
794 foreach ($args as $key => $value) {
795 if (is_array($value) && isset($new_args[ $key ])) {
796 $new_args[ $key ] = sbr_recursive_parse_args($value, $new_args[ $key ]);
797 } else {
798 $new_args[ $key ] = $value;
799 }
800 }
801 return $new_args;
802 }
803
804 function sbr_doing_openssl()
805 {
806 return extension_loaded('openssl');
807 }
808
809 function sbr_encrypt_decrypt($action, $string)
810 {
811 $output = false;
812
813 $encrypt_method = "AES-256-CBC";
814 $secret_key = 'SMA$H.BA[[OON#23121';
815 $secret_iv = '1231394873342102221';
816
817 // hash
818 $key = hash('sha256', $secret_key);
819
820 // iv - encrypt method AES-256-CBC expects 16 bytes - else you will get a warning
821 $iv = substr(hash('sha256', $secret_iv), 0, 16);
822
823 if ($action === 'encrypt') {
824 $output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
825 $output = base64_encode($output);
826 } elseif ($action === 'decrypt') {
827 $output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
828 }
829
830 return $output;
831 }
832
833
834 /**
835 * Returns a list of providers that needs
836 * the bulk history update
837 *
838 * @return array
839 *
840 * @since 1.5
841 */
842 function sbr_get_bulk_providers()
843 {
844 return [
845 'google',
846 'yelp'
847 ];
848 }
849
850 /**
851 * Returns a list of providers that
852 * have media
853 *
854 * @return array
855 *
856 * @since 1.5
857 */
858 function sbr_get_media_providers()
859 {
860 return [
861 'yelp',
862 'tripadvisor',
863 'google'
864 ];
865 }
866
867 /**
868 * Returns a list of providers that doesn't
869 * have media
870 *
871 * @return array
872 *
873 * @since 1.5
874 */
875 function sbr_get_no_media_providers()
876 {
877 return [
878 'facebook',
879 'woocommerce',
880 'edd',
881 'airbnb',
882 'booking',
883 'aliexpress'
884 ];
885 }
886
887 /**
888 * Returns a list of providers that have
889 * translations / languages Possibility
890 *
891 * @return array
892 *
893 * @since 1.5
894 */
895 function sbr_get_lang_providers()
896 {
897 return [
898 'google'
899 ];
900 }
901
902 /**
903 * Booking.com score band for a single review, on Booking's native 0-10 scale.
904 *
905 * Booking does NOT send a per-review qualifier: the only word its API returns is
906 * `review_score_word`, and that belongs to the PROPERTY (sb-relay
907 * RapidBookingRemoteSourcesRepository:492 — the SOURCE repository, not the reviews one).
908 * Verified on booking.com itself: the property block reads "Scored 9.5 / Exceptional",
909 * while an individual review card reads "Scored 8.0" followed straight by the review text,
910 * with no word. The per-review `title` is the guest's own headline, not a band — at rating 4
911 * our cached rows carry "Very good", "Fabulous", "Wonderful", "Good" and "Fantasico" all at
912 * once, so it cannot be a label.
913 *
914 * So the band is derived from the reviewer's own score. Provenance of each threshold,
915 * because it matters if these ever need defending:
916 * 9.5+ Exceptional — confirmed twice: Booking's API sent it for source 12166067 at 9.5,
917 * and booking.com renders "Rated exceptional / Exceptional" there.
918 * 9.0+ Superb — confirmed: Booking's API sent it for source 280149 at 9.4. Note
919 * third-party write-ups claim "Excellent" for this band; the API
920 * disagrees, and the API wins.
921 * 8.0+ Very good — Booking's published ladder. Their capitalisation, not "Very Good".
922 * 7.0+ Good — Booking's published ladder.
923 * 6.0+ Pleasant — Booking's published ladder.
924 * below — no word; Booking shows only the number.
925 *
926 * @param float $score Score on the 0-10 scale.
927 * @return string Band word, or '' when the score is below the lowest named band.
928 */
929 function sbr_booking_score_word($score)
930 {
931 $score = (float) $score;
932
933 if ($score >= 9.5) {
934 return __('Exceptional', 'reviews-feed');
935 }
936 if ($score >= 9) {
937 return __('Superb', 'reviews-feed');
938 }
939 if ($score >= 8) {
940 return __('Very good', 'reviews-feed');
941 }
942 if ($score >= 7) {
943 return __('Good', 'reviews-feed');
944 }
945 if ($score >= 6) {
946 return __('Pleasant', 'reviews-feed');
947 }
948
949 return '';
950 }
951
952 /**
953 * A single review's Booking score on the native 0-10 scale, from the reviewer's own rating.
954 *
955 * `$post['rating']` is stored 0-5 like every other provider. Booking's reviews API actually
956 * sends `average_score` on a 0-4 scale and the relay converts it with
957 * `round($value * 1.25, 1)` (sb-relay RapidRemoteBookingReviewsRepository::convertRating),
958 * so doubling back inherits up to ±0.125 of that rounding. Exactness needs the relay to
959 * forward the raw score. Verified against booking.com: our 4 -> 8.0 and 4.5 -> 9.0 match the
960 * "Scored 8.0" / "Scored 9.0" cards on the live page for source 12166067.
961 *
962 * @param array $post Normalised review.
963 * @return float Score on the 0-10 scale; 0.0 when there is no usable rating.
964 */
965 function sbr_booking_review_score($post)
966 {
967 $rating = isset($post['rating']) ? (float) $post['rating'] : 0.0;
968
969 return $rating > 0 ? round($rating * 2, 1) : 0.0;
970 }
971