PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / trunk
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More vtrunk
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 / Common / PostAggregator.php
reviews-feed / class / Common Last commit date
Admin 1 month ago Builder 6 days ago Customizer 6 days ago Exceptions 6 months ago Helpers 2 months ago Integrations 6 days ago Migrations 3 years ago ReviewAlerts 3 weeks ago Services 6 days ago Settings 6 days ago Support 3 months ago Traits 1 month ago UsageTracking 6 days ago Utils 2 months ago AuthorizationStatusCheck.php 1 month ago BusinessDataCache.php 6 months ago Clear_Cache.php 3 months ago Container.php 6 months ago DisplayElements.php 2 months ago Email_Notification.php 6 months ago Error_Reporter.php 3 months ago Feed.php 3 weeks ago FeedCache.php 5 months ago FeedCacheUpdater.php 3 months ago FeedDisplay.php 1 month ago Feed_Locator.php 6 months ago Parser.php 3 weeks ago PostAggregator.php 2 months ago RemoteRequest.php 6 days ago SBR_Education.php 3 months ago SBR_Schema_Service.php 3 weeks ago SBR_Settings.php 6 months ago ServiceContainer.php 6 days ago SinglePostCache.php 3 weeks ago TemplateRenderer.php 6 months ago Tooltip_Wizard.php 1 month ago Util.php 6 days ago
PostAggregator.php
576 lines
1 <?php
2
3 /**
4 * Class SinglePostCache
5 *
6 * @since 1.0
7 */
8
9 namespace SmashBalloon\Reviews\Common;
10
11 use SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager;
12 use SmashBalloon\Reviews\Common\Helpers\Data_Encryption;
13 use SmashBalloon\Reviews\Pro\Integrations\Forms\ReviewsSubmissionsRules;
14
15 class PostAggregator {
16 public const UPLOAD_FOLDER_NAME = 'sbr-feed-images';
17 public const POSTS_TABLE_NAME = SBR_POSTS_TABLE;
18
19 /**
20 * @var array
21 */
22 private $post_set = array();
23
24 private $upload_dir;
25
26 private $upload_url;
27
28 private $missing_media_found;
29
30 public function __construct()
31 {
32 $upload = wp_upload_dir();
33 $upload_dir = $upload['basedir'];
34 $upload_dir = trailingslashit($upload_dir) . self::UPLOAD_FOLDER_NAME;
35 $this->upload_dir = $upload_dir;
36
37 $upload_url = trailingslashit($upload['baseurl']) . self::UPLOAD_FOLDER_NAME;
38 $this->upload_url = $upload_url;
39 $this->missing_media_found = false;
40 }
41
42 public function missing_media_found()
43 {
44 return $this->missing_media_found;
45 }
46
47 public function add($post_set)
48 {
49 return $this->post_set = array_merge($this->post_set, $post_set);
50 }
51
52 /**
53 * Get posts from database for given sources
54 *
55 * @param array $requests_needed Source IDs to fetch posts for
56 * @param string|int $language_or_limit Language code (deprecated, ignored) or limit number
57 * @param int $limit Maximum number of posts to fetch (default 150)
58 * @return array
59 */
60 public function db_post_set($requests_needed, $language_or_limit = null, $limit = 150)
61 {
62 global $wpdb;
63 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
64 $where_clause = $this->build_provider_business_where_clause($requests_needed);
65
66 // Handle backwards compatibility: if second param is numeric, treat it as limit
67 // Otherwise, use third param (for calls that pass language string as second param)
68 if (is_numeric($language_or_limit) && $language_or_limit > 0) {
69 $limit = absint($language_or_limit);
70 } else {
71 $limit = absint($limit);
72 }
73
74 // Ensure minimum limit of 1
75 $limit = max(1, $limit);
76
77 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd, where_clause built from sanitized values
78 $results = $wpdb->get_results(
79 "SELECT * FROM $table_name
80 WHERE $where_clause ORDER BY time_stamp DESC LIMIT $limit",
81 ARRAY_A
82 );
83 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
84 $results = self::remove_duplicated_posts_list($results);
85 return $results;
86 }
87
88 // phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh -- Pre-existing complexity
89 public static function remove_duplicated_posts_list($posts_list, $type = 'db')
90 {
91 // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh
92 $posts_db = [];
93 $posts_list_result = [];
94 $encryption = new Data_Encryption();
95 foreach ($posts_list as $postKey => $post) {
96 $post = is_array($post) ? $post : json_decode($encryption->maybe_decrypt($post), true);
97 if ($type === 'db') {
98 $post['post_content'] = $post['provider'] === 'facebook' ? $encryption->maybe_decrypt($post['post_content']) : $post['post_content'];
99 $post['json_data'] = $post['provider'] === 'facebook' ? $encryption->maybe_decrypt($post['json_data']) : $post['json_data'];
100 $post_json = isset($post['json_data']) ? json_decode($post['json_data'], true) : null;
101 if (!empty($post_json)) {
102 $post_json['source'] = ['id' => $post['provider_id'], 'url' => $post_json['url'] ?? ''];
103 if (intval($post['images_done']) === 0) {
104 self::cache_review_images($post, $post_json);
105 }
106 }
107 } else {
108 $post_json = $post;
109 }
110
111 if ($post_json !== null) {
112 // SMASH-1587: PHP 7-era json_data can hold a scalar 'provider' slug;
113 // the $post_json['provider']['name'] reads below fatal on PHP 8 without this.
114 $post_json = Util::normalize_review_shape($post_json);
115 $search_value = $post_json['source']['id'] . '-' . $post_json['rating'] . '-' . $post_json['reviewer']['name'] . '-' . $post_json['provider']['name'];
116 //IF EDD CHECK WITH TIME TOO SINCE A USER CAN POST MULTIPLE REVIEWS
117 $search_value .= $post_json['provider']['name'] === 'edd' ? $post_json['time'] : '';
118 // Include text content hash to allow same rating from same reviewer with different content
119 $review_text = $post_json['text'] ?? '';
120 if (! empty($review_text)) {
121 $search_value .= '-' . md5($review_text);
122 }
123
124 if (!in_array($search_value, $posts_db)) {
125 array_push($posts_db, $search_value);
126 // For the 'json' path (cache-hit render, Feed.php:172) push the
127 // shape-normalized review so display readers (FeedDisplay/Parser)
128 // can't fatal on a scalar provider/reviewer/source from an old
129 // cache. The 'db' path returns raw rows that normalize_db_post_set
130 // re-decodes + normalizes, so it stays as-is. (SMASH-1587/WPSA-63160)
131 array_push($posts_list_result, $type === 'json' ? $post_json : $post);
132 }
133 }
134 }
135 return $posts_list_result;
136 }
137
138 public static function cache_review_images($post, $post_json)
139 {
140 return;
141 $cache_post = [
142 'review_id' => $post_json['review_id'],
143 'text' => $post_json['text'],
144 'rating' => $post_json['rating'],
145 'time' => $post_json['time'],
146 'reviewer' => $post_json['reviewer'],
147 'source' => [
148 'id' => $post_json['source']['id'],
149 'url' => $post_json['source']['url']
150 ],
151 'lang' => $post['lang'],
152 ];
153 SBR_Feed_Saver_Manager::cache_single_review(
154 $cache_post,
155 [
156 'id' => $post['provider_id'],
157 'provider' => $post['provider'],
158 'name' => $post['provider'],
159 'url' => $post_json['source']['url']
160 ],
161 $post['lang'],
162 sbr_get_no_media_providers(),
163 sbr_get_lang_providers()
164 );
165 }
166
167 // phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh -- Pre-existing complexity
168 public static function remove_duplicated_posts_routine()
169 {
170 global $wpdb;
171 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
172 $posts_db = [];
173 $posts_id_todelete = [];
174
175 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd
176 $results = $wpdb->get_results(
177 "SELECT * FROM $table_name",
178 ARRAY_A
179 );
180 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
181
182 foreach ($results as $post) {
183 $post_json = isset($post['json_data']) ? json_decode($post['json_data'], true) : null;
184 if ($post_json !== null) {
185 // SMASH-1587: scalar 'provider' in old json_data would fatal the reads below on PHP 8.
186 $post_json = Util::normalize_review_shape($post_json);
187 $search_value = $post_json['source']['id'] . '-' . $post_json['rating'] . '-' . $post_json['reviewer']['name'] . '-' . $post_json['provider']['name'];
188 //IF EDD CHECK WITH TIME TOO SINCE A USER CAN POST MULTIPLE REVIEWS
189 $search_value .= $post_json['provider']['name'] === 'edd' ? $post_json['time'] : '';
190
191 if (in_array($search_value, $posts_db)) {
192 array_push($posts_id_todelete, $post['id']);
193 } else {
194 array_push($posts_db, $search_value);
195 }
196 }
197 }
198
199 if (sizeof($posts_id_todelete) > 0) {
200 $posts_ids = implode(',', $posts_id_todelete);
201 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IDs are integers from DB, table name is esc_sql'd
202 $wpdb->query(
203 "DELETE FROM $table_name WHERE id IN ($posts_ids)"
204 );
205 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
206 }
207 }
208 // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh
209
210
211 public function db_posts_for_media_finding_and_resizing_set($requests_needed)
212 {
213 global $wpdb;
214 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
215
216 $where_clause = $this->build_provider_business_where_clause($requests_needed);
217
218 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd, where_clause built from sanitized values
219 $results = $wpdb->get_results(
220 "SELECT * FROM $table_name
221 WHERE $where_clause
222 AND images_done = 0
223 ORDER BY time_stamp DESC LIMIT 150",
224 ARRAY_A
225 );
226 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
227
228 return $results;
229 }
230
231
232
233 public function normalize_db_post_set($results)
234 {
235 $normalized_set = array();
236 foreach ($results as $result) {
237 if (! empty($result['json_data'])) {
238 $post = json_decode($result['json_data'], true);
239 if (! empty($post)) {
240 // SMASH-1587/WPSA-63160: PHP 7-era json_data can hold scalar
241 // provider/reviewer/source/media shapes. This is the render-path
242 // source (posts_from_db, moderation, media-finding), so coerce here
243 // once — covers add_local_image_urls below + every FeedDisplay /
244 // Parser reader downstream, which would otherwise fatal on PHP 8.
245 $post = Util::normalize_review_shape($post);
246 $post = self::add_local_image_urls($post, $result);
247 }
248 if ((int)$result['images_done'] === 0) {
249 $this->missing_media_found = true;
250 }
251 $normalized_set[] = $post;
252 }
253 }
254
255 return $normalized_set;
256 }
257
258 public function add_local_image_urls($post, $result)
259 {
260 $return = $post;
261 $base_url = $this->upload_url;
262 $resize_url = apply_filters('sbr_resize_url', trailingslashit($base_url));
263
264 if (! empty($post['reviewer']['avatar'])) {
265 if (! empty($result['avatar_id']) && $result['avatar_id'] !== 'error') {
266 $return['reviewer']['avatar_local'] = $resize_url . $result['avatar_id'] . '.png';
267 }
268 }
269
270 if (! empty($post['media'])) {
271 $sizes = ! empty($result['sizes']) ? json_decode($result['sizes']) : array();
272 $i = 0;
273 foreach ($post['media'] as $single_image) {
274 // A scalar media element (e.g. a flat URL string) would make the
275 // $return['media'][$i]['local'] write below a string-offset write =
276 // PHP 8 fatal. Skip it (keep $i aligned to the media index). SMASH-1587.
277 if (! is_array($single_image)) {
278 $i++;
279 continue;
280 }
281 if (! empty($result['media_id']) && $result['media_id'] !== 'error') {
282 $return['media'][ $i ]['local'] = array();
283 $media_id = $result['media_id'];
284 foreach ($sizes as $size) {
285 $local_url_for_size = $resize_url . $media_id . '-' . $i . '-' . $size . '.jpg';
286 $return['media'][ $i ]['local'][ $size ] = $local_url_for_size;
287 }
288 }
289 $i++;
290 }
291 }
292
293 return $return;
294 }
295
296 public function update_last_requested($requests_needed)
297 {
298 global $wpdb;
299 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
300 $where_clause = $this->build_provider_business_where_clause($requests_needed);
301 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd, where_clause built from sanitized values
302 $query = $wpdb->query(
303 $wpdb->prepare(
304 "UPDATE $table_name
305 SET last_requested = %s
306 WHERE $where_clause;",
307 date('Y-m-d')
308 )
309 );
310 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
311 }
312
313 private function build_provider_business_where_clause($requests_needed)
314 {
315
316 $i = 1;
317 $where_clause = '';
318 foreach ($requests_needed as $request) {
319 $single_clause = sprintf('provider_id = "%s"', esc_sql($request['account_id']));
320 if (isset($request['lang'])) {
321 $single_clause .= sprintf(' AND lang = "%s"', esc_sql($request['lang']));
322 }
323 $where_clause .= '(' . $single_clause . ')';
324 if ($i < count($requests_needed)) {
325 $where_clause .= ' OR ';
326 }
327 $i++;
328 }
329
330 return $where_clause;
331 }
332
333
334 /**
335 * Get List Provider/Source Posts
336 *
337 * @param string $provider_id | int $page
338 *
339 * @return array
340 *
341 * @since 1.4
342 */
343 public static function get_source_posts_list($provider_id, $page = 1)
344 {
345 global $wpdb;
346 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
347 $limit = 40;
348 $offset = ($page - 1) * $limit;
349 $encryption = new Data_Encryption();
350
351 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd
352 $results = $wpdb->get_results(
353 $wpdb->prepare(
354 "SELECT *
355 FROM $table_name
356 WHERE provider_id = %s
357 ORDER BY id DESC
358 LIMIT %d
359 OFFSET %d
360 ",
361 $provider_id,
362 $limit,
363 $offset
364 ),
365 ARRAY_A
366 );
367 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
368
369 $decrypted_results = [];
370 foreach ($results as $s_post) {
371 if ($s_post['provider'] === 'facebook') {
372 $s_post['post_content'] = $encryption->maybe_decrypt($s_post['post_content']);
373 $s_post['json_data'] = $encryption->maybe_decrypt($s_post['json_data']);
374 $json_data = json_decode($s_post['json_data'], true);
375
376 if (isset($json_data['rating']) && !is_numeric($json_data['rating'])) {
377 $rating = isset($json_data['rating']) && $json_data['rating'] === 'negative' ? 1 : 5;
378 $json_data['rating'] = $rating;
379 }
380
381 $s_post['json_data'] = wp_json_encode($json_data);
382 }
383 array_push($decrypted_results, $s_post);
384 }
385 return $decrypted_results;
386 }
387
388 /**
389 * Delete Review from Collection
390 *
391 * @param string $provider_id | string $provider
392 *
393 * @return void
394 *
395 * @since 1.4
396 */
397 public static function delete_review($provider_id, $review_id, $provider)
398 {
399 global $wpdb;
400 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
401 $provider_value = $provider === 'collection' ? 'none' : $provider;
402
403 ReviewsSubmissionsRules::remove_auto_approved($review_id, $provider_id, '', 'single', false);
404
405 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd
406 $wpdb->query(
407 $wpdb->prepare(
408 "DELETE FROM $table_name
409 WHERE provider_id = %s AND provider = %s AND post_id = %s
410 ",
411 $provider_id,
412 $provider_value,
413 $review_id
414 )
415 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
416 );
417 }
418
419
420 /**
421 * Select a List of reviews By Review ID
422 *
423 * @param array $reviews_ids
424 *
425 * @return array|boolean
426 *
427 * @since 1.4
428 */
429 public static function get_list_reviews_by_ids($reviews_ids)
430 {
431 global $wpdb;
432 $table_name = $wpdb->prefix . self::POSTS_TABLE_NAME;
433
434 if (empty($reviews_ids) || ! is_array($reviews_ids)) {
435 return false;
436 }
437
438 // Filter out empty values
439 $reviews_ids = array_filter($reviews_ids, function ($id) {
440 return ! empty($id);
441 });
442
443 if (empty($reviews_ids)) {
444 return false;
445 }
446
447 // Build placeholders for prepare() - one %s for each ID
448 $placeholders = implode(', ', array_fill(0, count($reviews_ids), '%s'));
449
450 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is internal constant, placeholders are dynamically generated
451 $results = $wpdb->get_results(
452 $wpdb->prepare(
453 "SELECT * FROM {$table_name} WHERE post_id IN ({$placeholders})",
454 ...$reviews_ids
455 ),
456 ARRAY_A
457 );
458 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
459
460 return $results;
461 }
462
463 /**
464 * Select a List of reviews By Review ID
465 *
466 * @param string $provider_id | array $reviews_ids
467 *
468 * @return void
469 *
470 * @since 1.4
471 */
472 public static function insert_multiple_reviews($provider_id, $reviews_ids)
473 {
474 $reviews_to_insert = PostAggregator::get_list_reviews_by_ids($reviews_ids);
475 $encryption = new Data_Encryption();
476 if (is_array($reviews_to_insert)) {
477 foreach ($reviews_to_insert as $rev) {
478 $review_id = $provider_id . time() . wp_rand();
479 $json_data = $encryption->maybe_decrypt($rev['json_data']);
480 $review = json_decode($json_data, true);
481 $review_store = Util::parse_single_review($review, $provider_id, $review_id);
482 $review_store['json_data'] = $review_store;
483
484 $single_post_cache = new \SmashBalloon\Reviews\Pro\SinglePostCache($review_store, new \SmashBalloon\Reviews\Pro\MediaFinder($review_store['source']));
485 $single_post_cache->set_provider_id($provider_id);
486 $single_post_cache->store();
487 }
488 }
489 }
490
491 /**
492 * Delete All Reviews from Provider/Source ID
493 *
494 * @param string $provider_id
495 *
496 * @return void
497 *
498 * @since 1.4
499 */
500 public static function delete_reviews_by_provide_id($provider_id)
501 {
502 global $wpdb;
503 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
504 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd
505 $wpdb->query(
506 $wpdb->prepare(
507 "DELETE FROM $table_name
508 WHERE provider_id = %s",
509 $provider_id
510 )
511 );
512 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
513 }
514
515 /**
516 * Get List Provider/Source Posts
517 *
518 * @param string $provider_id
519 *
520 * @return array
521 *
522 * @since 1.4
523 */
524 public static function get_source_all_posts($provider_id)
525 {
526 global $wpdb;
527 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
528 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is esc_sql'd
529 $results = $wpdb->get_results(
530 $wpdb->prepare(
531 "SELECT *
532 FROM $table_name
533 WHERE provider_id = %s
534 ",
535 $provider_id
536 ),
537 ARRAY_A
538 );
539 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
540 return $results;
541 }
542
543
544 /**
545 * Search Reviews of a specific Provider
546 *
547 * @param string $provider_id | string $search_text
548 *
549 * @return array
550 *
551 * @since 1.4
552 */
553 public static function search_source_posts_list($provider_id, $search_text)
554 {
555 global $wpdb;
556 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
557 $search_text_array = explode(' ', $search_text);
558 $search_string = implode('.*', $search_text_array);
559 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder -- Table name is esc_sql'd, REGEXP requires quotes
560 $results = $wpdb->get_results(
561 $wpdb->prepare(
562 "SELECT *
563 FROM $table_name
564 WHERE provider_id = %s
565 AND json_data REGEXP '%s'
566 ",
567 $provider_id,
568 $search_string
569 ),
570 ARRAY_A
571 );
572 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder
573 return $results;
574 }
575 }
576