Plugin Directory

Changeset 3431406


Ignore:
Timestamp:
01/03/2026 03:40:50 AM (2 weeks ago)
Author:
arothman
Message:

2.0.5 functionality

Location:
pcrecruiter-extensions/trunk
Files:
8 edited

Legend:

Unmodified
Added
Removed
  • pcrecruiter-extensions/trunk/PCRecruiter-Extensions.php

    r3423933 r3431406  
    55 * Plugin URI: https://www.pcrecruiter.net
    66 * Description: Integrates PCRecruiter job boards with WordPress via iframe embed or full job sync with /job/ custom post type.
    7  * Version: 2.0.4
     7 * Version: 2.0.5
    88 * Requires at least: 5.6
    99 * Requires PHP: 7.4
     
    1919 * phpcs:set WordPress.WP.I18n text_domain[] pcrecruiter-extensions
    2020 */
    21 
     21if ( ! defined( 'ABSPATH' ) ) {
     22    exit;
     23}
    2224// Public endpoint for PCRecruiter custom forms & job sync.
    2325// Form is submitted from an external domain - WordPress nonce cannot be added.
     
    306308    $pcrecruiter_has_shortcode = (strpos($job_board_page, $url) !== false);
    307309
    308     if ($pcrecruiter_has_shortcode && $_SERVER['REQUEST_METHOD'] === 'POST') {
     310    if ($pcrecruiter_has_shortcode && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
    309311        $rawData = file_get_contents('php://input');
    310312        $data = json_decode($rawData, true);
     
    445447    $is_jobmanager_page = false;
    446448    $is_job_post = is_singular('job');
     449    $is_apply_frame = $is_job_post && isset($_GET['apply']);
    447450
    448451    if ($using_full_sync) {
     
    495498    // Register all scripts/styles
    496499    wp_register_script('pcr-iframe', 'https://www2.pcrecruiter.net/pcrimg/inc/pcrframehost.js', false, PCRECRUITER_EXTENSIONS_VERSION, false);
    497     wp_register_script('pcr-jobboard-js', 'https://www2.pcrecruiter.net/pcrimg/js/wp_jobboard.js', false, PCRECRUITER_EXTENSIONS_VERSION, false);
    498     wp_register_script('pcr-frontend', plugin_dir_url(__FILE__) . 'assets/js/pcr-frontend.js', array(), PCRECRUITER_EXTENSIONS_VERSION, true);
     500    wp_register_script('pcr-frontend', plugin_dir_url(__FILE__) . 'assets/js/pcr-frontend.js', array(), PCRECRUITER_EXTENSIONS_VERSION, false);
    499501    wp_register_style('pcr-job-board', plugin_dir_url(__FILE__) . 'assets/css/pcr-job-board.css', array(), PCRECRUITER_EXTENSIONS_VERSION);
    500502
     
    505507
    506508        // Load jobmanager-specific assets on job board page(s) and single job posts
    507         if ($is_jobmanager_page || $is_job_post) {
    508             wp_enqueue_script('pcr-jobboard-js');
     509        if ($is_jobmanager_page || $is_job_post || $is_apply_frame) {
    509510            wp_enqueue_script('pcr-frontend');
    510511            wp_enqueue_style('pcr-job-board');
     
    516517            const ref = '$hostreferer';
    517518            if(ref.length > 0 && document.location.host?.indexOf(ref) === -1) {
    518                 localStorage['pcrecruiter_referer'] = ref;
     519                localStorage['pcr_referer'] = ref;
    519520            }
    520521           
     
    767768        }
    768769
    769         // Enqueue jobboard script for appendSrcToFrame() function
    770         wp_enqueue_script('pcr-jobboard-js');
     770        // Note: appendSrcToFrame() is now provided by pcr-frontend.js when in Full Sync mode
     771        // For iframe/passthrough mode, this function may not be available but is also not needed
    771772
    772773        $version = pcrecruiter_get_plugin_version();
     
    781782 */
    782783add_action('parse_request', 'pcrecruiter_handle_jobshare_redirect', 1);
    783 
    784784function pcrecruiter_handle_jobshare_redirect($wp)
    785785{
     
    788788    }
    789789
    790     $query_string = wp_unslash($_SERVER['QUERY_STRING']);
     790    $query_string = sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) );
    791791
    792792    // Check if starts with JOBSHARE
     
    820820    if ($current_path && $job_board_path && rtrim($current_path, '/') === rtrim($job_board_path, '/')) {
    821821        $redirect_url = 'https://host.pcrecruiter.net/pcrbin/jobboard.aspx?JOBSHARE' . $jobshare_token;
    822         wp_redirect($redirect_url, 301);
     822        $redirect_url = esc_url_raw($redirect_url);
     823
     824        // Whitelist PCRecruiter host for this redirect
     825        add_filter('allowed_redirect_hosts', function ($hosts) {
     826            $hosts[] = 'host.pcrecruiter.net';
     827            return $hosts;
     828        });
     829
     830        wp_safe_redirect($redirect_url, 301);
    823831        exit;
    824832    }
     
    16471655        }
    16481656        if (isset($_POST['pcrecruiter_job_default_og_image'])) {
    1649             update_option('pcrecruiter_job_default_og_image', esc_url_raw($_POST['pcrecruiter_job_default_og_image']));
     1657            update_option('pcrecruiter_job_default_og_image', esc_url_raw( wp_unslash( $_POST['pcrecruiter_job_default_og_image'] ) ));
    16501658        }
    16511659        // Handle pcrecruiter_seo_enable_overrides (separate option)
     
    18111819            $registered_by = get_post_type_object('job');
    18121820            if ($registered_by && !isset($registered_by->cap->edit_posts)) {
    1813                 echo '<p class="description" style="color: #d63638;">⚠️ Warning: Another plugin has registered the "job" post type. Full sync may not work correctly.</p>';
     1821                echo '<p class="description" style="color: #d63638;">⚠️ Warning: Another plugin has registered the "job" post type. Full sync may not work correctly.</p>';
    18141822            }
    18151823        } ?>
  • pcrecruiter-extensions/trunk/assets/js/pcr-frontend.js

    r3423324 r3431406  
    11/**
    22 * PCRecruiter Extensions - Frontend JavaScript
    3  * Handles pagination and apply button interactions
     3 * Handles search form, pagination, sorting, radius search, and apply button interactions
    44 */
    55
     
    1212  document.addEventListener("DOMContentLoaded", function () {
    1313    initSearchForm();
     14    initClearButton();
     15    initRadiusDropdown();
    1416  });
    1517
     
    5860
    5961  /**
     62   * Initialize clear button handler
     63   */
     64  function initClearButton() {
     65    const clearButton = document.getElementById("clearButton");
     66    if (clearButton) {
     67      clearButton.addEventListener("click", function (e) {
     68        e.preventDefault();
     69        const resultsPage = document.getElementById("resultspage");
     70        if (resultsPage) {
     71          resultsPage.value = 1;
     72        }
     73        // Redirect to clean URL (same page without query params)
     74        window.location.href = window.location.pathname;
     75      });
     76    }
     77  }
     78
     79  /**
     80   * Initialize radius dropdown autocomplete UI
     81   */
     82  function initRadiusDropdown() {
     83    const input = document.getElementById("Positions---Radius");
     84    if (!input) return;
     85
     86    const panel = document.getElementById("radius-dropdown-panel");
     87    const latLongInput = document.getElementById("Position---LatLong");
     88    if (!panel || !latLongInput) return;
     89
     90    let language = input.getAttribute("data-language") || "US";
     91
     92    // Create spinner
     93    const spinner = document.createElement("span");
     94    spinner.className = "loading-spinner";
     95    spinner.style.display = "none";
     96    input.parentNode.insertBefore(spinner, input.nextSibling);
     97
     98    let debounceTimer;
     99
     100    // Input handler with debounce
     101    input.addEventListener("input", function () {
     102      clearTimeout(debounceTimer);
     103      debounceTimer = setTimeout(function () {
     104        radiusDropdownSearch(input, panel, latLongInput, spinner, language);
     105      }, 300);
     106    });
     107
     108    // Down arrow to focus first result
     109    input.addEventListener("keydown", function (e) {
     110      if (e.key === "ArrowDown" || e.which === 40 || e.keyCode === 40) {
     111        const firstButton = panel.querySelector(".radius-button");
     112        if (firstButton) {
     113          firstButton.focus();
     114          e.stopPropagation();
     115          e.preventDefault();
     116        }
     117      }
     118    });
     119
     120    // Clear lat/long if input is cleared
     121    input.addEventListener("blur", function () {
     122      if (!input.value.trim()) {
     123        latLongInput.value = "";
     124      }
     125    });
     126
     127    // Close panel when clicking outside
     128    document.addEventListener("click", function (e) {
     129      if (
     130        panel.style.display === "block" &&
     131        !panel.contains(e.target) &&
     132        e.target !== input
     133      ) {
     134        panel.style.display = "none";
     135      }
     136    });
     137  }
     138
     139  /**
     140   * Perform radius dropdown search
     141   */
     142  function radiusDropdownSearch(input, panel, latLongInput, spinner, language) {
     143    const query = input.value.trim();
     144    if (!query) {
     145      panel.style.display = "none";
     146      return;
     147    }
     148
     149    // Setup panel UI
     150    panel.style.position = "absolute";
     151    panel.style.display = "block";
     152    panel.style.height = "160px";
     153    panel.style.width = "100%";
     154    panel.style.backgroundColor = "#fff";
     155    panel.style.border = "1px solid #ccc";
     156    panel.style.overflowY = "auto";
     157    panel.innerHTML = "";
     158    spinner.style.display = "inline-block";
     159
     160    const url =
     161      "https://maps.pcrecruiter.net/rest/uiapi/geolocations/quicksearch?location=" +
     162      encodeURIComponent(query) +
     163      "&language=" +
     164      encodeURIComponent(language) +
     165      "&rows=50";
     166
     167    fetch(url)
     168      .then(function (response) {
     169        return response.json();
     170      })
     171      .then(function (data) {
     172        spinner.style.display = "none";
     173        panel.innerHTML = "";
     174
     175        if (data && data.length > 0) {
     176          data.forEach(function (location) {
     177            var name = location.name;
     178            var region = location.region;
     179            var country = location.country;
     180            var latitude = location.latitude;
     181            var longitude = location.longitude;
     182            var zip = location.zip;
     183
     184            var displayText =
     185              name +
     186              ", " +
     187              region.toUpperCase() +
     188              ", " +
     189              country +
     190              (zip ? " " + zip : "");
     191
     192            var item = document.createElement("div");
     193            item.innerHTML =
     194              '<button class="radius-button" type="button">' +
     195              displayText +
     196              "</button>";
     197            item.style.padding = "5px";
     198            item.style.cursor = "pointer";
     199
     200            item.addEventListener("click", function () {
     201              input.value = displayText;
     202              latLongInput.value = latitude + "," + longitude;
     203              panel.style.display = "none";
     204            });
     205
     206            panel.appendChild(item);
     207          });
     208        } else {
     209          panel.innerHTML = '<div style="padding:5px;">No results found</div>';
     210        }
     211      })
     212      .catch(function (error) {
     213        spinner.style.display = "none";
     214        panel.innerHTML =
     215          '<div style="padding:5px;color:red;">Error: ' +
     216          error.message +
     217          "</div>";
     218      });
     219  }
     220
     221  /**
    60222   * Submit search form via GET redirect
    61223   */
     
    81243    const formData = new FormData(searchForm);
    82244
     245    // Check if radius search criteria have changed - if so, reset sortorigin
     246    // so the auto-Distance sort will be applied
     247    const currentParams = new URL(window.location.href).searchParams;
     248    const currentLatLong = currentParams.get("Position---LatLong") || "";
     249    const newLatLong = formData.get("Position---LatLong") || "";
     250    const currentRadius = currentParams.get("radius-distance") || "";
     251    const newRadius = formData.get("radius-distance") || "";
     252
     253    let resetSortOrigin = false;
     254    if (newLatLong && (newLatLong !== currentLatLong || newRadius !== currentRadius)) {
     255      // Radius criteria changed - reset to allow auto-Distance sort
     256      resetSortOrigin = true;
     257    }
     258
    83259    url.search = "";
    84260
    85261    for (let [key, value] of formData.entries()) {
    86262      if (value !== "" && key !== "action") {
    87         url.searchParams.append(key, value);
     263        // Override sortorigin if radius criteria changed
     264        if (key === "sortorigin" && resetSortOrigin) {
     265          url.searchParams.append(key, "default");
     266        } else {
     267          url.searchParams.append(key, value);
     268        }
    88269      }
    89270    }
     
    100281  /**
    101282   * Delegated click handler for dynamic elements
    102    */
    103   document.addEventListener("click", function (e) {
    104     // Handle pagination button clicks
    105     if (e.target.classList.contains("btn-pagination")) {
    106       e.preventDefault();
    107       const targetPage = e.target.getAttribute("data-page");
    108       if (targetPage) {
     283   * Using capture phase (true) to intercept before other handlers
     284   */
     285  document.addEventListener(
     286    "click",
     287    function (e) {
     288      // Handle sort links
     289      const sortLink = e.target.closest(".customsortfield");
     290      if (sortLink) {
     291        e.preventDefault();
     292        e.stopPropagation();
     293        e.stopImmediatePropagation();
     294
     295        const sortName = sortLink.getAttribute("data-sortname") || "";
     296        const sortDirRaw = sortLink.getAttribute("data-sortdirection") || "";
     297        const sortDirDefault = sortDirRaw ? sortDirRaw.toUpperCase() : "ASC";
     298
     299        if (!sortName) {
     300          return;
     301        }
     302
     303        const searchForm = document.getElementById("searchForm");
     304        const sortfieldInput = searchForm
     305          ? searchForm.querySelector('input[name="sortfield"]')
     306          : document.querySelector('input[name="sortfield"]');
     307        const sortoriginInput = searchForm
     308          ? searchForm.querySelector('input[name="sortorigin"]')
     309          : document.querySelector('input[name="sortorigin"]');
     310
     311        // Get current sort from hidden input
     312        let currentSort = "";
     313        if (sortfieldInput && sortfieldInput.value) {
     314          currentSort = sortfieldInput.value;
     315        }
     316
     317        // If still empty, check URL params
     318        if (!currentSort) {
     319          const urlParams = new URL(window.location.href).searchParams;
     320          currentSort = urlParams.get("sortfield") || "";
     321        }
     322
     323        // Determine next direction
     324        let nextDir = sortDirDefault;
     325        if (currentSort) {
     326          const parts = currentSort.trim().split(/\s+/);
     327          const currentField = parts[0] || "";
     328          const currentDir = (parts[1] || "").toUpperCase();
     329          if (
     330            currentField === sortName &&
     331            (currentDir === "ASC" || currentDir === "DESC")
     332          ) {
     333            nextDir = currentDir === "ASC" ? "DESC" : "ASC";
     334          }
     335        }
     336
     337        if (sortfieldInput) {
     338          sortfieldInput.value = sortName + (nextDir ? " " + nextDir : "");
     339        }
     340
     341        // Mark this as a user-initiated sort change
     342        if (sortoriginInput) {
     343          sortoriginInput.value = "user";
     344        }
     345
     346        if (searchForm) {
     347          submitSearchForm(searchForm);
     348        } else {
     349          const url = new URL(window.location.href);
     350          url.searchParams.set(
     351            "sortfield",
     352            sortName + (nextDir ? " " + nextDir : "")
     353          );
     354          url.searchParams.set("sortorigin", "user");
     355          url.searchParams.set("search", "1");
     356          if (!url.searchParams.has("pg")) {
     357            url.searchParams.set("pg", "1");
     358          }
     359          window.location.href = url.toString();
     360        }
     361        return;
     362      }
     363
     364      // Handle pagination button clicks
     365      if (e.target.classList.contains("btn-pagination")) {
     366        e.preventDefault();
     367        const targetPage = e.target.getAttribute("data-page");
     368        if (targetPage) {
     369          const url = new URL(window.location.href);
     370          url.searchParams.set("pg", targetPage);
     371
     372          // Preserve sortfield if present in form
     373          const sortfieldInput = document.querySelector(
     374            'input[name="sortfield"]'
     375          );
     376          if (sortfieldInput && sortfieldInput.value) {
     377            url.searchParams.set("sortfield", sortfieldInput.value);
     378          }
     379
     380          // Preserve sortorigin if present in form
     381          const sortoriginInput = document.querySelector(
     382            'input[name="sortorigin"]'
     383          );
     384          if (sortoriginInput && sortoriginInput.value) {
     385            url.searchParams.set("sortorigin", sortoriginInput.value);
     386          }
     387
     388          window.location.href = url.toString();
     389        }
     390      }
     391
     392      // Handle Apply button click
     393      if (e.target.id === "btnApply" || e.target.closest("#btnApply")) {
     394        e.preventDefault();
    109395        const url = new URL(window.location.href);
    110         url.searchParams.set("pg", targetPage);
    111 
    112         // Preserve sortfield if present in form
    113         const sortfieldInput = document.querySelector(
    114           'input[name="sortfield"]'
    115         );
    116         if (sortfieldInput && sortfieldInput.value) {
    117           url.searchParams.set("sortfield", sortfieldInput.value);
    118         }
    119 
     396        url.searchParams.set("apply", "y");
    120397        window.location.href = url.toString();
    121398      }
    122     }
    123 
    124     // Handle Apply button click
    125     if (e.target.id === "btnApply" || e.target.closest("#btnApply")) {
    126       e.preventDefault();
    127       const url = new URL(window.location.href);
    128       url.searchParams.set("apply", "y");
    129       window.location.href = url.toString();
    130     }
    131   });
     399    },
     400    true
     401  ); // Use capture phase to intercept before other handlers
     402
     403  /**
     404   * Append referrer source to iframe (used by job detail apply frame)
     405   * Global function called from inline onload handler
     406   */
     407  window.appendSrcToFrame = function (frameobj) {
     408    try {
     409      var src = localStorage["pcr_referer"];
     410      if (src) {
     411        var host = frameobj.getAttribute("host");
     412        if (host && host.indexOf("src=") < 0) {
     413          frameobj.setAttribute(
     414            "host",
     415            host + "&src=" + encodeURIComponent(src)
     416          );
     417        }
     418      }
     419    } catch (e) {
     420      // Silently ignore localStorage errors
     421    }
     422  };
    132423})();
  • pcrecruiter-extensions/trunk/includes/class-deactivation-handler.php

    r3423933 r3431406  
    66 * @since 2.0.0
    77 */
    8 
     8if (! defined('ABSPATH')) {
     9    exit;
     10}
    911class PCR_Deactivation_Handler
    1012{
    11 
    1213    /**
    1314     * Initialize the deactivation handler
     
    1920        add_action('admin_footer', [__CLASS__, 'render_deactivation_modal']);
    2021    }
    21 
    2222    /**
    2323     * Enqueue scripts and styles for deactivation modal
     
    2929            return;
    3030        }
    31 
    3231        wp_enqueue_style(
    3332            'pcr-deactivation-modal',
     
    3635            '1.0.0'
    3736        );
    38 
    3937        wp_enqueue_script(
    4038            'pcr-deactivation-handler',
     
    4442            true
    4543        );
    46 
    4744        wp_localize_script('pcr-deactivation-handler', 'pcrDeactivation', [
    4845            'ajaxurl' => admin_url('admin-ajax.php'),
     
    5148        ]);
    5249    }
    53 
    5450    /**
    5551     * Render the deactivation modal HTML
     
    6965                    <button type="button" class="pcr-modal-close" aria-label="Close">&times;</button>
    7066                </div>
    71 
    7267                <div class="pcr-modal-body">
    7368                    <p class="pcr-intro-text">Please select what data should be handled during deactivation:</p>
    74 
    7569                    <form id="pcr-deactivation-form">
    7670                        <div class="pcr-option-section">
     
    8175                                <span class="pcr-option-description">Preserve all plugin settings for future use (recommended for temporary deactivation)</span>
    8276                            </label>
    83 
    8477                            <label class="pcr-option-label pcr-warning-option">
    8578                                <input type="radio" name="settings_action" value="delete">
     
    8881                            </label>
    8982                        </div>
    90 
    9183                        <div class="pcr-option-section">
    9284                            <h3>Job Postings</h3>
     
    9688                                <span class="pcr-option-description">Preserve all synced job postings in WordPress (recommended)</span>
    9789                            </label>
    98 
    9990                            <label class="pcr-option-label pcr-danger-option">
    10091                                <input type="radio" name="jobs_action" value="delete">
     
    10394                            </label>
    10495                        </div>
    105 
    10696                        <div class="pcr-option-section">
    10797                            <h3>RSS Feed Files</h3>
     
    111101                                <span class="pcr-option-description">Preserve pcrjobfeed.xml and pcrjobfeed.json files (recommended)</span>
    112102                            </label>
    113 
    114103                            <label class="pcr-option-label pcr-warning-option">
    115104                                <input type="radio" name="rss_action" value="delete">
     
    118107                            </label>
    119108                        </div>
    120 
    121109                        <div class="pcr-confirmation-section" id="pcr-confirmation-required" style="display:none;">
    122110                            <div class="pcr-warning-box">
     
    126114                            </div>
    127115                        </div>
    128 
    129116                        <div class="pcr-summary-section" id="pcr-summary">
    130117                            <h4>Summary:</h4>
     
    133120                    </form>
    134121                </div>
    135 
    136122                <div class="pcr-modal-footer">
    137123                    <button type="button" class="button button-secondary pcr-cancel-deactivation">Cancel</button>
     
    145131<?php
    146132    }
    147 
    148133    /**
    149134     * AJAX handler for cleanup actions
     
    153138        // Verify nonce
    154139        check_ajax_referer('pcr_deactivation_cleanup', 'nonce');
    155 
    156140        // Check permissions
    157141        if (!current_user_can('activate_plugins')) {
    158142            wp_send_json_error(['message' => 'Insufficient permissions']);
    159143        }
    160 
    161144        $settings_action = isset($_POST['settings_action']) ? sanitize_text_field(wp_unslash($_POST['settings_action'])) : 'keep';
    162145        $jobs_action = isset($_POST['jobs_action']) ? sanitize_text_field(wp_unslash($_POST['jobs_action'])) : 'keep';
    163146        $rss_action = isset($_POST['rss_action']) ? sanitize_text_field(wp_unslash($_POST['rss_action'])) : 'keep';
    164147        $confirmation = isset($_POST['confirmation']) ? sanitize_text_field(wp_unslash($_POST['confirmation'])) : '';
    165 
    166148        // Validate confirmation for destructive actions
    167149        if (($settings_action === 'delete' || $jobs_action === 'delete' || $rss_action === 'delete') && $confirmation !== 'DELETE') {
    168150            wp_send_json_error(['message' => 'Invalid confirmation text']);
    169151        }
    170 
    171152        // Store cleanup actions for deactivation hook to execute
    172153        update_option('pcrecruiter_deactivation_actions', [
     
    176157            'timestamp' => current_datetime()->getTimestamp()
    177158        ], false);
    178 
    179159        wp_send_json_success([
    180160            'message' => 'Deactivation preferences saved. Proceeding with plugin deactivation...'
     
    188168        self::delete_plugin_settings();
    189169    }
    190 
    191170    public static function execute_jobs_cleanup()
    192171    {
    193172        self::delete_all_jobs();
    194173    }
    195 
    196174    public static function execute_rss_cleanup()
    197175    {
     
    204182    {
    205183        global $wpdb;
    206 
    207184        // List of all plugin options
    208185        $options = [
     
    228205            'pcrecruiter_full_sync_enabled'
    229206        ];
    230 
    231207        foreach ($options as $option) {
    232208            delete_option($option);
    233209        }
    234 
    235210        // Clear any transients
    236211        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
     
    241216        );
    242217    }
    243 
    244218    /**
    245219     * Delete all job posts
     
    249223    {
    250224        global $wpdb;
    251 
    252225        // Get all job post IDs
    253226        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
     
    258231            )
    259232        );
    260 
    261233        $deleted_count = 0;
    262 
    263234        if (!empty($job_ids)) {
    264235            foreach ($job_ids as $job_id) {
     
    268239                }
    269240            }
    270 
    271241            // Clear job caches
    272242            if (class_exists('PCR_Job_Manager')) {
     
    274244            }
    275245        }
    276 
    277246        return $deleted_count;
    278247    }
    279 
    280248    /**
    281249     * Delete RSS feed files
     
    285253    {
    286254        $deleted_files = [];
    287 
    288255        $feed_files = [
    289256            WP_CONTENT_DIR . '/uploads/pcrjobfeed.xml',
    290257            WP_CONTENT_DIR . '/uploads/pcrjobfeed.json'
    291258        ];
    292 
    293259        foreach ($feed_files as $file) {
    294260            if (file_exists($file)) {
     
    299265            }
    300266        }
    301 
    302267        return $deleted_files;
    303268    }
    304269}
    305 
    306270// Initialize the deactivation handler
    307271PCR_Deactivation_Handler::init();
  • pcrecruiter-extensions/trunk/includes/class-job-manager.php

    r3423933 r3431406  
    88 *
    99 * @package PCRecruiter_Extensions
    10  * @since 2.0.0
     10 * @since 1.0.0
    1111 */
    1212
     
    168168        return '[[' . strtolower($key) . ']]';
    169169    }
    170 
    171 
    172170    public function update_wp_settings($data): string
    173171    {
     
    220218        try {
    221219            // Lookup all post IDs with matching databaseid using WordPress built-in function
     220            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for job lookup by databaseid
    222221            $args = array(
    223222                'post_type' => 'job',
     
    300299        if ($isDeleted) {
    301300            //handle expired job based on settings
    302             if (strlen($existingpostid) > 0) {
     301            if (!empty($existingpostid)) {
    303302                $options = get_option('pcrecruiter_feed_options', array());
    304303                $handling = isset($options['expired_job_handling']) ? $options['expired_job_handling'] : 'delete';
     
    362361            $allowed_html['br'] = array();
    363362
    364             // Strip inline styles from content
     363            // Strip inline styles from content (but preserve style on span.positionlogo and span.companylogo)
    365364            $options = get_option('pcrecruiter_feed_options', array());
    366365            $strip_styles = isset($options['strip_inline_styles']) ? $options['strip_inline_styles'] : '1';
     
    369368
    370369            if ($strip_styles === '1') {
    371                 $content = preg_replace('/\s*style\s*=\s*["\'][^"\']*["\']/i', '', $content);
    372             }
    373 
     370                $pattern = '/<([a-z0-9]+)([^>]*?)\sstyle\s*=\s*(["\'])(.*?)\3([^>]*?)>/i';
     371
     372                $content = preg_replace_callback(
     373                    $pattern,
     374                    function ($m) {
     375                        $tag = strtolower($m[1]);
     376                        $before = $m[2];
     377                        $after = $m[5];
     378                        $full = $m[0];
     379
     380                        // Combine attributes around the style attr to inspect class
     381                        $attrs = $before . ' ' . $after;
     382
     383                        // Keep style attribute only for span logos, but strip default-logo markers
     384                        if ($tag === 'span') {
     385                            $hasLogoClass = preg_match('/\bclass\s*=\s*["\'][^"\']*\b(positionlogo|companylogo)\b[^"\']*["\']/i', $attrs);
     386                            $hasDefaultLogo = preg_match('/\bdata-default-logo\s*=\s*["\']\s*true\s*["\']/i', $attrs);
     387
     388                            if ($hasLogoClass) {
     389                                if ($hasDefaultLogo) {
     390                                    // Remove entirely when it's a default placeholder
     391                                    return '';
     392                                }
     393                                // Preserve original tag (including style) for real logos
     394                                return $full;
     395                            }
     396                        }
     397
     398                        // Otherwise remove the style attribute by rebuilding the tag without it
     399                        // Trim to avoid duplicate spaces
     400                        $reconstructed = '<' . $tag . preg_replace('/\s+/', ' ', trim($before . ' ' . $after)) . '>';
     401                        return $reconstructed;
     402                    },
     403                    $content
     404                );
     405            }
     406
     407            // Sanitize content - allow form tags for apply buttons
    374408            $job_data['post_content'] = wp_kses($content, $allowed_html);
    375 
    376             // Force all <a> tags to open in new tab, except apply links
    377             $job_data['post_content'] = preg_replace(
    378                 '/<a\s+(?![^>]*target=)(?![^>]*href=["\'][^"\']*[?&]apply=y)([^>]*)>/i',
    379                 '<a target="_blank" $1>',
    380                 $job_data['post_content']
    381             );
    382             $job_data['post_content'] = preg_replace(
    383                 '/<a\s+(?![^>]*href=["\'][^"\']*[?&]apply=y)([^>]*)target=["\'](?!_blank)[^"\']*["\']([^>]*)>/i',
    384                 '<a $1 target="_blank" $2>',
    385                 $job_data['post_content']
    386             );
    387409
    388410            if ($existingpostid) {
     
    444466                }
    445467
     468                // Populate Yoast SEO meta after all job meta is saved
     469                if (class_exists('PCR_SEO_Enhancements')) {
     470                    PCR_SEO_Enhancements::populate_yoast_meta($post_id);
     471                }
     472
    446473                //return postid
    447474                return $post_id;
     
    503530        $searchmetafields = get_option("global_search_metafields");
    504531        $resulthmetafields = get_option("global_result_metafields");
    505 
    506 
    507532        //result options
    508533        $showjobs = get_option("global_result_showjobs");
     
    526551
    527552        $search_template = PCR_Job_Manager::replace_search_fields($search_template, $searchmetafields);
    528 
    529 
    530553        $output = [];
    531554
     
    697720            }
    698721        }
    699 
    700 
    701722        // Get pagination style from settings
    702723        $pcr_options = get_option('pcrecruiter_feed_options', []);
     
    828849        return [$beforeTbody, $tableRow, $afterTbody];
    829850    }
    830 
    831 
    832851    public static function build_job_selection_query($showjobs, $resultsperpage, $page, $filter, $jobcategory = ''): string
    833852    {
     
    852871        $limitsql = esc_sql(($resultsperpage + 1));
    853872        $iskeywordsearch = false;
    854         $isradius = false;
    855873
    856874        $pageoffset = (intval($page) - 1);
     
    873891        $defaultsortfield = PCR_Job_Manager::parseOrderBy(get_option('global_sort_field'));
    874892
    875         // Check POST first, then GET for sortfield
     893        // Check POST first, then GET for sortfield and sortorigin
    876894        $sortfield_raw = '';
     895        $sortorigin = 'default';
    877896        if (isset($_POST['sortfield']) && $_POST['sortfield'] !== '') {
    878897            $sortfield_raw = sanitize_text_field(wp_unslash($_POST['sortfield']));
     898            $sortorigin = isset($_POST['sortorigin']) ? sanitize_text_field(wp_unslash($_POST['sortorigin'])) : 'default';
    879899        } elseif (isset($_GET['sortfield']) && $_GET['sortfield'] !== '') {
    880900            $sortfield_raw = sanitize_text_field(wp_unslash($_GET['sortfield']));
     901            $sortorigin = isset($_GET['sortorigin']) ? sanitize_text_field(wp_unslash($_GET['sortorigin'])) : 'default';
     902        }
     903
     904        // Check if radius search is active (for Distance sort logic)
     905        $isRadiusSearch = false;
     906        if (isset($_POST['Position---LatLong']) && strlen(sanitize_text_field(wp_unslash($_POST['Position---LatLong']))) > 0) {
     907            $isRadiusSearch = true;
     908        } elseif (isset($_GET['Position---LatLong']) && strlen(sanitize_text_field(wp_unslash($_GET['Position---LatLong']))) > 0) {
     909            $isRadiusSearch = true;
     910        }
     911
     912        // Auto-apply Distance sort for radius searches ONLY if user hasn't explicitly chosen a sort
     913        if ($isRadiusSearch && $sortorigin !== 'user') {
     914            if (strpos($sortfield_raw, '[[Distance]]') === false) {
     915                $sortfield_raw = '[[Distance]] ASC';
     916                $sortforced = true;
     917            }
    881918        }
    882919
     
    889926
    890927        $orderby = '';
     928        $sort_is_distance = false;
    891929        $sortdir = esc_sql($sortfield['ascdesc']);
    892930
     
    10551093                            FROM {$wpdb->postmeta} pm
    10561094                            WHERE pm.post_id = wp.ID
    1057                             AND pm.meta_key = '[[position.zip_code]]'
     1095                            AND pm.meta_key = '[[position.zip]]'
    10581096                            LIMIT 1
    10591097                            ) {$sortdir}, wp.post_title";
     
    10951133                            LIMIT 1
    10961134                            ) {$sortdir}, wp.post_title";
     1135                break;
     1136            case '[[Distance]]':
     1137                // Distance sort only works with radius search
     1138                if ($isRadiusSearch) {
     1139                    $orderby = "ORDER BY distance {$sortdir}";
     1140                    $sort_is_distance = true;
     1141                } else {
     1142                    // Fall back to default sort if no radius search active
     1143                    $orderby = "ORDER BY (
     1144                                    SELECT pm.meta_value
     1145                                    FROM {$wpdb->postmeta} pm
     1146                                    WHERE pm.post_id = wp.ID
     1147                                    AND pm.meta_key = '[[position.date_posted]]'
     1148                                    LIMIT 1
     1149                                ) DESC";
     1150                }
    10971151                break;
    10981152
     
    11341188                ) DESC";
    11351189        }
    1136 
    1137 
    11381190        ///////////////////////////////////////////////////////////////////////
    11391191        //BUILD WHERE CLAUSE
     
    11731225                $whereclause[] = "EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE meta_key = '[[position.city]]' AND meta_value = '{$match}' AND wp.ID = pm.post_id)";
    11741226                $whereclause[] = "OR EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE meta_key = '[[position.state]]' AND meta_value = '{$match}' AND wp.ID = pm.post_id)";
    1175                 $whereclause[] = "OR EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE meta_key = '[[position.zip_code]]' AND meta_value = '{$match}' AND wp.ID = pm.post_id)";
     1227                $whereclause[] = "OR EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE meta_key = '[[position.zip]]' AND meta_value = '{$match}' AND wp.ID = pm.post_id)";
    11761228                $whereclause[] = ")";
    11771229            }
     
    12541306                $whereclause[] = "AND EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE meta_key = '[[position.department]]' AND meta_value = '{$match}' AND wp.ID = pm.post_id)";
    12551307            }
    1256 
    1257             //Position---LatLong (Radius)
    1258             if (isset($_POST['Position---LatLong']) && strlen(sanitize_text_field(wp_unslash($_POST['Position---LatLong']))) > 0) {
    1259                 $isradius = true;
    1260             }
    1261 
    12621308            //Positions---Salary
    12631309            if (isset($_POST['Positions---Salary']) && strlen(sanitize_text_field(wp_unslash($_POST['Positions---Salary']))) > 0) {
     
    13221368                }
    13231369            }
    1324 
    1325 
    13261370            //jobcategory filter from shortcode parameter
    13271371            if (!empty($jobcategory)) {
     
    13311375
    13321376            //radius search uses a different query
    1333             if ($isradius) {
     1377            if ($isRadiusSearch) {
    13341378                global $wpdb;
    13351379
     
    13611405                $output[] = "HAVING distance <= {$radius}";
    13621406                //sort by
    1363                 if ($sortforced) {
     1407                $sort_is_date = ($sortfield['fieldname'] === '[[Date_Posted]]' || $sortfield['fieldname'] === '');
     1408                if ($sortforced || $sort_is_date) {
    13641409                    $output[] = $orderby;
    13651410                } else {
     
    13751420                //keyword search return in context order
    13761421                if ($iskeywordsearch == false || $sortforced) {
     1422                    if ($sort_is_distance) {
     1423                        $orderby = "ORDER BY (
     1424                                SELECT pm.meta_value
     1425                                FROM {$wpdb->postmeta} pm
     1426                                WHERE pm.post_id = wp.ID
     1427                                AND pm.meta_key = '[[position.date_posted]]'
     1428                                LIMIT 1
     1429                            ) DESC";
     1430                    }
    13771431                    $output[] = $orderby;
    13781432                }
     
    14071461        return $sql;
    14081462    }
    1409 
    1410 
    14111463    public static function draw_search_form($searchtemplate, $page): string
    14121464    {
    1413         //get sort if sort was overridden
     1465        // Determine sortfield and sortorigin
     1466        // sortorigin values: 'user' (clicked sort link), 'radius' (auto-applied), 'default' (global setting)
    14141467        $sortfield = '';
    1415 
     1468        $sortorigin = 'default';
     1469
     1470        // Check for user-provided sort (POST or GET)
    14161471        if (isset($_POST['sortfield']) && strlen(sanitize_text_field(wp_unslash($_POST['sortfield']))) > 0) {
    14171472            $sortfield = sanitize_text_field(wp_unslash($_POST['sortfield']));
     1473            // Check if this was a user click or auto-applied
     1474            $sortorigin = isset($_POST['sortorigin']) ? sanitize_text_field(wp_unslash($_POST['sortorigin'])) : 'default';
    14181475        } elseif (isset($_GET['sortfield']) && strlen(sanitize_text_field(wp_unslash($_GET['sortfield']))) > 0) {
    14191476            $sortfield = sanitize_text_field(wp_unslash($_GET['sortfield']));
    1420         }
     1477            $sortorigin = isset($_GET['sortorigin']) ? sanitize_text_field(wp_unslash($_GET['sortorigin'])) : 'default';
     1478        }
     1479
     1480        // If no sortfield provided, use default
     1481        if (empty($sortfield)) {
     1482            $sortfield = get_option('global_sort_field', '');
     1483            $sortorigin = 'default';
     1484        }
     1485
     1486        // Check if radius search is active
     1487        $isRadiusActive = false;
     1488        if (isset($_POST['Position---LatLong']) && strlen(sanitize_text_field(wp_unslash($_POST['Position---LatLong']))) > 0) {
     1489            $isRadiusActive = true;
     1490        } elseif (isset($_GET['Position---LatLong']) && strlen(sanitize_text_field(wp_unslash($_GET['Position---LatLong']))) > 0) {
     1491            $isRadiusActive = true;
     1492        }
     1493
     1494        // Auto-apply Distance sort for radius searches ONLY if user hasn't explicitly chosen a sort
     1495        if ($isRadiusActive && $sortorigin !== 'user') {
     1496            // Only override if not already Distance
     1497            if (strpos($sortfield, '[[Distance]]') === false) {
     1498                $sortfield = '[[Distance]] ASC';
     1499                $sortorigin = 'radius';
     1500            }
     1501        }
     1502
    14211503        // Build form action URL with current page parameter
    14221504        $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
     
    14331515        $output[] = '  <form id="searchForm" name="searchForm" autocomplete="off" method="post" action="' . esc_url($form_action) . '" title="Search Form">';
    14341516        $output[] = $searchtemplate;
    1435         $output[] = '    <input name="action" id="action" type="hidden" value="SEARCHJOBS"></input>';
    1436         $output[] = '    <input name="resultspage" id="resultspage" type="hidden" value="' . esc_attr($page) . '"></input>';
    1437         $output[] = '    <input name="sortfield" id="sortfield" type="hidden" value="' . esc_attr($sortfield) . '"></input>';
     1517        $output[] = '    <input name="action" id="action" type="hidden" value="SEARCHJOBS">';
     1518        $output[] = '    <input name="resultspage" id="resultspage" type="hidden" value="' . esc_attr($page) . '">';
     1519        $output[] = '    <input name="sortfield" id="sortfield" type="hidden" value="' . esc_attr($sortfield) . '">';
     1520        $output[] = '    <input name="sortorigin" id="sortorigin" type="hidden" value="' . esc_attr($sortorigin) . '">';
    14381521        $output[] = '  </form>';
    14391522        $output[] = '</div>';
     
    14561539    public static function replace_search_fields($template, $metafields): string
    14571540    {
    1458 
    1459 
    14601541        // Handle null/empty metafields gracefully
    14611542        if (empty($metafields) || !is_string($metafields)) {
     
    14841565            $fielddic[$field['fieldname']] = $field;
    14851566        }
    1486 
    1487 
    14881567        $allfields = preg_split('/(\[\[[^\]]+\]\])/', $template, -1, PREG_SPLIT_DELIM_CAPTURE);
    14891568
     
    15011580        return implode('', $allfields);
    15021581    }
    1503 
    1504 
    15051582    public static function replace_result_fields($post, $template, $metafields, $istable, $baseurl): string
    15061583    {
     
    15381615            $fieldvalue = str_replace(['http://', "https://"], ["", ""], esc_attr(get_permalink($post->ID)));
    15391616        }
    1540 
    15411617        if (strtolower($fieldname) == "[[position.date_posted]]") {
    15421618            //reformat date from ISO to readable with timezone adjustment
    1543             $fieldvalue = PCR_Job_Manager::adjustTimezoneUsingCookie($fieldvalue, self::get_date_format());
     1619            $fieldvalue = PCR_Job_Manager::adjustTimezoneUsingCookie($fieldvalue, "Y-m-d");
    15441620        }
    15451621
    15461622        if (strtolower($fieldname) == "[[position.last_modified]]") {
    15471623            //reformat date from ISO to readable with timezone adjustment
    1548             $fieldvalue = PCR_Job_Manager::adjustTimezoneUsingCookie($fieldvalue, self::get_date_format());
     1624            $fieldvalue = PCR_Job_Manager::adjustTimezoneUsingCookie($fieldvalue, "Y-m-d");
    15491625        }
    15501626
     
    15611637        return $fieldvalue;
    15621638    }
    1563 
    1564 
    15651639    public static function replace_field($template, $field): string
    15661640    {
     
    15991673
    16001674                //radius city/state/zip
    1601 
    1602 
    16031675
    16041676                $radius[] = '<div style="position: relative;">';
     
    16411713                $textbox = [];
    16421714
    1643 
    1644 
    16451715                $textbox[] = '<input autocomplete="on" class="form-control"
    16461716                    type="text" id="' . esc_attr(pcrecruiter_esc_form_var($fieldname)) . '" name="' . esc_attr(pcrecruiter_esc_form_var($fieldname)) . '"
     
    16851755        $output = [];
    16861756
    1687 
    1688 
    16891757        $fieldvalue = isset($_POST[pcrecruiter_esc_form_var($fieldname)]) ? sanitize_text_field(wp_unslash($_POST[pcrecruiter_esc_form_var($fieldname)])) : '';
    1690 
    1691 
    16921758        $whereclause = " AND EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE meta_key = '[[position.status]]' AND meta_value = '{$intext}' AND  wp.post_id = pm.post_id)";
    16931759
     
    17501816                $query = "SELECT DISTINCT meta_value as dropdown  FROM {$wpdb->postmeta} wp WHERE meta_key = '[[position.department]]' {$whereclause} ORDER BY meta_value";
    17511817                break;
    1752 
    1753 
    17541818            case 'Positions.Date_Posted':
    17551819                $output[] = '<option value="">' . 'Posted Within...</option>';
     
    17731837                $output[] = '<option ' . ($fieldvalue == '225000-300000' ? " selected " : "") . 'value="225000-300000">' . '225000 - 300000</option>';
    17741838                $output[] = '<option ' . ($fieldvalue == '300000-10000000000' ? " selected " : "") . 'value="300000-10000000000">' . '300000+</option>';
    1775 
    1776 
    17771839                return '<select id="' . esc_attr(pcrecruiter_esc_form_var($fieldname)) . '" name="' . esc_attr(pcrecruiter_esc_form_var($fieldname)) . '" size="1" class="form-control">' . implode("\r\n", $output) . '</select>';
    17781840        }
     
    18171879        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- $normalized from esc_sql(), $whereclause built with esc_sql()
    18181880        $results = $wpdb->get_results($query);
    1819 
    1820 
    18211881        $fieldvalues = isset($_POST[pcrecruiter_esc_form_var($fieldname)]) ? array_map('sanitize_text_field', wp_unslash($_POST[pcrecruiter_esc_form_var($fieldname)])) : [];
    18221882
     
    18451905    {
    18461906        $output = [];
    1847 
    1848 
    18491907        $template_css_url = $baseurl . '/pcrbin/jobboard.aspx?action=templatecss&uid=' . $databaseid . '&ver=' . $hash;
    18501908        wp_enqueue_style('pcr-template-css', $template_css_url, array(), $hash);
     
    18521910        $template_js_url = $baseurl . '/pcrbin/jobboard.aspx?action=templatejs&uid=' . $databaseid . '&ver=' . $hash;
    18531911        wp_enqueue_script('pcr-template-js', $template_js_url, array(), $hash, true);
    1854 
    1855 
    18561912        // CSS and JS are now properly enqueued above, no inline tags needed
    18571913
     
    18631919        $template = str_ireplace(["&#126;", "~"], ["|", "|"],  $template);
    18641920
    1865         // Add sort direction arrows
    1866         // Check POST first, then GET, then fall back to default
     1921        // Check if radius search is active
     1922        $isradius = false;
     1923        if (isset($_POST['Position---LatLong']) && strlen(sanitize_text_field(wp_unslash($_POST['Position---LatLong']))) > 0) {
     1924            $isradius = true;
     1925        } elseif (isset($_GET['Position---LatLong']) && strlen(sanitize_text_field(wp_unslash($_GET['Position---LatLong']))) > 0) {
     1926            $isradius = true;
     1927        }
     1928
     1929        // Convert bare [[field]] placeholders to clickable sort links
     1930        // Match [[FieldName]] patterns that are NOT already inside HTML tags
     1931        $template = preg_replace_callback(
     1932            '/(?<=>|^)([^<]*)(\[\[([^\]]+)\]\])([^<]*)(?=<|$)/i',
     1933            function ($matches) use ($isradius) {
     1934                $before = $matches[1];
     1935                $fieldPlaceholder = $matches[2];
     1936                $fieldName = $matches[3];
     1937                $after = $matches[4];
     1938               
     1939                // Check if this is Distance field and no radius search
     1940                $isDistance = stripos($fieldName, 'Distance') !== false;
     1941                if ($isDistance && !$isradius) {
     1942                    // Return as plain text (non-clickable) with disabled styling
     1943                    $displayName = str_replace(['Position.', 'Positions.', '_'], ['', '', ' '], $fieldName);
     1944                    return $before . '<span class="sort-disabled" style="opacity: 0.5; cursor: not-allowed;" title="Distance sort requires a location search">' . esc_html($displayName) . '</span>' . $after;
     1945                }
     1946               
     1947                // Check if this is a sortable field
     1948                $sortableFields = [
     1949                    'Distance', 'Job_Title', 'Position.Job_Title', 'Date_Posted',
     1950                    'Position.Date_Posted', 'PCity', 'Position.City', 'PState',
     1951                    'Position.State', 'PCountry', 'Position.Country', 'PCounty',
     1952                    'Department', 'Positions.Company_Name', 'Position Id'
     1953                ];
     1954               
     1955                $isSortable = false;
     1956                foreach ($sortableFields as $sortField) {
     1957                    if (stripos($fieldName, $sortField) !== false) {
     1958                        $isSortable = true;
     1959                        break;
     1960                    }
     1961                }
     1962               
     1963                if ($isSortable) {
     1964                    // Create clickable sort link
     1965                    $fullFieldName = '[[' . $fieldName . ']]';
     1966                    $displayName = str_replace(['Position.', 'Positions.', '_'], ['', '', ' '], $fieldName);
     1967                    $sortLink = '<a href="#" class="customsortfield" data-sortname="' .
     1968                                esc_attr($fullFieldName) . '" data-sortdirection="ASC">' .
     1969                                esc_html($displayName) . '</a>';
     1970                    return $before . $sortLink . $after;
     1971                }
     1972               
     1973                // Return unchanged if not sortable
     1974                return $matches[0];
     1975            },
     1976            $template
     1977        );
     1978
     1979        // Disable existing Distance sort links if no radius search
     1980        if (!$isradius) {
     1981            $template = preg_replace(
     1982                '/(<a[^>]*data-sortname[^>]*["\'].*?Distance.*?["\'][^>]*>)(.*?)<\/a>/i',
     1983                '<span class="sort-disabled" style="opacity: 0.5; cursor: not-allowed;" title="Distance sort requires a location search">$2</span>',
     1984                $template
     1985            );
     1986        }
     1987
     1988        // Determine current sort field (considering sortorigin for radius auto-sort)
    18671989        $currentSortField = '';
     1990        $sortorigin = 'default';
     1991       
    18681992        if (isset($_POST['sortfield']) && $_POST['sortfield'] !== '') {
    18691993            $currentSortField = sanitize_text_field(wp_unslash($_POST['sortfield']));
     1994            $sortorigin = isset($_POST['sortorigin']) ? sanitize_text_field(wp_unslash($_POST['sortorigin'])) : 'default';
    18701995        } elseif (isset($_GET['sortfield']) && $_GET['sortfield'] !== '') {
    18711996            $currentSortField = sanitize_text_field(wp_unslash($_GET['sortfield']));
    1872         } else {
     1997            $sortorigin = isset($_GET['sortorigin']) ? sanitize_text_field(wp_unslash($_GET['sortorigin'])) : 'default';
     1998        }
     1999
     2000        // If no sortfield provided, use default
     2001        if (empty($currentSortField)) {
    18732002            $currentSortField = get_option('global_sort_field', '');
     2003            $sortorigin = 'default';
     2004        }
     2005
     2006        // Auto-apply Distance sort for radius searches (same logic as draw_search_form)
     2007        if ($isradius && $sortorigin !== 'user') {
     2008            if (strpos($currentSortField, '[[Distance]]') === false) {
     2009                $currentSortField = '[[Distance]] ASC';
     2010            }
    18742011        }
    18752012
     
    18782015        $currentDirection = strtoupper($sortInfo['ascdesc'] ?? '');
    18792016
    1880         // Add sort direction arrows and toggle directions
     2017        // Add arrows to indicate sort direction
     2018        // Only show arrow if the field exists in the visible sort options
    18812019        if (!empty($currentField)) {
    1882             // Remove any existing arrows first
    1883             $template = preg_replace("/\s*[↓↑]\s*/u", "", $template);
    1884 
    1885             // Determine next direction (toggle)
    1886             $nextDirection = ($currentDirection === "DESC") ? "ASC" : "DESC";
    1887             $arrow = ($currentDirection === "DESC") ? " ↓" : " ↑";
    1888 
    1889             // Find sort links with data-sortname matching current field
    1890             $template = preg_replace_callback(
    1891                 '/(<a[^>]*data-sortname=["\']' . preg_quote($currentField, '/') . '["\'][^>]*data-sortdirection=["\'])([^"\']*)/i',
    1892                 function ($matches) use ($nextDirection, $arrow) {
    1893                     // Replace the direction attribute with the toggled direction
    1894                     return $matches[1] . $nextDirection;
    1895                 },
    1896                 $template
    1897             );
    1898 
    1899             // Add arrow to the link text
    1900             $template = preg_replace(
    1901                 '/(<a[^>]*data-sortname=["\']' . preg_quote($currentField, '/') . '["\'][^>]*>)([^<]*)/i',
    1902                 '$1$2' . $arrow,
    1903                 $template
    1904             );
     2020            // Remove any existing arrows first (HTML entities and Unicode)
     2021            $template = preg_replace('/\s*(&darr;|&uarr;|[\x{2193}\x{2191}])\s*/u', '', $template);
     2022
     2023            // Check if the current sort field exists as a clickable link in the template
     2024            $fieldExistsInTemplate = (strpos($template, 'data-sortname="' . $currentField . '"') !== false) ||
     2025                                     (strpos($template, "data-sortname='" . $currentField . "'") !== false);
     2026
     2027            if ($fieldExistsInTemplate) {
     2028                // Add appropriate arrow based on current sort direction
     2029                $arrow = ($currentDirection === 'DESC') ? ' &darr;' : ' &uarr;';
     2030
     2031                // Try to find the current field in the template and add arrow using data-sortname
     2032                $template = preg_replace(
     2033                    '/(<a[^>]*data-sortname\s*=\s*["\']' . preg_quote($currentField, '/') . '["\'][^>]*>)([^<]*)/i',
     2034                    '$1$2' . $arrow,
     2035                    $template
     2036                );
     2037            }
     2038            // If field doesn't exist in template (e.g., Distance sort with no Distance link), no arrow is shown
    19052039        }
    19062040
     
    21022236    {
    21032237        $jobid = get_post_meta($post_id, '[[position.job_id]]', true);
    2104 
    2105 
    21062238        // Try both possible cases for databaseid
    21072239        $databaseid = get_post_meta($post_id, "[[databaseid]]", true);
    2108 
    2109 
    21102240        $baseurl = get_post_meta($post_id, "[[baseurl]]", true);
    2111 
    2112 
    21132241        $hash = get_option('global_template_hash', 'any_version');
    21142242
     
    21362264
    21372265        try {
    2138             // Try to parse the date - handle both ISO and US formats
    2139             $date = false;
    2140 
    2141             // First try: ISO 8601 format (2025-12-18T15:34:39Z)
    2142             try {
    2143                 $date = new DateTime($datetime_string, new DateTimeZone('UTC'));
    2144             } catch (Exception $e) {
    2145                 // Second try: US format with AM/PM (12/18/2025 12:51:18 PM)
    2146                 $date = DateTime::createFromFormat('m/d/Y h:i:s A', $datetime_string, new DateTimeZone('UTC'));
    2147             }
    2148 
    2149             if ($date === false) {
    2150                 throw new Exception('Unable to parse date');
    2151             }
     2266            // Create DateTime object from the input (assume UTC/GMT)
     2267            $date = new DateTime($datetime_string, new DateTimeZone('UTC'));
    21522268
    21532269            // If no cookie, display as GMT with indicator
     
    21742290        }
    21752291    }
    2176     /**
    2177      * Get the configured date format
    2178      */
    2179     private static function get_date_format()
    2180     {
    2181         $options = get_option('pcrecruiter_feed_options', array());
    2182         $date_format = isset($options['date_format']) ? $options['date_format'] : 'wordpress';
    2183 
    2184         if ($date_format === 'custom') {
    2185             return isset($options['date_format_custom']) && !empty($options['date_format_custom'])
    2186                 ? $options['date_format_custom']
    2187                 : 'Y-m-d';
    2188         }
    2189 
    2190         if ($date_format === 'wordpress') {
    2191             return get_option('date_format');
    2192         }
    2193 
    2194         return $date_format;
    2195     }
     2292
    21962293    private static function time_ago($datetime, $full = false)
    21972294    {
     
    25132610            return;
    25142611        }
    2515 
    2516 
    25172612        if (!empty($job_board_page)) {
    25182613            // Get the page ID from URL
  • pcrecruiter-extensions/trunk/includes/class-schema-mapper.php

    r3423933 r3431406  
    327327            <h1>JobPosting Schema Mapper</h1>
    328328
    329                         <!-- Validation Status -->
     329            <!-- Validation Status -->
    330330            <div id="pcr-schema-status" class="pcr-schema-status">
    331331                <div class="pcr-status-loading">
     
    334334            </div>
    335335
    336                  <!-- Preview Section -->
     336            <!-- Preview Section -->
    337337            <div class="pcr-schema-preview-section">
    338338                <h2>Schema Preview</h2>
     
    367367
    368368            <div class="pcr-schema-intro">
    369             <h2>Schema Mapping</h2>   
    370             <p>Map your job post fields to <a href="https://schema.org/JobPosting" target="_blank" rel="noopener">schema.org JobPosting</a> properties. Jobs with incomplete required fields will not output schema.</p>
     369                <h2>Schema Mapping</h2>
     370                <p>Map your job post fields to <a href="https://schema.org/JobPosting" target="_blank" rel="noopener">schema.org JobPosting</a> properties. Jobs with incomplete required fields will not output schema.</p>
    371371                <p><strong>Source Types:</strong></p>
    372372                <ul>
     
    406406            </form>
    407407
    408        
     408
    409409        </div>
    410410<?php
     
    760760                $full_key = $prop_key . '.' . $sub_key;
    761761                $label_prefix = !empty($parent_label) ? $parent_label . ' - ' : '';
    762                
     762
    763763                // Recursively call for nested objects
    764764                if ($sub_property['type'] === 'object' && !empty($sub_property['properties'])) {
     
    10671067        // Check if it's HTML with background-image CSS
    10681068        if (strpos($logo_value, 'background-image') !== false) {
     1069            // If the element indicates it's a default logo, treat as empty
     1070            if (preg_match('/data-default-logo\s*=\s*(["\']?)true\1/i', $logo_value)) {
     1071                return '';
     1072            }
     1073
    10691074            // Extract URL from: background-image:url(/pcrbin/logo.exe?_=...)
    10701075            if (preg_match('/background-image\s*:\s*url\s*\(\s*([^)]+)\s*\)/i', $logo_value, $matches)) {
     
    12021207                // For description field specifically, clean up HTML
    12031208                if (isset($property['label']) && $property['label'] === 'Job Description') {
     1209                    // Look for pcr-description-start marker (same logic as build_meta_description)
     1210                    if (preg_match('/<!--\s*pcr-description-start\s*-->(.*)/is', $value, $matches)) {
     1211                        // Found comment marker - use everything after it
     1212                        $value = $matches[1];
     1213                    } elseif (preg_match("/<div[^>]*data-pcr-description=[\"']start[\"'][^>]*>(.*?)<\/div>/is", $value, $matches)) {
     1214                        // Found data attribute marker - use content inside the div
     1215                        $value = $matches[1];
     1216                    }
     1217
    12041218                    // Remove HTML comments
    12051219                    $value = preg_replace('/<!--(.|\s)*?-->/', '', $value);
    12061220
    1207                     // Remove empty span/div tags
    1208                     $value = preg_replace('/<(span|div)[^>]*>\s*<\/\1>/', '', $value);
    1209 
    1210                     // Remove standalone template fragments like "| Location"
    1211                     $value = preg_replace('/\|\s*Location\s*/', '', $value);
    1212 
    1213                     // Sanitize HTML (keeps safe tags like p, h2, ul, li, strong, em)
    1214                     $value = wp_kses_post($value);
     1221                    // Remove script and style tags completely
     1222                    $value = preg_replace('/<(script|style)[^>]*>.*?<\/\1>/is', '', $value);
     1223
     1224                    // Sanitize HTML - Google Jobs recognizes <p>, <ul>, <li>, <br>
     1225                    // Also allows header and character tags like <h1>, <strong>, <em> (though not used in formatting)
     1226                    $allowed_html = array(
     1227                        'p' => array(),
     1228                        'br' => array(),
     1229                        'ul' => array(),
     1230                        'ol' => array(),
     1231                        'li' => array(),
     1232                        'strong' => array(),
     1233                        'em' => array(),
     1234                        'b' => array(),
     1235                        'i' => array(),
     1236                        'u' => array(),
     1237                        'h1' => array(),
     1238                        'h2' => array(),
     1239                        'h3' => array(),
     1240                        'h4' => array(),
     1241                        'h5' => array(),
     1242                        'h6' => array(),
     1243                        'a' => array('href' => array()),
     1244                        'div' => array(),
     1245                        'span' => array(),
     1246                    );
     1247                    $value = wp_kses($value, $allowed_html);
     1248
     1249                    // Remove extra whitespace but preserve line breaks
     1250                    $value = preg_replace('/[ \t]+/', ' ', $value);
     1251                    $value = preg_replace('/\n\s*\n/', "\n", $value);
    12151252                }
    12161253
  • pcrecruiter-extensions/trunk/includes/class-seo-enhancements.php

    r3423933 r3431406  
    5454            $conflicts[] = array(
    5555                'name' => 'Yoast SEO',
    56                 'config' => 'SEO → Search Appearance → Content Types → Jobs'
     56                'config' => 'SEO &rarr; Search Appearance &rarr; Content Types &rarr; Jobs'
    5757            );
    5858        }
     
    6161            $conflicts[] = array(
    6262                'name' => 'Rank Math',
    63                 'config' => 'Rank Math → Titles & Meta → Job Posts'
     63                'config' => 'Rank Math &rarr; Titles &amp; Meta &rarr; Job Posts'
    6464            );
    6565        }
     
    6868            $conflicts[] = array(
    6969                'name' => 'Divi',
    70                 'config' => 'Divi → Theme Options → SEO'
     70                'config' => 'Divi &rarr; Theme Options &rarr; SEO'
    7171            );
    7272        }
     
    8282            $conflicts[] = array(
    8383                'name' => 'Avada',
    84                 'config' => 'Avada → Theme Options → SEO'
     84                'config' => 'Avada &rarr; Theme Options &rarr; SEO'
    8585            );
    8686        }
     
    8989            $conflicts[] = array(
    9090                'name' => 'Astra',
    91                 'config' => 'Customizer → General → Blog / Archive'
     91                'config' => 'Customizer &rarr; General &rarr; Blog / Archive'
    9292            );
    9393        }
     
    134134    /**
    135135     * Add comprehensive meta tags and Open Graph for job posts
     136     * Only outputs when no SEO plugin is handling meta tags
    136137     */
    137138    public static function add_job_meta_tags()
    138139    {
    139140        if (!is_singular('job')) {
     141            return;
     142        }
     143
     144        // Skip if Yoast or Rank Math is active - they handle meta tags
     145        // and we populate their fields via populate_yoast_meta()
     146        if (defined('WPSEO_VERSION') || defined('RANK_MATH_VERSION')) {
    140147            return;
    141148        }
     
    394401                // Found comment marker - use everything after it
    395402                $content = $matches[1];
    396             } elseif (preg_match('/<[^>]*data-pcr-description=["\']start["\'][^>]*>(.*?)(?=<\/div>|$)/is', $content, $matches)) {
     403            } elseif (preg_match("/<div[^>]*data-pcr-description=[\"']start[\"'][^>]*>(.*?)<\/div>/is", $content, $matches)) {
    397404                // Found data attribute marker - use content inside the element
    398405                $content = $matches[1];
     
    745752        return array();
    746753    }
     754
     755    /**
     756     * Populate Yoast SEO meta fields for job posts
     757     * Called after job creation/update
     758     * Only populates if fields are empty (preserves manual edits)
     759     *
     760     * @param int $job_id The job post ID
     761     */
     762    /**
     763     * Populate Yoast SEO meta fields for job posts
     764     * Called after job creation/update
     765     * Only populates if fields are empty (preserves manual edits)
     766     *
     767     * @param int $job_id The job post ID
     768     */
     769    /**
     770     * Populate Yoast SEO meta fields for job posts
     771     * Called after job creation/update via save_post_job hook
     772     * Only populates if fields are empty (preserves manual edits)
     773     *
     774     * @param int $job_id The job post ID
     775     */
     776    public static function populate_yoast_meta($job_id)
     777    {
     778        // Only for job posts
     779        if (get_post_type($job_id) !== 'job') {
     780            return;
     781        }
     782
     783        // Only if Yoast is active
     784        if (!defined('WPSEO_VERSION')) {
     785            return;
     786        }
     787
     788        // Build optimized title and description
     789        $job_title = get_the_title($job_id);
     790        if (empty($job_title)) {
     791            $job_title = get_post_meta($job_id, '[[position.job_title]]', true);
     792        }
     793       
     794        $seo_title = self::build_seo_title($job_id, $job_title);
     795        $meta_description = self::build_meta_description($job_id);
     796
     797        // Only populate if Yoast fields are empty (preserves manual edits)
     798        $existing_title = get_post_meta($job_id, '_yoast_wpseo_title', true);
     799        if (empty($existing_title) && !empty($seo_title)) {
     800            update_post_meta($job_id, '_yoast_wpseo_title', $seo_title);
     801        }
     802
     803        $existing_desc = get_post_meta($job_id, '_yoast_wpseo_metadesc', true);
     804        if (empty($existing_desc) && !empty($meta_description)) {
     805            update_post_meta($job_id, '_yoast_wpseo_metadesc', $meta_description);
     806        }
     807
     808        // Populate OpenGraph fields (Facebook)
     809        $existing_og_title = get_post_meta($job_id, '_yoast_wpseo_opengraph-title', true);
     810        if (empty($existing_og_title) && !empty($seo_title)) {
     811            update_post_meta($job_id, '_yoast_wpseo_opengraph-title', $seo_title);
     812        }
     813
     814        $existing_og_desc = get_post_meta($job_id, '_yoast_wpseo_opengraph-description', true);
     815        if (empty($existing_og_desc) && !empty($meta_description)) {
     816            update_post_meta($job_id, '_yoast_wpseo_opengraph-description', $meta_description);
     817        }
     818
     819        // Populate Twitter Card fields
     820        $existing_twitter_title = get_post_meta($job_id, '_yoast_wpseo_twitter-title', true);
     821        if (empty($existing_twitter_title) && !empty($seo_title)) {
     822            update_post_meta($job_id, '_yoast_wpseo_twitter-title', $seo_title);
     823        }
     824
     825        $existing_twitter_desc = get_post_meta($job_id, '_yoast_wpseo_twitter-description', true);
     826        if (empty($existing_twitter_desc) && !empty($meta_description)) {
     827            update_post_meta($job_id, '_yoast_wpseo_twitter-description', $meta_description);
     828        }
     829
     830        // Set OpenGraph image if available
     831        $job_image = self::get_job_image($job_id);
     832        if (!empty($job_image)) {
     833            $existing_og_image = get_post_meta($job_id, '_yoast_wpseo_opengraph-image', true);
     834            if (empty($existing_og_image)) {
     835                update_post_meta($job_id, '_yoast_wpseo_opengraph-image', $job_image);
     836            }
     837
     838            $existing_twitter_image = get_post_meta($job_id, '_yoast_wpseo_twitter-image', true);
     839            if (empty($existing_twitter_image)) {
     840                update_post_meta($job_id, '_yoast_wpseo_twitter-image', $job_image);
     841            }
     842        }
     843    }
    747844}
  • pcrecruiter-extensions/trunk/includes/class-sitemap-integration.php

    r3423933 r3431406  
    11<?php
     2
    23/**
    34 * PCRecruiter Sitemap Integration
     
    78 * @since 2.0.0
    89 */
    9 
    1010if (!defined('ABSPATH')) {
    1111    exit;
    1212}
    13 
    1413class PCR_Sitemap_Integration
    1514{
     
    2322        add_filter('wp_sitemaps_posts_entry', array(__CLASS__, 'filter_wp_sitemap_entry'), 10, 3);
    2423        add_filter('wp_sitemaps_posts_query_args', array(__CLASS__, 'filter_wp_sitemap_query'), 10, 2);
    25        
    2624        // Yoast SEO
    2725        add_filter('wpseo_sitemap_entry', array(__CLASS__, 'filter_yoast_sitemap_entry'), 10, 3);
    2826        add_filter('wpseo_exclude_from_sitemap_by_post_ids', array(__CLASS__, 'exclude_internal_jobs_yoast'));
    29        
    3027        // RankMath
    3128        add_filter('rank_math/sitemap/entry', array(__CLASS__, 'filter_rankmath_sitemap_entry'), 10, 3);
    3229        add_filter('rank_math/sitemap/exclude_post', array(__CLASS__, 'exclude_internal_jobs_rankmath'), 10, 2);
    3330    }
    34 
    3531    /**
    3632     * Add job post type to native WordPress sitemap
     
    4642            return $post_types;
    4743        }
    48 
    4944        // Check if job post type exists and is registered
    5045        $job_post_type = get_post_type_object('job');
    51        
    5246        if (!$job_post_type) {
    5347            return $post_types;
    5448        }
    55 
    5649        // Use the actual post type object to ensure all properties are correct
    5750        $post_types['job'] = $job_post_type;
    58 
    5951        return $post_types;
    6052    }
    61 
    6253    /**
    6354     * Filter WordPress sitemap entries for jobs
     
    7364            return $entry;
    7465        }
    75 
    7666        // Exclude internal jobs
    7767        if (self::is_internal_job($post->ID)) {
    7868            return false;
    7969        }
    80 
    8170        // Set priority, change frequency, and last modified date
    8271        $entry['priority'] = apply_filters('pcrecruiter_sitemap_priority', 0.6, $post->ID);
    8372        $entry['changefreq'] = apply_filters('pcrecruiter_sitemap_changefreq', 'weekly', $post->ID);
    8473        $entry['lastmod'] = get_the_modified_date('c', $post);
    85 
    8674        return $entry;
    8775    }
    88 
    8976    /**
    9077     * Filter WordPress sitemap query to exclude internal jobs
     
    9986            return $args;
    10087        }
    101 
    10288        // Exclude internal jobs via meta query
     89        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for filtering by job status
    10390        if (!isset($args['meta_query'])) {
    10491            $args['meta_query'] = array();
    10592        }
    106 
    10793        $args['meta_query'][] = array(
    10894            'key' => '[[position.status]]',
     
    11096            'compare' => '!=',
    11197        );
    112 
    11398        return $args;
    11499    }
    115 
    116100    /**
    117101     * Filter Yoast SEO sitemap entries for jobs
     
    127111            return $url;
    128112        }
    129 
    130113        // Exclude internal jobs
    131114        if (self::is_internal_job($object->ID)) {
    132115            return false;
    133116        }
    134 
    135117        // Set priority and change frequency if not manually set
    136118        if (!isset($url['pri']) || $url['pri'] == 0.5) { // Yoast default is 0.5
    137119            $url['pri'] = apply_filters('pcrecruiter_sitemap_priority', 0.6, $object->ID);
    138120        }
    139 
    140121        if (!isset($url['chf']) || $url['chf'] == 'weekly') {
    141122            $url['chf'] = apply_filters('pcrecruiter_sitemap_changefreq', 'weekly', $object->ID);
    142123        }
    143 
    144124        // Set last modified date
    145125        $url['mod'] = get_the_modified_date('c', $object);
    146 
    147126        return $url;
    148127    }
    149 
    150128    /**
    151129     * Exclude internal jobs from Yoast sitemap by ID
     
    157135    {
    158136        $internal_jobs = self::get_internal_job_ids();
    159        
    160137        if (!empty($internal_jobs)) {
    161138            $excluded_ids = array_merge($excluded_ids, $internal_jobs);
    162139        }
    163 
    164140        return $excluded_ids;
    165141    }
    166 
    167142    /**
    168143     * Filter RankMath sitemap entries for jobs
     
    178153            return $entry;
    179154        }
    180 
    181155        // Exclude internal jobs
    182156        if (self::is_internal_job($object->ID)) {
    183157            return false;
    184158        }
    185 
    186159        // Set priority, change frequency, and last modified date
    187160        $entry['priority'] = apply_filters('pcrecruiter_sitemap_priority', 0.6, $object->ID);
    188161        $entry['changefreq'] = apply_filters('pcrecruiter_sitemap_changefreq', 'weekly', $object->ID);
    189162        $entry['lastmod'] = get_the_modified_date('c', $object);
    190 
    191163        return $entry;
    192 
    193     }
    194 
     164    }
    195165    /**
    196166     * Exclude internal jobs from RankMath sitemap
     
    205175            return $exclude;
    206176        }
    207 
    208177        // Exclude if internal job
    209178        if (self::is_internal_job($post->ID)) {
    210179            return true;
    211180        }
    212 
    213181        return $exclude;
    214182    }
    215 
    216183    /**
    217184     * Check if a job is internal-only
     
    225192        return strtolower($status) === 'internalonly';
    226193    }
    227 
    228194    /**
    229195     * Get all internal job IDs
     
    239205            return $cached;
    240206        }
    241 
    242207        // Query for internal jobs
     208        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for filtering internal jobs
    243209        $args = array(
    244210            'post_type' => 'job',
     
    253219            ),
    254220        );
    255 
    256221        $internal_ids = get_posts($args);
    257 
    258222        // Cache for 1 hour
    259223        set_transient('pcr_internal_job_ids', $internal_ids, HOUR_IN_SECONDS);
    260 
    261224        return $internal_ids;
    262225    }
    263 
    264226    /**
    265227     * Clear internal job IDs cache when jobs are updated
     
    270232        delete_transient('pcr_internal_job_ids');
    271233    }
    272 
    273234    /**
    274235     * Get debug information about sitemap configuration
     
    280241    {
    281242        $info = array();
    282 
    283243        // Check if job post type is registered
    284244        $job_post_type = get_post_type_object('job');
    285245        $info['post_type_registered'] = !empty($job_post_type);
    286        
    287246        if ($job_post_type) {
    288247            $info['post_type_public'] = $job_post_type->public;
     
    290249            $info['post_type_has_archive'] = $job_post_type->has_archive;
    291250        }
    292 
    293251        // Check if job sync is configured
    294252        $options = get_option('pcrecruiter_feed_options', array());
    295253        $info['sync_configured'] = !empty($options['jobboard_security_key']);
    296 
    297254        // Count jobs
    298255        $info['total_jobs'] = wp_count_posts('job')->publish ?? 0;
    299256        $info['internal_jobs'] = count(self::get_internal_job_ids());
    300 
    301257        // Check which sitemap plugins are active
    302258        $info['wp_core_sitemaps'] = function_exists('wp_sitemaps_get_server');
    303259        $info['yoast_active'] = defined('WPSEO_VERSION');
    304260        $info['rankmath_active'] = defined('RANK_MATH_VERSION');
    305 
    306261        // Check if sitemaps are enabled
    307262        if (function_exists('get_option')) {
    308263            $info['wp_sitemaps_enabled'] = (bool) get_option('blog_public');
    309264        }
    310 
    311265        return $info;
    312266    }
  • pcrecruiter-extensions/trunk/readme.txt

    r3423933 r3431406  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 2.0.4
     7Stable tag: 2.0.5
    88License: GPLv3 or later
    99License URI: https://www.gnu.org/licenses/gpl-3.0.html
     
    123123
    124124== Changelog ==
     125= 2.0.5 - 2026-01-02 =
     126* Improved handling of company and position logos
     127* Radius search sort by distance
     128* Yoast fields generation
     129* Exempt PCR frames/js from interference by cache and optimization defer/lazyload plugins
     130* Performance optimizations
    125131
    126 = 2.0.4 - 2024-19-10 =
     132= 2.0.4 - 2025-12-19 =
    127133* Improved handling of v1 URLs
    128134* Performance optimizations
     
    130136* Zipcode field handler
    131137
    132 = 2.0.3 - 2024-18-10 =
     138= 2.0.3 - 2025-12-18 =
    133139* Additional checks and safeguards for pre-existing custom post type
    134140* Enhanced meta Description tagging
     
    138144* Multi-check search fields fix
    139145
    140 = 2.0.2 - 2024-16-10 =
     146= 2.0.2 - 2025-12-16 =
    141147* Accomodate other /job/ plugins in iframe-only mode
    142148
    143 = 2.0.1 - 2024-16-10 =
     149= 2.0.1 - 2025-12-16 =
    144150* Share widget width adjustment
    145151
    146 = 2.0.0 - 2024-12-10 =
     152= 2.0.0 - 2025-12-10 =
    147153* Initial public release of full sync mode
    148154* Added block bindings support
Note: See TracChangeset for help on using the changeset viewer.