| @@ -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,35 +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§ion=archive-ticket-list' | |
| 1295 | + : 'wpsc-tickets§ion=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 | - $view = (int) $view; | |
| 1137 | - if ( $view === 0 ) { | |
| 1138 | - $url = admin_url( 'admin.php?page=wpsc-tickets§ion=ticket-list&id=' . $ticket_id ); | |
| 1139 | - } elseif ( $page_settings['ticket-url-page'] == 'support-page' && $page_settings['support-page'] ) { | |
| 1140 | - $url = get_permalink( $page_settings['support-page'] ); | |
| 1141 | - $url = add_query_arg( | |
| 1142 | - array( | |
| 1143 | - 'wpsc-section' => 'ticket-list', | |
| 1144 | - 'ticket-id' => $ticket_id, | |
| 1145 | - ), | |
| 1146 | - $url | |
| 1147 | - ); | |
| 1148 | - } elseif ( $page_settings['ticket-url-page'] == 'open-ticket-page' && $page_settings['open-ticket-page'] ) { | |
| 1149 | - $url = get_permalink( $page_settings['open-ticket-page'] ); | |
| 1150 | - $ticket = new WPSC_Ticket( $ticket_id ); | |
| 1151 | - $url = add_query_arg( | |
| 1152 | - array( | |
| 1153 | - 'ticket-id' => $ticket_id, | |
| 1154 | - 'auth-code' => $ticket->auth_code, | |
| 1155 | - ), | |
| 1156 | - $url | |
| 1157 | - ); | |
| 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 ); | |
| 1158 | 1460 | } |
| 1159 | 1461 | |
| 1160 | - 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; | |
| 1161 | 1518 | } |
| 1162 | 1519 | } |
| 1163 | 1520 | endif; |
| 1164 | 1521 | |