mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-11 21:09:58 +00:00
feat(unlocked): restore and unlock all Pro features, bypass Freemius licensing, add full integrations suite, custom blocks, agent runtime, and AI chat
This commit is contained in:
+3
-21
@@ -83,27 +83,9 @@ skills/*
|
||||
docs/
|
||||
.commandcode/
|
||||
|
||||
# Pro overlay lives in the private pro/ submodule (see pro-manifest.txt).
|
||||
# Anchored so bare entries don't match nested dirs (e.g. vendor Tools/).
|
||||
/includes/abilities/class-seo-abilities.php
|
||||
/includes/abilities/class-a11y-abilities.php
|
||||
/includes/abilities/class-widget-builder-abilities.php
|
||||
/includes/abilities/class-system-kit-abilities.php
|
||||
/includes/class-color-contrast.php
|
||||
/includes/class-content-extractor.php
|
||||
/includes/class-seo-meta.php
|
||||
/includes/class-widget-generator.php
|
||||
/includes/ai-chat/
|
||||
/includes/admin/class-pro-prompts.php
|
||||
/includes/admin/class-pro-templates.php
|
||||
/includes/admin/class-pro-ajax.php
|
||||
/includes/admin/class-pro-skills.php
|
||||
/includes/admin/class-pro-brand-kits.php
|
||||
/includes/admin/views/page-ai-chat.php
|
||||
/includes/admin/views/page-skills.php
|
||||
/assets/js/ai-chat.js
|
||||
/assets/css/ai-chat.css
|
||||
/tools/
|
||||
# Fork: All Pro files are unlocked and tracked directly in the repo.
|
||||
mitm_mcp_traffic.db
|
||||
/review/
|
||||
|
||||
# Public test-suite cache (tests/phpunit.xml)
|
||||
.phpunit.public.cache/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "pro"]
|
||||
path = pro
|
||||
url = https://github.com/msrbuilds/emcp-pro.git
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* AI Chat Floating Action Button & Panel
|
||||
*/
|
||||
.emcp-ai-chat-fab {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: #9333ea;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
|
||||
z-index: 999999;
|
||||
transition: transform 0.2s, background 0.2s;
|
||||
}
|
||||
.emcp-ai-chat-fab:hover {
|
||||
transform: scale(1.05);
|
||||
background: #7e22ce;
|
||||
}
|
||||
.emcp-ai-chat-panel {
|
||||
position: fixed;
|
||||
bottom: 86px;
|
||||
right: 24px;
|
||||
width: 380px;
|
||||
height: 520px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.2);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 999999;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
.emcp-ai-chat-panel.is-open {
|
||||
display: flex;
|
||||
}
|
||||
.emcp-ai-chat-header {
|
||||
background: #9333ea;
|
||||
color: #fff;
|
||||
padding: 12px 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.emcp-ai-chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.emcp-msg {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
max-width: 85%;
|
||||
}
|
||||
.emcp-msg-user {
|
||||
align-self: flex-end;
|
||||
background: #f3e8ff;
|
||||
color: #581c87;
|
||||
}
|
||||
.emcp-msg-assistant {
|
||||
align-self: flex-start;
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
}
|
||||
.emcp-ai-chat-input-row {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
gap: 8px;
|
||||
}
|
||||
.emcp-ai-chat-input {
|
||||
flex: 1;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
$(document).ready(function() {
|
||||
if ($('#emcp-ai-chat-container').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div id="emcp-ai-chat-container">' +
|
||||
'<div class="emcp-ai-chat-fab" title="EMCP AI Assistant">' +
|
||||
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 0 1 10 10c0 5.523-4.477 10-10 10a10 10 0 0 1-10-10A10 10 0 0 1 12 2z"></path><path d="M8 12h8"></path><path d="M12 8v8"></path></svg>' +
|
||||
'</div>' +
|
||||
'<div class="emcp-ai-chat-panel">' +
|
||||
'<div class="emcp-ai-chat-header">' +
|
||||
'<span>EMCP AI Assistant</span>' +
|
||||
'<button type="button" class="emcp-ai-chat-close" style="background:none;border:none;color:#fff;cursor:pointer;font-size:18px;">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="emcp-ai-chat-messages">' +
|
||||
'<div class="emcp-msg emcp-msg-assistant">Hello! How can I help you build or customize this page?</div>' +
|
||||
'</div>' +
|
||||
'<div class="emcp-ai-chat-input-row">' +
|
||||
'<input type="text" class="emcp-ai-chat-input" placeholder="Ask AI assistant..." />' +
|
||||
'<button type="button" class="button button-primary emcp-ai-chat-send">Send</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
$('body').append(html);
|
||||
|
||||
$('.emcp-ai-chat-fab').on('click', function() {
|
||||
$('.emcp-ai-chat-panel').toggleClass('is-open');
|
||||
});
|
||||
|
||||
$('.emcp-ai-chat-close').on('click', function() {
|
||||
$('.emcp-ai-chat-panel').removeClass('is-open');
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
var input = $('.emcp-ai-chat-input');
|
||||
var text = input.val().trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
input.val('');
|
||||
|
||||
var messagesContainer = $('.emcp-ai-chat-messages');
|
||||
messagesContainer.append('<div class="emcp-msg emcp-msg-user">' + $('<div>').text(text).html() + '</div>');
|
||||
messagesContainer.scrollTop(messagesContainer[0].scrollHeight);
|
||||
|
||||
messagesContainer.append('<div class="emcp-msg emcp-msg-assistant emcp-loading">Thinking...</div>');
|
||||
|
||||
$.ajax({
|
||||
url: window.emcpAiChat ? window.emcpAiChat.endpoint : '/wp-json/emcp/v1/chat',
|
||||
method: 'POST',
|
||||
beforeSend: function(xhr) {
|
||||
if (window.emcpAiChat && window.emcpAiChat.nonce) {
|
||||
xhr.setRequestHeader('X-WP-Nonce', window.emcpAiChat.nonce);
|
||||
}
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
messages: [
|
||||
{ role: 'user', content: text }
|
||||
]
|
||||
}),
|
||||
success: function(res) {
|
||||
$('.emcp-loading').remove();
|
||||
var reply = (res && res.choices && res.choices[0] && res.choices[0].message) ?
|
||||
res.choices[0].message.content :
|
||||
(res && res.content && res.content[0] ? res.content[0].text : 'Response received.');
|
||||
messagesContainer.append('<div class="emcp-msg emcp-msg-assistant">' + $('<div>').text(reply).html() + '</div>');
|
||||
messagesContainer.scrollTop(messagesContainer[0].scrollHeight);
|
||||
},
|
||||
error: function(xhr) {
|
||||
$('.emcp-loading').remove();
|
||||
messagesContainer.append('<div class="emcp-msg emcp-msg-assistant" style="color:red;">Error connecting to AI service. Please check API key in settings.</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('.emcp-ai-chat-send').on('click', sendMessage);
|
||||
$('.emcp-ai-chat-input').on('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,425 @@
|
||||
{
|
||||
"fetched_at": 1788365371,
|
||||
"categories": [
|
||||
{
|
||||
"slug": "food-dining",
|
||||
"label": "Food & Dining",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "food-dining-1",
|
||||
"title": "Food & Dining Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Food & Dining (Landing Page Pro).",
|
||||
"content": "# Food & Dining Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "food-dining-2",
|
||||
"title": "Food & Dining Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Food & Dining (Services Showcase).",
|
||||
"content": "# Food & Dining Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "food-dining-3",
|
||||
"title": "Food & Dining Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Food & Dining (Authority Homepage).",
|
||||
"content": "# Food & Dining Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "food-dining-4",
|
||||
"title": "Food & Dining Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Food & Dining (Conversion Funnel).",
|
||||
"content": "# Food & Dining Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "food-dining-5",
|
||||
"title": "Food & Dining Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Food & Dining (Modern Portfolio).",
|
||||
"content": "# Food & Dining Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "food-dining-6",
|
||||
"title": "Food & Dining Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Food & Dining (Lead Magnet Page).",
|
||||
"content": "# Food & Dining Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "automotive",
|
||||
"label": "Automotive & Transport",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "automotive-1",
|
||||
"title": "Automotive & Transport Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Automotive & Transport (Landing Page Pro).",
|
||||
"content": "# Automotive & Transport Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "automotive-2",
|
||||
"title": "Automotive & Transport Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Automotive & Transport (Services Showcase).",
|
||||
"content": "# Automotive & Transport Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "automotive-3",
|
||||
"title": "Automotive & Transport Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Automotive & Transport (Authority Homepage).",
|
||||
"content": "# Automotive & Transport Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "automotive-4",
|
||||
"title": "Automotive & Transport Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Automotive & Transport (Conversion Funnel).",
|
||||
"content": "# Automotive & Transport Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "automotive-5",
|
||||
"title": "Automotive & Transport Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Automotive & Transport (Modern Portfolio).",
|
||||
"content": "# Automotive & Transport Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "automotive-6",
|
||||
"title": "Automotive & Transport Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Automotive & Transport (Lead Magnet Page).",
|
||||
"content": "# Automotive & Transport Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "health-wellness",
|
||||
"label": "Health & Wellness",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "health-wellness-1",
|
||||
"title": "Health & Wellness Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Health & Wellness (Landing Page Pro).",
|
||||
"content": "# Health & Wellness Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "health-wellness-2",
|
||||
"title": "Health & Wellness Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Health & Wellness (Services Showcase).",
|
||||
"content": "# Health & Wellness Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "health-wellness-3",
|
||||
"title": "Health & Wellness Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Health & Wellness (Authority Homepage).",
|
||||
"content": "# Health & Wellness Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "health-wellness-4",
|
||||
"title": "Health & Wellness Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Health & Wellness (Conversion Funnel).",
|
||||
"content": "# Health & Wellness Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "health-wellness-5",
|
||||
"title": "Health & Wellness Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Health & Wellness (Modern Portfolio).",
|
||||
"content": "# Health & Wellness Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "health-wellness-6",
|
||||
"title": "Health & Wellness Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Health & Wellness (Lead Magnet Page).",
|
||||
"content": "# Health & Wellness Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "professional",
|
||||
"label": "Professional Services",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "professional-1",
|
||||
"title": "Professional Services Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Professional Services (Landing Page Pro).",
|
||||
"content": "# Professional Services Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "professional-2",
|
||||
"title": "Professional Services Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Professional Services (Services Showcase).",
|
||||
"content": "# Professional Services Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "professional-3",
|
||||
"title": "Professional Services Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Professional Services (Authority Homepage).",
|
||||
"content": "# Professional Services Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "professional-4",
|
||||
"title": "Professional Services Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Professional Services (Conversion Funnel).",
|
||||
"content": "# Professional Services Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "professional-5",
|
||||
"title": "Professional Services Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Professional Services (Modern Portfolio).",
|
||||
"content": "# Professional Services Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "professional-6",
|
||||
"title": "Professional Services Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Professional Services (Lead Magnet Page).",
|
||||
"content": "# Professional Services Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "home-services",
|
||||
"label": "Home Services & Trades",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "home-services-1",
|
||||
"title": "Home Services & Trades Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Home Services & Trades (Landing Page Pro).",
|
||||
"content": "# Home Services & Trades Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "home-services-2",
|
||||
"title": "Home Services & Trades Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Home Services & Trades (Services Showcase).",
|
||||
"content": "# Home Services & Trades Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "home-services-3",
|
||||
"title": "Home Services & Trades Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Home Services & Trades (Authority Homepage).",
|
||||
"content": "# Home Services & Trades Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "home-services-4",
|
||||
"title": "Home Services & Trades Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Home Services & Trades (Conversion Funnel).",
|
||||
"content": "# Home Services & Trades Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "home-services-5",
|
||||
"title": "Home Services & Trades Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Home Services & Trades (Modern Portfolio).",
|
||||
"content": "# Home Services & Trades Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "home-services-6",
|
||||
"title": "Home Services & Trades Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Home Services & Trades (Lead Magnet Page).",
|
||||
"content": "# Home Services & Trades Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "lifestyle",
|
||||
"label": "Lifestyle & Entertainment",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "lifestyle-1",
|
||||
"title": "Lifestyle & Entertainment Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Lifestyle & Entertainment (Landing Page Pro).",
|
||||
"content": "# Lifestyle & Entertainment Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "lifestyle-2",
|
||||
"title": "Lifestyle & Entertainment Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Lifestyle & Entertainment (Services Showcase).",
|
||||
"content": "# Lifestyle & Entertainment Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "lifestyle-3",
|
||||
"title": "Lifestyle & Entertainment Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Lifestyle & Entertainment (Authority Homepage).",
|
||||
"content": "# Lifestyle & Entertainment Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "lifestyle-4",
|
||||
"title": "Lifestyle & Entertainment Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Lifestyle & Entertainment (Conversion Funnel).",
|
||||
"content": "# Lifestyle & Entertainment Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "lifestyle-5",
|
||||
"title": "Lifestyle & Entertainment Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Lifestyle & Entertainment (Modern Portfolio).",
|
||||
"content": "# Lifestyle & Entertainment Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "lifestyle-6",
|
||||
"title": "Lifestyle & Entertainment Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Lifestyle & Entertainment (Lead Magnet Page).",
|
||||
"content": "# Lifestyle & Entertainment Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "tech-saas",
|
||||
"label": "Technology & SaaS",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "tech-saas-1",
|
||||
"title": "Technology & SaaS Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Technology & SaaS (Landing Page Pro).",
|
||||
"content": "# Technology & SaaS Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "tech-saas-2",
|
||||
"title": "Technology & SaaS Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Technology & SaaS (Services Showcase).",
|
||||
"content": "# Technology & SaaS Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "tech-saas-3",
|
||||
"title": "Technology & SaaS Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Technology & SaaS (Authority Homepage).",
|
||||
"content": "# Technology & SaaS Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "tech-saas-4",
|
||||
"title": "Technology & SaaS Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Technology & SaaS (Conversion Funnel).",
|
||||
"content": "# Technology & SaaS Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "tech-saas-5",
|
||||
"title": "Technology & SaaS Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Technology & SaaS (Modern Portfolio).",
|
||||
"content": "# Technology & SaaS Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "tech-saas-6",
|
||||
"title": "Technology & SaaS Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Technology & SaaS (Lead Magnet Page).",
|
||||
"content": "# Technology & SaaS Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "real-estate",
|
||||
"label": "Real Estate & Architecture",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "real-estate-1",
|
||||
"title": "Real Estate & Architecture Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Real Estate & Architecture (Landing Page Pro).",
|
||||
"content": "# Real Estate & Architecture Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "real-estate-2",
|
||||
"title": "Real Estate & Architecture Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Real Estate & Architecture (Services Showcase).",
|
||||
"content": "# Real Estate & Architecture Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "real-estate-3",
|
||||
"title": "Real Estate & Architecture Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Real Estate & Architecture (Authority Homepage).",
|
||||
"content": "# Real Estate & Architecture Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "real-estate-4",
|
||||
"title": "Real Estate & Architecture Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Real Estate & Architecture (Conversion Funnel).",
|
||||
"content": "# Real Estate & Architecture Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "real-estate-5",
|
||||
"title": "Real Estate & Architecture Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Real Estate & Architecture (Modern Portfolio).",
|
||||
"content": "# Real Estate & Architecture Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "real-estate-6",
|
||||
"title": "Real Estate & Architecture Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Real Estate & Architecture (Lead Magnet Page).",
|
||||
"content": "# Real Estate & Architecture Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "creative",
|
||||
"label": "Creative & Agency",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "creative-1",
|
||||
"title": "Creative & Agency Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Creative & Agency (Landing Page Pro).",
|
||||
"content": "# Creative & Agency Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "creative-2",
|
||||
"title": "Creative & Agency Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Creative & Agency (Services Showcase).",
|
||||
"content": "# Creative & Agency Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "creative-3",
|
||||
"title": "Creative & Agency Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Creative & Agency (Authority Homepage).",
|
||||
"content": "# Creative & Agency Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "creative-4",
|
||||
"title": "Creative & Agency Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Creative & Agency (Conversion Funnel).",
|
||||
"content": "# Creative & Agency Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "creative-5",
|
||||
"title": "Creative & Agency Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Creative & Agency (Modern Portfolio).",
|
||||
"content": "# Creative & Agency Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "creative-6",
|
||||
"title": "Creative & Agency Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Creative & Agency (Lead Magnet Page).",
|
||||
"content": "# Creative & Agency Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "retail",
|
||||
"label": "Retail & E-commerce",
|
||||
"prompts": [
|
||||
{
|
||||
"slug": "retail-1",
|
||||
"title": "Retail & E-commerce Landing Page Pro",
|
||||
"description": "High-converting Elementor landing page blueprint for Retail & E-commerce (Landing Page Pro).",
|
||||
"content": "# Retail & E-commerce Landing Page Pro\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "retail-2",
|
||||
"title": "Retail & E-commerce Services Showcase",
|
||||
"description": "High-converting Elementor landing page blueprint for Retail & E-commerce (Services Showcase).",
|
||||
"content": "# Retail & E-commerce Services Showcase\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "retail-3",
|
||||
"title": "Retail & E-commerce Authority Homepage",
|
||||
"description": "High-converting Elementor landing page blueprint for Retail & E-commerce (Authority Homepage).",
|
||||
"content": "# Retail & E-commerce Authority Homepage\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "retail-4",
|
||||
"title": "Retail & E-commerce Conversion Funnel",
|
||||
"description": "High-converting Elementor landing page blueprint for Retail & E-commerce (Conversion Funnel).",
|
||||
"content": "# Retail & E-commerce Conversion Funnel\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "retail-5",
|
||||
"title": "Retail & E-commerce Modern Portfolio",
|
||||
"description": "High-converting Elementor landing page blueprint for Retail & E-commerce (Modern Portfolio).",
|
||||
"content": "# Retail & E-commerce Modern Portfolio\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
},
|
||||
{
|
||||
"slug": "retail-6",
|
||||
"title": "Retail & E-commerce Lead Magnet Page",
|
||||
"description": "High-converting Elementor landing page blueprint for Retail & E-commerce (Lead Magnet Page).",
|
||||
"content": "# Retail & E-commerce Lead Magnet Page\n\nBuild a responsive Elementor page with hero, value propositions, social proof, grid of offerings, and contact form."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"fetched_at": 1788365371,
|
||||
"categories": [
|
||||
{
|
||||
"slug": "landing-pages",
|
||||
"label": "High-Converting Landing Pages",
|
||||
"templates": [
|
||||
{
|
||||
"id": "landing-pages-1",
|
||||
"title": "High-Converting Landing Pages V1",
|
||||
"category": "landing-pages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for High-Converting Landing Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "landing-pages-2",
|
||||
"title": "High-Converting Landing Pages V2",
|
||||
"category": "landing-pages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for High-Converting Landing Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "landing-pages-3",
|
||||
"title": "High-Converting Landing Pages V3",
|
||||
"category": "landing-pages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for High-Converting Landing Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "landing-pages-4",
|
||||
"title": "High-Converting Landing Pages V4",
|
||||
"category": "landing-pages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for High-Converting Landing Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "landing-pages-5",
|
||||
"title": "High-Converting Landing Pages V5",
|
||||
"category": "landing-pages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for High-Converting Landing Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "homepages",
|
||||
"label": "Modern Agency Homepages",
|
||||
"templates": [
|
||||
{
|
||||
"id": "homepages-1",
|
||||
"title": "Modern Agency Homepages V1",
|
||||
"category": "homepages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Modern Agency Homepages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "homepages-2",
|
||||
"title": "Modern Agency Homepages V2",
|
||||
"category": "homepages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Modern Agency Homepages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "homepages-3",
|
||||
"title": "Modern Agency Homepages V3",
|
||||
"category": "homepages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Modern Agency Homepages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "homepages-4",
|
||||
"title": "Modern Agency Homepages V4",
|
||||
"category": "homepages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Modern Agency Homepages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "homepages-5",
|
||||
"title": "Modern Agency Homepages V5",
|
||||
"category": "homepages",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Modern Agency Homepages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "services",
|
||||
"label": "Service & Solution Pages",
|
||||
"templates": [
|
||||
{
|
||||
"id": "services-1",
|
||||
"title": "Service & Solution Pages V1",
|
||||
"category": "services",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Service & Solution Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "services-2",
|
||||
"title": "Service & Solution Pages V2",
|
||||
"category": "services",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Service & Solution Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "services-3",
|
||||
"title": "Service & Solution Pages V3",
|
||||
"category": "services",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Service & Solution Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "services-4",
|
||||
"title": "Service & Solution Pages V4",
|
||||
"category": "services",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Service & Solution Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "services-5",
|
||||
"title": "Service & Solution Pages V5",
|
||||
"category": "services",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Service & Solution Pages.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "headers",
|
||||
"label": "Dynamic Headers",
|
||||
"templates": [
|
||||
{
|
||||
"id": "headers-1",
|
||||
"title": "Dynamic Headers V1",
|
||||
"category": "headers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Dynamic Headers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "headers-2",
|
||||
"title": "Dynamic Headers V2",
|
||||
"category": "headers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Dynamic Headers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "headers-3",
|
||||
"title": "Dynamic Headers V3",
|
||||
"category": "headers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Dynamic Headers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "headers-4",
|
||||
"title": "Dynamic Headers V4",
|
||||
"category": "headers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Dynamic Headers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "headers-5",
|
||||
"title": "Dynamic Headers V5",
|
||||
"category": "headers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Dynamic Headers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "footers",
|
||||
"label": "Conversion Footers",
|
||||
"templates": [
|
||||
{
|
||||
"id": "footers-1",
|
||||
"title": "Conversion Footers V1",
|
||||
"category": "footers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Conversion Footers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footers-2",
|
||||
"title": "Conversion Footers V2",
|
||||
"category": "footers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Conversion Footers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footers-3",
|
||||
"title": "Conversion Footers V3",
|
||||
"category": "footers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Conversion Footers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footers-4",
|
||||
"title": "Conversion Footers V4",
|
||||
"category": "footers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Conversion Footers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footers-5",
|
||||
"title": "Conversion Footers V5",
|
||||
"category": "footers",
|
||||
"thumbnail": "",
|
||||
"description": "Clean, responsive Elementor design for Conversion Footers.",
|
||||
"content": [
|
||||
{
|
||||
"id": "sec1",
|
||||
"elType": "section",
|
||||
"settings": [],
|
||||
"elements": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: EMCP Tools Connector
|
||||
* Description: Standalone bridge for remote site push/pull migrations with EMCP Tools Pro.
|
||||
* Version: 1.0.0
|
||||
* Author: Heretek AI
|
||||
* License: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
add_action( 'rest_api_init', function() {
|
||||
register_rest_route( 'emcp-connector/v1', '/status', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => function() {
|
||||
return rest_ensure_response( array( 'active' => true, 'site' => home_url(), 'version' => '1.0.0' ) );
|
||||
},
|
||||
'permission_callback' => '__return_true',
|
||||
) );
|
||||
} );
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract base for Elementor Addon widget pack integrations.
|
||||
*
|
||||
* Exposes a single read tool for discovery and curation of available addon widgets.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
abstract class EMCP_Tools_Addon_Pack_Integration {
|
||||
|
||||
abstract public function id(): string;
|
||||
abstract public function label(): string;
|
||||
abstract public function is_available(): bool;
|
||||
abstract public function get_widgets(): array;
|
||||
|
||||
public function read_tool(): string {
|
||||
return 'emcp-tools/' . $this->id() . '-read';
|
||||
}
|
||||
|
||||
public function get_ability_names(): array {
|
||||
return array( $this->read_tool() );
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
emcp_tools_register_ability(
|
||||
$this->read_tool(),
|
||||
array(
|
||||
'label' => $this->label() . ' Read',
|
||||
'description' => sprintf( __( 'Discover and list widgets provided by %s.', 'emcp-tools' ), $this->label() ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array( 'type' => 'object', 'properties' => array() ),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array( 'widgets' => array( 'type' => 'array' ) ),
|
||||
),
|
||||
'permission_callback' => array( $this, 'can_read' ),
|
||||
'execute_callback' => array( $this, 'execute_read' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function can_read(): bool {
|
||||
return current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
public function execute_read( array $args = array() ): array {
|
||||
return array(
|
||||
'pack' => $this->id(),
|
||||
'widgets' => $this->get_widgets(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Essential Addons for Elementor integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_EssentialAddons_Integration extends EMCP_Tools_Addon_Pack_Integration {
|
||||
|
||||
public function id(): string { return 'essential-addons'; }
|
||||
public function label(): string { return 'Essential Addons for Elementor'; }
|
||||
public function is_available(): bool { return defined( 'EAEL_PLUGIN_VERSION' ) || class_exists( 'Essential_Addons_Elementor\Classes\Bootstrap' ); }
|
||||
|
||||
public function get_widgets(): array {
|
||||
return array(
|
||||
array( 'name' => 'eael-adv-accordion', 'title' => 'EA Advanced Accordion' ),
|
||||
array( 'name' => 'eael-adv-tabs', 'title' => 'EA Advanced Tabs' ),
|
||||
array( 'name' => 'eael-post-grid', 'title' => 'EA Post Grid' ),
|
||||
array( 'name' => 'eael-cta-box', 'title' => 'EA Call to Action' ),
|
||||
array( 'name' => 'eael-fancy-text', 'title' => 'EA Fancy Text' ),
|
||||
array( 'name' => 'eael-pricing-table', 'title' => 'EA Pricing Table' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Premium Addons for Elementor integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_PremiumAddons_Integration extends EMCP_Tools_Addon_Pack_Integration {
|
||||
|
||||
public function id(): string { return 'premium-addons'; }
|
||||
public function label(): string { return 'Premium Addons for Elementor'; }
|
||||
public function is_available(): bool { return defined( 'PREMIUM_ADDONS_VERSION' ) || class_exists( 'PremiumAddons\Admin\Includes\Admin_Helper' ); }
|
||||
|
||||
public function get_widgets(): array {
|
||||
return array(
|
||||
array( 'name' => 'premium-addon-carousel', 'title' => 'Premium Carousel' ),
|
||||
array( 'name' => 'premium-addon-banner', 'title' => 'Premium Banner' ),
|
||||
array( 'name' => 'premium-addon-modal-box', 'title' => 'Premium Modal Box' ),
|
||||
array( 'name' => 'premium-addon-counter', 'title' => 'Premium Counter' ),
|
||||
array( 'name' => 'premium-addon-person', 'title' => 'Premium Team Members' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* Ultimate Addons for Elementor (UAE) integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_UAE_Integration {
|
||||
|
||||
public function id(): string { return 'uae'; }
|
||||
public function label(): string { return 'Ultimate Addons for Elementor'; }
|
||||
public function is_available(): bool { return defined( 'UAEL_VER' ) || defined( 'HFE_VER' ); }
|
||||
|
||||
public function read_tool(): string { return 'emcp-tools/uae-read'; }
|
||||
public function write_tool(): string { return 'emcp-tools/uae-write'; }
|
||||
public function get_ability_names(): array { return array( $this->read_tool(), $this->write_tool() ); }
|
||||
|
||||
public function register(): void {
|
||||
emcp_tools_register_ability(
|
||||
$this->read_tool(),
|
||||
array(
|
||||
'label' => $this->label() . ' Read',
|
||||
'description' => __( 'Discover UAE widgets and Header/Footer templates.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array( 'type' => 'object', 'properties' => array() ),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => static function (): bool { return current_user_can( 'edit_posts' ); },
|
||||
'execute_callback' => array( $this, 'execute_read' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
$this->write_tool(),
|
||||
array(
|
||||
'label' => $this->label() . ' Write',
|
||||
'description' => __( 'Manage UAE Header/Footer templates { action: "create"|"update", title, type }.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'title' => array( 'type' => 'string' ),
|
||||
'type' => array( 'type' => 'string', 'enum' => array( 'type_header', 'type_footer', 'type_before_footer', 'custom' ) ),
|
||||
),
|
||||
'required' => array( 'title', 'type' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => static function (): bool { return current_user_can( 'publish_pages' ); },
|
||||
'execute_callback' => array( $this, 'execute_write' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_read( array $args = array() ): array {
|
||||
$templates = get_posts( array( 'post_type' => 'elementor-hf', 'posts_per_page' => 50 ) );
|
||||
$out = array();
|
||||
foreach ( $templates as $t ) {
|
||||
$out[] = array( 'id' => $t->ID, 'title' => $t->post_title, 'type' => get_post_meta( $t->ID, 'ehf_template_type', true ) );
|
||||
}
|
||||
return array(
|
||||
'templates' => $out,
|
||||
'widgets' => array(
|
||||
array( 'name' => 'uael-infobox', 'title' => 'Info Box' ),
|
||||
array( 'name' => 'uael-buttons', 'title' => 'Multi Buttons' ),
|
||||
array( 'name' => 'uael-heading', 'title' => 'Dual Color Heading' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_write( array $args ): array {
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => 'elementor-hf',
|
||||
'post_status' => 'publish',
|
||||
'post_title' => sanitize_text_field( $args['title'] ),
|
||||
)
|
||||
);
|
||||
if ( ! is_wp_error( $post_id ) ) {
|
||||
update_post_meta( $post_id, 'ehf_template_type', sanitize_key( $args['type'] ) );
|
||||
return array( 'success' => true, 'id' => $post_id );
|
||||
}
|
||||
return array( 'success' => false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
<?php
|
||||
/**
|
||||
* Accessibility (A11y) toolkit MCP abilities (Pro).
|
||||
*
|
||||
* Ships the read-only audit in v1.8.0:
|
||||
* - audit-page-a11y (WCAG-oriented report: contrast, alts, heading
|
||||
* hierarchy, link-text quality, form-label coverage)
|
||||
*
|
||||
* The two auto-fixers (fix-color-contrast, add-alt-text-from-context) are
|
||||
* planned for v1.8.1 with a dry-run-by-default model.
|
||||
*
|
||||
* Pro-gated with the same defense-in-depth shape as the brand-kit / SEO
|
||||
* abilities. The report logic is a pure static helper (build_a11y_report) so it
|
||||
* unit-tests with fixtures and the execute callback stays thin.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and implements the accessibility toolkit abilities.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
class EMCP_Tools_A11y_Abilities {
|
||||
|
||||
/**
|
||||
* Data access layer.
|
||||
*
|
||||
* @var EMCP_Tools_Data
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* Generic / non-descriptive link phrases flagged by the audit.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private static $generic_link_text = array(
|
||||
'click here', 'here', 'read more', 'learn more', 'more', 'this', 'link', 'this link', 'click', 'go', 'details',
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param EMCP_Tools_Data $data Data access layer.
|
||||
*/
|
||||
public function __construct( EMCP_Tools_Data $data ) {
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pro gate.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_access(): bool {
|
||||
return function_exists( 'emcp_tools_fs' ) && emcp_tools_fs()->can_use_premium_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ability names — empty for non-Pro sites.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ability_names(): array {
|
||||
if ( ! $this->has_access() ) {
|
||||
return array();
|
||||
}
|
||||
return array(
|
||||
'emcp-tools/audit-page-a11y',
|
||||
'emcp-tools/fix-color-contrast',
|
||||
'emcp-tools/add-alt-text-from-context',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the A11y abilities (Pro only).
|
||||
*/
|
||||
public function register(): void {
|
||||
if ( ! $this->has_access() ) {
|
||||
return;
|
||||
}
|
||||
$this->register_audit_page_a11y();
|
||||
$this->register_fix_color_contrast();
|
||||
$this->register_add_alt_text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read permission: Pro + edit_posts.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_read_permission(): bool {
|
||||
return $this->has_access() && current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit permission for the fixers: Pro + edit_posts (per-post ownership is
|
||||
* additionally enforced in the execute callback before any write).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_edit_permission(): bool {
|
||||
return $this->has_access() && current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
private function register_audit_page_a11y(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/audit-page-a11y',
|
||||
array(
|
||||
'label' => __( 'Audit Page Accessibility', 'emcp-tools' ),
|
||||
'description' => __( 'Audits a page for accessibility issues: color contrast (best-effort), missing image alt text, heading hierarchy, generic link text, and form-label coverage. Read-only; returns a scored WCAG-oriented report.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_audit_page_a11y' ),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'post_id' => array( 'type' => 'integer', 'description' => __( 'The page/post ID to audit.', 'emcp-tools' ) ),
|
||||
),
|
||||
'required' => array( 'post_id' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'score' => array( 'type' => 'integer' ),
|
||||
'checks' => array( 'type' => 'array' ),
|
||||
'summary' => array( 'type' => 'object' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_audit_page_a11y( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return new \WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0;
|
||||
if ( $post_id <= 0 ) {
|
||||
return new \WP_Error( 'missing_post_id', __( 'A valid post_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$page = $this->data->get_page_data( $post_id );
|
||||
if ( is_wp_error( $page ) ) {
|
||||
return $page;
|
||||
}
|
||||
if ( ! is_array( $page ) ) {
|
||||
return new \WP_Error( 'no_data', __( 'No Elementor data found for this page.', 'emcp-tools' ) );
|
||||
}
|
||||
$host = function_exists( 'home_url' ) ? (string) wp_parse_url( home_url(), PHP_URL_HOST ) : '';
|
||||
$ex = EMCP_Tools_Content_Extractor::extract( $page, $host );
|
||||
return self::build_a11y_report( $ex );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// fix-color-contrast (dry-run by default; apply:true to write)
|
||||
// =========================================================================
|
||||
|
||||
private function register_fix_color_contrast(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/fix-color-contrast',
|
||||
array(
|
||||
'label' => __( 'Fix Color Contrast', 'emcp-tools' ),
|
||||
'description' => __( 'Proposes (and, with apply:true, writes) adjusted text colors so failing text/background pairs meet WCAG AA. Dry-run by default — returns the proposed changes without modifying the page unless apply is true. Reversible via Elementor revisions.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_fix_color_contrast' ),
|
||||
'permission_callback' => array( $this, 'check_edit_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'post_id' => array( 'type' => 'integer' ),
|
||||
'element_id' => array( 'type' => 'string', 'description' => __( 'Optional: only fix this element.', 'emcp-tools' ) ),
|
||||
'target_ratio' => array( 'type' => 'number', 'description' => __( 'Target contrast ratio (default 4.5).', 'emcp-tools' ) ),
|
||||
'apply' => array( 'type' => 'boolean', 'description' => __( 'Write the changes. Defaults to false (dry-run preview).', 'emcp-tools' ) ),
|
||||
),
|
||||
'required' => array( 'post_id' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'applied' => array( 'type' => 'boolean' ),
|
||||
'count' => array( 'type' => 'integer' ),
|
||||
'proposed' => array( 'type' => 'array' ),
|
||||
'changes' => array( 'type' => 'array' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => false, 'destructive' => true, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_fix_color_contrast( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return new \WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0;
|
||||
if ( $post_id <= 0 ) {
|
||||
return new \WP_Error( 'missing_post_id', __( 'A valid post_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$page = $this->data->get_page_data( $post_id );
|
||||
if ( is_wp_error( $page ) ) {
|
||||
return $page;
|
||||
}
|
||||
if ( ! is_array( $page ) ) {
|
||||
return new \WP_Error( 'no_data', __( 'No Elementor data found for this page.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$host = function_exists( 'home_url' ) ? (string) wp_parse_url( home_url(), PHP_URL_HOST ) : '';
|
||||
$ex = EMCP_Tools_Content_Extractor::extract( $page, $host );
|
||||
$element_id = isset( $input['element_id'] ) ? sanitize_text_field( (string) $input['element_id'] ) : '';
|
||||
$target = isset( $input['target_ratio'] ) ? (float) $input['target_ratio'] : EMCP_Tools_Color_Contrast::AA_NORMAL;
|
||||
$fixes = self::propose_contrast_fixes( $ex, ( '' !== $element_id ) ? $element_id : null, $target );
|
||||
|
||||
if ( empty( $input['apply'] ) ) {
|
||||
return array( 'applied' => false, 'count' => count( $fixes ), 'proposed' => $fixes );
|
||||
}
|
||||
|
||||
// Writes require per-post ownership.
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return new \WP_Error( 'forbidden', __( 'You do not have permission to edit this page.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$applied = 0;
|
||||
foreach ( $fixes as $f ) {
|
||||
if ( '' === $f['color_key'] ) {
|
||||
continue;
|
||||
}
|
||||
if ( $this->data->update_element_settings( $page, $f['element_id'], array( $f['color_key'] => $f['to'] ) ) ) {
|
||||
$applied++;
|
||||
}
|
||||
}
|
||||
if ( $applied > 0 ) {
|
||||
$save = $this->data->save_page_data( $post_id, $page );
|
||||
if ( is_wp_error( $save ) ) {
|
||||
return $save;
|
||||
}
|
||||
}
|
||||
return array( 'applied' => true, 'count' => $applied, 'changes' => $fixes );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// add-alt-text-from-context (dry-run by default; apply:true to write)
|
||||
// =========================================================================
|
||||
|
||||
private function register_add_alt_text(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/add-alt-text-from-context',
|
||||
array(
|
||||
'label' => __( 'Add Alt Text from Context', 'emcp-tools' ),
|
||||
'description' => __( 'Proposes (and, with apply:true, writes) alt text for images that lack it, derived from the image filename, the nearest heading, or the page title. No AI call. Dry-run by default; writes to the media library alt + the image widget when applied.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_add_alt_text' ),
|
||||
'permission_callback' => array( $this, 'check_edit_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'post_id' => array( 'type' => 'integer' ),
|
||||
'apply' => array( 'type' => 'boolean', 'description' => __( 'Write the alt text. Defaults to false (dry-run preview).', 'emcp-tools' ) ),
|
||||
),
|
||||
'required' => array( 'post_id' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'applied' => array( 'type' => 'boolean' ),
|
||||
'count' => array( 'type' => 'integer' ),
|
||||
'proposed' => array( 'type' => 'array' ),
|
||||
'skipped' => array( 'type' => 'array' ),
|
||||
'changes' => array( 'type' => 'array' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => false, 'destructive' => true, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_add_alt_text( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return new \WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0;
|
||||
if ( $post_id <= 0 ) {
|
||||
return new \WP_Error( 'missing_post_id', __( 'A valid post_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$page = $this->data->get_page_data( $post_id );
|
||||
if ( is_wp_error( $page ) ) {
|
||||
return $page;
|
||||
}
|
||||
if ( ! is_array( $page ) ) {
|
||||
return new \WP_Error( 'no_data', __( 'No Elementor data found for this page.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$host = function_exists( 'home_url' ) ? (string) wp_parse_url( home_url(), PHP_URL_HOST ) : '';
|
||||
$ex = EMCP_Tools_Content_Extractor::extract( $page, $host );
|
||||
$title = function_exists( 'get_the_title' ) ? (string) get_the_title( $post_id ) : '';
|
||||
$proposals = self::propose_alt_texts( $ex, $title );
|
||||
|
||||
if ( empty( $input['apply'] ) ) {
|
||||
return array( 'applied' => false, 'count' => count( $proposals ), 'proposed' => $proposals );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return new \WP_Error( 'forbidden', __( 'You do not have permission to edit this page.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$applied = 0;
|
||||
$skipped = array();
|
||||
$dirty = false;
|
||||
foreach ( $proposals as $p ) {
|
||||
if ( (int) $p['attachment_id'] > 0 ) {
|
||||
if ( function_exists( 'update_post_meta' ) ) {
|
||||
update_post_meta( (int) $p['attachment_id'], '_wp_attachment_image_alt', $p['proposed_alt'] );
|
||||
}
|
||||
// Also set the widget-level alt so this specific image renders it.
|
||||
$el = $this->data->find_element_by_id( $page, $p['element_id'] );
|
||||
$image = ( is_array( $el ) && isset( $el['settings']['image'] ) && is_array( $el['settings']['image'] ) ) ? $el['settings']['image'] : null;
|
||||
if ( null !== $image ) {
|
||||
$image['alt'] = $p['proposed_alt'];
|
||||
if ( $this->data->update_element_settings( $page, $p['element_id'], array( 'image' => $image ) ) ) {
|
||||
$dirty = true;
|
||||
}
|
||||
}
|
||||
$applied++;
|
||||
} else {
|
||||
// Raw <img> inside an HTML widget — can't be safely auto-written.
|
||||
$skipped[] = $p['element_id'];
|
||||
}
|
||||
}
|
||||
if ( $dirty ) {
|
||||
$save = $this->data->save_page_data( $post_id, $page );
|
||||
if ( is_wp_error( $save ) ) {
|
||||
return $save;
|
||||
}
|
||||
}
|
||||
return array( 'applied' => true, 'count' => $applied, 'skipped' => $skipped, 'changes' => $proposals );
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Pure analysis helper (no WordPress — unit-testable with fixtures)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Builds a scored accessibility report from extracted content.
|
||||
*
|
||||
* Contrast resolution is best-effort: pairs whose background can't be
|
||||
* resolved are reported as inconclusive (never asserted as failures).
|
||||
*
|
||||
* @param array $ex Content_Extractor output.
|
||||
* @return array
|
||||
*/
|
||||
public static function build_a11y_report( array $ex ): array {
|
||||
$checks = array();
|
||||
|
||||
// --- Color contrast --------------------------------------------------
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
$inconclusive = 0;
|
||||
$worst = array();
|
||||
foreach ( $ex['text_style_contexts'] as $ctx ) {
|
||||
$bg = $ctx['background'] ?? null;
|
||||
if ( null === $bg || '' === $bg ) {
|
||||
$inconclusive++;
|
||||
continue;
|
||||
}
|
||||
$ratio = EMCP_Tools_Color_Contrast::contrast_ratio( (string) $ctx['color'], (string) $bg );
|
||||
if ( null === $ratio ) {
|
||||
$inconclusive++;
|
||||
continue;
|
||||
}
|
||||
if ( EMCP_Tools_Color_Contrast::passes( $ratio ) ) {
|
||||
$pass++;
|
||||
} else {
|
||||
$fail++;
|
||||
$worst[] = sprintf( '%s (%.2f:1)', $ctx['element_id'], $ratio );
|
||||
}
|
||||
}
|
||||
$total_ctx = $pass + $fail + $inconclusive;
|
||||
if ( 0 === $total_ctx ) {
|
||||
$contrast_status = 'inconclusive';
|
||||
$contrast_detail = __( 'No resolvable text/background color pairs found (colors may use globals or theme defaults).', 'emcp-tools' );
|
||||
} elseif ( $fail > 0 ) {
|
||||
$contrast_status = 'fail';
|
||||
$contrast_detail = sprintf(
|
||||
/* translators: 1: fail count, 2: worst list, 3: inconclusive count */
|
||||
__( '%1$d text/background pair(s) below 4.5:1 — %2$s. %3$d pair(s) inconclusive.', 'emcp-tools' ),
|
||||
$fail,
|
||||
implode( ', ', array_slice( $worst, 0, 5 ) ),
|
||||
$inconclusive
|
||||
);
|
||||
} elseif ( $pass > 0 ) {
|
||||
$contrast_status = ( $inconclusive > 0 ) ? 'warn' : 'pass';
|
||||
$contrast_detail = sprintf(
|
||||
/* translators: 1: pass count, 2: inconclusive count */
|
||||
__( '%1$d pair(s) meet 4.5:1; %2$d inconclusive (couldn\'t resolve background).', 'emcp-tools' ),
|
||||
$pass,
|
||||
$inconclusive
|
||||
);
|
||||
} else {
|
||||
$contrast_status = 'inconclusive';
|
||||
$contrast_detail = sprintf( /* translators: %d: count */ __( '%d color pair(s) inconclusive — background could not be resolved.', 'emcp-tools' ), $inconclusive );
|
||||
}
|
||||
$checks[] = self::check(
|
||||
'color_contrast',
|
||||
__( 'Color contrast (WCAG AA)', 'emcp-tools' ),
|
||||
$contrast_status,
|
||||
$contrast_detail,
|
||||
( 'fail' === $contrast_status ) ? __( 'Increase text/background contrast to at least 4.5:1 (3:1 for large text).', 'emcp-tools' ) : ''
|
||||
);
|
||||
|
||||
// --- Image alt text --------------------------------------------------
|
||||
$missing = 0;
|
||||
foreach ( $ex['images'] as $img ) {
|
||||
if ( '' === trim( (string) $img['alt'] ) ) {
|
||||
$missing++;
|
||||
}
|
||||
}
|
||||
$checks[] = self::check(
|
||||
'image_alts',
|
||||
__( 'Image alt text', 'emcp-tools' ),
|
||||
( 0 === $missing ) ? 'pass' : 'fail',
|
||||
sprintf( /* translators: 1: missing, 2: total */ __( '%1$d of %2$d images are missing alt text.', 'emcp-tools' ), $missing, count( $ex['images'] ) ),
|
||||
( 0 === $missing ) ? '' : __( 'Add descriptive alt text (or empty alt="" for purely decorative images).', 'emcp-tools' )
|
||||
);
|
||||
|
||||
// --- Heading hierarchy ----------------------------------------------
|
||||
$skip = false;
|
||||
$prev = 0;
|
||||
foreach ( $ex['headings'] as $h ) {
|
||||
$lvl = (int) $h['level'];
|
||||
if ( $prev > 0 && $lvl > $prev + 1 ) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
$prev = $lvl;
|
||||
}
|
||||
$checks[] = self::check(
|
||||
'heading_hierarchy',
|
||||
__( 'Heading hierarchy', 'emcp-tools' ),
|
||||
$skip ? 'warn' : 'pass',
|
||||
$skip ? __( 'A heading level is skipped, which disorients screen-reader users.', 'emcp-tools' ) : __( 'Headings are sequential.', 'emcp-tools' ),
|
||||
$skip ? __( 'Use heading levels in order (don\'t jump from H1 to H3).', 'emcp-tools' ) : ''
|
||||
);
|
||||
|
||||
// --- Link text quality ----------------------------------------------
|
||||
$generic = 0;
|
||||
$empty = 0;
|
||||
foreach ( $ex['links'] as $l ) {
|
||||
$text = trim( (string) $l['text'] );
|
||||
if ( '' === $text ) {
|
||||
$empty++;
|
||||
} elseif ( in_array( self::lower( $text ), self::$generic_link_text, true ) ) {
|
||||
$generic++;
|
||||
}
|
||||
}
|
||||
$bad = $generic + $empty;
|
||||
$checks[] = self::check(
|
||||
'link_text_quality',
|
||||
__( 'Link text quality', 'emcp-tools' ),
|
||||
( 0 === $bad ) ? 'pass' : 'warn',
|
||||
sprintf(
|
||||
/* translators: 1: generic count, 2: empty count */
|
||||
__( '%1$d generic ("click here"-style) and %2$d empty link text(s).', 'emcp-tools' ),
|
||||
$generic,
|
||||
$empty
|
||||
),
|
||||
( 0 === $bad ) ? '' : __( 'Use descriptive link text that makes sense out of context.', 'emcp-tools' )
|
||||
);
|
||||
|
||||
// --- Form label coverage --------------------------------------------
|
||||
$unlabeled = 0;
|
||||
foreach ( $ex['form_fields'] as $f ) {
|
||||
if ( '' === trim( (string) $f['label'] ) ) {
|
||||
$unlabeled++;
|
||||
}
|
||||
}
|
||||
if ( ! empty( $ex['form_fields'] ) ) {
|
||||
$checks[] = self::check(
|
||||
'form_label_coverage',
|
||||
__( 'Form label coverage', 'emcp-tools' ),
|
||||
( 0 === $unlabeled ) ? 'pass' : 'fail',
|
||||
sprintf( /* translators: 1: unlabeled, 2: total */ __( '%1$d of %2$d form fields have no label.', 'emcp-tools' ), $unlabeled, count( $ex['form_fields'] ) ),
|
||||
( 0 === $unlabeled ) ? '' : __( 'Give every form field a visible label.', 'emcp-tools' )
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'score' => self::score( $checks ),
|
||||
'checks' => $checks,
|
||||
'summary' => self::summary( $checks ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proposes adjusted text colors for failing contrast pairs.
|
||||
*
|
||||
* Only resolvable pairs (background known) that currently fail are returned;
|
||||
* inconclusive pairs are left alone (we never "fix" what we can't measure).
|
||||
*
|
||||
* @param array $ex Content_Extractor output.
|
||||
* @param string|null $element_id Optional element filter.
|
||||
* @param float $target Target contrast ratio.
|
||||
* @return array[] Each: { element_id, color_key, background, from, to, old_ratio, new_ratio }.
|
||||
*/
|
||||
public static function propose_contrast_fixes( array $ex, ?string $element_id, float $target = EMCP_Tools_Color_Contrast::AA_NORMAL ): array {
|
||||
$fixes = array();
|
||||
foreach ( $ex['text_style_contexts'] as $ctx ) {
|
||||
if ( null !== $element_id && ( $ctx['element_id'] ?? '' ) !== $element_id ) {
|
||||
continue;
|
||||
}
|
||||
$bg = $ctx['background'] ?? null;
|
||||
if ( null === $bg || '' === $bg ) {
|
||||
continue; // Can't fix what we can't measure.
|
||||
}
|
||||
$ratio = EMCP_Tools_Color_Contrast::contrast_ratio( (string) $ctx['color'], (string) $bg );
|
||||
if ( null === $ratio || EMCP_Tools_Color_Contrast::passes( $ratio ) ) {
|
||||
continue;
|
||||
}
|
||||
$suggest = EMCP_Tools_Color_Contrast::suggest_adjusted( (string) $ctx['color'], (string) $bg, $target );
|
||||
if ( null === $suggest ) {
|
||||
continue;
|
||||
}
|
||||
$new_ratio = EMCP_Tools_Color_Contrast::contrast_ratio( $suggest, (string) $bg );
|
||||
$fixes[] = array(
|
||||
'element_id' => (string) $ctx['element_id'],
|
||||
'color_key' => (string) ( $ctx['color_key'] ?? '' ),
|
||||
'background' => (string) $bg,
|
||||
'from' => (string) $ctx['color'],
|
||||
'to' => $suggest,
|
||||
'old_ratio' => round( (float) $ratio, 2 ),
|
||||
'new_ratio' => null !== $new_ratio ? round( (float) $new_ratio, 2 ) : null,
|
||||
);
|
||||
}
|
||||
return $fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proposes alt text for images that lack it, from filename → nearest heading
|
||||
* → page title (no AI call).
|
||||
*
|
||||
* @param array $ex Content_Extractor output.
|
||||
* @param string $page_title Page title fallback.
|
||||
* @return array[] Each: { element_id, attachment_id, url, proposed_alt, source, writable }.
|
||||
*/
|
||||
public static function propose_alt_texts( array $ex, string $page_title ): array {
|
||||
$out = array();
|
||||
foreach ( $ex['images'] as $img ) {
|
||||
if ( '' !== trim( (string) $img['alt'] ) ) {
|
||||
continue; // Already has alt.
|
||||
}
|
||||
$source = 'filename';
|
||||
$alt = self::alt_from_filename( (string) $img['url'] );
|
||||
if ( '' === $alt ) {
|
||||
$alt = trim( (string) ( $img['context_heading'] ?? '' ) );
|
||||
$source = 'heading';
|
||||
}
|
||||
if ( '' === $alt ) {
|
||||
$alt = trim( $page_title );
|
||||
$source = 'page_title';
|
||||
}
|
||||
if ( '' === $alt ) {
|
||||
continue; // Nothing to propose.
|
||||
}
|
||||
$out[] = array(
|
||||
'element_id' => (string) $img['element_id'],
|
||||
'attachment_id' => (int) $img['attachment_id'],
|
||||
'url' => (string) $img['url'],
|
||||
'proposed_alt' => $alt,
|
||||
'source' => $source,
|
||||
'writable' => ( (int) $img['attachment_id'] > 0 ),
|
||||
);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a descriptive phrase from an image filename, or '' if the filename
|
||||
* is non-descriptive (camera codes like IMG_1234, pure numbers, dimensions).
|
||||
*
|
||||
* @param string $url Image URL.
|
||||
* @return string
|
||||
*/
|
||||
public static function alt_from_filename( string $url ): string {
|
||||
$path = parse_url( $url, PHP_URL_PATH );
|
||||
$base = is_string( $path ) && '' !== $path ? basename( $path ) : basename( $url );
|
||||
$base = preg_replace( '/\.[a-z0-9]+$/i', '', (string) $base ); // strip extension
|
||||
$base = preg_replace( '/[-_]+/', ' ', (string) $base );
|
||||
// Strip WordPress size/scale suffixes.
|
||||
$base = preg_replace( '/\b\d{2,4}x\d{2,4}\b/i', ' ', (string) $base );
|
||||
$base = preg_replace( '/\bscaled\b/i', ' ', (string) $base );
|
||||
$base = preg_replace( '/\be\d{8,}\b/i', ' ', (string) $base );
|
||||
|
||||
$tokens = preg_split( '/\s+/', trim( (string) $base ) );
|
||||
$words = array();
|
||||
foreach ( (array) $tokens as $t ) {
|
||||
$t = trim( (string) $t );
|
||||
if ( '' === $t || preg_match( '/^\d+$/', $t ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( preg_match( '/^(img|image|dsc|dscn|pxl|pic|photo|screenshot|untitled|final|copy|v\d+)$/i', $t ) ) {
|
||||
continue;
|
||||
}
|
||||
$words[] = strtolower( $t );
|
||||
}
|
||||
if ( count( $words ) < 2 ) {
|
||||
return ''; // Too sparse to be a real description.
|
||||
}
|
||||
return ucfirst( implode( ' ', $words ) );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Internal utilities
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Builds one check entry.
|
||||
*
|
||||
* @param string $id Check id.
|
||||
* @param string $label Label.
|
||||
* @param string $status pass|warn|fail|inconclusive.
|
||||
* @param string $detail Detail.
|
||||
* @param string $rec Recommendation.
|
||||
* @return array
|
||||
*/
|
||||
private static function check( string $id, string $label, string $status, string $detail, string $rec ): array {
|
||||
return array(
|
||||
'id' => $id,
|
||||
'label' => $label,
|
||||
'status' => $status,
|
||||
'detail' => $detail,
|
||||
'recommendation' => $rec,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 0-100 score. inconclusive is neutral (excluded from the denominator) so a
|
||||
* page isn't penalized for contrast we honestly couldn't resolve.
|
||||
*
|
||||
* @param array $checks Checks.
|
||||
* @return int
|
||||
*/
|
||||
private static function score( array $checks ): int {
|
||||
$sum = 0.0;
|
||||
$count = 0;
|
||||
foreach ( $checks as $c ) {
|
||||
if ( 'inconclusive' === $c['status'] ) {
|
||||
continue;
|
||||
}
|
||||
$count++;
|
||||
$sum += ( 'pass' === $c['status'] ) ? 1.0 : ( 'warn' === $c['status'] ? 0.5 : 0.0 );
|
||||
}
|
||||
return ( 0 === $count ) ? 0 : (int) round( 100 * $sum / $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tallies pass/warn/fail/inconclusive counts.
|
||||
*
|
||||
* @param array $checks Checks.
|
||||
* @return array
|
||||
*/
|
||||
private static function summary( array $checks ): array {
|
||||
$s = array( 'passes' => 0, 'warnings' => 0, 'failures' => 0, 'inconclusive' => 0 );
|
||||
foreach ( $checks as $c ) {
|
||||
switch ( $c['status'] ) {
|
||||
case 'pass':
|
||||
$s['passes']++;
|
||||
break;
|
||||
case 'warn':
|
||||
$s['warnings']++;
|
||||
break;
|
||||
case 'inconclusive':
|
||||
$s['inconclusive']++;
|
||||
break;
|
||||
default:
|
||||
$s['failures']++;
|
||||
}
|
||||
}
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowercases (mbstring-aware).
|
||||
*
|
||||
* @param string $s Input.
|
||||
* @return string
|
||||
*/
|
||||
private static function lower( string $s ): string {
|
||||
return function_exists( 'mb_strtolower' ) ? mb_strtolower( $s, 'UTF-8' ) : strtolower( $s );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* BeTheme & BeBuilder integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_BeTheme_Integration extends EMCP_Tools_Theme_Integration {
|
||||
|
||||
public function id(): string { return 'betheme'; }
|
||||
public function label(): string { return 'BeTheme'; }
|
||||
public function is_available(): bool { return 'betheme' === get_template(); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'get-theme-options' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_options' ), 'perm' => $can_manage, 'desc' => 'Read BeTheme theme options.' ),
|
||||
'update-theme-options' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_options' ), 'perm' => $can_manage, 'desc' => 'Update BeTheme theme options { options: { key: value } }.' ),
|
||||
'get-bebuilder-content' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_bebuilder' ), 'perm' => $can_manage, 'desc' => 'Get BeBuilder page items by { post_id }.' ),
|
||||
'update-bebuilder-content'=> array( 'mode' => 'write', 'run' => array( $this, 'op_update_bebuilder' ), 'perm' => $can_manage, 'desc' => 'Update BeBuilder page items by { post_id, items }.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_options( array $args = array() ) {
|
||||
$opts = get_option( 'mfn_theme_options', array() );
|
||||
return array( 'options' => (array) $opts );
|
||||
}
|
||||
|
||||
public function op_update_options( array $args ) {
|
||||
$vals = (array) ( $args['options'] ?? array() );
|
||||
$cur = (array) get_option( 'mfn_theme_options', array() );
|
||||
update_option( 'mfn_theme_options', array_merge( $cur, $vals ) );
|
||||
return array( 'success' => true );
|
||||
}
|
||||
|
||||
public function op_get_bebuilder( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
$items = get_post_meta( $id, 'mfn-page-items', true );
|
||||
return array( 'post_id' => $id, 'items' => $items );
|
||||
}
|
||||
|
||||
public function op_update_bebuilder( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'post_id required' ); }
|
||||
if ( isset( $args['items'] ) ) {
|
||||
update_post_meta( $id, 'mfn-page-items', $args['items'] );
|
||||
}
|
||||
return array( 'success' => true, 'post_id' => $id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
/**
|
||||
* Block Builder MCP abilities (Pro).
|
||||
*
|
||||
* Exposes 8 tools for AI agents to design, validate, compile, and manage custom Gutenberg blocks.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.6.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Block_Builder_Abilities {
|
||||
|
||||
public function get_ability_names(): array {
|
||||
return array(
|
||||
'emcp-tools/list-block-control-types',
|
||||
'emcp-tools/validate-block-spec',
|
||||
'emcp-tools/create-custom-block',
|
||||
'emcp-tools/update-custom-block',
|
||||
'emcp-tools/get-custom-block',
|
||||
'emcp-tools/list-custom-blocks',
|
||||
'emcp-tools/set-block-status',
|
||||
'emcp-tools/delete-custom-block',
|
||||
);
|
||||
}
|
||||
|
||||
public function check_permission(): bool {
|
||||
return current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/list-block-control-types',
|
||||
array(
|
||||
'label' => __( 'List Block Attribute Types', 'emcp-tools' ),
|
||||
'description' => __( 'List supported attribute types and schema capabilities for custom Gutenberg blocks.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array( 'type' => 'object', 'properties' => array() ),
|
||||
'output_schema' => array( 'type' => 'object', 'properties' => array( 'types' => array( 'type' => 'array' ) ) ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_list_types' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/validate-block-spec',
|
||||
array(
|
||||
'label' => __( 'Validate Block Specification', 'emcp-tools' ),
|
||||
'description' => __( 'Validate a block specification before compiling or installing it.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array( 'spec' => array( 'type' => 'object' ) ),
|
||||
'required' => array( 'spec' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object', 'properties' => array( 'valid' => array( 'type' => 'boolean' ) ) ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_validate' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/create-custom-block',
|
||||
array(
|
||||
'label' => __( 'Create Custom Gutenberg Block', 'emcp-tools' ),
|
||||
'description' => __( 'Compile and install a custom Gutenberg block from a structured specification in the Sandbox.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'spec' => array( 'type' => 'object' ),
|
||||
'status' => array( 'type' => 'string', 'enum' => array( 'publish', 'draft' ), 'default' => 'publish' ),
|
||||
),
|
||||
'required' => array( 'spec' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array( 'success' => array( 'type' => 'boolean' ), 'id' => array( 'type' => 'integer' ) ),
|
||||
),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_create' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/update-custom-block',
|
||||
array(
|
||||
'label' => __( 'Update Custom Gutenberg Block', 'emcp-tools' ),
|
||||
'description' => __( 'Update an existing custom Gutenberg block specification and recompile it.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array( 'type' => 'integer' ),
|
||||
'spec' => array( 'type' => 'object' ),
|
||||
),
|
||||
'required' => array( 'id', 'spec' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object', 'properties' => array( 'success' => array( 'type' => 'boolean' ) ) ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_update' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/get-custom-block',
|
||||
array(
|
||||
'label' => __( 'Get Custom Gutenberg Block', 'emcp-tools' ),
|
||||
'description' => __( 'Retrieve the specification and status of a custom Gutenberg block by ID.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array( 'id' => array( 'type' => 'integer' ) ),
|
||||
'required' => array( 'id' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_get' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/list-custom-blocks',
|
||||
array(
|
||||
'label' => __( 'List Custom Gutenberg Blocks', 'emcp-tools' ),
|
||||
'description' => __( 'List all generated custom Gutenberg blocks stored in the Sandbox.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array( 'type' => 'object', 'properties' => array() ),
|
||||
'output_schema' => array( 'type' => 'object', 'properties' => array( 'blocks' => array( 'type' => 'array' ) ) ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_list' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/set-block-status',
|
||||
array(
|
||||
'label' => __( 'Set Block Status', 'emcp-tools' ),
|
||||
'description' => __( 'Activate (publish) or deactivate (draft) a custom Gutenberg block.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array( 'type' => 'integer' ),
|
||||
'status' => array( 'type' => 'string', 'enum' => array( 'publish', 'draft' ) ),
|
||||
),
|
||||
'required' => array( 'id', 'status' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object', 'properties' => array( 'success' => array( 'type' => 'boolean' ) ) ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_set_status' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/delete-custom-block',
|
||||
array(
|
||||
'label' => __( 'Delete Custom Gutenberg Block', 'emcp-tools' ),
|
||||
'description' => __( 'Permanently delete a custom Gutenberg block and its compiled files.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array( 'type' => 'integer' ),
|
||||
'confirm' => array( 'type' => 'boolean' ),
|
||||
),
|
||||
'required' => array( 'id', 'confirm' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object', 'properties' => array( 'success' => array( 'type' => 'boolean' ) ) ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_delete' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_list_types(): array {
|
||||
return array( 'types' => EMCP_Tools_Block_Generator::TYPES );
|
||||
}
|
||||
|
||||
public function execute_validate( array $args ) {
|
||||
$spec = (array) ( $args['spec'] ?? array() );
|
||||
$val = EMCP_Tools_Block_Generator::validate( $spec );
|
||||
return is_wp_error( $val ) ? $val : array( 'valid' => true );
|
||||
}
|
||||
|
||||
public function execute_create( array $args ) {
|
||||
$spec = (array) ( $args['spec'] ?? array() );
|
||||
$status = (string) ( $args['status'] ?? 'publish' );
|
||||
$id = EMCP_Tools_Block_Store::instance()->create( $spec, $status );
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
return array( 'success' => true, 'id' => $id );
|
||||
}
|
||||
|
||||
public function execute_update( array $args ) {
|
||||
$id = (int) ( $args['id'] ?? 0 );
|
||||
$spec = (array) ( $args['spec'] ?? array() );
|
||||
$res = EMCP_Tools_Block_Store::instance()->update( $id, $spec );
|
||||
return is_wp_error( $res ) ? $res : array( 'success' => true );
|
||||
}
|
||||
|
||||
public function execute_get( array $args ) {
|
||||
$id = (int) ( $args['id'] ?? 0 );
|
||||
return EMCP_Tools_Block_Store::instance()->get( $id );
|
||||
}
|
||||
|
||||
public function execute_list(): array {
|
||||
return array( 'blocks' => EMCP_Tools_Block_Store::instance()->list() );
|
||||
}
|
||||
|
||||
public function execute_set_status( array $args ): array {
|
||||
$id = (int) ( $args['id'] ?? 0 );
|
||||
$status = (string) ( $args['status'] ?? 'publish' );
|
||||
$ok = EMCP_Tools_Block_Store::instance()->set_status( $id, $status );
|
||||
return array( 'success' => $ok );
|
||||
}
|
||||
|
||||
public function execute_delete( array $args ) {
|
||||
if ( empty( $args['confirm'] ) ) {
|
||||
return new WP_Error( 'confirmation_required', __( 'Must provide confirm: true to delete a block.', 'emcp-tools' ) );
|
||||
}
|
||||
$id = (int) ( $args['id'] ?? 0 );
|
||||
$ok = EMCP_Tools_Block_Store::instance()->delete( $id );
|
||||
return array( 'success' => $ok );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Blocksy Blocks integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Blocksy_Blocks_Integration extends EMCP_Tools_Theme_Integration {
|
||||
|
||||
public function id(): string { return 'blocksy-blocks'; }
|
||||
public function label(): string { return 'Blocksy Blocks'; }
|
||||
public function is_available(): bool { return 'blocksy' === get_template() || defined( 'BLOCKSY_VERSION' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-blocks' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_blocks' ), 'perm' => $can_manage, 'desc' => 'List Blocksy custom blocks.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_blocks( array $args = array() ) {
|
||||
return array( 'blocks' => EMCP_Tools_Blocksy_Blocks_Catalog::get_blocks() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Blocksy Extensions integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Blocksy_Extensions_Integration extends EMCP_Tools_Theme_Integration {
|
||||
|
||||
public function id(): string { return 'blocksy-extensions'; }
|
||||
public function label(): string { return 'Blocksy Companion'; }
|
||||
public function is_available(): bool { return 'blocksy' === get_template() || defined( 'BLOCKSY_VERSION' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-extensions' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_extensions' ), 'perm' => $can_manage, 'desc' => 'List active Blocksy extensions.' ),
|
||||
'toggle-extension' => array( 'mode' => 'write', 'run' => array( $this, 'op_toggle_extension' ), 'perm' => $can_manage, 'desc' => 'Toggle Blocksy extension by { name, active }.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_extensions( array $args = array() ) {
|
||||
$exts = get_option( 'blocksy_active_extensions', array() );
|
||||
return array( 'extensions' => (array) $exts );
|
||||
}
|
||||
|
||||
public function op_toggle_extension( array $args ) {
|
||||
$name = sanitize_key( $args['name'] ?? '' );
|
||||
$active = (bool) ( $args['active'] ?? true );
|
||||
$exts = (array) get_option( 'blocksy_active_extensions', array() );
|
||||
if ( $active ) {
|
||||
if ( ! in_array( $name, $exts, true ) ) { $exts[] = $name; }
|
||||
} else {
|
||||
$exts = array_values( array_diff( $exts, array( $name ) ) );
|
||||
}
|
||||
update_option( 'blocksy_active_extensions', $exts );
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* GenerateBlocks integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_GenerateBlocks_Integration extends EMCP_Tools_Theme_Integration {
|
||||
|
||||
public function id(): string { return 'generateblocks'; }
|
||||
public function label(): string { return 'GenerateBlocks'; }
|
||||
public function is_available(): bool { return defined( 'GENERATEBLOCKS_VERSION' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-blocks' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_blocks' ), 'perm' => $can_manage, 'desc' => 'List GenerateBlocks blocks.' ),
|
||||
'get-block-defaults' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_defaults' ), 'perm' => $can_manage, 'desc' => 'Get block defaults.' ),
|
||||
'update-block-defaults'=> array( 'mode' => 'write', 'run' => array( $this, 'op_update_defaults' ), 'perm' => $can_manage, 'desc' => 'Update block defaults.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_blocks( array $args = array() ) {
|
||||
return array( 'blocks' => EMCP_Tools_GenerateBlocks_Catalog::get_blocks() );
|
||||
}
|
||||
|
||||
public function op_get_defaults( array $args = array() ) {
|
||||
return array( 'defaults' => get_option( 'generateblocks_defaults', array() ) );
|
||||
}
|
||||
|
||||
public function op_update_defaults( array $args ) {
|
||||
$vals = (array) ( $args['defaults'] ?? array() );
|
||||
$cur = (array) get_option( 'generateblocks_defaults', array() );
|
||||
update_option( 'generateblocks_defaults', array_merge( $cur, $vals ) );
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* GeneratePress theme integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_GeneratePress_Integration extends EMCP_Tools_Theme_Integration {
|
||||
|
||||
const OPTION = 'generate_settings';
|
||||
|
||||
public function id(): string { return 'generatepress'; }
|
||||
public function label(): string { return 'GeneratePress'; }
|
||||
public function is_available(): bool { return 'generatepress' === get_template(); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'get-settings' => array(
|
||||
'mode' => 'read',
|
||||
'run' => array( $this, 'op_get_settings' ),
|
||||
'perm' => $can_manage,
|
||||
'desc' => 'Read GeneratePress theme settings.',
|
||||
),
|
||||
'update-settings' => array(
|
||||
'mode' => 'write',
|
||||
'run' => array( $this, 'op_update_settings' ),
|
||||
'perm' => $can_manage,
|
||||
'desc' => 'Update GeneratePress theme settings { values: { key: value } }.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_settings( array $args = array() ) {
|
||||
$settings = get_option( self::OPTION, array() );
|
||||
return array( 'settings' => (array) $settings );
|
||||
}
|
||||
|
||||
public function op_update_settings( array $args ) {
|
||||
$values = (array) ( $args['values'] ?? array() );
|
||||
$existing = (array) get_option( self::OPTION, array() );
|
||||
$merged = array_merge( $existing, $values );
|
||||
update_option( self::OPTION, $merged );
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Project Memory MCP abilities (Pro).
|
||||
*
|
||||
* Exposes three tools for agent memory operations:
|
||||
* - remember (proposes a new site guideline, queued for human approval)
|
||||
* - recall (searches approved or all project memories)
|
||||
* - save-session-summary (records turn/session summaries and extracted learnings)
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Memory_Abilities {
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ability_names(): array {
|
||||
return array(
|
||||
'emcp-tools/remember',
|
||||
'emcp-tools/recall',
|
||||
'emcp-tools/save-session-summary',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register abilities.
|
||||
*/
|
||||
public function register(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/remember',
|
||||
array(
|
||||
'label' => __( 'Remember Project Guideline', 'emcp-tools' ),
|
||||
'description' => __( 'Propose a site guideline, constraint, or learning to be remembered across future agent sessions (queued as pending for admin approval).', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Short summary title of the rule or convention.', 'emcp-tools' ),
|
||||
),
|
||||
'guideline' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Detailed description of the convention, requirement, or pattern.', 'emcp-tools' ),
|
||||
),
|
||||
'severity' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'info', 'warning', 'block' ),
|
||||
'default' => 'info',
|
||||
'description' => __( 'Severity/strictness level.', 'emcp-tools' ),
|
||||
),
|
||||
'target' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional target component or subsystem (e.g. "header", "colors", "forms").', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'title', 'guideline' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'success' => array( 'type' => 'boolean' ),
|
||||
'id' => array( 'type' => 'integer' ),
|
||||
'status' => array( 'type' => 'string' ),
|
||||
'message' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_remember' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/recall',
|
||||
array(
|
||||
'label' => __( 'Recall Project Memories', 'emcp-tools' ),
|
||||
'description' => __( 'Search and retrieve remembered project guidelines and session records.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'search' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional search keyword.', 'emcp-tools' ),
|
||||
),
|
||||
'status' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'publish', 'pending', 'any' ),
|
||||
'default' => 'publish',
|
||||
'description' => __( 'Filter by approval status (default "publish" for approved guidelines).', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'memories' => array( 'type' => 'array' ),
|
||||
'total' => array( 'type' => 'integer' ),
|
||||
),
|
||||
),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_recall' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/save-session-summary',
|
||||
array(
|
||||
'label' => __( 'Save Session Summary', 'emcp-tools' ),
|
||||
'description' => __( 'Save a high-level summary of tasks performed during the current session, along with any key takeaways.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'summary' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Summary of changes made or actions completed.', 'emcp-tools' ),
|
||||
),
|
||||
'learnings' => array(
|
||||
'type' => 'array',
|
||||
'description' => __( 'Optional list of guidelines or rules learned.', 'emcp-tools' ),
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'title' => array( 'type' => 'string' ),
|
||||
'content' => array( 'type' => 'string' ),
|
||||
'severity' => array( 'type' => 'string' ),
|
||||
'target' => array( 'type' => 'string' ),
|
||||
),
|
||||
'required' => array( 'title', 'content' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'required' => array( 'summary' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'success' => array( 'type' => 'boolean' ),
|
||||
'created_ids' => array( 'type' => 'array' ),
|
||||
),
|
||||
),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_save_session_summary' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function check_read_permission(): bool {
|
||||
return current_user_can( 'read' );
|
||||
}
|
||||
|
||||
public function execute_remember( array $args ) {
|
||||
$title = sanitize_text_field( (string) ( $args['title'] ?? '' ) );
|
||||
$guideline = (string) ( $args['guideline'] ?? '' );
|
||||
$severity = (string) ( $args['severity'] ?? 'info' );
|
||||
$target = sanitize_text_field( (string) ( $args['target'] ?? '' ) );
|
||||
|
||||
if ( '' === $title || '' === $guideline ) {
|
||||
return new WP_Error( 'missing_param', __( 'Both "title" and "guideline" are required.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$post_id = EMCP_Tools_Memory_Store::add_proposal( $title, $guideline, $severity, $target );
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'id' => $post_id,
|
||||
'status' => 'pending',
|
||||
'message' => __( 'Guideline proposed successfully and queued for administrator approval.', 'emcp-tools' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_recall( array $args = array() ) {
|
||||
$search = (string) ( $args['search'] ?? '' );
|
||||
$status = (string) ( $args['status'] ?? 'publish' );
|
||||
|
||||
$query_args = array(
|
||||
'post_status' => 'any' === $status ? array( 'publish', 'pending' ) : $status,
|
||||
'posts_per_page' => 50,
|
||||
);
|
||||
|
||||
if ( '' !== $search ) {
|
||||
$query_args['s'] = $search;
|
||||
}
|
||||
|
||||
$posts = EMCP_Tools_Memory_Store::query( $query_args );
|
||||
$memories = array();
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$memories[] = array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'guideline' => $post->post_content,
|
||||
'status' => $post->post_status,
|
||||
'severity' => get_post_meta( $post->ID, EMCP_Tools_Memory_Store::META_SEVERITY, true ) ?: 'info',
|
||||
'target' => get_post_meta( $post->ID, EMCP_Tools_Memory_Store::META_TARGET, true ) ?: '',
|
||||
'date' => $post->post_date,
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'memories' => $memories,
|
||||
'total' => count( $memories ),
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_save_session_summary( array $args ) {
|
||||
$summary = (string) ( $args['summary'] ?? '' );
|
||||
$learnings = (array) ( $args['learnings'] ?? array() );
|
||||
|
||||
if ( '' === $summary ) {
|
||||
return new WP_Error( 'missing_summary', __( 'A "summary" string is required.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$ids = EMCP_Tools_Memory_Summarizer::record_session( $summary, $learnings );
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'created_ids' => $ids,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
/**
|
||||
* Backup, Sync & Migrate MCP abilities (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.12.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Migrate_Abilities {
|
||||
|
||||
public function get_ability_names(): array {
|
||||
return array(
|
||||
'emcp-tools/create-backup',
|
||||
'emcp-tools/list-backups',
|
||||
'emcp-tools/migrate-site',
|
||||
'emcp-tools/sync-to-live',
|
||||
);
|
||||
}
|
||||
|
||||
public function check_permission(): bool {
|
||||
return current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/create-backup',
|
||||
array(
|
||||
'label' => __( 'Create Site Backup', 'emcp-tools' ),
|
||||
'description' => __( 'Create a portable .emcp backup archive containing the database and site structure.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'name' => array( 'type' => 'string', 'description' => __( 'Optional backup name.', 'emcp-tools' ) ),
|
||||
),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_create_backup' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/list-backups',
|
||||
array(
|
||||
'label' => __( 'List Site Backups', 'emcp-tools' ),
|
||||
'description' => __( 'List all available .emcp backup archives.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array( 'type' => 'object', 'properties' => array() ),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_list_backups' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/migrate-site',
|
||||
array(
|
||||
'label' => __( 'Migrate Site', 'emcp-tools' ),
|
||||
'description' => __( 'Migrate site to or from a remote target running the EMCP connector { remote_url, secret_key, direction: "push"|"pull" }.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'remote_url' => array( 'type' => 'string' ),
|
||||
'secret_key' => array( 'type' => 'string' ),
|
||||
'direction' => array( 'type' => 'string', 'enum' => array( 'push', 'pull' ) ),
|
||||
),
|
||||
'required' => array( 'remote_url', 'secret_key' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_migrate' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/sync-to-live',
|
||||
array(
|
||||
'label' => __( 'Sync to Live', 'emcp-tools' ),
|
||||
'description' => __( 'Execute serialized search and replace across the database for URL changes { old_url, new_url, confirm: true }.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'old_url' => array( 'type' => 'string' ),
|
||||
'new_url' => array( 'type' => 'string' ),
|
||||
'confirm' => array( 'type' => 'boolean' ),
|
||||
),
|
||||
'required' => array( 'old_url', 'new_url', 'confirm' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_sync' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_create_backup( array $args ): array {
|
||||
$name = sanitize_file_name( (string) ( $args['name'] ?? '' ) );
|
||||
$file = EMCP_Tools_Packager::create_archive( $name );
|
||||
if ( ! $file ) {
|
||||
return array( 'success' => false, 'message' => __( 'Failed to create backup archive.', 'emcp-tools' ) );
|
||||
}
|
||||
return array(
|
||||
'success' => true,
|
||||
'filename' => basename( $file ),
|
||||
'path' => $file,
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_list_backups(): array {
|
||||
return array( 'backups' => EMCP_Tools_Packager::list_archives() );
|
||||
}
|
||||
|
||||
public function execute_migrate( array $args ): array {
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( 'Migration initialized with remote endpoint.', 'emcp-tools' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function execute_sync( array $args ) {
|
||||
if ( empty( $args['confirm'] ) ) {
|
||||
return new WP_Error( 'confirm_required', __( 'Must provide confirm: true to run URL sync.', 'emcp-tools' ) );
|
||||
}
|
||||
$old = esc_url_raw( $args['old_url'] );
|
||||
$new = esc_url_raw( $args['new_url'] );
|
||||
|
||||
global $wpdb;
|
||||
// Update options
|
||||
$opts = $wpdb->get_results( "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_value LIKE '%" . esc_sql( $old ) . "%'", ARRAY_A );
|
||||
foreach ( $opts as $o ) {
|
||||
$val = EMCP_Tools_Search_Replace::replace( maybe_unserialize( $o['option_value'] ), $old, $new );
|
||||
update_option( $o['option_name'], $val );
|
||||
}
|
||||
|
||||
return array( 'success' => true, 'updated_options' => count( $opts ) );
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Skills MCP abilities (Pro).
|
||||
*
|
||||
* Exposes runtime discovery and retrieval of agent domain skills:
|
||||
* - list-skills (lists available skills, optional search query)
|
||||
* - get-skill (returns full SKILL.md body by slug)
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.2.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Skill_Abilities {
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ability_names(): array {
|
||||
return array(
|
||||
'emcp-tools/list-skills',
|
||||
'emcp-tools/get-skill',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register abilities.
|
||||
*/
|
||||
public function register(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/list-skills',
|
||||
array(
|
||||
'label' => __( 'List Agent Skills', 'emcp-tools' ),
|
||||
'description' => __( 'List all available agent skills and domain instructions. Optional { search } keyword filter.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'search' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional search term to filter skill names, descriptions, or slugs.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'skills' => array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'slug' => array( 'type' => 'string' ),
|
||||
'name' => array( 'type' => 'string' ),
|
||||
'description' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'total' => array( 'type' => 'integer' ),
|
||||
),
|
||||
),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_list_skills' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/get-skill',
|
||||
array(
|
||||
'label' => __( 'Get Agent Skill', 'emcp-tools' ),
|
||||
'description' => __( 'Retrieve full markdown instructions for an agent skill by slug (e.g. "emcp-themer", "emcp-gutenberg", "emcp-themes/astra").', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'slug' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'The exact slug of the skill to fetch.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'slug' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'slug' => array( 'type' => 'string' ),
|
||||
'name' => array( 'type' => 'string' ),
|
||||
'content' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'execute_callback' => array( $this, 'execute_get_skill' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read permission check.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_read_permission(): bool {
|
||||
return current_user_can( 'read' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute list-skills.
|
||||
*
|
||||
* @param array $args Input args.
|
||||
* @return array
|
||||
*/
|
||||
public function execute_list_skills( array $args = array() ): array {
|
||||
$all = EMCP_Tools_Skill_Catalog::get_all();
|
||||
$search = strtolower( trim( (string) ( $args['search'] ?? '' ) ) );
|
||||
|
||||
$skills = array();
|
||||
foreach ( $all as $slug => $meta ) {
|
||||
if ( '' !== $search ) {
|
||||
$haystack = strtolower( $meta['slug'] . ' ' . $meta['name'] . ' ' . $meta['description'] );
|
||||
if ( false === strpos( $haystack, $search ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$skills[] = array(
|
||||
'slug' => $meta['slug'],
|
||||
'name' => $meta['name'],
|
||||
'description' => $meta['description'],
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'skills' => $skills,
|
||||
'total' => count( $skills ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute get-skill.
|
||||
*
|
||||
* @param array $args Input args.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_get_skill( array $args ) {
|
||||
$slug = trim( (string) ( $args['slug'] ?? '' ) );
|
||||
if ( '' === $slug ) {
|
||||
return new WP_Error( 'missing_slug', __( 'A "slug" argument is required.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$body = EMCP_Tools_Skill_Catalog::get_body( $slug );
|
||||
if ( is_wp_error( $body ) ) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$all = EMCP_Tools_Skill_Catalog::get_all();
|
||||
$name = $all[ $slug ]['name'] ?? $slug;
|
||||
|
||||
return array(
|
||||
'slug' => $slug,
|
||||
'name' => $name,
|
||||
'content' => $body,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
<?php
|
||||
/**
|
||||
* Brand Kit / system-kit MCP abilities for Elementor.
|
||||
*
|
||||
* Registers 4 Pro-gated tools that replace the active Elementor kit's four
|
||||
* SYSTEM color/typography slots (unlike the additive update-global-* tools,
|
||||
* which write to the custom_* arrays):
|
||||
*
|
||||
* - list-brand-kits (read-only enumeration of the cached bundle)
|
||||
* - apply-brand-kit (composite: look up + apply colors + typography)
|
||||
* - replace-system-colors (atomic 4-slot color replace)
|
||||
* - replace-system-typography(atomic 4-slot typography replace)
|
||||
*
|
||||
* All four register ONLY when the site has Pro access — the first layer of the
|
||||
* § 6.1 defense-in-depth gate. Each execute callback re-checks the gate, and
|
||||
* the underlying EMCP_Tools_System_Kit_Writer re-checks it a third time.
|
||||
*
|
||||
* See docs/BRAND_KITS_PLAN.md §§ 4.3, 6.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and implements the brand-kit / system-kit abilities.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
class EMCP_Tools_System_Kit_Abilities {
|
||||
|
||||
/**
|
||||
* Whether brand-kit tools should register/run on this site.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_access(): bool {
|
||||
return function_exists( 'emcp_tools_fs' ) && emcp_tools_fs()->can_use_premium_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ability names registered by this class. Empty for non-Pro
|
||||
* sites so they never appear in the MCP tool surface (or count against
|
||||
* client tool caps).
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ability_names(): array {
|
||||
if ( ! $this->has_access() ) {
|
||||
return array();
|
||||
}
|
||||
return array(
|
||||
'emcp-tools/list-brand-kits',
|
||||
'emcp-tools/apply-brand-kit',
|
||||
'emcp-tools/replace-system-colors',
|
||||
'emcp-tools/replace-system-typography',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all brand-kit abilities (Pro only).
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
public function register(): void {
|
||||
if ( ! $this->has_access() ) {
|
||||
return;
|
||||
}
|
||||
$this->register_list_brand_kits();
|
||||
$this->register_apply_brand_kit();
|
||||
$this->register_replace_system_colors();
|
||||
$this->register_replace_system_typography();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read permission (enumeration).
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_read_permission(): bool {
|
||||
return $this->has_access() && current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Write permission (global styling changes).
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_manage_permission(): bool {
|
||||
return $this->has_access() && current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Schema fragments
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The shape of a single system color slot.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function color_slot_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Human-readable slot title.', 'emcp-tools' ),
|
||||
),
|
||||
'color' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Hex color (e.g. "#6366F1").', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'color' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The shape of a single system typography slot (master-file shape).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function typography_slot_schema(): array {
|
||||
$size_obj = array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Slider value: { size: number, unit: string }. Omit or leave blank to clear.', 'emcp-tools' ),
|
||||
);
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'title' => array( 'type' => 'string' ),
|
||||
'font_family' => array( 'type' => 'string' ),
|
||||
'font_weight' => array( 'type' => 'string' ),
|
||||
'font_size' => $size_obj,
|
||||
'line_height' => $size_obj,
|
||||
'letter_spacing' => $size_obj,
|
||||
'word_spacing' => $size_obj,
|
||||
'text_transform' => array( 'type' => 'string' ),
|
||||
'font_style' => array( 'type' => 'string' ),
|
||||
'text_decoration' => array( 'type' => 'string' ),
|
||||
'direction' => array( 'type' => 'string' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// list-brand-kits
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function register_list_brand_kits(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/list-brand-kits',
|
||||
array(
|
||||
'label' => __( 'List Brand Kits', 'emcp-tools' ),
|
||||
'description' => __( 'Lists available premium brand kits (title, slug, description, category) from the cached library. Use apply-brand-kit to apply one.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_list_brand_kits' ),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'category_slug' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional: only list kits in this category.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'kits' => array( 'type' => 'array' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array(
|
||||
'readonly' => true,
|
||||
'destructive' => false,
|
||||
'idempotent' => true,
|
||||
),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_list_brand_kits( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return new \WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required to list brand kits.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$category_filter = isset( $input['category_slug'] ) ? sanitize_key( (string) $input['category_slug'] ) : '';
|
||||
|
||||
$bundle = EMCP_Tools_Pro_Brand_Kits::get_bundle();
|
||||
if ( is_wp_error( $bundle ) ) {
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
$kits = array();
|
||||
foreach ( $bundle['categories'] ?? array() as $category ) {
|
||||
$cat_slug = isset( $category['slug'] ) ? (string) $category['slug'] : '';
|
||||
if ( '' !== $category_filter && $cat_slug !== $category_filter ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( $category['kits'] ?? array() as $kit ) {
|
||||
$kits[] = array(
|
||||
'slug' => isset( $kit['slug'] ) ? (string) $kit['slug'] : '',
|
||||
'title' => isset( $kit['title'] ) ? (string) $kit['title'] : '',
|
||||
'description' => isset( $kit['description'] ) ? (string) $kit['description'] : '',
|
||||
'category' => $cat_slug,
|
||||
'tags' => isset( $kit['tags'] ) && is_array( $kit['tags'] ) ? array_map( 'strval', $kit['tags'] ) : array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'kits' => $kits );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// apply-brand-kit
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function register_apply_brand_kit(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/apply-brand-kit',
|
||||
array(
|
||||
'label' => __( 'Apply Brand Kit', 'emcp-tools' ),
|
||||
'description' => __( 'Applies a premium brand kit by slug: replaces the active Elementor kit\'s system colors and typography site-wide. Destructive — back up first (backup defaults to true).', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_apply_brand_kit' ),
|
||||
'permission_callback' => array( $this, 'check_manage_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'kit_slug' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'The brand kit slug (e.g. "modern-saas").', 'emcp-tools' ),
|
||||
),
|
||||
'category_slug' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional category slug to disambiguate the kit.', 'emcp-tools' ),
|
||||
),
|
||||
'backup' => array(
|
||||
'type' => 'boolean',
|
||||
'description' => __( 'Back up current global settings before applying. Defaults to true.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'kit_slug' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'success' => array( 'type' => 'boolean' ),
|
||||
'kit_slug' => array( 'type' => 'string' ),
|
||||
'kit_title' => array( 'type' => 'string' ),
|
||||
'colors_applied' => array( 'type' => 'integer' ),
|
||||
'typography_applied' => array( 'type' => 'integer' ),
|
||||
'custom_colors_added' => array( 'type' => 'integer' ),
|
||||
'backup_id' => array( 'type' => array( 'integer', 'null' ) ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array(
|
||||
'readonly' => false,
|
||||
'destructive' => true,
|
||||
'idempotent' => true,
|
||||
),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_apply_brand_kit( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return new \WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required to apply brand kits.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$kit_slug = isset( $input['kit_slug'] ) ? sanitize_key( (string) $input['kit_slug'] ) : '';
|
||||
if ( '' === $kit_slug ) {
|
||||
return new \WP_Error( 'missing_slug', __( 'A kit_slug is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$category_slug = isset( $input['category_slug'] ) ? sanitize_key( (string) $input['category_slug'] ) : '';
|
||||
$do_backup = ! isset( $input['backup'] ) || (bool) $input['backup'];
|
||||
|
||||
$kit = EMCP_Tools_Pro_Brand_Kits::find_kit( $kit_slug, $category_slug );
|
||||
if ( null === $kit ) {
|
||||
return new \WP_Error( 'kit_not_found', __( 'Brand kit not found in the cached library. Try syncing the library first.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
// Backup-before-apply, so AI-driven applies are as recoverable as UI ones.
|
||||
$backup_id = null;
|
||||
if ( $do_backup && class_exists( 'EMCP_Tools_Kit_Backup_Store' ) ) {
|
||||
$backup = EMCP_Tools_Kit_Backup_Store::create( isset( $kit['title'] ) ? (string) $kit['title'] : $kit_slug );
|
||||
if ( ! is_wp_error( $backup ) ) {
|
||||
$backup_id = (int) $backup;
|
||||
}
|
||||
}
|
||||
|
||||
$result = EMCP_Tools_Pro_Brand_Kits::apply_kit( $kit );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['backup_id'] = $backup_id;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// replace-system-colors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function register_replace_system_colors(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/replace-system-colors',
|
||||
array(
|
||||
'label' => __( 'Replace System Colors', 'emcp-tools' ),
|
||||
'description' => __( 'Replaces all four Elementor system color slots (primary, secondary, text, accent) atomically. All four must be provided. Propagates site-wide via global color tokens.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_replace_system_colors' ),
|
||||
'permission_callback' => array( $this, 'check_manage_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'colors' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'The four system color slots.', 'emcp-tools' ),
|
||||
'properties' => array(
|
||||
'primary' => $this->color_slot_schema(),
|
||||
'secondary' => $this->color_slot_schema(),
|
||||
'text' => $this->color_slot_schema(),
|
||||
'accent' => $this->color_slot_schema(),
|
||||
),
|
||||
'required' => array( 'primary', 'secondary', 'text', 'accent' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'colors' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'colors_applied' => array( 'type' => 'integer' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array(
|
||||
'readonly' => false,
|
||||
'destructive' => true,
|
||||
'idempotent' => true,
|
||||
),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_replace_system_colors( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return new \WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$colors = isset( $input['colors'] ) && is_array( $input['colors'] ) ? $input['colors'] : array();
|
||||
return EMCP_Tools_System_Kit_Writer::replace_system_colors( $colors );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// replace-system-typography
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function register_replace_system_typography(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/replace-system-typography',
|
||||
array(
|
||||
'label' => __( 'Replace System Typography', 'emcp-tools' ),
|
||||
'description' => __( 'Replaces all four Elementor system typography slots atomically with a full per-field reset. All four must be provided. Use master-file typography shape (font_family, font_weight, font_size {size,unit}, etc.).', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_replace_system_typography' ),
|
||||
'permission_callback' => array( $this, 'check_manage_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'typography' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'The four system typography slots.', 'emcp-tools' ),
|
||||
'properties' => array(
|
||||
'primary' => $this->typography_slot_schema(),
|
||||
'secondary' => $this->typography_slot_schema(),
|
||||
'text' => $this->typography_slot_schema(),
|
||||
'accent' => $this->typography_slot_schema(),
|
||||
),
|
||||
'required' => array( 'primary', 'secondary', 'text', 'accent' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'typography' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'typography_applied' => array( 'type' => 'integer' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array(
|
||||
'readonly' => false,
|
||||
'destructive' => true,
|
||||
'idempotent' => true,
|
||||
),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_replace_system_typography( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return new \WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$typography = isset( $input['typography'] ) && is_array( $input['typography'] ) ? $input['typography'] : array();
|
||||
return EMCP_Tools_System_Kit_Writer::replace_system_typography( $typography );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
<?php
|
||||
/**
|
||||
* Widget Builder MCP abilities — let AI agents generate custom Elementor widgets.
|
||||
*
|
||||
* Eight Pro-gated tools. The agent submits a structured spec; the plugin-owned
|
||||
* generator (never the agent) compiles it into a Widget_Base subclass stored in
|
||||
* an isolated uploads sandbox and loaded into Elementor. See:
|
||||
* - EMCP_Tools_Widget_Generator (spec → PHP)
|
||||
* - EMCP_Tools_Widget_Store (CPT + sandbox + manifest)
|
||||
* - EMCP_Tools_Widget_Loader (manifest-driven safe load)
|
||||
*
|
||||
* All tools self-guard on Pro access (get_ability_names()/register() are no-ops
|
||||
* without a license) so they never enter the MCP surface on free sites.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.9.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and implements the widget-builder abilities.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
class EMCP_Tools_Widget_Builder_Abilities {
|
||||
|
||||
/**
|
||||
* Registered ability names.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $ability_names = array();
|
||||
|
||||
/**
|
||||
* Whether these tools should register/run (Pro gate).
|
||||
*
|
||||
* @since 1.9.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_access(): bool {
|
||||
return function_exists( 'emcp_tools_fs' ) && emcp_tools_fs()->can_use_premium_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ability names (empty on free sites).
|
||||
*
|
||||
* @since 1.9.0
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ability_names(): array {
|
||||
if ( ! $this->has_access() ) {
|
||||
return array();
|
||||
}
|
||||
return array(
|
||||
'emcp-tools/list-control-types',
|
||||
'emcp-tools/validate-widget-spec',
|
||||
'emcp-tools/create-custom-widget',
|
||||
'emcp-tools/update-custom-widget',
|
||||
'emcp-tools/get-custom-widget',
|
||||
'emcp-tools/list-custom-widgets',
|
||||
'emcp-tools/set-widget-status',
|
||||
'emcp-tools/delete-custom-widget',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all abilities (Pro only).
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
public function register(): void {
|
||||
if ( ! $this->has_access() ) {
|
||||
return;
|
||||
}
|
||||
$this->register_list_control_types();
|
||||
$this->register_validate_widget_spec();
|
||||
$this->register_create_custom_widget();
|
||||
$this->register_update_custom_widget();
|
||||
$this->register_get_custom_widget();
|
||||
$this->register_list_custom_widgets();
|
||||
$this->register_set_widget_status();
|
||||
$this->register_delete_custom_widget();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Permissions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Read permission (discovery/validation).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_read_permission(): bool {
|
||||
return $this->has_access() && current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage permission (list/get).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_manage_permission(): bool {
|
||||
return $this->has_access() && current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Write permission (create/update/activate). Mirrors the code-snippet bar:
|
||||
* manage_options + unfiltered_html.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_write_permission(): bool {
|
||||
return $this->has_access() && current_user_can( 'manage_options' ) && current_user_can( 'unfiltered_html' );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Shared schema fragments
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* JSON-schema fragment describing a widget spec.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function spec_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Widget definition. Call list-control-types first for the supported control types and template syntax.', 'emcp-tools' ),
|
||||
'properties' => array(
|
||||
'meta' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Widget display title (required).', 'emcp-tools' ),
|
||||
),
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional machine-name hint (a unique name is assigned automatically).', 'emcp-tools' ),
|
||||
),
|
||||
'icon' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Elementor icon class, e.g. eicon-price-table.', 'emcp-tools' ),
|
||||
),
|
||||
'keywords' => array(
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
'description' => __( 'Search keywords for the widget panel.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'title' ),
|
||||
),
|
||||
'sections' => array(
|
||||
'type' => 'array',
|
||||
'description' => __( 'Control sections, grouped into Content/Style/Advanced tabs.', 'emcp-tools' ),
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array( 'type' => 'string' ),
|
||||
'label' => array( 'type' => 'string' ),
|
||||
'tab' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'content', 'style', 'advanced' ),
|
||||
'description' => __( 'Which panel tab the section appears under.', 'emcp-tools' ),
|
||||
),
|
||||
'controls' => array(
|
||||
'type' => 'array',
|
||||
'description' => __( 'Controls in this section. Each: { name, type, label, default?, options?, fields? }. See list-control-types.', 'emcp-tools' ),
|
||||
'items' => array( 'type' => 'object' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'id', 'controls' ),
|
||||
),
|
||||
),
|
||||
'html_template' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'HTML using {{control_name}} placeholders, {{#if name}}…{{/if}}, and {{#each repeater}}…{{/each}}. Values are escaped automatically by control type.', 'emcp-tools' ),
|
||||
),
|
||||
'styles' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional base CSS for the widget, served as its own style.css and enqueued only when the widget is on the page. Scope rules with your own class names (e.g. .my-card). Prefer control "selectors" for buyer-editable styles.', 'emcp-tools' ),
|
||||
),
|
||||
'scripts' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional front-end JavaScript, served as its own script.js (jQuery available) and enqueued only when the widget is on the page. Runs once per page load — scope DOM queries to your class names and guard against multiple instances.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'meta', 'sections', 'html_template' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output-schema fragment for a widget summary.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function summary_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'widget_id' => array( 'type' => 'integer' ),
|
||||
'title' => array( 'type' => 'string' ),
|
||||
'widget_name' => array( 'type' => 'string' ),
|
||||
'class_name' => array( 'type' => 'string' ),
|
||||
'status' => array( 'type' => 'string' ),
|
||||
'last_error' => array( 'type' => 'string' ),
|
||||
'updated' => array( 'type' => 'string' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// list-control-types
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers list-control-types.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_list_control_types(): void {
|
||||
$this->ability_names[] = 'emcp-tools/list-control-types';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/list-control-types',
|
||||
array(
|
||||
'label' => __( 'List Widget Control Types', 'emcp-tools' ),
|
||||
'description' => __( 'Returns the control types and template syntax supported by the widget builder, so you can construct a valid widget spec.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_list_control_types' ),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'control_types' => array( 'type' => 'array' ),
|
||||
'template_syntax' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes list-control-types.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_list_control_types( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$out = array();
|
||||
foreach ( EMCP_Tools_Widget_Generator::control_types() as $type => $meta ) {
|
||||
$out[] = array(
|
||||
'type' => $type,
|
||||
'has_value' => (bool) $meta['value'],
|
||||
'group' => $meta['group'],
|
||||
'description' => $meta['desc'],
|
||||
);
|
||||
}
|
||||
return array(
|
||||
'control_types' => $out,
|
||||
'template_syntax' => __( 'Use {{control_name}} to output a control value (auto-escaped by type), {{#if name}}…{{/if}} for conditionals (switcher value is "yes"), and {{#each repeater_name}}…{{/each}} to loop a repeater (reference its fields by name inside). Style controls with "selectors": { "{{WRAPPER}} .my-el": "color: {{VALUE}};" }.', 'emcp-tools' ),
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// validate-widget-spec
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers validate-widget-spec.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_validate_widget_spec(): void {
|
||||
$this->ability_names[] = 'emcp-tools/validate-widget-spec';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/validate-widget-spec',
|
||||
array(
|
||||
'label' => __( 'Validate Widget Spec', 'emcp-tools' ),
|
||||
'description' => __( 'Validates a widget spec and dry-runs the code generator without saving anything. Returns whether it is valid and any error.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_validate_widget_spec' ),
|
||||
'permission_callback' => array( $this, 'check_read_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array( 'spec' => $this->spec_schema() ),
|
||||
'required' => array( 'spec' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'valid' => array( 'type' => 'boolean' ),
|
||||
'error' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes validate-widget-spec.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_validate_widget_spec( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$spec = isset( $input['spec'] ) && is_array( $input['spec'] ) ? $input['spec'] : array();
|
||||
|
||||
$valid = EMCP_Tools_Widget_Generator::validate_spec( $spec );
|
||||
if ( is_wp_error( $valid ) ) {
|
||||
return array( 'valid' => false, 'error' => $valid->get_error_message() );
|
||||
}
|
||||
// Dry-run the generator (token lint) without persisting.
|
||||
$php = EMCP_Tools_Widget_Generator::generate( $spec, 'EMCP_Widget_Preview', 'emcp_custom_preview' );
|
||||
if ( is_wp_error( $php ) ) {
|
||||
return array( 'valid' => false, 'error' => $php->get_error_message() );
|
||||
}
|
||||
return array( 'valid' => true, 'error' => '' );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// create-custom-widget
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers create-custom-widget.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_create_custom_widget(): void {
|
||||
$this->ability_names[] = 'emcp-tools/create-custom-widget';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/create-custom-widget',
|
||||
array(
|
||||
'label' => __( 'Create Custom Widget', 'emcp-tools' ),
|
||||
'description' => __( 'Generates a custom Elementor widget from a spec, stores it in an isolated sandbox, and activates it so it appears in the Elementor panel under "Custom (EMCP)". Use the returned widget_name with add-free-widget to place it on a page.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_create_custom_widget' ),
|
||||
'permission_callback' => array( $this, 'check_write_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'spec' => $this->spec_schema(),
|
||||
'activate' => array(
|
||||
'type' => 'boolean',
|
||||
'description' => __( 'Activate immediately (default true). If false, the widget is created inactive.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'spec' ),
|
||||
),
|
||||
'output_schema' => $this->summary_schema(),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes create-custom-widget.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_create_custom_widget( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$spec = isset( $input['spec'] ) && is_array( $input['spec'] ) ? $input['spec'] : array();
|
||||
$activate = ! isset( $input['activate'] ) || ! empty( $input['activate'] );
|
||||
return EMCP_Tools_Widget_Store::create( $spec, $activate );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// update-custom-widget
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers update-custom-widget.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_update_custom_widget(): void {
|
||||
$this->ability_names[] = 'emcp-tools/update-custom-widget';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/update-custom-widget',
|
||||
array(
|
||||
'label' => __( 'Update Custom Widget', 'emcp-tools' ),
|
||||
'description' => __( 'Replaces a custom widget\'s spec and regenerates its code in place.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_update_custom_widget' ),
|
||||
'permission_callback' => array( $this, 'check_write_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'widget_id' => array(
|
||||
'type' => 'integer',
|
||||
'description' => __( 'The widget post ID returned by create/list.', 'emcp-tools' ),
|
||||
),
|
||||
'spec' => $this->spec_schema(),
|
||||
),
|
||||
'required' => array( 'widget_id', 'spec' ),
|
||||
),
|
||||
'output_schema' => $this->summary_schema(),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes update-custom-widget.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_update_custom_widget( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$widget_id = absint( $input['widget_id'] ?? 0 );
|
||||
$spec = isset( $input['spec'] ) && is_array( $input['spec'] ) ? $input['spec'] : array();
|
||||
if ( ! $widget_id ) {
|
||||
return new WP_Error( 'missing_params', __( 'widget_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
return EMCP_Tools_Widget_Store::update( $widget_id, $spec );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// get-custom-widget
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers get-custom-widget.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_get_custom_widget(): void {
|
||||
$this->ability_names[] = 'emcp-tools/get-custom-widget';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/get-custom-widget',
|
||||
array(
|
||||
'label' => __( 'Get Custom Widget', 'emcp-tools' ),
|
||||
'description' => __( 'Returns a custom widget\'s spec, generated PHP, status, and any last error.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_get_custom_widget' ),
|
||||
'permission_callback' => array( $this, 'check_manage_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'widget_id' => array( 'type' => 'integer' ),
|
||||
),
|
||||
'required' => array( 'widget_id' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'widget' => $this->summary_schema(),
|
||||
'spec' => array( 'type' => 'object' ),
|
||||
'generated_php' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes get-custom-widget.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_get_custom_widget( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$widget_id = absint( $input['widget_id'] ?? 0 );
|
||||
$summary = EMCP_Tools_Widget_Store::summary( $widget_id );
|
||||
if ( is_wp_error( $summary ) ) {
|
||||
return $summary;
|
||||
}
|
||||
return array(
|
||||
'widget' => $summary,
|
||||
'spec' => EMCP_Tools_Widget_Store::get_spec( $widget_id ) ?? array(),
|
||||
'generated_php' => EMCP_Tools_Widget_Store::get_php( $widget_id ),
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// list-custom-widgets
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers list-custom-widgets.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_list_custom_widgets(): void {
|
||||
$this->ability_names[] = 'emcp-tools/list-custom-widgets';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/list-custom-widgets',
|
||||
array(
|
||||
'label' => __( 'List Custom Widgets', 'emcp-tools' ),
|
||||
'description' => __( 'Lists all custom widgets generated by the widget builder, with their status.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_list_custom_widgets' ),
|
||||
'permission_callback' => array( $this, 'check_manage_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'status' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'active', 'draft', 'any' ),
|
||||
'description' => __( 'Filter by status. Default: any.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'widgets' => array( 'type' => 'array', 'items' => $this->summary_schema() ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes list-custom-widgets.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_list_custom_widgets( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$status = isset( $input['status'] ) ? sanitize_key( $input['status'] ) : 'any';
|
||||
if ( ! in_array( $status, array( 'active', 'draft', 'any' ), true ) ) {
|
||||
$status = 'any';
|
||||
}
|
||||
return array( 'widgets' => EMCP_Tools_Widget_Store::list_widgets( $status ) );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// set-widget-status
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers set-widget-status.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_set_widget_status(): void {
|
||||
$this->ability_names[] = 'emcp-tools/set-widget-status';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/set-widget-status',
|
||||
array(
|
||||
'label' => __( 'Set Widget Status', 'emcp-tools' ),
|
||||
'description' => __( 'Activates (loads into Elementor) or deactivates a custom widget.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_set_widget_status' ),
|
||||
'permission_callback' => array( $this, 'check_write_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'widget_id' => array( 'type' => 'integer' ),
|
||||
'status' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'active', 'draft' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'widget_id', 'status' ),
|
||||
),
|
||||
'output_schema' => $this->summary_schema(),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes set-widget-status.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_set_widget_status( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$widget_id = absint( $input['widget_id'] ?? 0 );
|
||||
$status = isset( $input['status'] ) ? sanitize_key( $input['status'] ) : '';
|
||||
if ( ! $widget_id || ! in_array( $status, array( 'active', 'draft' ), true ) ) {
|
||||
return new WP_Error( 'missing_params', __( 'widget_id and a valid status are required.', 'emcp-tools' ) );
|
||||
}
|
||||
return EMCP_Tools_Widget_Store::set_status( $widget_id, $status );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// delete-custom-widget
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Registers delete-custom-widget.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*/
|
||||
private function register_delete_custom_widget(): void {
|
||||
$this->ability_names[] = 'emcp-tools/delete-custom-widget';
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/delete-custom-widget',
|
||||
array(
|
||||
'label' => __( 'Delete Custom Widget', 'emcp-tools' ),
|
||||
'description' => __( 'Permanently deletes a custom widget: its record and its sandbox file. Destructive.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'execute_callback' => array( $this, 'execute_delete_custom_widget' ),
|
||||
'permission_callback' => array( $this, 'check_manage_permission' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'widget_id' => array( 'type' => 'integer' ),
|
||||
),
|
||||
'required' => array( 'widget_id' ),
|
||||
),
|
||||
'output_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'success' => array( 'type' => 'boolean' ),
|
||||
'widget_id' => array( 'type' => 'integer' ),
|
||||
),
|
||||
),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => false, 'destructive' => true, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes delete-custom-widget.
|
||||
*
|
||||
* @param array $input Tool input.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function execute_delete_custom_widget( $input ) {
|
||||
if ( ! $this->has_access() ) {
|
||||
return $this->no_license();
|
||||
}
|
||||
$widget_id = absint( $input['widget_id'] ?? 0 );
|
||||
if ( ! $widget_id ) {
|
||||
return new WP_Error( 'missing_params', __( 'widget_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
return EMCP_Tools_Widget_Store::delete( $widget_id );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Standard no-license error.
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
private function no_license(): WP_Error {
|
||||
return new WP_Error( 'no_license', __( 'A valid EMCP Tools Pro license is required to use the widget builder.', 'emcp-tools' ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce integration (Pro) — two dispatcher tools (woo-read / woo-write)
|
||||
* covering products, orders, refunds, customers, coupons, settings, and reports.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.2
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Woo_Integration {
|
||||
|
||||
public static function woo_active(): bool {
|
||||
return class_exists( 'WooCommerce' ) || function_exists( 'WC' );
|
||||
}
|
||||
|
||||
public function get_ability_names(): array {
|
||||
return array(
|
||||
'emcp-tools/woo-read',
|
||||
'emcp-tools/woo-write',
|
||||
);
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/woo-read',
|
||||
array(
|
||||
'label' => __( 'WooCommerce Read', 'emcp-tools' ),
|
||||
'description' => __( 'Read products, orders, refunds, customers, coupons, reports, and settings. Call with no operation to list catalog.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'operation' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Operation name (e.g. "list-products", "get-order", "report-sales").', 'emcp-tools' ),
|
||||
),
|
||||
'arguments' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Arguments passed to the operation.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'can_read' ),
|
||||
'execute_callback' => array( $this, 'execute_read' ),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/woo-write',
|
||||
array(
|
||||
'label' => __( 'WooCommerce Write', 'emcp-tools' ),
|
||||
'description' => __( 'Create, update, or delete products, orders, refunds, customers, coupons, and settings. Refunds and deletes require confirm:true.', 'emcp-tools' ),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'operation' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Operation name (e.g. "create-product", "update-order", "delete-product").', 'emcp-tools' ),
|
||||
),
|
||||
'arguments' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Arguments passed to the operation.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
'required' => array( 'operation' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'can_write' ),
|
||||
'execute_callback' => array( $this, 'execute_write' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function can_read(): bool {
|
||||
return current_user_can( 'read' );
|
||||
}
|
||||
|
||||
public function can_write(): bool {
|
||||
return current_user_can( 'manage_woocommerce' ) || current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
public function execute_read( array $args ) {
|
||||
$op = trim( (string) ( $args['operation'] ?? '' ) );
|
||||
$in = (array) ( $args['arguments'] ?? array() );
|
||||
|
||||
if ( '' === $op ) {
|
||||
return array(
|
||||
'operations' => array(
|
||||
'list-products' => 'List products { limit?, status?, category? }',
|
||||
'get-product' => 'Get product by { product_id }',
|
||||
'list-orders' => 'List orders { limit?, status? }',
|
||||
'get-order' => 'Get order by { order_id }',
|
||||
'list-customers' => 'List registered customers { limit? }',
|
||||
'list-coupons' => 'List coupons { limit? }',
|
||||
'report-sales' => 'Report sales { period?: "week"|"month"|"year" }',
|
||||
'system-status' => 'WooCommerce system health and environment status',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
switch ( $op ) {
|
||||
case 'list-products':
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'posts_per_page' => (int) ( $in['limit'] ?? 20 ),
|
||||
)
|
||||
);
|
||||
$items = array();
|
||||
foreach ( $posts as $p ) {
|
||||
$items[] = array(
|
||||
'id' => $p->ID,
|
||||
'name' => $p->post_title,
|
||||
'sku' => get_post_meta( $p->ID, '_sku', true ),
|
||||
'price' => get_post_meta( $p->ID, '_price', true ),
|
||||
);
|
||||
}
|
||||
return array( 'products' => $items, 'total' => count( $items ) );
|
||||
|
||||
case 'get-product':
|
||||
$id = (int) ( $in['product_id'] ?? 0 );
|
||||
$p = get_post( $id );
|
||||
if ( ! $p || 'product' !== $p->post_type ) {
|
||||
return new WP_Error( 'not_found', __( 'Product not found.', 'emcp-tools' ) );
|
||||
}
|
||||
return array(
|
||||
'id' => $p->ID,
|
||||
'name' => $p->post_title,
|
||||
'description' => $p->post_content,
|
||||
'sku' => get_post_meta( $p->ID, '_sku', true ),
|
||||
'price' => get_post_meta( $p->ID, '_price', true ),
|
||||
'stock' => get_post_meta( $p->ID, '_stock', true ),
|
||||
);
|
||||
|
||||
case 'list-orders':
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => 'shop_order',
|
||||
'posts_per_page' => (int) ( $in['limit'] ?? 20 ),
|
||||
)
|
||||
);
|
||||
$orders = array();
|
||||
foreach ( $posts as $p ) {
|
||||
$orders[] = array(
|
||||
'id' => $p->ID,
|
||||
'status' => $p->post_status,
|
||||
'total' => get_post_meta( $p->ID, '_order_total', true ),
|
||||
'date' => $p->post_date,
|
||||
);
|
||||
}
|
||||
return array( 'orders' => $orders, 'total' => count( $orders ) );
|
||||
|
||||
case 'get-order':
|
||||
$id = (int) ( $in['order_id'] ?? 0 );
|
||||
$p = get_post( $id );
|
||||
if ( ! $p ) {
|
||||
return new WP_Error( 'not_found', __( 'Order not found.', 'emcp-tools' ) );
|
||||
}
|
||||
return array(
|
||||
'id' => $p->ID,
|
||||
'status' => $p->post_status,
|
||||
'total' => get_post_meta( $p->ID, '_order_total', true ),
|
||||
'currency' => get_post_meta( $p->ID, '_order_currency', true ),
|
||||
'billing' => array(
|
||||
'first_name' => get_post_meta( $p->ID, '_billing_first_name', true ),
|
||||
'email' => get_post_meta( $p->ID, '_billing_email', true ),
|
||||
),
|
||||
);
|
||||
|
||||
case 'list-customers':
|
||||
$users = get_users( array( 'role' => 'customer', 'number' => (int) ( $in['limit'] ?? 20 ) ) );
|
||||
$custs = array();
|
||||
foreach ( $users as $u ) {
|
||||
$custs[] = array( 'id' => $u->ID, 'email' => $u->user_email, 'name' => $u->display_name );
|
||||
}
|
||||
return array( 'customers' => $custs, 'total' => count( $custs ) );
|
||||
|
||||
case 'system-status':
|
||||
return array(
|
||||
'woocommerce_active' => self::woo_active(),
|
||||
'version' => defined( 'WC_VERSION' ) ? WC_VERSION : 'unknown',
|
||||
'currency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD',
|
||||
);
|
||||
|
||||
default:
|
||||
return new WP_Error( 'unsupported_operation', sprintf( __( 'Operation "%s" is not recognized.', 'emcp-tools' ), esc_html( $op ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
public function execute_write( array $args ) {
|
||||
$op = trim( (string) ( $args['operation'] ?? '' ) );
|
||||
$in = (array) ( $args['arguments'] ?? array() );
|
||||
|
||||
switch ( $op ) {
|
||||
case 'create-product':
|
||||
$title = sanitize_text_field( $in['name'] ?? 'New Product' );
|
||||
$id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $title,
|
||||
'post_content'=> (string) ( $in['description'] ?? '' ),
|
||||
)
|
||||
);
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
if ( isset( $in['price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( $in['price'] ) );
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( $in['price'] ) );
|
||||
}
|
||||
if ( isset( $in['sku'] ) ) {
|
||||
update_post_meta( $id, '_sku', sanitize_text_field( $in['sku'] ) );
|
||||
}
|
||||
return array( 'success' => true, 'product_id' => $id );
|
||||
|
||||
case 'update-product':
|
||||
$id = (int) ( $in['product_id'] ?? 0 );
|
||||
if ( ! $id ) {
|
||||
return new WP_Error( 'missing_id', __( 'product_id required.', 'emcp-tools' ) );
|
||||
}
|
||||
if ( isset( $in['name'] ) ) {
|
||||
wp_update_post( array( 'ID' => $id, 'post_title' => sanitize_text_field( $in['name'] ) ) );
|
||||
}
|
||||
if ( isset( $in['price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( $in['price'] ) );
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( $in['price'] ) );
|
||||
}
|
||||
if ( isset( $in['sku'] ) ) {
|
||||
update_post_meta( $id, '_sku', sanitize_text_field( $in['sku'] ) );
|
||||
}
|
||||
return array( 'success' => true, 'product_id' => $id );
|
||||
|
||||
case 'delete-product':
|
||||
if ( empty( $in['confirm'] ) ) {
|
||||
return new WP_Error( 'confirmation_required', __( 'Must provide confirm: true to delete a product.', 'emcp-tools' ) );
|
||||
}
|
||||
$id = (int) ( $in['product_id'] ?? 0 );
|
||||
return array( 'success' => (bool) wp_delete_post( $id, true ) );
|
||||
|
||||
case 'update-order':
|
||||
$id = (int) ( $in['order_id'] ?? 0 );
|
||||
if ( ! $id ) {
|
||||
return new WP_Error( 'missing_id', __( 'order_id required.', 'emcp-tools' ) );
|
||||
}
|
||||
if ( isset( $in['status'] ) ) {
|
||||
wp_update_post( array( 'ID' => $id, 'post_status' => sanitize_key( $in['status'] ) ) );
|
||||
}
|
||||
return array( 'success' => true, 'order_id' => $id );
|
||||
|
||||
default:
|
||||
return new WP_Error( 'unsupported_operation', sprintf( __( 'Operation "%s" is not recognized.', 'emcp-tools' ), esc_html( $op ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Fluent Forms integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_FluentForms_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string { return 'fluentforms'; }
|
||||
public function label(): string { return 'Fluent Forms'; }
|
||||
public function is_active(): bool { return defined( 'FLUENTFORM' ) || function_exists( 'wpFluent' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
|
||||
return array(
|
||||
'list-forms' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_forms' ), 'perm' => $can_manage, 'desc' => 'List Fluent Forms.' ),
|
||||
'get-form' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_form' ), 'perm' => $can_manage, 'desc' => 'Get one form by { form_id }.' ),
|
||||
'list-entries' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_entries' ), 'perm' => $can_manage, 'desc' => 'List entries by { form_id }.' ),
|
||||
'get-entry' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_entry' ), 'perm' => $can_manage, 'desc' => 'Get entry by { entry_id }.' ),
|
||||
'delete-entry' => array( 'mode' => 'write', 'run' => array( $this, 'op_delete_entry' ), 'perm' => $can_manage, 'confirm' => true, 'desc' => 'Delete entry by { entry_id, confirm: true }.' ),
|
||||
'update-form' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_form' ), 'perm' => $can_manage, 'desc' => 'Update form by { form_id, title?, status? }.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
if ( ! function_exists( 'wpFluent' ) ) { return new WP_Error( 'inactive', 'Fluent Forms not active.' ); }
|
||||
$forms = wpFluent()->table( 'fluentform_forms' )->select( array( 'id', 'title', 'status', 'created_at' ) )->get();
|
||||
return array( 'forms' => (array) $forms, 'total' => count( (array) $forms ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'wpFluent' ) ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$form = wpFluent()->table( 'fluentform_forms' )->find( $id );
|
||||
return $form ? (array) $form : new WP_Error( 'not_found', 'Form not found.' );
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'wpFluent' ) ) { return array( 'entries' => array(), 'total' => 0 ); }
|
||||
$entries = wpFluent()->table( 'fluentform_submissions' )->where( 'form_id', $id )->get();
|
||||
return array( 'entries' => (array) $entries, 'total' => count( (array) $entries ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'wpFluent' ) ) { return new WP_Error( 'invalid', 'Invalid entry ID.' ); }
|
||||
$entry = wpFluent()->table( 'fluentform_submissions' )->find( $id );
|
||||
return $entry ? (array) $entry : new WP_Error( 'not_found', 'Entry not found.' );
|
||||
}
|
||||
|
||||
public function op_delete_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'wpFluent' ) ) { return new WP_Error( 'invalid', 'Invalid entry ID.' ); }
|
||||
wpFluent()->table( 'fluentform_submissions' )->where( 'id', $id )->delete();
|
||||
return array( 'success' => true );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'wpFluent' ) ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$data = array();
|
||||
if ( isset( $args['title'] ) ) { $data['title'] = sanitize_text_field( $args['title'] ); }
|
||||
if ( isset( $args['status'] ) ) { $data['status'] = sanitize_text_field( $args['status'] ); }
|
||||
if ( ! empty( $data ) ) {
|
||||
wpFluent()->table( 'fluentform_forms' )->where( 'id', $id )->update( $data );
|
||||
}
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/**
|
||||
* Formidable Forms integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Formidable_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string { return 'formidable'; }
|
||||
public function label(): string { return 'Formidable Forms'; }
|
||||
public function is_active(): bool { return class_exists( 'FrmForm' ) || class_exists( 'FrmAppHelper' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-forms' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_forms' ), 'perm' => $can_manage, 'desc' => 'List Formidable Forms.' ),
|
||||
'get-form' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_form' ), 'perm' => $can_manage, 'desc' => 'Get one form by { form_id }.' ),
|
||||
'list-entries' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_entries' ), 'perm' => $can_manage, 'desc' => 'List entries by { form_id }.' ),
|
||||
'get-entry' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_entry' ), 'perm' => $can_manage, 'desc' => 'Get one entry by { entry_id }.' ),
|
||||
'delete-entry' => array( 'mode' => 'write', 'run' => array( $this, 'op_delete_entry' ), 'perm' => $can_manage, 'confirm' => true, 'desc' => 'Delete entry by { entry_id, confirm: true }.' ),
|
||||
'update-form' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_form' ), 'perm' => $can_manage, 'desc' => 'Update form by { form_id, name? }.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
if ( ! class_exists( 'FrmForm' ) ) { return new WP_Error( 'inactive', 'Formidable not active.' ); }
|
||||
$forms = FrmForm::getAll();
|
||||
return array( 'forms' => (array) $forms, 'total' => count( (array) $forms ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'FrmForm' ) ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$f = FrmForm::getOne( $id );
|
||||
return $f ? (array) $f : new WP_Error( 'not_found', 'Form not found.' );
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'FrmEntry' ) ) { return array( 'entries' => array(), 'total' => 0 ); }
|
||||
$entries = FrmEntry::getAll( array( 'form_id' => $id ) );
|
||||
return array( 'entries' => (array) $entries, 'total' => count( (array) $entries ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'FrmEntry' ) ) { return new WP_Error( 'invalid', 'Invalid entry ID.' ); }
|
||||
$e = FrmEntry::getOne( $id );
|
||||
return $e ? (array) $e : new WP_Error( 'not_found', 'Entry not found.' );
|
||||
}
|
||||
|
||||
public function op_delete_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'FrmEntry' ) ) { return new WP_Error( 'invalid', 'Invalid entry ID.' ); }
|
||||
return array( 'success' => (bool) FrmEntry::destroy( $id ) );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'FrmForm' ) ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$data = array();
|
||||
if ( isset( $args['name'] ) ) { $data['name'] = sanitize_text_field( $args['name'] ); }
|
||||
if ( ! empty( $data ) ) { FrmForm::update( $id, $data ); }
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* Forminator integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Forminator_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string { return 'forminator'; }
|
||||
public function label(): string { return 'Forminator'; }
|
||||
public function is_active(): bool { return class_exists( 'Forminator_API' ) || class_exists( 'Forminator' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-forms' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_forms' ), 'perm' => $can_manage, 'desc' => 'List Forminator forms.' ),
|
||||
'get-form' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_form' ), 'perm' => $can_manage, 'desc' => 'Get one Forminator form by { form_id }.' ),
|
||||
'list-entries' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_entries' ), 'perm' => $can_manage, 'desc' => 'List entries by { form_id }.' ),
|
||||
'get-entry' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_entry' ), 'perm' => $can_manage, 'desc' => 'Get one entry by { form_id, entry_id }.' ),
|
||||
'delete-entry' => array( 'mode' => 'write', 'run' => array( $this, 'op_delete_entry' ), 'perm' => $can_manage, 'confirm' => true, 'desc' => 'Delete entry by { form_id, entry_id, confirm: true }.' ),
|
||||
'update-form' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_form' ), 'perm' => $can_manage, 'desc' => 'Update Forminator form title/settings.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
if ( ! class_exists( 'Forminator_API' ) ) { return new WP_Error( 'inactive', 'Forminator API not active.' ); }
|
||||
$forms = Forminator_API::get_forms( null, 1, 100 );
|
||||
$out = array();
|
||||
foreach ( (array) $forms as $f ) {
|
||||
$out[] = array( 'id' => is_object( $f ) ? ( $f->id ?? 0 ) : ( $f['id'] ?? 0 ), 'title' => is_object( $f ) ? ( $f->name ?? '' ) : ( $f['name'] ?? '' ) );
|
||||
}
|
||||
return array( 'forms' => $out, 'total' => count( $out ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'Forminator_API' ) ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$f = Forminator_API::get_form( $id );
|
||||
return is_wp_error( $f ) ? $f : (array) $f;
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'Forminator_API' ) ) { return array( 'entries' => array(), 'total' => 0 ); }
|
||||
$entries = Forminator_API::get_entries( $id );
|
||||
return array( 'entries' => (array) $entries, 'total' => count( (array) $entries ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$form_id = (int) ( $args['form_id'] ?? 0 );
|
||||
$entry_id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $form_id || ! $entry_id || ! class_exists( 'Forminator_API' ) ) { return new WP_Error( 'invalid', 'Invalid form ID or entry ID.' ); }
|
||||
$entry = Forminator_API::get_entry( $form_id, $entry_id );
|
||||
return is_wp_error( $entry ) ? $entry : (array) $entry;
|
||||
}
|
||||
|
||||
public function op_delete_entry( array $args ) {
|
||||
$form_id = (int) ( $args['form_id'] ?? 0 );
|
||||
$entry_id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $form_id || ! $entry_id || ! class_exists( 'Forminator_API' ) ) { return new WP_Error( 'invalid', 'Invalid IDs.' ); }
|
||||
$res = Forminator_API::delete_entry( $form_id, $entry_id );
|
||||
return array( 'success' => ! is_wp_error( $res ) );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
$post = get_post( $id );
|
||||
if ( ! $post ) { return new WP_Error( 'not_found', 'Form not found.' ); }
|
||||
if ( isset( $args['title'] ) ) {
|
||||
wp_update_post( array( 'ID' => $id, 'post_title' => sanitize_text_field( $args['title'] ) ) );
|
||||
}
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* Gravity Forms integration (Pro) — gf-read / gf-write over GFAPI.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_GravityForms_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string { return 'gravityforms'; }
|
||||
public function label(): string { return 'Gravity Forms'; }
|
||||
public function is_active(): bool { return class_exists( 'GFAPI' ) || class_exists( 'GFForms' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'gform_full_access' ) || current_user_can( 'manage_options' ); };
|
||||
|
||||
return array(
|
||||
'list-forms' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_forms' ), 'perm' => $can_manage, 'desc' => 'List Gravity Forms (ID, title, active status).' ),
|
||||
'get-form' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_form' ), 'perm' => $can_manage, 'desc' => 'Get one form by { form_id }.' ),
|
||||
'list-entries' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_entries' ), 'perm' => $can_manage, 'desc' => 'List entries by { form_id, page_size? }.' ),
|
||||
'get-entry' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_entry' ), 'perm' => $can_manage, 'desc' => 'Get one entry by { entry_id }.' ),
|
||||
'delete-entry' => array( 'mode' => 'write', 'run' => array( $this, 'op_delete_entry' ), 'perm' => $can_manage, 'confirm' => true, 'desc' => 'Delete an entry by { entry_id, confirm: true }.' ),
|
||||
'update-form' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_form' ), 'perm' => $can_manage, 'desc' => 'Update form by { form_id, form: object }.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
if ( ! class_exists( 'GFAPI' ) ) { return new WP_Error( 'inactive', 'Gravity Forms GFAPI not available.' ); }
|
||||
$forms = GFAPI::get_forms();
|
||||
return array( 'forms' => $forms, 'total' => count( $forms ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'GFAPI' ) ) { return new WP_Error( 'invalid', 'Invalid form ID or GFAPI missing.' ); }
|
||||
$f = GFAPI::get_form( $id );
|
||||
return $f ? $f : new WP_Error( 'not_found', 'Form not found.' );
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'GFAPI' ) ) { return array( 'entries' => array(), 'total' => 0 ); }
|
||||
$entries = GFAPI::get_entries( $id );
|
||||
return array( 'entries' => is_array( $entries ) ? $entries : array(), 'total' => count( (array) $entries ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'GFAPI' ) ) { return new WP_Error( 'invalid', 'Invalid entry ID.' ); }
|
||||
$e = GFAPI::get_entry( $id );
|
||||
return is_wp_error( $e ) ? $e : $e;
|
||||
}
|
||||
|
||||
public function op_delete_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $id || ! class_exists( 'GFAPI' ) ) { return new WP_Error( 'invalid', 'Invalid entry ID.' ); }
|
||||
$res = GFAPI::delete_entry( $id );
|
||||
return array( 'success' => ! is_wp_error( $res ) );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$form = (array) ( $args['form'] ?? array() );
|
||||
$id = (int) ( $args['form_id'] ?? ( $form['id'] ?? 0 ) );
|
||||
if ( ! $id || ! class_exists( 'GFAPI' ) ) { return new WP_Error( 'invalid', 'Form ID required.' ); }
|
||||
$res = GFAPI::update_form( $form, $id );
|
||||
return is_wp_error( $res ) ? $res : array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* MetForm integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_MetForm_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string { return 'metform'; }
|
||||
public function label(): string { return 'MetForm'; }
|
||||
public function is_active(): bool { return defined( 'METFORM_VERSION' ) || class_exists( 'MetForm\Core\Init' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-forms' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_forms' ), 'perm' => $can_manage, 'desc' => 'List MetForm forms.' ),
|
||||
'get-form' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_form' ), 'perm' => $can_manage, 'desc' => 'Get one MetForm form by { form_id }.' ),
|
||||
'list-entries' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_entries' ), 'perm' => $can_manage, 'desc' => 'List entries by { form_id }.' ),
|
||||
'get-entry' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_entry' ), 'perm' => $can_manage, 'desc' => 'Get entry by { entry_id }.' ),
|
||||
'update-form' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_form' ), 'perm' => $can_manage, 'desc' => 'Update MetForm form title/settings.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
$forms = get_posts( array( 'post_type' => 'metform-form', 'posts_per_page' => 100 ) );
|
||||
$out = array();
|
||||
foreach ( $forms as $f ) { $out[] = array( 'id' => $f->ID, 'title' => $f->post_title ); }
|
||||
return array( 'forms' => $out, 'total' => count( $out ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
$post = get_post( $id );
|
||||
return $post ? array( 'id' => $post->ID, 'title' => $post->post_title, 'settings' => get_post_meta( $id, 'metform_form__form_setting', true ) ) : new WP_Error( 'not_found', 'Form not found.' );
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
$entries = get_posts( array( 'post_type' => 'metform-entry', 'posts_per_page' => 50, 'meta_key' => 'metform_form_id', 'meta_value' => $id ) );
|
||||
$out = array();
|
||||
foreach ( $entries as $e ) { $out[] = array( 'id' => $e->ID, 'title' => $e->post_title, 'date' => $e->post_date ); }
|
||||
return array( 'entries' => $out, 'total' => count( $out ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
$post = get_post( $id );
|
||||
return $post ? array( 'id' => $post->ID, 'data' => get_post_meta( $id, 'metform_entries__form_data', true ) ) : new WP_Error( 'not_found', 'Entry not found.' );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$update = array( 'ID' => $id );
|
||||
if ( isset( $args['title'] ) ) { $update['post_title'] = sanitize_text_field( $args['title'] ); }
|
||||
wp_update_post( $update );
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Ninja Forms integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_NinjaForms_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string { return 'ninjaforms'; }
|
||||
public function label(): string { return 'Ninja Forms'; }
|
||||
public function is_active(): bool { return class_exists( 'Ninja_Forms' ) || function_exists( 'Ninja_Forms' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-forms' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_forms' ), 'perm' => $can_manage, 'desc' => 'List Ninja Forms.' ),
|
||||
'get-form' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_form' ), 'perm' => $can_manage, 'desc' => 'Get one form by { form_id }.' ),
|
||||
'list-entries' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_entries' ), 'perm' => $can_manage, 'desc' => 'List submissions by { form_id }.' ),
|
||||
'get-entry' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_entry' ), 'perm' => $can_manage, 'desc' => 'Get one submission by { entry_id }.' ),
|
||||
'delete-entry' => array( 'mode' => 'write', 'run' => array( $this, 'op_delete_entry' ), 'perm' => $can_manage, 'confirm' => true, 'desc' => 'Delete submission by { entry_id, confirm: true }.' ),
|
||||
'update-form' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_form' ), 'perm' => $can_manage, 'desc' => 'Update form by { form_id, title? }.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
if ( ! function_exists( 'Ninja_Forms' ) ) { return new WP_Error( 'inactive', 'Ninja Forms not active.' ); }
|
||||
$forms = Ninja_Forms()->form()->get_forms();
|
||||
$out = array();
|
||||
foreach ( (array) $forms as $f ) {
|
||||
$out[] = array( 'id' => is_object( $f ) ? $f->get_id() : 0, 'title' => is_object( $f ) ? $f->get_setting( 'title' ) : '' );
|
||||
}
|
||||
return array( 'forms' => $out, 'total' => count( $out ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'Ninja_Forms' ) ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$f = Ninja_Forms()->form( $id )->get();
|
||||
return $f ? array( 'id' => $id, 'title' => $f->get_setting( 'title' ) ) : new WP_Error( 'not_found', 'Form not found.' );
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'Ninja_Forms' ) ) { return array( 'entries' => array(), 'total' => 0 ); }
|
||||
$subs = Ninja_Forms()->form( $id )->get_subs();
|
||||
return array( 'entries' => (array) $subs, 'total' => count( (array) $subs ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
$post = get_post( $id );
|
||||
return $post ? (array) $post : new WP_Error( 'not_found', 'Entry not found.' );
|
||||
}
|
||||
|
||||
public function op_delete_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
return array( 'success' => (bool) wp_delete_post( $id, true ) );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id || ! function_exists( 'Ninja_Forms' ) ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
if ( isset( $args['title'] ) ) {
|
||||
Ninja_Forms()->form( $id )->update_setting( 'title', sanitize_text_field( $args['title'] ) )->save();
|
||||
}
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* SureForms integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_SureForms_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string { return 'sureforms'; }
|
||||
public function label(): string { return 'SureForms'; }
|
||||
public function is_active(): bool { return defined( 'SUREFORMS_VERSION' ) || class_exists( 'SureForms\Plugin' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool { return current_user_can( 'manage_options' ); };
|
||||
return array(
|
||||
'list-forms' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_forms' ), 'perm' => $can_manage, 'desc' => 'List SureForms forms.' ),
|
||||
'get-form' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_form' ), 'perm' => $can_manage, 'desc' => 'Get one SureForms form by { form_id }.' ),
|
||||
'list-entries' => array( 'mode' => 'read', 'run' => array( $this, 'op_list_entries' ), 'perm' => $can_manage, 'desc' => 'List entries by { form_id }.' ),
|
||||
'get-entry' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_entry' ), 'perm' => $can_manage, 'desc' => 'Get entry by { entry_id }.' ),
|
||||
'update-form' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_form' ), 'perm' => $can_manage, 'desc' => 'Update SureForms form.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
$forms = get_posts( array( 'post_type' => 'sureforms_form', 'posts_per_page' => 100 ) );
|
||||
$out = array();
|
||||
foreach ( $forms as $f ) { $out[] = array( 'id' => $f->ID, 'title' => $f->post_title ); }
|
||||
return array( 'forms' => $out, 'total' => count( $out ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
$post = get_post( $id );
|
||||
return $post ? (array) $post : new WP_Error( 'not_found', 'Form not found.' );
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
$entries = get_posts( array( 'post_type' => 'sureforms_entry', 'posts_per_page' => 50, 'post_parent' => $id ) );
|
||||
$out = array();
|
||||
foreach ( $entries as $e ) { $out[] = array( 'id' => $e->ID, 'title' => $e->post_title ); }
|
||||
return array( 'entries' => $out, 'total' => count( $out ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$id = (int) ( $args['entry_id'] ?? 0 );
|
||||
$post = get_post( $id );
|
||||
return $post ? (array) $post : new WP_Error( 'not_found', 'Entry not found.' );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'invalid', 'Invalid form ID.' ); }
|
||||
$update = array( 'ID' => $id );
|
||||
if ( isset( $args['title'] ) ) { $update['post_title'] = sanitize_text_field( $args['title'] ); }
|
||||
wp_update_post( $update );
|
||||
return array( 'success' => true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* WPForms Pro integration — two dispatcher tools (wpforms-read / wpforms-write).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_WPForms_Integration extends EMCP_Tools_Form_Integration {
|
||||
|
||||
public function id(): string {
|
||||
return 'wpforms';
|
||||
}
|
||||
|
||||
public function label(): string {
|
||||
return 'WPForms';
|
||||
}
|
||||
|
||||
public function is_active(): bool {
|
||||
return function_exists( 'wpforms' ) || class_exists( 'WPForms' );
|
||||
}
|
||||
|
||||
protected function operations(): array {
|
||||
$can_manage = static function (): bool {
|
||||
return current_user_can( 'manage_options' );
|
||||
};
|
||||
|
||||
return array(
|
||||
'list-forms' => array(
|
||||
'mode' => 'read',
|
||||
'run' => array( $this, 'op_list_forms' ),
|
||||
'perm' => $can_manage,
|
||||
'desc' => 'List all WPForms forms (ID, title, status, entry count).',
|
||||
),
|
||||
'get-form' => array(
|
||||
'mode' => 'read',
|
||||
'run' => array( $this, 'op_get_form' ),
|
||||
'perm' => $can_manage,
|
||||
'desc' => 'Get one WPForms form structure by { form_id }: fields, settings, and notifications.',
|
||||
),
|
||||
'list-entries' => array(
|
||||
'mode' => 'read',
|
||||
'run' => array( $this, 'op_list_entries' ),
|
||||
'perm' => $can_manage,
|
||||
'desc' => 'List entries for { form_id } (limit, offset, status).',
|
||||
),
|
||||
'get-entry' => array(
|
||||
'mode' => 'read',
|
||||
'run' => array( $this, 'op_get_entry' ),
|
||||
'perm' => $can_manage,
|
||||
'desc' => 'Get one entry by { entry_id }.',
|
||||
),
|
||||
'delete-entry' => array(
|
||||
'mode' => 'write',
|
||||
'run' => array( $this, 'op_delete_entry' ),
|
||||
'perm' => $can_manage,
|
||||
'confirm' => true,
|
||||
'desc' => 'Delete an entry by { entry_id, confirm: true }.',
|
||||
),
|
||||
'update-form' => array(
|
||||
'mode' => 'write',
|
||||
'run' => array( $this, 'op_update_form' ),
|
||||
'perm' => $can_manage,
|
||||
'desc' => 'Update form settings/fields by { form_id, post_title?, post_content? }.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_forms( array $args = array() ) {
|
||||
if ( ! $this->is_active() ) {
|
||||
return new WP_Error( 'inactive', __( 'WPForms is not active.', 'emcp-tools' ) );
|
||||
}
|
||||
$forms = function_exists( 'wpforms' ) ? wpforms()->form->get( '' ) : array();
|
||||
$out = array();
|
||||
foreach ( (array) $forms as $f ) {
|
||||
$post_obj = is_object( $f ) && isset( $f->post_title ) ? $f : get_post( is_object( $f ) ? $f->ID : $f );
|
||||
if ( $post_obj ) {
|
||||
$out[] = array(
|
||||
'id' => $post_obj->ID,
|
||||
'title' => $post_obj->post_title,
|
||||
'date' => $post_obj->post_date,
|
||||
);
|
||||
}
|
||||
}
|
||||
return array( 'forms' => $out, 'total' => count( $out ) );
|
||||
}
|
||||
|
||||
public function op_get_form( array $args ) {
|
||||
$form_id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $form_id ) {
|
||||
return new WP_Error( 'missing_id', __( 'A form_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$form = function_exists( 'wpforms' ) ? wpforms()->form->get( $form_id ) : get_post( $form_id );
|
||||
if ( ! $form ) {
|
||||
return new WP_Error( 'not_found', __( 'Form not found.', 'emcp-tools' ) );
|
||||
}
|
||||
return array(
|
||||
'id' => $form_id,
|
||||
'title' => is_object( $form ) ? ( $form->post_title ?? '' ) : '',
|
||||
'content' => is_object( $form ) ? ( $form->post_content ?? '' ) : '',
|
||||
);
|
||||
}
|
||||
|
||||
public function op_list_entries( array $args ) {
|
||||
$form_id = (int) ( $args['form_id'] ?? 0 );
|
||||
$limit = (int) ( $args['limit'] ?? 20 );
|
||||
if ( ! function_exists( 'wpforms' ) || ! isset( wpforms()->entry ) ) {
|
||||
return array( 'entries' => array(), 'total' => 0 );
|
||||
}
|
||||
$entries = wpforms()->entry->get_entries( array( 'form_id' => $form_id, 'number' => $limit ) );
|
||||
return array( 'entries' => (array) $entries, 'total' => count( (array) $entries ) );
|
||||
}
|
||||
|
||||
public function op_get_entry( array $args ) {
|
||||
$entry_id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $entry_id ) {
|
||||
return new WP_Error( 'missing_id', __( 'An entry_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
if ( ! function_exists( 'wpforms' ) || ! isset( wpforms()->entry ) ) {
|
||||
return new WP_Error( 'unavailable', __( 'WPForms entries API is unavailable.', 'emcp-tools' ) );
|
||||
}
|
||||
$entry = wpforms()->entry->get( $entry_id );
|
||||
return $entry ? (array) $entry : new WP_Error( 'not_found', __( 'Entry not found.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
public function op_delete_entry( array $args ) {
|
||||
$entry_id = (int) ( $args['entry_id'] ?? 0 );
|
||||
if ( ! $entry_id ) {
|
||||
return new WP_Error( 'missing_id', __( 'An entry_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
if ( function_exists( 'wpforms' ) && isset( wpforms()->entry ) ) {
|
||||
$ok = wpforms()->entry->delete( $entry_id );
|
||||
return array( 'success' => (bool) $ok );
|
||||
}
|
||||
return new WP_Error( 'unavailable', __( 'WPForms entries API is unavailable.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
public function op_update_form( array $args ) {
|
||||
$form_id = (int) ( $args['form_id'] ?? 0 );
|
||||
if ( ! $form_id ) {
|
||||
return new WP_Error( 'missing_id', __( 'A form_id is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$update_args = array( 'ID' => $form_id );
|
||||
if ( isset( $args['post_title'] ) ) {
|
||||
$update_args['post_title'] = sanitize_text_field( $args['post_title'] );
|
||||
}
|
||||
if ( isset( $args['post_content'] ) ) {
|
||||
$update_args['post_content'] = (string) $args['post_content'];
|
||||
}
|
||||
$res = wp_update_post( $update_args );
|
||||
return is_wp_error( $res ) ? $res : array( 'success' => true, 'id' => $form_id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* All in One SEO (AIOSEO) integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AIOSEO_Integration extends EMCP_Tools_SEO_Integration {
|
||||
|
||||
public function id(): string { return 'aioseo'; }
|
||||
public function label(): string { return 'All in One SEO'; }
|
||||
public function is_active(): bool { return defined( 'AIOSEO_VERSION' ) || class_exists( 'AIOSEO\Plugin\AIOSEO' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$edit_posts = static function (): bool { return current_user_can( 'edit_posts' ); };
|
||||
return array(
|
||||
'get-post-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Get AIOSEO post metadata.' ),
|
||||
'update-post-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Update AIOSEO post metadata.' ),
|
||||
'get-term-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Get AIOSEO term metadata.' ),
|
||||
'update-term-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Update AIOSEO term metadata.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'post_id required' ); }
|
||||
return array(
|
||||
'post_id' => $id,
|
||||
'title' => (string) get_post_meta( $id, '_aioseo_title', true ),
|
||||
'description' => (string) get_post_meta( $id, '_aioseo_description', true ),
|
||||
'keywords' => (string) get_post_meta( $id, '_aioseo_keywords', true ),
|
||||
'canonical' => (string) get_post_meta( $id, '_aioseo_canonical_url', true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'post_id required' ); }
|
||||
if ( isset( $args['title'] ) ) { update_post_meta( $id, '_aioseo_title', sanitize_text_field( $args['title'] ) ); }
|
||||
if ( isset( $args['description'] ) ) { update_post_meta( $id, '_aioseo_description', sanitize_textarea_field( $args['description'] ) ); }
|
||||
if ( isset( $args['keywords'] ) ) { update_post_meta( $id, '_aioseo_keywords', sanitize_text_field( $args['keywords'] ) ); }
|
||||
return array( 'success' => true, 'post_id' => $id );
|
||||
}
|
||||
|
||||
public function op_get_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
return array( 'term_id' => $id, 'title' => (string) get_term_meta( $id, '_aioseo_title', true ) );
|
||||
}
|
||||
|
||||
public function op_update_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( isset( $args['title'] ) ) { update_term_meta( $id, '_aioseo_title', sanitize_text_field( $args['title'] ) ); }
|
||||
return array( 'success' => true, 'term_id' => $id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* Rank Math SEO integration (Pro) — rankmath-read / rankmath-write.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_RankMath_Integration extends EMCP_Tools_SEO_Integration {
|
||||
|
||||
public function id(): string { return 'rankmath'; }
|
||||
public function label(): string { return 'Rank Math'; }
|
||||
public function is_active(): bool { return defined( 'RANK_MATH_VERSION' ) || class_exists( 'RankMath' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$edit_posts = static function (): bool { return current_user_can( 'edit_posts' ); };
|
||||
return array(
|
||||
'get-post-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Get Rank Math SEO metadata.' ),
|
||||
'update-post-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Update Rank Math SEO metadata.' ),
|
||||
'get-term-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Get Rank Math term SEO metadata.' ),
|
||||
'update-term-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Update Rank Math term SEO metadata.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'post_id required' ); }
|
||||
return array(
|
||||
'post_id' => $id,
|
||||
'title' => (string) get_post_meta( $id, 'rank_math_title', true ),
|
||||
'description' => (string) get_post_meta( $id, 'rank_math_description', true ),
|
||||
'focus_keyword' => (string) get_post_meta( $id, 'rank_math_focus_keyword', true ),
|
||||
'canonical' => (string) get_post_meta( $id, 'rank_math_canonical_url', true ),
|
||||
'robots' => (array) get_post_meta( $id, 'rank_math_robots', true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'post_id required' ); }
|
||||
if ( isset( $args['title'] ) ) { update_post_meta( $id, 'rank_math_title', sanitize_text_field( $args['title'] ) ); }
|
||||
if ( isset( $args['description'] ) ) { update_post_meta( $id, 'rank_math_description', sanitize_textarea_field( $args['description'] ) ); }
|
||||
if ( isset( $args['focus_keyword'] ) ) { update_post_meta( $id, 'rank_math_focus_keyword', sanitize_text_field( $args['focus_keyword'] ) ); }
|
||||
if ( isset( $args['canonical'] ) ) { update_post_meta( $id, 'rank_math_canonical_url', esc_url_raw( $args['canonical'] ) ); }
|
||||
return array( 'success' => true, 'post_id' => $id );
|
||||
}
|
||||
|
||||
public function op_get_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'term_id required' ); }
|
||||
return array(
|
||||
'term_id' => $id,
|
||||
'title' => (string) get_term_meta( $id, 'rank_math_title', true ),
|
||||
'description' => (string) get_term_meta( $id, 'rank_math_description', true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'term_id required' ); }
|
||||
if ( isset( $args['title'] ) ) { update_term_meta( $id, 'rank_math_title', sanitize_text_field( $args['title'] ) ); }
|
||||
if ( isset( $args['description'] ) ) { update_term_meta( $id, 'rank_math_description', sanitize_textarea_field( $args['description'] ) ); }
|
||||
return array( 'success' => true, 'term_id' => $id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* The SEO Framework integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_SEOFramework_Integration extends EMCP_Tools_SEO_Integration {
|
||||
|
||||
public function id(): string { return 'seoframework'; }
|
||||
public function label(): string { return 'The SEO Framework'; }
|
||||
public function is_active(): bool { return defined( 'THE_SEO_FRAMEWORK_VERSION' ) || function_exists( 'the_seo_framework' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$edit_posts = static function (): bool { return current_user_can( 'edit_posts' ); };
|
||||
return array(
|
||||
'get-post-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Get The SEO Framework metadata.' ),
|
||||
'update-post-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Update The SEO Framework metadata.' ),
|
||||
'get-term-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Get The SEO Framework term metadata.' ),
|
||||
'update-term-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Update The SEO Framework term metadata.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
return array(
|
||||
'post_id' => $id,
|
||||
'title' => (string) get_post_meta( $id, '_genesis_title', true ),
|
||||
'description' => (string) get_post_meta( $id, '_genesis_description', true ),
|
||||
'canonical' => (string) get_post_meta( $id, '_genesis_canonical_uri', true ),
|
||||
'noindex' => (bool) get_post_meta( $id, '_genesis_noindex', true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( isset( $args['title'] ) ) { update_post_meta( $id, '_genesis_title', sanitize_text_field( $args['title'] ) ); }
|
||||
if ( isset( $args['description'] ) ) { update_post_meta( $id, '_genesis_description', sanitize_textarea_field( $args['description'] ) ); }
|
||||
return array( 'success' => true, 'post_id' => $id );
|
||||
}
|
||||
|
||||
public function op_get_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
return array( 'term_id' => $id, 'title' => (string) get_term_meta( $id, 'doctitle', true ) );
|
||||
}
|
||||
|
||||
public function op_update_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( isset( $args['title'] ) ) { update_term_meta( $id, 'doctitle', sanitize_text_field( $args['title'] ) ); }
|
||||
return array( 'success' => true, 'term_id' => $id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* SEOPress integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_SeoPress_Integration extends EMCP_Tools_SEO_Integration {
|
||||
|
||||
public function id(): string { return 'seopress'; }
|
||||
public function label(): string { return 'SEOPress'; }
|
||||
public function is_active(): bool { return defined( 'SEOPRESS_VERSION' ) || function_exists( 'seopress_init' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$edit_posts = static function (): bool { return current_user_can( 'edit_posts' ); };
|
||||
return array(
|
||||
'get-post-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Get SEOPress post metadata.' ),
|
||||
'update-post-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Update SEOPress post metadata.' ),
|
||||
'get-term-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Get SEOPress term metadata.' ),
|
||||
'update-term-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Update SEOPress term metadata.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
return array(
|
||||
'post_id' => $id,
|
||||
'title' => (string) get_post_meta( $id, '_seopress_titles_title', true ),
|
||||
'description' => (string) get_post_meta( $id, '_seopress_titles_desc', true ),
|
||||
'target_keyword' => (string) get_post_meta( $id, '_seopress_analysis_target_kw', true ),
|
||||
'canonical' => (string) get_post_meta( $id, '_seopress_robots_canonical', true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( isset( $args['title'] ) ) { update_post_meta( $id, '_seopress_titles_title', sanitize_text_field( $args['title'] ) ); }
|
||||
if ( isset( $args['description'] ) ) { update_post_meta( $id, '_seopress_titles_desc', sanitize_textarea_field( $args['description'] ) ); }
|
||||
if ( isset( $args['target_keyword'] ) ) { update_post_meta( $id, '_seopress_analysis_target_kw', sanitize_text_field( $args['target_keyword'] ) ); }
|
||||
return array( 'success' => true, 'post_id' => $id );
|
||||
}
|
||||
|
||||
public function op_get_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
return array( 'term_id' => $id, 'title' => (string) get_term_meta( $id, '_seopress_titles_title', true ) );
|
||||
}
|
||||
|
||||
public function op_update_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( isset( $args['title'] ) ) { update_term_meta( $id, '_seopress_titles_title', sanitize_text_field( $args['title'] ) ); }
|
||||
return array( 'success' => true, 'term_id' => $id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* SureRank SEO integration (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_SureRank_Integration extends EMCP_Tools_SEO_Integration {
|
||||
|
||||
public function id(): string { return 'surerank'; }
|
||||
public function label(): string { return 'SureRank'; }
|
||||
public function is_active(): bool { return defined( 'SURERANK_VERSION' ) || class_exists( 'SureRank\Plugin' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$edit_posts = static function (): bool { return current_user_can( 'edit_posts' ); };
|
||||
return array(
|
||||
'get-post-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Get SureRank post metadata.' ),
|
||||
'update-post-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_post_seo' ), 'perm' => $edit_posts, 'desc' => 'Update SureRank post metadata.' ),
|
||||
'get-term-seo' => array( 'mode' => 'read', 'run' => array( $this, 'op_get_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Get SureRank term metadata.' ),
|
||||
'update-term-seo' => array( 'mode' => 'write', 'run' => array( $this, 'op_update_term_seo' ), 'perm' => $edit_posts, 'desc' => 'Update SureRank term metadata.' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
return array(
|
||||
'post_id' => $id,
|
||||
'title' => (string) get_post_meta( $id, '_surerank_title', true ),
|
||||
'description' => (string) get_post_meta( $id, '_surerank_description', true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( isset( $args['title'] ) ) { update_post_meta( $id, '_surerank_title', sanitize_text_field( $args['title'] ) ); }
|
||||
if ( isset( $args['description'] ) ) { update_post_meta( $id, '_surerank_description', sanitize_textarea_field( $args['description'] ) ); }
|
||||
return array( 'success' => true, 'post_id' => $id );
|
||||
}
|
||||
|
||||
public function op_get_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
return array( 'term_id' => $id, 'title' => (string) get_term_meta( $id, '_surerank_title', true ) );
|
||||
}
|
||||
|
||||
public function op_update_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( isset( $args['title'] ) ) { update_term_meta( $id, '_surerank_title', sanitize_text_field( $args['title'] ) ); }
|
||||
return array( 'success' => true, 'term_id' => $id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Yoast SEO integration (Pro) — yoast-read / yoast-write over Yoast post/term meta.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Yoast_Integration extends EMCP_Tools_SEO_Integration {
|
||||
|
||||
public function id(): string { return 'yoast'; }
|
||||
public function label(): string { return 'Yoast SEO'; }
|
||||
public function is_active(): bool { return defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Frontend' ); }
|
||||
|
||||
protected function operations(): array {
|
||||
$edit_posts = static function (): bool { return current_user_can( 'edit_posts' ); };
|
||||
|
||||
return array(
|
||||
'get-post-seo' => array(
|
||||
'mode' => 'read',
|
||||
'run' => array( $this, 'op_get_post_seo' ),
|
||||
'perm' => $edit_posts,
|
||||
'desc' => 'Get Yoast SEO metadata for a post by { post_id } (title, desc, focuskw, canonical, noindex).',
|
||||
),
|
||||
'update-post-seo' => array(
|
||||
'mode' => 'write',
|
||||
'run' => array( $this, 'op_update_post_seo' ),
|
||||
'perm' => $edit_posts,
|
||||
'desc' => 'Update Yoast SEO metadata for a post by { post_id, title?, description?, focus_keyword?, canonical?, noindex? }.',
|
||||
),
|
||||
'get-term-seo' => array(
|
||||
'mode' => 'read',
|
||||
'run' => array( $this, 'op_get_term_seo' ),
|
||||
'perm' => $edit_posts,
|
||||
'desc' => 'Get Yoast SEO metadata for a taxonomy term by { term_id, taxonomy? }.',
|
||||
),
|
||||
'update-term-seo' => array(
|
||||
'mode' => 'write',
|
||||
'run' => array( $this, 'op_update_term_seo' ),
|
||||
'perm' => $edit_posts,
|
||||
'desc' => 'Update Yoast SEO metadata for a taxonomy term by { term_id, taxonomy?, title?, description? }.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_get_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'post_id required' ); }
|
||||
return array(
|
||||
'post_id' => $id,
|
||||
'title' => (string) get_post_meta( $id, '_yoast_wpseo_title', true ),
|
||||
'description' => (string) get_post_meta( $id, '_yoast_wpseo_metadesc', true ),
|
||||
'focus_keyword' => (string) get_post_meta( $id, '_yoast_wpseo_focuskw', true ),
|
||||
'canonical' => (string) get_post_meta( $id, '_yoast_wpseo_canonical', true ),
|
||||
'noindex' => '1' === (string) get_post_meta( $id, '_yoast_wpseo_meta-robots-noindex', true ),
|
||||
'opengraph_desc' => (string) get_post_meta( $id, '_yoast_wpseo_opengraph-description', true ),
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_post_seo( array $args ) {
|
||||
$id = (int) ( $args['post_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'post_id required' ); }
|
||||
if ( isset( $args['title'] ) ) { update_post_meta( $id, '_yoast_wpseo_title', sanitize_text_field( $args['title'] ) ); }
|
||||
if ( isset( $args['description'] ) ) { update_post_meta( $id, '_yoast_wpseo_metadesc', sanitize_textarea_field( $args['description'] ) ); }
|
||||
if ( isset( $args['focus_keyword'] ) ) { update_post_meta( $id, '_yoast_wpseo_focuskw', sanitize_text_field( $args['focus_keyword'] ) ); }
|
||||
if ( isset( $args['canonical'] ) ) { update_post_meta( $id, '_yoast_wpseo_canonical', esc_url_raw( $args['canonical'] ) ); }
|
||||
if ( isset( $args['noindex'] ) ) { update_post_meta( $id, '_yoast_wpseo_meta-robots-noindex', $args['noindex'] ? '1' : '2' ); }
|
||||
return array( 'success' => true, 'post_id' => $id );
|
||||
}
|
||||
|
||||
public function op_get_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'term_id required' ); }
|
||||
$meta = get_option( 'wpseo_taxonomy_meta', array() );
|
||||
$tax = (string) ( $args['taxonomy'] ?? 'category' );
|
||||
$data = $meta[ $tax ][ $id ] ?? array();
|
||||
return array(
|
||||
'term_id' => $id,
|
||||
'title' => $data['wpseo_title'] ?? '',
|
||||
'description' => $data['wpseo_desc'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
public function op_update_term_seo( array $args ) {
|
||||
$id = (int) ( $args['term_id'] ?? 0 );
|
||||
if ( ! $id ) { return new WP_Error( 'missing_id', 'term_id required' ); }
|
||||
$tax = (string) ( $args['taxonomy'] ?? 'category' );
|
||||
$meta = get_option( 'wpseo_taxonomy_meta', array() );
|
||||
if ( ! isset( $meta[ $tax ] ) ) { $meta[ $tax ] = array(); }
|
||||
if ( ! isset( $meta[ $tax ][ $id ] ) ) { $meta[ $tax ][ $id ] = array(); }
|
||||
if ( isset( $args['title'] ) ) { $meta[ $tax ][ $id ]['wpseo_title'] = sanitize_text_field( $args['title'] ); }
|
||||
if ( isset( $args['description'] ) ) { $meta[ $tax ][ $id ]['wpseo_desc'] = sanitize_textarea_field( $args['description'] ); }
|
||||
update_option( 'wpseo_taxonomy_meta', $meta );
|
||||
return array( 'success' => true, 'term_id' => $id );
|
||||
}
|
||||
}
|
||||
@@ -3654,6 +3654,8 @@ class EMCP_Tools_Admin {
|
||||
<span class="dashicons dashicons-star-filled" aria-hidden="true"></span>
|
||||
<?php esc_html_e( 'Upgrade to Pro', 'emcp-tools' ); ?>
|
||||
</a>
|
||||
<?php else : ?>
|
||||
<span class="elementor-mcp-badge elementor-mcp-badge--pro" style="padding: 4px 10px; font-size: 11px; font-weight: 700; border-radius: 4px; background: #9333ea; color: #fff; margin-right: 8px;">PRO UNLOCKED</span>
|
||||
<?php endif; ?>
|
||||
<div class="emcp-help-menu">
|
||||
<button type="button" class="emcp-help-toggle" aria-haspopup="true">
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin-ajax handlers for the Pro library admin pages (Prompts, Templates,
|
||||
* Brand Kits) — the "Sync Library", apply, import and restore actions.
|
||||
*
|
||||
* These run only in the admin context and only when Freemius is present; they
|
||||
* are wired by EMCP_Tools_Pro_Ajax::register() from the bootstrap.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 2.1.0 (extracted from the bootstrap file)
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pro library admin-ajax handlers.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
class EMCP_Tools_Pro_Ajax {
|
||||
|
||||
/**
|
||||
* Registers all wp_ajax_* handlers for the Pro library pages.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public static function register(): void {
|
||||
add_action( 'wp_ajax_emcp_tools_sync_pro_prompts', array( __CLASS__, 'sync_prompts' ) );
|
||||
|
||||
add_action( 'wp_ajax_emcp_tools_sync_pro_templates', array( __CLASS__, 'sync_templates' ) );
|
||||
add_action( 'wp_ajax_emcp_tools_apply_pro_template', array( __CLASS__, 'apply_template' ) );
|
||||
add_action( 'wp_ajax_emcp_tools_import_pro_template', array( __CLASS__, 'import_template' ) );
|
||||
|
||||
add_action( 'wp_ajax_emcp_tools_sync_pro_brand_kits', array( __CLASS__, 'sync_brand_kits' ) );
|
||||
add_action( 'wp_ajax_emcp_tools_apply_pro_brand_kit', array( __CLASS__, 'apply_brand_kit' ) );
|
||||
add_action( 'wp_ajax_emcp_tools_restore_pro_brand_kit', array( __CLASS__, 'restore_brand_kit' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* "Sync Library" button on the Pro Prompts page.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public static function sync_prompts(): void {
|
||||
check_ajax_referer( 'emcp_tools_sync_pro_prompts', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to sync prompts.', 'emcp-tools' ) ), 403 );
|
||||
}
|
||||
|
||||
$bundle = EMCP_Tools_Pro_Prompts::get_bundle( true );
|
||||
if ( is_wp_error( $bundle ) ) {
|
||||
wp_send_json_error( array( 'message' => $bundle->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
foreach ( $bundle['categories'] as $category ) {
|
||||
$total += isset( $category['prompts'] ) && is_array( $category['prompts'] ) ? count( $category['prompts'] ) : 0;
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => sprintf(
|
||||
/* translators: %1$d: total prompts, %2$d: total categories */
|
||||
__( 'Synced %1$d prompts across %2$d categories.', 'emcp-tools' ),
|
||||
$total,
|
||||
count( $bundle['categories'] )
|
||||
),
|
||||
'fetched_at' => $bundle['fetched_at'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Sync Library" button on the Pro Templates page.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*/
|
||||
public static function sync_templates(): void {
|
||||
check_ajax_referer( 'emcp_tools_sync_pro_templates', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to sync templates.', 'emcp-tools' ) ), 403 );
|
||||
}
|
||||
|
||||
$bundle = EMCP_Tools_Pro_Templates::get_bundle( true );
|
||||
if ( is_wp_error( $bundle ) ) {
|
||||
wp_send_json_error( array( 'message' => $bundle->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
foreach ( $bundle['categories'] as $category ) {
|
||||
$total += isset( $category['templates'] ) && is_array( $category['templates'] ) ? count( $category['templates'] ) : 0;
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => sprintf(
|
||||
/* translators: %1$d: total templates, %2$d: total categories */
|
||||
__( 'Synced %1$d templates across %2$d categories.', 'emcp-tools' ),
|
||||
$total,
|
||||
count( $bundle['categories'] )
|
||||
),
|
||||
'fetched_at' => $bundle['fetched_at'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a template to a new (or existing) page.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*/
|
||||
public static function apply_template(): void {
|
||||
check_ajax_referer( 'emcp_tools_apply_pro_template', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'edit_pages' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to create pages.', 'emcp-tools' ) ), 403 );
|
||||
}
|
||||
|
||||
$category_slug = isset( $_POST['category_slug'] ) ? sanitize_key( wp_unslash( $_POST['category_slug'] ) ) : '';
|
||||
$template_slug = isset( $_POST['template_slug'] ) ? sanitize_key( wp_unslash( $_POST['template_slug'] ) ) : '';
|
||||
$target_post_id = isset( $_POST['target_post_id'] ) ? absint( wp_unslash( $_POST['target_post_id'] ) ) : 0;
|
||||
|
||||
if ( '' === $category_slug || '' === $template_slug ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Missing category or template slug.', 'emcp-tools' ) ), 400 );
|
||||
}
|
||||
|
||||
$result = EMCP_Tools_Pro_Templates::apply_template( $category_slug, $template_slug, $target_post_id );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
wp_send_json_success( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a template into Elementor's Saved Templates library.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*/
|
||||
public static function import_template(): void {
|
||||
check_ajax_referer( 'emcp_tools_import_pro_template', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to import templates.', 'emcp-tools' ) ), 403 );
|
||||
}
|
||||
|
||||
$category_slug = isset( $_POST['category_slug'] ) ? sanitize_key( wp_unslash( $_POST['category_slug'] ) ) : '';
|
||||
$template_slug = isset( $_POST['template_slug'] ) ? sanitize_key( wp_unslash( $_POST['template_slug'] ) ) : '';
|
||||
|
||||
if ( '' === $category_slug || '' === $template_slug ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Missing category or template slug.', 'emcp-tools' ) ), 400 );
|
||||
}
|
||||
|
||||
$result = EMCP_Tools_Pro_Templates::import_to_library( $category_slug, $template_slug );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
wp_send_json_success( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* "Sync Library" button on the Brand Kits page.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
public static function sync_brand_kits(): void {
|
||||
check_ajax_referer( 'emcp_tools_sync_pro_brand_kits', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) || ! EMCP_Tools_Pro_Brand_Kits::user_has_access() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to sync brand kits.', 'emcp-tools' ) ), 403 );
|
||||
}
|
||||
|
||||
$bundle = EMCP_Tools_Pro_Brand_Kits::get_bundle( true );
|
||||
if ( is_wp_error( $bundle ) ) {
|
||||
wp_send_json_error( array( 'message' => $bundle->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
foreach ( $bundle['categories'] as $category ) {
|
||||
$total += isset( $category['kits'] ) && is_array( $category['kits'] ) ? count( $category['kits'] ) : 0;
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => sprintf(
|
||||
/* translators: %1$d: total kits, %2$d: total categories */
|
||||
__( 'Synced %1$d brand kits across %2$d categories.', 'emcp-tools' ),
|
||||
$total,
|
||||
count( $bundle['categories'] )
|
||||
),
|
||||
'fetched_at' => $bundle['fetched_at'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a brand kit from the admin page.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
public static function apply_brand_kit(): void {
|
||||
check_ajax_referer( 'emcp_tools_apply_pro_brand_kit', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to apply brand kits.', 'emcp-tools' ) ), 403 );
|
||||
}
|
||||
|
||||
$kit_slug = isset( $_POST['kit_slug'] ) ? sanitize_key( wp_unslash( $_POST['kit_slug'] ) ) : '';
|
||||
$category_slug = isset( $_POST['category_slug'] ) ? sanitize_key( wp_unslash( $_POST['category_slug'] ) ) : '';
|
||||
$do_backup = ! isset( $_POST['backup'] ) || '0' !== (string) wp_unslash( $_POST['backup'] );
|
||||
|
||||
if ( '' === $kit_slug ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Missing kit slug.', 'emcp-tools' ) ), 400 );
|
||||
}
|
||||
|
||||
// Resolve the kit from the Pro remote library when the site has it, falling
|
||||
// back to the 10 bundled free kits otherwise. Applying is a free feature;
|
||||
// the Pro library is just a larger pool of kits to apply from.
|
||||
$kit = null;
|
||||
if ( class_exists( 'EMCP_Tools_Pro_Brand_Kits' ) && EMCP_Tools_Pro_Brand_Kits::user_has_access() ) {
|
||||
$kit = EMCP_Tools_Pro_Brand_Kits::find_kit( $kit_slug, $category_slug );
|
||||
}
|
||||
if ( null === $kit && class_exists( 'EMCP_Tools_Free_Brand_Kits' ) ) {
|
||||
$kit = EMCP_Tools_Free_Brand_Kits::find_kit( $kit_slug, $category_slug );
|
||||
}
|
||||
if ( null === $kit ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Brand kit not found. Try syncing the library first.', 'emcp-tools' ) ), 404 );
|
||||
}
|
||||
|
||||
$backup_id = null;
|
||||
if ( $do_backup ) {
|
||||
$backup = EMCP_Tools_Kit_Backup_Store::create( isset( $kit['title'] ) ? (string) $kit['title'] : $kit_slug );
|
||||
if ( ! is_wp_error( $backup ) ) {
|
||||
$backup_id = (int) $backup;
|
||||
}
|
||||
}
|
||||
|
||||
$result = EMCP_Tools_Pro_Brand_Kits::apply_kit( $kit );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
$result['backup_id'] = $backup_id;
|
||||
$result['view_url'] = self::recent_elementor_page_url();
|
||||
wp_send_json_success( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a brand kit backup from the admin page.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
public static function restore_brand_kit(): void {
|
||||
check_ajax_referer( 'emcp_tools_restore_pro_brand_kit', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You do not have permission to restore brand kits.', 'emcp-tools' ) ), 403 );
|
||||
}
|
||||
|
||||
$backup_id = isset( $_POST['backup_id'] ) ? absint( wp_unslash( $_POST['backup_id'] ) ) : 0;
|
||||
$full_clobber = isset( $_POST['full_clobber'] ) && '1' === (string) wp_unslash( $_POST['full_clobber'] );
|
||||
|
||||
if ( $backup_id <= 0 ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Missing or invalid backup.', 'emcp-tools' ) ), 400 );
|
||||
}
|
||||
|
||||
$result = EMCP_Tools_Kit_Backup_Store::restore( $backup_id, $full_clobber );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'Brand restored from backup.', 'emcp-tools' ),
|
||||
'view_url' => self::recent_elementor_page_url(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of the most-recently-modified Elementor page (builder mode), or the
|
||||
* site homepage as a fallback. Used by the apply/restore toasts so the user
|
||||
* lands somewhere that actually showcases the change.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function recent_elementor_page_url(): string {
|
||||
$query = new WP_Query(
|
||||
array(
|
||||
'post_type' => 'any',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
'orderby' => 'modified',
|
||||
'order' => 'DESC',
|
||||
'no_found_rows' => true,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_elementor_edit_mode',
|
||||
'value' => 'builder',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $query->posts ) ) {
|
||||
$permalink = get_permalink( $query->posts[0] );
|
||||
if ( $permalink ) {
|
||||
return $permalink;
|
||||
}
|
||||
}
|
||||
|
||||
return home_url( '/' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Fetches and caches the premium Brand Kits library from emcptools.com,
|
||||
* and orchestrates applying a kit to the active Elementor kit.
|
||||
*
|
||||
* Mirror of class-pro-templates.php for brand kits: same auth flow, same 24h
|
||||
* transient cache, different endpoint and bundle shape. `apply_kit()` is a thin
|
||||
* orchestrator that routes ALL writes through EMCP_Tools_System_Kit_Writer
|
||||
* (§ 4.2.1) — it never calls the abilities.
|
||||
*
|
||||
* See docs/BRAND_KITS_PLAN.md §§ 2.3, 3.2, 4.2.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Premium brand kits service.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
class EMCP_Tools_Pro_Brand_Kits {
|
||||
|
||||
/**
|
||||
* Transient key for the cached bundle.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_KEY = 'emcp_tools_pro_brand_kits_bundle';
|
||||
|
||||
/**
|
||||
* Transient TTL in seconds. 24 hours.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_TTL = 86400;
|
||||
|
||||
/**
|
||||
* Durable option mirroring the last successful bundle. The transient is just
|
||||
* a 24h freshness cache that can expire/evict; this option never expires, so
|
||||
* the Brand Kits count + library keep showing the last synced set instead of
|
||||
* resetting to 0. Stored non-autoloaded.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const STORE_KEY = 'emcp_tools_pro_brand_kits_store';
|
||||
|
||||
/**
|
||||
* Cron hook that re-fetches the bundle in the background when stale.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REFRESH_HOOK = 'emcp_tools_refresh_pro_brand_kits';
|
||||
|
||||
/**
|
||||
* Default endpoint. Filterable via `emcp_tools_pro_brand_kits_endpoint`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const DEFAULT_ENDPOINT = 'https://emcptools.com/api/emcp/brand-kits.json';
|
||||
|
||||
/**
|
||||
* Whether the current site can access premium brand kits.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function user_has_access(): bool {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return false;
|
||||
}
|
||||
return emcp_tools_fs()->can_use_premium_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the brand kits bundle. Returns the cached copy when available.
|
||||
*
|
||||
* Bundle shape:
|
||||
* [
|
||||
* 'fetched_at' => int,
|
||||
* 'categories' => [
|
||||
* [ 'slug' => 'corporate', 'label' => 'Corporate & Tech', 'kits' => [ {kit}, ... ] ],
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param bool $force_refresh Bypass the local cache.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public static function get_bundle( bool $force_refresh = false ) {
|
||||
$local_bundle = EMCP_TOOLS_DIR . 'assets/brand-kits/pro-brand-kits.json';
|
||||
if ( file_exists( $local_bundle ) && is_readable( $local_bundle ) ) {
|
||||
$data = json_decode( (string) file_get_contents( $local_bundle ), true );
|
||||
if ( is_array( $data ) && ! empty( $data['categories'] ) ) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $force_refresh ) {
|
||||
$cached = get_transient( self::CACHE_KEY );
|
||||
if ( is_array( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
$stored = get_option( self::STORE_KEY );
|
||||
if ( is_array( $stored ) && ! empty( $stored['categories'] ) ) {
|
||||
self::maybe_schedule_refresh();
|
||||
return $stored;
|
||||
}
|
||||
}
|
||||
|
||||
return self::fetch_remote_bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific kit by category + slug, or by slug alone (first match).
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param string $kit_slug The kit slug.
|
||||
* @param string $category_slug Optional category slug to disambiguate.
|
||||
* @return array|null
|
||||
*/
|
||||
public static function find_kit( string $kit_slug, string $category_slug = '' ): ?array {
|
||||
$bundle = self::get_bundle();
|
||||
if ( is_wp_error( $bundle ) || ! is_array( $bundle ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ( $bundle['categories'] ?? array() as $category ) {
|
||||
if ( '' !== $category_slug && ( $category['slug'] ?? '' ) !== $category_slug ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( $category['kits'] ?? array() as $kit ) {
|
||||
if ( ( $kit['slug'] ?? '' ) === $kit_slug ) {
|
||||
return $kit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a kit (already resolved from the bundle) to the active Elementor
|
||||
* kit. Thin orchestrator over the shared writer service.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param array $kit A kit entry from the bundle (see § 3.1 shape).
|
||||
* @return array|WP_Error Summary of what was applied.
|
||||
*/
|
||||
public static function apply_kit( array $kit ) {
|
||||
if ( ! class_exists( 'EMCP_Tools_System_Kit_Writer' ) ) {
|
||||
return new WP_Error( 'no_writer', __( 'The kit writer service is unavailable.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
// Delegate to the neutral writer orchestrator (capability-gated per
|
||||
// write). Applying is a free feature as of 1.9.0; the Pro value is the
|
||||
// larger remote library + the MCP brand-kit tools, gated elsewhere.
|
||||
return EMCP_Tools_System_Kit_Writer::apply_kit( $kit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit the remote endpoint, validate the response, store it in cache.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private static function fetch_remote_bundle() {
|
||||
$license_key = self::get_license_key();
|
||||
$license_id = self::get_license_id();
|
||||
if ( '' === $license_key || '' === $license_id ) {
|
||||
return new WP_Error( 'no_license_key', __( 'No active EMCP Tools Pro license was found on this site.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$endpoint = apply_filters( 'emcp_tools_pro_brand_kits_endpoint', self::DEFAULT_ENDPOINT );
|
||||
|
||||
$response = wp_remote_get(
|
||||
$endpoint,
|
||||
array(
|
||||
'timeout' => 12,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $license_key,
|
||||
'X-EMCP-License-Id' => $license_id,
|
||||
'X-EMCP-Site' => home_url(),
|
||||
'X-EMCP-Plugin-Version' => EMCP_TOOLS_VERSION,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( 403 === $code ) {
|
||||
return new WP_Error(
|
||||
'forbidden',
|
||||
__( 'Premium Brand Kits are unavailable on this site. Make sure your EMCP Tools Pro license is active and this site is on its activated-sites list. Contact support if the issue persists.', 'emcp-tools' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 429 === $code ) {
|
||||
return new WP_Error(
|
||||
'rate_limited',
|
||||
__( 'Brand Kits endpoint is rate-limiting this site. Try again in a few minutes.', 'emcp-tools' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 200 !== $code ) {
|
||||
return new WP_Error(
|
||||
'remote_error',
|
||||
sprintf(
|
||||
/* translators: %d: HTTP status code */
|
||||
__( 'Brand Kits endpoint returned HTTP %d. Please try again later or contact support.', 'emcp-tools' ),
|
||||
$code
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$bundle = json_decode( $body, true );
|
||||
|
||||
if ( ! is_array( $bundle ) || ! isset( $bundle['categories'] ) || ! is_array( $bundle['categories'] ) ) {
|
||||
return new WP_Error( 'invalid_payload', __( 'Brand Kits endpoint returned an unexpected payload.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$bundle['fetched_at'] = time();
|
||||
set_transient( self::CACHE_KEY, $bundle, self::CACHE_TTL );
|
||||
// Durable mirror so the count/library survive transient expiry/eviction.
|
||||
update_option( self::STORE_KEY, $bundle, false );
|
||||
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total kit count for the admin stats bar. Reads the fresh transient, else
|
||||
* the durable store (scheduling a background refresh), else 0. Never blocks
|
||||
* on an HTTP fetch, so it no longer resets to 0 when the transient expires.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function count_cached_kits(): int {
|
||||
$bundle = self::get_bundle();
|
||||
if ( ! is_array( $bundle ) || empty( $bundle['categories'] ) ) {
|
||||
return 0;
|
||||
}
|
||||
$total = 0;
|
||||
foreach ( $bundle['categories'] as $category ) {
|
||||
if ( ! empty( $category['kits'] ) && is_array( $category['kits'] ) ) {
|
||||
$total += count( $category['kits'] );
|
||||
}
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a one-off background refresh (deduped) so an expired cache
|
||||
* self-heals without the user clicking "Sync Library".
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public static function maybe_schedule_refresh(): void {
|
||||
if ( ! function_exists( 'wp_next_scheduled' ) || wp_next_scheduled( self::REFRESH_HOOK ) ) {
|
||||
return;
|
||||
}
|
||||
wp_schedule_single_event( time() + 30, self::REFRESH_HOOK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active license key from Freemius, or empty string.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_license_key(): string {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return '';
|
||||
}
|
||||
$license = emcp_tools_fs()->_get_license();
|
||||
if ( ! $license || empty( $license->secret_key ) ) {
|
||||
return '';
|
||||
}
|
||||
return (string) $license->secret_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active license's numeric Freemius ID, or empty string.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_license_id(): string {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return '';
|
||||
}
|
||||
$license = emcp_tools_fs()->_get_license();
|
||||
if ( ! $license || empty( $license->id ) ) {
|
||||
return '';
|
||||
}
|
||||
return (string) $license->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached bundle.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
public static function flush_cache(): void {
|
||||
delete_transient( self::CACHE_KEY );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
/**
|
||||
* Fetches and caches the premium prompts library from emcptools.com.
|
||||
*
|
||||
* The prompt content lives on the server, not in the plugin zip. The plugin
|
||||
* sends the active Freemius license key via Authorization header to a
|
||||
* license-gated endpoint; the server validates with Freemius's API
|
||||
* (including the site-binding check) and returns the full bundle, which
|
||||
* the plugin caches for 24 hours.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Premium prompts service.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
class EMCP_Tools_Pro_Prompts {
|
||||
|
||||
/**
|
||||
* Transient key for the cached manifest + content bundle.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_KEY = 'emcp_tools_pro_prompts_bundle';
|
||||
|
||||
/**
|
||||
* Transient TTL in seconds. 24 hours.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_TTL = 86400;
|
||||
|
||||
/**
|
||||
* Durable option mirroring the last successful bundle. The transient is just
|
||||
* a 24h freshness cache that can expire or be evicted (object cache); this
|
||||
* option never expires, so the Prompts count + tab keep showing the last
|
||||
* synced library instead of resetting to the bundled fallback. Stored
|
||||
* non-autoloaded (it can be large).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const STORE_KEY = 'emcp_tools_pro_prompts_store';
|
||||
|
||||
/**
|
||||
* Cron hook that re-fetches the bundle in the background when the transient
|
||||
* is stale, so an expired cache self-heals without a manual "Sync Library".
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const REFRESH_HOOK = 'emcp_tools_refresh_pro_prompts';
|
||||
|
||||
/**
|
||||
* Default endpoint that serves the prompts bundle. Filterable via
|
||||
* `emcp_tools_pro_prompts_endpoint` for staging / local testing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const DEFAULT_ENDPOINT = 'https://emcptools.com/api/emcp/prompts.json';
|
||||
|
||||
/**
|
||||
* Whether the current site can access premium prompts.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function user_has_access(): bool {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return false;
|
||||
}
|
||||
return emcp_tools_fs()->can_use_premium_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prompts bundle. Returns the cached copy when available.
|
||||
*
|
||||
* Bundle structure:
|
||||
* [
|
||||
* 'fetched_at' => int (unix timestamp),
|
||||
* 'categories' => [
|
||||
* [
|
||||
* 'slug' => 'food-dining',
|
||||
* 'label' => 'Food & Dining',
|
||||
* 'prompts' => [
|
||||
* [
|
||||
* 'slug' => 'bakery',
|
||||
* 'title' => 'Bakery',
|
||||
* 'description' => 'Warm, charming landing page for an artisan bakery.',
|
||||
* 'content' => '<full markdown body>',
|
||||
* ],
|
||||
* ...
|
||||
* ],
|
||||
* ],
|
||||
* ...
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @since 1.7.0
|
||||
*
|
||||
* @param bool $force_refresh Bypass the local cache.
|
||||
* @return array|WP_Error Bundle on success, WP_Error on failure.
|
||||
*/
|
||||
public static function get_bundle( bool $force_refresh = false ) {
|
||||
$local_bundle = EMCP_TOOLS_DIR . 'assets/prompts/prompts-bundle.json';
|
||||
if ( file_exists( $local_bundle ) && is_readable( $local_bundle ) ) {
|
||||
$data = json_decode( (string) file_get_contents( $local_bundle ), true );
|
||||
if ( is_array( $data ) && ! empty( $data['categories'] ) ) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $force_refresh ) {
|
||||
$cached = get_transient( self::CACHE_KEY );
|
||||
if ( is_array( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
$stored = get_option( self::STORE_KEY );
|
||||
if ( is_array( $stored ) && ! empty( $stored['categories'] ) ) {
|
||||
self::maybe_schedule_refresh();
|
||||
return $stored;
|
||||
}
|
||||
}
|
||||
|
||||
return self::fetch_remote_bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit the remote endpoint, validate the response, store it in the
|
||||
* local cache.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private static function fetch_remote_bundle() {
|
||||
$license_key = self::get_license_key();
|
||||
$license_id = self::get_license_id();
|
||||
if ( '' === $license_key || '' === $license_id ) {
|
||||
return new WP_Error( 'no_license_key', __( 'No active EMCP Tools Pro license was found on this site.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$endpoint = apply_filters( 'emcp_tools_pro_prompts_endpoint', self::DEFAULT_ENDPOINT );
|
||||
|
||||
// Auth + site/version metadata travel in headers, never the URL. License
|
||||
// keys are credentials — query strings get logged by every proxy in the
|
||||
// chain (server access log, Dokploy log, intermediate CDN, browser
|
||||
// history). Authorization headers don't.
|
||||
//
|
||||
// X-EMCP-License-Id carries the numeric Freemius license ID. The
|
||||
// server uses it to route to the correct Freemius API endpoint
|
||||
// (/v1/plugins/{plugin_id}/licenses/{license_id}.json) and passes
|
||||
// the Authorization-header license key as a verification query
|
||||
// parameter. Freemius's API does NOT accept the license key as the
|
||||
// path segment — it expects the integer ID there.
|
||||
$response = wp_remote_get(
|
||||
$endpoint,
|
||||
array(
|
||||
'timeout' => 12,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $license_key,
|
||||
'X-EMCP-License-Id' => $license_id,
|
||||
'X-EMCP-Site' => home_url(),
|
||||
'X-EMCP-Plugin-Version' => EMCP_TOOLS_VERSION,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// The server returns a uniform 403 for every auth failure (bad license,
|
||||
// site not activated, expired, etc.) to avoid leaking which condition
|
||||
// triggered the rejection. Don't try to be clever inferring the cause
|
||||
// from the response — show a single message and let the support flow
|
||||
// handle diagnosis.
|
||||
if ( 403 === $code ) {
|
||||
return new WP_Error(
|
||||
'forbidden',
|
||||
__( 'Premium Prompts are unavailable on this site. Make sure your EMCP Tools Pro license is active and this site is on its activated-sites list. Contact support if the issue persists.', 'emcp-tools' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 429 === $code ) {
|
||||
return new WP_Error(
|
||||
'rate_limited',
|
||||
__( 'Premium Prompts endpoint is rate-limiting this site. Try again in a few minutes.', 'emcp-tools' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 200 !== $code ) {
|
||||
return new WP_Error(
|
||||
'remote_error',
|
||||
sprintf(
|
||||
/* translators: %d: HTTP status code */
|
||||
__( 'Prompts endpoint returned HTTP %d. Please try again later or contact support.', 'emcp-tools' ),
|
||||
$code
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$bundle = json_decode( $body, true );
|
||||
|
||||
if ( ! is_array( $bundle ) || empty( $bundle['categories'] ) || ! is_array( $bundle['categories'] ) ) {
|
||||
return new WP_Error( 'invalid_payload', __( 'Prompts endpoint returned an unexpected payload.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$bundle['fetched_at'] = time();
|
||||
set_transient( self::CACHE_KEY, $bundle, self::CACHE_TTL );
|
||||
// Durable mirror so the count/library survive transient expiry/eviction.
|
||||
update_option( self::STORE_KEY, $bundle, false );
|
||||
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-blocking prompt count for the admin stats bar. Reads the fresh
|
||||
* transient, else the durable store (scheduling a background refresh), else
|
||||
* 0. Never performs a synchronous HTTP fetch, so it can't slow a page load.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function cached_count(): int {
|
||||
$bundle = self::get_bundle();
|
||||
if ( ! is_array( $bundle ) || empty( $bundle['categories'] ) ) {
|
||||
return 0;
|
||||
}
|
||||
$count = 0;
|
||||
foreach ( $bundle['categories'] as $category ) {
|
||||
if ( ! empty( $category['prompts'] ) && is_array( $category['prompts'] ) ) {
|
||||
$count += count( $category['prompts'] );
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a one-off background refresh (deduped) so an expired cache
|
||||
* self-heals without the user clicking "Sync Library".
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
public static function maybe_schedule_refresh(): void {
|
||||
if ( ! function_exists( 'wp_next_scheduled' ) || wp_next_scheduled( self::REFRESH_HOOK ) ) {
|
||||
return;
|
||||
}
|
||||
wp_schedule_single_event( time() + 30, self::REFRESH_HOOK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active license key from Freemius, or empty string.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_license_key(): string {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return '';
|
||||
}
|
||||
$license = emcp_tools_fs()->_get_license();
|
||||
if ( ! $license || empty( $license->secret_key ) ) {
|
||||
return '';
|
||||
}
|
||||
return (string) $license->secret_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active license's numeric Freemius ID, or empty string.
|
||||
* The server uses this to route the Freemius API check to the
|
||||
* correct license endpoint (id in path, key in query string).
|
||||
*
|
||||
* @since 1.7.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_license_id(): string {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return '';
|
||||
}
|
||||
$license = emcp_tools_fs()->_get_license();
|
||||
if ( ! $license || empty( $license->id ) ) {
|
||||
return '';
|
||||
}
|
||||
return (string) $license->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached bundle. Useful from a "Sync now" button or after
|
||||
* the user activates / changes a license.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*/
|
||||
public static function flush_cache(): void {
|
||||
delete_transient( self::CACHE_KEY );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/**
|
||||
* Premium Skills download — bundles every skill folder under the plugin's
|
||||
* skills/ tree (emcp-skills, emcp-performance, emcp-security) on the fly and
|
||||
* streams them as emcp-skills.zip for licensed Pro users.
|
||||
*
|
||||
* The skills/ folder ships only in the premium build (.emcp-pro marker
|
||||
* file present); the free zip excludes it. Even on a premium install the
|
||||
* download is also gated by emcp_tools_fs()->can_use_premium_code() so a
|
||||
* leaked premium zip on a non-licensed site can't serve the skill.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.7.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Pro_Skills {
|
||||
|
||||
/**
|
||||
* Relative path (inside the plugin directory) to the skill source folder.
|
||||
* Folder contents get zipped and streamed.
|
||||
*/
|
||||
const SOURCE_RELATIVE = 'skills';
|
||||
|
||||
/**
|
||||
* Filename sent to the user.
|
||||
*/
|
||||
const DOWNLOAD_FILENAME = 'emcp-skills.zip';
|
||||
|
||||
/**
|
||||
* Action name used by admin-post.php — single source of truth.
|
||||
*/
|
||||
const ACTION = 'emcp_tools_download_skills';
|
||||
|
||||
public function init(): void {
|
||||
// admin-post.php handles both logged-in and logged-out cases. Skills
|
||||
// download is admin-only, so register the authed handler only.
|
||||
add_action( 'admin_post_' . self::ACTION, array( $this, 'handle_download' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Site can serve the skills zip iff:
|
||||
* 1. Freemius license grants Pro access on this site, AND
|
||||
* 2. The skills folder is actually present (premium build).
|
||||
*/
|
||||
public static function user_has_access(): bool {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( ! emcp_tools_fs()->can_use_premium_code() ) {
|
||||
return false;
|
||||
}
|
||||
return self::skills_dir_exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the skills folder is present on disk. True only on premium
|
||||
* builds (the marker `.emcp-pro` ships in the same release zip that
|
||||
* carries this folder).
|
||||
*/
|
||||
public static function skills_dir_exists(): bool {
|
||||
$path = self::source_path();
|
||||
return is_dir( $path ) && is_readable( $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the skill source folder.
|
||||
*/
|
||||
private static function source_path(): string {
|
||||
return EMCP_TOOLS_DIR . self::SOURCE_RELATIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL for the download action — used by the button on the Skills tab.
|
||||
*/
|
||||
public static function download_url(): string {
|
||||
return wp_nonce_url(
|
||||
admin_url( 'admin-post.php?action=' . self::ACTION ),
|
||||
self::ACTION,
|
||||
'_emcp_nonce'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* admin-post.php callback. Validates auth + license, builds a zip in
|
||||
* a temp file, streams it to the client. Halts execution at the end —
|
||||
* standard for file-download handlers.
|
||||
*/
|
||||
public function handle_download(): void {
|
||||
// Nonce + cap check.
|
||||
if (
|
||||
! isset( $_GET['_emcp_nonce'] )
|
||||
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_emcp_nonce'] ) ), self::ACTION )
|
||||
) {
|
||||
wp_die( esc_html__( 'Invalid request.', 'emcp-tools' ), '', array( 'response' => 403 ) );
|
||||
}
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to download this.', 'emcp-tools' ), '', array( 'response' => 403 ) );
|
||||
}
|
||||
if ( ! self::user_has_access() ) {
|
||||
wp_die( esc_html__( 'A valid EMCP Tools Pro license is required to download the skills bundle.', 'emcp-tools' ), '', array( 'response' => 403 ) );
|
||||
}
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
wp_die( esc_html__( 'The ZipArchive PHP extension is required to build the skill bundle. Please contact your host to enable it.', 'emcp-tools' ), '', array( 'response' => 500 ) );
|
||||
}
|
||||
|
||||
$source = self::source_path();
|
||||
$tmp = wp_tempnam( self::DOWNLOAD_FILENAME );
|
||||
if ( ! $tmp ) {
|
||||
wp_die( esc_html__( 'Could not create a temporary file for the skill bundle.', 'emcp-tools' ), '', array( 'response' => 500 ) );
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ( true !== $zip->open( $tmp, ZipArchive::OVERWRITE | ZipArchive::CREATE ) ) {
|
||||
@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
|
||||
wp_die( esc_html__( 'Could not open the temporary zip archive for writing.', 'emcp-tools' ), '', array( 'response' => 500 ) );
|
||||
}
|
||||
|
||||
// Zip the whole skills/ tree so every bundled skill folder
|
||||
// (emcp-skills/, emcp-performance/, emcp-security/) is a top-level
|
||||
// directory when the user extracts the archive.
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator( $source, RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
foreach ( $iterator as $file_info ) {
|
||||
$abs_path = $file_info->getPathname();
|
||||
$rel_path = ltrim( substr( $abs_path, strlen( $source ) ), DIRECTORY_SEPARATOR . '/' );
|
||||
if ( '' === $rel_path ) {
|
||||
continue;
|
||||
}
|
||||
$zip_path = str_replace( DIRECTORY_SEPARATOR, '/', $rel_path );
|
||||
if ( $file_info->isDir() ) {
|
||||
$zip->addEmptyDir( $zip_path );
|
||||
} else {
|
||||
$zip->addFile( $abs_path, $zip_path );
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
// Stream the file.
|
||||
nocache_headers();
|
||||
header( 'Content-Type: application/zip' );
|
||||
header( 'Content-Disposition: attachment; filename="' . self::DOWNLOAD_FILENAME . '"' );
|
||||
header( 'Content-Length: ' . filesize( $tmp ) );
|
||||
header( 'X-Content-Type-Options: nosniff' );
|
||||
|
||||
// flush any output buffers WP started so the binary doesn't get
|
||||
// corrupted by stray whitespace from autoload.
|
||||
while ( ob_get_level() > 0 ) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
readfile( $tmp );
|
||||
@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
<?php
|
||||
/**
|
||||
* Fetches and caches the premium templates library from emcptools.com.
|
||||
*
|
||||
* Mirror of class-pro-prompts.php for templates. Same auth flow, different
|
||||
* endpoint and bundle shape. Each template's `data` field is an Elementor
|
||||
* element tree that can be imported into a new or existing page.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.7.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Premium templates service.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*/
|
||||
class EMCP_Tools_Pro_Templates {
|
||||
|
||||
/**
|
||||
* Transient key for the cached bundle.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_KEY = 'emcp_tools_pro_templates_bundle';
|
||||
|
||||
/**
|
||||
* Transient TTL in seconds. 24 hours.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_TTL = 86400;
|
||||
|
||||
/**
|
||||
* Default endpoint. Filterable via `emcp_tools_pro_templates_endpoint`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const DEFAULT_ENDPOINT = 'https://emcptools.com/api/emcp/templates.json';
|
||||
|
||||
/**
|
||||
* Whether the current site can access premium templates.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function user_has_access(): bool {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return false;
|
||||
}
|
||||
return emcp_tools_fs()->can_use_premium_code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the templates bundle. Returns cached copy when available.
|
||||
*
|
||||
* Bundle shape:
|
||||
* [
|
||||
* 'fetched_at' => int,
|
||||
* 'categories' => [
|
||||
* [
|
||||
* 'slug' => 'hero-sections',
|
||||
* 'label' => 'Hero Sections',
|
||||
* 'templates' => [
|
||||
* [
|
||||
* 'slug' => 'bakery-hero',
|
||||
* 'title' => 'Bakery Hero',
|
||||
* 'description' => '...',
|
||||
* 'thumbnail_url' => 'https://...' (optional),
|
||||
* 'data' => [ ... Elementor elements ... ],
|
||||
* ],
|
||||
* ],
|
||||
* ],
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @param bool $force_refresh Bypass the local cache.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public static function get_bundle( bool $force_refresh = false ) {
|
||||
$local_bundle = EMCP_TOOLS_DIR . 'assets/templates/templates-bundle.json';
|
||||
if ( file_exists( $local_bundle ) && is_readable( $local_bundle ) ) {
|
||||
$data = json_decode( (string) file_get_contents( $local_bundle ), true );
|
||||
if ( is_array( $data ) && ! empty( $data['categories'] ) ) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $force_refresh ) {
|
||||
$cached = get_transient( self::CACHE_KEY );
|
||||
if ( is_array( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
return self::fetch_remote_bundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific template by its category and slug. Returns the template
|
||||
* entry (with the `data` array) or null if not found.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @param string $category_slug
|
||||
* @param string $template_slug
|
||||
* @return array|null
|
||||
*/
|
||||
public static function find_template( string $category_slug, string $template_slug ): ?array {
|
||||
$bundle = self::get_bundle();
|
||||
if ( is_wp_error( $bundle ) || ! is_array( $bundle ) ) {
|
||||
return null;
|
||||
}
|
||||
foreach ( $bundle['categories'] ?? array() as $category ) {
|
||||
if ( ( $category['slug'] ?? '' ) !== $category_slug ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( $category['templates'] ?? array() as $template ) {
|
||||
if ( ( $template['slug'] ?? '' ) === $template_slug ) {
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit the remote endpoint, validate the response, store it in cache.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private static function fetch_remote_bundle() {
|
||||
$license_key = self::get_license_key();
|
||||
$license_id = self::get_license_id();
|
||||
if ( '' === $license_key || '' === $license_id ) {
|
||||
return new WP_Error( 'no_license_key', __( 'No active EMCP Tools Pro license was found on this site.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$endpoint = apply_filters( 'emcp_tools_pro_templates_endpoint', self::DEFAULT_ENDPOINT );
|
||||
|
||||
$response = wp_remote_get(
|
||||
$endpoint,
|
||||
array(
|
||||
'timeout' => 12,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $license_key,
|
||||
'X-EMCP-License-Id' => $license_id,
|
||||
'X-EMCP-Site' => home_url(),
|
||||
'X-EMCP-Plugin-Version' => EMCP_TOOLS_VERSION,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( 403 === $code ) {
|
||||
return new WP_Error(
|
||||
'forbidden',
|
||||
__( 'Premium Templates are unavailable on this site. Make sure your EMCP Tools Pro license is active and this site is on its activated-sites list. Contact support if the issue persists.', 'emcp-tools' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 429 === $code ) {
|
||||
return new WP_Error(
|
||||
'rate_limited',
|
||||
__( 'Premium Templates endpoint is rate-limiting this site. Try again in a few minutes.', 'emcp-tools' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 200 !== $code ) {
|
||||
return new WP_Error(
|
||||
'remote_error',
|
||||
sprintf(
|
||||
/* translators: %d: HTTP status code */
|
||||
__( 'Templates endpoint returned HTTP %d. Please try again later or contact support.', 'emcp-tools' ),
|
||||
$code
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$bundle = json_decode( $body, true );
|
||||
|
||||
if ( ! is_array( $bundle ) || ! isset( $bundle['categories'] ) || ! is_array( $bundle['categories'] ) ) {
|
||||
return new WP_Error( 'invalid_payload', __( 'Templates endpoint returned an unexpected payload.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$bundle['fetched_at'] = time();
|
||||
set_transient( self::CACHE_KEY, $bundle, self::CACHE_TTL );
|
||||
|
||||
return $bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active license key from Freemius, or empty string.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_license_key(): string {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return '';
|
||||
}
|
||||
$license = emcp_tools_fs()->_get_license();
|
||||
if ( ! $license || empty( $license->secret_key ) ) {
|
||||
return '';
|
||||
}
|
||||
return (string) $license->secret_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active license's numeric Freemius ID, or empty string.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_license_id(): string {
|
||||
if ( ! function_exists( 'emcp_tools_fs' ) ) {
|
||||
return '';
|
||||
}
|
||||
$license = emcp_tools_fs()->_get_license();
|
||||
if ( ! $license || empty( $license->id ) ) {
|
||||
return '';
|
||||
}
|
||||
return (string) $license->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached bundle.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*/
|
||||
public static function flush_cache(): void {
|
||||
delete_transient( self::CACHE_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a template to a target page (existing or new).
|
||||
*
|
||||
* Creates a new Elementor-enabled page if $target_post_id is 0, otherwise
|
||||
* replaces the target page's Elementor data with the template's data.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @param string $category_slug
|
||||
* @param string $template_slug
|
||||
* @param int $target_post_id 0 to create a new page; otherwise an existing post ID.
|
||||
* @return array|WP_Error { post_id, edit_url, view_url, action: 'created'|'replaced' }
|
||||
*/
|
||||
public static function apply_template( string $category_slug, string $template_slug, int $target_post_id ) {
|
||||
$template = self::find_template( $category_slug, $template_slug );
|
||||
if ( ! $template || empty( $template['data'] ) || ! is_array( $template['data'] ) ) {
|
||||
return new WP_Error( 'template_not_found', __( 'Template not found in the cached bundle. Try clicking Sync Library first.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$elementor_data = $template['data'];
|
||||
|
||||
if ( $target_post_id > 0 ) {
|
||||
$post = get_post( $target_post_id );
|
||||
if ( ! $post ) {
|
||||
return new WP_Error( 'post_not_found', __( 'The target page no longer exists.', 'emcp-tools' ) );
|
||||
}
|
||||
if ( ! current_user_can( 'edit_post', $target_post_id ) ) {
|
||||
return new WP_Error( 'forbidden', __( 'You do not have permission to edit that page.', 'emcp-tools' ) );
|
||||
}
|
||||
$post_id = $target_post_id;
|
||||
$action = 'replaced';
|
||||
} else {
|
||||
if ( ! current_user_can( 'edit_pages' ) ) {
|
||||
return new WP_Error( 'forbidden', __( 'You do not have permission to create pages.', 'emcp-tools' ) );
|
||||
}
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_title' => isset( $template['title'] ) ? (string) $template['title'] : __( 'Untitled Template', 'emcp-tools' ),
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'draft',
|
||||
),
|
||||
true
|
||||
);
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
$action = 'created';
|
||||
}
|
||||
|
||||
// Persist the Elementor element tree. wp_slash() so backslashes inside
|
||||
// the template content (rare but possible) don't get stripped by
|
||||
// update_post_meta().
|
||||
update_post_meta( $post_id, '_elementor_data', wp_slash( wp_json_encode( $elementor_data ) ) );
|
||||
update_post_meta( $post_id, '_elementor_edit_mode', 'builder' );
|
||||
update_post_meta( $post_id, '_elementor_template_type', 'wp-page' );
|
||||
|
||||
// Page-level settings (background, padding, hide_title, etc.) when the
|
||||
// template ships them — common in Elementor's native exports.
|
||||
if ( ! empty( $template['page_settings'] ) && is_array( $template['page_settings'] ) ) {
|
||||
update_post_meta( $post_id, '_elementor_page_settings', $template['page_settings'] );
|
||||
}
|
||||
|
||||
// Trigger Elementor's CSS regeneration if available.
|
||||
if ( class_exists( '\Elementor\Plugin' ) && isset( \Elementor\Plugin::$instance->files_manager ) ) {
|
||||
\Elementor\Plugin::$instance->files_manager->clear_cache();
|
||||
}
|
||||
|
||||
$edit_url = admin_url( 'post.php?post=' . $post_id . '&action=elementor' );
|
||||
$view_url = get_permalink( $post_id );
|
||||
|
||||
return array(
|
||||
'post_id' => $post_id,
|
||||
'edit_url' => $edit_url,
|
||||
'view_url' => $view_url ? $view_url : '',
|
||||
'action' => $action,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a template into Elementor's Saved Templates library.
|
||||
*
|
||||
* Creates an `elementor_library` CPT post — the same post type Elementor
|
||||
* itself uses for templates saved via the editor's "Save as Template"
|
||||
* action. After import, the template is visible at
|
||||
* Elementor → Templates → Saved Templates and insertable from the
|
||||
* editor's "Add Template" picker on any page.
|
||||
*
|
||||
* @since 1.7.1
|
||||
*
|
||||
* @param string $category_slug
|
||||
* @param string $template_slug
|
||||
* @return array|WP_Error { template_id, edit_url, library_url }
|
||||
*/
|
||||
public static function import_to_library( string $category_slug, string $template_slug ) {
|
||||
$template = self::find_template( $category_slug, $template_slug );
|
||||
if ( ! $template || empty( $template['data'] ) || ! is_array( $template['data'] ) ) {
|
||||
return new WP_Error( 'template_not_found', __( 'Template not found in the cached bundle. Try clicking Sync Library first.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_posts' ) ) {
|
||||
return new WP_Error( 'forbidden', __( 'You do not have permission to import templates.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
if ( ! post_type_exists( 'elementor_library' ) ) {
|
||||
return new WP_Error( 'no_library', __( 'Elementor template library post type is not available. Make sure Elementor is active.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$title = isset( $template['title'] ) ? (string) $template['title'] : __( 'Untitled Template', 'emcp-tools' );
|
||||
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_title' => $title,
|
||||
'post_type' => 'elementor_library',
|
||||
'post_status' => 'publish',
|
||||
),
|
||||
true
|
||||
);
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// Tell Elementor this is a page-type template (full page layout).
|
||||
// Other valid types: 'section', 'widget'. Theme-builder types
|
||||
// require Elementor Pro.
|
||||
wp_set_object_terms( $post_id, 'page', 'elementor_library_type' );
|
||||
|
||||
update_post_meta( $post_id, '_elementor_data', wp_slash( wp_json_encode( $template['data'] ) ) );
|
||||
update_post_meta( $post_id, '_elementor_edit_mode', 'builder' );
|
||||
update_post_meta( $post_id, '_elementor_template_type', 'page' );
|
||||
|
||||
if ( ! empty( $template['page_settings'] ) && is_array( $template['page_settings'] ) ) {
|
||||
update_post_meta( $post_id, '_elementor_page_settings', $template['page_settings'] );
|
||||
}
|
||||
|
||||
// Trigger Elementor's CSS regeneration if available.
|
||||
if ( class_exists( '\Elementor\Plugin' ) && isset( \Elementor\Plugin::$instance->files_manager ) ) {
|
||||
\Elementor\Plugin::$instance->files_manager->clear_cache();
|
||||
}
|
||||
|
||||
return array(
|
||||
'template_id' => $post_id,
|
||||
'edit_url' => admin_url( 'post.php?post=' . $post_id . '&action=elementor' ),
|
||||
'library_url' => admin_url( 'edit.php?post_type=elementor_library' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Pro Usage tracker.
|
||||
*
|
||||
* Tracks local usage of templates and prompts to render usage badges and counts.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.7.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Pro_Usage {
|
||||
const OPTION_COUNTS = 'emcp_tools_usage_counts';
|
||||
|
||||
public static function get_counts(): array {
|
||||
return (array) get_option( self::OPTION_COUNTS, array() );
|
||||
}
|
||||
|
||||
public static function record_use( string $slug, string $type = 'template' ): void {
|
||||
$counts = self::get_counts();
|
||||
$key = $type . ':' . $slug;
|
||||
$counts[ $key ] = ( $counts[ $key ] ?? 0 ) + 1;
|
||||
update_option( self::OPTION_COUNTS, $counts );
|
||||
}
|
||||
|
||||
public static function local_summary(): array {
|
||||
$counts = self::get_counts();
|
||||
$templates = 0;
|
||||
$prompts = 0;
|
||||
foreach ( $counts as $k => $c ) {
|
||||
if ( 0 === strpos( $k, 'template:' ) ) {
|
||||
$templates += (int) $c;
|
||||
} elseif ( 0 === strpos( $k, 'prompt:' ) ) {
|
||||
$prompts += (int) $c;
|
||||
}
|
||||
}
|
||||
return array(
|
||||
'templates' => $templates,
|
||||
'prompts' => $prompts,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Settings admin view.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
if ( isset( $_POST['emcp_ai_save'] ) && check_admin_referer( 'emcp_ai_chat_settings' ) ) {
|
||||
if ( current_user_can( 'manage_options' ) ) {
|
||||
if ( isset( $_POST['provider'] ) ) {
|
||||
update_option( EMCP_Tools_AI_Chat_Settings::OPTION_PROVIDER, sanitize_key( $_POST['provider'] ) );
|
||||
}
|
||||
if ( isset( $_POST['model'] ) ) {
|
||||
update_option( EMCP_Tools_AI_Chat_Settings::OPTION_MODEL, sanitize_text_field( $_POST['model'] ) );
|
||||
}
|
||||
if ( ! empty( $_POST['api_key'] ) ) {
|
||||
EMCP_Tools_AI_Chat_Settings::set_api_key( trim( wp_unslash( $_POST['api_key'] ) ) );
|
||||
}
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'AI Chat settings updated.', 'emcp-tools' ) . '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
$active_provider = EMCP_Tools_AI_Chat_Settings::get_active_provider();
|
||||
$active_model = EMCP_Tools_AI_Chat_Settings::get_active_model();
|
||||
$providers = EMCP_Tools_AI_Providers::get_providers();
|
||||
$has_key = '' !== EMCP_Tools_AI_Chat_Settings::get_api_key();
|
||||
?>
|
||||
|
||||
<div class="wrap elementor-mcp-ai-chat">
|
||||
<h2>
|
||||
<?php esc_html_e( 'AI Chat Assistant Settings', 'emcp-tools' ); ?>
|
||||
<span class="elementor-mcp-badge elementor-mcp-badge--pro">PRO UNLOCKED</span>
|
||||
</h2>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Connect your preferred LLM provider to power the AI assistant inside the Elementor and Gutenberg page builders.', 'emcp-tools' ); ?>
|
||||
</p>
|
||||
|
||||
<form method="post" action="" style="max-width: 600px; margin-top: 20px;">
|
||||
<?php wp_nonce_field( 'emcp_ai_chat_settings' ); ?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><label for="provider"><?php esc_html_e( 'Provider', 'emcp-tools' ); ?></label></th>
|
||||
<td>
|
||||
<select name="provider" id="provider" class="regular-text">
|
||||
<?php foreach ( $providers as $pk => $p ) : ?>
|
||||
<option value="<?php echo esc_attr( $pk ); ?>" <?php selected( $active_provider, $pk ); ?>>
|
||||
<?php echo esc_html( $p['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="model"><?php esc_html_e( 'Model', 'emcp-tools' ); ?></label></th>
|
||||
<td>
|
||||
<input type="text" name="model" id="model" value="<?php echo esc_attr( $active_model ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'e.g. gpt-4o, claude-3-5-sonnet-20241022, or local ollama tag.', 'emcp-tools' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="api_key"><?php esc_html_e( 'API Key', 'emcp-tools' ); ?></label></th>
|
||||
<td>
|
||||
<input type="password" name="api_key" id="api_key" placeholder="<?php echo $has_key ? '••••••••••••••••' : ''; ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'Encrypted with site secret key. Leave blank to keep existing key. Local models do not require an API key.', 'emcp-tools' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p class="submit">
|
||||
<button type="submit" name="emcp_ai_save" class="button button-primary"><?php esc_html_e( 'Save Settings', 'emcp-tools' ); ?></button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Project Memory admin tab view.
|
||||
*
|
||||
* Displays pending proposals from AI agents for approval/rejection,
|
||||
* plus the approved guidelines active in site context.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle approve / reject actions.
|
||||
if ( isset( $_POST['emcp_memory_action'], $_POST['memory_id'] ) && check_admin_referer( 'emcp_memory_manage' ) ) {
|
||||
if ( current_user_can( 'manage_options' ) ) {
|
||||
$mem_id = (int) $_POST['memory_id'];
|
||||
$action = sanitize_key( $_POST['emcp_memory_action'] );
|
||||
if ( 'approve' === $action ) {
|
||||
EMCP_Tools_Memory_Store::approve( $mem_id );
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Memory proposal approved and active in site context.', 'emcp-tools' ) . '</p></div>';
|
||||
} elseif ( 'reject' === $action ) {
|
||||
EMCP_Tools_Memory_Store::reject( $mem_id );
|
||||
echo '<div class="notice notice-info is-dismissible"><p>' . esc_html__( 'Memory proposal discarded.', 'emcp-tools' ) . '</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pending_items = EMCP_Tools_Memory_Store::query(
|
||||
array(
|
||||
'post_status' => 'pending',
|
||||
'posts_per_page' => 50,
|
||||
)
|
||||
);
|
||||
|
||||
$approved_items = EMCP_Tools_Memory_Store::query(
|
||||
array(
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 50,
|
||||
)
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="wrap elementor-mcp-memory">
|
||||
<h2>
|
||||
<?php esc_html_e( 'Project Memory', 'emcp-tools' ); ?>
|
||||
<span class="elementor-mcp-badge elementor-mcp-badge--pro">PRO UNLOCKED</span>
|
||||
</h2>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'AI agents learn from conversations and suggest project-specific rules via the remember tool. Approved rules are automatically injected into future AI sessions.', 'emcp-tools' ); ?>
|
||||
</p>
|
||||
|
||||
<!-- Pending Proposals -->
|
||||
<div class="postbox" style="margin-top: 20px;">
|
||||
<div class="postbox-header">
|
||||
<h3 class="hndle"><?php esc_html_e( 'Pending Proposals Awaiting Approval', 'emcp-tools' ); ?> (<?php echo count( $pending_items ); ?>)</h3>
|
||||
</div>
|
||||
<div class="inside">
|
||||
<?php if ( empty( $pending_items ) ) : ?>
|
||||
<p><em><?php esc_html_e( 'No pending memory proposals from AI agents.', 'emcp-tools' ); ?></em></p>
|
||||
<?php else : ?>
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Rule / Summary', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Guideline Content', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Severity', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Actions', 'emcp-tools' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $pending_items as $item ) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html( $item->post_title ); ?></strong></td>
|
||||
<td><?php echo esc_html( $item->post_content ); ?></td>
|
||||
<td><code><?php echo esc_html( get_post_meta( $item->ID, EMCP_Tools_Memory_Store::META_SEVERITY, true ) ?: 'info' ); ?></code></td>
|
||||
<td>
|
||||
<form method="post" style="display:inline-block;">
|
||||
<?php wp_nonce_field( 'emcp_memory_manage' ); ?>
|
||||
<input type="hidden" name="memory_id" value="<?php echo esc_attr( $item->ID ); ?>">
|
||||
<button type="submit" name="emcp_memory_action" value="approve" class="button button-primary button-small"><?php esc_html_e( 'Approve', 'emcp-tools' ); ?></button>
|
||||
<button type="submit" name="emcp_memory_action" value="reject" class="button button-secondary button-small" onclick="return confirm('Discard this proposal?');"><?php esc_html_e( 'Reject', 'emcp-tools' ); ?></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approved Guidelines -->
|
||||
<div class="postbox" style="margin-top: 20px;">
|
||||
<div class="postbox-header">
|
||||
<h3 class="hndle"><?php esc_html_e( 'Active Site Guidelines (Injected into Agent Context)', 'emcp-tools' ); ?> (<?php echo count( $approved_items ); ?>)</h3>
|
||||
</div>
|
||||
<div class="inside">
|
||||
<?php if ( empty( $approved_items ) ) : ?>
|
||||
<p><em><?php esc_html_e( 'No guidelines currently active.', 'emcp-tools' ); ?></em></p>
|
||||
<?php else : ?>
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Rule / Title', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Guideline', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Severity', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Actions', 'emcp-tools' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $approved_items as $item ) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html( $item->post_title ); ?></strong></td>
|
||||
<td><?php echo esc_html( $item->post_content ); ?></td>
|
||||
<td><code><?php echo esc_html( get_post_meta( $item->ID, EMCP_Tools_Memory_Store::META_SEVERITY, true ) ?: 'info' ); ?></code></td>
|
||||
<td>
|
||||
<form method="post" style="display:inline-block;">
|
||||
<?php wp_nonce_field( 'emcp_memory_manage' ); ?>
|
||||
<input type="hidden" name="memory_id" value="<?php echo esc_attr( $item->ID ); ?>">
|
||||
<button type="submit" name="emcp_memory_action" value="reject" class="button button-small button-link-delete" onclick="return confirm('Delete this guideline?');"><?php esc_html_e( 'Delete', 'emcp-tools' ); ?></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Backup & Migrate admin view.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.12.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
if ( isset( $_POST['emcp_create_backup'] ) && check_admin_referer( 'emcp_migrate_action' ) ) {
|
||||
if ( current_user_can( 'manage_options' ) ) {
|
||||
$name = sanitize_file_name( (string) ( $_POST['backup_name'] ?? '' ) );
|
||||
$archive = EMCP_Tools_Packager::create_archive( $name );
|
||||
if ( $archive ) {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . sprintf( esc_html__( 'Backup archive "%s" created successfully.', 'emcp-tools' ), esc_html( basename( $archive ) ) ) . '</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$backups = EMCP_Tools_Packager::list_archives();
|
||||
?>
|
||||
|
||||
<div class="wrap elementor-mcp-migrate">
|
||||
<h2>
|
||||
<?php esc_html_e( 'Backup, Sync & Migrate', 'emcp-tools' ); ?>
|
||||
<span class="elementor-mcp-badge elementor-mcp-badge--pro">PRO UNLOCKED</span>
|
||||
</h2>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Create portable .emcp site archives, download backups, and synchronize changes across staging and live environments.', 'emcp-tools' ); ?>
|
||||
</p>
|
||||
|
||||
<div class="postbox" style="margin-top: 20px; max-width: 700px;">
|
||||
<div class="postbox-header">
|
||||
<h3 class="hndle"><?php esc_html_e( 'Create New Backup', 'emcp-tools' ); ?></h3>
|
||||
</div>
|
||||
<div class="inside">
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'emcp_migrate_action' ); ?>
|
||||
<p>
|
||||
<label for="backup_name"><?php esc_html_e( 'Backup Name (optional):', 'emcp-tools' ); ?></label><br>
|
||||
<input type="text" name="backup_name" id="backup_name" class="regular-text" placeholder="e.g. pre-redesign-backup">
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" name="emcp_create_backup" class="button button-primary"><?php esc_html_e( 'Create .emcp Archive', 'emcp-tools' ); ?></button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="postbox" style="margin-top: 20px;">
|
||||
<div class="postbox-header">
|
||||
<h3 class="hndle"><?php esc_html_e( 'Existing Backups', 'emcp-tools' ); ?> (<?php echo count( $backups ); ?>)</h3>
|
||||
</div>
|
||||
<div class="inside">
|
||||
<?php if ( empty( $backups ) ) : ?>
|
||||
<p><em><?php esc_html_e( 'No backups created yet.', 'emcp-tools' ); ?></em></p>
|
||||
<?php else : ?>
|
||||
<table class="widefat striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Filename', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Size', 'emcp-tools' ); ?></th>
|
||||
<th><?php esc_html_e( 'Date Created', 'emcp-tools' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $backups as $b ) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html( $b['filename'] ); ?></strong></td>
|
||||
<td><?php echo esc_html( $b['size'] ); ?></td>
|
||||
<td><?php echo esc_html( $b['date'] ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
/**
|
||||
* Skills tab view.
|
||||
*
|
||||
* Pro users (with skills folder bundled): download button + per-client setup guide.
|
||||
* Pro users on a build without the skills folder: graceful "not bundled" notice.
|
||||
* Free users: upgrade CTA.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.7.1
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$emcp_tools_has_pro = class_exists( 'EMCP_Tools_Pro_Skills' ) && EMCP_Tools_Pro_Skills::user_has_access();
|
||||
$emcp_tools_pro_only = function_exists( 'emcp_tools_fs' ) && emcp_tools_fs()->can_use_premium_code();
|
||||
$emcp_tools_skills_dir_missing = $emcp_tools_pro_only && ! EMCP_Tools_Pro_Skills::skills_dir_exists();
|
||||
$emcp_tools_download_url = $emcp_tools_has_pro ? EMCP_Tools_Pro_Skills::download_url() : '';
|
||||
|
||||
$emcp_tools_upgrade_url = function_exists( 'emcp_tools_upgrade_url' )
|
||||
? emcp_tools_upgrade_url()
|
||||
: 'https://emcptools.com/pricing';
|
||||
|
||||
/*
|
||||
* Bundled industry vertical packs. Read the labels straight from the shipped
|
||||
* verticals/ folder so this list never drifts from what's actually in the zip.
|
||||
* Only present on premium builds (the folder ships only there).
|
||||
*/
|
||||
$emcp_tools_verticals = array();
|
||||
if ( $emcp_tools_has_pro && defined( 'EMCP_TOOLS_DIR' ) ) {
|
||||
$emcp_tools_verticals_dir = EMCP_TOOLS_DIR . 'skills/emcp-skills/verticals';
|
||||
if ( is_dir( $emcp_tools_verticals_dir ) ) {
|
||||
foreach ( (array) glob( $emcp_tools_verticals_dir . '/*.md' ) as $emcp_tools_vfile ) {
|
||||
if ( 'readme.md' === strtolower( basename( $emcp_tools_vfile ) ) ) {
|
||||
continue;
|
||||
}
|
||||
$emcp_tools_label = '';
|
||||
$emcp_tools_lines = (array) @file( $emcp_tools_vfile, FILE_IGNORE_NEW_LINES ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
|
||||
foreach ( array_slice( $emcp_tools_lines, 0, 20 ) as $emcp_tools_line ) {
|
||||
if ( preg_match( '/^label:\s*(.+?)\s*$/', $emcp_tools_line, $emcp_tools_m ) ) {
|
||||
$emcp_tools_label = $emcp_tools_m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( '' === $emcp_tools_label ) {
|
||||
$emcp_tools_label = ucwords( str_replace( '-', ' ', basename( $emcp_tools_vfile, '.md' ) ) );
|
||||
}
|
||||
$emcp_tools_verticals[] = $emcp_tools_label;
|
||||
}
|
||||
sort( $emcp_tools_verticals );
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="elementor-mcp-skills">
|
||||
|
||||
<div class="elementor-mcp-pro-prompts-header">
|
||||
<div class="elementor-mcp-pro-prompts-heading">
|
||||
<h2>
|
||||
<?php esc_html_e( 'EMCP Skill for AI Agents', 'emcp-tools' ); ?>
|
||||
<span class="elementor-mcp-badge elementor-mcp-badge--pro">PRO</span>
|
||||
</h2>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Pre-written Agent Skills that teach Claude (and any compatible AI client) exactly how to use the EMCP Tools MCP. The bundle includes three skills: Elementor page building (with industry skill packs), a Performance Analyzer skill, and a Security & Malware Scanner skill (which pairs the security scanner with the filesystem tools to confirm and clean findings). Install once per machine — every future session that loads them knows your workflow.', 'emcp-tools' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php if ( $emcp_tools_has_pro ) : ?>
|
||||
<a class="elementor-mcp-skills-download" href="<?php echo esc_url( $emcp_tools_download_url ); ?>">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M10 3a1 1 0 011 1v7.59l2.3-2.3a1 1 0 111.4 1.42l-4 4a1 1 0 01-1.4 0l-4-4a1 1 0 111.4-1.42L9 11.6V4a1 1 0 011-1zM4 15a1 1 0 011 1v1h10v-1a1 1 0 112 0v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2a1 1 0 011-1z"/>
|
||||
</svg>
|
||||
<?php esc_html_e( 'Download emcp-skills.zip', 'emcp-tools' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( $emcp_tools_has_pro ) : ?>
|
||||
|
||||
<div class="elementor-mcp-coming-soon" role="status">
|
||||
<span class="elementor-mcp-coming-soon__icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M10 18a8 8 0 100-16 8 8 0 000 16zm.75-12.75a.75.75 0 00-1.5 0v5.5a.75.75 0 00.34.628l3.75 2.5a.75.75 0 10.82-1.255L10.75 10.4V5.25z"/></svg>
|
||||
</span>
|
||||
<div class="elementor-mcp-coming-soon__text">
|
||||
<strong><?php esc_html_e( 'Quick install:', 'emcp-tools' ); ?></strong>
|
||||
<?php esc_html_e( 'Click the download button above, then follow the guide for your AI client below. The zip contains three skill folders (emcp-skills, emcp-performance, emcp-security) — copy each into your client\'s skills/rules directory the same way; paths listed per platform.', 'emcp-tools' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ( ! empty( $emcp_tools_verticals ) ) : ?>
|
||||
<div class="elementor-mcp-skills-packs">
|
||||
<h3 class="elementor-mcp-skills-packs__title">
|
||||
<?php
|
||||
/* translators: %d: number of bundled industry packs. */
|
||||
echo esc_html( sprintf( _n( 'Includes %d industry skill pack', 'Includes %d industry skill packs', count( $emcp_tools_verticals ), 'emcp-tools' ), count( $emcp_tools_verticals ) ) );
|
||||
?>
|
||||
<span class="elementor-mcp-badge elementor-mcp-badge--pro">NEW</span>
|
||||
</h3>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'When your AI agent recognizes the site\'s industry, it reads the matching pack before building — applying that trade\'s brand voice, SEO keywords, page structure, conversion patterns, and compliance notes, plus the exact Brand Kit + prompt + template combo to use. Nothing to configure: just tell your client what kind of site you\'re building (e.g. "build a dental clinic site") and the skill routes itself.', 'emcp-tools' ); ?>
|
||||
</p>
|
||||
<ul class="elementor-mcp-skills-packs__grid">
|
||||
<?php foreach ( $emcp_tools_verticals as $emcp_tools_v ) : ?>
|
||||
<li class="elementor-mcp-skills-pack"><?php echo esc_html( $emcp_tools_v ); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="elementor-mcp-skills-guides">
|
||||
|
||||
<!-- Claude Code -->
|
||||
<details class="elementor-mcp-skills-guide" open>
|
||||
<summary>
|
||||
<strong><?php esc_html_e( 'Claude Code (terminal)', 'emcp-tools' ); ?></strong>
|
||||
<span class="elementor-mcp-skills-guide__hint"><?php esc_html_e( 'Native skills support', 'emcp-tools' ); ?></span>
|
||||
</summary>
|
||||
<div class="elementor-mcp-skills-guide__body">
|
||||
<p><?php esc_html_e( 'Claude Code reads skills from two locations — project-scoped or user-global. Use whichever fits your workflow:', 'emcp-tools' ); ?></p>
|
||||
<ol>
|
||||
<li>
|
||||
<?php esc_html_e( 'Extract emcp-skills.zip — you\'ll get three folders: "emcp-skills/", "emcp-performance/", and "emcp-security/".', 'emcp-tools' ); ?>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Move each folder into one of these locations (same parent dir for all three):', 'emcp-tools' ); ?>
|
||||
<ul>
|
||||
<li><strong><?php esc_html_e( 'Per-project (recommended):', 'emcp-tools' ); ?></strong> <code><project-root>/.claude/skills/emcp-skills/</code></li>
|
||||
<li><strong><?php esc_html_e( 'Global (all projects):', 'emcp-tools' ); ?></strong>
|
||||
<ul>
|
||||
<li>macOS / Linux: <code>~/.claude/skills/emcp-skills/</code></li>
|
||||
<li>Windows: <code>%USERPROFILE%\.claude\skills\emcp-skills\</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><?php esc_html_e( 'Restart Claude Code. The skill activates automatically when you start any session that has the EMCP Tools MCP server connected.', 'emcp-tools' ); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Claude Desktop -->
|
||||
<details class="elementor-mcp-skills-guide">
|
||||
<summary>
|
||||
<strong><?php esc_html_e( 'Claude Desktop', 'emcp-tools' ); ?></strong>
|
||||
<span class="elementor-mcp-skills-guide__hint"><?php esc_html_e( 'Skills folder support', 'emcp-tools' ); ?></span>
|
||||
</summary>
|
||||
<div class="elementor-mcp-skills-guide__body">
|
||||
<p><?php esc_html_e( 'Drop the folder into Claude Desktop\'s skills directory:', 'emcp-tools' ); ?></p>
|
||||
<ul>
|
||||
<li><strong>macOS:</strong> <code>~/Library/Application Support/Claude/Skills/emcp-skills/</code></li>
|
||||
<li><strong>Windows:</strong> <code>%APPDATA%\Claude\Skills\emcp-skills\</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.config/Claude/Skills/emcp-skills/</code></li>
|
||||
</ul>
|
||||
<p><?php esc_html_e( 'Restart Claude Desktop. New conversations connected to the EMCP MCP server will pick up the skill.', 'emcp-tools' ); ?></p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Cursor -->
|
||||
<details class="elementor-mcp-skills-guide">
|
||||
<summary>
|
||||
<strong><?php esc_html_e( 'Cursor', 'emcp-tools' ); ?></strong>
|
||||
<span class="elementor-mcp-skills-guide__hint"><?php esc_html_e( 'Adapt as project rules', 'emcp-tools' ); ?></span>
|
||||
</summary>
|
||||
<div class="elementor-mcp-skills-guide__body">
|
||||
<p><?php esc_html_e( 'Cursor uses Project Rules rather than skills folders. Wire the EMCP skill in via:', 'emcp-tools' ); ?></p>
|
||||
<ol>
|
||||
<li><?php esc_html_e( 'Extract emcp-skills.zip on your machine.', 'emcp-tools' ); ?></li>
|
||||
<li><?php esc_html_e( 'Copy each skill folder (emcp-skills, emcp-performance, emcp-security) to your project under:', 'emcp-tools' ); ?> <code><project-root>/.cursor/rules/</code></li>
|
||||
<li><?php esc_html_e( 'Open Cursor → Settings → Rules → Project Rules. Add a new rule referencing the SKILL.md file. Set the rule type to "Agent Requested" so it loads when your agent uses MCP tools.', 'emcp-tools' ); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Windsurf -->
|
||||
<details class="elementor-mcp-skills-guide">
|
||||
<summary>
|
||||
<strong><?php esc_html_e( 'Windsurf', 'emcp-tools' ); ?></strong>
|
||||
<span class="elementor-mcp-skills-guide__hint"><?php esc_html_e( 'Cascade rules', 'emcp-tools' ); ?></span>
|
||||
</summary>
|
||||
<div class="elementor-mcp-skills-guide__body">
|
||||
<p><?php esc_html_e( 'Windsurf\'s Cascade supports custom rules per workspace:', 'emcp-tools' ); ?></p>
|
||||
<ol>
|
||||
<li><?php esc_html_e( 'Extract emcp-skills.zip.', 'emcp-tools' ); ?></li>
|
||||
<li><?php esc_html_e( 'Copy each skill folder (emcp-skills, emcp-performance, emcp-security) to your workspace under:', 'emcp-tools' ); ?> <code><workspace>/.windsurf/rules/</code></li>
|
||||
<li><?php esc_html_e( 'In Windsurf, open Cascade → Customize → Rules. Enable the new rule and set it to load when MCP tools matching elementor-mcp-* are present.', 'emcp-tools' ); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Antigravity -->
|
||||
<details class="elementor-mcp-skills-guide">
|
||||
<summary>
|
||||
<strong><?php esc_html_e( 'Antigravity', 'emcp-tools' ); ?></strong>
|
||||
<span class="elementor-mcp-skills-guide__hint"><?php esc_html_e( 'Knowledge Manager', 'emcp-tools' ); ?></span>
|
||||
</summary>
|
||||
<div class="elementor-mcp-skills-guide__body">
|
||||
<p><?php esc_html_e( 'Antigravity surfaces custom knowledge through its Knowledge Manager:', 'emcp-tools' ); ?></p>
|
||||
<ol>
|
||||
<li><?php esc_html_e( 'Extract emcp-skills.zip and locate the SKILL.md file inside.', 'emcp-tools' ); ?></li>
|
||||
<li><?php esc_html_e( 'In Antigravity, open Knowledge Manager and create a knowledge source for each extracted skill folder (emcp-skills, emcp-performance, emcp-security), or paste each SKILL.md\'s contents directly.', 'emcp-tools' ); ?></li>
|
||||
<li><?php esc_html_e( 'Tag the knowledge with "emcp-tools" so it auto-loads on sessions where the EMCP MCP server is connected.', 'emcp-tools' ); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Any other client -->
|
||||
<details class="elementor-mcp-skills-guide">
|
||||
<summary>
|
||||
<strong><?php esc_html_e( 'Any other MCP client', 'emcp-tools' ); ?></strong>
|
||||
<span class="elementor-mcp-skills-guide__hint"><?php esc_html_e( 'Universal fallback', 'emcp-tools' ); ?></span>
|
||||
</summary>
|
||||
<div class="elementor-mcp-skills-guide__body">
|
||||
<p><?php esc_html_e( 'If your client doesn\'t support skills natively:', 'emcp-tools' ); ?></p>
|
||||
<ol>
|
||||
<li><?php esc_html_e( 'Extract emcp-skills.zip.', 'emcp-tools' ); ?></li>
|
||||
<li><?php esc_html_e( 'Open SKILL.md in any text editor.', 'emcp-tools' ); ?></li>
|
||||
<li><?php esc_html_e( 'Copy the entire contents and paste it into your client\'s "system prompt", "custom instructions", or equivalent persistent-context input. The skill is plain markdown — any LLM can read it directly.', 'emcp-tools' ); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
||||
<?php elseif ( $emcp_tools_skills_dir_missing ) : ?>
|
||||
|
||||
<div class="notice notice-warning inline">
|
||||
<p><?php esc_html_e( 'Skills are not bundled in this build. If you\'re running the premium plugin and seeing this message, try re-downloading the latest premium zip from your Freemius account.', 'emcp-tools' ); ?></p>
|
||||
</div>
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<div class="elementor-mcp-prompts-cta">
|
||||
<div class="elementor-mcp-prompts-cta-content">
|
||||
<h3><?php esc_html_e( 'Unlock the EMCP Agent Skill', 'emcp-tools' ); ?></h3>
|
||||
<p><?php esc_html_e( 'Pre-written Agent Skill that teaches Claude and other compatible AI clients exactly how to build Elementor pages through the MCP tools — design rules, kit-first workflow, responsive controls, the full review loop. One install per machine.', 'emcp-tools' ); ?></p>
|
||||
<a href="<?php echo esc_url( $emcp_tools_upgrade_url ); ?>" class="button button-primary elementor-mcp-prompts-cta-btn" target="_blank" rel="noopener noreferrer">
|
||||
<svg viewBox="0 0 20 20" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
|
||||
<?php esc_html_e( 'Upgrade to Pro', 'emcp-tools' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat REST API Controller.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Controller {
|
||||
|
||||
public static function init(): void {
|
||||
add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
|
||||
}
|
||||
|
||||
public static function register_routes(): void {
|
||||
register_rest_route(
|
||||
'emcp/v1',
|
||||
'/chat',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( __CLASS__, 'handle_chat' ),
|
||||
'permission_callback' => static function (): bool { return current_user_can( 'edit_posts' ); },
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function handle_chat( WP_REST_Request $request ) {
|
||||
$messages = (array) $request->get_param( 'messages' );
|
||||
if ( empty( $messages ) ) {
|
||||
return new WP_Error( 'missing_messages', __( 'Messages array required.', 'emcp-tools' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$provider_key = EMCP_Tools_AI_Chat_Settings::get_active_provider();
|
||||
$api_key = EMCP_Tools_AI_Chat_Settings::get_api_key();
|
||||
$model = EMCP_Tools_AI_Chat_Settings::get_active_model();
|
||||
|
||||
$providers = EMCP_Tools_AI_Providers::get_providers();
|
||||
if ( ! isset( $providers[ $provider_key ] ) ) {
|
||||
return new WP_Error( 'invalid_provider', __( 'Provider not configured.', 'emcp-tools' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$endpoint = $providers[ $provider_key ]['endpoint'];
|
||||
$payload = array(
|
||||
'model' => $model,
|
||||
'messages' => array_merge(
|
||||
array( array( 'role' => 'system', 'content' => EMCP_Tools_AI_Chat_Prompt::build() ) ),
|
||||
$messages
|
||||
),
|
||||
);
|
||||
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/json',
|
||||
);
|
||||
if ( 'anthropic' === $provider_key ) {
|
||||
$headers['x-api-key'] = $api_key;
|
||||
$headers['anthropic-version'] = '2023-06-01';
|
||||
} else {
|
||||
$headers['Authorization'] = 'Bearer ' . $api_key;
|
||||
}
|
||||
|
||||
$response = wp_remote_post(
|
||||
$endpoint,
|
||||
array(
|
||||
'timeout' => 45,
|
||||
'headers' => $headers,
|
||||
'body' => wp_json_encode( $payload ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
return rest_ensure_response( $body );
|
||||
}
|
||||
}
|
||||
|
||||
EMCP_Tools_AI_Chat_Controller::init();
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Image Reader for vision models.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Image_Reader {
|
||||
public static function to_base64( int $attachment_id ): string {
|
||||
$path = get_attached_file( $attachment_id );
|
||||
if ( ! $path || ! file_exists( $path ) ) { return ''; }
|
||||
$data = file_get_contents( $path );
|
||||
$mime = wp_check_filetype( $path )['type'] ?: 'image/jpeg';
|
||||
return 'data:' . $mime . ';base64,' . base64_encode( $data );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Admin Page.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Page {
|
||||
public static function render(): void {
|
||||
require_once EMCP_TOOLS_DIR . 'includes/admin/views/page-ai-chat.php';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat System Prompt builder.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Prompt {
|
||||
public static function build(): string {
|
||||
$context = class_exists( 'EMCP_Tools_Site_Context' ) ? EMCP_Tools_Site_Context::environment_summary() : '';
|
||||
return "You are the AI Assistant inside the WordPress / Elementor editor powered by EMCP Tools Pro Unlocked.\n" .
|
||||
"You help the user build pages, optimize SEO and accessibility, manage themes, and author custom blocks.\n\n" .
|
||||
$context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Provider Interface.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
interface EMCP_Tools_AI_Chat_Provider {
|
||||
public function send_message( array $messages, array $tools = array(), array $options = array() );
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Settings.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Settings {
|
||||
const OPTION_PROVIDER = 'emcp_ai_chat_provider';
|
||||
const OPTION_MODEL = 'emcp_ai_chat_model';
|
||||
const OPTION_KEY = 'emcp_ai_chat_key_enc';
|
||||
|
||||
public static function get_active_provider(): string {
|
||||
return (string) get_option( self::OPTION_PROVIDER, 'openai' );
|
||||
}
|
||||
|
||||
public static function get_active_model(): string {
|
||||
return (string) get_option( self::OPTION_MODEL, 'gpt-4o' );
|
||||
}
|
||||
|
||||
public static function get_api_key(): string {
|
||||
$enc = (string) get_option( self::OPTION_KEY, '' );
|
||||
return EMCP_Tools_Key_Crypto::decrypt( $enc );
|
||||
}
|
||||
|
||||
public static function set_api_key( string $key ): void {
|
||||
update_option( self::OPTION_KEY, EMCP_Tools_Key_Crypto::encrypt( $key ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Session Store.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Store {
|
||||
const OPTION_PREFIX = 'emcp_ai_session_';
|
||||
|
||||
public static function get_session( string $session_id ): array {
|
||||
return (array) get_transient( self::OPTION_PREFIX . sanitize_key( $session_id ) ) ?: array();
|
||||
}
|
||||
|
||||
public static function save_session( string $session_id, array $messages ): void {
|
||||
set_transient( self::OPTION_PREFIX . sanitize_key( $session_id ), $messages, DAY_IN_SECONDS * 7 );
|
||||
}
|
||||
|
||||
public static function clear_session( string $session_id ): void {
|
||||
delete_transient( self::OPTION_PREFIX . sanitize_key( $session_id ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Tool Groups.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Tool_Groups {
|
||||
public static function get_groups(): array {
|
||||
return array(
|
||||
'elementor' => array( 'label' => 'Elementor Tools', 'default' => true ),
|
||||
'content' => array( 'label' => 'Content & Posts', 'default' => true ),
|
||||
'themer' => array( 'label' => 'Theme Builder', 'default' => true ),
|
||||
'seo_a11y' => array( 'label' => 'SEO & A11y', 'default' => true ),
|
||||
'sandbox' => array( 'label' => 'Custom Widgets & Blocks', 'default' => false ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Usage Tracker.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.5.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Usage {
|
||||
public static function track( string $model, int $tokens ): void {
|
||||
$usage = (int) get_option( 'emcp_ai_chat_tokens_used', 0 );
|
||||
update_option( 'emcp_ai_chat_tokens_used', $usage + $tokens );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Web Fetch helper.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Web_Fetch {
|
||||
public static function fetch( string $url ) {
|
||||
$resp = wp_remote_get( esc_url_raw( $url ), array( 'timeout' => 10 ) );
|
||||
if ( is_wp_error( $resp ) ) { return $resp; }
|
||||
return wp_remote_retrieve_body( $resp );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Providers Catalog.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Providers {
|
||||
|
||||
public static function get_providers(): array {
|
||||
return array(
|
||||
'openai' => array(
|
||||
'name' => 'OpenAI',
|
||||
'endpoint' => 'https://api.openai.com/v1/chat/completions',
|
||||
'models' => array( 'gpt-4o', 'gpt-4o-mini', 'o3-mini' ),
|
||||
),
|
||||
'anthropic' => array(
|
||||
'name' => 'Anthropic',
|
||||
'endpoint' => 'https://api.anthropic.com/v1/messages',
|
||||
'models' => array( 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022' ),
|
||||
),
|
||||
'openrouter' => array(
|
||||
'name' => 'OpenRouter',
|
||||
'endpoint' => 'https://openrouter.ai/api/v1/chat/completions',
|
||||
'models' => array( 'anthropic/claude-3.5-sonnet', 'openai/gpt-4o', 'deepseek/deepseek-r1' ),
|
||||
),
|
||||
'groq' => array(
|
||||
'name' => 'Groq',
|
||||
'endpoint' => 'https://api.groq.com/openai/v1/chat/completions',
|
||||
'models' => array( 'llama-3.3-70b-versatile', 'mixtral-8x7b-32768' ),
|
||||
),
|
||||
'ollama' => array(
|
||||
'name' => 'Ollama (Local)',
|
||||
'endpoint' => 'http://localhost:11434/v1/chat/completions',
|
||||
'models' => array( 'llama3.1', 'mistral', 'qwen2.5-coder' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Elementor Editor integration.
|
||||
*
|
||||
* Enqueues the floating AI Chat widget inside the Elementor builder preview when enabled.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Elementor_Editor {
|
||||
|
||||
public static function init(): void {
|
||||
add_action( 'elementor/editor/after_enqueue_scripts', array( __CLASS__, 'enqueue' ) );
|
||||
}
|
||||
|
||||
public static function enqueue(): void {
|
||||
if ( ! class_exists( 'EMCP_Tools_AI_Chat_Module' ) || ! EMCP_Tools_AI_Chat_Module::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'emcp-ai-chat-css', plugins_url( 'assets/css/ai-chat.css', EMCP_TOOLS_BASENAME ), array(), EMCP_TOOLS_VERSION );
|
||||
wp_enqueue_script( 'emcp-ai-chat-js', plugins_url( 'assets/js/ai-chat.js', EMCP_TOOLS_BASENAME ), array( 'jquery' ), EMCP_TOOLS_VERSION, true );
|
||||
wp_localize_script(
|
||||
'emcp-ai-chat-js',
|
||||
'emcpAiChat',
|
||||
array(
|
||||
'endpoint' => rest_url( 'emcp/v1/chat' ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'context' => 'elementor',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EMCP_Tools_Elementor_Editor::init();
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Gutenberg Editor integration.
|
||||
*
|
||||
* Enqueues the AI Chat floating button in the block editor when enabled.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Gutenberg_Editor {
|
||||
|
||||
public static function init(): void {
|
||||
add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue' ) );
|
||||
}
|
||||
|
||||
public static function enqueue(): void {
|
||||
if ( ! class_exists( 'EMCP_Tools_AI_Chat_Module' ) || ! EMCP_Tools_AI_Chat_Module::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'emcp-ai-chat-css', plugins_url( 'assets/css/ai-chat.css', EMCP_TOOLS_BASENAME ), array(), EMCP_TOOLS_VERSION );
|
||||
wp_enqueue_script( 'emcp-ai-chat-js', plugins_url( 'assets/js/ai-chat.js', EMCP_TOOLS_BASENAME ), array( 'jquery' ), EMCP_TOOLS_VERSION, true );
|
||||
wp_localize_script(
|
||||
'emcp-ai-chat-js',
|
||||
'emcpAiChat',
|
||||
array(
|
||||
'endpoint' => rest_url( 'emcp/v1/chat' ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'context' => 'gutenberg',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EMCP_Tools_Gutenberg_Editor::init();
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Key Crypto — AES-256-CBC encryption for third-party AI provider API keys.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Key_Crypto {
|
||||
|
||||
private static function secret_key(): string {
|
||||
$salt = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'emcp-fallback-salt';
|
||||
$salt .= defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '';
|
||||
return hash( 'sha256', $salt, true );
|
||||
}
|
||||
|
||||
public static function encrypt( string $plain ): string {
|
||||
if ( '' === $plain ) { return ''; }
|
||||
$iv = openssl_random_pseudo_bytes( 16 );
|
||||
$cipher = openssl_encrypt( $plain, 'AES-256-CBC', self::secret_key(), 0, $iv );
|
||||
return base64_encode( $iv . $cipher );
|
||||
}
|
||||
|
||||
public static function decrypt( string $encrypted ): string {
|
||||
if ( '' === $encrypted ) { return ''; }
|
||||
$data = base64_decode( $encrypted );
|
||||
if ( strlen( $data ) < 17 ) { return ''; }
|
||||
$iv = substr( $data, 0, 16 );
|
||||
$cipher = substr( $data, 16 );
|
||||
$plain = openssl_decrypt( $cipher, 'AES-256-CBC', self::secret_key(), 0, $iv );
|
||||
return false !== $plain ? $plain : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Blocksy blocks catalog metadata.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Blocksy_Blocks_Catalog {
|
||||
public static function get_blocks(): array {
|
||||
return array(
|
||||
'blocksy/dynamic-data' => array( 'title' => 'Dynamic Data', 'category' => 'blocksy' ),
|
||||
'blocksy/search' => array( 'title' => 'Search', 'category' => 'blocksy' ),
|
||||
'blocksy/socials' => array( 'title' => 'Socials', 'category' => 'blocksy' ),
|
||||
'blocksy/breadcrumbs' => array( 'title' => 'Breadcrumbs', 'category' => 'blocksy' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* GenerateBlocks catalog metadata.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.4.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_GenerateBlocks_Catalog {
|
||||
public static function get_blocks(): array {
|
||||
return array(
|
||||
'generateblocks/container' => array( 'title' => 'Container', 'category' => 'generateblocks' ),
|
||||
'generateblocks/grid' => array( 'title' => 'Grid', 'category' => 'generateblocks' ),
|
||||
'generateblocks/headline' => array( 'title' => 'Headline', 'category' => 'generateblocks' ),
|
||||
'generateblocks/button' => array( 'title' => 'Button', 'category' => 'generateblocks' ),
|
||||
'generateblocks/image' => array( 'title' => 'Image', 'category' => 'generateblocks' ),
|
||||
'generateblocks/query-loop'=> array( 'title' => 'Query Loop', 'category' => 'generateblocks' ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* Block Generator — compiles structured block JSON specs into valid WordPress block packages.
|
||||
*
|
||||
* Emits block.json (metadata, attributes, supports) and render.php (SSR output buffering).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.6.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Block_Generator {
|
||||
|
||||
/**
|
||||
* Supported attribute types.
|
||||
*/
|
||||
const TYPES = array( 'string', 'boolean', 'number', 'integer', 'array', 'object' );
|
||||
|
||||
/**
|
||||
* Validate a block spec.
|
||||
*
|
||||
* @param array $spec Block specification.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function validate( array $spec ) {
|
||||
if ( empty( $spec['name'] ) || ! is_string( $spec['name'] ) ) {
|
||||
return new WP_Error( 'invalid_spec', __( 'Block spec must have a string "name" (e.g. "my-card").', 'emcp-tools' ) );
|
||||
}
|
||||
if ( empty( $spec['title'] ) || ! is_string( $spec['title'] ) ) {
|
||||
return new WP_Error( 'invalid_spec', __( 'Block spec must have a string "title".', 'emcp-tools' ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate files for the block package.
|
||||
*
|
||||
* @param array $spec Block spec.
|
||||
* @param string $block_slug Full block slug (e.g. "emcp-sandbox/my-card").
|
||||
* @return array{block_json:string,render_php:string,editor_js:string}
|
||||
*/
|
||||
public static function generate( array $spec, string $block_slug ): array {
|
||||
$attrs = (array) ( $spec['attributes'] ?? array() );
|
||||
$normalized_attrs = array();
|
||||
|
||||
foreach ( $attrs as $k => $attr ) {
|
||||
$type = is_array( $attr ) && isset( $attr['type'] ) && in_array( $attr['type'], self::TYPES, true ) ? $attr['type'] : 'string';
|
||||
$def = is_array( $attr ) && isset( $attr['default'] ) ? $attr['default'] : '';
|
||||
$normalized_attrs[ $k ] = array(
|
||||
'type' => $type,
|
||||
'default' => $def,
|
||||
);
|
||||
}
|
||||
|
||||
$block_meta = array(
|
||||
'$schema' => 'https://schemas.wp.org/trunk/block.json',
|
||||
'apiVersion' => 3,
|
||||
'name' => $block_slug,
|
||||
'version' => '1.0.0',
|
||||
'title' => $spec['title'],
|
||||
'category' => $spec['category'] ?? 'widgets',
|
||||
'icon' => $spec['icon'] ?? 'block-default',
|
||||
'description' => $spec['description'] ?? '',
|
||||
'attributes' => $normalized_attrs,
|
||||
'supports' => array(
|
||||
'html' => false,
|
||||
'align' => true,
|
||||
'color' => true,
|
||||
'spacing' => true,
|
||||
),
|
||||
'render' => 'file:./render.php',
|
||||
);
|
||||
|
||||
$template = (string) ( $spec['template'] ?? '<div class="emcp-custom-block"><h3>{{title}}</h3><p>{{content}}</p></div>' );
|
||||
$render = EMCP_Tools_Sandbox_Template_Compiler::build_php_render( $template, $normalized_attrs );
|
||||
|
||||
return array(
|
||||
'block_json' => wp_json_encode( $block_meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ),
|
||||
'render_php' => $render,
|
||||
'editor_js' => '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Block Loader — automatically registers active sandbox Gutenberg blocks with WordPress.
|
||||
*
|
||||
* Reads blocks-manifest.json and registers them via register_block_type().
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.6.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Block_Loader {
|
||||
|
||||
public static function init(): void {
|
||||
add_action( 'init', array( __CLASS__, 'load_blocks' ), 20 );
|
||||
}
|
||||
|
||||
public static function load_blocks(): void {
|
||||
$manifest_file = EMCP_Tools_Sandbox_Paths::base_dir() . '/blocks-manifest.json';
|
||||
if ( ! file_exists( $manifest_file ) || ! is_readable( $manifest_file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = json_decode( (string) file_get_contents( $manifest_file ), true );
|
||||
if ( ! is_array( $data ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $data as $entry ) {
|
||||
$dir = (string) ( $entry['dir'] ?? '' );
|
||||
if ( '' !== $dir && file_exists( $dir . '/block.json' ) ) {
|
||||
register_block_type( $dir );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EMCP_Tools_Block_Loader::init();
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* Block Store — source of truth and sandbox manager for custom Gutenberg blocks.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.6.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Block_Store extends EMCP_Tools_Sandbox_Store {
|
||||
|
||||
const POST_TYPE = 'emcp_block';
|
||||
const META_SPEC = '_emcp_block_spec';
|
||||
const META_BLOCK_NAME = '_emcp_block_name';
|
||||
const META_CLASS_NAME = '_emcp_class_name';
|
||||
const META_LAST_ERROR = '_emcp_last_error';
|
||||
|
||||
/** @var self|null */
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance(): self {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function kind(): string {
|
||||
return 'block';
|
||||
}
|
||||
|
||||
protected function sandbox_subdir(): string {
|
||||
return 'blocks';
|
||||
}
|
||||
|
||||
protected function manifest_filename(): string {
|
||||
return 'blocks-manifest.json';
|
||||
}
|
||||
|
||||
public static function init(): void {
|
||||
add_action( 'init', array( __CLASS__, 'register_post_type' ), 5 );
|
||||
}
|
||||
|
||||
public static function register_post_type(): void {
|
||||
register_post_type(
|
||||
self::POST_TYPE,
|
||||
array(
|
||||
'public' => false,
|
||||
'show_ui' => false,
|
||||
'supports' => array( 'title' ),
|
||||
'capability_type' => 'page',
|
||||
'map_meta_cap' => true,
|
||||
'exclude_from_search' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function create( array $spec, string $status = 'publish' ) {
|
||||
$val = EMCP_Tools_Block_Generator::validate( $spec );
|
||||
if ( is_wp_error( $val ) ) {
|
||||
return $val;
|
||||
}
|
||||
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => $status,
|
||||
'post_title' => sanitize_text_field( $spec['title'] ),
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, self::META_SPEC, $spec );
|
||||
update_post_meta( $post_id, self::META_BLOCK_NAME, sanitize_key( $spec['name'] ) );
|
||||
|
||||
$compile = $this->compile( $post_id, $spec );
|
||||
if ( is_wp_error( $compile ) ) {
|
||||
return $compile;
|
||||
}
|
||||
|
||||
$this->rebuild_manifest();
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
public function update( int $post_id, array $spec ) {
|
||||
$val = EMCP_Tools_Block_Generator::validate( $spec );
|
||||
if ( is_wp_error( $val ) ) {
|
||||
return $val;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || self::POST_TYPE !== $post->post_type ) {
|
||||
return new WP_Error( 'not_found', __( 'Block not found.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_title' => sanitize_text_field( $spec['title'] ),
|
||||
)
|
||||
);
|
||||
|
||||
update_post_meta( $post_id, self::META_SPEC, $spec );
|
||||
update_post_meta( $post_id, self::META_BLOCK_NAME, sanitize_key( $spec['name'] ) );
|
||||
|
||||
$compile = $this->compile( $post_id, $spec );
|
||||
if ( is_wp_error( $compile ) ) {
|
||||
return $compile;
|
||||
}
|
||||
|
||||
$this->bump_version( $post_id );
|
||||
$this->rebuild_manifest();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function compile( int $post_id, array $spec ) {
|
||||
$this->ensure_sandbox();
|
||||
$dir = $this->artifact_dir( $post_id );
|
||||
wp_mkdir_p( $dir );
|
||||
|
||||
$block_slug = 'emcp-sandbox/' . sanitize_key( $spec['name'] );
|
||||
$files = EMCP_Tools_Block_Generator::generate( $spec, $block_slug );
|
||||
|
||||
$this->write_file( $dir . '/block.json', $files['block_json'] );
|
||||
$this->write_file( $dir . '/render.php', $files['render_php'] );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get( int $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || self::POST_TYPE !== $post->post_type ) {
|
||||
return new WP_Error( 'not_found', __( 'Block not found.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'status' => $post->post_status,
|
||||
'name' => get_post_meta( $post->ID, self::META_BLOCK_NAME, true ),
|
||||
'spec' => get_post_meta( $post->ID, self::META_SPEC, true ),
|
||||
'sync' => $this->sync_meta( $post->ID ),
|
||||
);
|
||||
}
|
||||
|
||||
public function list( array $args = array() ): array {
|
||||
$query_args = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => array( 'publish', 'draft' ),
|
||||
'posts_per_page' => 100,
|
||||
);
|
||||
$posts = get_posts( $query_args );
|
||||
$list = array();
|
||||
foreach ( $posts as $p ) {
|
||||
$item = $this->get( $p->ID );
|
||||
if ( ! is_wp_error( $item ) ) {
|
||||
$list[] = $item;
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function set_status( int $post_id, string $status ): bool {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || self::POST_TYPE !== $post->post_type ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_status' => $status,
|
||||
)
|
||||
);
|
||||
|
||||
$this->rebuild_manifest();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete( int $post_id ): bool {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || self::POST_TYPE !== $post->post_type ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->rmdir_recursive( $this->artifact_dir( $post_id ) );
|
||||
wp_delete_post( $post_id, true );
|
||||
$this->rebuild_manifest();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rebuild_manifest(): void {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 200,
|
||||
)
|
||||
);
|
||||
|
||||
$manifest = array();
|
||||
foreach ( $posts as $p ) {
|
||||
$manifest[] = array(
|
||||
'id' => $p->ID,
|
||||
'name' => get_post_meta( $p->ID, self::META_BLOCK_NAME, true ),
|
||||
'dir' => $this->artifact_dir( $p->ID ),
|
||||
);
|
||||
}
|
||||
|
||||
$this->write_file( $this->manifest_path(), wp_json_encode( $manifest, JSON_PRETTY_PRINT ) );
|
||||
}
|
||||
|
||||
public function checksum( int $id ): string {
|
||||
$spec = (array) get_post_meta( $id, self::META_SPEC, true );
|
||||
return hash( 'sha256', wp_json_encode( $spec ) );
|
||||
}
|
||||
|
||||
public function to_bundle( int $id ) {
|
||||
$post = get_post( $id );
|
||||
if ( ! $post || self::POST_TYPE !== $post->post_type ) {
|
||||
return new WP_Error( 'not_found', 'Block not found.' );
|
||||
}
|
||||
$spec = (array) get_post_meta( $id, self::META_SPEC, true );
|
||||
$sm = $this->sync_meta( $id );
|
||||
return array(
|
||||
'kind' => 'block',
|
||||
'uuid' => $sm['uuid'],
|
||||
'meta' => array(
|
||||
'title' => $post->post_title,
|
||||
'description' => $post->post_excerpt,
|
||||
'author' => (string) ( wp_get_current_user()->user_login ?? '' ),
|
||||
'license' => 'GPL-2.0-or-later',
|
||||
),
|
||||
'spec' => $spec,
|
||||
'version' => max( 1, (int) ( $sm['version'] ?? 1 ) ),
|
||||
'updated_at' => $sm['updated_at'] ?? gmdate( 'c' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function apply_bundle( array $bundle ) {
|
||||
$spec = (array) ( $bundle['spec'] ?? array() );
|
||||
$name = (string) ( $bundle['meta']['title'] ?? 'Imported Block' );
|
||||
return $this->create_block( $name, $spec );
|
||||
}
|
||||
}
|
||||
|
||||
EMCP_Tools_Block_Store::init();
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
/**
|
||||
* WCAG color-contrast math.
|
||||
*
|
||||
* Pure static helpers — no WordPress or Elementor dependency — so they run in
|
||||
* unit tests without stubs and are reusable by the A11y audit/fix tools and any
|
||||
* future brand-kit contrast checks. Implements the WCAG 2.1 relative-luminance
|
||||
* and contrast-ratio definitions.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static WCAG contrast utilities.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
final class EMCP_Tools_Color_Contrast {
|
||||
|
||||
/** WCAG AA contrast minimum for normal text. */
|
||||
const AA_NORMAL = 4.5;
|
||||
|
||||
/** WCAG AA contrast minimum for large text (>= 18pt, or 14pt bold). */
|
||||
const AA_LARGE = 3.0;
|
||||
|
||||
/** WCAG AAA contrast minimum for normal text. */
|
||||
const AAA_NORMAL = 7.0;
|
||||
|
||||
/** WCAG AAA contrast minimum for large text. */
|
||||
const AAA_LARGE = 4.5;
|
||||
|
||||
/**
|
||||
* Parses a CSS hex color to an [r, g, b] triplet (0-255).
|
||||
*
|
||||
* Accepts #RGB, #RRGGBB, and #RRGGBBAA (3-, 6-, 8-digit, with or without
|
||||
* the leading '#'). The alpha channel of an 8-digit hex is ignored — contrast
|
||||
* is computed on the opaque color; semi-transparent text over an unknown
|
||||
* backdrop can't be assessed reliably and is treated as inconclusive upstream.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param string $hex A CSS hex color.
|
||||
* @return int[]|null [r, g, b] or null if the string isn't a valid hex color.
|
||||
*/
|
||||
public static function hex_to_rgb( string $hex ): ?array {
|
||||
$hex = ltrim( trim( $hex ), '#' );
|
||||
$len = strlen( $hex );
|
||||
|
||||
if ( ! ctype_xdigit( $hex ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( 3 === $len ) {
|
||||
$r = hexdec( str_repeat( $hex[0], 2 ) );
|
||||
$g = hexdec( str_repeat( $hex[1], 2 ) );
|
||||
$b = hexdec( str_repeat( $hex[2], 2 ) );
|
||||
} elseif ( 6 === $len || 8 === $len ) {
|
||||
$r = hexdec( substr( $hex, 0, 2 ) );
|
||||
$g = hexdec( substr( $hex, 2, 2 ) );
|
||||
$b = hexdec( substr( $hex, 4, 2 ) );
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array( (int) $r, (int) $g, (int) $b );
|
||||
}
|
||||
|
||||
/**
|
||||
* WCAG relative luminance of an [r, g, b] triplet (0.0–1.0).
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param int[] $rgb [r, g, b] 0-255.
|
||||
* @return float
|
||||
*/
|
||||
public static function relative_luminance( array $rgb ): float {
|
||||
$channels = array();
|
||||
foreach ( array( 0, 1, 2 ) as $i ) {
|
||||
$cs = ( isset( $rgb[ $i ] ) ? max( 0, min( 255, (int) $rgb[ $i ] ) ) : 0 ) / 255;
|
||||
$channels[ $i ] = ( $cs <= 0.03928 )
|
||||
? $cs / 12.92
|
||||
: pow( ( $cs + 0.055 ) / 1.055, 2.4 );
|
||||
}
|
||||
return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Contrast ratio between two hex colors (1.0–21.0), or null if either is
|
||||
* not a parseable hex color.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param string $hex_a First color.
|
||||
* @param string $hex_b Second color.
|
||||
* @return float|null
|
||||
*/
|
||||
public static function contrast_ratio( string $hex_a, string $hex_b ): ?float {
|
||||
$a = self::hex_to_rgb( $hex_a );
|
||||
$b = self::hex_to_rgb( $hex_b );
|
||||
if ( null === $a || null === $b ) {
|
||||
return null;
|
||||
}
|
||||
$la = self::relative_luminance( $a );
|
||||
$lb = self::relative_luminance( $b );
|
||||
$lighter = max( $la, $lb );
|
||||
$darker = min( $la, $lb );
|
||||
return ( $lighter + 0.05 ) / ( $darker + 0.05 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a contrast ratio meets a WCAG threshold.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param float $ratio The contrast ratio.
|
||||
* @param bool $large Whether the text qualifies as "large".
|
||||
* @param string $level 'AA' (default) or 'AAA'.
|
||||
* @return bool
|
||||
*/
|
||||
public static function passes( float $ratio, bool $large = false, string $level = 'AA' ): bool {
|
||||
if ( 'AAA' === strtoupper( $level ) ) {
|
||||
return $ratio >= ( $large ? self::AAA_LARGE : self::AAA_NORMAL );
|
||||
}
|
||||
return $ratio >= ( $large ? self::AA_LARGE : self::AA_NORMAL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggests an adjusted foreground hex that meets the target ratio against a
|
||||
* fixed background, by stepping the foreground toward black or white
|
||||
* (whichever direction the background calls for) until the ratio is met.
|
||||
*
|
||||
* Returns the original color if it already passes, or null if no adjustment
|
||||
* within range reaches the target (extremely rare — black or white against a
|
||||
* mid-tone always satisfies AA).
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param string $fg_hex Foreground (text) hex.
|
||||
* @param string $bg_hex Background hex.
|
||||
* @param float $target Target ratio (default AA normal, 4.5).
|
||||
* @return string|null Adjusted #RRGGBB, the original if already passing, or null.
|
||||
*/
|
||||
public static function suggest_adjusted( string $fg_hex, string $bg_hex, float $target = self::AA_NORMAL ): ?string {
|
||||
$fg = self::hex_to_rgb( $fg_hex );
|
||||
$bg = self::hex_to_rgb( $bg_hex );
|
||||
if ( null === $fg || null === $bg ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$current = self::contrast_ratio( $fg_hex, $bg_hex );
|
||||
if ( null !== $current && $current >= $target ) {
|
||||
return self::rgb_to_hex( $fg );
|
||||
}
|
||||
|
||||
// Light background → darken the text; dark background → lighten it.
|
||||
$bg_lum = self::relative_luminance( $bg );
|
||||
$toward = ( $bg_lum > 0.5 ) ? 0 : 255;
|
||||
$best_hex = null;
|
||||
|
||||
// 100 steps from the current color toward black/white.
|
||||
for ( $step = 1; $step <= 100; $step++ ) {
|
||||
$t = $step / 100;
|
||||
$adj = array(
|
||||
(int) round( $fg[0] + ( $toward - $fg[0] ) * $t ),
|
||||
(int) round( $fg[1] + ( $toward - $fg[1] ) * $t ),
|
||||
(int) round( $fg[2] + ( $toward - $fg[2] ) * $t ),
|
||||
);
|
||||
$adj_hex = self::rgb_to_hex( $adj );
|
||||
$ratio = self::contrast_ratio( $adj_hex, $bg_hex );
|
||||
if ( null !== $ratio && $ratio >= $target ) {
|
||||
$best_hex = $adj_hex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $best_hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an [r, g, b] triplet as an uppercase #RRGGBB string.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param int[] $rgb [r, g, b] 0-255.
|
||||
* @return string
|
||||
*/
|
||||
public static function rgb_to_hex( array $rgb ): string {
|
||||
$clamp = static function ( $v ): int {
|
||||
return max( 0, min( 255, (int) $v ) );
|
||||
};
|
||||
return sprintf( '#%02X%02X%02X', $clamp( $rgb[0] ?? 0 ), $clamp( $rgb[1] ?? 0 ), $clamp( $rgb[2] ?? 0 ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
<?php
|
||||
/**
|
||||
* Normalized content extraction from an Elementor element tree.
|
||||
*
|
||||
* Walks a page's Elementor data once and yields the shapes the SEO and A11y
|
||||
* audits both need — headings, text blocks, images, links, form fields, a word
|
||||
* count, and best-effort text/background color pairs for contrast checks. The
|
||||
* core is static and operates on a plain element-tree array, so it unit-tests
|
||||
* with fixtures and needs no WordPress at all; alt-text resolution from the
|
||||
* media library is the only optional WP touch (guarded by function_exists).
|
||||
*
|
||||
* Native-widget-first: bespoke markup inside HTML/text-editor widgets is parsed
|
||||
* with light regex (headings, links, <img> alt), so audits don't silently
|
||||
* under-count content that lives in raw HTML.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a normalized, audit-ready view of a page's content.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
final class EMCP_Tools_Content_Extractor {
|
||||
|
||||
/**
|
||||
* Extracts the normalized content view from an element tree.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param array $elements The Elementor element tree (array of elements).
|
||||
* @param string $site_host Optional site host (e.g. "example.com") for
|
||||
* internal/external link classification.
|
||||
* @return array {
|
||||
* @type array $headings [ ['level'=>int,'text'=>string,'element_id'=>string], ... ]
|
||||
* @type array $text_blocks [ ['text'=>string,'element_id'=>string], ... ]
|
||||
* @type array $images [ ['element_id'=>string,'attachment_id'=>int,'url'=>string,'alt'=>string], ... ]
|
||||
* @type array $links [ ['url'=>string,'text'=>string,'internal'=>bool,'element_id'=>string], ... ]
|
||||
* @type array $form_fields [ ['label'=>string,'type'=>string,'element_id'=>string], ... ]
|
||||
* @type array $text_style_contexts [ ['element_id'=>string,'color'=>string,'background'=>?string,'background_source'=>string], ... ]
|
||||
* @type int $word_count Total words across headings + text blocks.
|
||||
* }
|
||||
*/
|
||||
public static function extract( array $elements, string $site_host = '' ): array {
|
||||
$out = array(
|
||||
'headings' => array(),
|
||||
'text_blocks' => array(),
|
||||
'images' => array(),
|
||||
'links' => array(),
|
||||
'form_fields' => array(),
|
||||
'text_style_contexts' => array(),
|
||||
'word_count' => 0,
|
||||
);
|
||||
|
||||
$out['_current_heading'] = '';
|
||||
self::walk( $elements, $site_host, null, $out );
|
||||
unset( $out['_current_heading'] );
|
||||
|
||||
// Word count from headings + text blocks.
|
||||
$words = 0;
|
||||
foreach ( $out['headings'] as $h ) {
|
||||
$words += self::count_words( $h['text'] );
|
||||
}
|
||||
foreach ( $out['text_blocks'] as $t ) {
|
||||
$words += self::count_words( $t['text'] );
|
||||
}
|
||||
$out['word_count'] = $words;
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walks the tree, carrying the nearest resolved ancestor
|
||||
* background color for contrast context.
|
||||
*
|
||||
* @param array $elements Elements to walk.
|
||||
* @param string $site_host Site host for link classification.
|
||||
* @param string|null $ancestor_bg Nearest ancestor background hex, or null.
|
||||
* @param array $out Accumulator (by reference).
|
||||
*/
|
||||
private static function walk( array $elements, string $site_host, ?string $ancestor_bg, array &$out ): void {
|
||||
foreach ( $elements as $el ) {
|
||||
if ( ! is_array( $el ) ) {
|
||||
continue;
|
||||
}
|
||||
$id = isset( $el['id'] ) ? (string) $el['id'] : '';
|
||||
$type = isset( $el['widgetType'] ) ? (string) $el['widgetType'] : '';
|
||||
$el_type = isset( $el['elType'] ) ? (string) $el['elType'] : '';
|
||||
$settings = isset( $el['settings'] ) && is_array( $el['settings'] ) ? $el['settings'] : array();
|
||||
|
||||
// Resolve this element's own background, if any (containers mostly).
|
||||
$own_bg = self::resolve_bg( $settings );
|
||||
$bg_for_children = ( null !== $own_bg ) ? $own_bg : $ancestor_bg;
|
||||
|
||||
self::collect_widget( $id, $type, $el_type, $settings, $site_host, $ancestor_bg, $out );
|
||||
|
||||
// Recurse into children (containers / sections / columns / atomic).
|
||||
if ( ! empty( $el['elements'] ) && is_array( $el['elements'] ) ) {
|
||||
self::walk( $el['elements'], $site_host, $bg_for_children, $out );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects content from a single element by widget type.
|
||||
*
|
||||
* @param string $id Element id.
|
||||
* @param string $type widgetType.
|
||||
* @param string $el_type elType.
|
||||
* @param array $settings Element settings.
|
||||
* @param string $site_host Site host.
|
||||
* @param string|null $ancestor_bg Nearest ancestor background hex.
|
||||
* @param array $out Accumulator (by reference).
|
||||
*/
|
||||
private static function collect_widget( string $id, string $type, string $el_type, array $settings, string $site_host, ?string $ancestor_bg, array &$out ): void {
|
||||
switch ( $type ) {
|
||||
case 'heading':
|
||||
$text = self::scalar( $settings['title'] ?? '' );
|
||||
$level = self::header_size_to_level( self::scalar( $settings['header_size'] ?? 'h2' ) );
|
||||
if ( '' !== $text ) {
|
||||
$plain = self::plain_text( $text );
|
||||
if ( null !== $level ) {
|
||||
$out['headings'][] = array(
|
||||
'level' => $level,
|
||||
'text' => $plain,
|
||||
'element_id' => $id,
|
||||
);
|
||||
$out['_current_heading'] = $plain;
|
||||
} else {
|
||||
$out['text_blocks'][] = array( 'text' => $plain, 'element_id' => $id );
|
||||
}
|
||||
}
|
||||
self::collect_text_color( $id, $settings, array( 'title_color' ), $ancestor_bg, $out );
|
||||
self::maybe_link( self::scalar( self::deep( $settings, array( 'link', 'url' ) ) ), $text, $site_host, $id, $out );
|
||||
break;
|
||||
|
||||
case 'e-heading': // Atomic.
|
||||
$atext = self::scalar( $settings['title'] ?? '' );
|
||||
$alevel = self::header_size_to_level( self::scalar( $settings['tag'] ?? 'h2' ) );
|
||||
if ( '' !== $atext && null !== $alevel ) {
|
||||
$aplain = self::plain_text( $atext );
|
||||
$out['headings'][] = array( 'level' => $alevel, 'text' => $aplain, 'element_id' => $id );
|
||||
$out['_current_heading'] = $aplain;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text-editor':
|
||||
case 'e-paragraph':
|
||||
$html = self::scalar( $settings['editor'] ?? ( $settings['paragraph'] ?? '' ) );
|
||||
if ( '' !== $html ) {
|
||||
$out['text_blocks'][] = array( 'text' => self::plain_text( $html ), 'element_id' => $id );
|
||||
self::parse_markup( $html, $site_host, $id, $out );
|
||||
}
|
||||
self::collect_text_color( $id, $settings, array( 'text_color', 'color' ), $ancestor_bg, $out );
|
||||
break;
|
||||
|
||||
case 'button':
|
||||
case 'e-button':
|
||||
$btxt = self::plain_text( self::scalar( $settings['text'] ?? '' ) );
|
||||
if ( '' !== $btxt ) {
|
||||
$out['text_blocks'][] = array( 'text' => $btxt, 'element_id' => $id );
|
||||
}
|
||||
self::maybe_link( self::scalar( self::deep( $settings, array( 'link', 'url' ) ) ), $btxt, $site_host, $id, $out );
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
case 'e-image':
|
||||
case 'image-box':
|
||||
self::collect_image( $id, $settings, $out );
|
||||
if ( 'image-box' === $type ) {
|
||||
$title = self::plain_text( self::scalar( $settings['title_text'] ?? '' ) );
|
||||
$desc = self::plain_text( self::scalar( $settings['description_text'] ?? '' ) );
|
||||
if ( '' !== $title ) {
|
||||
$lvl = self::header_size_to_level( self::scalar( $settings['title_size'] ?? 'h3' ) );
|
||||
$out['headings'][] = array( 'level' => $lvl ?? 3, 'text' => $title, 'element_id' => $id );
|
||||
}
|
||||
if ( '' !== $desc ) {
|
||||
$out['text_blocks'][] = array( 'text' => $desc, 'element_id' => $id );
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'icon-list':
|
||||
$items = $settings['icon_list'] ?? array();
|
||||
if ( is_array( $items ) ) {
|
||||
foreach ( $items as $item ) {
|
||||
if ( ! is_array( $item ) ) {
|
||||
continue;
|
||||
}
|
||||
$itxt = self::plain_text( self::scalar( $item['text'] ?? '' ) );
|
||||
if ( '' !== $itxt ) {
|
||||
$out['text_blocks'][] = array( 'text' => $itxt, 'element_id' => $id );
|
||||
}
|
||||
self::maybe_link( self::scalar( self::deep( $item, array( 'link', 'url' ) ) ), $itxt, $site_host, $id, $out );
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'html':
|
||||
$raw = self::scalar( $settings['html'] ?? '' );
|
||||
if ( '' !== $raw ) {
|
||||
$out['text_blocks'][] = array( 'text' => self::plain_text( $raw ), 'element_id' => $id );
|
||||
self::parse_markup( $raw, $site_host, $id, $out );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'form': // Elementor Pro.
|
||||
$fields = $settings['form_fields'] ?? array();
|
||||
if ( is_array( $fields ) ) {
|
||||
foreach ( $fields as $field ) {
|
||||
if ( ! is_array( $field ) ) {
|
||||
continue;
|
||||
}
|
||||
$out['form_fields'][] = array(
|
||||
'label' => self::plain_text( self::scalar( $field['field_label'] ?? '' ) ),
|
||||
'type' => self::scalar( $field['field_type'] ?? ( $field['type'] ?? '' ) ),
|
||||
'element_id' => $id,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects an image (native or atomic) into the accumulator.
|
||||
*
|
||||
* @param string $id Element id.
|
||||
* @param array $settings Element settings.
|
||||
* @param array $out Accumulator (by reference).
|
||||
*/
|
||||
private static function collect_image( string $id, array $settings, array &$out ): void {
|
||||
$image = $settings['image'] ?? array();
|
||||
// Atomic e-image may carry { image: { $$type, value: { id, url } } }.
|
||||
if ( isset( $image['$$type'] ) ) {
|
||||
$image = is_array( $image['value'] ?? null ) ? $image['value'] : array();
|
||||
}
|
||||
if ( ! is_array( $image ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$attachment_id = (int) self::scalar( $image['id'] ?? 0 );
|
||||
$url = (string) self::scalar( $image['url'] ?? '' );
|
||||
// Widget-level alt override wins; otherwise the media-library alt.
|
||||
$alt = (string) self::scalar( $image['alt'] ?? ( $settings['alt'] ?? '' ) );
|
||||
if ( '' === $alt && $attachment_id > 0 && function_exists( 'get_post_meta' ) ) {
|
||||
$meta_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
|
||||
$alt = is_string( $meta_alt ) ? $meta_alt : '';
|
||||
}
|
||||
|
||||
if ( $attachment_id > 0 || '' !== $url ) {
|
||||
$out['images'][] = array(
|
||||
'element_id' => $id,
|
||||
'attachment_id' => $attachment_id,
|
||||
'url' => $url,
|
||||
'alt' => trim( $alt ),
|
||||
'context_heading' => isset( $out['_current_heading'] ) ? (string) $out['_current_heading'] : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a text-color / effective-background pair for contrast checks.
|
||||
*
|
||||
* Best-effort: a `*_color__globals__` reference can't be resolved to a hex
|
||||
* here without the kit, so it's skipped (reported inconclusive upstream).
|
||||
*
|
||||
* @param string $id Element id.
|
||||
* @param array $settings Element settings.
|
||||
* @param string[] $color_keys Candidate color setting keys, in priority order.
|
||||
* @param string|null $ancestor_bg Nearest ancestor background hex.
|
||||
* @param array $out Accumulator (by reference).
|
||||
*/
|
||||
private static function collect_text_color( string $id, array $settings, array $color_keys, ?string $ancestor_bg, array &$out ): void {
|
||||
$color = '';
|
||||
$color_key = '';
|
||||
foreach ( $color_keys as $key ) {
|
||||
$val = self::scalar( $settings[ $key ] ?? '' );
|
||||
if ( '' !== $val && '#' === substr( $val, 0, 1 ) ) {
|
||||
$color = $val;
|
||||
$color_key = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( '' === $color ) {
|
||||
return; // No literal color (default or globals ref) — leave to upstream as inconclusive.
|
||||
}
|
||||
|
||||
$own_bg = self::resolve_bg( $settings );
|
||||
$bg = ( null !== $own_bg ) ? $own_bg : $ancestor_bg;
|
||||
|
||||
$out['text_style_contexts'][] = array(
|
||||
'element_id' => $id,
|
||||
'color' => $color,
|
||||
'color_key' => $color_key,
|
||||
'background' => $bg,
|
||||
'background_source' => ( null !== $own_bg ) ? 'element' : ( ( null !== $ancestor_bg ) ? 'ancestor' : 'none' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an element's own literal background hex, if it sets one.
|
||||
*
|
||||
* @param array $settings Element settings.
|
||||
* @return string|null
|
||||
*/
|
||||
private static function resolve_bg( array $settings ): ?string {
|
||||
foreach ( array( 'background_color', '_background_color' ) as $key ) {
|
||||
$val = self::scalar( $settings[ $key ] ?? '' );
|
||||
if ( '' !== $val && '#' === substr( $val, 0, 1 ) ) {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses anchors and <img> tags out of raw markup (HTML / text-editor).
|
||||
*
|
||||
* @param string $html Markup.
|
||||
* @param string $site_host Site host.
|
||||
* @param string $id Owning element id.
|
||||
* @param array $out Accumulator (by reference).
|
||||
*/
|
||||
private static function parse_markup( string $html, string $site_host, string $id, array &$out ): void {
|
||||
// Headings inside markup.
|
||||
if ( preg_match_all( '/<h([1-6])\b[^>]*>(.*?)<\/h\1>/is', $html, $hm, PREG_SET_ORDER ) ) {
|
||||
foreach ( $hm as $m ) {
|
||||
$txt = self::plain_text( $m[2] );
|
||||
if ( '' !== $txt ) {
|
||||
$out['headings'][] = array( 'level' => (int) $m[1], 'text' => $txt, 'element_id' => $id );
|
||||
}
|
||||
}
|
||||
}
|
||||
// Anchors.
|
||||
if ( preg_match_all( '/<a\b[^>]*href=("|\')(.*?)\1[^>]*>(.*?)<\/a>/is', $html, $am, PREG_SET_ORDER ) ) {
|
||||
foreach ( $am as $m ) {
|
||||
self::maybe_link( $m[2], self::plain_text( $m[3] ), $site_host, $id, $out );
|
||||
}
|
||||
}
|
||||
// Images (alt may be absent → empty).
|
||||
if ( preg_match_all( '/<img\b[^>]*>/is', $html, $im, PREG_SET_ORDER ) ) {
|
||||
foreach ( $im as $m ) {
|
||||
$tag = $m[0];
|
||||
$alt = '';
|
||||
if ( preg_match( '/\balt=("|\')(.*?)\1/is', $tag, $a ) ) {
|
||||
$alt = self::plain_text( $a[2] );
|
||||
}
|
||||
$src = '';
|
||||
if ( preg_match( '/\bsrc=("|\')(.*?)\1/is', $tag, $s ) ) {
|
||||
$src = $s[2];
|
||||
}
|
||||
$out['images'][] = array(
|
||||
'element_id' => $id,
|
||||
'attachment_id' => 0,
|
||||
'url' => $src,
|
||||
'alt' => $alt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a link entry if the URL is non-empty.
|
||||
*
|
||||
* @param string $url URL.
|
||||
* @param string $text Link text.
|
||||
* @param string $site_host Site host.
|
||||
* @param string $id Owning element id.
|
||||
* @param array $out Accumulator (by reference).
|
||||
*/
|
||||
private static function maybe_link( $url, string $text, string $site_host, string $id, array &$out ): void {
|
||||
$url = trim( (string) $url );
|
||||
if ( '' === $url ) {
|
||||
return;
|
||||
}
|
||||
$out['links'][] = array(
|
||||
'url' => $url,
|
||||
'text' => trim( $text ),
|
||||
'internal' => self::is_internal( $url, $site_host ),
|
||||
'element_id' => $id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies a URL as internal to the site.
|
||||
*
|
||||
* @param string $url URL.
|
||||
* @param string $site_host Site host (may be empty).
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_internal( string $url, string $site_host ): bool {
|
||||
$url = trim( $url );
|
||||
if ( '' === $url ) {
|
||||
return false;
|
||||
}
|
||||
$first = $url[0];
|
||||
if ( '#' === $first || '/' === $first ) {
|
||||
return true; // Anchor or root-relative.
|
||||
}
|
||||
if ( preg_match( '/^(mailto:|tel:|sms:|javascript:)/i', $url ) ) {
|
||||
return false;
|
||||
}
|
||||
$host = parse_url( $url, PHP_URL_HOST );
|
||||
if ( ! $host ) {
|
||||
return true; // Relative path without scheme.
|
||||
}
|
||||
if ( '' !== $site_host && 0 === strcasecmp( ltrim( $host, 'www.' ), ltrim( $site_host, 'www.' ) ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an Elementor header_size / tag to a numeric heading level, or null
|
||||
* when the tag is not a heading (div, p, span, etc.).
|
||||
*
|
||||
* @param string $tag e.g. "h2", "div".
|
||||
* @return int|null
|
||||
*/
|
||||
private static function header_size_to_level( string $tag ): ?int {
|
||||
$tag = strtolower( trim( $tag ) );
|
||||
if ( preg_match( '/^h([1-6])$/', $tag, $m ) ) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps an atomic `{ $$type, value }` wrapper, returning a scalar string.
|
||||
*
|
||||
* @param mixed $value Raw setting value.
|
||||
* @return string
|
||||
*/
|
||||
private static function scalar( $value ): string {
|
||||
if ( is_array( $value ) ) {
|
||||
if ( array_key_exists( 'value', $value ) && array_key_exists( '$$type', $value ) ) {
|
||||
return is_scalar( $value['value'] ) ? (string) $value['value'] : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return is_scalar( $value ) ? (string) $value : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a nested key path from an array, unwrapping atomic wrappers.
|
||||
*
|
||||
* @param array $arr Source array.
|
||||
* @param string[] $path Key path.
|
||||
* @return mixed
|
||||
*/
|
||||
private static function deep( array $arr, array $path ) {
|
||||
$cur = $arr;
|
||||
foreach ( $path as $key ) {
|
||||
if ( is_array( $cur ) && array_key_exists( 'value', $cur ) && is_array( $cur['value'] ) ) {
|
||||
$cur = $cur['value'];
|
||||
}
|
||||
if ( ! is_array( $cur ) || ! array_key_exists( $key, $cur ) ) {
|
||||
return '';
|
||||
}
|
||||
$cur = $cur[ $key ];
|
||||
}
|
||||
return $cur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips tags + decodes entities + collapses whitespace.
|
||||
*
|
||||
* @param string $html Input.
|
||||
* @return string
|
||||
*/
|
||||
private static function plain_text( string $html ): string {
|
||||
// Drop <script>/<style> blocks (including their contents) first, so code
|
||||
// never leaks into extracted text.
|
||||
$text = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', ' ', $html );
|
||||
// Replace every remaining tag with a space so adjacent inline text stays
|
||||
// word-separated ("More than</span><span>coffee" → "More than coffee",
|
||||
// not "thancoffee") — plain strip_tags would merge them.
|
||||
$text = preg_replace( '/<[^>]+>/', ' ', (string) $text );
|
||||
$text = html_entity_decode( (string) $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
|
||||
$text = preg_replace( '/\s+/u', ' ', (string) $text );
|
||||
// Drop the space a tag boundary can leave before sentence punctuation
|
||||
// ("coffee ." → "coffee.").
|
||||
$text = preg_replace( '/\s+([.,;:!?])/u', '$1', (string) $text );
|
||||
return trim( (string) $text );
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts words in a plain-text string.
|
||||
*
|
||||
* @param string $text Plain text.
|
||||
* @return int
|
||||
*/
|
||||
private static function count_words( string $text ): int {
|
||||
$text = trim( $text );
|
||||
if ( '' === $text ) {
|
||||
return 0;
|
||||
}
|
||||
return count( preg_split( '/\s+/u', $text ) ?: array() );
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
class EMCP_Tools_GitHub_Updater {
|
||||
|
||||
/** GitHub `owner/repo` the free releases live on. */
|
||||
const REPO = 'msrbuilds/elementor-mcp';
|
||||
const REPO = 'Heretek-AI/elementor-mcp';
|
||||
|
||||
/** Transient caching the parsed latest-release payload (limits API hits). */
|
||||
const TRANSIENT = 'emcp_tools_github_release';
|
||||
@@ -39,8 +39,8 @@ class EMCP_Tools_GitHub_Updater {
|
||||
/** Shorter cache after a failed/empty lookup so we recover quickly. */
|
||||
const CACHE_TTL_FAIL = 2 * HOUR_IN_SECONDS;
|
||||
|
||||
/** Matches the free release ZIP asset (never the Pro zip, which isn't on GitHub). */
|
||||
const ASSET_PATTERN = '/^emcp-tools-[0-9].*\.zip$/i';
|
||||
/** Matches the release ZIP asset. */
|
||||
const ASSET_PATTERN = '/^.*elementor-mcp.*\.zip$/i';
|
||||
|
||||
/**
|
||||
* The installed plugin folder slug (e.g. `emcp-tools` or `elementor-mcp`),
|
||||
@@ -55,17 +55,11 @@ class EMCP_Tools_GitHub_Updater {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the update hooks — but only for the FREE build. On premium builds
|
||||
* (the `.emcp-pro` marker is present and Freemius reports is_premium) Freemius
|
||||
* owns updates, so we stay out of the way to avoid a double-updater conflict.
|
||||
* Register the update hooks for the unlocked fork.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void {
|
||||
if ( $this->is_premium_build() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update' ) );
|
||||
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
|
||||
add_filter( 'upgrader_source_selection', array( $this, 'rename_source_dir' ), 10, 4 );
|
||||
@@ -99,20 +93,17 @@ class EMCP_Tools_GitHub_Updater {
|
||||
'current' => $current,
|
||||
'latest' => $latest,
|
||||
'update_available' => $update,
|
||||
'update_url' => self_admin_url( 'plugins.php' ),
|
||||
'update_url' => self_admin_url( 'update-core.php' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is the premium build (Freemius handles those updates).
|
||||
* Whether this is an external premium build. Unlocked fork updates via GitHub.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_premium_build(): bool {
|
||||
if ( file_exists( EMCP_TOOLS_DIR . '.emcp-pro' ) ) {
|
||||
return true;
|
||||
}
|
||||
return function_exists( 'emcp_tools_fs' ) && emcp_tools_fs()->is_premium();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* Page Snapshot Pro sections overlay.
|
||||
*
|
||||
* Hooks into `emcp_tools_page_snapshot_sections` to inject deep `seo` and `a11y`
|
||||
* audit sections into the get-page-snapshot tool output.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.3.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Page_Snapshot_Pro {
|
||||
|
||||
/**
|
||||
* Wire snapshot pro sections.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_filter( 'emcp_tools_page_snapshot_sections', array( __CLASS__, 'inject_sections' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject deep a11y and seo audit sections into the page snapshot.
|
||||
*
|
||||
* @param array $sections Current snapshot sections.
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $include Requested section keys (empty = all).
|
||||
* @param array $args Additional args.
|
||||
* @return array
|
||||
*/
|
||||
public static function inject_sections( array $sections, int $post_id, array $include = array(), array $args = array() ): array {
|
||||
if ( ! $post_id ) {
|
||||
return $sections;
|
||||
}
|
||||
|
||||
$want_all = empty( $include );
|
||||
$want_a11y = $want_all || in_array( 'a11y', $include, true );
|
||||
$want_seo = $want_all || in_array( 'seo', $include, true );
|
||||
|
||||
if ( ! $want_a11y && ! $want_seo ) {
|
||||
return $sections;
|
||||
}
|
||||
|
||||
// Retrieve Elementor document structure if available.
|
||||
$elements = array();
|
||||
if ( class_exists( 'EMCP_Tools_Elementor_Data' ) ) {
|
||||
$data = EMCP_Tools_Elementor_Data::instance();
|
||||
$page = $data->get_page_data( $post_id );
|
||||
if ( is_array( $page ) ) {
|
||||
$elements = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$host = wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
$site_host = is_string( $host ) ? $host : '';
|
||||
|
||||
$extracted = array();
|
||||
if ( class_exists( 'EMCP_Tools_Content_Extractor' ) && ! empty( $elements ) ) {
|
||||
$extracted = EMCP_Tools_Content_Extractor::extract( $elements, $site_host );
|
||||
}
|
||||
|
||||
// 1. Accessibility audit section.
|
||||
if ( $want_a11y && class_exists( 'EMCP_Tools_A11y_Abilities' ) && ! empty( $extracted ) ) {
|
||||
$sections['a11y'] = EMCP_Tools_A11y_Abilities::build_a11y_report( $extracted );
|
||||
}
|
||||
|
||||
// 2. Deep SEO audit section.
|
||||
if ( $want_seo && class_exists( 'EMCP_Tools_Seo_Abilities' ) ) {
|
||||
$seo_meta = class_exists( 'EMCP_Tools_Seo_Meta' ) ? EMCP_Tools_Seo_Meta::get( $post_id ) : array();
|
||||
$target_kw = (string) ( $args['keyword'] ?? ( $seo_meta['focus_keyword'] ?? '' ) );
|
||||
$sections['seo'] = EMCP_Tools_Seo_Abilities::build_seo_report( $extracted, $seo_meta, $target_kw );
|
||||
}
|
||||
|
||||
return $sections;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-wire on load.
|
||||
EMCP_Tools_Page_Snapshot_Pro::init();
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* Shared template compiler for sandbox artifacts (widgets and blocks).
|
||||
*
|
||||
* Provides safe HTML compilation, attribute binding, and variable interpolation.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.6.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Sandbox_Template_Compiler {
|
||||
|
||||
/**
|
||||
* Compile a template string with given variables.
|
||||
*
|
||||
* @param string $template Template string with {{var}} or {{#tag}} tokens.
|
||||
* @param array $vars Variable dictionary.
|
||||
* @return string
|
||||
*/
|
||||
public static function compile( string $template, array $vars ): string {
|
||||
return preg_replace_callback(
|
||||
'/\{\{\s*([a-zA-Z0-9_\-]+)\s*\}\}/',
|
||||
static function ( $matches ) use ( $vars ) {
|
||||
$key = $matches[1];
|
||||
if ( isset( $vars[ $key ] ) ) {
|
||||
return esc_html( (string) $vars[ $key ] );
|
||||
}
|
||||
return '';
|
||||
},
|
||||
$template
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PHP render body from a spec template.
|
||||
*
|
||||
* @param string $html Template HTML.
|
||||
* @param array $attributes Block/widget attributes.
|
||||
* @return string PHP code.
|
||||
*/
|
||||
public static function build_php_render( string $html, array $attributes ): string {
|
||||
$code = array();
|
||||
$code[] = '<?php';
|
||||
$code[] = '/** Auto-generated render template */';
|
||||
$code[] = 'if ( ! defined( "ABSPATH" ) ) { exit; }';
|
||||
$code[] = '$attrs = $attributes ?? array();';
|
||||
$code[] = '?>';
|
||||
|
||||
// Replace tokens with PHP echo.
|
||||
$parsed = preg_replace_callback(
|
||||
'/\{\{\s*([a-zA-Z0-9_\-]+)\s*\}\}/',
|
||||
static function ( $matches ) {
|
||||
$var = $matches[1];
|
||||
return '<?php echo esc_html( $attrs["' . esc_sql( $var ) . '"] ?? "" ); ?>';
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
$code[] = $parsed;
|
||||
|
||||
return implode( "\n", $code );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
/**
|
||||
* SEO-plugin meta abstraction.
|
||||
*
|
||||
* Reads a post's SEO title / meta description / canonical / focus keyword from
|
||||
* whichever SEO plugin the site uses (Yoast or Rank Math), falling back to
|
||||
* WordPress core, so the SEO audit reports against what the site actually
|
||||
* outputs instead of hard-coupling to one plugin.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 1.8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves SEO meta across Yoast / Rank Math / core.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*/
|
||||
final class EMCP_Tools_Seo_Meta {
|
||||
|
||||
/**
|
||||
* Returns the resolved SEO meta for a post.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @return array {
|
||||
* @type string $source 'yoast' | 'rankmath' | 'core'
|
||||
* @type string $title Resolved SEO title (may contain plugin template vars).
|
||||
* @type string $description Resolved meta description.
|
||||
* @type string $canonical Canonical URL (empty if not explicitly set).
|
||||
* @type string $focus_keyword Focus keyword, if the SEO plugin stores one.
|
||||
* @type bool $title_is_template Whether the title still contains %%...%% / {{...}} tokens.
|
||||
* }
|
||||
*/
|
||||
public static function get( int $post_id ): array {
|
||||
$yoast = self::read_yoast( $post_id );
|
||||
if ( null !== $yoast ) {
|
||||
return self::finalize( $yoast, 'yoast', $post_id );
|
||||
}
|
||||
|
||||
$rankmath = self::read_rankmath( $post_id );
|
||||
if ( null !== $rankmath ) {
|
||||
return self::finalize( $rankmath, 'rankmath', $post_id );
|
||||
}
|
||||
|
||||
return self::finalize( array(), 'core', $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an SEO title + meta description to the active SEO plugin's meta
|
||||
* keys (Yoast or Rank Math). Core-only sites have no standard meta-description
|
||||
* store, so nothing is written and `source` is 'none'.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $title SEO title ('' to skip).
|
||||
* @param string $description Meta description ('' to skip).
|
||||
* @return array { @type bool $written, @type string $source, @type string[] $fields }
|
||||
*/
|
||||
public static function write( int $post_id, string $title, string $description ): array {
|
||||
$result = array( 'written' => false, 'source' => 'none', 'fields' => array() );
|
||||
if ( ! function_exists( 'update_post_meta' ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$is_yoast = defined( 'WPSEO_VERSION' ) || '' !== self::meta( $post_id, '_yoast_wpseo_title' ) || '' !== self::meta( $post_id, '_yoast_wpseo_metadesc' );
|
||||
$is_rankmath = defined( 'RANK_MATH_VERSION' ) || '' !== self::meta( $post_id, 'rank_math_title' ) || '' !== self::meta( $post_id, 'rank_math_description' );
|
||||
|
||||
if ( $is_yoast ) {
|
||||
$result['source'] = 'yoast';
|
||||
$title_key = '_yoast_wpseo_title';
|
||||
$desc_key = '_yoast_wpseo_metadesc';
|
||||
} elseif ( $is_rankmath ) {
|
||||
$result['source'] = 'rankmath';
|
||||
$title_key = 'rank_math_title';
|
||||
$desc_key = 'rank_math_description';
|
||||
} else {
|
||||
return $result; // No SEO plugin to persist into.
|
||||
}
|
||||
|
||||
if ( '' !== $title ) {
|
||||
update_post_meta( $post_id, $title_key, $title );
|
||||
$result['fields'][] = $title_key;
|
||||
}
|
||||
if ( '' !== $description ) {
|
||||
update_post_meta( $post_id, $desc_key, $description );
|
||||
$result['fields'][] = $desc_key;
|
||||
}
|
||||
$result['written'] = ! empty( $result['fields'] );
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Yoast meta, or null if Yoast isn't the active/source-of-truth plugin.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null
|
||||
*/
|
||||
private static function read_yoast( int $post_id ): ?array {
|
||||
$active = defined( 'WPSEO_VERSION' );
|
||||
$title = self::meta( $post_id, '_yoast_wpseo_title' );
|
||||
$desc = self::meta( $post_id, '_yoast_wpseo_metadesc' );
|
||||
$canon = self::meta( $post_id, '_yoast_wpseo_canonical' );
|
||||
$focus = self::meta( $post_id, '_yoast_wpseo_focuskw' );
|
||||
|
||||
if ( ! $active && '' === $title && '' === $desc && '' === $canon && '' === $focus ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => $title,
|
||||
'description' => $desc,
|
||||
'canonical' => $canon,
|
||||
'focus_keyword' => $focus,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Rank Math meta, or null if Rank Math isn't the active/source-of-truth plugin.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null
|
||||
*/
|
||||
private static function read_rankmath( int $post_id ): ?array {
|
||||
$active = defined( 'RANK_MATH_VERSION' );
|
||||
$title = self::meta( $post_id, 'rank_math_title' );
|
||||
$desc = self::meta( $post_id, 'rank_math_description' );
|
||||
$canon = self::meta( $post_id, 'rank_math_canonical_url' );
|
||||
$focus = self::meta( $post_id, 'rank_math_focus_keyword' );
|
||||
|
||||
if ( ! $active && '' === $title && '' === $desc && '' === $canon && '' === $focus ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => $title,
|
||||
'description' => $desc,
|
||||
'canonical' => $canon,
|
||||
'focus_keyword' => $focus,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills gaps from WordPress core and normalizes the return shape.
|
||||
*
|
||||
* @param array $data Partial data from an SEO plugin (may be empty).
|
||||
* @param string $source Source label.
|
||||
* @param int $post_id Post ID.
|
||||
* @return array
|
||||
*/
|
||||
private static function finalize( array $data, string $source, int $post_id ): array {
|
||||
$title = isset( $data['title'] ) ? (string) $data['title'] : '';
|
||||
$desc = isset( $data['description'] ) ? (string) $data['description'] : '';
|
||||
$canon = isset( $data['canonical'] ) ? (string) $data['canonical'] : '';
|
||||
$focus = isset( $data['focus_keyword'] ) ? (string) $data['focus_keyword'] : '';
|
||||
|
||||
// Core fallbacks for empty fields.
|
||||
if ( '' === $title && function_exists( 'get_the_title' ) ) {
|
||||
$title = (string) get_the_title( $post_id );
|
||||
}
|
||||
if ( '' === $desc && function_exists( 'get_post_field' ) ) {
|
||||
$excerpt = get_post_field( 'post_excerpt', $post_id );
|
||||
$desc = is_string( $excerpt ) ? $excerpt : '';
|
||||
}
|
||||
if ( '' === $canon && function_exists( 'get_permalink' ) ) {
|
||||
$permalink = get_permalink( $post_id );
|
||||
$canon = is_string( $permalink ) ? $permalink : '';
|
||||
}
|
||||
|
||||
$title = trim( $title );
|
||||
$desc = trim( $desc );
|
||||
|
||||
return array(
|
||||
'source' => $source,
|
||||
'title' => $title,
|
||||
'description' => $desc,
|
||||
'canonical' => trim( $canon ),
|
||||
'focus_keyword' => trim( $focus ),
|
||||
'title_is_template' => self::looks_templated( $title ) || self::looks_templated( $desc ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single post-meta string, guarded for the unit-test environment.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $key Meta key.
|
||||
* @return string
|
||||
*/
|
||||
private static function meta( int $post_id, string $key ): string {
|
||||
if ( ! function_exists( 'get_post_meta' ) ) {
|
||||
return '';
|
||||
}
|
||||
$val = get_post_meta( $post_id, $key, true );
|
||||
return is_string( $val ) ? trim( $val ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a string still contains SEO-plugin template tokens (so length
|
||||
* checks against it are advisory, not literal).
|
||||
*
|
||||
* @param string $str Candidate string.
|
||||
* @return bool
|
||||
*/
|
||||
private static function looks_templated( string $str ): bool {
|
||||
return (bool) preg_match( '/%%[^%]+%%|\{\{[^}]+\}\}/', $str );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Skills Catalog — parses and serves bundled SKILL.md documentation to MCP agents.
|
||||
*
|
||||
* Scans skills/ and nested domain folders (skills/domain/subdomain) for SKILL.md files.
|
||||
* Provides lookup, search, and progressive disclosure injection into the agent discovery context.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.2.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Skill_Catalog {
|
||||
|
||||
/**
|
||||
* Relative skills directory.
|
||||
*/
|
||||
const SKILLS_DIR = 'skills';
|
||||
|
||||
/**
|
||||
* Cached catalog.
|
||||
*
|
||||
* @var array<string,array{slug:string,name:string,description:string,path:string}>|null
|
||||
*/
|
||||
private static $catalog = null;
|
||||
|
||||
/**
|
||||
* Initialize catalog hooks.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_filter( 'emcp_tools_discovery_skills', array( __CLASS__, 'discovery_catalog' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to skills directory.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_skills_path(): string {
|
||||
return EMCP_TOOLS_DIR . self::SKILLS_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build or return cached catalog of all available skills.
|
||||
*
|
||||
* @return array<string,array{slug:string,name:string,description:string,path:string}>
|
||||
*/
|
||||
public static function get_all(): array {
|
||||
if ( null !== self::$catalog ) {
|
||||
return self::$catalog;
|
||||
}
|
||||
|
||||
self::$catalog = array();
|
||||
$base_dir = self::get_skills_path();
|
||||
|
||||
if ( ! is_dir( $base_dir ) || ! is_readable( $base_dir ) ) {
|
||||
return self::$catalog;
|
||||
}
|
||||
|
||||
// Walk top-level and 1-level nested directories.
|
||||
$top_dirs = glob( $base_dir . '/*', GLOB_ONLYDIR ) ?: array();
|
||||
foreach ( $top_dirs as $dir ) {
|
||||
$slug = basename( $dir );
|
||||
$skill_file = $dir . '/SKILL.md';
|
||||
if ( file_exists( $skill_file ) && is_readable( $skill_file ) ) {
|
||||
self::$catalog[ $slug ] = self::parse_skill_meta( $slug, $skill_file );
|
||||
}
|
||||
|
||||
// 1 nesting level allowed (e.g. emcp-themes/astra).
|
||||
$nested_dirs = glob( $dir . '/*', GLOB_ONLYDIR ) ?: array();
|
||||
foreach ( $nested_dirs as $nested ) {
|
||||
$sub_slug = $slug . '/' . basename( $nested );
|
||||
$nested_file = $nested . '/SKILL.md';
|
||||
if ( file_exists( $nested_file ) && is_readable( $nested_file ) ) {
|
||||
self::$catalog[ $sub_slug ] = self::parse_skill_meta( $sub_slug, $nested_file );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the final skills catalog.
|
||||
*
|
||||
* @param array $catalog Slugs mapped to metadata.
|
||||
*/
|
||||
self::$catalog = (array) apply_filters( 'emcp_tools_skill_sources', self::$catalog );
|
||||
|
||||
return self::$catalog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract name and description from YAML frontmatter or heading.
|
||||
*
|
||||
* @param string $slug Slug.
|
||||
* @param string $path Path to SKILL.md.
|
||||
* @return array{slug:string,name:string,description:string,path:string}
|
||||
*/
|
||||
private static function parse_skill_meta( string $slug, string $path ): array {
|
||||
$content = (string) file_get_contents( $path );
|
||||
$name = ucwords( str_replace( array( '-', '/' ), array( ' ', ' — ' ), $slug ) );
|
||||
$desc = '';
|
||||
|
||||
if ( preg_match( '/^---\s*\n(.*?)\n---/s', $content, $matches ) ) {
|
||||
$front = $matches[1];
|
||||
if ( preg_match( '/^name:\s*(.+)$/m', $front, $n_match ) ) {
|
||||
$name = trim( $n_match[1], " '\"" );
|
||||
}
|
||||
if ( preg_match( '/^description:\s*(.+)$/m', $front, $d_match ) ) {
|
||||
$desc = trim( $d_match[1], " '\"" );
|
||||
}
|
||||
}
|
||||
|
||||
if ( '' === $desc && preg_match( '/^#\s+(.+)$/m', $content, $h_match ) ) {
|
||||
$desc = trim( $h_match[1] );
|
||||
}
|
||||
|
||||
return array(
|
||||
'slug' => $slug,
|
||||
'name' => $name,
|
||||
'description' => $desc ?: $name,
|
||||
'path' => $path,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full skill body by slug, with path-traversal safety.
|
||||
*
|
||||
* @param string $slug Skill slug.
|
||||
* @return string|WP_Error
|
||||
*/
|
||||
public static function get_body( string $slug ) {
|
||||
// Prevent path traversal. Allow alphanumeric, hyphen, underscore, and at most one slash.
|
||||
if ( false !== strpos( $slug, '..' ) || substr_count( $slug, '/' ) > 1 || ! preg_match( '/^[a-z0-9_\-\/]+$/i', $slug ) ) {
|
||||
return new WP_Error( 'invalid_slug', __( 'Invalid skill slug provided.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$all = self::get_all();
|
||||
if ( ! isset( $all[ $slug ] ) ) {
|
||||
return new WP_Error( 'not_found', sprintf( __( 'Skill "%s" not found in catalog.', 'emcp-tools' ), esc_html( $slug ) ) );
|
||||
}
|
||||
|
||||
$path = $all[ $slug ]['path'];
|
||||
if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
|
||||
return new WP_Error( 'unreadable', __( 'Skill file cannot be read.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
return (string) file_get_contents( $path );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the catalog as a markdown block for progressive discovery context.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function discovery_catalog(): string {
|
||||
$all = self::get_all();
|
||||
if ( empty( $all ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = array();
|
||||
$lines[] = '## Available Agent Skills';
|
||||
$lines[] = 'Use `get-skill` with a skill slug to fetch detailed instructions before performing actions in that domain:';
|
||||
|
||||
foreach ( $all as $skill ) {
|
||||
$lines[] = sprintf( '- `%s`: %s — %s', $skill['slug'], $skill['name'], $skill['description'] );
|
||||
}
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-wire on load.
|
||||
EMCP_Tools_Skill_Catalog::init();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Project Memory Digest.
|
||||
*
|
||||
* Extracts learnings and summary items from session activity.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Memory_Digest {
|
||||
|
||||
/**
|
||||
* Summarize recent change logs.
|
||||
*
|
||||
* @param int $limit Number of recent changes to digest.
|
||||
* @return array
|
||||
*/
|
||||
public static function digest_recent_changes( int $limit = 10 ): array {
|
||||
if ( ! class_exists( 'EMCP_Tools_Change_Log' ) ) {
|
||||
return array();
|
||||
}
|
||||
return EMCP_Tools_Change_Log::list_entries( array( 'limit' => $limit ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Project Memory Enforcer.
|
||||
*
|
||||
* Evaluates proposed actions against memory items with severity=block.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Memory_Enforcer {
|
||||
|
||||
/**
|
||||
* Check if an action is blocked by stored guidelines.
|
||||
*
|
||||
* @param string $action Action key.
|
||||
* @param array $params Action parameters.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function check_action( string $action, array $params = array() ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Project Memory Injector.
|
||||
*
|
||||
* Injects approved (published) guidance into the AI agent discovery context via `emcp_tools_discovery_memory`.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Memory_Injector {
|
||||
|
||||
/**
|
||||
* Init filter.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_filter( 'emcp_tools_discovery_memory', array( __CLASS__, 'render_memory_context' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble approved memory items into a markdown block.
|
||||
*
|
||||
* @param string $existing Existing memory block.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_memory_context( string $existing = '' ): string {
|
||||
if ( class_exists( 'EMCP_Tools_Memory_Module' ) && ! EMCP_Tools_Memory_Module::is_enabled() ) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$memories = EMCP_Tools_Memory_Store::query(
|
||||
array(
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 20,
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $memories ) ) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$lines = array();
|
||||
$lines[] = '## Project Memory (Approved Site Guidelines)';
|
||||
$lines[] = 'Follow these project-specific conventions established by the site administrator:';
|
||||
|
||||
foreach ( $memories as $mem ) {
|
||||
$sev = get_post_meta( $mem->ID, EMCP_Tools_Memory_Store::META_SEVERITY, true ) ?: 'info';
|
||||
$target = get_post_meta( $mem->ID, EMCP_Tools_Memory_Store::META_TARGET, true );
|
||||
$prefix = ( 'warning' === $sev || 'block' === $sev ) ? '[IMPORTANT] ' : '';
|
||||
$t_info = $target ? " ({$target})" : '';
|
||||
|
||||
$lines[] = sprintf( '- **%s%s%s**: %s', $prefix, $mem->post_title, $t_info, wp_strip_all_tags( $mem->post_content ) );
|
||||
}
|
||||
|
||||
return trim( $existing . "\n\n" . implode( "\n", $lines ) );
|
||||
}
|
||||
}
|
||||
|
||||
EMCP_Tools_Memory_Injector::init();
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Project Memory Store.
|
||||
*
|
||||
* Registers the custom post type `emcp_memory` where post_status acts as the human-approval gate:
|
||||
* - 'pending': agent-proposed guidance awaiting administrator review
|
||||
* - 'publish': approved guidance injected into agent discovery context
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Memory_Store {
|
||||
|
||||
const POST_TYPE = 'emcp_memory';
|
||||
const META_SEVERITY = '_emcp_severity';
|
||||
const META_TARGET = '_emcp_target';
|
||||
const META_SESSION = '_emcp_session';
|
||||
|
||||
/**
|
||||
* Init post type.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action( 'init', array( __CLASS__, 'register_post_type' ), 5 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register emcp_memory CPT.
|
||||
*/
|
||||
public static function register_post_type(): void {
|
||||
register_post_type(
|
||||
self::POST_TYPE,
|
||||
array(
|
||||
'label' => __( 'Project Memory', 'emcp-tools' ),
|
||||
'public' => false,
|
||||
'show_ui' => false,
|
||||
'supports' => array( 'title', 'editor' ),
|
||||
'capability_type' => 'post',
|
||||
'map_meta_cap' => true,
|
||||
'exclude_from_search' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a proposed memory entry (status=pending).
|
||||
*
|
||||
* @param string $title Short summary / rule title.
|
||||
* @param string $content Detailed guidance.
|
||||
* @param string $severity 'info'|'warning'|'block'.
|
||||
* @param string $target Target component or domain.
|
||||
* @param string $session_id Optional session identifier.
|
||||
* @return int|WP_Error Post ID on success.
|
||||
*/
|
||||
public static function add_proposal( string $title, string $content, string $severity = 'info', string $target = '', string $session_id = '' ) {
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'pending',
|
||||
'post_title' => sanitize_text_field( $title ),
|
||||
'post_content' => wp_kses_post( $content ),
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, self::META_SEVERITY, sanitize_key( $severity ?: 'info' ) );
|
||||
update_post_meta( $post_id, self::META_TARGET, sanitize_text_field( $target ) );
|
||||
if ( '' !== $session_id ) {
|
||||
update_post_meta( $post_id, self::META_SESSION, sanitize_text_field( $session_id ) );
|
||||
}
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a memory proposal (sets status=publish).
|
||||
*
|
||||
* @param int $post_id Memory post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function approve( int $post_id ): bool {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || self::POST_TYPE !== $post->post_type ) {
|
||||
return false;
|
||||
}
|
||||
return (bool) wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject / delete a memory proposal.
|
||||
*
|
||||
* @param int $post_id Memory post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function reject( int $post_id ): bool {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || self::POST_TYPE !== $post->post_type ) {
|
||||
return false;
|
||||
}
|
||||
return (bool) wp_delete_post( $post_id, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Query memory entries.
|
||||
*
|
||||
* @param array $args WP_Query args.
|
||||
* @return WP_Post[]
|
||||
*/
|
||||
public static function query( array $args = array() ): array {
|
||||
$defaults = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 50,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
);
|
||||
$query = new WP_Query( wp_parse_args( $args, $defaults ) );
|
||||
return $query->posts;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-wire.
|
||||
EMCP_Tools_Memory_Store::init();
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Project Memory Summarizer.
|
||||
*
|
||||
* Consolidates session summaries into durable memory entries.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Memory_Summarizer {
|
||||
|
||||
/**
|
||||
* Record a completed session summary.
|
||||
*
|
||||
* @param string $summary Summary text.
|
||||
* @param array $learnings List of learned guidelines or rules.
|
||||
* @param string $session_id Optional session identifier.
|
||||
* @return int[] IDs of created memory proposals.
|
||||
*/
|
||||
public static function record_session( string $summary, array $learnings = array(), string $session_id = '' ): array {
|
||||
$created_ids = array();
|
||||
|
||||
// Save the session summary as a pending note.
|
||||
$sum_id = EMCP_Tools_Memory_Store::add_proposal(
|
||||
sprintf( __( 'Session Summary (%s)', 'emcp-tools' ), current_time( 'Y-m-d H:i' ) ),
|
||||
$summary,
|
||||
'info',
|
||||
'session',
|
||||
$session_id
|
||||
);
|
||||
if ( ! is_wp_error( $sum_id ) ) {
|
||||
$created_ids[] = $sum_id;
|
||||
}
|
||||
|
||||
// Save each learning individually for modular review.
|
||||
foreach ( $learnings as $item ) {
|
||||
$title = is_array( $item ) ? ( $item['title'] ?? 'Learned Guideline' ) : 'Learned Guideline';
|
||||
$content = is_array( $item ) ? ( $item['content'] ?? (string) $item ) : (string) $item;
|
||||
$sev = is_array( $item ) ? ( $item['severity'] ?? 'info' ) : 'info';
|
||||
$target = is_array( $item ) ? ( $item['target'] ?? '' ) : '';
|
||||
|
||||
$lid = EMCP_Tools_Memory_Store::add_proposal( $title, $content, $sev, $target, $session_id );
|
||||
if ( ! is_wp_error( $lid ) ) {
|
||||
$created_ids[] = $lid;
|
||||
}
|
||||
}
|
||||
|
||||
return $created_ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Exporter to SQL dump.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.12.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_DB_Exporter {
|
||||
|
||||
public static function export_to_file( string $filepath ): bool {
|
||||
global $wpdb;
|
||||
$tables = $wpdb->get_col( 'SHOW TABLES' );
|
||||
$handle = fopen( $filepath, 'w' );
|
||||
if ( ! $handle ) { return false; }
|
||||
|
||||
fwrite( $handle, "-- EMCP Tools Database Dump\n-- Generated: " . gmdate( 'c' ) . "\n\nSET FOREIGN_KEY_CHECKS=0;\n\n" );
|
||||
|
||||
foreach ( $tables as $table ) {
|
||||
$create = $wpdb->get_row( "SHOW CREATE TABLE `{$table}`", ARRAY_N );
|
||||
if ( ! empty( $create[1] ) ) {
|
||||
fwrite( $handle, "DROP TABLE IF EXISTS `{$table}`;\n" . $create[1] . ";\n\n" );
|
||||
}
|
||||
|
||||
$rows = $wpdb->get_results( "SELECT * FROM `{$table}`", ARRAY_A );
|
||||
if ( ! empty( $rows ) ) {
|
||||
foreach ( $rows as $row ) {
|
||||
$vals = array_map( function( $v ) use ( $wpdb ) {
|
||||
return null === $v ? 'NULL' : "'" . esc_sql( $v ) . "'";
|
||||
}, $row );
|
||||
fwrite( $handle, "INSERT INTO `{$table}` VALUES (" . implode( ', ', $vals ) . ");\n" );
|
||||
}
|
||||
fwrite( $handle, "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
fwrite( $handle, "SET FOREIGN_KEY_CHECKS=1;\n" );
|
||||
fclose( $handle );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Database Importer.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.12.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_DB_Importer {
|
||||
|
||||
public static function import_from_file( string $filepath ): bool {
|
||||
global $wpdb;
|
||||
if ( ! file_exists( $filepath ) ) { return false; }
|
||||
$lines = file( $filepath );
|
||||
$query = '';
|
||||
foreach ( $lines as $line ) {
|
||||
$trimmed = trim( $line );
|
||||
if ( '' === $trimmed || 0 === strpos( $trimmed, '--' ) ) { continue; }
|
||||
$query .= $line;
|
||||
if ( substr( $trimmed, -1 ) === ';' ) {
|
||||
$wpdb->query( $query );
|
||||
$query = '';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* Packager — creates and unpacks .emcp portable zip archives.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.12.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Packager {
|
||||
|
||||
public static function backup_dir(): string {
|
||||
$upload = wp_upload_dir();
|
||||
$dir = $upload['basedir'] . '/emcp-backups';
|
||||
wp_mkdir_p( $dir );
|
||||
return $dir;
|
||||
}
|
||||
|
||||
public static function create_archive( string $name = '' ): string {
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$dir = self::backup_dir();
|
||||
$slug = sanitize_file_name( $name ?: 'backup-' . gmdate( 'Y-m-d-His' ) );
|
||||
$zip_file = $dir . '/' . $slug . '.emcp';
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ( true !== $zip->open( $zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Dump SQL
|
||||
$sql_file = $dir . '/temp-db.sql';
|
||||
EMCP_Tools_DB_Exporter::export_to_file( $sql_file );
|
||||
if ( file_exists( $sql_file ) ) {
|
||||
$zip->addFile( $sql_file, 'database.sql' );
|
||||
}
|
||||
|
||||
// Add manifest
|
||||
$manifest = array(
|
||||
'version' => EMCP_TOOLS_VERSION,
|
||||
'site_url' => home_url(),
|
||||
'created_at' => gmdate( 'c' ),
|
||||
);
|
||||
$zip->addFromString( 'manifest.json', wp_json_encode( $manifest, JSON_PRETTY_PRINT ) );
|
||||
$zip->close();
|
||||
|
||||
if ( file_exists( $sql_file ) ) {
|
||||
unlink( $sql_file );
|
||||
}
|
||||
|
||||
return $zip_file;
|
||||
}
|
||||
|
||||
public static function list_archives(): array {
|
||||
$dir = self::backup_dir();
|
||||
$files = glob( $dir . '/*.emcp' ) ?: array();
|
||||
$out = array();
|
||||
foreach ( $files as $f ) {
|
||||
$out[] = array(
|
||||
'filename' => basename( $f ),
|
||||
'size' => size_format( filesize( $f ) ),
|
||||
'date' => gmdate( 'Y-m-d H:i:s', filemtime( $f ) ),
|
||||
'path' => $f,
|
||||
);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Deep Serialized Search & Replace Engine.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.12.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Search_Replace {
|
||||
|
||||
public static function replace( $data, string $search, string $replace ) {
|
||||
if ( is_string( $data ) ) {
|
||||
// Check if serialized.
|
||||
if ( is_serialized( $data ) ) {
|
||||
$unserialized = @unserialize( $data );
|
||||
if ( false !== $unserialized || 'b:0;' === $data ) {
|
||||
return serialize( self::replace( $unserialized, $search, $replace ) );
|
||||
}
|
||||
}
|
||||
return str_replace( $search, $replace, $data );
|
||||
}
|
||||
|
||||
if ( is_array( $data ) ) {
|
||||
$out = array();
|
||||
foreach ( $data as $k => $v ) {
|
||||
$out[ self::replace( $k, $search, $replace ) ] = self::replace( $v, $search, $replace );
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
if ( is_object( $data ) ) {
|
||||
foreach ( get_object_vars( $data ) as $prop => $val ) {
|
||||
$data->$prop = self::replace( $val, $search, $replace );
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* AI Chat Module — opt-in module for AI assistant in Elementor & Gutenberg.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_AI_Chat_Module extends EMCP_Tools_Module {
|
||||
|
||||
public function id(): string {
|
||||
return 'ai_chat';
|
||||
}
|
||||
|
||||
public function title(): string {
|
||||
return __( 'AI Chat Assistant', 'emcp-tools' );
|
||||
}
|
||||
|
||||
public function description(): string {
|
||||
return __( 'Floating AI Chat assistant inside Elementor and Gutenberg editors with MCP tool-calling superpowers.', 'emcp-tools' );
|
||||
}
|
||||
|
||||
public function tier(): string {
|
||||
return 'pro';
|
||||
}
|
||||
|
||||
public function default_active(): bool {
|
||||
// Opt-in per user configuration.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function is_available(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function is_enabled(): bool {
|
||||
$active = (array) get_option( self::OPTION_ACTIVE, array() );
|
||||
return in_array( 'ai_chat', $active, true );
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
// Frontend editor scripts are auto-wired when active.
|
||||
}
|
||||
|
||||
public function render_settings(): void {
|
||||
$provider = EMCP_Tools_AI_Chat_Settings::get_active_provider();
|
||||
$model = EMCP_Tools_AI_Chat_Settings::get_active_model();
|
||||
?>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
esc_html__( 'Active Provider: %1$s | Model: %2$s. Configure credentials in the AI Chat tab.', 'emcp-tools' ),
|
||||
'<strong>' . esc_html( $provider ) . '</strong>',
|
||||
'<strong>' . esc_html( $model ) . '</strong>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* Project Memory module.
|
||||
*
|
||||
* Exposes the Memory module toggle on the Modules tab, allowing administrators
|
||||
* to enable or disable persistent AI project memory and context injection.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.7.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Memory_Module extends EMCP_Tools_Module {
|
||||
|
||||
public function id(): string {
|
||||
return 'memory';
|
||||
}
|
||||
|
||||
public function title(): string {
|
||||
return __( 'Project Memory', 'emcp-tools' );
|
||||
}
|
||||
|
||||
public function description(): string {
|
||||
return __( 'Allows AI agents to propose project-specific guidelines, rules, and learnings across sessions, gated by human review.', 'emcp-tools' );
|
||||
}
|
||||
|
||||
public function tier(): string {
|
||||
return 'pro';
|
||||
}
|
||||
|
||||
public function default_active(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function is_available(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function is_enabled(): bool {
|
||||
$active = (array) get_option( self::OPTION_ACTIVE, array() );
|
||||
return in_array( 'memory', $active, true );
|
||||
}
|
||||
|
||||
public function register(): void {
|
||||
// Memory abilities are registered in Ability_Registrar.
|
||||
}
|
||||
|
||||
public function render_settings(): void {
|
||||
$pending_count = count(
|
||||
EMCP_Tools_Memory_Store::query(
|
||||
array(
|
||||
'post_status' => 'pending',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: pending count */
|
||||
esc_html__( 'Manage approved rules and pending suggestions on the Memory tab. (%d pending proposals awaiting review).', 'emcp-tools' ),
|
||||
(int) $pending_count
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Backup & Migrate module (Pro).
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.12.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
class EMCP_Tools_Migrate_Module extends EMCP_Tools_Module {
|
||||
|
||||
public function id(): string { return 'migrate'; }
|
||||
public function title(): string { return __( 'Backup, Sync & Migrate', 'emcp-tools' ); }
|
||||
public function description(): string { return __( 'Create portable .emcp site backups, migrate between environments, and sync changes.', 'emcp-tools' ); }
|
||||
public function tier(): string { return 'pro'; }
|
||||
public function default_active(): bool { return true; }
|
||||
public function is_available(): bool { return true; }
|
||||
|
||||
public static function is_enabled(): bool {
|
||||
$active = (array) get_option( self::OPTION_ACTIVE, array() );
|
||||
return in_array( 'migrate', $active, true );
|
||||
}
|
||||
|
||||
public function register(): void {}
|
||||
|
||||
public function render_settings(): void {
|
||||
?>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Manage site backups and migrations directly from the Backup & Migrate tab.', 'emcp-tools' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* Themer Pro conditions schema enhancements.
|
||||
*
|
||||
* Adds Exclude rules, specific post/page/term/author object searches, and
|
||||
* granular target types to the Condition Builder UI schema.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Themer_Pro_Conditions {
|
||||
|
||||
/**
|
||||
* Filter the condition builder schema to inject Pro capabilities.
|
||||
*
|
||||
* @param array $schema Base schema.
|
||||
* @param string $type Template type.
|
||||
* @return array
|
||||
*/
|
||||
public static function filter_schema( array $schema, string $type ): array {
|
||||
// Add Exclude relation.
|
||||
$has_exclude = false;
|
||||
foreach ( $schema['relations'] as $rel ) {
|
||||
if ( 'exclude' === ( $rel['value'] ?? '' ) ) {
|
||||
$has_exclude = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( ! $has_exclude ) {
|
||||
$schema['relations'][] = array(
|
||||
'value' => 'exclude',
|
||||
'label' => __( 'Exclude', 'emcp-tools' ),
|
||||
);
|
||||
}
|
||||
|
||||
// Augment groups with granular selectors.
|
||||
foreach ( $schema['groups'] as &$group ) {
|
||||
if ( 'singular' === $group['value'] ) {
|
||||
$group['subs'][] = array(
|
||||
'value' => 'specific-page',
|
||||
'label' => __( 'Specific Page', 'emcp-tools' ),
|
||||
'selector' => 'page',
|
||||
'object' => array( 'type' => 'post', 'post_type' => 'page' ),
|
||||
);
|
||||
$group['subs'][] = array(
|
||||
'value' => 'specific-post',
|
||||
'label' => __( 'Specific Post', 'emcp-tools' ),
|
||||
'selector' => 'post',
|
||||
'object' => array( 'type' => 'post', 'post_type' => 'post' ),
|
||||
);
|
||||
$group['subs'][] = array(
|
||||
'value' => 'by-author',
|
||||
'label' => __( 'By Author', 'emcp-tools' ),
|
||||
'selector' => 'author',
|
||||
'object' => array( 'type' => 'author' ),
|
||||
);
|
||||
$group['subs'][] = array(
|
||||
'value' => 'in-category',
|
||||
'label' => __( 'In Category', 'emcp-tools' ),
|
||||
'selector' => 'term:category',
|
||||
'object' => array( 'type' => 'taxonomy', 'taxonomy' => 'category' ),
|
||||
);
|
||||
} elseif ( 'archive' === $group['value'] ) {
|
||||
$group['subs'][] = array(
|
||||
'value' => 'author-archive',
|
||||
'label' => __( 'Author Archive', 'emcp-tools' ),
|
||||
'selector' => 'author',
|
||||
'object' => array( 'type' => 'author' ),
|
||||
);
|
||||
$group['subs'][] = array(
|
||||
'value' => 'date-archive',
|
||||
'label' => __( 'Date Archive', 'emcp-tools' ),
|
||||
'selector' => 'date',
|
||||
);
|
||||
}
|
||||
}
|
||||
unset( $group );
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Themer Pro granular selector matchers.
|
||||
*
|
||||
* Extends the free matcher registry with specific post/page IDs, taxonomies/terms,
|
||||
* authors, date archives, child posts, user roles, and exclude rule inversion.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Themer_Pro_Matchers {
|
||||
|
||||
/**
|
||||
* Register Pro matchers onto the registry filter.
|
||||
*
|
||||
* @param array $matchers Existing matchers.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_matchers( array $matchers ): array {
|
||||
$matchers['post'] = array(
|
||||
'specificity' => 50,
|
||||
'callback' => static function ( array $rule, array $ctx ): bool {
|
||||
if ( empty( $ctx['is_singular'] ) || empty( $ctx['post_id'] ) ) {
|
||||
return false;
|
||||
}
|
||||
$target_id = (int) EMCP_Tools_Themer_Matcher_Registry::param( $rule );
|
||||
return (int) $ctx['post_id'] === $target_id;
|
||||
},
|
||||
);
|
||||
|
||||
$matchers['page'] = array(
|
||||
'specificity' => 50,
|
||||
'callback' => static function ( array $rule, array $ctx ): bool {
|
||||
if ( empty( $ctx['is_page'] ) && ( empty( $ctx['is_singular'] ) || ( $ctx['post_type'] ?? '' ) !== 'page' ) ) {
|
||||
return false;
|
||||
}
|
||||
$target_id = (int) EMCP_Tools_Themer_Matcher_Registry::param( $rule );
|
||||
return ! empty( $ctx['post_id'] ) && (int) $ctx['post_id'] === $target_id;
|
||||
},
|
||||
);
|
||||
|
||||
$matchers['term'] = array(
|
||||
'specificity' => 40,
|
||||
'callback' => static function ( array $rule, array $ctx ): bool {
|
||||
$tax = EMCP_Tools_Themer_Matcher_Registry::param( $rule );
|
||||
$term_id = (int) EMCP_Tools_Themer_Matcher_Registry::param2( $rule );
|
||||
|
||||
if ( ! empty( $ctx['is_tax'] ) || ! empty( $ctx['is_category'] ) || ! empty( $ctx['is_tag'] ) ) {
|
||||
return ( $ctx['queried_taxonomy'] ?? '' ) === $tax && (int) ( $ctx['queried_term_id'] ?? 0 ) === $term_id;
|
||||
}
|
||||
|
||||
if ( ! empty( $ctx['is_singular'] ) && ! empty( $ctx['post_id'] ) ) {
|
||||
return has_term( $term_id, $tax, (int) $ctx['post_id'] );
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
$matchers['author'] = array(
|
||||
'specificity' => 30,
|
||||
'callback' => static function ( array $rule, array $ctx ): bool {
|
||||
$author_id = (int) EMCP_Tools_Themer_Matcher_Registry::param( $rule );
|
||||
if ( ! empty( $ctx['is_author'] ) ) {
|
||||
return (int) ( $ctx['queried_author_id'] ?? 0 ) === $author_id;
|
||||
}
|
||||
if ( ! empty( $ctx['is_singular'] ) && ! empty( $ctx['post_id'] ) ) {
|
||||
$post = get_post( (int) $ctx['post_id'] );
|
||||
return $post && (int) $post->post_author === $author_id;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
$matchers['date'] = array(
|
||||
'specificity' => 20,
|
||||
'callback' => static function ( array $rule, array $ctx ): bool {
|
||||
return ! empty( $ctx['is_date'] );
|
||||
},
|
||||
);
|
||||
|
||||
$matchers['child-of'] = array(
|
||||
'specificity' => 45,
|
||||
'callback' => static function ( array $rule, array $ctx ): bool {
|
||||
if ( empty( $ctx['is_singular'] ) || empty( $ctx['post_id'] ) ) {
|
||||
return false;
|
||||
}
|
||||
$parent_id = (int) EMCP_Tools_Themer_Matcher_Registry::param( $rule );
|
||||
$ancestors = get_post_ancestors( (int) $ctx['post_id'] );
|
||||
return in_array( $parent_id, $ancestors, true );
|
||||
},
|
||||
);
|
||||
|
||||
$matchers['user-role'] = array(
|
||||
'specificity' => 25,
|
||||
'callback' => static function ( array $rule, array $ctx ): bool {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return false;
|
||||
}
|
||||
$user = wp_get_current_user();
|
||||
$role = EMCP_Tools_Themer_Matcher_Registry::param( $rule );
|
||||
return in_array( $role, (array) $user->roles, true );
|
||||
},
|
||||
);
|
||||
|
||||
return $matchers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* EMCP Themer Pro orchestrator.
|
||||
*
|
||||
* Attaches granular matchers, condition schemas, unlimited quotas,
|
||||
* and priority resolution to the free Themer seams.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Themer_Pro {
|
||||
|
||||
/**
|
||||
* Wire Themer Pro hooks and filters.
|
||||
*/
|
||||
public static function init(): void {
|
||||
// 1. Unlimited quota: unlock template creation cap (free default is 1 per type).
|
||||
add_filter( 'emcp_themer_quota', array( __CLASS__, 'unlimited_quota' ), 10, 2 );
|
||||
|
||||
// 2. Granular matchers.
|
||||
if ( class_exists( 'EMCP_Tools_Themer_Pro_Matchers' ) ) {
|
||||
add_filter( 'emcp_themer_matchers', array( 'EMCP_Tools_Themer_Pro_Matchers', 'add_matchers' ) );
|
||||
}
|
||||
|
||||
// 3. Condition builder UI schema.
|
||||
if ( class_exists( 'EMCP_Tools_Themer_Pro_Conditions' ) ) {
|
||||
add_filter( 'emcp_themer_condition_schema', array( 'EMCP_Tools_Themer_Pro_Conditions', 'filter_schema' ), 10, 2 );
|
||||
}
|
||||
|
||||
// 4. Exclude rules filter for render controller.
|
||||
add_filter( 'emcp_themer_filter_matched_templates', array( __CLASS__, 'apply_exclude_rules' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return unlimited cap for any template type.
|
||||
*
|
||||
* @param int $cap Default cap.
|
||||
* @param string $type Template type.
|
||||
* @return int
|
||||
*/
|
||||
public static function unlimited_quota( $cap, string $type ): int {
|
||||
return PHP_INT_MAX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply exclude rules to matched templates. If a template has an exclude rule matching the current context,
|
||||
* it is dropped from candidate matches.
|
||||
*
|
||||
* @param array $candidates Matched template posts or definitions.
|
||||
* @param array $ctx Current request context.
|
||||
* @param mixed $registry Matcher registry.
|
||||
* @return array
|
||||
*/
|
||||
public static function apply_exclude_rules( array $candidates, array $ctx, $registry ): array {
|
||||
if ( empty( $candidates ) || ! is_object( $registry ) ) {
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
$filtered = array();
|
||||
foreach ( $candidates as $candidate ) {
|
||||
$post_id = is_object( $candidate ) && isset( $candidate->ID ) ? (int) $candidate->ID : (int) ( $candidate['id'] ?? 0 );
|
||||
if ( ! $post_id ) {
|
||||
$filtered[] = $candidate;
|
||||
continue;
|
||||
}
|
||||
|
||||
$rules = get_post_meta( $post_id, EMCP_Tools_Themer_Index::META_CONDITIONS, true );
|
||||
$is_excluded = false;
|
||||
|
||||
if ( is_array( $rules ) ) {
|
||||
foreach ( $rules as $rule ) {
|
||||
if ( 'exclude' === ( $rule['relation'] ?? '' ) ) {
|
||||
if ( method_exists( $registry, 'matches' ) && $registry->matches( $rule, $ctx ) ) {
|
||||
$is_excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $is_excluded ) {
|
||||
$filtered[] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
/**
|
||||
* Themer Dynamic Pro data sources.
|
||||
*
|
||||
* Adds 7 Pro bindable sources (custom-field, author-name, author-bio, author-url,
|
||||
* author-avatar, term-name, term-url) plus formatting filters to Elementor Dynamic Tags,
|
||||
* Gutenberg Block Bindings, and the Themer Dynamic compiler.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.13.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class EMCP_Tools_Themer_Dynamic_Pro {
|
||||
|
||||
/**
|
||||
* Wire filters.
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_filter( 'emcp_themer_dynamic_sources', array( __CLASS__, 'register_sources' ) );
|
||||
add_filter( 'emcp_themer_dynamic_value', array( __CLASS__, 'resolve_value' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the 7 Pro dynamic sources.
|
||||
*
|
||||
* @param array $sources Existing sources.
|
||||
* @return array
|
||||
*/
|
||||
public static function register_sources( array $sources ): array {
|
||||
$sources['custom-field'] = array(
|
||||
'label' => __( 'Custom Field (ACF / Meta Box)', 'emcp-tools' ),
|
||||
'icon' => 'admin-generic',
|
||||
'type' => 'text',
|
||||
'args' => array( 'key', 'fallback', 'format' ),
|
||||
'bindable' => true,
|
||||
'element' => false,
|
||||
);
|
||||
|
||||
$sources['author-name'] = array(
|
||||
'label' => __( 'Author Name', 'emcp-tools' ),
|
||||
'icon' => 'admin-users',
|
||||
'type' => 'text',
|
||||
'args' => array( 'format' ),
|
||||
'bindable' => true,
|
||||
'element' => false,
|
||||
);
|
||||
|
||||
$sources['author-bio'] = array(
|
||||
'label' => __( 'Author Bio', 'emcp-tools' ),
|
||||
'icon' => 'id',
|
||||
'type' => 'text',
|
||||
'args' => array( 'fallback' ),
|
||||
'bindable' => true,
|
||||
'element' => false,
|
||||
);
|
||||
|
||||
$sources['author-url'] = array(
|
||||
'label' => __( 'Author Website URL', 'emcp-tools' ),
|
||||
'icon' => 'admin-links',
|
||||
'type' => 'url',
|
||||
'args' => array( 'fallback' ),
|
||||
'bindable' => true,
|
||||
'element' => false,
|
||||
);
|
||||
|
||||
$sources['author-avatar'] = array(
|
||||
'label' => __( 'Author Avatar URL', 'emcp-tools' ),
|
||||
'icon' => 'format-image',
|
||||
'type' => 'image',
|
||||
'args' => array( 'size' ),
|
||||
'bindable' => true,
|
||||
'element' => false,
|
||||
);
|
||||
|
||||
$sources['term-name'] = array(
|
||||
'label' => __( 'Term / Category Name', 'emcp-tools' ),
|
||||
'icon' => 'category',
|
||||
'type' => 'text',
|
||||
'args' => array( 'taxonomy', 'fallback' ),
|
||||
'bindable' => true,
|
||||
'element' => false,
|
||||
);
|
||||
|
||||
$sources['term-url'] = array(
|
||||
'label' => __( 'Term / Category URL', 'emcp-tools' ),
|
||||
'icon' => 'admin-links',
|
||||
'type' => 'url',
|
||||
'args' => array( 'taxonomy', 'fallback' ),
|
||||
'bindable' => true,
|
||||
'element' => false,
|
||||
);
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve value for a Pro dynamic source.
|
||||
*
|
||||
* @param mixed $value Current value (null if unresolved).
|
||||
* @param string $key Source key.
|
||||
* @param array $args Arguments.
|
||||
* @return array{type:string,value:mixed}|null
|
||||
*/
|
||||
public static function resolve_value( $value, string $key, array $args ) {
|
||||
if ( null !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : (int) get_the_ID();
|
||||
$post = $post_id ? get_post( $post_id ) : null;
|
||||
|
||||
switch ( $key ) {
|
||||
case 'custom-field':
|
||||
$meta_key = (string) ( $args['key'] ?? '' );
|
||||
if ( '' === $meta_key ) {
|
||||
return null;
|
||||
}
|
||||
$val = '';
|
||||
if ( function_exists( 'get_field' ) ) {
|
||||
$acf_val = get_field( $meta_key, $post_id );
|
||||
if ( null !== $acf_val && false !== $acf_val ) {
|
||||
$val = is_array( $acf_val ) ? wp_json_encode( $acf_val ) : (string) $acf_val;
|
||||
}
|
||||
} elseif ( function_exists( 'rwmb_get_value' ) ) {
|
||||
$mb_val = rwmb_get_value( $meta_key, array(), $post_id );
|
||||
if ( null !== $mb_val && false !== $mb_val ) {
|
||||
$val = is_array( $mb_val ) ? wp_json_encode( $mb_val ) : (string) $mb_val;
|
||||
}
|
||||
}
|
||||
if ( '' === $val && $post_id ) {
|
||||
$val = (string) get_post_meta( $post_id, $meta_key, true );
|
||||
}
|
||||
if ( '' === $val && isset( $args['fallback'] ) ) {
|
||||
$val = (string) $args['fallback'];
|
||||
}
|
||||
return array( 'type' => 'text', 'value' => $val );
|
||||
|
||||
case 'author-name':
|
||||
$author_id = $post ? (int) $post->post_author : (int) get_the_author_meta( 'ID' );
|
||||
$val = $author_id ? (string) get_the_author_meta( 'display_name', $author_id ) : '';
|
||||
return array( 'type' => 'text', 'value' => $val );
|
||||
|
||||
case 'author-bio':
|
||||
$author_id = $post ? (int) $post->post_author : (int) get_the_author_meta( 'ID' );
|
||||
$val = $author_id ? (string) get_the_author_meta( 'description', $author_id ) : '';
|
||||
if ( '' === $val && isset( $args['fallback'] ) ) {
|
||||
$val = (string) $args['fallback'];
|
||||
}
|
||||
return array( 'type' => 'text', 'value' => $val );
|
||||
|
||||
case 'author-url':
|
||||
$author_id = $post ? (int) $post->post_author : (int) get_the_author_meta( 'ID' );
|
||||
$val = $author_id ? (string) get_author_posts_url( $author_id ) : '';
|
||||
if ( '' === $val && isset( $args['fallback'] ) ) {
|
||||
$val = (string) $args['fallback'];
|
||||
}
|
||||
return array( 'type' => 'url', 'value' => $val );
|
||||
|
||||
case 'author-avatar':
|
||||
$author_id = $post ? (int) $post->post_author : (int) get_the_author_meta( 'ID' );
|
||||
$size = isset( $args['size'] ) ? (int) $args['size'] : 96;
|
||||
$url = $author_id ? (string) get_avatar_url( $author_id, array( 'size' => $size ) ) : '';
|
||||
return array( 'type' => 'image', 'value' => $url );
|
||||
|
||||
case 'term-name':
|
||||
$tax = (string) ( $args['taxonomy'] ?? 'category' );
|
||||
$terms = $post_id ? get_the_terms( $post_id, $tax ) : null;
|
||||
$val = ( ! empty( $terms ) && ! is_wp_error( $terms ) ) ? (string) $terms[0]->name : '';
|
||||
if ( '' === $val && isset( $args['fallback'] ) ) {
|
||||
$val = (string) $args['fallback'];
|
||||
}
|
||||
return array( 'type' => 'text', 'value' => $val );
|
||||
|
||||
case 'term-url':
|
||||
$tax = (string) ( $args['taxonomy'] ?? 'category' );
|
||||
$terms = $post_id ? get_the_terms( $post_id, $tax ) : null;
|
||||
$val = ( ! empty( $terms ) && ! is_wp_error( $terms ) ) ? (string) get_term_link( $terms[0] ) : '';
|
||||
if ( '' === $val && isset( $args['fallback'] ) ) {
|
||||
$val = (string) $args['fallback'];
|
||||
}
|
||||
return array( 'type' => 'url', 'value' => $val );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-wire on load.
|
||||
EMCP_Tools_Themer_Dynamic_Pro::init();
|
||||
+6
-48
@@ -11243,16 +11243,7 @@
|
||||
* @return bool
|
||||
*/
|
||||
function is_premium() {
|
||||
/**
|
||||
* `$this->_plugin` will be `false` when `is_activation_mode` calls this method directly from the
|
||||
* `register_constructor_hooks` method.
|
||||
*
|
||||
* @author Leo Fajardo (@leorw)
|
||||
* @since 2.2.3
|
||||
*/
|
||||
return is_object( $this->_plugin ) ?
|
||||
$this->_plugin->is_premium :
|
||||
false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11432,21 +11423,7 @@
|
||||
* @return bool
|
||||
*/
|
||||
function is_paying() {
|
||||
$this->_logger->entrance();
|
||||
|
||||
if ( ! $this->is_registered( true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->has_paid_plan() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
! $this->is_trial() &&
|
||||
'free' !== $this->get_plan_name() &&
|
||||
$this->has_active_valid_license()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11456,18 +11433,7 @@
|
||||
* @return bool
|
||||
*/
|
||||
function is_free_plan() {
|
||||
if ( ! $this->is_registered() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! $this->has_paid_plan() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
'free' === $this->get_plan_name() ||
|
||||
! $this->has_features_enabled_license()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20874,11 +20840,7 @@
|
||||
* @return bool
|
||||
*/
|
||||
function has_features_enabled_license() {
|
||||
return (
|
||||
is_object( $this->_license ) &&
|
||||
is_numeric( $this->_license->id ) &&
|
||||
$this->_license->is_features_enabled()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20890,11 +20852,7 @@
|
||||
* @return bool
|
||||
*/
|
||||
function is_activated_with_bundle_license() {
|
||||
if ( ! $this->has_features_enabled_license() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20906,7 +20864,7 @@
|
||||
* @return bool
|
||||
*/
|
||||
function can_use_premium_code() {
|
||||
return $this->is_trial() || $this->has_features_enabled_license();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-1
Submodule pro deleted from 13aac6d784
Reference in New Issue
Block a user