startup optimizations
This commit is contained in:
parent
77af1935b3
commit
c5bfde12a6
Binary file not shown.
|
|
@ -3,7 +3,7 @@
|
||||||
* Plugin Name: FECA Mailshots
|
* Plugin Name: FECA Mailshots
|
||||||
* Plugin URI: https://fenedge.co.uk/
|
* Plugin URI: https://fenedge.co.uk/
|
||||||
* Description: FECA mailshots plugin.
|
* Description: FECA mailshots plugin.
|
||||||
* Version: 1.1.5
|
* Version: 1.1.6
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
* Author: FECA
|
* Author: FECA
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,7 @@ final class DataSourcesAdminPage
|
||||||
$dsl = (string) ($draft['dsl_text'] ?? $dsl);
|
$dsl = (string) ($draft['dsl_text'] ?? $dsl);
|
||||||
}
|
}
|
||||||
$result = $this->consumeOptionArray(self::RESULT_OPTION_KEY);
|
$result = $this->consumeOptionArray(self::RESULT_OPTION_KEY);
|
||||||
$sourceFieldsMap = $this->service()->sourceFields();
|
$builtInSources = $this->service()->knownSources();
|
||||||
$schemaList = $this->service()->listSchemas();
|
|
||||||
|
|
||||||
echo '<div class="wrap feca-mailshots-admin">';
|
echo '<div class="wrap feca-mailshots-admin">';
|
||||||
echo $this->renderAdminUiStyles();
|
echo $this->renderAdminUiStyles();
|
||||||
|
|
@ -184,16 +183,13 @@ final class DataSourcesAdminPage
|
||||||
echo '<div class="feca-builder-grid">';
|
echo '<div class="feca-builder-grid">';
|
||||||
echo '<div class="feca-builder-col-left">';
|
echo '<div class="feca-builder-col-left">';
|
||||||
echo '<h3 class="feca-modal-title">Data Sources</h3>';
|
echo '<h3 class="feca-modal-title">Data Sources</h3>';
|
||||||
foreach (array_keys($sourceFieldsMap) as $builtInSource) {
|
foreach ($builtInSources as $builtInSource) {
|
||||||
$escapedSource = htmlspecialchars((string) $builtInSource, ENT_QUOTES);
|
$escapedSource = htmlspecialchars((string) $builtInSource, ENT_QUOTES);
|
||||||
echo '<label><input type="checkbox" class="ds-source-built" value="' . $escapedSource . '"> ' . $escapedSource . '</label><br>';
|
echo '<label><input type="checkbox" class="ds-source-built" value="' . $escapedSource . '"> ' . $escapedSource . '</label><br>';
|
||||||
}
|
}
|
||||||
echo '<br>';
|
echo '<br>';
|
||||||
echo '<strong>Add custom source</strong><br>';
|
echo '<strong>Add custom source</strong><br>';
|
||||||
echo '<label>Schema <select id="ds-builder-schema"><option value="">Select schema</option>';
|
echo '<label>Schema <select id="ds-builder-schema"><option value="">Select schema</option>';
|
||||||
foreach ($schemaList as $schema) {
|
|
||||||
echo '<option value="' . htmlspecialchars((string) $schema, ENT_QUOTES) . '">' . htmlspecialchars((string) $schema) . '</option>';
|
|
||||||
}
|
|
||||||
echo '</select></label> ';
|
echo '</select></label> ';
|
||||||
echo '<label>Table <select id="ds-builder-table"><option value="">Select table</option></select></label> ';
|
echo '<label>Table <select id="ds-builder-table"><option value="">Select table</option></select></label> ';
|
||||||
echo '<button type="button" class="button button-small" id="ds-builder-add-custom">Add</button>';
|
echo '<button type="button" class="button button-small" id="ds-builder-add-custom">Add</button>';
|
||||||
|
|
@ -285,7 +281,7 @@ final class DataSourcesAdminPage
|
||||||
echo '</div></div>';
|
echo '</div></div>';
|
||||||
echo '<script>';
|
echo '<script>';
|
||||||
echo 'window.fecaDataSourcesBuilderConfig = ' . json_encode([
|
echo 'window.fecaDataSourcesBuilderConfig = ' . json_encode([
|
||||||
'sourceFields' => $sourceFieldsMap,
|
'sourceFields' => [],
|
||||||
'restBase' => '/wp-json/mailshots/v1/data-sources',
|
'restBase' => '/wp-json/mailshots/v1/data-sources',
|
||||||
'adminPostApi' => $this->wp->adminUrl('admin-post.php?action=feca_mailshots_data_sources_api'),
|
'adminPostApi' => $this->wp->adminUrl('admin-post.php?action=feca_mailshots_data_sources_api'),
|
||||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
|
||||||
|
|
@ -320,6 +316,9 @@ final class DataSourcesAdminPage
|
||||||
echo 'var customSources=[];';
|
echo 'var customSources=[];';
|
||||||
echo 'var sourceOrder=[];';
|
echo 'var sourceOrder=[];';
|
||||||
echo 'var constraints=[];';
|
echo 'var constraints=[];';
|
||||||
|
echo 'var sourceFieldRequests={};';
|
||||||
|
echo 'var schemasLoaded=false;';
|
||||||
|
echo 'var schemasLoading=false;';
|
||||||
echo 'var isDirty=false;';
|
echo 'var isDirty=false;';
|
||||||
echo 'function confirmDiscard(){if(!isDirty){return true;}return window.confirm("You have unsaved changes. Close without saving?");}';
|
echo 'function confirmDiscard(){if(!isDirty){return true;}return window.confirm("You have unsaved changes. Close without saving?");}';
|
||||||
echo 'if(editorForm){editorForm.querySelectorAll("input,select,textarea").forEach(function(el){el.addEventListener("input",function(){isDirty=true;});el.addEventListener("change",function(){isDirty=true;});});editorForm.addEventListener("submit",function(){isDirty=false;});}';
|
echo 'if(editorForm){editorForm.querySelectorAll("input,select,textarea").forEach(function(el){el.addEventListener("input",function(){isDirty=true;});el.addEventListener("change",function(){isDirty=true;});});editorForm.addEventListener("submit",function(){isDirty=false;});}';
|
||||||
|
|
@ -329,7 +328,57 @@ final class DataSourcesAdminPage
|
||||||
echo 'function noteSourceDeselected(src){sourceOrder=sourceOrder.filter(function(v){return v!==src;});}';
|
echo 'function noteSourceDeselected(src){sourceOrder=sourceOrder.filter(function(v){return v!==src;});}';
|
||||||
echo 'function updateCustomList(){customList.innerHTML="";customSources.forEach(function(src,i){var li=document.createElement("li");li.textContent=src+" ";var b=document.createElement("button");b.type="button";b.className="button-link-delete";b.textContent="Remove";b.onclick=function(){noteSourceDeselected(src);customSources.splice(i,1);updateCustomList();renderRows();updateDsl();};li.appendChild(b);customList.appendChild(li);});}';
|
echo 'function updateCustomList(){customList.innerHTML="";customSources.forEach(function(src,i){var li=document.createElement("li");li.textContent=src+" ";var b=document.createElement("button");b.type="button";b.className="button-link-delete";b.textContent="Remove";b.onclick=function(){noteSourceDeselected(src);customSources.splice(i,1);updateCustomList();renderRows();updateDsl();};li.appendChild(b);customList.appendChild(li);});}';
|
||||||
echo 'function fetchTables(schema){tableSel.innerHTML="<option value=\"\">Loading...</option>";tableSel.style.color="#1d2327";if(tableErr){tableErr.textContent="";}var pickLabel=function(t){if(typeof t==="string"){return t.trim();}if(t===null||t===undefined){return "";}if(typeof t==="number"){return String(t);}if(typeof t==="object"){var direct=[t.table_name,t.name,t.table,t.label];for(var i=0;i<direct.length;i++){var dv=String(direct[i]||"").trim();if(dv){return dv;}}var keys=Object.keys(t||{});for(var k=0;k<keys.length;k++){var key=String(keys[k]||"").trim();if(/^Tables_in_/i.test(key)){var vv=String(t[key]||"").trim();if(vv){return vv;}}}var vals=Object.values(t||{});for(var j=0;j<vals.length;j++){var v=String(vals[j]||"").trim();if(v){return v;}}}return "";};var fill=function(items){tableSel.innerHTML="<option value=\"\">Select table</option>";var invalid=0;(items||[]).forEach(function(t){var label=pickLabel(t);if(!label){invalid++;return;}var o=document.createElement("option");o.value=label;o.textContent=label;tableSel.appendChild(o);});if((items||[]).length===0&&tableErr){tableErr.textContent="No tables returned for this schema. Check DB grants: SELECT and SHOW VIEW on schema tables.";}if(invalid>0&&tableErr){tableErr.textContent="Received "+invalid+" table entries without names; showing only valid table names.";}};var showErr=function(msg){tableSel.innerHTML="<option value=\"\">Select table</option>";if(tableErr){tableErr.textContent=msg||"Table lookup failed. Resolve the error before continuing.";}if(err){err.textContent="Table lookup failed: "+(msg||"unknown error");}};var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=tables&schema="+encodeURIComponent(schema);fetch(u,{credentials:"same-origin"}).then(function(r){if(!r.ok){throw new Error("API "+r.status);}return r.json();}).then(function(j){if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}fill((j&&j.items)||[]);}).catch(function(e){showErr(e&&e.message?e.message:"Need MySQL grants on the selected schema.");});}';
|
echo 'function fetchTables(schema){tableSel.innerHTML="<option value=\"\">Loading...</option>";tableSel.style.color="#1d2327";if(tableErr){tableErr.textContent="";}var pickLabel=function(t){if(typeof t==="string"){return t.trim();}if(t===null||t===undefined){return "";}if(typeof t==="number"){return String(t);}if(typeof t==="object"){var direct=[t.table_name,t.name,t.table,t.label];for(var i=0;i<direct.length;i++){var dv=String(direct[i]||"").trim();if(dv){return dv;}}var keys=Object.keys(t||{});for(var k=0;k<keys.length;k++){var key=String(keys[k]||"").trim();if(/^Tables_in_/i.test(key)){var vv=String(t[key]||"").trim();if(vv){return vv;}}}var vals=Object.values(t||{});for(var j=0;j<vals.length;j++){var v=String(vals[j]||"").trim();if(v){return v;}}}return "";};var fill=function(items){tableSel.innerHTML="<option value=\"\">Select table</option>";var invalid=0;(items||[]).forEach(function(t){var label=pickLabel(t);if(!label){invalid++;return;}var o=document.createElement("option");o.value=label;o.textContent=label;tableSel.appendChild(o);});if((items||[]).length===0&&tableErr){tableErr.textContent="No tables returned for this schema. Check DB grants: SELECT and SHOW VIEW on schema tables.";}if(invalid>0&&tableErr){tableErr.textContent="Received "+invalid+" table entries without names; showing only valid table names.";}};var showErr=function(msg){tableSel.innerHTML="<option value=\"\">Select table</option>";if(tableErr){tableErr.textContent=msg||"Table lookup failed. Resolve the error before continuing.";}if(err){err.textContent="Table lookup failed: "+(msg||"unknown error");}};var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=tables&schema="+encodeURIComponent(schema);fetch(u,{credentials:"same-origin"}).then(function(r){if(!r.ok){throw new Error("API "+r.status);}return r.json();}).then(function(j){if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}fill((j&&j.items)||[]);}).catch(function(e){showErr(e&&e.message?e.message:"Need MySQL grants on the selected schema.");});}';
|
||||||
echo 'function sourceFields(source){var map=cfg.sourceFields||{};if(map[source]){return map[source];}if(source&&source.indexOf(".")!==-1){var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=source_fields&source="+encodeURIComponent(source);fetch(u,{credentials:"same-origin"}).then(function(r){if(!r.ok){throw new Error("API "+r.status);}return r.json();}).then(function(j){if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}var fields=(j&&j.fields)||[];if(fields&&fields.length){map[source]=fields;cfg.sourceFields=map;renderRows();updateDsl();return;}if(err){err.textContent="Source field lookup returned no fields for "+source+".";}}).catch(function(e){if(err){err.textContent="Source field lookup failed for "+source+": "+(e&&e.message?e.message:"unknown error");}});}return [];}';
|
$lazyMetadataJs = <<<'JS'
|
||||||
|
function loadSchemas(){
|
||||||
|
if(schemasLoaded||schemasLoading||!schemaSel){return;}
|
||||||
|
schemasLoading=true;
|
||||||
|
var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=schemas";
|
||||||
|
fetch(u,{credentials:"same-origin"}).then(function(r){
|
||||||
|
if(!r.ok){throw new Error("API "+r.status);}
|
||||||
|
return r.json();
|
||||||
|
}).then(function(j){
|
||||||
|
if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}
|
||||||
|
schemaSel.innerHTML="<option value=\"\">Select schema</option>";
|
||||||
|
(j.items||[]).forEach(function(schema){
|
||||||
|
var value=String(schema||"").trim();
|
||||||
|
if(!value){return;}
|
||||||
|
var option=document.createElement("option");
|
||||||
|
option.value=value;
|
||||||
|
option.textContent=value;
|
||||||
|
schemaSel.appendChild(option);
|
||||||
|
});
|
||||||
|
schemasLoaded=true;
|
||||||
|
schemasLoading=false;
|
||||||
|
}).catch(function(e){
|
||||||
|
schemasLoading=false;
|
||||||
|
if(err){err.textContent="Schema lookup failed: "+(e&&e.message?e.message:"unknown error");}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function sourceFields(source){
|
||||||
|
var map=cfg.sourceFields||{};
|
||||||
|
if(Object.prototype.hasOwnProperty.call(map,source)){return map[source];}
|
||||||
|
if(!source||sourceFieldRequests[source]){return [];}
|
||||||
|
sourceFieldRequests[source]=true;
|
||||||
|
var u=(cfg.adminPostApi||"/wp-admin/admin-post.php?action=feca_mailshots_data_sources_api")+"&op=source_fields&source="+encodeURIComponent(source);
|
||||||
|
fetch(u,{credentials:"same-origin"}).then(function(r){
|
||||||
|
if(!r.ok){throw new Error("API "+r.status);}
|
||||||
|
return r.json();
|
||||||
|
}).then(function(j){
|
||||||
|
delete sourceFieldRequests[source];
|
||||||
|
if(!j||j.ok===false){throw new Error((j&&j.error)||"API error");}
|
||||||
|
var fields=(j&&j.fields)||[];
|
||||||
|
map[source]=fields;
|
||||||
|
cfg.sourceFields=map;
|
||||||
|
if(fields.length){renderRows();updateDsl();return;}
|
||||||
|
if(err){err.textContent="Source field lookup returned no fields for "+source+".";}
|
||||||
|
}).catch(function(e){
|
||||||
|
delete sourceFieldRequests[source];
|
||||||
|
if(err){err.textContent="Source field lookup failed for "+source+": "+(e&&e.message?e.message:"unknown error");}
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
JS;
|
||||||
|
echo $lazyMetadataJs;
|
||||||
echo 'function mkSelect(options,value){var s=document.createElement("select");options.forEach(function(opt){var o=document.createElement("option");o.value=opt[0];o.textContent=opt[1];if(opt[0]===value){o.selected=true;}s.appendChild(o);});return s;}';
|
echo 'function mkSelect(options,value){var s=document.createElement("select");options.forEach(function(opt){var o=document.createElement("option");o.value=opt[0];o.textContent=opt[1];if(opt[0]===value){o.selected=true;}s.appendChild(o);});return s;}';
|
||||||
echo 'function filterArgType(name){for(var i=0;i<filters.length;i++){if(filters[i][0]===name){return filters[i][2]||"none";}}return "none";}';
|
echo 'function filterArgType(name){for(var i=0;i<filters.length;i++){if(filters[i][0]===name){return filters[i][2]||"none";}}return "none";}';
|
||||||
echo 'function addConstraint(){constraints.push({kind:"filter",negate:false,filter:"selected-renewal",filterArg:"",lhsSource:"",lhsField:"",op:"=",rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""});renderRows();updateDsl();}';
|
echo 'function addConstraint(){constraints.push({kind:"filter",negate:false,filter:"selected-renewal",filterArg:"",lhsSource:"",lhsField:"",op:"=",rhsMode:"literal",rhsLiteral:"",rhsSource:"",rhsField:""});renderRows();updateDsl();}';
|
||||||
|
|
@ -350,7 +399,7 @@ final class DataSourcesAdminPage
|
||||||
echo 'if(addCustomBtn){addCustomBtn.onclick=function(){if(!schemaSel.value||!tableSel.value){return;}var src=schemaSel.value+"."+tableSel.value;if(customSources.indexOf(src)===-1){customSources.push(src);}noteSourceSelected(src);updateCustomList();renderRows();updateDsl();};}';
|
echo 'if(addCustomBtn){addCustomBtn.onclick=function(){if(!schemaSel.value||!tableSel.value){return;}var src=schemaSel.value+"."+tableSel.value;if(customSources.indexOf(src)===-1){customSources.push(src);}noteSourceSelected(src);updateCustomList();renderRows();updateDsl();};}';
|
||||||
echo 'builtChecks.forEach(function(c){c.onchange=function(){if(c.checked){noteSourceSelected(c.value);}else{noteSourceDeselected(c.value);}renderRows();updateDsl();};});';
|
echo 'builtChecks.forEach(function(c){c.onchange=function(){if(c.checked){noteSourceSelected(c.value);}else{noteSourceDeselected(c.value);}renderRows();updateDsl();};});';
|
||||||
echo 'if(addRowBtn){addRowBtn.onclick=addConstraint;}';
|
echo 'if(addRowBtn){addRowBtn.onclick=addConstraint;}';
|
||||||
echo 'if(openBtn){openBtn.onclick=function(){resetBuilder();prefillFromDsl();modal.style.display="block";};}';
|
echo 'if(openBtn){openBtn.onclick=function(){loadSchemas();resetBuilder();prefillFromDsl();modal.style.display="block";};}';
|
||||||
echo 'if(cancelBtn){cancelBtn.onclick=function(){modal.style.display="none";};}';
|
echo 'if(cancelBtn){cancelBtn.onclick=function(){modal.style.display="none";};}';
|
||||||
echo 'if(applyBtn){applyBtn.onclick=function(){if(dslInput){dslInput.value=out.value;}modal.style.display="none";};}';
|
echo 'if(applyBtn){applyBtn.onclick=function(){if(dslInput){dslInput.value=out.value;}modal.style.display="none";};}';
|
||||||
echo 'if(openNew){openNew.onclick=function(){if(!confirmDiscard()){return;}if(editorId){editorId.value="";}if(editorName){editorName.value="";}if(dslInput){dslInput.value="";}isDirty=false;editorModal.style.display="block";};}';
|
echo 'if(openNew){openNew.onclick=function(){if(!confirmDiscard()){return;}if(editorId){editorId.value="";}if(editorName){editorName.value="";}if(dslInput){dslInput.value="";}isDirty=false;editorModal.style.display="block";};}';
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ final class SetupAdminPage
|
||||||
|
|
||||||
$this->wp->updateOption(self::OPTION_KEY, $settings);
|
$this->wp->updateOption(self::OPTION_KEY, $settings);
|
||||||
$this->wp->updateOption(self::DOWNLOAD_MEMORY_LIMIT_OPTION, $downloadMemoryLimit);
|
$this->wp->updateOption(self::DOWNLOAD_MEMORY_LIMIT_OPTION, $downloadMemoryLimit);
|
||||||
|
$this->wp->deleteOption(\FecaMailshots\Infrastructure\MailshotSchemaInstaller::STATE_OPTION_KEY);
|
||||||
|
|
||||||
if (!headers_sent()) {
|
if (!headers_sent()) {
|
||||||
$location = $this->wp->adminUrl('admin.php?page=feca-mailshots-setup&saved=1');
|
$location = $this->wp->adminUrl('admin.php?page=feca-mailshots-setup&saved=1');
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,12 @@ final class DataSourceService
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return list<string> */
|
||||||
|
public function knownSources(): array
|
||||||
|
{
|
||||||
|
return $this->metadata->allKnownSources();
|
||||||
|
}
|
||||||
|
|
||||||
/** @return list<string> */
|
/** @return list<string> */
|
||||||
public function listSchemas(): array
|
public function listSchemas(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ use PDO;
|
||||||
|
|
||||||
final class MailshotSchemaInstaller
|
final class MailshotSchemaInstaller
|
||||||
{
|
{
|
||||||
|
public const SCHEMA_VERSION = '1';
|
||||||
|
public const STATE_OPTION_KEY = 'feca_mailshots_schema_state';
|
||||||
|
|
||||||
private DatabaseRouter $router;
|
private DatabaseRouter $router;
|
||||||
|
|
||||||
public function __construct(DatabaseRouter $router)
|
public function __construct(DatabaseRouter $router)
|
||||||
|
|
@ -26,6 +29,16 @@ final class MailshotSchemaInstaller
|
||||||
$this->ensureMailshotQueriesColumns($pdo);
|
$this->ensureMailshotQueriesColumns($pdo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string,string> $dbConfig @return array{version:string,config_hash:string} */
|
||||||
|
public static function migrationTarget(array $dbConfig): array
|
||||||
|
{
|
||||||
|
ksort($dbConfig);
|
||||||
|
return [
|
||||||
|
'version' => self::SCHEMA_VERSION,
|
||||||
|
'config_hash' => hash('sha256', json_encode($dbConfig, JSON_UNESCAPED_SLASHES) ?: ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @return list<string> */
|
/** @return list<string> */
|
||||||
private function createTableSql(): array
|
private function createTableSql(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace FecaMailshots\Infrastructure;
|
|
||||||
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
final class SchemaEnsuringDatabaseRouter implements DatabaseRouter
|
|
||||||
{
|
|
||||||
private DatabaseRouter $inner;
|
|
||||||
private bool $installed = false;
|
|
||||||
|
|
||||||
public function __construct(DatabaseRouter $inner)
|
|
||||||
{
|
|
||||||
$this->inner = $inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mailshotsPdo(): PDO
|
|
||||||
{
|
|
||||||
$this->installOnce();
|
|
||||||
return $this->inner->mailshotsPdo();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function membersPdo(): PDO
|
|
||||||
{
|
|
||||||
return $this->inner->membersPdo();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mailshotsDbName(): string
|
|
||||||
{
|
|
||||||
return $this->inner->mailshotsDbName();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function membersDbName(): string
|
|
||||||
{
|
|
||||||
return $this->inner->membersDbName();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fenDbName(): string
|
|
||||||
{
|
|
||||||
return $this->inner->fenDbName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function installOnce(): void
|
|
||||||
{
|
|
||||||
if ($this->installed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
(new MailshotSchemaInstaller($this->inner))->install();
|
|
||||||
$this->installed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -32,7 +32,6 @@ use FecaMailshots\Infrastructure\MailshotSchemaInstaller;
|
||||||
use FecaMailshots\Infrastructure\DatabaseSourceMetadataProvider;
|
use FecaMailshots\Infrastructure\DatabaseSourceMetadataProvider;
|
||||||
use FecaMailshots\Infrastructure\PhpImapAppender;
|
use FecaMailshots\Infrastructure\PhpImapAppender;
|
||||||
use FecaMailshots\Infrastructure\PdoDatabaseRouter;
|
use FecaMailshots\Infrastructure\PdoDatabaseRouter;
|
||||||
use FecaMailshots\Infrastructure\SchemaEnsuringDatabaseRouter;
|
|
||||||
use FecaMailshots\Infrastructure\WordPressSaltSecretKeyProvider;
|
use FecaMailshots\Infrastructure\WordPressSaltSecretKeyProvider;
|
||||||
use FecaMailshots\Repository\AttachmentRepository;
|
use FecaMailshots\Repository\AttachmentRepository;
|
||||||
use FecaMailshots\Repository\LastRunRepository;
|
use FecaMailshots\Repository\LastRunRepository;
|
||||||
|
|
@ -54,7 +53,7 @@ final class Plugin
|
||||||
|
|
||||||
$c->set('logger', static fn() => new ErrorLogLogger());
|
$c->set('logger', static fn() => new ErrorLogLogger());
|
||||||
|
|
||||||
$c->set(DatabaseRouter::class, static fn() => $router ?? new SchemaEnsuringDatabaseRouter(new PdoDatabaseRouter($dbConfig)));
|
$c->set(DatabaseRouter::class, static fn() => $router ?? new PdoDatabaseRouter($dbConfig));
|
||||||
$c->set(MailshotSchemaInstaller::class, static fn(Container $c) => new MailshotSchemaInstaller($c->get(DatabaseRouter::class)));
|
$c->set(MailshotSchemaInstaller::class, static fn(Container $c) => new MailshotSchemaInstaller($c->get(DatabaseRouter::class)));
|
||||||
|
|
||||||
$c->set(DatabaseSourceMetadataProvider::class, static fn(Container $c) => new DatabaseSourceMetadataProvider($c->get(DatabaseRouter::class)));
|
$c->set(DatabaseSourceMetadataProvider::class, static fn(Container $c) => new DatabaseSourceMetadataProvider($c->get(DatabaseRouter::class)));
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,37 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// Load Composer dependencies (Twig, Dompdf, etc.) when available.
|
/** @return object|null Composer class loader when runtime dependencies are available. */
|
||||||
|
function feca_mailshots_load_composer_dependencies(): ?object
|
||||||
|
{
|
||||||
|
static $attempted = false;
|
||||||
|
static $loader = null;
|
||||||
|
|
||||||
|
if ($attempted) {
|
||||||
|
return is_object($loader) ? $loader : null;
|
||||||
|
}
|
||||||
|
$attempted = true;
|
||||||
|
|
||||||
$autoloadCandidates = [
|
$autoloadCandidates = [
|
||||||
dirname(__DIR__) . '/vendor/autoload.php', // Preferred: packaged/deployed plugin-local vendor
|
dirname(__DIR__) . '/vendor/autoload.php',
|
||||||
dirname(__DIR__, 2) . '/vendor/autoload.php', // Backward-compatible: workspace/shared vendor
|
dirname(__DIR__, 2) . '/vendor/autoload.php',
|
||||||
];
|
];
|
||||||
foreach ($autoloadCandidates as $composerAutoload) {
|
foreach ($autoloadCandidates as $composerAutoload) {
|
||||||
if (is_file($composerAutoload) && feca_mailshots_composer_autoload_is_usable($composerAutoload)) {
|
if (!is_file($composerAutoload) || !feca_mailshots_composer_autoload_is_usable($composerAutoload)) {
|
||||||
try {
|
|
||||||
require_once $composerAutoload;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('FECA Mailshots skipped unusable Composer autoload: ' . $e->getMessage());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
break;
|
try {
|
||||||
|
$candidate = require $composerAutoload;
|
||||||
|
if (is_object($candidate)) {
|
||||||
|
$loader = $candidate;
|
||||||
|
return $loader;
|
||||||
}
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('FECA Mailshots skipped unusable Composer autoload: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function feca_mailshots_composer_autoload_is_usable(string $composerAutoload): bool
|
function feca_mailshots_composer_autoload_is_usable(string $composerAutoload): bool
|
||||||
|
|
@ -55,3 +71,15 @@ spl_autoload_register(static function (string $class): void {
|
||||||
require_once $path;
|
require_once $path;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Composer's generated function files are expensive to load. Load them only
|
||||||
|
// when Twig or Dompdf is first requested, then delegate that initial class load.
|
||||||
|
spl_autoload_register(static function (string $class): void {
|
||||||
|
if (strncmp($class, 'Twig\\', 5) !== 0 && strncmp($class, 'Dompdf\\', 7) !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$loader = feca_mailshots_load_composer_dependencies();
|
||||||
|
if ($loader !== null && method_exists($loader, 'loadClass')) {
|
||||||
|
$loader->loadClass($class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,28 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use FecaMailshots\Plugin;
|
use FecaMailshots\Plugin;
|
||||||
|
use FecaMailshots\Infrastructure\MailshotSchemaInstaller;
|
||||||
|
use FecaMailshots\Infrastructure\PdoDatabaseRouter;
|
||||||
use FecaMailshots\WordPress\ProductionWordPressFacade;
|
use FecaMailshots\WordPress\ProductionWordPressFacade;
|
||||||
|
|
||||||
require_once __DIR__ . '/autoload.php';
|
require_once __DIR__ . '/autoload.php';
|
||||||
|
|
||||||
$setupErrors = [];
|
|
||||||
$getOption = static function (string $key) use (&$setupErrors): string {
|
|
||||||
if (!function_exists('get_option')) {
|
if (!function_exists('get_option')) {
|
||||||
throw new \RuntimeException('WordPress get_option() is unavailable while bootstrapping mailshots plugin.');
|
throw new \RuntimeException('WordPress get_option() is unavailable while bootstrapping mailshots plugin.');
|
||||||
}
|
}
|
||||||
$raw = get_option(\FecaMailshots\Admin\SetupAdminPage::OPTION_KEY, []);
|
|
||||||
if (!is_array($raw)) {
|
$setupErrors = [];
|
||||||
|
$rawSetup = get_option(\FecaMailshots\Admin\SetupAdminPage::OPTION_KEY, []);
|
||||||
|
if (!is_array($rawSetup)) {
|
||||||
$setupErrors[] = 'Mailshots setup option is missing or invalid.';
|
$setupErrors[] = 'Mailshots setup option is missing or invalid.';
|
||||||
return '';
|
$rawSetup = [];
|
||||||
}
|
}
|
||||||
if (!array_key_exists($key, $raw)) {
|
$readSetupValue = static function (string $key) use (&$setupErrors, $rawSetup): string {
|
||||||
|
if (!array_key_exists($key, $rawSetup)) {
|
||||||
$setupErrors[] = 'Missing setup configuration key: ' . $key;
|
$setupErrors[] = 'Missing setup configuration key: ' . $key;
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
$value = trim((string) $raw[$key]);
|
$value = trim((string) $rawSetup[$key]);
|
||||||
if ($value === '') {
|
if ($value === '') {
|
||||||
$setupErrors[] = 'Empty setup configuration value: ' . $key;
|
$setupErrors[] = 'Empty setup configuration value: ' . $key;
|
||||||
}
|
}
|
||||||
|
|
@ -29,16 +32,37 @@ $getOption = static function (string $key) use (&$setupErrors): string {
|
||||||
};
|
};
|
||||||
|
|
||||||
$dbConfig = [
|
$dbConfig = [
|
||||||
'MYSQL_HOST' => $getOption('db_host'),
|
'MYSQL_HOST' => $readSetupValue('db_host'),
|
||||||
'MYSQL_PORT' => $getOption('db_port'),
|
'MYSQL_PORT' => $readSetupValue('db_port'),
|
||||||
'MYSQL_USER' => $getOption('db_user'),
|
'MYSQL_USER' => $readSetupValue('db_user'),
|
||||||
'MYSQL_PASSWORD' => $getOption('db_password'),
|
'MYSQL_PASSWORD' => $readSetupValue('db_password'),
|
||||||
'MAILSHOTS_REMOTE_MYSQL_DB' => $getOption('mailshots_db_name'),
|
'MAILSHOTS_REMOTE_MYSQL_DB' => $readSetupValue('mailshots_db_name'),
|
||||||
'MEMBERS_REMOTE_MYSQL_DB' => $getOption('members_db_name'),
|
'MEMBERS_REMOTE_MYSQL_DB' => $readSetupValue('members_db_name'),
|
||||||
'FEN_REMOTE_MYSQL_DB' => $getOption('fen_db_name'),
|
'FEN_REMOTE_MYSQL_DB' => $readSetupValue('fen_db_name'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$wp = new ProductionWordPressFacade();
|
$wp = new ProductionWordPressFacade();
|
||||||
|
if ($setupErrors === [] && function_exists('is_admin') && is_admin()) {
|
||||||
|
$target = MailshotSchemaInstaller::migrationTarget($dbConfig);
|
||||||
|
$state = get_option(MailshotSchemaInstaller::STATE_OPTION_KEY, []);
|
||||||
|
$sameTarget = is_array($state)
|
||||||
|
&& ($state['version'] ?? '') === $target['version']
|
||||||
|
&& ($state['config_hash'] ?? '') === $target['config_hash'];
|
||||||
|
if ($sameTarget && ($state['status'] ?? '') === 'failed') {
|
||||||
|
$setupErrors[] = 'Mailshots database schema setup failed: ' . (string) ($state['error'] ?? 'unknown error');
|
||||||
|
} elseif (!$sameTarget || ($state['status'] ?? '') !== 'complete') {
|
||||||
|
try {
|
||||||
|
(new MailshotSchemaInstaller(new PdoDatabaseRouter($dbConfig)))->install();
|
||||||
|
update_option(MailshotSchemaInstaller::STATE_OPTION_KEY, $target + ['status' => 'complete']);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
update_option(MailshotSchemaInstaller::STATE_OPTION_KEY, $target + [
|
||||||
|
'status' => 'failed',
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
$setupErrors[] = 'Mailshots database schema setup failed: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($setupErrors !== []) {
|
if ($setupErrors !== []) {
|
||||||
$setupPage = new \FecaMailshots\Admin\SetupAdminPage($wp, array_values(array_unique($setupErrors)), true);
|
$setupPage = new \FecaMailshots\Admin\SetupAdminPage($wp, array_values(array_unique($setupErrors)), true);
|
||||||
$setupPage->register();
|
$setupPage->register();
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,10 @@ Hard constraints for this task:
|
||||||
|
|
||||||
## Dependency Installation Scripts
|
## Dependency Installation Scripts
|
||||||
|
|
||||||
|
Runtime Composer dependencies must be autoloaded lazily when Twig or Dompdf is first used. Ordinary WordPress/admin bootstrap requests must not load Composer-generated runtime function files.
|
||||||
|
|
||||||
|
Mailshot database schema installation/checking must run only when the schema version or database configuration changes. Normal page reads must not execute `CREATE TABLE`, `ALTER TABLE`, or `information_schema` migration checks.
|
||||||
|
|
||||||
- `scripts/install_dependencies.sh`
|
- `scripts/install_dependencies.sh`
|
||||||
- Installs Composer dependencies (`vendor/`) for plugin runtime.
|
- Installs Composer dependencies (`vendor/`) for plugin runtime.
|
||||||
- Installs/bundles editor assets (Jodit + Ace) into `feca_mailshots_plugin/assets/vendor/`.
|
- Installs/bundles editor assets (Jodit + Ace) into `feca_mailshots_plugin/assets/vendor/`.
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,8 @@ Optional:
|
||||||
|
|
||||||
Provide a UI to view, create, update, validate, and preview DSL sentences.
|
Provide a UI to view, create, update, validate, and preview DSL sentences.
|
||||||
|
|
||||||
|
The initial Data Sources page GET must load only the saved query list and built-in source names. Schema enumeration and source-column discovery must be requested lazily within the Build DSL dialog when the user opens or uses the relevant controls; failures must be displayed inside that dialog.
|
||||||
|
|
||||||
### Mailshot data source page
|
### Mailshot data source page
|
||||||
|
|
||||||
Add a dedicated page "Data Sources" under FECA Mailshots admin page for managing data source sentences.
|
Add a dedicated page "Data Sources" under FECA Mailshots admin page for managing data source sentences.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,43 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { adminPath, apiPost, cleanupByNames, ensureDataSource } from './helpers.mjs';
|
import { adminPath, apiPost, cleanupByNames, ensureDataSource } from './helpers.mjs';
|
||||||
|
|
||||||
|
test('data sources: builder metadata is lazy and browser runtime is clean', async ({ page }) => {
|
||||||
|
const runtimeErrors = [];
|
||||||
|
const metadataRequests = [];
|
||||||
|
page.on('pageerror', error => runtimeErrors.push(error.message));
|
||||||
|
page.on('console', message => {
|
||||||
|
if (message.type() === 'error') {
|
||||||
|
runtimeErrors.push(message.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
page.on('request', request => {
|
||||||
|
const url = request.url();
|
||||||
|
if (url.includes('op=schemas') || url.includes('op=source_fields')) {
|
||||||
|
metadataRequests.push(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(adminPath('feca-mailshot-data-sources'));
|
||||||
|
await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible();
|
||||||
|
expect(metadataRequests).toHaveLength(0);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'New Data Source' }).click();
|
||||||
|
await page.locator('#ds-build-dsl-open').click();
|
||||||
|
await expect(page.locator('#ds-builder-modal')).toBeVisible();
|
||||||
|
await expect.poll(() => metadataRequests.filter(url => url.includes('op=schemas')).length).toBe(1);
|
||||||
|
await expect(page.locator('#ds-builder-schema option')).not.toHaveCount(1);
|
||||||
|
|
||||||
|
await page.locator('.ds-source-built[value="contacts"]').check();
|
||||||
|
await page.locator('#ds-builder-add-row').click();
|
||||||
|
const row = page.locator('.feca-builder-row').first();
|
||||||
|
await row.locator('select').first().selectOption('compare');
|
||||||
|
await row.locator('select').nth(1).selectOption('contacts');
|
||||||
|
await expect.poll(() => metadataRequests.filter(url => url.includes('op=source_fields')).length).toBe(1);
|
||||||
|
await expect(row.locator('select').nth(2).locator('option')).not.toHaveCount(1);
|
||||||
|
|
||||||
|
expect(runtimeErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test('data sources: create + preview + validation failure path', async ({ page, request }) => {
|
test('data sources: create + preview + validation failure path', async ({ page, request }) => {
|
||||||
const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||||
const dsName = `e2e_ds_${uniq}`;
|
const dsName = `e2e_ds_${uniq}`;
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,11 @@ if (!defined('ABSPATH')) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$GLOBALS['feca_bootstrap_actions'] = [];
|
$GLOBALS['feca_bootstrap_actions'] = [];
|
||||||
|
$GLOBALS['feca_setup_option_reads'] = 0;
|
||||||
|
|
||||||
function get_option($key, $default = null)
|
function get_option($key, $default = null)
|
||||||
{
|
{
|
||||||
if ($key !== \FecaMailshots\Admin\SetupAdminPage::OPTION_KEY) {
|
$config = [
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
'db_host' => 'invalid-host.local.test',
|
'db_host' => 'invalid-host.local.test',
|
||||||
'db_port' => '3306',
|
'db_port' => '3306',
|
||||||
'db_user' => 'mailshots',
|
'db_user' => 'mailshots',
|
||||||
|
|
@ -22,6 +20,28 @@ function get_option($key, $default = null)
|
||||||
'members_db_name' => 'members',
|
'members_db_name' => 'members',
|
||||||
'fen_db_name' => 'fen',
|
'fen_db_name' => 'fen',
|
||||||
];
|
];
|
||||||
|
if ($key === \FecaMailshots\Admin\SetupAdminPage::OPTION_KEY) {
|
||||||
|
$GLOBALS['feca_setup_option_reads']++;
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
if ($key === \FecaMailshots\Infrastructure\MailshotSchemaInstaller::STATE_OPTION_KEY) {
|
||||||
|
$dbConfig = [
|
||||||
|
'MYSQL_HOST' => $config['db_host'],
|
||||||
|
'MYSQL_PORT' => $config['db_port'],
|
||||||
|
'MYSQL_USER' => $config['db_user'],
|
||||||
|
'MYSQL_PASSWORD' => $config['db_password'],
|
||||||
|
'MAILSHOTS_REMOTE_MYSQL_DB' => $config['mailshots_db_name'],
|
||||||
|
'MEMBERS_REMOTE_MYSQL_DB' => $config['members_db_name'],
|
||||||
|
'FEN_REMOTE_MYSQL_DB' => $config['fen_db_name'],
|
||||||
|
];
|
||||||
|
return \FecaMailshots\Infrastructure\MailshotSchemaInstaller::migrationTarget($dbConfig) + ['status' => 'complete'];
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_admin(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function add_action(string $hook, callable $callback): void
|
function add_action(string $hook, callable $callback): void
|
||||||
|
|
@ -47,5 +67,15 @@ if (!isset($actions['admin_post_feca_mailshots_data_sources_api'], $actions['adm
|
||||||
fwrite(STDERR, "Expected normal handlers to register without touching the database\n");
|
fwrite(STDERR, "Expected normal handlers to register without touching the database\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
if ($GLOBALS['feca_setup_option_reads'] !== 1) {
|
||||||
|
fwrite(STDERR, "Expected setup configuration to be read exactly once during bootstrap\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
foreach (get_included_files() as $includedFile) {
|
||||||
|
if (strpos($includedFile, '/vendor/thecodingmachine/safe/') !== false) {
|
||||||
|
fwrite(STDERR, "Composer runtime function files should remain unloaded during admin bootstrap\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
echo "Bootstrap full setup no DB touch regression test passed\n";
|
echo "Bootstrap full setup no DB touch regression test passed\n";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue