register_routes();
});
// Initialize Shortcode Manager
Shortcode_Manager::get_instance();
// Register custom widgets with Elementor
add_action('elementor/widgets/register', [$this, 'register_custom_widgets']);
}
/**
* Register custom widgets from CPT with Elementor
*/
public function register_custom_widgets($widgets_manager) {
// Register custom categories first
$this->register_custom_categories();
// Get all published widgets
$args = [
'post_type' => 'jltma_widget',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'date',
'order' => 'DESC'
];
$widgets = get_posts($args);
if (empty($widgets)) {
return;
}
// Widgets are rendered at runtime by Dynamic_Widget — no generated or
// executed PHP. (No user input is ever written to or required as PHP.)
require_once __DIR__ . '/class-dynamic-widget.php';
foreach ($widgets as $widget_post) {
$widgets_manager->register(new Dynamic_Widget([], ['jltma_post_id' => $widget_post->ID]));
}
}
/**
* Register custom Elementor categories
*/
private function register_custom_categories() {
if (!did_action('elementor/loaded')) {
return;
}
// Get custom categories from options
$custom_categories = get_option('jltma_custom_widget_categories', []);
if (empty($custom_categories) || !is_array($custom_categories)) {
return;
}
$elements_manager = \Elementor\Plugin::$instance->elements_manager;
// Register each custom category
foreach ($custom_categories as $slug => $title) {
// Check if category doesn't already exist
$existing_categories = $elements_manager->get_categories();
if (!isset($existing_categories[$slug])) {
$elements_manager->add_category(
$slug,
[
'title' => $title,
'icon' => 'eicon-posts-ticker',
]
);
}
}
}
public function admin_redirects() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only checks of WordPress-set admin query vars for redirect flow; no form data is processed.
global $pagenow;
$target_post_type = 'jltma_widget';
$redirect_url = admin_url('edit.php?post_type=jltma_widget');
if ('post.php' === $pagenow && isset($_GET['post'])) {
if (isset($_GET['action']) && in_array($_GET['action'], ['elementor', 'trash', 'delete', 'restore', 'untrash'], true)) {
return;
}
$post_id = absint($_GET['post']);
if ($post_id && $target_post_type === get_post_type($post_id)) {
wp_safe_redirect($redirect_url);
exit;
}
}
if ('post-new.php' === $pagenow && isset($_GET['post_type'])) {
$current_post_type = sanitize_key($_GET['post_type']);
if ($target_post_type === $current_post_type) {
wp_safe_redirect($redirect_url);
exit;
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
* One-time migration. Re-sanitizes stored widget code (strips any PHP/script that
* older versions may have persisted), purges stale generated files from current and
* legacy locations, and regenerates every widget from the now-clean data. Existing
* widget posts are never deleted — only their data is scrubbed and files rebuilt.
* Runs once per version (gated by an option) inside an authorised admin request.
*/
public function maybe_migrate() {
$option_key = 'jltma_widget_builder_migrated';
$version = '3.1.2-runtime';
if (get_option($option_key) === $version) {
return;
}
if (!current_user_can('manage_options')) {
return;
}
// 1) Remove stale generated trees (current + legacy locations).
$upload = wp_upload_dir();
$base = trailingslashit($upload['basedir']);
$this->delete_directory($base . 'master_addons/widgets');
$this->delete_directory($base . 'master-addons/widget-builder/generated');
$this->delete_directory($base . 'master-addons/widget-builder/tmp');
// 2) Scrub persisted widget data, then regenerate files from clean data.
$widget_ids = get_posts([
'post_type' => 'jltma_widget',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
]);
foreach ($widget_ids as $widget_id) {
$data = get_post_meta($widget_id, '_jltma_widget_data', true);
if (is_array($data)) {
if (isset($data['html_code'])) {
$data['html_code'] = $this->scrub_html($data['html_code']);
}
if (isset($data['css_code'])) {
$data['css_code'] = $this->scrub_css($data['css_code']);
}
if (isset($data['js_code'])) {
$data['js_code'] = $this->scrub_js($data['js_code']);
}
update_post_meta($widget_id, '_jltma_widget_data', $data);
}
$generator = new Widget_Generator($widget_id);
$generator->generate();
}
// 3) Drop the orphaned option left by the old option-based builder.
delete_option('jltma_custom_widgets');
update_option($option_key, $version);
}
/**
* Strip PHP tags and inline #is', '', $code);
$code = preg_replace('#?script\b[^>]*>#i', '', $code);
return $code;
}
/**
* Strip PHP tags and