| 1 |
<?php |
| 2 |
|
| 3 |
namespace Templately\Modules\ThemeBuilder; |
| 4 |
|
| 5 |
use Templately\Admin\Roles; |
| 6 |
|
| 7 |
use Elementor\Core\Base\Document; |
| 8 |
use Templately\Modules\ThemeBuilder\Managers\LocationManager; |
| 9 |
use Templately\Modules\ThemeBuilder\Managers\TemplateManager; |
| 10 |
use Templately\Modules\ThemeBuilder\Managers\ThemeCompatibility; |
| 11 |
use Templately\Modules\ThemeBuilder\Types\ThemeTemplate; |
| 12 |
use Templately\Utils\Response\AjaxResponder; |
| 13 |
use Templately\Utils\Response\ErrorCode; |
| 14 |
use Templately\Utils\Base; |
| 15 |
use Templately\Utils\Views; |
| 16 |
|
| 17 |
class ThemeBuilder extends Base { |
| 18 |
/** |
| 19 |
* Names of the theme-builder widgets shown in the editor panel. |
| 20 |
* |
| 21 |
* The widgets declare show_in_panel() => false, so they are hidden on every |
| 22 |
* document by default; elementor_document_config() re-enables them (via the |
| 23 |
* per-document panel.widgets_settings override) only for the |
| 24 |
* templately_library CPT. Keep in sync with register_elements(). |
| 25 |
*/ |
| 26 |
const PANEL_WIDGETS = [ |
| 27 |
'tl-post-title', |
| 28 |
'tl-post-content', |
| 29 |
'tl-post-fatured-image', |
| 30 |
'tl-site-logo', |
| 31 |
'tl-post-comments', |
| 32 |
]; |
| 33 |
|
| 34 |
/** |
| 35 |
* Post types that are `public` but must never be offered as display-condition |
| 36 |
* targets. |
| 37 |
* |
| 38 |
* `templately_library` is registered `public => true, show_in_nav_menus => false` |
| 39 |
* (see Source::register_post_type()), so it only stayed out of the condition |
| 40 |
* dropdowns by ACCIDENT of the old `show_in_nav_menus` gate. Widening that gate |
| 41 |
* without this list would let a template target the template library itself. |
| 42 |
* `elementor_library`, `e-floating-buttons` and `e-landing-page` are Elementor's |
| 43 |
* own builder-document CPTs (all `public => true`), and `attachment` has no |
| 44 |
* meaningful theme-builder use. None of these are pages a visitor browses, so |
| 45 |
* none belongs in a display condition. |
| 46 |
*/ |
| 47 |
const EXCLUDED_POST_TYPES = [ |
| 48 |
Source::CPT, |
| 49 |
'elementor_library', |
| 50 |
'e-floating-buttons', |
| 51 |
'e-landing-page', |
| 52 |
'attachment', |
| 53 |
]; |
| 54 |
|
| 55 |
/** |
| 56 |
* @var Source |
| 57 |
*/ |
| 58 |
public static $source; |
| 59 |
|
| 60 |
/** |
| 61 |
* @var \Templately\Modules\DisplayConditions\Managers\ConditionManager|null |
| 62 |
*/ |
| 63 |
public static $conditions_manager; |
| 64 |
|
| 65 |
/** |
| 66 |
* @var TemplateManager |
| 67 |
*/ |
| 68 |
public static $templates_manager; |
| 69 |
|
| 70 |
/** |
| 71 |
* @var LocationManager |
| 72 |
*/ |
| 73 |
public static $location_manager; |
| 74 |
|
| 75 |
/** |
| 76 |
* @var ThemeCompatibility |
| 77 |
*/ |
| 78 |
public static $theme_compatibility; |
| 79 |
|
| 80 |
/** |
| 81 |
* @var PageTemplates |
| 82 |
*/ |
| 83 |
public static $page_template_module; |
| 84 |
|
| 85 |
public static $views; |
| 86 |
|
| 87 |
public function __construct() { |
| 88 |
self::$views = Views::get_instance( TEMPLATELY_VIEWS_ABSPATH ); |
| 89 |
|
| 90 |
add_action( 'pre_get_posts', [ $this, 'pre_get_posts' ], 1 ); |
| 91 |
add_action( 'wp', function () { |
| 92 |
new TemplateLoader( $this, self::$views ); |
| 93 |
} ); |
| 94 |
// Unconditional (admin + front end): the editor's admin page bootstrap |
| 95 |
// (post.php?action=elementor) never fires `wp`, so this can't be gated |
| 96 |
// behind the TemplateLoader instantiation above. See TemplateLoader:: |
| 97 |
// register_preview_product_hooks() for why two render pathways are covered. |
| 98 |
TemplateLoader::register_preview_product_hooks(); |
| 99 |
|
| 100 |
|
| 101 |
add_action( 'init', [ $this, 'source_register' ] ); |
| 102 |
add_action( 'wp_ajax_templately_create_template', [ $this, 'create_template' ] ); |
| 103 |
|
| 104 |
add_filter( 'elementor/document/config', [$this, 'elementor_document_config'], 10, 2 ); |
| 105 |
add_filter( 'elementor/documents/get/post_id', [$this, 'elementor_documents_get_post_id'] ); |
| 106 |
add_action( 'elementor/widgets/register', [ $this, 'register_elements' ] ); |
| 107 |
|
| 108 |
add_filter( 'the_content', [$this, 'filter_content'], 10 ,1); |
| 109 |
|
| 110 |
self::$theme_compatibility = new ThemeCompatibility(); |
| 111 |
self::$location_manager = new LocationManager( $this ); |
| 112 |
self::$page_template_module = new PageTemplates(); |
| 113 |
|
| 114 |
} |
| 115 |
|
| 116 |
|
| 117 |
|
| 118 |
public function pre_get_posts( $query ) { |
| 119 |
if ( ! is_admin() || ! $query->is_main_query() || empty( $query->query['post_type'] ) || $query->query['post_type'] !== 'templately_library' ) { |
| 120 |
return; |
| 121 |
} |
| 122 |
|
| 123 |
$template_types = isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : ''; |
| 124 |
|
| 125 |
if ( ! empty( $template_types ) ) { |
| 126 |
$query->set( 'meta_query', [ |
| 127 |
[ |
| 128 |
'key' => Source::TYPE_META_KEY, |
| 129 |
'value' => $template_types, |
| 130 |
] |
| 131 |
] ); |
| 132 |
} |
| 133 |
} |
| 134 |
|
| 135 |
public function source_register() { |
| 136 |
self::$templates_manager = new TemplateManager( $this ); |
| 137 |
// The ConditionManager is owned by the display-conditions module (spec 034, |
| 138 |
// extracted FROM theme-builder). It is supplied here through the |
| 139 |
// `templately_condition_manager` filter that display-conditions registers at |
| 140 |
// boot — inverting the old direct dependency so theme-builder no longer imports |
| 141 |
// the display-conditions namespace. Fires on `init`, long after |
| 142 |
// `plugins_loaded` has booted (and let the filter register) every module. |
| 143 |
self::$conditions_manager = apply_filters( 'templately_condition_manager', null, $this ); |
| 144 |
self::$source = new Source( $this ); |
| 145 |
register_rest_field('templately_library','templately_type', [ |
| 146 |
'get_callback' => [ $this, 'get_rest_template_type' ], |
| 147 |
]); |
| 148 |
} |
| 149 |
|
| 150 |
public function get_rest_template_type() { |
| 151 |
return get_post_meta( get_the_ID(), '_templately_template_type', true ); |
| 152 |
} |
| 153 |
|
| 154 |
public function create_template() { |
| 155 |
check_admin_referer( 'templately_create_template' ); |
| 156 |
|
| 157 |
// A theme-builder template controls what every visitor sees in the header, |
| 158 |
// footer, archive and single slots. `delete_posts` is held by the Author |
| 159 |
// role, which is scoped to its own post content — gate on the plugin's own |
| 160 |
// administrator-only capability instead. |
| 161 |
if ( ! Roles::can_manage_builder() ) { |
| 162 |
AjaxResponder::error(ErrorCode::FORBIDDEN); |
| 163 |
return; |
| 164 |
} |
| 165 |
|
| 166 |
// Both fields are required; reading them unguarded raises an undefined-index |
| 167 |
// warning on any malformed POST instead of failing cleanly. |
| 168 |
if ( empty( $_POST['template_type'] ) || ! isset( $_POST['title'] ) ) { |
| 169 |
wp_die( esc_html__( 'Missing required template fields.', 'templately' ) ); |
| 170 |
} |
| 171 |
|
| 172 |
$_type = sanitize_text_field( wp_unslash( $_POST['template_type'] ) ); |
| 173 |
|
| 174 |
$_meta = []; |
| 175 |
|
| 176 |
if ( isset( $_POST['meta'] ) && is_array( $_POST['meta'] ) ) { |
| 177 |
$_meta = array_map( 'sanitize_text_field', wp_unslash( $_POST['meta'] ) ); |
| 178 |
} |
| 179 |
|
| 180 |
$_post_data = [ |
| 181 |
'post_type' => 'templately_library', |
| 182 |
'post_title' => sanitize_text_field( wp_unslash( $_POST['title'] ) ) |
| 183 |
]; |
| 184 |
|
| 185 |
$template = self::$templates_manager->create( $_type, $_post_data, $_meta ); |
| 186 |
|
| 187 |
if ( is_wp_error( $template ) ) { |
| 188 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_die() renders a WP_Error itself (title/status/message); stringifying loses that. |
| 189 |
wp_die( $template ); |
| 190 |
} |
| 191 |
|
| 192 |
// wp_safe_redirect keeps the destination on an allowed host; the URL comes |
| 193 |
// from the templates manager, but there is no reason to accept an offsite one. |
| 194 |
wp_safe_redirect( $template->get_edit_url() ); |
| 195 |
die; |
| 196 |
} |
| 197 |
|
| 198 |
public function get_public_post_types(): array { |
| 199 |
// `public` (not `show_in_nav_menus`) is the right gate: whether a post type |
| 200 |
// belongs in a nav menu says nothing about whether it has templatable views. |
| 201 |
// A CPT registered `show_in_nav_menus => false` — a very common choice — |
| 202 |
// still has singles and archives the theme builder must be able to target, |
| 203 |
// and the old filter made every one of them invisible here. |
| 204 |
$post_type_args = [ |
| 205 |
'public' => true, |
| 206 |
]; |
| 207 |
|
| 208 |
$_post_types = get_post_types( $post_type_args, 'objects' ); |
| 209 |
|
| 210 |
/** |
| 211 |
* Post types never offered as theme-builder display-condition targets. |
| 212 |
* |
| 213 |
* Other builder plugins register `public` CPTs that hold DOCUMENTS rather |
| 214 |
* than visitor-facing pages; without this they would appear as meaningless |
| 215 |
* condition entries the moment the gate above widened. |
| 216 |
* |
| 217 |
* @param array $excluded Post type slugs to exclude. |
| 218 |
*/ |
| 219 |
$excluded = apply_filters( 'templately_builder_excluded_post_types', self::EXCLUDED_POST_TYPES ); |
| 220 |
|
| 221 |
$post_types = []; |
| 222 |
|
| 223 |
foreach ( $_post_types as $post_type => $object ) { |
| 224 |
if ( in_array( $post_type, (array) $excluded, true ) ) { |
| 225 |
continue; |
| 226 |
} |
| 227 |
|
| 228 |
$post_types[ $post_type ] = $object->label; |
| 229 |
} |
| 230 |
|
| 231 |
return $post_types; |
| 232 |
} |
| 233 |
|
| 234 |
public function get_template( $template_id ) { |
| 235 |
$template = self::$templates_manager->get( $template_id ); |
| 236 |
|
| 237 |
if ( ! empty( $template ) && ! $template instanceof ThemeTemplate ) { |
| 238 |
$template = null; |
| 239 |
} |
| 240 |
|
| 241 |
return $template; |
| 242 |
} |
| 243 |
|
| 244 |
/** |
| 245 |
* Save additional data for a specific post. |
| 246 |
* |
| 247 |
* This function filters the data by adding additional configuration. |
| 248 |
* External developers can use this function to add custom configuration for different posts. |
| 249 |
* It checks if the post type is not 'templately_library', then it adds a new configuration |
| 250 |
* 'theme-post-content' to the data. |
| 251 |
* |
| 252 |
* @param array $data The original data that needs to be saved. |
| 253 |
* @param int $post_id The ID of the post for which the data is being saved. |
| 254 |
* |
| 255 |
* @return array The modified data with the additional configuration. |
| 256 |
*/ |
| 257 |
public function elementor_document_config( $data, $post_id ) { |
| 258 |
if ( Source::CPT === get_post_type( $post_id ) ) { |
| 259 |
$data['panel'] = [ |
| 260 |
'widgets_settings' => array_merge( |
| 261 |
[ |
| 262 |
'theme-post-content' => [ |
| 263 |
'show_in_panel' => true, |
| 264 |
], |
| 265 |
], |
| 266 |
// Templately widgets are panel-hidden by default; surface |
| 267 |
// them only when editing a templately_library template. |
| 268 |
array_fill_keys( self::PANEL_WIDGETS, [ 'show_in_panel' => true ] ) |
| 269 |
), |
| 270 |
'elements_categories' => [ |
| 271 |
'theme-elements-archive' => [ |
| 272 |
'title' => esc_html__( 'Archive', 'elementor-pro' ), |
| 273 |
], |
| 274 |
// The Templately widget group. This merge (array_replace_recursive |
| 275 |
// in Document::get_config) can only append new category keys, so it |
| 276 |
// lands last here — the editor-JS WidgetPanelModule reorders it to |
| 277 |
// the top of the panel on document:loaded. |
| 278 |
'templately' => [ |
| 279 |
'title' => esc_html__( 'Templately', 'templately' ), |
| 280 |
'icon' => 'eicon-folder', |
| 281 |
'hideIfEmpty' => false, |
| 282 |
], |
| 283 |
], |
| 284 |
]; |
| 285 |
|
| 286 |
$conditions = self::$conditions_manager->get_conditions_for_display( $post_id ); |
| 287 |
|
| 288 |
$data['templately_builder']['conditions'] = $conditions; |
| 289 |
$data['templately_builder']['post_type'] = get_post_type( $post_id ); |
| 290 |
} |
| 291 |
|
| 292 |
return $data; |
| 293 |
} |
| 294 |
|
| 295 |
public function elementor_documents_get_post_id( $post_id ) { |
| 296 |
if ( Source::CPT === get_post_type( $post_id ) ) { |
| 297 |
add_filter( 'get_post_metadata', [ $this, 'modify_template_type' ], 10, 4 ); |
| 298 |
} |
| 299 |
|
| 300 |
return $post_id; |
| 301 |
} |
| 302 |
|
| 303 |
public function modify_template_type( $value, $object_id, $meta_key, $single ) { |
| 304 |
$_value = $value; |
| 305 |
if ( empty($value) && Document::TYPE_META_KEY === $meta_key && Source::CPT === get_post_type( $object_id ) ) { |
| 306 |
remove_filter( 'get_post_metadata', [ $this, 'modify_template_type' ] ); |
| 307 |
$value = get_post_meta( $object_id, Source::TYPE_META_KEY, $single ); |
| 308 |
$arr = [ |
| 309 |
'post' => 'single-post', |
| 310 |
'page' => 'single-page', |
| 311 |
'error' => 'error-404', |
| 312 |
'product_single' => 'product', |
| 313 |
'product_archive' => 'product-archive', |
| 314 |
'course_single' => 'single-post', |
| 315 |
'course_archive' => 'archive', |
| 316 |
]; |
| 317 |
$key = $single ? $value : (isset($value[0]) ? $value[0] : ''); |
| 318 |
$value = isset( $arr[ $key ] ) ? $arr[ $key ] : $value; |
| 319 |
if(in_array($value, ['header', 'footer'])){ |
| 320 |
return $_value; |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
return $value; |
| 325 |
} |
| 326 |
|
| 327 |
public function register_elements( $widgets_manager ) { |
| 328 |
$widgets_manager->register( new \Templately\Modules\ThemeBuilder\Widgets\Post_Title() ); |
| 329 |
$widgets_manager->register( new \Templately\Modules\ThemeBuilder\Widgets\Post_Content() ); |
| 330 |
$widgets_manager->register( new \Templately\Modules\ThemeBuilder\Widgets\Featured_Image() ); |
| 331 |
$widgets_manager->register( new \Templately\Modules\ThemeBuilder\Widgets\Site_Logo() ); |
| 332 |
$widgets_manager->register( new \Templately\Modules\ThemeBuilder\Widgets\Post_Comments() ); |
| 333 |
} |
| 334 |
|
| 335 |
public function filter_content( $content ) { |
| 336 |
if(is_singular()){ |
| 337 |
global $post; |
| 338 |
if(!empty($post) && $post->post_type === 'templately_library'){ |
| 339 |
if(in_array(get_post_meta($post->ID, '_templately_template_type', true), ['header', 'footer'])){ |
| 340 |
return ''; |
| 341 |
} |
| 342 |
} |
| 343 |
} |
| 344 |
return $content; |
| 345 |
} |
| 346 |
|
| 347 |
} |