PluginProbe
SupportCandy – AI Customer Support Ticket System & Live Chatbot Agent / 3.5.4
SupportCandy – AI Customer Support Ticket System & Live Chatbot Agent v3.5.4
3.5.4 3.5.3 3.5.2 3.5.1 3.4.9 3.5.0 3.4.8 3.4.7 trunk 2.3.1 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6
← All changes | includes/class-wpsc-functions.php +381 -23 3.4.1 → 3.5.4 View file →
@@ -20,8 +20,12 @@
20 20 public static function init() {
21 21
22 22 // Load ref classes.
23 23 add_action( 'init', array( __CLASS__, 'load_ref_classes' ), 1 );
24 +
25 + // Refresh paid customer option when plugins state changes.
26 + add_action( 'activated_plugin', array( __CLASS__, 'update_paid_customer_status_option' ) );
27 + add_action( 'deactivated_plugin', array( __CLASS__, 'update_paid_customer_status_option' ), 99 );
24 28 }
25 29
26 30 /**
27 31 * Load ref classes
@@ -68,8 +72,23 @@
68 72 $object = WPSC_Customer::get_anonymous_customer();
69 73 }
70 74 break;
71 75
76 + case 'wpsc_ticket':
77 + if ( $value && isset( self::$ref_classes[ $ref_class ] ) ) {
78 + $class = self::$ref_classes[ $ref_class ]['class'];
79 + $object = new $class( $value );
80 + // A ticket referenced from another record (a todo, a thread, a timer log, ...)
81 + // may have since been archived - archived tickets are moved out of the live
82 + // tickets table entirely, so the lookup above resolves to an empty object. Fall
83 + // back to the archived-ticket table rather than silently returning a blank
84 + // ticket (blank id, blank subject, blank link) to every caller.
85 + if ( ! $object->id ) {
86 + $object = new WPSC_Archive_Ticket( $value );
87 + }
88 + }
89 + break;
90 +
72 91 default:
73 92 $object = apply_filters( 'wpsc_fun_get_object', $value, $ref_class );
74 93 if ( ! is_object( $object ) && isset( self::$ref_classes[ $ref_class ] ) ) {
75 94 $class = self::$ref_classes[ $ref_class ]['class'];
@@ -475,8 +494,127 @@
475 494 return $date->format( 'Y-m-d H:i:s' );
476 495 }
477 496
478 497 /**
498 + * Check whether given date is valid or not
499 + *
500 + * @param string $date - date string.
501 + * @param string $format - date format.
502 + * @return boolean
503 + */
504 + public static function is_valid_date( $date, $format = 'Y-m-d' ) {
505 + if ( preg_match( '/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}(:\d{2})?$/', $date ) ) {
506 + return self::is_valid_datetime( $date );
507 + }
508 + $d = DateTime::createFromFormat( $format, $date );
509 + return $d && $d->format( $format ) === $date;
510 + }
511 +
512 + /**
513 + * Check whether given datetime is valid or not
514 + *
515 + * @param string $datetime - datetime string.
516 + * @return boolean
517 + */
518 + public static function is_valid_datetime( $datetime ) {
519 +
520 + if ( ! is_string( $datetime ) ) {
521 + return false;
522 + }
523 + if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $datetime ) ) {
524 + return self::is_valid_date( $datetime );
525 + }
526 + $datetime = trim( $datetime );
527 + $formats = array(
528 + 'Y-m-d H:i',
529 + 'Y-m-d H:i:s',
530 + );
531 +
532 + foreach ( $formats as $format ) {
533 + $dt = DateTime::createFromFormat( $format, $datetime );
534 + if (
535 + $dt !== false &&
536 + $dt->format( $format ) === $datetime
537 + ) {
538 + return true;
539 + }
540 + }
541 + return false;
542 + }
543 +
544 + /**
545 + * Check whether given time is valid or not
546 + *
547 + * @param string $time - time string.
548 + * @return boolean
549 + */
550 + public static function is_valid_time( $time ) {
551 + if ( ! is_string( $time ) ) {
552 + return false;
553 + }
554 +
555 + // Accept HH:MM or HH:MM:SS (24-hour).
556 + return (bool) preg_match(
557 + '/^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/',
558 + $time
559 + );
560 + }
561 +
562 + /**
563 + * Normalize date string to UTC date string
564 + *
565 + * @param string $date - date string.
566 + * @param boolean $end_of_day - end of day flag.
567 + * @return string|false
568 + */
569 + public static function normalize_date( $date, $end_of_day = false ) {
570 + if ( ! is_string( $date ) ) {
571 + return false;
572 + }
573 +
574 + $date = trim( $date );
575 +
576 + $formats = array(
577 + 'Y-m-d H:i:s',
578 + 'Y-m-d',
579 + );
580 +
581 + foreach ( $formats as $format ) {
582 + $dt = DateTime::createFromFormat( $format, $date );
583 + if ( $dt instanceof DateTime ) {
584 + if ( $format === 'Y-m-d' ) {
585 + $dt->setTime(
586 + $end_of_day ? 23 : 0,
587 + $end_of_day ? 59 : 0,
588 + $end_of_day ? 59 : 0
589 + );
590 + }
591 + return self::get_utc_date_str( $dt->format( 'Y-m-d H:i:s' ) );
592 + }
593 + }
594 +
595 + return false;
596 + }
597 +
598 + /**
599 + * Return SQL BETWEEN string
600 + *
601 + * @param string $column - column name.
602 + * @param string $from - from value.
603 + * @param string $to - to value.
604 + * @return string
605 + */
606 + public static function sql_between( $column, $from, $to ) {
607 + return sprintf(
608 + "%s BETWEEN '%s' AND '%s'",
609 + $column,
610 + esc_sql( $from ),
611 + esc_sql( $to )
612 + );
613 + }
614 +
615 +
616 + /**
479 617 * Get new ticket url
480 618 *
481 619 * @return string
482 620 */
@@ -1130,34 +1268,254 @@
1130 1268 * @return string
1131 1269 */
1132 1270 public static function get_ticket_url( $ticket_id, $view ) {
1133 1271
1134 - $url = '';
1272 + $ticket_id = absint( $ticket_id );
1273 + $view = (int) $view;
1274 + $url = '';
1275 +
1276 + if ( ! $ticket_id ) {
1277 + return $url;
1278 + }
1279 +
1280 + $page_settings = get_option( 'wpsc-gs-page-settings', array() );
1281 +
1282 + // Detect ticket type.
1283 + $is_archive = false;
1284 + $ticket = new WPSC_Ticket( $ticket_id );
1285 + if ( ! $ticket->id ) {
1286 + $ticket = new WPSC_Archive_Ticket( $ticket_id );
1287 + if ( ! $ticket->id ) {
1288 + return $url;
1289 + }
1290 + $is_archive = true;
1291 + }
1292 +
1293 + $type = $is_archive
1294 + ? 'wpsc-archive-tickets&section=archive-ticket-list'
1295 + : 'wpsc-tickets&section=ticket-list';
1296 +
1297 + $admin_url = admin_url( 'admin.php?page=' . $type . '&id=' . $ticket_id );
1298 +
1299 + // Backend view always wins.
1300 + if ( $view === 0 || $is_archive ) {
1301 + return apply_filters( 'wpsc_get_ticket_url_by_view', $admin_url, $ticket_id, $view );
1302 + }
1303 +
1304 + // Frontend view (normal tickets only).
1305 + if ( $view === 1 ) {
1306 +
1307 + $ticket_url_page = $page_settings['ticket-url-page'] ?? '';
1308 + $support_page = absint( $page_settings['support-page'] ?? 0 );
1309 + $open_page = absint( $page_settings['open-ticket-page'] ?? 0 );
1310 +
1311 + if ( $ticket_url_page === 'support-page' && $support_page ) {
1312 +
1313 + $url = add_query_arg(
1314 + array(
1315 + 'wpsc-section' => 'ticket-list',
1316 + 'ticket-id' => $ticket_id,
1317 + ),
1318 + get_permalink( $support_page )
1319 + );
1320 +
1321 + } elseif ( $ticket_url_page === 'open-ticket-page' && $open_page && $ticket->auth_code ) {
1322 +
1323 + $url = add_query_arg(
1324 + array(
1325 + 'ticket-id' => $ticket_id,
1326 + 'auth-code' => $ticket->auth_code,
1327 + ),
1328 + get_permalink( $open_page )
1329 + );
1330 + }
1331 + }
1332 +
1333 + if ( ! $url ) {
1334 + $url = $admin_url;
1335 + }
1336 +
1337 + return apply_filters( 'wpsc_get_ticket_url_by_view', $url, $ticket_id, $view );
1338 + }
1339 +
1340 + /**
1341 + * Get unique, non-empty closed statuses from merged settings.
1342 + *
1343 + * Merges advanced and general settings, filters out empty values,
1344 + * ensures uniqueness, and returns an indexed array.
1345 + *
1346 + * @return array Unique, non-empty closed statuses.
1347 + */
1348 + public static function get_closed_statuses() {
1349 +
1350 + $tl_ms_advance_settings = get_option( 'wpsc-tl-ms-advanced' );
1351 + $general_settings = get_option( 'wpsc-gs-general' );
1352 +
1353 + // Merge, filter non-empty, get unique values, and convert all to string.
1354 + $closed_statuses = array_unique(
1355 + array_filter(
1356 + array_merge(
1357 + $tl_ms_advance_settings['closed-ticket-statuses'],
1358 + (array) $general_settings['close-ticket-status']
1359 + ),
1360 + function ( $statuses ) {
1361 + return ! empty( $statuses );
1362 + }
1363 + )
1364 + );
1365 + return array_map( 'strval', $closed_statuses ); // Ensure indexed array.
1366 + }
1367 +
1368 + /**
1369 + * Check whether any SupportCandy addon is active.
1370 + *
1371 + * @return bool
1372 + */
1373 + public static function is_paid_customer() {
1374 +
1375 + $cache_key = 'wpsc_is_paid_customer';
1376 + $cached_status = get_option( $cache_key, null );
1377 +
1378 + if ( null !== $cached_status ) {
1379 + return (bool) $cached_status;
1380 + }
1381 +
1382 + return self::update_paid_customer_status_option();
1383 + }
1384 +
1385 + /**
1386 + * Update the paid customer status option based on active addons.
1387 + *
1388 + * @param string $plugin - Optional plugin path to exclude from check (used during activation/deactivation).
1389 + * @return bool Updated paid customer status.
1390 + */
1391 + public static function update_paid_customer_status_option( $plugin = '' ) {
1392 + $cache_key = 'wpsc_is_paid_customer';
1393 +
1394 + $addon_plugins = array(
1395 + 'wpsc-agentgroup/wpsc-agentgroup.php',
1396 + 'wpsc-assign-agent-rules/wpsc-assign-agent-rules.php',
1397 + 'wpsc-automatic-close-ticket/wpsc-automatic-close-ticket.php',
1398 + 'wpsc-canned-reply/wpsc-canned-reply.php',
1399 + 'wpsc-edd/wpsc-edd.php',
1400 + 'wpsc-email-marketing-tools/wpsc-email-marketing-tools-integration.php',
1401 + 'wpsc-email-piping/wpsc-email-piping.php',
1402 + 'wpsc-export-ticket/wpsc-export-ticket.php',
1403 + 'wpsc-gravity-forms/wpsc-gravity-form-integration.php',
1404 + 'wpsc-lms/wpsc-lms.php',
1405 + 'wpsc-pressapps-knowledge-base/wpsc-pressapps-knowledge-base.php',
1406 + 'wpsc-print-ticket/wpsc-print-ticket.php',
1407 + 'wpsc-private-credentials/wpsc_private_credentials.php',
1408 + 'wpsc-productivity-suite/wpsc-productivity-suite.php',
1409 + 'wpsc-reports/wpsc-reports.php',
1410 + 'wpsc-satisfaction-survey/wpsc-satisfaction-survey.php',
1411 + 'wpsc-schedule-tickets/wpsc-schedule-tickets.php',
1412 + 'wpsc-sla/wpsc-sla.php',
1413 + 'wpsc-slack/wpsc-slack.php',
1414 + 'wpsc-timer/wpsc-timer.php',
1415 + 'wpsc-ultimate-faq/wpsc-ultimate-faq.php',
1416 + 'wpsc-usergroup/wpsc-usergroup.php',
1417 + 'wpsc-webhooks/wpsc-webhooks.php',
1418 + 'wpsc-woocommerce/wpsc-woocommerce.php',
1419 + 'wpsc-workflows/wpsc-workflows.php',
1420 + );
1421 +
1422 + $active_plugins = (array) get_option( 'active_plugins', array() );
1423 +
1424 + if ( current_filter() === 'deactivated_plugin' && ! empty( $plugin ) ) {
1425 + $active_plugins = array_diff( $active_plugins, array( $plugin ) );
1426 + }
1427 +
1428 + if ( is_multisite() ) {
1429 + $active_network_plugins = (array) get_site_option( 'active_sitewide_plugins', array() );
1430 + $active_plugins = array_merge( $active_plugins, array_keys( $active_network_plugins ) );
1431 + }
1432 +
1433 + $is_paid_customer = ! empty( array_intersect( $addon_plugins, $active_plugins ) );
1434 + update_option( $cache_key, $is_paid_customer ? 1 : 0 );
1435 + return (bool) $is_paid_customer;
1436 + }
1437 +
1438 + /**
1439 + * Whether SupportCandy front-end scripts/styles should load on the current page,
1440 + * based on Settings -> General -> Page settings -> Load scripts.
1441 + *
1442 + * Translation aware: if the current page is a WPML or Polylang translation of a
1443 + * page selected in the 'Custom' list, scripts still load - the settings screen can
1444 + * only ever store the ID of the page in the language active while saving.
1445 + *
1446 + * @return bool
1447 + */
1448 + public static function is_load_scripts_page() {
1449 +
1135 1450 $page_settings = get_option( 'wpsc-gs-page-settings' );
1136 - if ( $view === '0' ) {
1137 - $url = admin_url( 'admin.php?page=wpsc-tickets&section=ticket-list&id=' . $ticket_id );
1138 - } elseif ( $page_settings['ticket-url-page'] == 'support-page' && $page_settings['support-page'] ) {
1139 - $url = get_permalink( $page_settings['support-page'] );
1140 - $url = add_query_arg(
1141 - array(
1142 - 'wpsc-section' => 'ticket-list',
1143 - 'ticket-id' => $ticket_id,
1144 - ),
1145 - $url
1146 - );
1147 - } elseif ( $page_settings['ticket-url-page'] == 'open-ticket-page' && $page_settings['open-ticket-page'] ) {
1148 - $url = get_permalink( $page_settings['open-ticket-page'] );
1149 - $ticket = new WPSC_Ticket( $ticket_id );
1150 - $url = add_query_arg(
1151 - array(
1152 - 'ticket-id' => $ticket_id,
1153 - 'auth-code' => $ticket->auth_code,
1154 - ),
1155 - $url
1156 - );
1451 +
1452 + $load = true;
1453 +
1454 + if ( isset( $page_settings['load-scripts'] ) && $page_settings['load-scripts'] === 'custom' ) {
1455 +
1456 + $current_id = get_the_id();
1457 + $pages = isset( $page_settings['load-script-pages'] ) ? (array) $page_settings['load-script-pages'] : array();
1458 +
1459 + $load = $current_id && self::page_id_in_pages_or_translations( $current_id, $pages );
1157 1460 }
1158 1461
1159 - return apply_filters( 'wpsc_get_ticket_url_by_view', $url, $ticket_id, $view );
1462 + return (bool) apply_filters( 'wpsc_load_frontend_scripts', $load, $page_settings );
1463 + }
1464 +
1465 + /**
1466 + * Check if given post id matches (directly or via translation) any of the given page ids.
1467 + *
1468 + * @param int $current_id current post id.
1469 + * @param array $page_ids selected page ids from settings.
1470 + * @return bool
1471 + */
1472 + private static function page_id_in_pages_or_translations( $current_id, $page_ids ) {
1473 +
1474 + if ( in_array( $current_id, $page_ids ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1475 + return true;
1476 + }
1477 +
1478 + foreach ( $page_ids as $page_id ) {
1479 +
1480 + // WPML.
1481 + if ( has_filter( 'wpml_object_id' ) ) {
1482 + $translated_id = apply_filters( 'wpml_object_id', $page_id, 'page', false );
1483 + if ( $translated_id && (int) $translated_id === (int) $current_id ) {
1484 + return true;
1485 + }
1486 + }
1487 +
1488 + // Polylang.
1489 + if ( function_exists( 'pll_get_post_translations' ) ) {
1490 + $translations = pll_get_post_translations( $page_id );
1491 + if ( in_array( $current_id, $translations ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1492 + return true;
1493 + }
1494 + }
1495 + }
1496 +
1497 + return false;
1498 + }
1499 +
1500 + /**
1501 + * Get current active language code, if a multilingual plugin is active.
1502 + *
1503 + * @return string|null
1504 + */
1505 + public static function get_current_language_code() {
1506 +
1507 + if ( has_filter( 'wpml_current_language' ) ) {
1508 + $lang = apply_filters( 'wpml_current_language', null );
1509 + return $lang ? $lang : null;
1510 + }
1511 +
1512 + if ( function_exists( 'pll_current_language' ) ) {
1513 + $lang = pll_current_language();
1514 + return $lang ? $lang : null;
1515 + }
1516 +
1517 + return null;
1160 1518 }
1161 1519 }
1162 1520 endif;
1163 1521