PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.8.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.8.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 / Common / SinglePostCache.php
reviews-feed / class / Common Last commit date
Admin 1 month ago Builder 1 month ago Customizer 1 month ago Exceptions 1 month ago Helpers 1 month ago Integrations 1 month ago Migrations 1 month ago ReviewAlerts 1 month ago Services 1 month ago Settings 1 month ago Support 1 month ago Traits 1 month ago Utils 1 month ago AuthorizationStatusCheck.php 1 month ago BusinessDataCache.php 1 month ago Clear_Cache.php 1 month ago Container.php 1 month ago DisplayElements.php 1 month ago Email_Notification.php 1 month ago Error_Reporter.php 1 month ago Feed.php 1 month ago FeedCache.php 1 month ago FeedCacheUpdater.php 1 month ago FeedDisplay.php 1 month ago Feed_Locator.php 1 month ago Parser.php 1 month ago PostAggregator.php 1 month ago RemoteRequest.php 1 month ago SBR_Education.php 1 month ago SBR_Settings.php 1 month ago ServiceContainer.php 1 month ago SinglePostCache.php 1 month ago TemplateRenderer.php 1 month ago Tooltip_Wizard.php 1 month ago Util.php 1 month ago
SinglePostCache.php
501 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\Helpers\Data_Encryption;
12
13 class SinglePostCache {
14 public const UPLOAD_FOLDER_NAME = 'sbr-feed-images';
15
16 public const POSTS_TABLE_NAME = SBR_POSTS_TABLE;
17
18 /**
19 * @var array
20 */
21 private $post_data;
22
23 private $upload_dir;
24
25 private $storage_data;
26 private $provider_id;
27 private $lang;
28 public $encryption;
29
30 public function __construct($post_data, $media_finder = null, $provider_id = null)
31 {
32 // SMASH-1587: coerce a scalar 'provider' slug into ['name' => ...] before
33 // any $this->post_data['provider']['name'] read in this class (which would
34 // fatal on PHP 8). Shared normalizer in Util — single source of truth.
35 $this->post_data = Util::normalize_review_shape($post_data);
36
37 $upload = wp_upload_dir();
38 $upload_dir = $upload['basedir'];
39 $upload_dir = trailingslashit($upload_dir) . self::UPLOAD_FOLDER_NAME;
40 $this->upload_dir = $upload_dir;
41
42 $this->lang = '';
43
44 $this->storage_data = array(
45 'media_id' => '',
46 'sizes' => '[]',
47 'aspect_ratio' => 1,
48 'images_done' => 0
49 );
50 $this->encryption = new Data_Encryption();
51 }
52
53 public function get_storage_data()
54 {
55 return $this->storage_data;
56 }
57
58 public function get_post_data()
59 {
60 return $this->post_data;
61 }
62
63 public function set_provider_id($provider_id)
64 {
65 $this->provider_id = $provider_id;
66 }
67
68 public function set_lang($lang)
69 {
70 $this->lang = $lang;
71 }
72
73 /**
74 * Check and process API media (reviews photos)
75 *
76 * Stub method for Common version. Pro version overrides with full implementation.
77 *
78 * @since 2.5.0
79 * @return void
80 */
81 public function check_api_media()
82 {
83 // No-op in Common version. Pro version handles media processing.
84 }
85
86 public function get_provider_id()
87 {
88 return $this->provider_id;
89 }
90
91 public function set_storage_data($key, $value)
92 {
93 return $this->storage_data[ $key ] = $value;
94 }
95
96
97 public function resize_images($image_sizes)
98 {
99 $new_file_name = $this->post_data['provider']['name'] . '-' . $this->post_data['review_id'];
100
101 $image_source_set = ! empty($this->post_data['media']) ? $this->post_data['media'] : array();
102
103 $image_source_set = empty($image_source_set) && !empty($this->post_data['reviews_photos']) ? $this->post_data['reviews_photos'] : $image_source_set;
104 // the process is considered a success if one image is successfully resized
105 $one_successful_image_resize = false;
106
107 foreach ($image_sizes as $image_size) {
108 $i = 0;
109 foreach ($image_source_set as $image_file_to_resize) {
110 // A scalar element (e.g. a flat URL string in reviews_photos) would
111 // fatal the ['type'] read on PHP 8; a partial array element (no
112 // 'type'/'url') would notice. Guard both (SMASH-1587 + PR #482 Copilot).
113 if ($i < 10 && is_array($image_file_to_resize) && isset($image_file_to_resize['type'], $image_file_to_resize['url']) && $image_file_to_resize['type'] === 'image') {
114 $this_image_file_name = $new_file_name . '-' . $i . '-' . $image_size . '.jpg';
115
116 $image_editor = wp_get_image_editor($image_file_to_resize['url']);
117 // not uncommon for the image editor to not work using it this way
118 if (! is_wp_error($image_editor)) {
119 $sizes = $image_editor->get_size();
120
121 $image_editor->resize($image_size, null);
122
123 $full_file_name = trailingslashit($this->upload_dir) . $this_image_file_name;
124
125 $saved_image = $image_editor->save($full_file_name);
126
127 if ($saved_image) {
128 $one_successful_image_resize = true;
129 }
130 } else {
131 // was error
132 }
133 }
134
135 $i++;
136 }
137 }
138
139 if ($one_successful_image_resize) {
140 $aspect_ratio = round($sizes['width'] / $sizes['height'], 2);
141 $media_id = $new_file_name;
142 } else {
143 $aspect_ratio = 1;
144 if (empty($image_source_set)) {
145 $media_id = '';
146 $image_sizes = array();
147 } else {
148 $media_id = 'error';
149 }
150 }
151
152 $this->storage_data['media_id'] = $media_id;
153 $this->storage_data['sizes'] = wp_json_encode($image_sizes);
154 $this->storage_data['aspect_ratio'] = $aspect_ratio;
155 $this->storage_data['images_done'] = 1;
156 }
157
158 public function resize_avatar($image_size)
159 {
160 $new_file_name = $this->post_data['provider']['name'] . '-' . $this->post_data['review_id'] . '-avatar';
161 $avatar = ! empty($this->post_data['reviewer']['avatar']) ? $this->post_data['reviewer']['avatar'] : false;
162
163 if (empty($avatar)) {
164 return;
165 }
166
167 $avatar_id = $new_file_name . '-' . $image_size;
168 $this_image_file_name = $avatar_id . '.png';
169
170 $image_editor = wp_get_image_editor($avatar);
171 // not uncommon for the image editor to not work using it this way
172 if (! is_wp_error($image_editor)) {
173 $sizes = $image_editor->get_size();
174
175 $image_editor->resize($image_size, null);
176
177 $full_file_name = trailingslashit($this->upload_dir) . $this_image_file_name;
178
179 $saved_image = $image_editor->save($full_file_name);
180
181 if (! $saved_image) {
182 $avatar_id = 'error';
183 }
184 } else {
185 $avatar_id = 'error';
186 }
187
188
189 $this->storage_data['avatar_id'] = $avatar_id;
190 }
191
192 public function db_record_exists()
193 {
194 $feed_id_match = $this->db_record();
195 if (! empty($feed_id_match)) {
196 $this->storage_data['media_id'] = $feed_id_match['media_id'];
197 $this->storage_data['sizes'] = $feed_id_match['sizes'];
198 $this->storage_data['aspect_ratio'] = $feed_id_match['aspect_ratio'];
199 $this->storage_data['images_done'] = $feed_id_match['images_done'];
200 $this->storage_data['json_data'] = $feed_id_match['json_data'];
201 }
202 return null !== $feed_id_match;
203 }
204
205 public function db_record()
206 {
207 global $wpdb;
208 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
209 $provider_id = !empty($this->post_data['provider_id'])
210 ? $this->post_data['provider_id']
211 : $this->provider_id;
212
213 if (isset($this->post_data['review_id'])) {
214 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is prefixed constant
215 $feed_id_match = $wpdb->get_results(
216 $wpdb->prepare(
217 "SELECT * FROM $table_name
218 WHERE post_id = %s AND lang = %s AND provider_id = %s LIMIT 1",
219 $this->post_data['review_id'],
220 $this->lang,
221 $provider_id
222 ),
223 ARRAY_A
224 );
225 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
226
227 if (! empty($feed_id_match[0])) {
228 return $feed_id_match[0];
229 }
230 }
231
232 return null;
233 }
234
235 public function store()
236 {
237 $rating = $this->post_data['rating'];
238 if (Util::is_facebook_collection_post($this->post_data)) {
239 $rating = $this->post_data['rating'] === 'positive' ? 5 : 1;
240 }
241
242 $now = date('Y-m-d H:i:s');
243 $post_id = $this->post_data['review_id'];
244 $provider_id = $this->get_provider_id();
245 $json_data = $this->should_encrypt($this->post_data, wp_json_encode($this->post_data));
246 $post_content = $this->should_encrypt($this->post_data, $this->post_data['text']);
247 $provider = $this->post_data['provider']['name'];
248 $business = $this->post_data['business']['id'] ?? '';
249 $time_stamp = date('Y-m-d H:i:s', $this->post_data['time']);
250
251 global $wpdb;
252 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
253
254 // Use INSERT ... ON DUPLICATE KEY UPDATE to prevent duplicates
255 // This handles race conditions where two requests try to insert the same review
256 // Note: We pass values directly in UPDATE clause instead of using VALUES() function
257 // for cross-version MySQL compatibility (VALUES() deprecated in MySQL 8.0.20+)
258 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safely prefixed constant
259 $result = $wpdb->query(
260 $wpdb->prepare(
261 "INSERT INTO $table_name
262 (created_on, post_id, time_stamp, json_data, post_content, rating, provider, provider_id, business, media_id, avatar_id, sizes, aspect_ratio, images_done, last_requested, lang)
263 VALUES (%s, %s, %s, %s, %s, %d, %s, %s, %s, %s, %s, %s, %s, %d, %s, %s)
264 ON DUPLICATE KEY UPDATE
265 json_data = %s,
266 post_content = %s,
267 rating = %d,
268 last_requested = %s",
269 // INSERT values
270 $now,
271 $post_id,
272 $time_stamp,
273 $json_data,
274 $post_content,
275 $rating,
276 $provider,
277 $provider_id,
278 $business,
279 $this->storage_data['media_id'],
280 $this->storage_data['avatar_id'] ?? '',
281 $this->storage_data['sizes'],
282 $this->storage_data['aspect_ratio'],
283 $this->storage_data['images_done'],
284 $now,
285 $this->lang,
286 // UPDATE values (repeated for ON DUPLICATE KEY UPDATE)
287 $json_data,
288 $post_content,
289 $rating,
290 $now
291 )
292 );
293 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
294 }
295
296 public function update_single($strict_update = false)
297 {
298 $rating = $this->post_data['rating'];
299 if (Util::is_facebook_collection_post($this->post_data)) {
300 $rating = $this->post_data['rating'] === 'positive' ? 5 : 1;
301 }
302
303 $to_store = array(
304 array('post_id', $this->post_data['review_id'], '%s'),
305 array('time_stamp', date('Y-m-d H:i:s', $this->post_data['time']), '%s'),
306 array('json_data', $this->should_encrypt($this->post_data, wp_json_encode($this->post_data)), '%s'),
307 array('post_content', $this->should_encrypt($this->post_data, $this->post_data['text']), '%s'),
308 array('rating', $rating, '%d'),
309 array('provider', $this->post_data['provider']['name'], '%s'),
310 array('provider_id', $this->get_provider_id(), '%s'),
311 array('business', $this->post_data['business']['id'] ?? '', '%s'),
312 array('media_id', $this->storage_data['media_id'], '%s'),
313 array('sizes', $this->storage_data['sizes'], '%s'),
314 array('aspect_ratio', $this->storage_data['aspect_ratio'], '%s'),
315 array('images_done', $this->storage_data['images_done'], '%d'),
316 array('last_requested', date('Y-m-d H:i:s'), '%s'),
317 );
318 $data = array();
319 $format = array();
320 foreach ($to_store as $single_store) {
321 $data[$single_store[0]] = $single_store[1];
322 $format[] = $single_store[2];
323 }
324
325 global $wpdb;
326 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
327 $where = array();
328 $where_format = array();
329
330 $where['post_id'] = $this->post_data['review_id'];
331 $where_format[] = '%s';
332
333 // Scope by language: db_record() (the existence gate that precedes this update)
334 // keys on (post_id, lang, provider_id), but this UPDATE matched post_id alone —
335 // so a single-language update overwrote every sibling-language row for the same
336 // review, collapsing them onto the last-written text. Only 'google' carries a
337 // non-'' lang, so other providers keep lang='' here and are unaffected (SMASH-1631).
338 $where['lang'] = $this->lang;
339 $where_format[] = '%s';
340
341 if ($strict_update) {
342 $where['provider_id'] = $this->get_provider_id();
343 $where_format[] = '%s';
344 }
345
346 $error = $wpdb->update($table_name, $data, $where, $format, $where_format);
347
348 if ($error !== false) {
349 $insert_id = $wpdb->insert_id;
350 } else {
351 // log error
352 }
353 }
354
355 public function update($to_update)
356 {
357 $data = array();
358 $format = array();
359 foreach ($to_update as $single_update) {
360 $data[ $single_update[0] ] = $single_update[1];
361 $format[] = $single_update[2];
362 }
363
364 global $wpdb;
365 $where = array();
366 $where_format = array();
367
368 $where['post_id'] = $this->post_data['review_id'];
369 $where_format[] = '%s';
370 // Scope by language, same reason as update_single(): without it a post_id-only
371 // UPDATE overwrites every sibling-language row for the review. Callers set
372 // $this->lang to the row's own language (or leave the '' default for non-lang
373 // providers), so this targets exactly the intended row (SMASH-1631).
374 $where['lang'] = $this->lang;
375 $where_format[] = '%s';
376 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
377 $error = $wpdb->update($table_name, $data, $where, $format, $where_format);
378
379 if ($error !== false) {
380 $insert_id = $wpdb->insert_id;
381 } else {
382 // log error
383 }
384 }
385
386 public static function delete_resizing_table_and_images()
387 {
388 $upload = wp_upload_dir();
389
390 global $wpdb;
391
392 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
393
394 self::delete_local_images();
395
396 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is prefixed constant
397 $wpdb->query("DROP TABLE IF EXISTS $table_name");
398 }
399
400 public static function create_resizing_table_and_uploads_folder()
401 {
402 $upload = wp_upload_dir();
403 $upload_dir = $upload['basedir'];
404 $upload_dir = trailingslashit($upload_dir) . self::UPLOAD_FOLDER_NAME;
405 if (! file_exists($upload_dir)) {
406 $created = wp_mkdir_p($upload_dir);
407 }
408
409 global $wpdb;
410 $table_name = esc_sql($wpdb->prefix . self::POSTS_TABLE_NAME);
411 $max_index_length = 191;
412 $charset_collate = '';
413 if (method_exists($wpdb, 'get_charset_collate')) { // get_charset_collate introduced in WP 3.5
414 $charset_collate = $wpdb->get_charset_collate();
415 }
416
417 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is prefixed constant
418 if ($wpdb->get_var("show tables like '$table_name'") !== $table_name) {
419 $sql = 'CREATE TABLE ' . $table_name . " (
420 id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
421 created_on DATETIME,
422 post_id VARCHAR(1000) DEFAULT '' NOT NULL,
423 time_stamp DATETIME,
424 json_data LONGTEXT DEFAULT '' NOT NULL,
425 post_content LONGTEXT DEFAULT '' NOT NULL,
426 rating INT(1) UNSIGNED NOT NULL,
427 provider VARCHAR(1000) DEFAULT '' NOT NULL,
428 provider_id VARCHAR(1000) DEFAULT '' NOT NULL,
429 business VARCHAR(1000) DEFAULT '' NOT NULL,
430 media_id VARCHAR(1000) DEFAULT '' NOT NULL,
431 sizes VARCHAR(1000) DEFAULT '' NOT NULL,
432 aspect_ratio DECIMAL (4,2) DEFAULT 0 NOT NULL,
433 avatar_id VARCHAR(1000) DEFAULT '' NOT NULL,
434 images_done TINYINT(1) DEFAULT 0 NOT NULL,
435 last_requested DATE,
436 lang VARCHAR(1000) DEFAULT '' NOT NULL,
437 INDEX provider (provider($max_index_length)),
438 INDEX business (business($max_index_length)),
439 INDEX provider_business (provider(10), business(15)),
440 INDEX provider_lang (provider(140),lang(51))
441 ) $charset_collate;";
442 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is a CREATE TABLE statement with prefixed table name
443 $wpdb->query($sql);
444 }
445 $error = $wpdb->last_error;
446 $query = $wpdb->last_query;
447 }
448
449 public static function delete_least_used_image()
450 {
451 }
452
453 public function max_total_records_reached()
454 {
455 }
456
457 public function media_supplied()
458 {
459 return $this->post_data['provider']['name'] !== 'yelp' && $this->post_data['provider']['name'] !== 'tripadvisor';
460 }
461
462
463 public function should_encrypt($post, $element)
464 {
465 return $post['provider']['name'] === 'facebook' && $element !== null ? $this->encryption->maybe_encrypt($element) : $element;
466 }
467
468
469 /**
470 * Used to update or insert Single Post Data
471 *
472 *
473 * @return void
474 *
475 * @since 1.6
476 */
477 public function update_or_insert($strict_update = false)
478 {
479 if ($this->db_record_exists()) {
480 $this->update_single($strict_update);
481 } else {
482 $this->store();
483 }
484 }
485
486 public static function delete_local_images()
487 {
488 $upload = wp_upload_dir();
489 $upload_dir = $upload['basedir'];
490 $upload_dir = trailingslashit($upload_dir) . self::UPLOAD_FOLDER_NAME;
491 $image_files = glob(trailingslashit($upload_dir) . '*');
492 if (!empty($image_files)) {
493 foreach ($image_files as $file) {
494 if (is_file($file)) {
495 unlink($file);
496 }
497 }
498 }
499 }
500 }
501