Snapshot: MLS sync fixes, image refresh, plugin/theme updates

MLS plugin fixes from this session:
- Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls,
  causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL
  with ST_PointFromText so the spatial column is populated atomically.
- Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by
  a property-level GET_LOCK so concurrent fetches share one API refresh.
- Normalize WP_Error to null in mls_get_property_image() so callers can rely on the
  documented string|null contract.
- Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the
  homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm)
  actually filters correctly.
- Incremental sync now looks back 10 minutes past the latest modification timestamp
  as a safety margin against missed records.
- Smart sync exits silently (info-level, not warning) when a full sync is in progress.

Operational:
- New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync).
- New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/
  (/usr/local/bin/mls-image-cache-cap).
- Logrotate config for wp-content/debug.log (2-day retention, daily rotation,
  delaycompress).

Repo policy:
- CLAUDE.md updated with explicit "commit everything except build artifacts" policy.
- .gitignore: untrack runtime image caches and debug.log rotations.

Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-29 15:32:23 +00:00
parent 57b752f54e
commit b6df4dbb92
5385 changed files with 838580 additions and 2416 deletions
@@ -0,0 +1,125 @@
/* global wpforms_settings */
/**
* @param wpforms_settings.address_field.list_countries_without_states
*/
( function( window, $ ) {
const app = {
/**
* List of countries without states.
*
* @see WPForms\Forms\Fields\Address\Frontend::strings for PHP filter.
*
* @since 1.9.5
*/
noStateCountries: [],
/**
* Init Address field.
*
* @since 1.9.5
*/
init() {
$( window ).on( 'load', app.onLoad );
$( document )
.on( 'wpformsRepeaterFieldCloneCreated', app.setChangeHandlers );
},
/**
* On load event.
*
* @since 1.9.5
*/
onLoad() {
app.noStateCountries = wpforms_settings?.address_field?.list_countries_without_states || [];
if ( ! app.noStateCountries.length ) {
return;
}
app.setChangeHandlers();
},
/**
* Set change handlers.
*
* @since 1.9.5
*/
setChangeHandlers() {
$( '.wpforms-field-address' ).each( function() {
const $countrySelect = $( this ).find( 'select.wpforms-field-address-country' );
if ( ! $countrySelect.length ) {
return;
}
app.handleCountryChange( $countrySelect );
$countrySelect
.off( 'change' )
.on( 'change', function() {
app.handleCountryChange( this );
} );
} );
},
/**
* Handle country change.
*
* @since 1.9.5
*
* @param {HTMLElement} field Country select field.
*/
handleCountryChange( field ) {
const $this = $( field ),
$stateInput = $this.closest( '.wpforms-field' ).find( '.wpforms-field-address-state' ),
$rowWithState = $stateInput.closest( '.wpforms-field-row' );
if ( ! $rowWithState.length ) {
return;
}
const value = $this.val();
app.handleStateInput( $stateInput, $rowWithState, value );
},
/**
* Handle state input.
*
* @since 1.9.5
*
* @param {jQuery} $stateInput State input.
* @param {jQuery} $rowWithState Row with state.
* @param {string} countryValue Country value.
*/
handleStateInput( $stateInput, $rowWithState, countryValue ) {
if ( app.noStateCountries.includes( countryValue ) ) {
$stateInput
.val( '' )
.prop( 'disabled', true )
.prop( 'required', false )
.on( 'change', function() {
$( this ).val( '' );
} );
$rowWithState.addClass( 'wpforms-without-state' );
return;
}
$stateInput
.prop( 'disabled', false )
.prop( 'required', $rowWithState.find( '.wpforms-first input' ).prop( 'required' ) ) // Set required same as first input.
.off( 'change' );
$rowWithState.removeClass( 'wpforms-without-state' );
},
};
app.init();
return app;
}( window, jQuery ) );
@@ -0,0 +1 @@
((e,s)=>{let o={noStateCountries:[],init(){s(e).on("load",o.onLoad),s(document).on("wpformsRepeaterFieldCloneCreated",o.setChangeHandlers)},onLoad(){o.noStateCountries=wpforms_settings?.address_field?.list_countries_without_states||[],o.noStateCountries.length&&o.setChangeHandlers()},setChangeHandlers(){s(".wpforms-field-address").each(function(){var e=s(this).find("select.wpforms-field-address-country");e.length&&(o.handleCountryChange(e),e.off("change").on("change",function(){o.handleCountryChange(this)}))})},handleCountryChange(e){var e=s(e),t=e.closest(".wpforms-field").find(".wpforms-field-address-state"),n=t.closest(".wpforms-field-row");n.length&&(e=e.val(),o.handleStateInput(t,n,e))},handleStateInput(e,t,n){o.noStateCountries.includes(n)?(e.val("").prop("disabled",!0).prop("required",!1).on("change",function(){s(this).val("")}),t.addClass("wpforms-without-state")):(e.prop("disabled",!1).prop("required",t.find(".wpforms-first input").prop("required")).off("change"),t.removeClass("wpforms-without-state"))}};o.init(),o})(window,jQuery);
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
!function i(r,a,o){function l(n,t){if(!a[n]){if(!r[n]){var e="function"==typeof require&&require;if(!t&&e)return e(n,!0);if(s)return s(n,!0);throw new Error("Cannot find module '"+n+"'")}t=a[n]={exports:{}};r[n][0].call(t.exports,function(t){var e=r[n][1][t];return l(e||t)},t,t.exports,i,r,a,o)}return a[n].exports}for(var s="function"==typeof require&&require,t=0;t<o.length;t++)l(o[t]);return l}({1:[function(t,e,n){function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function l(t,e,n){return t.replace("{count}",e).replace("{limit}",n).replace("{remaining}",n-e)}function s(t,e,n){var i=document.createElement("div");return t="object"===r(t)?"":t,e="object"===r(e)?"":e,i.classList.add("wpforms-field-limit-text"),i.id="wpforms-field-limit-text-"+t+"-"+e,i.setAttribute("aria-live","polite"),i.textContent=n,i}function u(e){return"string"==typeof e&&e.length?([/([A-Z]+),([A-Z]+)/gi,/([0-9]+),([A-Z]+)/gi,/([A-Z]+),([0-9]+)/gi].forEach(function(t){e=e.replace(t,"$1, $2")}),e.split(/\s+/).length):0}function c(t){return window.clipboardData&&window.clipboardData.getData?window.clipboardData.getData("Text"):t.clipboardData&&t.clipboardData.getData?t.clipboardData.getData("text/plain"):""}function d(t,e){var n="",i=/\s+/g,r=t.trim().match(i)||[],a=t.split(i);a.splice(e,a.length);for(var o=0;o<a.length;o++)n+=a[o]+(r[o]||"");return n.trim()}function i(t){return[].slice.call(t)}function f(t){(t=t.parentNode.querySelector(".wpforms-field-limit-text"))&&t.remove()}function a(){(window.WPFormsTextLimit=o).initHint("body")}var o;o={initHint:function(t){i(document.querySelectorAll(t+" .wpforms-limit-characters-enabled")).map(function(t){function e(t){n.textContent=l(window.wpforms_settings.val_limit_characters,this.value.length,i)}var n,i,r,a=parseInt(t.dataset.textLimit,10)||0,o=(t.value=t.value.slice(0,a),s(t.dataset.formId,t.dataset.fieldId,l(wpforms_settings.val_limit_characters,t.value.length,a)));n=o,i=a;f(t),t.parentNode.appendChild(o),t.addEventListener("keydown",e),t.addEventListener("keyup",e),t.addEventListener("paste",(r=a,function(t){t.preventDefault();var t=c(t),e=this.selectionStart+t.length,t=this.value.substring(0,this.selectionStart)+t+this.value.substring(this.selectionStart);this.value=t.substring(0,r),this.setSelectionRange(e,e)}))}),i(document.querySelectorAll(t+" .wpforms-limit-words-enabled")).map(function(t){function e(t){var e=u(this.value.trim());n.textContent=l(window.wpforms_settings.val_limit_words,e,i),-1<[13,32,188].indexOf(t.keyCode)&&i<=e&&t.preventDefault()}var n,i,r,a=parseInt(t.dataset.textLimit,10)||0,o=(t.value=d(t.value,a),s(t.dataset.formId,t.dataset.fieldId,l(wpforms_settings.val_limit_words,u(t.value.trim()),a)));n=o,i=a;f(t),t.parentNode.appendChild(o),t.addEventListener("keydown",e),t.addEventListener("keyup",e),t.addEventListener("paste",(r=a,function(t){t.preventDefault();var t=c(t),e=this.selectionStart+t.length,t=this.value.substring(0,this.selectionStart)+t+this.value.substring(this.selectionStart);this.value=d(t,r),this.setSelectionRange(e,e)}))})}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",a):a()},{}]},{},[1]);
@@ -0,0 +1,338 @@
/* global wpforms_settings */
( function() {
/**
* Predefine hint text to display.
*
* @since 1.5.6
* @since 1.6.4 Added a new macros - {remaining}.
*
* @param {string} hintText Hint text.
* @param {number} count Current count.
* @param {number} limit Limit to.
*
* @return {string} Predefined hint text.
*/
function renderHint( hintText, count, limit ) {
return hintText.replace( '{count}', count ).replace( '{limit}', limit ).replace( '{remaining}', limit - count );
}
/**
* Create HTMLElement hint element with text.
*
* @since 1.5.6
*
* @param {number|string} formId Form id.
* @param {number|string} fieldId Form field id.
* @param {string} text Hint text.
*
* @return {Object} HTMLElement hint element with text.
*/
function createHint( formId, fieldId, text ) {
const hint = document.createElement( 'div' );
formId = typeof formId === 'object' ? '' : formId;
fieldId = typeof fieldId === 'object' ? '' : fieldId;
hint.classList.add( 'wpforms-field-limit-text' );
hint.id = 'wpforms-field-limit-text-' + formId + '-' + fieldId;
hint.setAttribute( 'aria-live', 'polite' );
hint.textContent = text;
return hint;
}
/**
* Keyup/Keydown event higher order function for characters limit.
*
* @since 1.5.6
*
* @param {Object} hint HTMLElement hint element.
* @param {number} limit Max allowed number of characters.
*
* @return {Function} Handler function.
*/
function checkCharacters( hint, limit ) {
// noinspection JSUnusedLocalSymbols
return function( e ) { // eslint-disable-line no-unused-vars
hint.textContent = renderHint(
window.wpforms_settings.val_limit_characters,
this.value.length,
limit
);
};
}
/**
* Count words in the string.
*
* @since 1.6.2
*
* @param {string} string String value.
*
* @return {number} Words count.
*/
function countWords( string ) {
if ( typeof string !== 'string' ) {
return 0;
}
if ( ! string.length ) {
return 0;
}
[
/([A-Z]+),([A-Z]+)/gi,
/([0-9]+),([A-Z]+)/gi,
/([A-Z]+),([0-9]+)/gi,
].forEach( function( pattern ) {
string = string.replace( pattern, '$1, $2' );
} );
return string.split( /\s+/ ).length;
}
/**
* Keyup/Keydown event higher order function for words limit.
*
* @since 1.5.6
*
* @param {Object} hint HTMLElement hint element.
* @param {number} limit Max allowed number of characters.
*
* @return {Function} Handler function.
*/
function checkWords( hint, limit ) {
return function( e ) {
const value = this.value.trim(),
words = countWords( value );
hint.textContent = renderHint(
window.wpforms_settings.val_limit_words,
words,
limit
);
// We should prevent the keys: Enter, Space, Comma.
if ( [ 13, 32, 188 ].indexOf( e.keyCode ) > -1 && words >= limit ) {
e.preventDefault();
}
};
}
/**
* Get passed text from the clipboard.
*
* @since 1.5.6
*
* @param {ClipboardEvent} e Clipboard event.
*
* @return {string} Text from clipboard.
*/
function getPastedText( e ) {
if ( window.clipboardData && window.clipboardData.getData ) { // IE
return window.clipboardData.getData( 'Text' );
} else if ( e.clipboardData && e.clipboardData.getData ) {
return e.clipboardData.getData( 'text/plain' );
}
return '';
}
/**
* Paste event higher order function for character limit.
*
* @since 1.6.7.1
*
* @param {number} limit Max allowed number of characters.
*
* @return {Function} Event handler.
*/
function pasteText( limit ) {
return function( e ) {
e.preventDefault();
const pastedText = getPastedText( e ),
newPosition = this.selectionStart + pastedText.length,
newText = this.value.substring( 0, this.selectionStart ) + pastedText + this.value.substring( this.selectionStart );
this.value = newText.substring( 0, limit );
this.setSelectionRange( newPosition, newPosition );
};
}
/**
* Limit string length to a certain number of words, preserving line breaks.
*
* @since 1.6.8
*
* @param {string} text Text.
* @param {number} limit Max allowed number of words.
*
* @return {string} Text with the limited number of words.
*/
function limitWords( text, limit ) {
let result = '';
// Regular expression pattern: match any space character.
const regEx = /\s+/g;
// Store separators for further join.
const separators = text.trim().match( regEx ) || [];
// Split the new text by regular expression.
const newTextArray = text.split( regEx );
// Limit the number of words.
newTextArray.splice( limit, newTextArray.length );
// Join the words together using stored separators.
for ( let i = 0; i < newTextArray.length; i++ ) {
result += newTextArray[ i ] + ( separators[ i ] || '' );
}
return result.trim();
}
/**
* Paste event higher order function for words limit.
*
* @since 1.5.6
*
* @param {number} limit Max allowed number of words.
*
* @return {Function} Event handler.
*/
function pasteWords( limit ) {
return function( e ) {
e.preventDefault();
const pastedText = getPastedText( e ),
newPosition = this.selectionStart + pastedText.length,
newText = this.value.substring( 0, this.selectionStart ) + pastedText + this.value.substring( this.selectionStart );
this.value = limitWords( newText, limit );
this.setSelectionRange( newPosition, newPosition );
};
}
/**
* Array.from polyfill.
*
* @since 1.5.6
*
* @param {Object} el Iterator.
*
* @return {Object} Array.
*/
function arrFrom( el ) {
return [].slice.call( el );
}
/**
* Remove existing hint.
*
* @since 1.9.5.1
*
* @param {Object} element Element.
*/
const removeExistingHint = ( element ) => {
const existingHint = element.parentNode.querySelector( '.wpforms-field-limit-text' );
if ( existingHint ) {
existingHint.remove();
}
};
/**
* Public functions and properties.
*
* @since 1.8.9
*
* @type {Object}
*/
const app = {
/**
* Init text limit hint.
*
* @since 1.8.9
*
* @param {string} context Context selector.
*/
initHint( context ) {
arrFrom( document.querySelectorAll( context + ' .wpforms-limit-characters-enabled' ) )
.map(
function( e ) { // eslint-disable-line array-callback-return
const limit = parseInt( e.dataset.textLimit, 10 ) || 0;
e.value = e.value.slice( 0, limit );
const hint = createHint(
e.dataset.formId,
e.dataset.fieldId,
renderHint(
wpforms_settings.val_limit_characters,
e.value.length,
limit
)
);
const fn = checkCharacters( hint, limit );
removeExistingHint( e );
e.parentNode.appendChild( hint );
e.addEventListener( 'keydown', fn );
e.addEventListener( 'keyup', fn );
e.addEventListener( 'paste', pasteText( limit ) );
}
);
arrFrom( document.querySelectorAll( context + ' .wpforms-limit-words-enabled' ) )
.map(
function( e ) { // eslint-disable-line array-callback-return
const limit = parseInt( e.dataset.textLimit, 10 ) || 0;
e.value = limitWords( e.value, limit );
const hint = createHint(
e.dataset.formId,
e.dataset.fieldId,
renderHint(
wpforms_settings.val_limit_words,
countWords( e.value.trim() ),
limit
)
);
const fn = checkWords( hint, limit );
removeExistingHint( e );
e.parentNode.appendChild( hint );
e.addEventListener( 'keydown', fn );
e.addEventListener( 'keyup', fn );
e.addEventListener( 'paste', pasteWords( limit ) );
}
);
},
};
/**
* DOMContentLoaded handler.
*
* @since 1.5.6
*/
function ready() {
// Expose to the world.
window.WPFormsTextLimit = app;
app.initHint( 'body' );
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', ready );
} else {
ready();
}
}() );