PluginProbe ʕ •ᴥ•ʔ
Smash Balloon Social Post Feed – Simple Social Feeds for WordPress / 4.10.0
Smash Balloon Social Post Feed – Simple Social Feeds for WordPress v4.10.0
4.12.0 4.10.0 4.9.0 4.8.1 trunk 1.0 1.1 1.12.1 1.2.3 1.2.4 1.2.5 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.5 1.5.1 1.5.2 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.4.1 1.6.5 1.6.5.1 1.6.6 1.6.6.1 1.6.6.2 1.6.6.3 1.6.7 1.6.7.1 1.6.8 1.6.8.1 1.6.8.2 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.9.0 1.9.1 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.8.1 1.9.9 1.9.9.1 1.9.9.2 1.9.9.3 2.0 2.0.1 2.1 2.1.1 2.1.2 2.1.3 2.10 2.11 2.11.1 2.12 2.12.1 2.12.2 2.12.3 2.12.4 2.13 2.14 2.14.1 2.15 2.15.1 2.16 2.16.1 2.17 2.17.1 2.18 2.18.1 2.18.2 2.18.3 2.19 2.19.1 2.19.2 2.19.3 2.2 2.2.1 2.3 2.3.1 2.3.10 2.3.2 2.3.3 2.3.4 2.3.6 2.3.7 2.3.8 2.3.9 2.4 2.4.1 2.4.1.1 2.4.1.2 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5 2.5.1 2.5.2 2.6 2.6.1 2.6.2 2.6.3 2.6.4 2.7 2.7.1 2.7.2 2.8 2.9 2.9.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.1 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.7.5 4.7.6 4.7.7
custom-facebook-feed / inc / UsageTracking / Facebook / FacebookFreeReporter.php
custom-facebook-feed / inc / UsageTracking / Facebook Last commit date
FacebookFreeReporter.php 3 weeks ago
FacebookFreeReporter.php
874 lines
1 <?php
2 /**
3 * Facebook reporter for Smash Usage Tracking — Free plugin variant.
4 *
5 * Identical to FacebookReporter (Pro) except license methods always return
6 * free/null values since the Free plugin has no EDD license infrastructure.
7 *
8 * @package CustomFacebookFeed\UsageTracking\Facebook
9 * @since 4.0
10 */
11
12 namespace CustomFacebookFeed\UsageTracking\Facebook;
13
14 use CustomFacebookFeed\UsageTracking\ReporterInterface;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 class FacebookFreeReporter implements ReporterInterface {
21
22 const SCHEMA_VERSION = '1.0';
23
24 /**
25 * Plugin slug for payload root.
26 *
27 * @return string
28 */
29 public function get_plugin_slug() {
30 return 'facebook';
31 }
32
33 /**
34 * Schema version for the report payload.
35 *
36 * @return string
37 */
38 public function get_schema_version() {
39 return self::SCHEMA_VERSION;
40 }
41
42 /**
43 * Configuration snapshot (environment, settings, sources, extensions, feeds).
44 *
45 * @return array
46 */
47 public function get_configuration_snapshot() {
48 $global_settings = $this->get_global_settings();
49 $extensions = $this->get_extensions();
50 $oembed = $this->get_oembed_config();
51
52 // Single DB scan — reused for latest sample, summary, and features map.
53 $all_feed_data = $this->get_all_feed_data();
54
55 return array(
56 'environment' => $this->get_environment(),
57 'global_settings' => $global_settings,
58 'sources' => $this->get_sources_summary(),
59 'extensions' => $extensions,
60 'oembed' => $oembed,
61 'latest_10_feeds' => $this->get_latest_feeds( $all_feed_data ),
62 'feeds' => $this->get_feeds_summary( $all_feed_data ),
63 'features_enabled' => $this->get_features_enabled( $all_feed_data, $global_settings, $extensions, $oembed ),
64 'extension_usage' => $this->get_extension_usage( $extensions, $all_feed_data ),
65 'version' => defined( 'CFFVER' ) ? CFFVER : '',
66 'license_tier' => $this->get_license_tier(),
67 'license_status' => $this->get_license_status(),
68 'license_expires' => $this->get_license_expires(),
69 'license_item_id' => $this->get_license_item_id(),
70 );
71 }
72
73 /**
74 * Dynamic metrics for the given period.
75 *
76 * @param string|int $period_start Start of period (ISO 8601 or timestamp).
77 * @param string|int $period_end End of period (ISO 8601 or timestamp).
78 * @return array
79 */
80 public function get_dynamic_metrics( $period_start, $period_end ) {
81 $ts_start = is_numeric( $period_start ) ? (int) $period_start : strtotime( $period_start );
82 $ts_end = is_numeric( $period_end ) ? (int) $period_end : strtotime( $period_end );
83
84 return array(
85 'period_start' => $period_start,
86 'period_end' => $period_end,
87 'performance' => $this->get_performance_metrics(),
88 'errors' => $this->get_error_metrics(),
89 'events' => $this->get_events_for_period( $ts_start, $ts_end ),
90 'days_active' => $this->get_days_active( $period_start, $period_end ),
91 'session_duration' => $this->get_session_duration(),
92 );
93 }
94
95 /**
96 * Environment data (WP, PHP, theme, locale, multisite, install age).
97 *
98 * @return array
99 */
100 private function get_environment() {
101 $install_ts = null;
102 $statuses = get_option( 'cff_statuses', array() );
103 if ( ! empty( $statuses['first_install'] ) && is_numeric( $statuses['first_install'] ) ) {
104 $install_ts = (int) $statuses['first_install'];
105 }
106 if ( null === $install_ts ) {
107 $install_ts = get_option( 'cff_installed_timestamp', 0 );
108 }
109 $install_age_days = $install_ts ? max( 0, (int) ((time() - $install_ts) / DAY_IN_SECONDS) ) : 0;
110
111 $theme = wp_get_theme();
112 $theme_name = $theme->exists() ? $theme->get( 'Name' ) : '';
113
114 return array(
115 'wp_version' => get_bloginfo( 'version' ),
116 'php_version' => PHP_VERSION,
117 'active_theme' => $theme_name,
118 'locale' => get_locale(),
119 'multisite' => is_multisite(),
120 'site_count' => is_multisite() ? (int) get_blog_count() : 1,
121 'active_plugins_count' => count(
122 array_unique(
123 array_merge(
124 (array) get_option( 'active_plugins', array() ),
125 array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) )
126 )
127 )
128 ),
129 'install_age_days' => $install_age_days,
130 );
131 }
132
133 /**
134 * Global CFF settings (caching, cron, gdpr, preserve + advanced tab).
135 *
136 * @return array
137 */
138 private function get_global_settings() {
139 $style = get_option( 'cff_style_settings', array() );
140 $ajax = get_option( 'cff_ajax', '' );
141
142 return array(
143 'caching_type' => get_option( 'cff_caching_type', 'background' ),
144 'cron_interval' => get_option( 'cff_cache_cron_interval', '12hours' ),
145 'gdpr' => isset( $style['gdpr'] ) ? $style['gdpr'] : 'auto',
146 'preserve_settings' => (bool) get_option( 'cff_preserve_settings', false ),
147 'optimize_images' => empty( $style['cff_disable_resize'] ),
148 'ajax_theme_loading_fix' => (bool) $ajax,
149 'show_credit' => ! empty( $style['cff_show_credit'] ),
150 'fix_text_shortening' => ! empty( $style['cff_format_issue'] ),
151 'js_image_loading' => ! empty( $style['enable_js_image_loading'] ),
152 'disable_admin_notice' => ! empty( $style['disable_admin_notice'] ),
153 'enable_email_report' => ! empty( $style['enable_email_report'] ) && 'off' !== $style['enable_email_report'],
154 'email_notification_day' => isset( $style['email_notification'] ) ? $style['email_notification'] : '',
155 'email_notification_set' => ! empty( $style['email_notification_addresses'] ),
156 );
157 }
158
159 /**
160 * Sources summary (connected accounts count, page vs group).
161 *
162 * @return array
163 */
164 private function get_sources_summary() {
165 global $wpdb;
166 $sources_table = $wpdb->prefix . 'cff_sources';
167 $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $sources_table ) ) === $sources_table;
168
169 $connected_accounts_count = 0;
170 $page_type = array(
171 'page' => 0,
172 'group' => 0,
173 );
174
175 if ( $table_exists ) {
176 $connected_accounts_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$sources_table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name derived from $wpdb->prefix, not user input.
177 $page_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$sources_table} WHERE account_type = %s", 'page' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name derived from $wpdb->prefix, not user input.
178 $group_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$sources_table} WHERE account_type = %s", 'group' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name derived from $wpdb->prefix, not user input.
179 $page_type = array(
180 'page' => $page_count,
181 'group' => $group_count,
182 );
183 }
184
185 return array(
186 'connected_accounts_count' => $connected_accounts_count,
187 'page_type' => $page_type,
188 );
189 }
190
191 /**
192 * Extension activation flags.
193 *
194 * @return array
195 */
196 private function get_extensions() {
197 return array(
198 'extensions' => $this->is_plugin_active( 'cff-extensions/cff-extensions.php' ),
199 'multifeed' => $this->is_plugin_active( 'cff-multifeed/cff-multifeed.php' ),
200 'reviews' => $this->is_plugin_active( 'cff-reviews/cff-reviews.php' ),
201 'carousel' => $this->is_plugin_active( 'cff-carousel/cff-carousel.php' ),
202 'date_range' => $this->is_plugin_active( 'cff-date-range/cff-date-range.php' ),
203 'featured_post' => $this->is_plugin_active( 'cff-featured-post/cff-featured-post.php' ),
204 'album' => $this->is_plugin_active( 'cff-album/cff-album.php' ),
205 );
206 }
207
208 /**
209 * Whether a plugin is active (works without loading wp-admin).
210 *
211 * @param string $plugin_basename Plugin basename e.g. 'cff-extensions/cff-extensions.php'.
212 * @return bool
213 */
214 private function is_plugin_active( $plugin_basename ) {
215 if ( function_exists( 'is_plugin_active' ) ) {
216 return (bool) \is_plugin_active( $plugin_basename );
217 }
218 $active = (array) get_option( 'active_plugins', array() );
219 if ( in_array( $plugin_basename, $active, true ) ) {
220 return true;
221 }
222 if ( is_multisite() ) {
223 $sitewide = (array) get_site_option( 'active_sitewide_plugins', array() );
224 return isset( $sitewide[ $plugin_basename ] );
225 }
226 return false;
227 }
228
229 /**
230 * oEmbed config (enabled, has_expiring_token).
231 *
232 * @return array
233 */
234 private function get_oembed_config() {
235 $token = get_option( 'cff_oembed_token', array() );
236 $enabled = ! empty( $token['access_token'] ) && empty( $token['disabled'] );
237 $expiration = isset( $token['expiration_date'] ) ? $token['expiration_date'] : '';
238 $has_expiring_token = $enabled && '' !== $expiration && 'never' !== strtolower( (string) $expiration );
239
240 return array(
241 'enabled' => $enabled,
242 'has_expiring_token' => $has_expiring_token,
243 );
244 }
245
246 /**
247 * Whitelist of feed setting keys to track.
248 *
249 * @var string[]
250 */
251 private static $feed_settings_whitelist = array(
252 'feedtype',
253 'type',
254 'feedlayout',
255 'feedtemplate',
256 'cols',
257 'colsmobile',
258 'num',
259 'loadmore',
260 'showheader',
261 'lightbox',
262 'disablelightbox',
263 'masonry',
264 'daterange',
265 'multifeedactive',
266 'carouselactive',
267 'layout',
268 'eventsource',
269 'albumsource',
270 'photosource',
271 'videosource',
272 'pastevents',
273 'cachetime',
274 'cacheunit',
275 'loadiframes',
276 'apipostlimit',
277 'timelinepag',
278 'gridpag',
279 'loadcommentsjs',
280 'salesposts',
281 'storytags',
282 'locale',
283 );
284
285 /**
286 * Load every feed's decoded settings plus feed_name, sorted newest-first.
287 *
288 * @return array[]
289 */
290 private function get_all_feed_data(): array {
291 global $wpdb;
292 $table = $wpdb->prefix . 'cff_feeds';
293 $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
294
295 if ( ! $table_exists ) {
296 return array();
297 }
298
299 $rows = $wpdb->get_results(
300 "SELECT feed_name, settings FROM {$table} ORDER BY last_modified DESC LIMIT 500", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name derived from $wpdb->prefix, not user input.
301 ARRAY_A
302 );
303
304 $out = array();
305 foreach ( $rows as $row ) {
306 $decoded = ! empty( $row['settings'] ) ? json_decode( $row['settings'], true ) : array();
307 $out[] = array(
308 'feed_name' => isset( $row['feed_name'] ) ? sanitize_text_field( (string) $row['feed_name'] ) : '',
309 'settings' => is_array( $decoded ) ? $decoded : array(),
310 );
311 }
312
313 return $out;
314 }
315
316 /**
317 * Latest 15 feeds with whitelisted settings.
318 *
319 * @param array[] $all_feed_data From get_all_feed_data().
320 * @return array
321 */
322 private function get_latest_feeds( array $all_feed_data ): array {
323 $feeds = array();
324 foreach ( array_slice( $all_feed_data, 0, 15 ) as $row ) {
325 $feed_name = $row['feed_name'];
326 if ( strlen( $feed_name ) > 255 ) {
327 $feed_name = substr( $feed_name, 0, 255 );
328 }
329 $feeds[] = array(
330 'feed_name' => $feed_name,
331 'settings' => $this->pick_whitelisted_settings( $row['settings'] ),
332 );
333 }
334 return $feeds;
335 }
336
337 /**
338 * Aggregate feed type and layout distribution across ALL feeds.
339 *
340 * @param array[] $all_feed_data From get_all_feed_data().
341 * @return array
342 */
343 private function get_feeds_summary( array $all_feed_data ): array {
344 $by_type = array();
345 $by_layout = array();
346
347 foreach ( $all_feed_data as $row ) {
348 $s = $row['settings'];
349 $type = isset( $s['feedtype'] ) ? (string) $s['feedtype'] : 'unknown';
350 $layout = isset( $s['feedlayout'] ) ? (string) $s['feedlayout'] : 'unknown';
351
352 $by_type[ $type ] = ( $by_type[ $type ] ?? 0 ) + 1;
353 $by_layout[ $layout ] = ( $by_layout[ $layout ] ?? 0 ) + 1;
354 }
355
356 return array(
357 'total_count' => count( $all_feed_data ),
358 'by_type' => $by_type,
359 'by_layout' => $by_layout,
360 );
361 }
362
363 /**
364 * Flat boolean feature map for the Laravel dashboard's feature adoption page.
365 *
366 * @param array[] $all_feed_data From get_all_feed_data().
367 * @param array $global_settings From get_global_settings().
368 * @param array $extensions From get_extensions().
369 * @param array $oembed From get_oembed_config().
370 * @return array<string,bool>
371 */
372 private function get_features_enabled( array $all_feed_data, array $global_settings, array $extensions, array $oembed ): array {
373 $feed_flags = array(
374 'lightbox' => false,
375 'load_more' => false,
376 'show_header' => false,
377 'masonry_layout' => false,
378 'date_range' => false,
379 'multifeed' => false,
380 'carousel' => false,
381 'event_source' => false,
382 'album_source' => false,
383 'photo_source' => false,
384 'video_source' => false,
385 'past_events' => false,
386 'load_iframes' => false,
387 'sales_posts' => false,
388 'story_tags' => false,
389 'custom_pagination' => false,
390 'load_comments' => false,
391 );
392
393 foreach ( $all_feed_data as $row ) {
394 $s = $row['settings'];
395
396 if ( ! $feed_flags['lightbox'] && ! empty( $s['lightbox'] ) && empty( $s['disablelightbox'] ) ) {
397 $feed_flags['lightbox'] = true;
398 }
399 if ( ! $feed_flags['load_more'] && ! empty( $s['loadmore'] ) ) {
400 $feed_flags['load_more'] = true;
401 }
402 if ( ! $feed_flags['show_header'] && ! empty( $s['showheader'] ) ) {
403 $feed_flags['show_header'] = true;
404 }
405 if ( ! $feed_flags['masonry_layout'] && isset( $s['feedlayout'] ) && 'masonry' === $s['feedlayout'] ) {
406 $feed_flags['masonry_layout'] = true;
407 }
408 if ( ! $feed_flags['date_range'] && ! empty( $s['daterange'] ) ) {
409 $feed_flags['date_range'] = true;
410 }
411 if ( ! $feed_flags['multifeed'] && ! empty( $s['multifeedactive'] ) ) {
412 $feed_flags['multifeed'] = true;
413 }
414 if ( ! $feed_flags['carousel'] && ! empty( $s['carouselactive'] ) ) {
415 $feed_flags['carousel'] = true;
416 }
417 if ( ! $feed_flags['event_source'] && ! empty( $s['eventsource'] ) ) {
418 $feed_flags['event_source'] = true;
419 }
420 if ( ! $feed_flags['album_source'] && ! empty( $s['albumsource'] ) ) {
421 $feed_flags['album_source'] = true;
422 }
423 if ( ! $feed_flags['photo_source'] && ! empty( $s['photosource'] ) ) {
424 $feed_flags['photo_source'] = true;
425 }
426 if ( ! $feed_flags['video_source'] && ! empty( $s['videosource'] ) ) {
427 $feed_flags['video_source'] = true;
428 }
429 if ( ! $feed_flags['past_events'] && ! empty( $s['pastevents'] ) ) {
430 $feed_flags['past_events'] = true;
431 }
432 if ( ! $feed_flags['load_iframes'] && ! empty( $s['loadiframes'] ) ) {
433 $feed_flags['load_iframes'] = true;
434 }
435 if ( ! $feed_flags['sales_posts'] && ! empty( $s['salesposts'] ) ) {
436 $feed_flags['sales_posts'] = true;
437 }
438 if ( ! $feed_flags['story_tags'] && ! empty( $s['storytags'] ) ) {
439 $feed_flags['story_tags'] = true;
440 }
441 if ( ! $feed_flags['custom_pagination'] && ( ! empty( $s['timelinepag'] ) || ! empty( $s['gridpag'] ) ) ) {
442 $feed_flags['custom_pagination'] = true;
443 }
444 if ( ! $feed_flags['load_comments'] && ! empty( $s['loadcommentsjs'] ) ) {
445 $feed_flags['load_comments'] = true;
446 }
447
448 if ( ! in_array( false, $feed_flags, true ) ) {
449 break;
450 }
451 }
452
453 return array_merge(
454 $feed_flags,
455 array(
456 'optimize_images' => (bool) ( $global_settings['optimize_images'] ?? true ),
457 'ajax_theme_fix' => (bool) ( $global_settings['ajax_theme_loading_fix'] ?? false ),
458 'gdpr_enabled' => isset( $global_settings['gdpr'] ) && 'auto' !== $global_settings['gdpr'],
459 'background_caching' => ( $global_settings['caching_type'] ?? '' ) === 'background',
460 'email_report' => (bool) ( $global_settings['enable_email_report'] ?? false ),
461 'show_credit' => (bool) ( $global_settings['show_credit'] ?? false ),
462 'oembed_enabled' => (bool) ( $oembed['enabled'] ?? false ),
463 'ext_multifeed' => (bool) ( $extensions['multifeed'] ?? false ),
464 'ext_reviews' => (bool) ( $extensions['reviews'] ?? false ),
465 'ext_carousel' => (bool) ( $extensions['carousel'] ?? false ),
466 'ext_date_range' => (bool) ( $extensions['date_range'] ?? false ),
467 'ext_featured_post' => (bool) ( $extensions['featured_post'] ?? false ),
468 'ext_album' => (bool) ( $extensions['album'] ?? false ),
469 )
470 );
471 }
472
473 /**
474 * Richer extension usage: active flag plus feeds_using count.
475 *
476 * @param array $extensions From get_extensions().
477 * @param array[] $all_feed_data From get_all_feed_data().
478 * @return array
479 */
480 private function get_extension_usage( array $extensions, array $all_feed_data ): array {
481 $ext_feed_key_map = array(
482 'multifeed' => 'multifeedactive',
483 'carousel' => 'carouselactive',
484 'date_range' => 'daterange',
485 );
486
487 $usage = array();
488
489 foreach ( $ext_feed_key_map as $ext => $feed_key ) {
490 $active = (bool) ( $extensions[ $ext ] ?? false );
491 $feeds_using = 0;
492 if ( $active ) {
493 foreach ( $all_feed_data as $row ) {
494 if ( ! empty( $row['settings'][ $feed_key ] ) ) {
495 ++$feeds_using;
496 }
497 }
498 }
499 $usage[ $ext ] = array(
500 'active' => $active,
501 'feeds_using' => $feeds_using,
502 );
503 }
504
505 foreach ( array( 'reviews', 'featured_post', 'album' ) as $ext ) {
506 $usage[ $ext ] = array( 'active' => (bool) ( $extensions[ $ext ] ?? false ) );
507 }
508
509 return $usage;
510 }
511
512 /**
513 * Return only whitelisted feed settings.
514 *
515 * @param array $settings Raw feed settings.
516 * @return array
517 */
518 private function pick_whitelisted_settings( array $settings ) {
519 $out = array();
520 foreach ( self::$feed_settings_whitelist as $key ) {
521 if ( ! array_key_exists( $key, $settings ) ) {
522 continue;
523 }
524 $value = $settings[ $key ];
525 if ( is_array( $value ) ) {
526 $out[ $key ] = $value;
527 } elseif ( is_scalar( $value ) ) {
528 $out[ $key ] = $value;
529 }
530 }
531 return $out;
532 }
533
534 // ── License methods ───────────────────────────────────────────────────────
535 // The Free plugin has no EDD license infrastructure. These return static
536 // values so the payload is always consistent and the dashboard can correctly
537 // segment free vs paid sites.
538
539 /** @return string */
540 private function get_license_tier() {
541 return 'free'; }
542
543 /** @return null */
544 private function get_license_status() {
545 return null; }
546
547 /** @return null */
548 private function get_license_expires() {
549 return null; }
550
551 /** @return null */
552 private function get_license_item_id() {
553 return null; }
554
555 // ── Metrics methods ───────────────────────────────────────────────────────
556
557 /**
558 * Performance metrics (feed caches count, cache requests count).
559 *
560 * @return array
561 */
562 private function get_performance_metrics() {
563 global $wpdb;
564 $cache_table = $wpdb->prefix . 'cff_feed_caches';
565 $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $cache_table ) ) === $cache_table;
566
567 $feed_caches_count = 0;
568 $cache_requests_count = 0;
569
570 if ( $table_exists ) {
571 $feed_caches_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$cache_table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name derived from $wpdb->prefix, not user input.
572 $cache_requests_count = (int) get_option( 'cff_smash_cache_requests_count', 0 );
573 }
574
575 return array(
576 'feed_caches_count' => $feed_caches_count,
577 'cache_requests_count' => $cache_requests_count,
578 );
579 }
580
581 /**
582 * Map a Facebook Graph API error code to a category.
583 *
584 * @param int|string $code
585 * @return string
586 */
587 private function categorise_error_code( $code ): string {
588 $code = (int) $code;
589
590 if ( in_array( $code, array( 102, 190, 458, 460, 461, 463, 467, 999 ), true ) ) {
591 return 'auth';
592 }
593 if ( in_array( $code, array( 4, 17, 32, 341, 613 ), true ) ) {
594 return 'rate_limit';
595 }
596 if ( 10 === $code || ( $code >= 200 && $code <= 299 ) ) {
597 return 'permission';
598 }
599 if ( in_array( $code, array( 100, 803 ), true ) ) {
600 return 'not_found';
601 }
602 if ( in_array( $code, array( 1, 2, 18 ), true ) ) {
603 return 'server';
604 }
605
606 return 'other';
607 }
608
609 /**
610 * Error metrics: categorised counts and latest 10 errors.
611 *
612 * @return array
613 */
614 private function get_error_metrics() {
615 $reporter = get_option( 'cff_error_reporter', array() );
616 $connection = isset( $reporter['connection'] ) && is_array( $reporter['connection'] ) ? $reporter['connection'] : array();
617 $accounts = isset( $reporter['accounts'] ) && is_array( $reporter['accounts'] ) ? $reporter['accounts'] : array();
618 $error_log = isset( $reporter['error_log'] ) && is_array( $reporter['error_log'] ) ? $reporter['error_log'] : array();
619 $revoked = isset( $reporter['revoked'] ) && is_array( $reporter['revoked'] ) ? $reporter['revoked'] : array();
620
621 $api_failures = count( $error_log ) > 0 ? count( $error_log ) : ( ! empty( $connection ) ? 1 : 0 );
622
623 $provider_errors = 0;
624 foreach ( $accounts as $account_errors ) {
625 if ( is_array( $account_errors ) ) {
626 $provider_errors += count( $account_errors );
627 }
628 }
629
630 $latest = $this->build_latest_errors_array( $reporter, $error_log, $connection, $accounts );
631
632 $by_type = array(
633 'auth' => 0,
634 'rate_limit' => 0,
635 'permission' => 0,
636 'not_found' => 0,
637 'server' => 0,
638 'network' => 0,
639 'other' => 0,
640 );
641 $critical_count = 0;
642 foreach ( $latest as $err ) {
643 $cat = $err['category'] ?? 'other';
644 if ( array_key_exists( $cat, $by_type ) ) {
645 ++$by_type[ $cat ];
646 } else {
647 ++$by_type['other'];
648 }
649 if ( ! empty( $err['critical'] ) ) {
650 ++$critical_count;
651 }
652 }
653
654 return array(
655 'api_failures' => $api_failures,
656 'provider_errors' => $provider_errors,
657 'by_type' => $by_type,
658 'critical_count' => $critical_count,
659 'revoked_accounts' => count( $revoked ),
660 'latest' => array_slice( $latest, 0, 10 ),
661 );
662 }
663
664 /**
665 * Build annotated error list (no tokens or PII).
666 */
667 private function build_latest_errors_array( $reporter, $error_log, $connection, $accounts ) {
668 $latest = array();
669
670 foreach ( array_slice( $error_log, -10 ) as $log_entry ) {
671 $str = is_string( $log_entry ) ? $log_entry : '';
672 $prefix = '';
673 $logged_at = '';
674
675 if ( preg_match( '/^(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+-/', $str, $m ) ) {
676 $logged_at = $m[1];
677 $prefix = $m[0];
678 }
679
680 $message = trim( substr( $str, strlen( $prefix ) ) );
681 $message = $this->sanitize_error_message( $message, 300 );
682 $code = null;
683 $category = 'other';
684
685 if ( preg_match( '/(?:api error|error)\s+(\d{1,4})/i', $message, $cm ) ) {
686 $code = (int) $cm[1];
687 $category = $this->categorise_error_code( $code );
688 }
689
690 $entry = array(
691 'type' => 'log',
692 'category' => $category,
693 'logged_at' => $logged_at,
694 'message' => $message,
695 );
696 if ( null !== $code ) {
697 $entry['error_code'] = $code;
698 }
699 $latest[] = $entry;
700 }
701
702 if ( ! empty( $connection ) && isset( $connection['error_id'] ) ) {
703 $error_id = $connection['error_id'];
704 $code = is_numeric( $error_id ) ? (int) $error_id : null;
705 $category = null !== $code ? $this->categorise_error_code( $code ) : 'network';
706 $latest[] = array(
707 'type' => 'connection',
708 'category' => $category,
709 'error_code' => $code,
710 'error_id' => $error_id,
711 'critical' => ! empty( $connection['critical'] ),
712 );
713 }
714
715 foreach ( $accounts as $account_id => $by_type ) {
716 if ( ! is_array( $by_type ) ) {
717 continue;
718 }
719 foreach ( $by_type as $error_type => $err ) {
720 if ( ! is_array( $err ) ) {
721 continue;
722 }
723 $code = isset( $err['error']['code'] ) ? (int) $err['error']['code'] : ( isset( $err['errorno'] ) ? (int) $err['errorno'] : null );
724 $category = 'accesstoken' === $error_type ? 'auth' : ( null !== $code ? $this->categorise_error_code( $code ) : 'auth' );
725 $item = array(
726 'type' => 'account',
727 'category' => $category,
728 'error_type' => $error_type,
729 'critical' => ! empty( $err['critical'] ),
730 );
731 if ( null !== $code ) {
732 $item['error_code'] = $code;
733 }
734 if ( isset( $err['errorno'] ) ) {
735 $item['errorno'] = $err['errorno'];
736 }
737 $latest[] = $item;
738 }
739 }
740
741 $system_keys = array( 'resizing', 'database_create', 'upload_dir', 'platform_data_deleted' );
742 foreach ( $system_keys as $key ) {
743 if ( empty( $reporter[ $key ] ) ) {
744 continue;
745 }
746 $msg = is_array( $reporter[ $key ] ) ? wp_json_encode( $reporter[ $key ] ) : (string) $reporter[ $key ];
747 $latest[] = array(
748 'type' => $key,
749 'category' => 'other',
750 'message' => $this->sanitize_error_message( $msg, 300 ),
751 );
752 }
753
754 return $latest;
755 }
756
757 /**
758 * Strip tokens and truncate error message.
759 */
760 private function sanitize_error_message( $message, $max_len = 300 ) {
761 // Redact known credential key=value patterns
762 $message = preg_replace(
763 '/\b(access_token|accesstoken|api_key|api_secret|client_id|client_secret|consumer_key|consumer_secret|secret_key|auth_token|refresh_token|private_key|token)\s*[=:]\s*["\']?[^\s&"\'\\\\,\]}\)]{4,}["\']?/i',
764 '$1=[REDACTED]',
765 $message
766 );
767 // Redact Bearer tokens
768 $message = preg_replace( '/\bBearer\s+[A-Za-z0-9\-._~+\/]+=*/i', 'Bearer [REDACTED]', $message );
769 if ( strlen( $message ) > $max_len ) {
770 $message = substr( $message, 0, $max_len ) . '...';
771 }
772 return $message;
773 }
774
775 /**
776 * Days active in the given period.
777 */
778 private function get_days_active( $period_start, $period_end ) {
779 $dates = get_option( \CustomFacebookFeed\UsageTracking\Config::OPTION_ACTIVE_DATES, array() );
780 if ( ! is_array( $dates ) || empty( $dates ) ) {
781 return 0;
782 }
783 $count = 0;
784 $start = strtotime( $period_start );
785 $end = strtotime( $period_end );
786 foreach ( $dates as $d ) {
787 $ts = strtotime( $d );
788 if ( false !== $ts && $ts >= $start && $ts <= $end ) {
789 ++$count;
790 }
791 }
792 return $count;
793 }
794
795 /**
796 * Average of last recorded session durations in seconds.
797 */
798 private function get_session_duration() {
799 $durations = get_option( \CustomFacebookFeed\UsageTracking\Config::OPTION_SESSION_DURATIONS, array() );
800 if ( ! is_array( $durations ) || empty( $durations ) ) {
801 return 0;
802 }
803 return (int) round( array_sum( $durations ) / count( $durations ) );
804 }
805
806 /**
807 * Event counts and last_date for each event.
808 */
809 private function get_events_for_period( $ts_start, $ts_end ) {
810 $events = get_option( 'cff_smash_usage_events', array() );
811 if ( ! is_array( $events ) ) {
812 return array();
813 }
814
815 $first = reset( $events );
816 $is_legacy_list = is_array( $first ) && ! isset( $first['count'] ) && (isset( $first['event'] ) || isset( $first['timestamp'] ) || isset( $first['time'] ));
817
818 if ( $is_legacy_list ) {
819 $out = array();
820 foreach ( $events as $entry ) {
821 if ( ! is_array( $entry ) ) {
822 continue;
823 }
824 $ts = isset( $entry['timestamp'] ) ? (int) $entry['timestamp'] : (isset( $entry['time'] ) ? (int) $entry['time'] : 0);
825 if ( $ts < $ts_start || $ts > $ts_end ) {
826 continue;
827 }
828 $name = isset( $entry['event'] ) ? $entry['event'] : (isset( $entry['name'] ) ? $entry['name'] : '');
829 if ( '' !== $name ) {
830 if ( ! isset( $out[ $name ] ) ) {
831 $out[ $name ] = array(
832 'count' => 0,
833 'last_date' => null,
834 );
835 }
836 ++$out[ $name ]['count'];
837 $date = $ts ? gmdate( 'Y-m-d', $ts ) : null;
838 if ( $date && ( null === $out[ $name ]['last_date'] || $date > $out[ $name ]['last_date'] ) ) {
839 $out[ $name ]['last_date'] = $date;
840 }
841 }
842 }
843 return $out;
844 }
845
846 $out = array();
847 foreach ( $events as $name => $value ) {
848 if ( ! is_string( $name ) || '' === $name ) {
849 continue;
850 }
851 if ( is_array( $value ) && isset( $value['count'] ) ) {
852 $last_date = isset( $value['last_date'] ) && is_string( $value['last_date'] ) ? $value['last_date'] : null;
853 $last_ts = $last_date ? strtotime( $last_date ) : false;
854 if ( false === $last_ts || $last_ts < $ts_start || $last_ts > $ts_end ) {
855 continue;
856 }
857 $out[ $name ] = array(
858 'count' => (int) $value['count'],
859 'last_date' => $last_date,
860 );
861 continue;
862 }
863 if ( is_numeric( $value ) ) {
864 $out[ $name ] = array(
865 'count' => (int) $value,
866 'last_date' => null,
867 );
868 }
869 }
870
871 return $out;
872 }
873 }
874