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,249 @@
/* global wpf, WPFormsBuilder, WPFormsConstantContactV3AuthVars */
/**
* @param window.wpforms_admin
* @param window.wpforms_builder
* @param WPFormsConstantContactV3AuthVars.auth_url
*/
/**
* WPForms Constant Contact V3 Popup.
*
* @since 1.9.3
*/
const WPFormsConstantContactV3Auth = window.WPFormsConstantContactV3Auth || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.9.3
*
* @type {Object}
*/
const app = {
/**
* Is the authorization window opened?
*
* @since 1.9.3
*/
isOpened : false,
/**
* URL to listen for messages from the window.
*
* @since 1.9.3
*/
listenURL: '',
/**
* Start the engine.
*
* @since 1.9.3
*/
init: () => {
$( app.ready );
},
/**
* Document ready.
*
* @since 1.9.3
*/
ready: () => {
const redirectUri = new URL( WPFormsConstantContactV3AuthVars.auth_url ).searchParams.get( 'redirect_uri' );
app.listenURL = new URL( redirectUri ).origin;
$( document )
.on( 'click', '.wpforms-constant-contact-v3-auth, .wpforms-builder-constant-contact-v3-provider-sign-up', app.showWindow )
.on( 'click', '#wpforms-settings-constant-contact-v3-migration-prompt-link', app.promptMigration );
},
/**
* Show a window.
*
* @since 1.9.3
*
* @param {Event} e Click event.
*/
showWindow: ( e ) => {
e.preventDefault();
if ( app.isOpened ) {
return;
}
const authUrl = WPFormsConstantContactV3AuthVars.auth_url,
width = 500,
height = 600,
left = ( screen.width / 2 ) - ( width / 2 ),
top = ( screen.height / 2 ) - ( height / 2 ),
loginHintEmail = $( '.wpforms-constant-contact-v3-auth' ).data( 'login-hint' ),
url = new URL( authUrl );
if ( loginHintEmail ) {
url.searchParams.set( 'login_hint', loginHintEmail );
}
const newWindow = window.open(
url.toString(),
'authPopup',
'width=' + width + ', height=' + height + ', top=' + top + ', left=' + left
);
window.addEventListener( 'message', app.listenResponse );
const checkWindowClosed = setInterval( () => {
if ( newWindow.closed ) {
clearInterval( checkWindowClosed );
app.isOpened = false;
}
}, 1000 );
app.isOpened = true;
},
/**
* Listen for response.
*
* @since 1.9.3
*
* @param {Event} event Message event.
*/
listenResponse: ( event ) => {
if ( event.origin !== app.listenURL ) {
return;
}
if ( ! event.data ) {
app.errorModal( WPFormsConstantContactV3AuthVars.strings.error );
return;
}
app.saveAccount( event.data );
},
/**
* Save account.
*
* @since 1.9.3
*
* @param {string} code Authorization code.
*/
saveAccount: ( code ) => {
const modal = app.waitModal();
$.post(
WPFormsConstantContactV3AuthVars.ajax_url,
{
action: 'wpforms_constant_contact_popup_auth',
data: JSON.stringify( { code } ),
nonce: WPFormsConstantContactV3AuthVars.nonce,
}
)
.done( ( response ) => {
if ( ! response.success ) {
modal.close();
const errorMessage =
'<p>' + WPFormsConstantContactV3AuthVars.strings.error + '</p><p><strong>' + wpf.sanitizeHTML( response.data ) + '</strong></p>';
app.errorModal( errorMessage );
return;
}
if ( typeof WPFormsBuilder === 'undefined' ) {
modal.close();
window.location.href = WPFormsConstantContactV3AuthVars.page_url;
return;
}
WPFormsBuilder.formSave( false ).done( () => {
WPFormsBuilder.setCloseConfirmation( false );
WPFormsBuilder.showLoadingOverlay();
location.reload();
} );
} );
},
/**
* Show a waiting modal.
*
* @since 1.9.3
*
* @return {Object} Modal object.
*/
waitModal: () => {
return $.alert( {
title: '',
content: WPFormsConstantContactV3AuthVars.strings.wait,
icon: 'fa fa-info-circle',
type: 'blue',
buttons: false,
} );
},
/**
* Show an error modal.
*
* @since 1.9.3
*
* @param {string} content Alert text.
*
* @return {Object} Modal object.
*/
errorModal: ( content ) => {
const strings = window?.wpforms_builder || window?.wpforms_admin;
return $.alert( {
title: strings.uh_oh,
content,
icon: 'fa fa-exclamation-circle',
type: 'red',
buttons: {
cancel: {
text: strings.cancel,
action: () => {
app.isOpened = false;
},
},
},
} );
},
/**
* Prompt and start migration from v2 to v3 in the notice.
*
* @since 1.9.3
*
* @param {Object} e Event object.
*/
promptMigration( e ) {
e.preventDefault();
const modal = app.waitModal();
$.post( {
url: WPFormsConstantContactV3AuthVars.ajax_url,
data: {
action: 'wpforms_constant_contact_migration_prompt',
nonce: WPFormsConstantContactV3AuthVars.nonce,
},
success: () => {
modal.close();
window.location.href = WPFormsConstantContactV3AuthVars.page_url;
},
error: () => {
modal.close();
app.errorModal( WPFormsConstantContactV3AuthVars.strings.error );
},
} );
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPFormsConstantContactV3Auth.init();
@@ -0,0 +1 @@
let WPFormsConstantContactV3Auth=window.WPFormsConstantContactV3Auth||((o,s,i)=>{let c={isOpened:!1,listenURL:"",init:()=>{i(c.ready)},ready:()=>{var t=new URL(WPFormsConstantContactV3AuthVars.auth_url).searchParams.get("redirect_uri");c.listenURL=new URL(t).origin,i(o).on("click",".wpforms-constant-contact-v3-auth, .wpforms-builder-constant-contact-v3-provider-sign-up",c.showWindow).on("click","#wpforms-settings-constant-contact-v3-migration-prompt-link",c.promptMigration)},showWindow:n=>{if(n.preventDefault(),!c.isOpened){var n=WPFormsConstantContactV3AuthVars.auth_url,a=screen.width/2-250,r=screen.height/2-300,e=i(".wpforms-constant-contact-v3-auth").data("login-hint"),n=new URL(n);e&&n.searchParams.set("login_hint",e);let t=s.open(n.toString(),"authPopup","width=500, height=600, top="+r+", left="+a),o=(s.addEventListener("message",c.listenResponse),setInterval(()=>{t.closed&&(clearInterval(o),c.isOpened=!1)},1e3));c.isOpened=!0}},listenResponse:t=>{t.origin===c.listenURL&&(t.data?c.saveAccount(t.data):c.errorModal(WPFormsConstantContactV3AuthVars.strings.error))},saveAccount:t=>{let o=c.waitModal();i.post(WPFormsConstantContactV3AuthVars.ajax_url,{action:"wpforms_constant_contact_popup_auth",data:JSON.stringify({code:t}),nonce:WPFormsConstantContactV3AuthVars.nonce}).done(t=>{t.success?"undefined"==typeof WPFormsBuilder?(o.close(),s.location.href=WPFormsConstantContactV3AuthVars.page_url):WPFormsBuilder.formSave(!1).done(()=>{WPFormsBuilder.setCloseConfirmation(!1),WPFormsBuilder.showLoadingOverlay(),location.reload()}):(o.close(),t="<p>"+WPFormsConstantContactV3AuthVars.strings.error+"</p><p><strong>"+wpf.sanitizeHTML(t.data)+"</strong></p>",c.errorModal(t))})},waitModal:()=>i.alert({title:"",content:WPFormsConstantContactV3AuthVars.strings.wait,icon:"fa fa-info-circle",type:"blue",buttons:!1}),errorModal:t=>{var o=s?.wpforms_builder||s?.wpforms_admin;return i.alert({title:o.uh_oh,content:t,icon:"fa fa-exclamation-circle",type:"red",buttons:{cancel:{text:o.cancel,action:()=>{c.isOpened=!1}}}})},promptMigration(t){t.preventDefault();let o=c.waitModal();i.post({url:WPFormsConstantContactV3AuthVars.ajax_url,data:{action:"wpforms_constant_contact_migration_prompt",nonce:WPFormsConstantContactV3AuthVars.nonce},success:()=>{o.close(),s.location.href=WPFormsConstantContactV3AuthVars.page_url},error:()=>{o.close(),c.errorModal(WPFormsConstantContactV3AuthVars.strings.error)}})}};return c})(document,window,jQuery);WPFormsConstantContactV3Auth.init();
@@ -0,0 +1,603 @@
/* global WPForms, wpf */
/**
* WPForms Providers Builder ConstantContactV3 module.
*
* @since 1.9.3
*/
WPForms.Admin.Builder.Providers.ConstantContactV3 = WPForms.Admin.Builder.Providers.ConstantContactV3 || ( function( document, window, $ ) {
/**
* Public functions and properties.
*
* @since 1.9.3
*
* @type {Object}
*/
const app = {
/**
* CSS selectors.
*
* @since 1.9.3
*
* @type {Object}
*/
selectors: {
accountField: '.js-wpforms-builder-constant-contact-v3-provider-connection-account',
actionData: '.wpforms-builder-constant-contact-v3-provider-actions-data',
actionField: '.js-wpforms-builder-constant-contact-v3-provider-connection-action',
connection: '.wpforms-panel-content-section-constant-contact-v3 .wpforms-builder-provider-connection',
},
/**
* jQuery elements.
*
* @since 1.9.3
*
* @type {Object}
*/
$elements: {
$connections: $( '.wpforms-panel-content-section-constant-contact-v3 .wpforms-builder-provider-connections' ),
$holder: $( '#wpforms-panel-providers' ),
$panel: $( '#constant-contact-v3-provider' ),
},
/**
* Current provider slug.
*
* @since 1.9.3
*
* @type {string}
*/
provider: 'constant-contact-v3',
/**
* This is a shortcut to the WPForms.Admin.Builder.Providers object,
* that handles the parent all-providers functionality.
*
* @since 1.9.3
*
* @type {Object}
*/
Providers: {},
/**
* This is a shortcut to the WPForms.Admin.Builder.Templates object,
* that handles all the template management.
*
* @since 1.9.3
*
* @type {Object}
*/
Templates: {},
/**
* This is a shortcut to the WPForms.Admin.Builder.Providers.cache object,
* that handles all the cache management.
*
* @since 1.9.3
*
* @type {Object}
*/
Cache: {},
/**
* This is a flag for ready state.
*
* @since 1.9.3
*
* @type {boolean}
*/
isReady: false,
/**
* Start the engine.
*
* Run initialization on the providers panel only.
*
* @since 1.9.3
*/
init() {
// We are requesting/loading a Providers panel.
if ( wpf.getQueryString( 'view' ) === 'providers' ) {
app.$elements.$holder.on( 'WPForms.Admin.Builder.Providers.ready', app.ready );
}
// We have switched to a Providers panel.
$( document ).on( 'wpformsPanelSwitched', function( event, panel ) {
if ( panel === 'providers' ) {
app.ready();
}
} );
},
/**
* Initialized once the DOM and Providers are fully loaded.
*
* @since 1.9.3
*/
ready() {
if ( app.isReady ) {
return;
}
app.Providers = WPForms.Admin.Builder.Providers;
app.Templates = WPForms.Admin.Builder.Templates;
app.Cache = app.Providers.cache;
// Register custom Underscore.js templates.
app.Templates.add( [
'wpforms-constant-contact-v3-builder-content-connection',
'wpforms-constant-contact-v3-builder-content-connection-error',
'wpforms-constant-contact-v3-builder-content-connection-select-field',
'wpforms-constant-contact-v3-builder-content-connection-conditionals',
] );
// Events registration.
app.bindUIActions();
app.bindTriggers();
app.processInitial();
// Save a flag for ready state.
app.isReady = true;
},
/**
* Process various events as a response to UI interactions.
*
* @since 1.9.3
*/
bindUIActions() {
app.$elements.$panel
.on( 'connectionCreate', app.connection.create )
.on( 'connectionDelete', app.connection.delete )
.on( 'change', app.selectors.accountField, app.ui.accountField.change )
.on( 'change', app.selectors.actionField, app.ui.actionField.change );
},
/**
* Fire certain events on certain actions, specific for related connections.
* These are not directly caused by user manipulations.
*
* @since 1.9.3
*/
bindTriggers() {
app.$elements.$connections.on( 'connectionsDataLoaded', function( event, data ) {
if ( _.isEmpty( data.connections ) ) {
return;
}
for ( const connectionId in data.connections ) {
app.connection.generate( {
connection: data.connections[ connectionId ],
conditional: data.conditionals[ connectionId ],
} );
}
} );
app.$elements.$connections.on( 'connectionGenerated', function( event, data ) {
const $connection = app.connection.getById( data.connection.id );
if ( _.has( data.connection, 'isNew' ) && data.connection.isNew ) {
// Run replacing temporary connection ID if it's a new connection.
app.connection.replaceIds( data.connection.id, $connection );
return;
}
$( app.selectors.actionField, $connection ).trigger( 'change' );
} );
},
/**
* Compile template with data if any and display them on a page.
*
* @since 1.9.3
*/
processInitial() {
app.connection.dataLoad();
},
/**
* Connection property.
*
* @since 1.9.3
*/
connection: {
/**
* Sometimes we might need to a get a connection DOM element by its ID.
*
* @since 1.9.3
*
* @param {string} connectionId Connection ID to search for a DOM element by.
*
* @return {jQuery} jQuery object for connection.
*/
getById( connectionId ) {
return app.$elements.$connections.find( '.wpforms-builder-provider-connection[data-connection_id="' + connectionId + '"]' );
},
/**
* Sometimes in DOM we might have placeholders or temporary connection IDs.
* We need to replace them with actual values.
*
* @since 1.9.3
*
* @param {string} connectionId New connection ID to replace to.
* @param {Object} $connection jQuery DOM connection element.
*/
replaceIds( connectionId, $connection ) {
// Replace old temporary %connection_id% from PHP code with the new one.
$connection.find( 'input, select, label' ).each( function() {
const $this = $( this );
if ( $this.attr( 'name' ) ) {
$this.attr( 'name', $this.attr( 'name' ).replace( /%connection_id%/gi, connectionId ) );
}
if ( $this.attr( 'id' ) ) {
$this.attr( 'id', $this.attr( 'id' ).replace( /%connection_id%/gi, connectionId ) );
}
if ( $this.attr( 'for' ) ) {
$this.attr( 'for', $this.attr( 'for' ).replace( /%connection_id%/gi, connectionId ) );
}
if ( $this.attr( 'data-name' ) ) {
$this.attr( 'data-name', $this.attr( 'data-name' ).replace( /%connection_id%/gi, connectionId ) );
}
} );
},
/**
* Create a connection using the user entered name.
*
* @since 1.9.3
*
* @param {Object} event Event object.
* @param {string} name Connection name.
*/
create( event, name ) {
const connectionId = new Date().getTime().toString( 16 ),
connection = {
id: connectionId,
name,
isNew: true,
};
app.Cache.addTo( app.provider, 'connections', connectionId, connection );
app.connection.generate( {
connection,
} );
},
/**
* Connection is deleted - delete a cache as well.
*
* @since 1.9.3
*
* @param {Object} event Event object.
* @param {Object} $connection jQuery DOM element for a connection.
*/
delete( event, $connection ) {
const $holder = app.Providers.getProviderHolder( app.provider );
if ( ! $connection.closest( $holder ).length ) {
return;
}
const connectionId = $connection.data( 'connection_id' );
if ( _.isString( connectionId ) ) {
app.Cache.deleteFrom( app.provider, 'connections', connectionId );
}
},
/**
* Get the template and data for a connection and process it.
*
* @since 1.9.3
*
* @param {Object} data Connection data.
*
* @return {void}
*/
generate( data ) {
const accounts = app.Cache.get( app.provider, 'accounts' );
if ( _.isEmpty( accounts ) || ! app.account.isAccountExists( data.connection.account_id, accounts ) ) {
return;
}
const actions = app.Cache.get( app.provider, 'actions' ),
lists = app.Cache.get( app.provider, 'lists' );
return app.connection.renderConnections( accounts, lists, actions, data );
},
/**
* Render connections.
*
* @since 1.9.3
*
* @param {Object} accounts List of accounts.
* @param {Object} lists List of lists.
* @param {Object} actions List of actions.
* @param {Object} data Connection data.
*/
renderConnections( accounts, lists, actions, data ) {
if ( ! app.account.isAccountExists( data.connection.account_id, accounts ) ) {
return;
}
const tmplConnection = app.Templates.get( 'wpforms-' + app.provider + '-builder-content-connection' ),
tmplConditional = app.Templates.get( 'wpforms-constant-contact-v3-builder-content-connection-conditionals' ),
conditional = _.has( data.connection, 'isNew' ) && data.connection.isNew ? tmplConditional() : data.conditional;
app.$elements.$connections.prepend(
tmplConnection( {
accounts,
lists,
actions,
connection: data.connection,
conditional,
provider: app.provider,
} )
);
app.$elements.$connections.trigger( 'connectionGenerated', [ data ] );
},
/**
* Fire AJAX-request to retrieve the list of all saved connections.
*
* @since 1.9.3
*/
dataLoad() {
app
.Providers.ajax
.request( app.provider, {
data: {
task: 'connections_get',
},
} )
.done( function( response ) {
if (
! response.success ||
! _.has( response.data, 'connections' )
) {
return;
}
[
'accounts',
'actions',
'actions_fields',
'conditionals',
'connections',
'custom_fields',
'lists',
].forEach( ( dataType ) => {
app.Cache.set( app.provider, dataType, jQuery.extend( {}, response.data[ dataType ] ) );
} );
app.$elements.$connections.trigger( 'connectionsDataLoaded', [ response.data ] );
} );
},
},
/**
* Account property.
*
* @since 1.9.3
*/
account: {
/**
* Check if a provided account is listed inside an account list.
*
* @since 1.9.3
*
* @param {string} accountId Connection account ID to check.
* @param {Object} accounts Array of objects, usually received from API.
*
* @return {boolean} True if an account exists.
*/
isAccountExists( accountId, accounts ) {
if ( _.isEmpty( accounts ) ) {
return false;
}
// New connections that have not been saved don't have the account ID yet.
if ( _.isEmpty( accountId ) ) {
return true;
}
return _.has( accounts, accountId );
},
},
/**
* All methods that modify the UI of a page.
*
* @since 1.9.3
*/
ui: {
/**
* Account field methods.
*
* @since 1.9.3
*/
accountField: {
/**
* Callback-function on change event.
*
* @since 1.9.3
*/
change() {
const $this = $( this ),
$connection = $this.closest( app.selectors.connection ),
$actionName = $( app.selectors.actionField, $connection );
$actionName.prop( 'selectedIndex', 0 ).trigger( 'change' );
// If an account is empty.
if ( _.isEmpty( $this.val() ) ) {
$actionName.prop( 'disabled', true );
$( app.selectors.actionData, $connection ).html( '' );
return;
}
$actionName.prop( 'disabled', false );
$this.removeClass( 'wpforms-error' );
},
},
/**
* Action methods.
*
* @since 1.9.3
*/
actionField: {
/**
* Callback-function on change event.
*
* @since 1.9.3
*/
change() {
const $this = $( this ),
$connection = $this.closest( app.selectors.connection ),
$account = $( app.selectors.accountField, $connection ),
$action = $( app.selectors.actionField, $connection );
app.ui.actionField.render( {
action: 'action',
target: $this,
/* eslint-disable camelcase */
account_id: $account.val(),
action_name: $action.val(),
connection_id: $connection.data( 'connection_id' ),
/* eslint-enable camelcase */
} );
$this.removeClass( 'wpforms-error' );
},
/**
* Render HTML.
*
* @since 1.9.3
*
* @param {Object} args Arguments.
*/
render( args ) {
const fields = app.tmpl.renderActionFields( args ),
$connection = app.connection.getById( args.connection_id ),
$connectionData = $( app.selectors.actionData, $connection );
$connectionData.html( fields );
app.$elements.$holder.trigger( 'connectionRendered', [ app.provider, args.connection_id ] );
},
/**
* Get a list of constant-contact lists.
*
* @since 1.9.3
*
* @param {string} accountId Account ID.
*
* @return {Array} List of constant-contact lists.
*/
getList( accountId ) {
const listsCache = app.Cache.get( app.provider, 'lists' );
return ! _.isEmpty( listsCache ) && ! _.isEmpty( listsCache[ accountId ] ) ? listsCache[ accountId ] : [];
},
},
},
/**
* All methods for JavaScript templates.
*
* @since 1.9.3
*/
tmpl: {
/**
* Compile and retrieve an HTML for common elements.
*
* @since 1.9.3
* @deprecated 1.9.5
*
* @return {string} Compiled HTML.
*/
commonsHTML() {
// eslint-disable-next-line no-console
console.warn( 'WARNING! Function "WPForms.Admin.Builder.Providers.ConstantContactV3.tmpl.commonsHTML()" has been deprecated!' );
const tmplError = app.Templates.get( 'wpforms-' + app.provider + '-builder-content-connection-error' );
return tmplError();
},
/**
* Compile and retrieve an HTML for "Custom Fields Table".
*
* @since 1.9.3
*
* @param {Object} args Arguments
*
* @return {string} Compiled HTML.
*/
renderActionFields( args ) {
const fields = wpf.getFields(),
actionsFields = app.Cache.get( app.provider, 'actions_fields' ),
customFields = app.Cache.get( app.provider, 'custom_fields' ),
connection = app.Cache.getById( app.provider, 'connections', args.connection_id );
let fieldHTML = '';
$.each( actionsFields[ args.target.val() ], function( key, field ) {
if ( key === 'custom_fields' ) {
const tmplFields = app.Templates.get( 'wpforms-providers-builder-content-connection-fields' );
fieldHTML += tmplFields( {
connection,
fields,
provider: {
slug: app.provider,
fields: customFields[ args.account_id ],
},
isSupportSubfields: true,
} );
return;
}
const options = key === 'list' ? app.ui.actionField.getList( args.account_id ) : Object.values( fields );
const templateName = 'wpforms-' + app.provider + '-builder-content-connection-' + field.type + '-field';
const tmplField = app.Templates.get( templateName );
fieldHTML += tmplField( {
connection,
name: key,
field,
provider: {
slug: app.provider,
fields: actionsFields[ args.target.val() ],
},
options,
} );
} );
return fieldHTML;
},
},
};
// Provide access to public functions/properties.
return app;
}( document, window, jQuery ) );
// Initialize.
WPForms.Admin.Builder.Providers.ConstantContactV3.init();
File diff suppressed because one or more lines are too long