Changeset 3420599
- Timestamp:
- 12/16/2025 02:41:23 AM (5 weeks ago)
- Location:
- pcrecruiter-extensions/trunk
- Files:
-
- 29 added
- 6 edited
-
PCRecruiter-Extensions.php (modified) (9 diffs)
-
assets/banner-1544x500.png (added)
-
assets/banner-772x250.png (added)
-
assets/css (added)
-
assets/css/pcr-block-bindings-editor.css (added)
-
assets/css/pcr-deactivation-modal.css (added)
-
assets/css/pcr-job-board.css (added)
-
assets/css/pcr-schema-mapper.css (added)
-
assets/css/pcr-settings.css (added)
-
assets/icon-128x128.png (modified) (previous)
-
assets/icon-256x256.png (modified) (previous)
-
assets/icon.svg (modified) (1 diff)
-
assets/js/pcr-admin.js (modified) (1 diff)
-
assets/js/pcr-block-bindings-editor.js (added)
-
assets/js/pcr-deactivation-handler.js (added)
-
assets/js/pcr-frontend.js (added)
-
assets/js/pcr-schema-mapper.js (added)
-
assets/svg (added)
-
assets/svg/bsky.svg (added)
-
assets/svg/envelope.svg (added)
-
assets/svg/facebook.svg (added)
-
assets/svg/link.svg (added)
-
assets/svg/linkedin.svg (added)
-
assets/svg/x_logo.svg (added)
-
includes (added)
-
includes/class-block-bindings.php (added)
-
includes/class-deactivation-handler.php (added)
-
includes/class-job-manager.php (added)
-
includes/class-schema-frontend.php (added)
-
includes/class-schema-mapper.php (added)
-
includes/class-seo-enhancements.php (added)
-
includes/class-sitemap-integration.php (added)
-
includes/class-social-widget.php (added)
-
phpcs.xml (added)
-
readme.txt (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
pcrecruiter-extensions/trunk/PCRecruiter-Extensions.php
r3400600 r3420599 1 1 <?php 2 3 /** 4 * Plugin Name: PCRecruiter Extensions 5 * Plugin URI: https://www.pcrecruiter.net 6 * Description: Embeds PCRecruiter forms and iframe content via shortcodes, facilitates RSS/XML Job Feeds, syncs job board API content. 7 * Version: 2.0 8 * Author: Main Sequence Technology, Inc. 9 * Author URI: https://www.pcrecruiter.net 10 * License: GPL3 11 * Text Domain: pcrecruiter-extensions 12 * 13 * @package PCRecruiter_Extensions 14 * phpcs:set WordPress.NamingConventions.PrefixAllGlobals prefixes[] pcrecruiter 15 * phpcs:set WordPress.WP.I18n text_domain[] pcrecruiter-extensions 16 */ 17 18 // Public endpoint for PCRecruiter custom forms & job sync. 19 // Form is submitted from an external domain - WordPress nonce cannot be added. 20 // All inputs are sanitized with sanitize_text_field() / sanitize_textarea_field(). 21 // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended 22 23 function pcrecruiter_maybe_migrate_v1_settings() 24 { 25 // Check if old options exist and new ones don't 26 $old_options = get_option('pcr_feed_options'); 27 $new_options = get_option('pcrecruiter_feed_options'); 28 29 if ($old_options && !$new_options) { 30 // Migrate settings 31 update_option('pcrecruiter_feed_options', $old_options); 32 33 // Clear old cron and set up new one if activated 34 wp_clear_scheduled_hook('pcr_feed'); 35 if (!empty($old_options['activation'])) { 36 $frequency = $old_options['frequency'] ?? 'daily'; 37 wp_schedule_event(time(), $frequency, 'pcrecruiter_feed'); 38 } 39 } 40 } 41 add_action('plugins_loaded', 'pcrecruiter_maybe_migrate_v1_settings', 5); 42 43 function pcrecruiter_define_version_constant() 44 { 45 if (defined('PCRECRUITER_EXTENSIONS_VERSION')) { 46 return; 47 } 48 49 if (!function_exists('get_plugin_data')) { 50 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 51 } 52 53 $plugin_data = get_plugin_data(__FILE__); 54 $version = $plugin_data['Version'] ?? '2.0'; 55 56 define('PCRECRUITER_EXTENSIONS_VERSION', $version); 57 } 58 59 function pcrecruiter_get_plugin_version() 60 { 61 pcrecruiter_define_version_constant(); 62 return PCRECRUITER_EXTENSIONS_VERSION; 63 } 64 65 add_action('init', 'pcrecruiter_define_version_constant', 1); 66 2 67 /* 3 Plugin Name: PCRecruiter Extensions 4 Plugin URI: https://www.pcrecruiter.net 5 Description: Embeds PCRecruiter forms and iframe content via shortcodes, and facilitiates RSS/XML Job Feeds. 6 Version: 1.4.37 7 Author: Main Sequence Technology, Inc. 8 Author URI: https://www.pcrecruiter.net 9 License: GPLv2 or later 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html 68 JOB MANAGER 11 69 */ 70 function pcrecruiter_load_plugin() 71 { 72 require_once plugin_dir_path(__FILE__) . 'includes/class-job-manager.php'; 73 require_once plugin_dir_path(__FILE__) . 'includes/class-social-widget.php'; 74 require_once plugin_dir_path(__FILE__) . 'includes/class-deactivation-handler.php'; 75 require_once plugin_dir_path(__FILE__) . 'includes/class-block-bindings.php'; 76 require_once plugin_dir_path(__FILE__) . 'includes/class-schema-mapper.php'; 77 require_once plugin_dir_path(__FILE__) . 'includes/class-schema-frontend.php'; 78 require_once plugin_dir_path(__FILE__) . 'includes/class-seo-enhancements.php'; 79 require_once plugin_dir_path(__FILE__) . 'includes/class-sitemap-integration.php'; 80 } 81 add_action('plugins_loaded', 'pcrecruiter_load_plugin'); 82 83 84 /** 85 * Activation hook for PCR_Job_Manager. 86 * Registers CPT and creates fulltext index on wp_posts for keyword search performance. 87 * Direct DB query required "" dbDelta() does not support FULLTEXT indexes. 88 */ 89 function pcrecruiter_activate_job_manager_plugin() 90 { 91 require_once plugin_dir_path(__FILE__) . 'includes/class-job-manager.php'; 92 93 PCR_Job_Manager::register_job_post_type(); 94 pcrecruiter_ensure_fulltext_index(); 95 flush_rewrite_rules(); 96 } 97 register_activation_hook(__FILE__, 'pcrecruiter_activate_job_manager_plugin'); 98 99 /** 100 * Create fulltext index on wp_posts if missing. 101 * Required for efficient keyword search on job titles/content. 102 * Uses direct query because WP_Query lacks native fulltext support. 103 */ 104 function pcrecruiter_ensure_fulltext_index() 105 { 106 global $wpdb; 107 108 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Activation: one-time setup, no caching needed 109 $index_exists = $wpdb->get_var( 110 $wpdb->prepare( 111 "SHOW INDEX FROM {$wpdb->posts} WHERE Key_name = %s", 112 'ft_wp_posts_content' 113 ) 114 ); 115 116 if (! $index_exists) { 117 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Activation: one-time fulltext index creation (dbDelta unsupported; standard for search plugins) 118 $result = $wpdb->query( 119 "ALTER TABLE {$wpdb->posts} ADD FULLTEXT ft_wp_posts_content (post_title, post_content)" 120 ); 121 if (false === $result) { 122 // Log error for debugging 123 if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { 124 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 125 error_log('PCRecruiter: Failed to create fulltext index - ' . $wpdb->last_error); 126 } 127 // Store error for admin notice 128 add_option('pcrecruiter_fulltext_index_error', $wpdb->last_error, '', 'no'); 129 } else { 130 // Success "" clear any previous error 131 delete_option('pcrecruiter_fulltext_index_error'); 132 } 133 } 134 } 135 function pcrecruiter_deactivate_job_manager_plugin() 136 { 137 // Get user's cleanup choices from modal 138 $deactivation_actions = get_option('pcrecruiter_deactivation_actions', []); 139 140 // Execute cleanup based on user's choices 141 if (!empty($deactivation_actions)) { 142 // Settings cleanup 143 if (($deactivation_actions['settings'] ?? 'keep') === 'delete') { 144 PCR_Deactivation_Handler::execute_settings_cleanup(); 145 } 146 147 // Jobs cleanup 148 if (($deactivation_actions['jobs'] ?? 'keep') === 'delete') { 149 PCR_Deactivation_Handler::execute_jobs_cleanup(); 150 } 151 152 // RSS cleanup 153 if (($deactivation_actions['rss'] ?? 'keep') === 'delete') { 154 PCR_Deactivation_Handler::execute_rss_cleanup(); 155 } 156 } 157 158 // Always cleanup 159 PCR_Job_Manager::unregister_job_post_type(); 160 flush_rewrite_rules(); 161 delete_option('pcrecruiter_deactivation_actions'); 162 } 163 register_deactivation_hook(__FILE__, 'pcrecruiter_deactivate_job_manager_plugin'); 164 // Hook to initialize the custom post type 165 add_action('init', ['PCR_Job_Manager', 'register_job_post_type']); 166 167 // Add columns to admin area 168 add_action('init', ['PCR_Job_Manager', 'setup_admin_columns']); 169 170 // Clear job cache when jobs are updated or deleted 171 add_action('save_post_job', ['PCR_Job_Manager', 'clear_all_job_caches']); 172 add_action('delete_post', 'pcrecruiter_clear_job_cache_on_delete'); 173 174 function pcrecruiter_clear_job_cache_on_delete($post_id) 175 { 176 if (get_post_type($post_id) === 'job') { 177 PCR_Job_Manager::clear_all_job_caches(); 178 } 179 } 180 181 /** 182 * Optional: Add admin notice if schema mapper is not enabled 183 * This reminds admins that the feature exists 184 */ 185 function pcrecruiter_schema_mapper_notice() 186 { 187 // Only show on Jobs admin pages 188 $screen = get_current_screen(); 189 if (!$screen || $screen->post_type !== 'job') { 190 return; 191 } 192 193 // Check if schema mapper is enabled 194 if (class_exists('PCR_Schema_Mapper')) { 195 return; 196 } 197 198 // Check if user has already dismissed this notice 199 $user_id = get_current_user_id(); 200 if (get_user_meta($user_id, 'pcr_schema_mapper_notice_dismissed', true)) { 201 return; 202 } 203 204 ?> 205 <div class="notice notice-info is-dismissible" data-dismissible="pcr-schema-mapper-notice"> 206 <p> 207 <strong>PCRecruiter Extensions:</strong> 208 Custom schema mapping is available but not enabled. 209 This allows you to create JobPosting schema even when PCR doesn't provide it. 210 <a href="<?php echo esc_url(admin_url('edit.php?post_type=job&page=pcr-schema-mapper')); ?>">Configure Schema Mapping</a> 211 </p> 212 </div> 213 <script> 214 jQuery(function($) { 215 $(document).on('click', '.notice[data-dismissible="pcr-schema-mapper-notice"] .notice-dismiss', function() { 216 $.post(ajaxurl, { 217 action: 'pcr_dismiss_schema_notice', 218 nonce: '<?php echo esc_js(wp_create_nonce('pcr_dismiss_schema_notice')); ?>' 219 }); 220 }); 221 }); 222 </script> 223 <?php 224 } 225 // Uncomment to enable the notice: 226 // add_action('admin_notices', 'pcrecruiter_schema_mapper_notice'); 227 228 /** 229 * Handle dismissal of schema mapper notice 230 */ 231 function pcrecruiter_dismiss_schema_notice() 232 { 233 check_ajax_referer('pcr_dismiss_schema_notice', 'nonce'); 234 $user_id = get_current_user_id(); 235 update_user_meta($user_id, 'pcr_schema_mapper_notice_dismissed', true); 236 wp_send_json_success(); 237 } 238 add_action('wp_ajax_pcr_dismiss_schema_notice', 'pcrecruiter_dismiss_schema_notice'); 239 240 241 242 243 /* 244 END JOB MANAGER 245 */ 246 12 247 13 248 /* … … 15 250 */ 16 251 17 function pcr_assets() { 18 wp_register_script( 'pcr-iframe', 'https://www2.pcrecruiter.net/pcrimg/inc/pcrframehost.js', false, false, false); 19 wp_enqueue_script( 'pcr-iframe' ); 20 wp_enqueue_style( 'pcr-styles', 'https://www2.pcrecruiter.net/pcrimg/inc/pcrframehost.css', array(), null ); 21 } 22 add_action( 'wp_enqueue_scripts', 'pcr_assets' ); 23 function sanitize_loadurl($urlparam) 24 { 25 // Allow only letters, numbers, periods, equals, colons, hyphens, question marks, hyphens, forward slashes, percent signs, spaces, and ampersands 26 return preg_replace('/[^a-zA-Z0-9\.\=\:\?\/%\s&\-_]/', '', $urlparam); 27 } 28 29 function pcr_frame($atts) 30 { 31 $a = shortcode_atts([ 252 function pcrecruiter_plugin_controller() 253 { 254 global $pcrecruiter_has_shortcode; 255 256 $options = get_option('pcrecruiter_feed_options', []); 257 $job_board_page = $options['job_board_page'] ?? ''; 258 259 if (empty($job_board_page)) { 260 return; 261 } 262 // Get current URL (sanitize and unslash server input) 263 $current_path = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; 264 $url = $current_path; 265 $pcrecruiter_has_shortcode = (strpos($job_board_page, $url) !== false); 266 267 if ($pcrecruiter_has_shortcode) { 268 $rawData = file_get_contents('php://input'); 269 $data = json_decode($rawData, true); 270 $action = ""; 271 272 // Internal job manager cookie 273 if (stripos($current_path, 'internaljobmanager') !== false) { 274 PCR_Job_Manager::write_internal_cookie(); 275 add_action('wp_head', ['PCR_Job_Manager', 'write_no_index_header']); 276 } 277 278 if (isset($data['action'])) { 279 $action = $data['action']; 280 } 281 282 // Check if there was an error during decoding 283 if (json_last_error() === JSON_ERROR_NONE) { 284 switch ($action) { 285 case 'DELETEALL': 286 $job_manager = new PCR_Job_Manager(); 287 $rslt = $job_manager->delete_jobs($data); 288 $status = $rslt ? 200 : 500; 289 290 header("Content-Type: application/json"); 291 292 if ($rslt) { 293 exit(json_encode([ 294 'status' => $status, 295 'message' => 'Jobs were deleted successfully.', 296 'content' => 'Jobs were deleted successfully.' 297 ])); 298 } else { 299 exit(json_encode([ 300 'status' => $status, 301 'message' => 'Jobs could not be deleted', 302 'content' => 'Error: jobs could not be deleted' 303 ])); 304 } 305 break; 306 307 case 'UPDATEJOB': 308 $job_manager = new PCR_Job_Manager(); 309 $validfields = true; 310 $postid = ""; 311 $status = 200; 312 $content = 'Job was updated successfully.'; 313 314 // Check for missing or empty fields 315 if (empty($data['title'])) { 316 $validfields = false; 317 } 318 if (empty($data['description'])) { 319 $validfields = false; 320 } 321 322 if ($validfields) { 323 $postid = $job_manager->create_Update_job($data); 324 if (!$postid) { 325 $validfields = false; 326 } 327 } 328 329 header("Content-Type: application/json"); 330 331 if ($validfields) { 332 if (strpos($postid, 'Error') !== false) { 333 $status = 410; 334 $content = $postid; 335 } 336 337 exit(json_encode([ 338 'status' => $status, 339 'message' => 'The JSON action is UPDATEJOB.', 340 'content' => $content, 341 'shortcode' => $pcrecruiter_has_shortcode, 342 'permalink' => get_permalink($postid) 343 ])); 344 } else { 345 exit(json_encode([ 346 'status' => 400, 347 'message' => 'Missing or Invalid Fields', 348 'content' => $content, 349 'shortcode' => $pcrecruiter_has_shortcode 350 ])); 351 } 352 break; 353 354 case 'UPDATESETTING': 355 if (isset($data['resulttemplate'], $data['searchtemplate'], $data['hash'])) { 356 $job_manager = new PCR_Job_Manager(); 357 $resultcode = $job_manager->update_wp_settings($data); 358 $status = 200; 359 $content = 'Settings Updated'; 360 361 if (strpos($resultcode, 'Error') !== false) { 362 $status = 410; 363 $content = $resultcode; 364 } 365 366 header("Content-Type: application/json"); 367 exit(json_encode([ 368 'status' => $status, 369 'message' => $content 370 ])); 371 } else { 372 header("Content-Type: application/json"); 373 exit(json_encode([ 374 'status' => 400, 375 'message' => 'Missing Setting Data' 376 ])); 377 } 378 break; 379 380 default: 381 header("Content-Type: application/json"); 382 exit(json_encode([ 383 'status' => 400, 384 'message' => 'The JSON action is unknown.' 385 ])); 386 } 387 } 388 } 389 } 390 add_action('wp', 'pcrecruiter_plugin_controller'); 391 392 function pcrecruiter_assets() 393 { 394 global $post; 395 396 // Get job board page configuration 397 $options = get_option('pcrecruiter_feed_options', []); 398 $job_board_page = isset($options['job_board_page']) ? trim($options['job_board_page']) : ''; 399 400 // Determine if we're using Full Sync (jobmanager) or just iframes 401 $using_full_sync = !empty($job_board_page); 402 403 // Determine if current page should load jobmanager assets 404 $is_jobmanager_page = false; 405 $is_job_post = is_singular('job'); 406 407 if ($using_full_sync) { 408 409 // Check if job_board_page is a URL or page ID(s) 410 if (filter_var($job_board_page, FILTER_VALIDATE_URL)) { 411 // It's a URL - check if current URL matches 412 $current_url = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; 413 $current_path = wp_parse_url($current_url, PHP_URL_PATH); 414 $is_jobmanager_page = ($current_path && strpos($job_board_page, $current_path) !== false); 415 } elseif ($post) { 416 // It's page ID(s) - convert to array and check current page 417 $page_ids = array_map('intval', array_map('trim', explode(',', $job_board_page))); 418 $is_jobmanager_page = in_array($post->ID, $page_ids, true); 419 } 420 } 421 422 $hostreferer = ""; 423 $host = ""; 424 425 // URL referer 426 if (isset($_SERVER['HTTP_REFERER'])) { 427 $referer = sanitize_url(wp_unslash($_SERVER['HTTP_REFERER'])); 428 $parsed = wp_parse_url($referer); 429 $host = isset($parsed['host']) ? $parsed['host'] : ""; 430 $hostreferer = "referer||website||" . $host; 431 } 432 433 // User-provided source 434 if (isset($_GET['src'])) { 435 $src = sanitize_text_field(wp_unslash($_GET['src'])); 436 if (strpos($src, "||") == false) { 437 $hostreferer = $src . "||website||" . $host; 438 } else { 439 $hostreferer = $src; 440 } 441 } 442 443 // Google Tag Manager 444 if (isset($_GET['utm_source'])) { 445 $utm_source = sanitize_text_field(wp_unslash($_GET['utm_source'])); 446 $utm_medium = isset($_GET['utm_medium']) ? sanitize_text_field(wp_unslash($_GET['utm_medium'])) : $host; 447 $utm_campaign = isset($_GET['utm_campaign']) ? sanitize_text_field(wp_unslash($_GET['utm_campaign'])) : "website"; 448 $hostreferer = $utm_source . "||" . $utm_campaign . "||" . $utm_medium; 449 } 450 451 $hostreferer = esc_js(sanitize_text_field(preg_replace('/[\x{0022}\x{0027}\x{2018}\x{2019}\x{201C}\x{201D}\x{005C}]/u', '', $hostreferer))); 452 453 454 // Register all scripts/styles 455 wp_register_script('pcr-iframe', 'https://www2.pcrecruiter.net/pcrimg/inc/pcrframehost.js', false, PCRECRUITER_EXTENSIONS_VERSION, false); 456 wp_register_script('pcr-jobboard-js', 'https://www2.pcrecruiter.net/pcrimg/js/wp_jobboard.js', false, PCRECRUITER_EXTENSIONS_VERSION, false); 457 wp_register_script('pcr-frontend', plugin_dir_url(__FILE__) . 'assets/js/pcr-frontend.js', array(), PCRECRUITER_EXTENSIONS_VERSION, true); 458 wp_register_style('pcr-job-board', plugin_dir_url(__FILE__) . 'assets/css/pcr-job-board.css', array(), PCRECRUITER_EXTENSIONS_VERSION); 459 460 // Load based on configuration 461 if ($using_full_sync) { 462 // Full Sync mode: Load tracking on all pages, jobmanager assets on specific pages 463 wp_enqueue_script('pcr-iframe'); 464 465 // Load jobmanager-specific assets on job board page(s) and single job posts 466 if ($is_jobmanager_page || $is_job_post) { 467 wp_enqueue_script('pcr-jobboard-js'); 468 wp_enqueue_script('pcr-frontend'); 469 wp_enqueue_style('pcr-job-board'); 470 } 471 472 // Inline script for PHP-dependent variables (referer tracking and timezone) 473 // Only in Full Sync mode - CORS prevents iframe access to localStorage 474 wp_add_inline_script('pcr-iframe', " 475 const ref = '$hostreferer'; 476 if(ref.length > 0 && document.location.host?.indexOf(ref) === -1) { 477 localStorage['pcrecruiter_referer'] = ref; 478 } 479 480 // Set timezone cookie 481 try { 482 const offset = new Date().getTimezoneOffset(); 483 document.cookie = 'wp_timezone_offset=' + offset + '; path=/; secure; samesite=lax'; 484 } catch(e) { 485 // Silently fail if cookie setting fails 486 } 487 "); 488 } else { 489 // Iframe-only mode: Only load iframe scripts when shortcode is present 490 // Scripts will be conditionally loaded by pcrecruiter_frame() when executed 491 wp_enqueue_script('pcr-iframe'); 492 } 493 } 494 add_action('wp_enqueue_scripts', 'pcrecruiter_assets'); 495 496 // Register 'pg' query variable for pagination 497 function pcrecruiter_register_query_vars($vars) 498 { 499 $vars[] = 'pg'; 500 return $vars; 501 } 502 add_filter('query_vars', 'pcrecruiter_register_query_vars'); 503 504 /** 505 * Disable wpautop for job posts using proper filter management 506 */ 507 function pcrecruiter_manage_wpautop_for_jobs($content) 508 { 509 if (get_post_type() !== 'job') { 510 return $content; 511 } 512 513 $wpautop_priority = has_filter('the_content', 'wpautop'); 514 515 if ($wpautop_priority !== false) { 516 remove_filter('the_content', 'wpautop', $wpautop_priority); 517 518 // Store priority globally for restoration 519 global $pcrecruiter_wpautop_priority; 520 $pcrecruiter_wpautop_priority = $wpautop_priority; 521 522 add_filter('the_content', 'pcrecruiter_restore_wpautop_after_job', 999); 523 } 524 525 return $content; 526 } 527 528 /** 529 * Restore wpautop after job content is processed 530 */ 531 function pcrecruiter_restore_wpautop_after_job($processed_content) 532 { 533 global $pcrecruiter_wpautop_priority; 534 if (isset($pcrecruiter_wpautop_priority)) { 535 add_filter('the_content', 'wpautop', $pcrecruiter_wpautop_priority); 536 unset($pcrecruiter_wpautop_priority); 537 } 538 return $processed_content; 539 } 540 add_filter('the_content', 'pcrecruiter_manage_wpautop_for_jobs', 8); 541 542 /** 543 * Insert custom job content into the post body. 544 * Handles Avada and other page builders that call the_content filter multiple times. 545 */ 546 function pcrecruiter_insert_custom_content($content) 547 { 548 if (get_post_type() !== 'job') { 549 return $content; 550 } 551 552 if (!is_singular('job')) { 553 return $content; 554 } 555 556 // Get the actual post content from database 557 global $post; 558 if (!$post instanceof WP_Post) { 559 return $content; 560 } 561 562 $raw_post_content = $post->post_content ?? ''; 563 564 // Some builders (like Avada) store content base64 encoded 565 $decoded = base64_decode($raw_post_content, true); 566 if ($decoded !== false && mb_check_encoding($decoded, 'UTF-8')) { 567 $raw_post_content = $decoded; 568 } 569 570 // Only process if current content matches the actual post content 571 // This prevents processing header/footer/sidebar template sections 572 $is_main_content = false; 573 if ($content === $raw_post_content) { 574 $is_main_content = true; 575 } elseif (strlen($raw_post_content) >= 100) { 576 $is_main_content = strpos($content, substr($raw_post_content, 0, 100)) !== false; 577 } elseif (strlen($raw_post_content) > 0) { 578 $is_main_content = strpos($content, $raw_post_content) !== false; 579 } 580 581 if (!$is_main_content) { 582 return $content; 583 } 584 585 $post_id = get_the_ID(); 586 return PCR_Job_Manager::Render_Job_Posting($post_id, $content); 587 } 588 add_filter('the_content', 'pcrecruiter_insert_custom_content', 20); 589 590 // CSS class by status 591 add_filter('body_class', 'pcrecruiter_add_expired_job_body_class'); 592 function pcrecruiter_add_expired_job_body_class($classes) 593 { 594 if (is_singular('job')) { 595 $job_id = get_the_ID(); 596 $is_expired = get_post_meta($job_id, '_pcr_expired', true); 597 598 if ($is_expired) { 599 $classes[] = 'pcr-expired-job'; 600 $classes[] = 'job-status-expired'; 601 } else { 602 $classes[] = 'job-status-active'; 603 } 604 } 605 return $classes; 606 } 607 add_filter('post_class', 'pcrecruiter_add_expired_job_post_class', 10, 3); 608 function pcrecruiter_add_expired_job_post_class($classes, $class, $post_id) 609 { 610 if (get_post_type($post_id) === 'job') { 611 $is_expired = get_post_meta($post_id, '_pcr_expired', true); 612 613 if ($is_expired) { 614 $classes[] = 'pcr-expired-job'; 615 $classes[] = 'job-status-expired'; 616 } else { 617 $classes[] = 'job-status-active'; 618 } 619 } 620 return $classes; 621 } 622 623 624 // Custom 404 handler for job post type 625 function pcrecruiter_custom_job_404_redirect() 626 { 627 if (is_404()) { 628 global $wp_query; 629 630 if (isset($wp_query->query_vars['post_type']) && $wp_query->query_vars['post_type'] === 'job') { 631 $pcrecruiter_feed_options = get_option('pcrecruiter_feed_options'); 632 $job_404_page = isset($pcrecruiter_feed_options['job_404_page']) ? trim($pcrecruiter_feed_options['job_404_page']) : ''; 633 634 if (!empty($job_404_page)) { 635 if (strpos($job_404_page, '/') !== 0) { 636 $job_404_page = '/' . $job_404_page; 637 } 638 639 wp_safe_redirect(home_url($job_404_page), 302); 640 exit; 641 } 642 } 643 } 644 } 645 add_action('template_redirect', 'pcrecruiter_custom_job_404_redirect'); 646 647 function pcrecruiter_sanitize_loadurl($urlparam) 648 { 649 return preg_replace('/[^a-zA-Z0-9\.\=\:\?\/%\s&\-_]/', '', $urlparam); 650 } 651 652 function pcrecruiter_frame($atts) 653 { 654 // Register the framehost CSS so it can be enqueued conditionally below 655 wp_register_style('pcr-framehost', 'https://www2.pcrecruiter.net/pcrimg/inc/pcrframehost.css', array(), PCRECRUITER_EXTENSIONS_VERSION); 656 657 $a = shortcode_atts(array( 32 658 'link' => 'about:blank', 33 659 'background' => 'transparent', 34 660 'initialheight' => '640', 35 'form' => '' 36 ], $atts); 37 38 $sid = intval($a['form']); 661 'analytics' => '', 662 'form' => '', 663 'jobcategory' => '' 664 ), $atts); 665 666 $sid = isset($a['form']) && $a['form'] !== '' ? intval($a['form']) : null; 39 667 $loadurl = $a['link']; 40 668 $loadurl = htmlspecialchars_decode($loadurl, ENT_QUOTES); 41 $loadurl = sanitize_loadurl($loadurl); 42 43 669 $loadurl = pcrecruiter_sanitize_loadurl($loadurl); 44 670 45 671 $initialheight = intval($a['initialheight']); 46 672 $background = preg_match('/^#[a-fA-F0-9]{3,6}$|^transparent$/', $a['background']) ? $a['background'] : 'transparent'; 47 673 48 // Modify the URL when needed 49 if (strpos($loadurl,'jobboard.') > -1 || strpos($loadurl,'temphrs.') > -1 || strpos($loadurl,'employer.') > -1 || strpos($loadurl,'/reg') > -1 || strpos($loadurl,'reg') > -1) { 50 51 } else { 52 if ($sid < 1 || strpos($loadurl, '.asp?') === false || strpos($loadurl, '.exe?') === false || strpos($loadurl, '.aspx?') === false) { 53 $loadurl = 'jobboard.aspx?uid=' . $loadurl; 54 $pcrframecss = ''; 55 } 56 } 57 58 // If a custom form is specified 59 if ($sid && $loadurl !== "about:blank") { 60 // Create the <script> tag with DOMDocument 61 $doc = new DOMDocument('1.0', 'UTF-8'); 62 $script = $doc->createElement('script'); 63 $loadurl = urldecode($loadurl); 64 $script->setAttribute('src', "https://host.pcrecruiter.net/pcrbin/{$loadurl}&action=opencustomform&sid={$sid}"); 65 $doc->appendChild($script); 66 67 return "<!-- Start PCRecruiter Form -->" 68 . $doc->saveHTML() 69 . "<!-- End PCRecruiter Form -->"; 70 } 71 72 // Prepend URL based on the link 73 if (substr($loadurl, 0, 4) !== "http") { 74 $prefix = (substr($loadurl, 0, 8) === "jobboard") 75 ? 'https://host.pcrecruiter.net/pcrbin/' 76 : 'https://www2.pcrecruiter.net/pcrbin/'; 77 $loadurl = $prefix . $loadurl; 78 } 79 80 $loadurl = str_replace('http:','https:',$loadurl); 81 $loadurl = str_replace('.exe?','.aspx?',$loadurl); 82 83 // Create iframe with DOMDocument 84 $doc = new DOMDocument('1.0', 'UTF-8'); 85 $iframe = $doc->createElement('iframe'); 86 $iframe->setAttribute('frameborder', '0'); 87 $iframe->setAttribute('host', $loadurl); 88 $iframe->setAttribute('id', 'pcrframe'); 89 $iframe->setAttribute('name', 'pcrframe'); 90 $iframe->setAttribute('src', 'about:blank'); 91 $iframe->setAttribute('style', "height:{$a['initialheight']}px;width:100%;background-color:{$a['background']};border:0;margin:0;padding:0"); 92 $iframe->setAttribute('onload', 'pcrframeurl();'); 93 94 $doc->appendChild($iframe); 95 96 return "<!-- Start PCRecruiter WP 1.4.36-->" 97 . $doc->saveHTML() 98 . "<!-- End PCRecruiter WP -->"; 99 } 100 101 add_shortcode('PCRecruiter', 'pcr_frame'); 102 674 $analytics = $a['analytics']; 675 if ($a['analytics'] != '') { 676 $analytics = ' analytics="true" '; 677 }; 678 679 if ($loadurl == "jobmanager" || $loadurl == "internaljobmanager") { 680 $job_manager = new PCR_Job_Manager(); 681 $jobcategory = isset($a['jobcategory']) && $a['jobcategory'] !== '' ? sanitize_text_field($a['jobcategory']) : ''; 682 return $job_manager->job_listings($loadurl, $jobcategory); 683 } 684 685 $needs_css = true; 686 if (strpos($loadurl, '.asp?') === false && strpos($loadurl, '.exe?') === false && strpos($loadurl, '.aspx?') === false) { 687 // Parse URL to extract base UID and any additional parameters 688 // This handles: "pcr demo.pcrdemo", "pcr%20demo.pcrdemo", and "pcr%20demo.pcrdemo&filter=value" 689 $parts = explode('&', $loadurl, 2); 690 $uid = $parts[0]; // Base UID (may have spaces or %20) 691 $extra_params = isset($parts[1]) ? '&' . $parts[1] : ''; // Additional params like &filter=value 692 693 // Decode then encode only the UID part to normalize encoding 694 $loadurl = 'jobboard.aspx?uid=' . rawurlencode(rawurldecode($uid)) . $extra_params; 695 $needs_css = false; 696 } 697 698 if (is_numeric($sid) && $loadurl !== "about:blank") { 699 $script_url = 'https://www2.pcrecruiter.net/pcrbin/' . $loadurl . '&action=opencustomform&sid=' . rawurlencode($sid); 700 701 $handle = 'pcrecruiter-customform-' . $sid; 702 703 wp_register_script($handle, esc_url_raw($script_url), [], $sid, true); 704 wp_enqueue_script($handle); 705 706 // Prevent duplicate loading in footer 707 wp_dequeue_script($handle); 708 wp_deregister_script($handle); 709 710 // Required for PCRecruiter's DOM-based form injection 711 // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript 712 return '<script src="' . esc_url($script_url) . '"></script>'; 713 } else { 714 if (substr($loadurl, 0, 4) !== "http" && substr($loadurl, 0, 8) !== "jobboard") { 715 $aspurl = 'https://www2.pcrecruiter.net/pcrbin/' . $loadurl; 716 $loadurl = $aspurl; 717 }; 718 if (substr($loadurl, 0, 4) !== "http" && substr($loadurl, 0, 8) == "jobboard") { 719 $needs_css = false; 720 $aspurl = 'https://host.pcrecruiter.net/pcrbin/' . $loadurl; 721 $loadurl = $aspurl; 722 }; 723 724 if ($needs_css) { 725 wp_enqueue_style('pcr-framehost'); 726 } 727 728 // Enqueue jobboard script for appendSrcToFrame() function 729 wp_enqueue_script('pcr-jobboard-js'); 730 731 $version = pcrecruiter_get_plugin_version(); 732 733 return "<!-- Start PCRecruiter WP {$version}--><iframe frameborder=\"0\" host=\"{$loadurl}\" id=\"pcrframe\" name=\"pcrframe\" src=\"about:blank\" style=\"height:{$initialheight}px;width:100%;background-color:{$background};border:0;margin:0;padding:0\" {$analytics} onload=\"pcrframeurl();\"></iframe><!-- End PCRecruiter WP -->"; 734 } 735 } 736 add_shortcode('PCRecruiter', 'pcrecruiter_frame'); 737 738 /** 739 * Redirect legacy PCR iframe job detail links to synced WP job URLs 740 */ 741 add_action('template_redirect', 'pcrecruiter_redirect_legacy_job_links'); 742 743 function pcrecruiter_redirect_legacy_job_links() 744 { 745 if (is_admin()) { 746 return; 747 } 748 749 // Only redirect if sync is configured (has security key) 750 $options = get_option('pcrecruiter_feed_options', []); 751 if (empty($options['jobboard_security_key'])) { 752 return; 753 } 754 755 if (empty($_GET['recordid'])) { 756 return; 757 } 758 759 $recordid = ''; 760 761 if (isset($_GET['recordid'])) { 762 $recordid = preg_replace('/\D+/', '', sanitize_text_field(wp_unslash($_GET['recordid']))); 763 } 764 765 if ('' === $recordid) { 766 return; 767 } 768 769 if (is_singular('job')) { 770 return; 771 } 772 773 $match = get_posts([ 774 'post_type' => 'job', 775 'posts_per_page' => 1, 776 'fields' => 'ids', 777 'meta_key' => '[[position.job_id]]', 778 'meta_value' => $recordid, 779 'no_found_rows' => true, 780 'update_post_meta_cache' => false, 781 'update_post_term_cache' => false, 782 ]); 783 784 if (!$match) { 785 return; 786 } 787 788 $url = get_permalink($match[0]); 789 if (!empty($url) && $url !== home_url(add_query_arg([]))) { 790 wp_safe_redirect($url, 301); 791 exit; 792 } 793 } 103 794 104 795 /** … … 106 797 * Prevents search engines from consolidating ?recordid= variations into one page 107 798 */ 108 function pcrecruiter_remove_canonical_for_iframe() { 799 function pcrecruiter_remove_canonical_for_iframe() 800 { 109 801 global $post; 110 802 111 803 if (!is_a($post, 'WP_Post')) { 112 804 return; 113 805 } 114 806 115 807 // Only proceed if this page has the PCRecruiter shortcode 116 808 if (!has_shortcode($post->post_content, 'PCRecruiter')) { 117 809 return; 118 810 } 119 811 120 812 // Check if it's an iframe installation (not jobmanager) 121 813 $pattern = get_shortcode_regex(['PCRecruiter']); … … 128 820 } 129 821 } 130 822 131 823 // This is an iframe page - remove canonical to allow search engines 132 824 // to index individual ?recordid= URLs as separate pages … … 137 829 138 830 /* 139 140 831 BEGIN Get PCR Feed 141 142 832 */ 143 833 144 // checkop() to see if options exist. If not, set the defaults 145 function checkop() { 146 //check if option is already present 147 //option key is pcr_feed_options 148 if(!get_option('pcr_feed_options')) { 149 //not present, so add 150 $pcr_options = array( 151 //'activation' => 'true', 152 'frequency' => 'daily', 153 'query' => 'Status%20eq%20Available%20and%20ShowOnWeb%20eq%20true%20and%20%28NumberOfOpenings%20ne%200%20OR%20NumberOfOpenings%20eq%20%27%27)', 154 'mode' => 'job', 155 ); 156 add_option('pcr_feed_options', $pcr_options); 157 } 158 } 159 160 register_activation_hook(__FILE__, 'pcr_feed_activation'); 161 162 function pcr_feed_activation(){ 163 // set the defaults with checkop() 164 checkop(); 165 pcr_feed_func(); 166 } 167 add_action('pcr_feed', 'pcr_feed_activation'); 168 169 // Register hook for admin notices 170 add_action('admin_notices', 'pcr_admin_notices'); 834 function pcrecruiter_checkop() 835 { 836 if (!get_option('pcrecruiter_feed_options')) { 837 $pcrecruiter_options = array( 838 'frequency' => 'daily', 839 'query' => 'Status%20eq%20Available%20and%20ShowOnWeb%20eq%20true%20and%20%28NumberOfOpenings%20ne%200%20OR%20NumberOfOpenings%20eq%20%27%27)', 840 'mode' => 'job', 841 ); 842 add_option('pcrecruiter_feed_options', $pcrecruiter_options); 843 } 844 } 845 846 register_activation_hook(__FILE__, 'pcrecruiter_feed_activation'); 847 848 function pcrecruiter_feed_activation() 849 { 850 pcrecruiter_checkop(); 851 pcrecruiter_feed_func(); 852 } 853 add_action('pcrecruiter_feed', 'pcrecruiter_feed_activation'); 854 855 function pcrecruiter_feed_func($force = false, &$error = null) 856 { 857 $pcrecruiter_feed_options = get_option('pcrecruiter_feed_options', array()); 858 $pcrecruiter_customfields = str_replace(' ', '%20', $pcrecruiter_feed_options['custom_fields'] ?? ''); 859 $pcrecruiter_standardfields = str_replace(' ', '%20', $pcrecruiter_feed_options['standard_fields'] ?? ''); 860 $pcrecruiter_id_number = $pcrecruiter_feed_options['id_number'] ?? ''; 861 $pcrecruiter_activation = $pcrecruiter_feed_options['activation'] ?? ''; 862 $url = "https://host.pcrecruiter.net/pcrbin/feeds.aspx?action=customfeed&query=" . $pcrecruiter_feed_options['query'] . "&xtransform=RSS2&SessionId=" . $pcrecruiter_id_number . "&FieldsPlus=" . $pcrecruiter_standardfields . "&custom=" . $pcrecruiter_customfields . "&mode=" . $pcrecruiter_feed_options['mode']; 863 864 if (! $force && ! $pcrecruiter_activation) { 865 return false; 866 } 867 868 $buffer = file_get_contents($url); 869 870 if ($buffer === false) { 871 $error = 'Feed download failed (check SessionID/query/mode/network).'; 872 return false; 873 } 874 875 $buffer = str_replace(array("\n", "\r", "\t"), '', $buffer); 876 $xml = simplexml_load_string($buffer, null, LIBXML_NOCDATA); 877 878 if ($xml === false) { 879 $error = 'Feed XML could not be parsed.'; 880 return false; 881 } 882 883 // Convert XML to array 884 $array = json_decode(json_encode($xml), true); 885 886 // Recursively trim whitespace from all string values 887 $array = pcrecruiter_array_trim_recursive($array); 888 889 $json = json_encode($array, JSON_PRETTY_PRINT); 890 file_put_contents(WP_CONTENT_DIR . '/uploads/pcrjobfeed.json', $json); 891 copy($url, WP_CONTENT_DIR . "/uploads/pcrjobfeed.xml"); 892 893 return true; 894 } 171 895 172 896 /** 173 * Display admin notices for PCRecruiter feed status897 * Recursively trim whitespace from all string values in an array 174 898 */ 175 function pcr_admin_notices() { 176 // Only show on the PCRecruiter settings page 177 $screen = get_current_screen(); 178 if ($screen->id !== 'settings_page_pcr-ext-setting-admin') { 179 return; 180 } 181 182 // Check if we have a feed status to display 183 $feed_status = get_option('pcr_feed_status'); 184 if (!$feed_status) { 185 return; 186 } 187 188 // Format the time for display 189 $status_time = !empty($feed_status['time']) ? 190 ' (' . human_time_diff(strtotime($feed_status['time']), current_time('timestamp')) . ' ago)' : ''; 191 192 // Display success or error message 193 if ($feed_status['success']) { 194 echo '<div class="notice notice-success is-dismissible">'; 195 echo '<p><strong>Feed:</strong> ' . esc_html($feed_status['message']) . esc_html($status_time) . '</p>'; 196 echo '</div>'; 197 } else { 198 echo '<div class="notice notice-error is-dismissible">'; 199 echo '<p><strong>Feed Error:</strong> ' . esc_html($feed_status['message']) . esc_html($status_time) . '</p>'; 200 echo '</div>'; 201 } 202 } 203 204 205 function pcr_feed_func() { 206 // do this via cron 207 $pcr_feed_options = get_option('pcr_feed_options', array()); 208 $pcr_customfields = str_replace(' ', '%20', $pcr_feed_options['custom_fields'] ?? ''); 209 $pcr_standardfields = str_replace(' ', '%20', $pcr_feed_options['standard_fields'] ?? ''); 210 $pcr_id_number = $pcr_feed_options['id_number'] ?? ''; 211 $pcr_activation = $pcr_feed_options['activation'] ?? ''; 212 $url = "https://host.pcrecruiter.net/pcrbin/feeds.aspx?action=customfeed&query=". $pcr_feed_options['query'] ."&xtransform=RSS2&SessionId=" . $pcr_id_number . "&FieldsPlus=" . $pcr_standardfields . "&custom=" . $pcr_customfields . "&mode=" . $pcr_feed_options['mode']; 213 214 if($pcr_activation) { 215 // First, try to safely get the content from the URL 216 $buffer = @file_get_contents($url); 217 218 // Check if we actually got content 219 if ($buffer === false) { 220 // Log the error and store the status message 221 $error_message = 'Failed to fetch XML feed. Please check your PCRecruiter SessionID and internet connection.'; 222 error_log('PCRecruiter Extensions: ' . $error_message); 223 update_option('pcr_feed_status', array( 224 'success' => false, 225 'message' => $error_message, 226 'time' => current_time('mysql') 227 )); 228 return false; 229 } 230 231 // Clean the buffer 232 $buffer = str_replace(array("\n", "\r", "\t"), '', $buffer); 233 234 // Try to load the XML with error suppression 235 libxml_use_internal_errors(true); // Suppress XML errors 236 $xml = @simplexml_load_string($buffer, null, LIBXML_NOCDATA); 237 238 // Check if XML loading was successful 239 if ($xml === false) { 240 $errors = libxml_get_errors(); 241 libxml_clear_errors(); 242 243 $error_message = 'XML load from '.$xml.' failed. Please verify your PCR SessionID and that the feed data is valid.'; 244 if (count($errors) > 0) { 245 $error_message .= ' Error: ' . $errors[0]->message; 246 } 247 248 // Log the error and store the status message 249 error_log('PCRecruiter Extensions: ' . $error_message); 250 update_option('pcr_feed_status', array( 251 'success' => false, 252 'message' => $error_message, 253 'time' => current_time('mysql') 254 )); 255 return false; 256 } 257 258 // Try to get namespaces safely 259 try { 260 $namespaces = $xml->getDocNamespaces(true); 261 262 $array = []; 263 if (!empty($namespaces)) { 264 foreach ($namespaces as $prefix => $namespace) { 265 if (is_string($namespace)) { 266 $nsXml = @simplexml_load_string($buffer, null, LIBXML_NOCDATA, $prefix); 267 if ($nsXml !== false) { 268 $array = array_merge_recursive($array, (array) $nsXml); 269 } 270 } 271 } 272 } else { 273 // If no namespaces, just use the base XML 274 $array = (array) $xml; 275 } 276 277 // Encode to JSON 278 $json = json_encode($array, JSON_PRETTY_PRINT); 279 if ($json === false) { 280 $error_message = 'JSON encoding failed: ' . json_last_error_msg(); 281 error_log('PCRecruiter Extensions: ' . $error_message); 282 update_option('pcr_feed_status', array( 283 'success' => false, 284 'message' => $error_message, 285 'time' => current_time('mysql') 286 )); 287 return false; 288 } 289 290 // Save the files 291 $json_result = @file_put_contents(WP_CONTENT_DIR . '/uploads/pcrjobfeed.json', $json); 292 $xml_result = @copy($url, WP_CONTENT_DIR . "/uploads/pcrjobfeed.xml"); 293 294 if ($json_result === false || $xml_result === false) { 295 $error_message = 'Failed to save feed file. Please check folder permissions.'; 296 error_log('PCRecruiter Extensions: ' . $error_message); 297 update_option('pcr_feed_status', array( 298 'success' => false, 299 'message' => $error_message, 300 'time' => current_time('mysql') 301 )); 302 return false; 303 } 304 305 // Update status with success message 306 update_option('pcr_feed_status', array( 307 'success' => true, 308 'message' => 'Feed successfully updated.', 309 'time' => current_time('mysql') 310 )); 311 312 return true; 313 314 } catch (Exception $e) { 315 $error_message = 'Exception occurred: ' . $e->getMessage(); 316 error_log('PCRecruiter Extensions: ' . $error_message); 317 update_option('pcr_feed_status', array( 318 'success' => false, 319 'message' => $error_message, 320 'time' => current_time('mysql') 321 )); 322 return false; 323 } 324 } 325 326 return true; 327 } 328 329 // deactivate hook 330 register_deactivation_hook(__FILE__, 'pcr_deactivation'); 331 function pcr_deactivation(){ 332 wp_clear_scheduled_hook('pcr_feed'); 333 } 334 /**/ 899 function pcrecruiter_array_trim_recursive($array) 900 { 901 if (!is_array($array)) { 902 return is_string($array) ? trim($array) : $array; 903 } 904 905 return array_map('pcrecruiter_array_trim_recursive', $array); 906 } 907 908 register_deactivation_hook(__FILE__, 'pcrecruiter_deactivation'); 909 function pcrecruiter_deactivation() 910 { 911 wp_clear_scheduled_hook('pcrecruiter_feed'); 912 } 913 335 914 /* 336 915 END Get PCR Feed 337 916 */ 917 338 918 /* 339 919 Settings for the PCRecruiter Extensions Plugin 340 920 */ 341 class PcrSettingsPage 342 { 343 /** 344 * Holds the values to be used in the fields callbacks 345 */ 921 class PCRecruiter_Extensions_Settings 922 { 346 923 private $options; 347 924 348 /**349 * Start up350 */351 925 public function __construct() 352 926 { 353 add_action( 'admin_menu', array( $this, 'add_plugin_page' ) ); 354 add_action( 'admin_init', array( $this, 'page_init' ) ); 355 } 356 357 /** 358 * Add options page 359 */ 927 $this->options = get_option('pcrecruiter_feed_options', []); 928 if (!is_array($this->options)) { 929 $this->options = []; 930 } 931 932 add_action('admin_menu', [$this, 'add_plugin_page']); 933 add_action('admin_init', [$this, 'page_init']); 934 } 935 360 936 public function add_plugin_page() 361 937 { 362 // This page will be under "Settings"363 938 add_options_page( 364 939 'Settings Admin', … … 366 941 'manage_options', 367 942 'pcr-ext-setting-admin', 368 array( $this, 'create_admin_page' ) 369 ); 370 } 371 372 /** 373 * Options page callback 374 */ 943 array($this, 'create_admin_page') 944 ); 945 } 946 947 public function job_404_page_callback() 948 { 949 $options = get_option('pcrecruiter_feed_options'); 950 $job_404_page = isset($options['job_404_page']) ? esc_attr($options['job_404_page']) : ''; 951 ?> 952 <input type="text" id="job_404_page" name="pcrecruiter_feed_options[job_404_page]" value="<?php echo esc_attr($job_404_page); ?>" size="60" placeholder="/job-not-found/" /> 953 <p class="description">Enter the URL path (e.g., /job-not-found/) to redirect users when a job posting is not found. Leave blank to use WordPress default 404 page.</p> 954 <?php 955 } 956 957 public function expired_job_handling_callback() 958 { 959 $options = get_option('pcrecruiter_feed_options'); 960 $handling = isset($options['expired_job_handling']) ? $options['expired_job_handling'] : 'delete'; 961 ?> 962 <fieldset> 963 <label> 964 <input type="radio" name="pcrecruiter_feed_options[expired_job_handling]" value="delete" <?php checked($handling, 'delete'); ?> /> 965 <strong>Delete expired jobs</strong> 966 </label> 967 <p class="description" style="margin-left: 25px;">Permanently remove expired/filled jobs from WordPress when PCR marks them as deleted. URLs will become 404s.</p> 968 969 <br> 970 971 <label> 972 <input type="radio" name="pcrecruiter_feed_options[expired_job_handling]" value="keep" <?php checked($handling, 'keep'); ?> /> 973 <strong>Keep expired jobs visible</strong> 974 </label> 975 <p class="description" style="margin-left: 25px;">Keep expired jobs in search results and listings, but mark them as "Position Filled" and disable apply functionality. Jobs get special CSS classes for custom styling.</p> 976 977 <p class="description" style="margin-top: 10px;"><em>Note: Keeping expired jobs visible helps maintain SEO value and provides better transparency to job seekers. Expired jobs will be marked with the "pcr-expired-job" class for custom styling.</em></p> 978 </fieldset> 979 <?php 980 } 981 public function job_title_format_callback() 982 { 983 $options = get_option('pcrecruiter_feed_options'); 984 $title_fields = isset($options['job_title_fields']) ? $options['job_title_fields'] : array( 985 'job_title' => '1', 986 'location' => '1', 987 'company' => '1' 988 ); 989 $force_titles = get_option('pcrecruiter_seo_enable_overrides', false); 990 ?> 991 <fieldset> 992 <p class="description">Choose which fields to include in job page titles (browser tabs and search results).</p> 993 <p class="description" style="margin-bottom: 10px;"><em>Note: Affects <title> tag only, not URL slugs. WordPress automatically appends site name.</em></p> 994 995 996 997 <div style="margin: 20px 0; padding: 20px; background: #f9f9f9; border-left: 4px solid #2271b1;"> 998 <h4 style="margin-top: 0;">Title Tag Components (in order)</h4> 999 <p class="description" style="margin-bottom: 15px;">Fields are joined with " - ". Empty fields are automatically skipped.</p> 1000 1001 <label style="display: block; margin: 10px 0; padding: 8px; background: #fff; border: 1px solid #ddd; border-radius: 3px;"> 1002 <input type="checkbox" name="pcrecruiter_feed_options[job_title_fields][job_title]" value="1" checked disabled style="margin-right: 8px;" /> 1003 <strong>Job Title</strong> <em>(always included)</em> 1004 </label> 1005 <!-- Hidden field to ensure job_title is always saved --> 1006 <input type="hidden" name="pcrecruiter_feed_options[job_title_fields][job_title]" value="1" /> 1007 1008 <label style="display: block; margin: 10px 0; padding: 8px; background: #fff; border: 1px solid #ddd; border-radius: 3px;"> 1009 <input type="checkbox" name="pcrecruiter_feed_options[job_title_fields][city]" value="1" <?php checked(!empty($title_fields['city']), true); ?> style="margin-right: 8px;" /> 1010 <strong>City</strong> 1011 </label> 1012 1013 <label style="display: block; margin: 10px 0; padding: 8px; background: #fff; border: 1px solid #ddd; border-radius: 3px;"> 1014 <input type="checkbox" name="pcrecruiter_feed_options[job_title_fields][state]" value="1" <?php checked(!empty($title_fields['state']), true); ?> style="margin-right: 8px;" /> 1015 <strong>State</strong> 1016 </label> 1017 1018 <label style="display: block; margin: 10px 0; padding: 8px; background: #fff; border: 1px solid #ddd; border-radius: 3px;"> 1019 <input type="checkbox" name="pcrecruiter_feed_options[job_title_fields][country]" value="1" <?php checked(!empty($title_fields['country']), true); ?> style="margin-right: 8px;" /> 1020 <strong>Country</strong> 1021 </label> 1022 1023 <label style="display: block; margin: 10px 0; padding: 8px; background: #fff; border: 1px solid #ddd; border-radius: 3px;"> 1024 <input type="checkbox" name="pcrecruiter_feed_options[job_title_fields][position_id]" value="1" <?php checked(!empty($title_fields['position_id']), true); ?> style="margin-right: 8px;" /> 1025 <strong>Position ID</strong> <span class="description">(Requisition number)</span> 1026 </label> 1027 1028 <label style="display: block; margin: 10px 0; padding: 8px; background: #fff; border: 1px solid #ddd; border-radius: 3px;"> 1029 <input type="checkbox" name="pcrecruiter_feed_options[job_title_fields][job_type]" value="1" <?php checked(!empty($title_fields['job_type']), true); ?> style="margin-right: 8px;" /> 1030 <strong>Job Type</strong> <span class="description">(Full-time, Part-time, etc.)</span> 1031 </label> 1032 <label style="display: block; margin: 10px 0; padding: 8px; background: #fff; border: 1px solid #ddd; border-radius: 3px;"> 1033 <input type="checkbox" name="pcrecruiter_feed_options[job_title_fields][company]" value="1" <?php checked(!empty($title_fields['company']), true); ?> style="margin-right: 8px;" /> 1034 <strong>Company Name</strong> (Synced from PCR) <span class="description"> 1035 <p><strong>Note:</strong> WordPress commonly adds " - <?php echo esc_html(get_bloginfo('name')); ?>" to the end of page titles.</p> 1036 </span> 1037 </label> 1038 </div> 1039 1040 <div style="margin: 10px 0; padding: 20px; background: #f9f9f9; border-left: 4px solid #ffc107;"> 1041 <label> 1042 <input type="checkbox" name="pcrecruiter_seo_enable_overrides" value="1" <?php checked($force_titles, true); ?> style="margin-right: 8px;" /> 1043 Force Titles 1044 </label> 1045 <p class="description" style="margin: 8px 0 0 24px;"> 1046 Some site themes and builders do not honor the default WordPress document title tags. 1047 Check this box if your jobs are not showing your selected Title Tag Components in the page title, 1048 or use your theme or plugin's provided tools to define the <title> tag. 1049 </p> 1050 <?php 1051 $conflicts = PCR_SEO_Enhancements::get_detected_conflicts(); 1052 if (!empty($conflicts)): ?> 1053 <div style="margin-top: 15px; padding: 10px; background: #fff; border: 1px solid #ddd;"> 1054 <strong>Detected SEO plugins/themes:</strong> 1055 <ul style="list-style: disc; margin: 5px 0 0 20px;"> 1056 <?php foreach ($conflicts as $conflict): ?> 1057 <li><strong><?php echo esc_html($conflict['name']); ?>:</strong> <?php echo esc_html($conflict['config']); ?></li> 1058 <?php endforeach; ?> 1059 </ul> 1060 </div> 1061 <?php endif; ?> 1062 </div> 1063 1064 1065 <div style="margin: 10px 0; padding: 20px; background: #f9f9f9; border-left: 4px solid #ffc107;"> 1066 1067 <label> 1068 <input type="checkbox" name="pcrecruiter_seo_use_pcr_logos" value="1" 1069 <?php checked(get_option('pcrecruiter_seo_use_pcr_logos'), 1); ?> /> 1070 Use Position/Company logos from PCR as <a href="https://ogp.me/" target="_blank">Open Graph</a> images. 1071 </label> 1072 <p class="description"> 1073 When unchecked, your theme or SEO plugin will control social sharing images. 1074 When checked, PCR Position logos (or Company logos as fallback) will be used. NOTE: Images should be at least 1200x630px for optimal social sharing, 1.91:1 aspect ratio. 1075 </p> 1076 </div> 1077 1078 1079 </fieldset> 1080 <?php 1081 } 1082 1083 public function strip_inline_styles_callback() 1084 { 1085 $options = get_option('pcrecruiter_feed_options'); 1086 $strip_styles = isset($options['strip_inline_styles']) ? $options['strip_inline_styles'] : '1'; 1087 ?> 1088 <label> 1089 <input type="checkbox" name="pcrecruiter_feed_options[strip_inline_styles]" value="1" <?php checked($strip_styles, '1'); ?> /> 1090 Remove inline styles from job descriptions 1091 </label> 1092 <p class="description">When enabled, removes style="" attributes from job content so jobs match your site's fonts and colors. Recommended for consistent styling.</p> 1093 <?php 1094 } 1095 1096 375 1097 public function create_admin_page() 376 1098 { 377 1099 $this->show_cron_status(); 378 // Set class property 379 $this->options = get_option( 'pcr_feed_options' ); 380 ?> 1100 $this->options = get_option('pcrecruiter_feed_options'); 1101 if (!is_array($this->options)) { 1102 $this->options = []; 1103 } 1104 ?> 381 1105 <div class="wrap"> 382 1106 <h1>PCRecruiter Extensions Settings</h1> 383 1107 <form method="post" onsubmit="return validatePCRfeed()" action="options.php"> 384 <?php 385 // This prints out all hidden setting fields 386 settings_fields( 'pcr_ext_option_group' ); 387 do_settings_sections( 'pcr-ext-setting-admin' ); 388 ?> 389 390 <?php submit_button();?> 1108 <?php 1109 settings_fields('pcrecruiter_ext_option_group'); 1110 do_settings_sections('pcr-ext-setting-admin'); 1111 submit_button(); 1112 ?> 391 1113 </form> 392 1114 </div> 393 1115 <?php 394 1116 } 395 /** 396 * Gets the status of cron functionality on the site by performing a test spawn. Cached for one hour when all is well. 397 * 398 * @param bool $cache Whether to use the cached result from previous calls. 399 * @return true|WP_Error Boolean true if the cron spawner is working as expected, or a WP_Error object if not. 400 */ 401 public function test_cron_spawn( $cache = true ) { 1117 1118 public function test_cron_spawn($cache = true) 1119 { 402 1120 global $wp_version; 403 1121 404 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {405 /* translators: 1: The name of the PHP constant that is set. */406 return new WP_Error( 'cron_info', sprintf( __( 'The %s constant is set to true. WP-Cron spawning is disabled.', 'pcrecruiter-extensions' ), 'DISABLE_WP_CRON' ));407 } 408 409 if ( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) {410 /* translators: 1: The name of the PHP constant that is set. */411 return new WP_Error( 'cron_info', sprintf( __( 'The %s constant is set to true.', 'pcrecruiter-extensions' ), 'ALTERNATE_WP_CRON' ));412 } 413 414 $cached_status = get_transient( 'wp-cron-test-ok');415 416 if ( $cache && $cached_status) {1122 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core constant check 1123 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) { 1124 return new WP_Error('cron_info', sprintf('The %s constant is set to true. WP-Cron spawning is disabled.', 'DISABLE_WP_CRON')); 1125 } 1126 1127 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core constant check 1128 if (defined('ALTERNATE_WP_CRON') && ALTERNATE_WP_CRON) { 1129 return new WP_Error('cron_info', sprintf('The %s constant is set to true. WP-Cron spawning is handled externally.', 'ALTERNATE_WP_CRON')); 1130 } 1131 1132 $cached_status = get_transient('wp-cron-test-ok'); 1133 1134 if ($cache && $cached_status) { 417 1135 return true; 418 1136 } 419 1137 420 $sslverify = version_compare( $wp_version, 4.0, '<' ); 421 $doing_wp_cron = sprintf( '%.22F', microtime( true ) ); 422 423 $cron_request = apply_filters( 'cron_request', array( 424 'url' => site_url( 'wp-cron.php?doing_wp_cron=' . $doing_wp_cron ), 425 'key' => $doing_wp_cron, 1138 $sslverify = version_compare($wp_version, 4.0, '<'); 1139 $doing_wp_cron = sprintf('%.22F', microtime(true)); 1140 1141 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using WordPress core hook 1142 $cron_request = apply_filters('cron_request', array( 1143 'url' => site_url('wp-cron.php?doing_wp_cron=' . $doing_wp_cron), 1144 'key' => $doing_wp_cron, 426 1145 'args' => array( 427 'timeout' => 3, 428 'blocking' => true, 429 'sslverify' => apply_filters( 'https_local_ssl_verify', $sslverify ), 1146 'timeout' => 3, 1147 'blocking' => true, 1148 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using WordPress core hook 1149 'sslverify' => apply_filters('https_local_ssl_verify', $sslverify), 430 1150 ), 431 ) );1151 )); 432 1152 433 1153 $cron_request['args']['blocking'] = true; 434 1154 435 $result = wp_remote_post( $cron_request['url'], $cron_request['args']);436 437 if ( is_wp_error( $result )) {1155 $result = wp_remote_post($cron_request['url'], $cron_request['args']); 1156 1157 if (is_wp_error($result)) { 438 1158 return $result; 439 } elseif ( wp_remote_retrieve_response_code( $result ) >= 300 ) { 440 return new WP_Error( 'unexpected_http_response_code', sprintf( 441 /* translators: 1: The HTTP response code. */ 442 __( 'Unexpected HTTP response code: %s', 'pcrecruiter-extensions' ), 443 intval( wp_remote_retrieve_response_code( $result ) ) 444 ) ); 1159 } elseif (wp_remote_retrieve_response_code($result) >= 300) { 1160 return new WP_Error('unexpected_http_response_code', sprintf( 1161 'Unexpected HTTP response code: %s', 1162 intval(wp_remote_retrieve_response_code($result)) 1163 )); 445 1164 } else { 446 set_transient( 'wp-cron-test-ok', 1, 3600);1165 set_transient('wp-cron-test-ok', 1, 3600); 447 1166 return true; 448 1167 } 449 450 } 451 /** 452 * Shows the status of cron functionality on the site. Only displays a message when there's a problem. 453 */ 454 public function show_cron_status() { 455 1168 } 1169 1170 public function show_cron_status() 1171 { 456 1172 $status = $this->test_cron_spawn(); 457 1173 458 if ( is_wp_error( $status )) {459 if ( 'cron_info' === $status->get_error_code()) {460 ?>1174 if (is_wp_error($status)) { 1175 if ('cron_info' === $status->get_error_code()) { 1176 ?> 461 1177 <div id="cron-status-notice" class="notice notice-info"> 462 <p><?php echo esc_html( $status->get_error_message()); ?></p>1178 <p><?php echo esc_html($status->get_error_message()); ?></p> 463 1179 </div> 464 <?php1180 <?php 465 1181 } else { 466 ?>1182 ?> 467 1183 <div id="cron-status-error" class="error"> 468 1184 <p> 469 1185 <?php 470 1186 printf( 471 /* translators: 1: Error message text. */ 472 esc_html__( 'There was a problem with your cron configuration. The cron events on your site may not work. The problem was: %s', 'pcrecruiter-extensions' ), 473 '<br><strong>' . esc_html( $status->get_error_message() ) . '</strong>' 1187 'There was a problem with your cron configuration. The cron events on your site may not work. The problem was: %s', 1188 '<br><strong>' . esc_html($status->get_error_message()) . '</strong>' 474 1189 ); 475 1190 ?> 476 1191 </p> 477 1192 </div> 478 <?php1193 <?php 479 1194 } 480 1195 } 481 1196 } 482 1197 483 /**484 * Register and add settings485 */486 487 488 /**489 * Sanitize each setting field as needed490 *491 * @param array $input Contains all settings fields as array keys492 */493 public function sanitize($input)494 {495 $new_input = array();496 497 // Activation should be a boolean498 if (isset($input['activation'])) {499 $new_input['activation'] = (bool)($input['activation']);500 }501 502 // Frequency should be one of the predefined options503 if (isset($input['frequency'])) {504 $allowed_frequencies = array('daily', 'hourly', 'twicedaily');505 $new_input['frequency'] = in_array($input['frequency'], $allowed_frequencies)506 ? $input['frequency']507 : 'daily'; // Default to daily if invalid508 }509 510 // ID number should be sanitized as text field511 if (isset($input['id_number'])) {512 $new_input['id_number'] = sanitize_text_field($input['id_number']);513 }514 515 // Standard fields should be sanitized as text516 if (isset($input['standard_fields'])) {517 $new_input['standard_fields'] = sanitize_text_field($input['standard_fields']);518 }519 520 // Custom fields should be sanitized as text521 if (isset($input['custom_fields'])) {522 $new_input['custom_fields'] = sanitize_text_field($input['custom_fields']);523 }524 525 if (isset($input['query'])) {526 // Expanded character set to handle all PCRecruiter query parameters safely527 $new_input['query'] = preg_replace('/[^\w\s%\(\)\=\-\'\"\.\_\+\!\*\,\;\:\[\]\{\}\<\>\?\/\&\|]+/', '', $input['query']);528 }529 530 // Mode should be either 'job' or 'apply'531 if (isset($input['mode'])) {532 $new_input['mode'] = in_array($input['mode'], array('job', 'apply'))533 ? $input['mode']534 : 'job'; // Default to job if invalid535 }536 537 // If activation is enabled, check the feed immediately538 if (isset($new_input['activation']) && $new_input['activation']) {539 // Schedule a one-time event to check the feed after settings are saved540 wp_schedule_single_event(time() + 2, 'pcr_check_feed_once');541 }542 543 return $new_input;544 }545 546 1198 public function page_init() 547 1199 { 548 549 1200 register_setting( 550 'pcr_ext_option_group', // Option group 551 'pcr_feed_options', // Option name 552 array( $this, 'sanitize' ) // Sanitize 553 ); 554 1201 'pcrecruiter_ext_option_group', 1202 'pcrecruiter_feed_options', 1203 array($this, 'sanitize') 1204 ); 1205 1206 // Register separate SEO settings 1207 register_setting( 1208 'pcrecruiter_ext_option_group', 1209 'pcrecruiter_seo_enable_overrides', 1210 array( 1211 'type' => 'boolean', 1212 'default' => false, 1213 'sanitize_callback' => array($this, 'sanitize_checkbox') 1214 ) 1215 ); 1216 register_setting( 1217 'pcrecruiter_ext_option_group', // ← Match the form 1218 'pcrecruiter_seo_use_pcr_logos', 1219 array( 1220 'type' => 'boolean', 1221 'default' => false, 1222 'sanitize_callback' => array($this, 'sanitize_checkbox') 1223 ) 1224 ); 555 1225 add_settings_section( 556 'setting_section_id', // ID 557 'PCRecruiter Feed Settings (Optional)', // Title 558 array( $this, 'print_section_info' ), // Callback 559 'pcr-ext-setting-admin' // Page 1226 'jobboard_section_id', 1227 'Job Board Sync Settings', 1228 array($this, 'print_jobboard_section_info'), 1229 'pcr-ext-setting-admin' 1230 ); 1231 1232 add_settings_field( 1233 'jobboard_security_key', 1234 'Job Board Sync Token', 1235 array($this, 'jobboard_security_key_callback'), 1236 'pcr-ext-setting-admin', 1237 'jobboard_section_id' 1238 ); 1239 1240 add_settings_field( 1241 'pagination_style', 1242 'Pagination Style', 1243 array($this, 'pagination_style_callback'), 1244 'pcr-ext-setting-admin', 1245 'jobboard_section_id' 1246 ); 1247 add_settings_field( 1248 'job_board_page', 1249 'Primary Job Board Page', 1250 array($this, 'job_board_page_callback'), 1251 'pcr-ext-setting-admin', 1252 'jobboard_section_id' 1253 ); 1254 add_settings_field( 1255 'job_404_page', 1256 'Job Not Found Page', 1257 array($this, 'job_404_page_callback'), 1258 'pcr-ext-setting-admin', 1259 'jobboard_section_id' 1260 ); 1261 add_settings_field( 1262 'expired_job_handling', 1263 'Expired Job Handling', 1264 array($this, 'expired_job_handling_callback'), 1265 'pcr-ext-setting-admin', 1266 'jobboard_section_id' 1267 ); 1268 1269 add_settings_field( 1270 'job_title_format', 1271 'Job Title & Image Tags', 1272 array($this, 'job_title_format_callback'), 1273 'pcr-ext-setting-admin', 1274 'jobboard_section_id' 1275 ); 1276 add_settings_field( 1277 'strip_inline_styles', 1278 'Strip Inline Styles', 1279 array($this, 'strip_inline_styles_callback'), 1280 'pcr-ext-setting-admin', 1281 'jobboard_section_id' 1282 ); 1283 add_settings_section( 1284 'setting_section_id', 1285 'RSS Feed Settings (Optional)', 1286 array($this, 'print_rss_section_info'), 1287 'pcr-ext-setting-admin' 560 1288 ); 561 1289 … … 563 1291 'activation', 564 1292 'Job Feed Enabled', 565 array( $this, 'activation_callback'),1293 array($this, 'activation_callback'), 566 1294 'pcr-ext-setting-admin', 567 1295 'setting_section_id' … … 571 1299 'frequency', 572 1300 'Frequency of Update', 573 array( $this, 'frequency_callback'),1301 array($this, 'frequency_callback'), 574 1302 'pcr-ext-setting-admin', 575 1303 'setting_section_id' … … 577 1305 578 1306 add_settings_field( 579 'id_number', // ID580 'PCR SessionID', // Title581 array( $this, 'id_number_callback' ), // Callback582 'pcr-ext-setting-admin', // Page583 'setting_section_id' // Section1307 'id_number', 1308 'PCR SessionID', 1309 array($this, 'id_number_callback'), 1310 'pcr-ext-setting-admin', 1311 'setting_section_id' 584 1312 ); 585 1313 586 1314 add_settings_field( 587 'standard_fields', // ID588 'Standard Fields', // Title589 array( $this, 'standard_fields_callback' ), // Callback590 'pcr-ext-setting-admin', // Page591 'setting_section_id' // Section1315 'standard_fields', 1316 'Standard Fields', 1317 array($this, 'standard_fields_callback'), 1318 'pcr-ext-setting-admin', 1319 'setting_section_id' 592 1320 ); 593 1321 594 1322 add_settings_field( 595 'custom_fields', // ID596 'Custom Fields', // Title597 array( $this, 'custom_fields_callback' ), // Callback598 'pcr-ext-setting-admin', // Page599 'setting_section_id' // Section1323 'custom_fields', 1324 'Custom Fields', 1325 array($this, 'custom_fields_callback'), 1326 'pcr-ext-setting-admin', 1327 'setting_section_id' 600 1328 ); 601 1329 602 1330 add_settings_field( 603 'query', // ID 604 'Query', // Title 605 array( $this, 'query_callback' ), // Callback 606 'pcr-ext-setting-admin', // Page 607 'setting_section_id' // Section 608 ); 609 1331 'query', 1332 'Query', 1333 array($this, 'query_callback'), 1334 'pcr-ext-setting-admin', 1335 'setting_section_id' 1336 ); 610 1337 add_settings_field( 611 'mode', // ID 612 'Mode', // Title 613 array( $this, 'mode_callback' ), // Callback 614 'pcr-ext-setting-admin', // Page 615 'setting_section_id' // Section 616 ); 617 } 618 619 620 /** 621 * Print the Section text 622 */ 623 public function print_section_info() 624 { 625 echo '<p style="font-size:1.25em;"><strong style="text-transform:uppercase">NOTE: These settings are NOT involved in typical PCR Job Board implementations.</strong></p><p style="font-size:1em;">When this optional function is enabled, the plugin will duplicate PCR\'s dynamic RSS feed as a static file at <a target="_blank" href="'. esc_url( site_url() ) .'/wp-content/uploads/pcrjobfeed.xml">'. esc_url( site_url() ) .'/wp-content/uploads/pcrjobfeed.xml</a>. You may use this data as a source for plugins and other third-party feed utilities.</p>'; 626 // Check to see if "Store Local Feed" is active. If it is, show the manual save button 627 if($this->options['activation'] ?? false){ 628 $filename = 'pcrjobfeed.xml'; 629 $fname = WP_CONTENT_DIR . "/uploads/".$filename; 630 if (file_exists($fname)) { 631 $d = gmdate("F d Y H:i:s", filectime($fname)); 632 echo "<em style=\"font-weight:bold\">" . esc_html( $filename ) . " last updated: " . esc_html( $d ) . " (UTC).</em>"; 633 } else { 634 echo "<i>File " . esc_html( $fname ) . " doesn't exist...</i>"; 635 } 1338 'mode', 1339 'Mode', 1340 array($this, 'mode_callback'), 1341 'pcr-ext-setting-admin', 1342 'setting_section_id' 1343 ); 1344 add_settings_field( 1345 'show_attribution', 1346 'Attribution Link', 1347 array($this, 'show_attribution_callback'), 1348 'pcr-ext-setting-admin', 1349 'jobboard_section_id' 1350 ); 1351 } 1352 1353 public function sanitize($input) 1354 { 1355 $existing_options = get_option('pcrecruiter_feed_options', array()); 1356 $new_input = $existing_options; 1357 1358 if (isset($input['pagination_style'])) { 1359 $new_input['pagination_style'] = sanitize_text_field($input['pagination_style']); 1360 $new_input['show_job_count'] = isset($input['show_job_count']) ? '1' : '0'; 1361 } 1362 1363 if (isset($input['jobboard_security_key'])) { 1364 $new_input['jobboard_security_key'] = sanitize_text_field($input['jobboard_security_key']); 1365 } 1366 1367 if (isset($input['job_404_page'])) { 1368 $new_input['job_404_page'] = sanitize_text_field($input['job_404_page']); 1369 } 1370 if (isset($input['expired_job_handling'])) { 1371 $new_input['expired_job_handling'] = in_array($input['expired_job_handling'], array('delete', 'keep')) 1372 ? $input['expired_job_handling'] 1373 : 'delete'; 1374 } 1375 if (isset($input['strip_inline_styles'])) { 1376 $new_input['strip_inline_styles'] = '1'; 1377 } else { 1378 $new_input['strip_inline_styles'] = '0'; 1379 } 1380 if (isset($input['job_title_fields'])) { 1381 $allowed_fields = array('job_title', 'city', 'state', 'country', 'position_id', 'job_type', 'company'); 1382 $new_input['job_title_fields'] = array(); 1383 1384 foreach ($allowed_fields as $field) { 1385 $new_input['job_title_fields'][$field] = isset($input['job_title_fields'][$field]) ? '1' : '0'; 1386 } 1387 1388 // Job title is always required 1389 $new_input['job_title_fields']['job_title'] = '1'; 1390 } 1391 1392 if (isset($input['frequency'])) { 1393 $new_input['activation'] = isset($input['activation']) ? '1' : ''; 1394 $new_input['frequency'] = sanitize_text_field($input['frequency']); 1395 } 1396 1397 if (isset($input['id_number'])) { 1398 $new_input['id_number'] = $input['id_number']; 1399 } 1400 1401 if (isset($input['standard_fields'])) { 1402 $new_input['standard_fields'] = sanitize_text_field($input['standard_fields']); 1403 } 1404 1405 if (isset($input['custom_fields'])) { 1406 $new_input['custom_fields'] = sanitize_text_field($input['custom_fields']); 1407 } 1408 1409 if (isset($input['query'])) { 1410 $new_input['query'] = $input['query']; 1411 } 1412 1413 if (isset($input['mode'])) { 1414 $new_input['mode'] = $input['mode']; 1415 } 1416 if (isset($input['pagination_style'])) { 1417 $new_input['pagination_style'] = sanitize_text_field($input['pagination_style']); 1418 $new_input['show_job_count'] = isset($input['show_job_count']) ? '1' : '0'; 1419 $new_input['show_attribution'] = isset($input['show_attribution']) ? '1' : '0'; 1420 } 1421 if (isset($input['job_board_page'])) { 1422 $url = esc_url_raw($input['job_board_page']); 1423 $new_input['job_board_page'] = $url; 1424 } 1425 // Handle pcrecruiter_seo_use_pcr_logos (separate option, not in feed_options array) 1426 if (isset($_POST['pcrecruiter_seo_use_pcr_logos'])) { 1427 update_option('pcrecruiter_seo_use_pcr_logos', '1'); 1428 } else { 1429 delete_option('pcrecruiter_seo_use_pcr_logos'); // or update_option(..., '0') 1430 } 1431 1432 // Handle pcrecruiter_seo_enable_overrides (separate option) 1433 if (isset($_POST['pcrecruiter_seo_enable_overrides'])) { 1434 update_option('pcrecruiter_seo_enable_overrides', true); 1435 } else { 1436 update_option('pcrecruiter_seo_enable_overrides', false); 1437 } 1438 return $new_input; 1439 } 1440 1441 public function print_jobboard_section_info() 1442 { 1443 echo '<div class="pcr-section-primary">'; 1444 echo '<p>This sync token connects PCRecruiter directly to WordPress, creating a "Jobs" custom post type and actively syncing job data and settings from the PCR Job Board configuration to your WordPress database.</p>'; 1445 echo '<p><strong>This requires admin-level access to the PCR database System menu.</strong> Once configured, job postings will automatically sync to WordPress. For more information, please contact a PCR support representative.</p>'; 1446 echo '</div>'; 1447 } 1448 1449 public function sanitize_checkbox($input) 1450 { 1451 return $input ? true : false; 1452 } 1453 1454 public function print_rss_section_info() 1455 { 1456 echo '<div class="pcr-section-secondary">'; 1457 echo '<p>When enabled, this feature will duplicate PCRecruiter\'s dynamic RSS feed as a static file at <a target="_blank" href="' . esc_attr(site_url()) . '/wp-content/uploads/pcrjobfeed.xml">' . esc_html(site_url()) . '/wp-content/uploads/pcrjobfeed.xml</a>. You may use this data as a source for plugins and other third-party feed utilities. <strong>The RSS function is NOT required for standard PCRecruiter Job Board use.</strong> Checking the "Job Feed Enabled" box below without proper values in the rest of this form may introduce errors into your website. Please <a target="_blank" href="https://help.pcrecruiter.com">contact PCRecruiter Support</a> for guidance if you wish to enable this feature.</p>'; 1458 1459 if ($this->options['activation'] ?? false) { 1460 $filename = 'pcrjobfeed.xml'; 1461 $fname = WP_CONTENT_DIR . "/uploads/" . $filename; 1462 if (file_exists($fname)) { 1463 $mtime = filemtime($fname); 1464 echo "<p><em style=\"font-weight:bold\">" . esc_html($filename) . " last updated: " . esc_html(gmdate("F d Y H:i:s", $mtime)) . " (UTC).</em></p>"; 1465 } else { 1466 echo "<p><em>File " . esc_html($fname) . " doesn't exist...</em></p>"; 1467 } 1468 } 1469 echo '</div>'; 1470 } 1471 1472 public function activation_callback() 1473 { 1474 $manual_update_url = wp_nonce_url( 1475 admin_url('admin-post.php?action=pcrecruiter_manual_feed_update'), 1476 'pcrecruiter_manual_feed_update' 1477 ); 1478 1479 printf( 1480 '<label><input id="activation" name="pcrecruiter_feed_options[activation]" type="checkbox" %s /> Enable scheduled job feed</label><br><br>', 1481 checked(!empty($this->options['activation']), true, false) 1482 ); 1483 1484 echo '<a class="button" href="' . esc_url($manual_update_url) . '">Update Feed Now</a>'; 1485 echo '<p class="description">Runs a one-time feed download immediately.</p>'; 1486 } 1487 1488 1489 public function frequency_callback() 1490 { 1491 $freq = $this->options['frequency'] ?? ''; // default empty 1492 $items = ['daily', 'hourly', 'twicedaily']; 1493 1494 echo "<select id='frequency' name='pcrecruiter_feed_options[frequency]'>"; 1495 foreach ($items as $item) { 1496 $selected = ($freq === $item) ? 'selected="selected"' : ''; 1497 echo "<option value='" . esc_attr($item) . "' " . esc_attr($selected) . ">" . esc_html($item) . "</option>"; 1498 } 1499 echo "</select>"; 1500 } 1501 1502 public function id_number_callback() 1503 { 1504 printf( 1505 '<input type="text" id="id_number" name="pcrecruiter_feed_options[id_number]" value="%s" size="60" />', 1506 isset($this->options['id_number']) ? esc_attr($this->options['id_number']) : '' 1507 ); 1508 } 1509 1510 public function standard_fields_callback() 1511 { 1512 printf( 1513 '<input type="text" id="standard_fields" name="pcrecruiter_feed_options[standard_fields]" value="%s" size="60" /><br /><span style="font-size:.8em;">Comma separated.</span>', 1514 isset($this->options['standard_fields']) ? esc_attr($this->options['standard_fields']) : '' 1515 ); 1516 } 1517 1518 public function custom_fields_callback() 1519 { 1520 printf( 1521 '<input type="text" id="custom_fields" name="pcrecruiter_feed_options[custom_fields]" value="%s" size="60" /><br /><span style="font-size:.8em;">Comma separated.</span>', 1522 isset($this->options['custom_fields']) ? esc_attr($this->options['custom_fields']) : '' 1523 ); 1524 } 1525 1526 public function query_callback() 1527 { 1528 printf( 1529 '<input type="text" id="query" name="pcrecruiter_feed_options[query]" value="%s" rows="4" size="60" />', 1530 isset($this->options['query']) ? esc_attr($this->options['query']) : '' 1531 ); 1532 } 1533 public function mode_callback() 1534 { 1535 $mode = $this->options['mode'] ?? 'job'; // default to job 1536 $check1 = $mode === 'job' ? 'checked' : ''; 1537 $check2 = $mode === 'apply' ? 'checked' : ''; 1538 1539 printf('<input type="radio" id="job" name="pcrecruiter_feed_options[mode]" value="job" %s /> Job Link<br />', esc_attr($check1)); 1540 printf('<input type="radio" id="apply" name="pcrecruiter_feed_options[mode]" value="apply" %s /> Apply Link<br />', esc_attr($check2)); 1541 } 1542 1543 public function pagination_style_callback() 1544 { 1545 $options = get_option('pcrecruiter_feed_options'); 1546 $style = isset($options['pagination_style']) ? $options['pagination_style'] : 'paged'; 1547 $show_count = isset($options['show_job_count']) ? $options['show_job_count'] : '1'; 1548 ?> 1549 <select id="pagination_style" name="pcrecruiter_feed_options[pagination_style]"> 1550 <option value="paged" <?php selected($style, 'paged'); ?>>Paged (Show Page Numbers)</option> 1551 <option value="simple" <?php selected($style, 'simple'); ?>>Simple (Next/Previous Only)</option> 1552 </select> 1553 <p class="description">Choose how pagination is displayed in job listings.</p> 1554 1555 <br><br> 1556 <label> 1557 <input type="checkbox" id="show_job_count" name="pcrecruiter_feed_options[show_job_count]" value="1" <?php checked($show_count, '1'); ?> /> 1558 Show job count (e.g., "Showing 1-25 of 150 jobs") 1559 </label> 1560 <p class="description">Display the result count above pagination controls.</p> 1561 <?php 1562 } 1563 public function show_attribution_callback() 1564 { 1565 $options = get_option('pcrecruiter_feed_options'); 1566 $show_attribution = isset($options['show_attribution']) ? $options['show_attribution'] : '1'; 1567 ?> 1568 <label> 1569 <input type="checkbox" id="show_attribution" name="pcrecruiter_feed_options[show_attribution]" value="1" <?php checked($show_attribution, '1'); ?> /> 1570 PCRecruiter Attribution 1571 </label> 1572 <p class="description">Give PCRecruiter some search engine love by displaying a small attribution link below job listings. Thank you!</p> 1573 <?php 1574 } 1575 public function job_board_page_callback() 1576 { 1577 $options = get_option('pcrecruiter_feed_options'); 1578 $job_board_page = isset($options['job_board_page']) ? esc_attr($options['job_board_page']) : ''; 1579 ?> 1580 <input type="text" id="job_board_page" name="pcrecruiter_feed_options[job_board_page]" value="<?php echo esc_attr($job_board_page); ?>" size="60" placeholder="Ex: <?php echo esc_attr(home_url('/careers/')); ?>" /> 1581 <p class="description">Enter the full URL of your primary job board page (where the <code>[PCRecruiter link="jobmanager"]</code> shortcode is). Visitors attempting to load the /job/ directory will be redirected to this URL.</p> 1582 <?php 1583 } 1584 1585 public function jobboard_security_key_callback() 1586 { 1587 $options = get_option('pcrecruiter_feed_options'); 1588 $key = isset($options['jobboard_security_key']) ? esc_textarea($options['jobboard_security_key']) : ''; 1589 ?> 1590 <textarea placeholder="Click 'Generate Key' and 'Save Changes' to create a new key" id="jobboard_security_key" name="pcrecruiter_feed_options[jobboard_security_key]" rows="5" cols="80"><?php echo esc_html($key); ?></textarea> 1591 <br> 1592 <button type="button" class="button" onclick="generateJobboardKey()">Generate Key</button> 1593 1594 <script type="text/javascript"> 1595 function generateJobboardKey() { 1596 const keyLength = 256; 1597 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 1598 const array = new Uint32Array(keyLength); 1599 window.crypto.getRandomValues(array); 1600 1601 let result = ''; 1602 for (let i = 0; i < keyLength; i++) { 1603 result += chars.charAt(array[i] % chars.length); 636 1604 } 637 } 638 639 /** 640 * Get the settings option array and print one of its values 641 */ 642 643 public function activation_callback() 644 { 645 printf( 646 '<input id="activation" name="pcr_feed_options[activation]" type="checkbox" %2$s />', 647 'activation', 648 checked( isset( $this->options['activation'] ), true, false ) 649 ); 650 } 651 652 public function frequency_callback() 653 { 654 $options = $this->options['frequency']; 655 $items = array("daily", "hourly", "twicedaily"); 656 echo "<select id='frequency' name='pcr_feed_options[frequency]'>"; 657 foreach($items as $item) { 658 $selected = ($this->options['frequency'] == $item) ? 'selected="selected"' : ''; 659 echo "<option value='" . esc_attr( $item ) . "' " . esc_attr( $selected ) . ">" . esc_html( $item ) . "</option>"; 660 } 661 echo "</select>"; 662 } 663 664 public function id_number_callback() 665 { 666 printf( 667 '<input type="text" id="id_number" name="pcr_feed_options[id_number]" value="%s" size="60" placeholder="Please contact PCR support for your unique session-id value." />', 668 isset( $this->options['id_number'] ) ? esc_attr( $this->options['id_number']) : '' 669 ); 670 } 671 672 public function standard_fields_callback() 673 { 674 printf( 675 '<input type="text" id="standard_fields" name="pcr_feed_options[standard_fields]" value="%s" size="60" /><br /><span style="font-size:.8em;">Comma separated.</span>', 676 isset( $this->options['standard_fields'] ) ? esc_attr( $this->options['standard_fields']) : '' 677 ); 678 } 679 680 public function custom_fields_callback() 681 { 682 683 printf( 684 '<input type="text" id="custom_fields" name="pcr_feed_options[custom_fields]" value="%s" size="60" /><br /><span style="font-size:.8em;">Comma separated. Be sure to replace any %%20 characters with spaces.</span>', 685 isset( $this->options['custom_fields'] ) ? esc_attr( $this->options['custom_fields']) : '' 686 ); 687 } 688 689 public function query_callback() 690 { 691 printf( 692 '<input type="text" id="query" name="pcr_feed_options[query]" value="%s" rows="4" size="60" />', 693 isset( $this->options['query'] ) ? esc_attr( $this->options['query']) : '' 694 ); 695 } 696 697 public function mode_callback() 698 { 699 if($this->options['mode'] == "job"){ 700 $check1 = "checked"; 701 $check2 = ""; 702 } else if($this->options['mode'] == "apply"){ 703 $check2 = "checked"; 704 $check1 = ""; 705 } else { 706 $check1 = "checked"; 707 $check2 = ""; 708 } 709 printf('<input type="radio" id="%s" name="pcr_feed_options[mode]" value="job" %s /> %s<br />', 710 esc_attr('job'), 711 esc_attr($check1), 712 esc_html('Job Link') 1605 1606 document.getElementById('jobboard_security_key').value = result; 1607 } 1608 </script> 1609 <?php 1610 } 1611 } 1612 1613 1614 function pcrecruiter_do_after_update($old, $new) 1615 { 1616 $pcrecruiter_feed_options = get_option('pcrecruiter_feed_options', array()); 1617 $current_frequency = $pcrecruiter_feed_options['frequency']; 1618 $activated = $pcrecruiter_feed_options['activation'] ?? ''; 1619 wp_clear_scheduled_hook('pcrecruiter_feed'); 1620 1621 if ($activated) { 1622 wp_schedule_event(time(), $current_frequency, 'pcrecruiter_feed'); 1623 pcrecruiter_feed_func(); 1624 } else { 1625 $feedlinkxml = WP_CONTENT_DIR . "/uploads/pcrjobfeed.xml"; 1626 $feedlinkjson = WP_CONTENT_DIR . "/uploads/pcrjobfeed.json"; 1627 1628 if (file_exists($feedlinkxml)) { 1629 wp_delete_file($feedlinkxml); 1630 } 1631 1632 if (file_exists($feedlinkjson)) { 1633 wp_delete_file($feedlinkjson); 1634 } 1635 } 1636 } 1637 add_action('update_option_pcrecruiter_feed_options', 'pcrecruiter_do_after_update', 10, 2); 1638 1639 function pcrecruiter_migrate_legacy_options() 1640 { 1641 $legacy = get_option('pcr_feed_options', null); 1642 $current = get_option('pcrecruiter_feed_options', null); 1643 1644 // Migrate whenever the legacy option exists and the new one is unset 1645 if (!is_null($legacy) && is_null($current)) { 1646 update_option('pcrecruiter_feed_options', $legacy); 1647 delete_option('pcr_feed_options'); // optional 1648 } 1649 } 1650 add_action('admin_init', 'pcrecruiter_migrate_legacy_options'); 1651 1652 function pcrecruiter_manual_feed_update_handler() 1653 { 1654 if (! current_user_can('manage_options')) { 1655 wp_die('You do not have permission to do this.', '', array('response' => 403)); 1656 } 1657 check_admin_referer('pcrecruiter_manual_feed_update'); 1658 1659 // Ensure defaults exist, then run the feed once 1660 pcrecruiter_checkop(); 1661 $error = null; 1662 $ok = pcrecruiter_feed_func(true, $error); 1663 1664 $redirect = wp_get_referer() ?: admin_url('options-general.php?page=pcr-ext-setting-admin'); 1665 $redirect = add_query_arg('pcr_feed_updated', $ok ? '1' : '0', $redirect); 1666 if (! $ok && $error) { 1667 $redirect = add_query_arg('pcr_feed_error', rawurlencode($error), $redirect); 1668 } 1669 wp_safe_redirect($redirect); 1670 exit; 1671 } 1672 add_action('admin_post_pcrecruiter_manual_feed_update', 'pcrecruiter_manual_feed_update_handler'); 1673 1674 function pcrecruiter_manual_feed_update_notice() 1675 { 1676 $updated_flag = filter_input(INPUT_GET, 'pcr_feed_updated', FILTER_SANITIZE_FULL_SPECIAL_CHARS); 1677 1678 if ($updated_flag === '1') { 1679 echo '<div class="notice notice-success is-dismissible"><p>Job feed updated successfully.</p></div>'; 1680 } elseif ($updated_flag === '0') { 1681 $msg = 'Feed update failed.'; 1682 $raw_error = filter_input(INPUT_GET, 'pcr_feed_error', FILTER_UNSAFE_RAW); 1683 if ($raw_error !== null) { 1684 $decoded = rawurldecode(wp_unslash($raw_error)); 1685 $sanitized = sanitize_text_field($decoded); 1686 $msg = mb_substr($sanitized, 0, 300); 1687 } 1688 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($msg) . '</p></div>'; 1689 } 1690 } 1691 add_action('admin_notices', 'pcrecruiter_manual_feed_update_notice'); 1692 1693 if (is_admin()) { 1694 $pcrecruiter_settings_page = new PCRecruiter_Extensions_Settings(); 1695 } 1696 1697 function pcrecruiter_settings_scripts() 1698 { 1699 wp_enqueue_media(); 1700 1701 wp_register_script( 1702 'pcr-settings-scripts', 1703 plugin_dir_url(__FILE__) . 'assets/js/pcr-admin.js', 1704 array('jquery'), 1705 '2.0.0', 1706 true 713 1707 ); 714 printf('<input type="radio" id="%s" name="pcr_feed_options[mode]" value="apply" %s /> %s<br />', 715 esc_attr('apply'), 716 esc_attr($check2), 717 esc_html('Apply Link') 1708 wp_enqueue_script('pcr-settings-scripts'); 1709 1710 wp_enqueue_style( 1711 'pcr-settings-style', 1712 plugin_dir_url(__FILE__) . 'assets/css/pcr-settings.css', 1713 array(), 1714 '1.0.0' 718 1715 ); 719 } 720 } 721 722 723 724 /* Begin update cron schedule if frequency changes */ 725 726 function do_after_update($old, $new) { 727 // Do the stuff here 728 $pcr_feed_options = get_option('pcr_feed_options', array()); 729 $current_frequency = $pcr_feed_options['frequency']; 730 $activated = $pcr_feed_options['activation'] ?? ''; 731 wp_clear_scheduled_hook('pcr_feed'); 732 if($activated){ 733 wp_schedule_event( time(), $current_frequency, 'pcr_feed' ); 734 pcr_feed_func(); 735 } else { 736 $feedlinkxml = WP_CONTENT_DIR . "/uploads/pcrjobfeed.xml"; 737 $feedlinkjson = WP_CONTENT_DIR . "/uploads/pcrjobfeed.json"; 738 if (file_exists($feedlinkxml)) { 739 unlink(WP_CONTENT_DIR . "/uploads/pcrjobfeed.xml"); 740 } else {} 741 if (file_exists($feedlinkjson)) { 742 unlink(WP_CONTENT_DIR . "/uploads/pcrjobfeed.json"); 743 } else {} 744 } 745 } 746 add_action('update_option_pcr_feed_options','do_after_update', 10, 2); 747 /* End update cron schedule if frequency changes */ 748 749 750 if( is_admin() ) 751 $my_settings_page = new PcrSettingsPage(); 752 753 // JS for Admin Panel 754 function pcr_settings_scripts(){ 755 wp_enqueue_media(); 756 wp_register_script('pcr-settings-scripts',plugin_dir_url( __DIR__ ) .'pcrecruiter-extensions/assets/js/pcr-admin.js', array('jquery'), '1.0.0', true); 757 wp_enqueue_script('pcr-settings-scripts'); 758 } 759 add_action( 'admin_enqueue_scripts', 'pcr_settings_scripts' ); 1716 } 1717 add_action('admin_enqueue_scripts', 'pcrecruiter_settings_scripts'); -
pcrecruiter-extensions/trunk/assets/icon.svg
r1173648 r3420599 1 <?xml version="1.0" encoding="utf-8"?> 2 <!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 4 <svg version="1.1" id="Layer_6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 5 width="95px" height="95px" viewBox="0 0 95 95" enable-background="new 0 0 95 95" xml:space="preserve"> 6 <g> 7 <polygon fill="#19254C" points="6.712,71.048 47.5,94.598 47.5,47.499 "/> 8 <polygon fill="#041726" points="47.5,0.402 47.5,47.5 88.288,23.951 "/> 9 <polygon fill="#19488F" points="6.712,71.048 47.5,47.499 6.712,23.951 "/> 10 <polygon fill="#1A623C" points="88.288,71.049 47.5,47.5 88.288,23.951 "/> 11 <polygon fill="#73C3C8" points="47.5,0.402 6.713,23.951 47.5,47.5 "/> 12 <polygon fill="#3B9C37" points="47.5,94.598 88.288,71.048 47.5,47.499 "/> 13 </g> 1 <?xml version="1.0" encoding="UTF-8"?> 2 <svg id="a" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 640 640"> 3 <!-- Generator: Adobe Illustrator 30.0.0, SVG Export Plug-In . SVG Version: 2.1.1 Build 123) --> 4 <defs> 5 <style> 6 .st0 { 7 fill: #a460da; 8 } 9 10 .st1 { 11 fill: #21a777; 12 } 13 14 .st2 { 15 fill: #308add; 16 } 17 18 .st3 { 19 fill: #d9beef; 20 } 21 22 .st4 { 23 fill: #3660cd; 24 } 25 26 .st5 { 27 fill: #79d2b2; 28 } 29 </style> 30 </defs> 31 <polygon class="st2" points="320 640 597.1 480 320 320 320 640"/> 32 <polygon class="st3" points="320 0 42.9 160 320 320 320 0"/> 33 <polygon class="st1" points="597.1 480 320 320 597.1 160 597.1 480"/> 34 <polygon class="st0" points="42.9 480 320 320 42.9 160 42.9 480"/> 35 <polygon class="st5" points="320 0 320 320 597.1 160 320 0"/> 36 <polygon class="st4" points="42.9 480 320 640 320 320 42.9 480"/> 14 37 </svg> -
pcrecruiter-extensions/trunk/assets/js/pcr-admin.js
r2206149 r3420599 4 4 if(id_length < 40 && jQuery('#activation').prop('checked')){ 5 5 jQuery('#id_number').css('border-color','red'); 6 jQuery('#activation').prop('checked',false) 6 jQuery('#activation').prop('checked',false); 7 7 return false; 8 8 } else { -
pcrecruiter-extensions/trunk/readme.txt
r3400600 r3420599 1 1 === PCRecruiter Extensions === 2 Contributors: arothman, mstdev3 Tags: Recruiting, Staffing, Applicant Tracking4 Requires at least: 3.05 Tested up to: 6. 86 Stable tag: 1.4.37 7 License: GPLv2 or later 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html9 Text Domain: pcrecruiter-extensions 2 Contributors: Main Sequence Technology, Inc. 3 Tags: recruiting, staffing, applicant tracking, job board, job posting 4 Requires at least: 5.6 5 Tested up to: 6.9 6 Requires PHP: 7.4 7 Stable tag: 2.0 8 License: GPLv3 or later 9 License URI: https://www.gnu.org/licenses/gpl-3.0.html 10 10 11 PCR Job Board iframe embed, PCR Custom Form scripts, and PCR RSS download handler. Note: This plugin does not interact with the WordPress database.11 Integrates your WordPress site with PCRecruiter (PCR) to embed job boards or sync live job postings as native WordPress content. 12 12 13 == Description == 13 14 14 == Job Board Installation == 15 PCRecruiter Extensions provides two primary integration methods. The setup is typically done with the direct involvement of a PCR consultant; it is advised that you contact [email protected] before proceeding. 15 16 16 IMPORTANT!!! You do NOT need to touch the SETTINGS panel for standard PCRecruiter Job Board installations. 17 **1. Full WordPress Job Sync** 18 Sync active jobs from PCRecruiter into WordPress as native custom post types (job). 19 Includes keyword search, radius search, structured data (JobPosting), social sharing, expired-job handling, and support for WordPress native styling and control. 17 20 18 1. Click 'Add New' from the 'Plugins' menu in your WordPress admin panel. 19 2. Use the 'Upload Plugin' option and browse to the PCRecruiter-Extensions.zip file. 20 3. Activate the plugin. 21 4. Edit the page where you want to display the PCRecruiter content and paste in the following: 21 Additional Features: 22 * Block bindings for WordPress 6.5+ 23 * SEO-friendly URLs and optional job title customization 24 * Smart caching for improved performance 25 * Internal-only job board support 26 * Optional inline-style stripping for consistent site design 27 * Accessibility-friendly pagination 28 * Backward compatibility with legacy PCRecruiter usage 22 29 23 [PCRecruiter link=""] 30 **2. Simple iFrame Embed** 31 Embed the hosted PCRecruiter job board using a simple shortcode. Ideal for quick and simple installs or sites that prefer PCR-rendered pages, or sites where SEO and link friendliness are of less importance. 24 32 25 Place the shortcode content provided to you by Main Sequence between the quotes. If the URL does not begin with 'http', the plugin will assume you intend to load a URL from the PCRecruiter ASP hosting system and will prepend the correct URL.33 Also: PCRecruiter job XML/JSON feed downloading for integrations or imports. 26 34 27 Optional parameters: 35 Full documentation: 36 <a href="https://learning.pcrecruiter.net/site/docs/wordpress/">https://learning.pcrecruiter.net/site/docs/wordpress/</a> 28 37 29 * initialheight="" (defaults to 640 pixels if omitted) 30 * background="" (defaults to transparent if omitted) 31 * form="" (insert the 15-digit ID of a custom form) 38 Support: 39 [email protected] 32 40 33 == XML Feed Setup==41 == Installation == 34 42 35 From the Settings > PCRecruiter Extensions panel, you may configure the plugin to duplicate a dynamic RSS job feed from PCRecruiter to a static copy on your WordPress server. This may be advantageous for running RSS-based display widgets, feed-based distribution services, or other functions that require a static XML/RSS feed. 43 1. Upload the plugin ZIP via Plugins → Add New → Upload Plugin, or install from WordPress.org 44 2. Activate the plugin. 36 45 37 IMPORTANT!!! You do NOT need to touch the Settings panel to use this plugin for standard job board setups. Enabling the feed without correct settings values may cause errors or break your website. Please contact a Main Sequence Technology support representative for the settings required for use of this function. 46 == Quick Start == 38 47 39 * Job Feed Enabled: Checking this box activates the feed. Unchecking will deactivate and delete the XML file. 48 Iframe Job Board: 40 49 41 * Frequency of Update: The feed can be set to refresh daily, hourly, or twice daily. 50 No plugin settings are required for iframe installations! Just insert the shortcode into your jobs page: 42 51 43 * PCR SessionID: This encoded string identifies which PCRecruiter database to load the content from. Your support contact will provide the appropriate value for this field. 52 [PCRecruiter link="yourdatabase.yourprofile"] 44 53 45 * Standard Fields: The feed will contain the job link, date entered, title, and description. To include additional fields, enter them in comma-separated form in this box. The list of values that are accepted can be found in the API documentation at https://www.pcrecruiter.net/apidocs_v2/#!/positions/GetPosition_get_0.54 Contact your support representative for your shortcode. 46 55 47 * Custom Fields: Custom fields can be included in comma-separated form as well. 56 Full WordPress Job Sync: 57 1. Create a page and add: 58 [PCRecruiter link="jobmanager"] 59 2. Go to Settings → PCRecruiter Extensions → Job Board Sync. 60 3. Generate a Sync Token and enter it into the PCR “WordPress Sync Settings” panel. 48 61 49 * Query: By default, the feed will contain jobs that are set to Available/Open status, have the Show On Web field set to "Show", and have a Number of Openings that is 1 or greater. Contact Main Sequence Technology for assistance if alternate queries are needed. 62 == Features == 50 63 51 * Mode: This setting dictates whether the job links in the feed point to the job details page or to the apply screen for the job. The 'apply' mode would be useful in scenarios where the candidate will have already seen the description and should be directed straight to the first step of self-entry. 64 * Full job sync via secure token authentication 65 * Jobs stored as native custom posts (job) 66 * Keyword and radius search 67 * Structured data (schema.org/JobPosting) 68 * Internal-only job view support 69 * Social sharing widget 70 * Accessible pagination 71 * Expired-job handling (delete or keep visible) 72 * Customizable SEO job title formats 73 * Optional inline-style removal 74 * Automatic legacy URL redirection (?recordid= to clean URLs) 75 * Yoast SEO schema suppression to prevent duplication 76 * Block bindings for dynamic Site Editor templates 77 * Static XML/JSON feed mirroring (optional) 78 * Cron-based automatic feed updates 79 * Deactivation cleanup with keep/delete options 80 * Secure coding practices: sanitization, escaping, nonces, prepared queries 81 82 == Example Shortcodes == 83 84 Iframe Job Board: 85 [PCRecruiter link="my%20data.mycompany"] 86 87 Full Sync Job Board: 88 [PCRecruiter link="jobmanager"] 89 90 Optional filtering: 91 [PCRecruiter link="jobmanager" jobcategory="Engineering"] 92 93 Internal Job Board: 94 [PCRecruiter link="internaljobmanager"] 95 96 == Frequently Asked Questions == 97 98 = Which method should I use: sync or iframe? = 99 Use full sync for SEO-friendly, WordPress-native jobs, structured data, and customization. This method requires advanced skills and should be done with the help of an experienced WordPress developer and/or PCRecruiter support. 100 Use the simple iframe method for quick installation and no-code rendering. 101 102 = Do I need to edit any settings when using the iframe job board mode? = 103 No. The iframe mode works with only the shortcode. This method doesn't interact with the WordPress database or software. 104 105 = Can I run multiple full-sync job boards on different pages? = 106 Yes. Use the jobcategory parameter to filter the listings by "job_category" Position custom field. 107 108 = Can I combine multiple databases into a single job board? = 109 Yes, when using the full-sync multiple databases can be fed to the same WordPress site. With the simple iframe method, the search/display can only show one database's jobs at a time. 110 111 = How are expired jobs handled? = 112 Typically, jobs that are no longer open are deleted automatically. You may choose to keep expired jobs visible with the full-sync mode - these will display a “Filled” badge and will have no apply button. 113 114 = Can I customize how job titles display in browser tabs? = 115 Yes, with the full-sync you may configure this in Settings → PCRecruiter Extensions → Job Title Format. 116 117 = Does the plugin work with Google Jobs? = 118 Yes. The plugin supports the required Schema for Google's jobs results. It will suppress Yoast Schema automatically if this feature is configured. 119 120 = Where can I find the full setup guide? = 121 All documentation is online at: 122 <a href="https://learning.pcrecruiter.net/site/docs/wordpress/">https://learning.pcrecruiter.net/site/docs/wordpress/</a> 123 124 == Changelog == 125 126 = 2.0.0 - 2024-12-10 = 127 * Initial public release of full sync mode 128 * Added block bindings support 129 * Added pagination and job count options 130 * Added schema utility 131 * Added expired job handling 132 * Added social sharing widget 133 * Added customizable title formats 134 * Added legacy URL redirect support 135 * Added feed update button and improved feed options 136 * Added deactivation cleanup wizard 137 * Security and sanitization improvements 138 * Caching improvements and timezone fixes 139 140 = 1.x = 141 * Simple iframe and RSS/JSON feed functionality
Note: See TracChangeset
for help on using the changeset viewer.