>
*/
static function definitions() {
$definitions = array();
$builders = array(
'pagelayer' => 'Pagelayer',
'elementor' => 'Elementor',
'seedprod' => 'SeedProd',
'divi' => 'Divi',
'bricks' => 'Bricks',
'beaver' => 'Beaver Builder',
'breakdance' => 'Breakdance',
);
foreach ($builders as $slug => $label) {
if (!\ONEMCP\Detector::is_active($slug)) {
continue;
}
// Pagelayer's own abilities create pages, posts, theme templates, and widgets.
if ($slug === 'pagelayer' && \OneMCPPro\Pagelayer::editor_definitions() !== array()) {
continue;
}
$definitions = array_merge($definitions, self::builder_abilities($slug, $label));
}
return $definitions;
}
/**
* List, read, and update abilities for one builder.
*
* @param string $slug Builder slug.
* @param string $label Builder name.
* @return array>
*/
private static function builder_abilities($slug, $label) {
$structure_description = sprintf(
/* translators: %s: builder name. */
__('Returns an outline of the %s elements on one document. Set include_raw to also return the document when it is small enough.', 'onemcp-pro'),
$label
);
$update_description = sprintf(
/* translators: %s: builder name. */
__('Merges settings into one %1$s widget. Use the element id from the structure outline. settings must be that widget\'s own %1$s controls. Do not invent setting names. For Divi shortcodes, the id is the module admin_label or its numeric position.', 'onemcp-pro'),
$label
);
if ($slug === 'pagelayer') {
$structure_description = __('Returns an outline of the Pagelayer elements on one document. Each id is the element pagelayer-id, or an @0.1 path when the element has no id yet. Set include_raw to also return the document when it is small enough.', 'onemcp-pro');
$update_description = __('Merges attributes into one Pagelayer element. Use the id from the structure outline. A content value replaces that element\'s inner HTML when it has no child elements.', 'onemcp-pro');
}
list($label, $structure_description, $update_description) = self::editor_copy($slug, $label, $structure_description, $update_description);
$name = self::design_slug($slug);
$abilities = array(
Helper::ability(
'list-' . $name . '-documents',
sprintf(
/* translators: %s: builder name. */
__('List %s documents', 'onemcp-pro'),
$label
),
sprintf(
/* translators: %s: builder name. */
__('Lists pages, posts, custom post types, and templates built with %s.', 'onemcp-pro'),
$label
),
self::design_category(),
true,
function ($input) use ($slug) {
return Builders::list_documents($slug, $input);
},
array(
'post_type' => Helper::string_prop(__('Limit the list to one post type, including a custom post type.', 'onemcp-pro'), 40),
'search' => Helper::string_prop(__('Search titles.', 'onemcp-pro'), 120),
'limit' => Helper::int_prop(__('How many documents to return. Maximum 50.', 'onemcp-pro')),
'page' => Helper::int_prop(__('Page number, starting at 1.', 'onemcp-pro')),
),
array(),
self::design_extra(true)
),
Helper::ability(
'get-' . $name . '-structure',
sprintf(
/* translators: %s: builder name. */
__('Get %s structure', 'onemcp-pro'),
$label
),
$structure_description,
self::design_category(),
true,
function ($input) use ($slug) {
return \OneMCPPro\Builders::get_structure($slug, $input);
},
array(
'post_id' => Helper::int_prop(__('Document ID.', 'onemcp-pro')),
'include_raw' => Helper::bool_prop(__('Include the raw builder document when it fits in a tool response.', 'onemcp-pro')),
),
array('post_id'),
self::design_extra(true)
),
Helper::ability(
'update-' . $name . '-element',
sprintf(
/* translators: %s: builder name. */
__('Update %s element', 'onemcp-pro'),
$label
),
$update_description,
self::design_category(),
false,
function ($input) use ($slug) {
return \OneMCPPro\Builders::update_element($slug, $input);
},
array(
'post_id' => Helper::int_prop(__('Document ID.', 'onemcp-pro')),
'element_id' => Helper::string_prop(__('Element id from the structure outline.', 'onemcp-pro'), 120),
'settings' => Helper::object_prop(__('Settings to merge into the element. Existing keys not included here are left as they are.', 'onemcp-pro')),
),
array('post_id', 'element_id', 'settings'),
self::design_extra(false, $slug !== 'pagelayer')
),
);
return array_merge($abilities, self::design_abilities($slug, $label));
}
/**
* List documents for one builder.
*
* @param string $slug Builder slug.
* @param array $input Input.
* @return array|WP_Error
*/
static function list_documents($slug, $input) {
$args = self::query_args($slug);
if (is_wp_error($args)) {
return $args;
}
$limit = Helper::limit(isset($input['limit']) ? $input['limit'] : 20, 20, 50);
$page = Helper::page($input);
$args['posts_per_page'] = $limit;
$args['paged'] = $page;
$args['orderby'] = 'modified';
$args['order'] = 'DESC';
if (!empty($input['search'])) {
$args['s'] = Helper::text($input['search'], 120);
}
if (!empty($input['post_type'])) {
$type = Helper::post_type($input['post_type']);
if (is_wp_error($type)) {
return $type;
}
$args['post_type'] = $type;
}
$query = Helper::query_posts($args);
$items = array();
foreach ($query->posts as $post) {
if ($post instanceof WP_Post) {
$items[] = Helper::post_row($post);
}
}
return array(
'builder' => $slug,
'items' => $items,
'total' => (int) $query->found_posts,
'page' => $page,
);
}
/**
* Outline one document.
*
* @param string $slug Builder slug.
* @param array $input Input.
* @return array|WP_Error
*/
static function get_structure($slug, $input) {
$post = self::document($slug, isset($input['post_id']) ? $input['post_id'] : 0);
if (is_wp_error($post)) {
return $post;
}
$loaded = self::load($slug, $post);
if (is_wp_error($loaded)) {
return $loaded;
}
$outline = array();
if ($slug === 'pagelayer') {
if (!empty($loaded['blocks']) && is_array($loaded['blocks'])) {
$outline = self::pagelayer_outline_blocks($loaded['blocks']);
} else {
$outline = self::pagelayer_outline_nodes($loaded['data']);
}
} elseif ($slug === 'bricks' || $slug === 'beaver') {
$outline = self::flat_outline($loaded['data']);
} elseif ($slug === 'divi' && !empty($loaded['shortcodes'])) {
$outline = self::divi_outline($post->post_content);
} else {
$outline = Tree::summarize($loaded['data'], self::tree_args($slug));
}
$response = array(
'builder' => $slug,
'post' => Helper::post_row($post),
'count' => count($outline),
'outline' => $outline,
);
if (!empty($input['include_raw'])) {
$response['document'] = Helper::raw_document($loaded['raw'], true);
}
return $response;
}
/**
* Merge settings into one element and save the document.
*
* @param string $slug Builder slug.
* @param array $input Input.
* @return array|WP_Error
*/
static function update_element($slug, $input) {
$post = self::document($slug, isset($input['post_id']) ? $input['post_id'] : 0);
if (is_wp_error($post)) {
return $post;
}
$element_id = Helper::text(isset($input['element_id']) ? $input['element_id'] : '', 120);
$settings = Helper::clean_settings(isset($input['settings']) ? $input['settings'] : array());
if ($element_id === '' || !is_array($settings) || $settings === array()) {
return Helper::error('onemcp_invalid', __('Provide an element id and at least one setting.', 'onemcp-pro'));
}
$loaded = self::load($slug, $post);
if (is_wp_error($loaded)) {
return $loaded;
}
if ($slug === 'pagelayer') {
return self::pagelayer_update_document($post, $element_id, $settings, $loaded);
}
if ($slug === 'divi' && !empty($loaded['shortcodes'])) {
$updated = self::update_divi_shortcode($post->post_content, $element_id, $settings);
if (is_wp_error($updated)) {
return $updated;
}
$result = wp_update_post(wp_slash(array(
'ID' => $post->ID,
'post_content' => $updated,
)), true);
if (is_wp_error($result)) {
return $result;
}
return array(
'builder' => $slug,
'post_id' => (int) $post->ID,
'element_id' => $element_id,
'updated' => true,
);
}
$data = $loaded['data'];
$found = false;
if ($slug === 'bricks') {
self::update_bricks($data, $element_id, $settings, $found);
} elseif ($slug === 'beaver') {
self::update_beaver($data, $element_id, $settings, $found);
} else {
self::merge_tree($data, $element_id, $settings, $found);
}
if (!$found) {
return Helper::error('onemcp_not_found', __('That element was not found in the document.', 'onemcp-pro'));
}
$saved = self::save($slug, $post, $data, $loaded);
if (is_wp_error($saved)) {
return $saved;
}
return array(
'builder' => $slug,
'post_id' => (int) $post->ID,
'element_id' => $element_id,
'updated' => true,
);
}
/**
* WP_Query arguments that find one builder's documents.
*
* @param string $slug Builder slug.
* @return array|WP_Error
*/
private static function query_args($slug) {
$args = array(
'post_status' => array('publish', 'draft', 'pending', 'private', 'future'),
'post_type' => 'any',
);
if ($slug === 'pagelayer') {
$args['meta_key'] = 'pagelayer-data';
$args['meta_compare'] = 'EXISTS';
return $args;
}
if ($slug === 'elementor') {
$args['meta_key'] = '_elementor_edit_mode';
$args['meta_value'] = 'builder';
return $args;
}
if ($slug === 'seedprod') {
if (post_type_exists('seedprod')) {
$args['post_type'] = 'seedprod';
return $args;
}
$args['meta_key'] = '_seedprod_page';
return $args;
}
if ($slug === 'divi') {
$args['meta_query'] = array(
array(
'key' => '_et_pb_use_builder',
'value' => 'on',
),
);
return $args;
}
if ($slug === 'bricks') {
$args['meta_query'] = array(
'relation' => 'OR',
array(
'key' => '_bricks_editor_mode',
'value' => 'bricks',
),
array(
'key' => '_bricks_page_content_2',
'compare' => 'EXISTS',
),
);
return $args;
}
if ($slug === 'beaver') {
$args['meta_key'] = '_fl_builder_enabled';
$args['meta_value'] = '1';
return $args;
}
if ($slug === 'breakdance') {
$args['meta_key'] = '_breakdance_data';
$args['meta_compare'] = 'EXISTS';
return $args;
}
return Helper::error('onemcp_invalid', __('That builder is not supported.', 'onemcp-pro'));
}
/**
* A document the current user can edit.
*
* @param string $slug Builder slug.
* @param mixed $post_id Post ID.
* @return WP_Post|WP_Error
*/
private static function document($slug, $post_id) {
$post = Helper::editable_post($post_id);
if (is_wp_error($post)) {
return $post;
}
if (!self::belongs($slug, $post)) {
return Helper::error('onemcp_not_builder', __('This content is not a document for that builder.', 'onemcp-pro'));
}
return $post;
}
/**
* Whether a post stores data for this builder.
*
* @param string $slug Builder slug.
* @param WP_Post $post Document.
* @return bool
*/
private static function belongs($slug, $post) {
if ($slug === 'pagelayer') {
$meta = get_post_meta($post->ID, 'pagelayer-data', true);
if ($meta !== '' && $meta !== null && $meta !== false && $meta !== array()) {
return true;
}
if ($post->post_type === 'pagelayer-template') {
return true;
}
$content = (string) $post->post_content;
return strpos($content, 'pagelayer/') !== false || strpos($content, '[pl_') !== false;
}
if ($slug === 'elementor') {
return get_post_meta($post->ID, '_elementor_edit_mode', true) === 'builder';
}
if ($slug === 'seedprod') {
return $post->post_type === 'seedprod' || get_post_meta($post->ID, '_seedprod_page', true) !== '';
}
if ($slug === 'divi') {
return get_post_meta($post->ID, '_et_pb_use_builder', true) === 'on';
}
if ($slug === 'bricks') {
$mode = get_post_meta($post->ID, '_bricks_editor_mode', true);
$data = get_post_meta($post->ID, '_bricks_page_content_2', true);
return $mode === 'bricks' || (is_array($data) && $data !== array());
}
if ($slug === 'beaver') {
return (string) get_post_meta($post->ID, '_fl_builder_enabled', true) === '1';
}
if ($slug === 'breakdance') {
$data = get_post_meta($post->ID, '_breakdance_data', true);
return $data !== '' && $data !== array() && $data !== null;
}
return false;
}
/**
* Load builder data.
*
* @param string $slug Builder slug.
* @param WP_Post $post Document.
* @return array|WP_Error
*/
private static function load($slug, $post) {
if ($slug === 'pagelayer') {
return self::load_pagelayer($post);
}
if ($slug === 'elementor') {
$raw = get_post_meta($post->ID, '_elementor_data', true);
$data = is_array($raw) ? $raw : json_decode((string) $raw, true);
if (!is_array($data)) {
return Helper::error('onemcp_empty', __('This Elementor document has no data.', 'onemcp-pro'));
}
return array('data' => $data, 'raw' => $data);
}
if ($slug === 'seedprod') {
// Current SeedProd stores the builder JSON in post_content_filtered.
// _seedprod_page is usually the flag "1". Older copies keep the JSON in that meta.
$data = Helper::decode_meta($post->post_content_filtered);
$source = 'filtered';
if ($data === array()) {
$data = Helper::decode_meta(get_post_meta($post->ID, '_seedprod_page', true));
$source = 'meta';
}
if ($data === array()) {
return Helper::error('onemcp_empty', __('This SeedProd document has no data.', 'onemcp-pro'));
}
$tree = isset($data['document']) && is_array($data['document']) ? $data['document'] : $data;
return array(
'data' => $tree,
'raw' => $data,
'wrapped' => isset($data['document']),
'source' => $source,
);
}
if ($slug === 'divi') {
$content = (string) $post->post_content;
$trim = ltrim($content);
if ($trim !== '' && ($trim[0] === '{' || $trim[0] === '[')) {
$json = json_decode($content, true);
if (is_array($json)) {
return array('data' => $json, 'raw' => $json, 'shortcodes' => false);
}
}
if (strpos($content, '[et_pb_') === false) {
return Helper::error('onemcp_empty', __('This Divi document has no modules.', 'onemcp-pro'));
}
return array('data' => array(), 'raw' => $content, 'shortcodes' => true);
}
if ($slug === 'bricks') {
$data = get_post_meta($post->ID, '_bricks_page_content_2', true);
$key = '_bricks_page_content_2';
if (!is_array($data) || $data === array()) {
$data = get_post_meta($post->ID, '_bricks_page_content', true);
$key = '_bricks_page_content';
}
if (!is_array($data)) {
return Helper::error('onemcp_empty', __('This Bricks document has no data.', 'onemcp-pro'));
}
return array('data' => $data, 'raw' => $data, 'meta_key' => $key);
}
if ($slug === 'beaver') {
$data = get_post_meta($post->ID, '_fl_builder_data', true);
if (!is_array($data)) {
return Helper::error('onemcp_empty', __('This Beaver Builder document has no data.', 'onemcp-pro'));
}
return array('data' => $data, 'raw' => $data);
}
if ($slug === 'breakdance') {
$stored = get_post_meta($post->ID, '_breakdance_data', true);
$meta = is_array($stored) ? $stored : json_decode((string) $stored, true);
if (!is_array($meta)) {
return Helper::error('onemcp_empty', __('This Breakdance document has no data.', 'onemcp-pro'));
}
$tree = array();
if (!empty($meta['tree_json_string']) && is_string($meta['tree_json_string'])) {
$decoded = json_decode($meta['tree_json_string'], true);
$tree = is_array($decoded) ? $decoded : array();
} elseif (isset($meta['tree']) && is_array($meta['tree'])) {
$tree = $meta['tree'];
}
if ($tree === array()) {
return Helper::error('onemcp_empty', __('This Breakdance document has no tree.', 'onemcp-pro'));
}
return array('data' => $tree, 'raw' => $tree, 'meta' => $meta);
}
return Helper::error('onemcp_invalid', __('That builder is not supported.', 'onemcp-pro'));
}
/**
* Persist builder data.
*
* @param string $slug Builder slug.
* @param WP_Post $post Document.
* @param array $data Updated tree or element list.
* @param array $loaded Original load payload.
* @return true|WP_Error
*/
private static function save($slug, $post, $data, $loaded) {
if ($slug === 'pagelayer') {
return self::save_pagelayer($post, $data, $loaded);
}
if ($slug === 'elementor') {
$json = wp_json_encode($data);
if (!is_string($json)) {
return Helper::error('onemcp_failed', __('Elementor data could not be encoded.', 'onemcp-pro'));
}
update_post_meta($post->ID, '_elementor_data', wp_slash($json));
delete_post_meta($post->ID, '_elementor_css');
delete_post_meta($post->ID, '_elementor_element_cache');
if (class_exists('\Elementor\Plugin') && isset(\Elementor\Plugin::$instance->files_manager)) {
$files = \Elementor\Plugin::$instance->files_manager;
if (is_object($files) && method_exists($files, 'clear_cache')) {
$files->clear_cache();
}
}
return true;
}
if ($slug === 'seedprod') {
$raw = $loaded['raw'];
if (!empty($loaded['wrapped']) && is_array($raw)) {
$raw['document'] = $data;
} else {
$raw = $data;
}
$json = wp_json_encode($raw);
if (!is_string($json)) {
return Helper::error('onemcp_failed', __('SeedProd data could not be encoded.', 'onemcp-pro'));
}
if (isset($loaded['source']) && $loaded['source'] === 'meta') {
update_post_meta($post->ID, '_seedprod_page', wp_slash($json));
}
$result = wp_update_post(wp_slash(array(
'ID' => $post->ID,
'post_content_filtered' => $json,
)), true);
return is_wp_error($result) ? $result : true;
}
if ($slug === 'divi') {
$json = wp_json_encode($data);
if (!is_string($json)) {
return Helper::error('onemcp_failed', __('Divi data could not be encoded.', 'onemcp-pro'));
}
$result = wp_update_post(wp_slash(array(
'ID' => $post->ID,
'post_content' => $json,
)), true);
return is_wp_error($result) ? $result : true;
}
if ($slug === 'bricks') {
$key = isset($loaded['meta_key']) ? $loaded['meta_key'] : '_bricks_page_content_2';
update_post_meta($post->ID, $key, $data);
return true;
}
if ($slug === 'beaver') {
if (class_exists('FLBuilderModel') && method_exists('FLBuilderModel', 'update_layout_data')) {
\FLBuilderModel::update_layout_data($data, 'published', $post->ID);
return true;
}
update_post_meta($post->ID, '_fl_builder_data', $data);
return true;
}
if ($slug === 'breakdance') {
$meta = isset($loaded['meta']) && is_array($loaded['meta']) ? $loaded['meta'] : array();
$json = wp_json_encode($data);
if (!is_string($json)) {
return Helper::error('onemcp_failed', __('Breakdance data could not be encoded.', 'onemcp-pro'));
}
if (isset($meta['tree_json_string']) || !isset($meta['tree'])) {
$meta['tree_json_string'] = $json;
} else {
$meta['tree'] = $data;
}
// set_meta() stores this as a JSON string. A PHP array is rejected by the decoder.
$stored = wp_json_encode($meta);
if (!is_string($stored)) {
return Helper::error('onemcp_failed', __('Breakdance data could not be encoded.', 'onemcp-pro'));
}
update_post_meta($post->ID, '_breakdance_data', wp_slash($stored));
if (function_exists('\\Breakdance\\Render\\generateCacheForPost')) {
try {
\Breakdance\Render\generateCacheForPost($post->ID);
} catch (Throwable $exception) {
unset($exception);
}
}
do_action('breakdance_after_save_document', $post->ID);
return true;
}
return Helper::error('onemcp_invalid', __('That builder is not supported.', 'onemcp-pro'));
}
/**
* Tree walker arguments.
*
* @param string $slug Builder slug.
* @return array>
*/
private static function tree_args($slug) {
if ($slug === 'elementor') {
return array(
'id_keys' => array('id'),
'type_keys' => array('widgetType', 'elType'),
'child_keys' => array('elements'),
);
}
return array(
'id_keys' => array('id', 'element_id'),
'type_keys' => array('type', 'widgetType', 'elType', 'name'),
'child_keys' => array('elements', 'children', 'sections', 'rows', 'cols', 'blocks', 'content', 'nodes'),
);
}
/**
* Outline a flat element list used by Bricks and Beaver Builder.
*
* @param array $elements Elements.
* @return array>
*/
private static function flat_outline($elements) {
$out = array();
foreach ($elements as $key => $element) {
if (count($out) >= 200) {
break;
}
$values = is_object($element) ? get_object_vars($element) : (is_array($element) ? $element : array());
if ($values === array()) {
continue;
}
$id = '';
if (isset($values['id'])) {
$id = (string) $values['id'];
} elseif (isset($values['node'])) {
$id = (string) $values['node'];
} else {
$id = (string) $key;
}
$type = '';
if (isset($values['name'])) {
$type = (string) $values['name'];
} elseif (isset($values['type'])) {
$type = (string) $values['type'];
} elseif (isset($values['settings']->type)) {
$type = (string) $values['settings']->type;
}
$settings = array();
if (isset($values['settings'])) {
$settings = is_object($values['settings']) ? get_object_vars($values['settings']) : (array) $values['settings'];
}
$row = array(
'id' => $id,
'type' => $type,
);
if (isset($values['parent'])) {
$row['parent'] = (string) $values['parent'];
}
$text = Tree::first_text($settings);
if ($text !== '') {
$row['text'] = $text;
}
$out[] = $row;
}
return $out;
}
/**
* Outline Divi shortcodes.
*
* @param string $content Post content.
* @return array>
*/
private static function divi_outline($content) {
$out = array();
if (!preg_match_all('/\[(et_pb_[a-z0-9_]+)([^\]]*)\]/', $content, $matches, PREG_SET_ORDER)) {
return $out;
}
$index = 0;
foreach ($matches as $match) {
if (count($out) >= 200) {
break;
}
$attrs = $match[2];
$label = '';
if (preg_match('/admin_label=("|\')(.*?)\1/', $attrs, $label_match)) {
$label = $label_match[2];
}
$row = array(
'id' => $label !== '' ? $label : (string) $index,
'type' => $match[1],
'path' => (string) $index,
);
if ($label !== '') {
$row['admin_label'] = $label;
}
$out[] = $row;
$index++;
}
return $out;
}
/**
* Replace attributes, and optionally inner content, on one Divi module.
*
* @param string $content Post content.
* @param string $element_id Admin label or numeric module index.
* @param array $settings Settings.
* @return string|WP_Error
*/
private static function update_divi_shortcode($content, $element_id, $settings) {
$pattern = '/\[(et_pb_[a-z0-9_]+)([^\]]*)\]/';
if (!preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
return Helper::error('onemcp_not_found', __('No Divi modules were found.', 'onemcp-pro'));
}
$target = null;
if (preg_match('/^[0-9]+$/', $element_id)) {
$index = (int) $element_id;
if (isset($matches[0][$index])) {
$target = $index;
}
}
if ($target === null) {
foreach ($matches[2] as $index => $attr_match) {
if (preg_match('/admin_label=("|\')' . preg_quote($element_id, '/') . '\1/', $attr_match[0])) {
$target = $index;
break;
}
}
}
if ($target === null) {
return Helper::error('onemcp_not_found', __('That Divi module was not found. Use its admin_label or the numeric path from the outline.', 'onemcp-pro'));
}
$full = $matches[0][$target][0];
$offset = $matches[0][$target][1];
$tag = $matches[1][$target][0];
$attrs = $matches[2][$target][0];
$inner = null;
foreach ($settings as $key => $value) {
if (!is_string($key) || !is_scalar($value)) {
continue;
}
if ($key === 'content') {
$inner = (string) $value;
continue;
}
$attr = sanitize_key(str_replace('-', '_', $key));
if ($attr === '') {
continue;
}
$encoded = esc_attr((string) $value);
$attr_pattern = '/' . preg_quote($attr, '/') . '=("|\')(.*?)\1/';
if (preg_match($attr_pattern, $attrs)) {
$attrs = preg_replace($attr_pattern, $attr . '="' . $encoded . '"', $attrs, 1);
} else {
$attrs .= ' ' . $attr . '="' . $encoded . '"';
}
}
$opening = '[' . $tag . $attrs . ']';
$content = substr_replace($content, $opening, $offset, strlen($full));
if ($inner !== null) {
$close = '[/' . $tag . ']';
$start = $offset + strlen($opening);
$end = strpos($content, $close, $start);
if ($end === false) {
return Helper::error('onemcp_failed', __('That Divi module has no closing tag, so its inner content was not changed.', 'onemcp-pro'));
}
$content = substr_replace($content, $inner, $start, $end - $start);
}
return $content;
}
/**
* Merge settings into a nested builder tree.
*
* @param array $nodes Tree.
* @param string $element_id Element id.
* @param array $settings Settings.
* @param bool $found Set when updated.
* @param int $depth Recursion depth.
* @return void
*/
private static function merge_tree(&$nodes, $element_id, $settings, &$found, $depth = 0) {
if ($found || $depth > 30 || !is_array($nodes)) {
return;
}
if (Helper::is_list($nodes)) {
foreach ($nodes as &$node) {
self::merge_tree($node, $element_id, $settings, $found, $depth + 1);
if ($found) {
return;
}
}
unset($node);
return;
}
foreach (array('id', 'element_id') as $id_key) {
if (isset($nodes[$id_key]) && (string) $nodes[$id_key] === (string) $element_id) {
self::merge_node($nodes, $settings);
$found = true;
return;
}
}
foreach (self::tree_args('generic')['child_keys'] as $child_key) {
if (!empty($nodes[$child_key]) && is_array($nodes[$child_key])) {
self::merge_tree($nodes[$child_key], $element_id, $settings, $found, $depth + 1);
if ($found) {
return;
}
}
}
}
/**
* Merge settings onto the field the builder actually reads.
*
* @param array $node Element.
* @param array $settings Settings.
* @return void
*/
private static function merge_node(&$node, $settings) {
if (isset($node['data']['properties']) && is_array($node['data']['properties'])) {
$node['data']['properties'] = array_replace_recursive($node['data']['properties'], $settings);
return;
}
if (isset($node['props']) && is_array($node['props'])) {
$node['props'] = array_replace_recursive($node['props'], $settings);
return;
}
if (isset($node['attrs']) && is_array($node['attrs'])) {
$node['attrs'] = array_replace_recursive($node['attrs'], $settings);
return;
}
\OneMCPPro\Tree::merge_settings($node, $settings);
}
/**
* Update one Bricks element.
*
* @param array $elements Element list.
* @param string $element_id Element id.
* @param array $settings Settings.
* @param bool $found Set when updated.
* @return void
*/
private static function update_bricks(&$elements, $element_id, $settings, &$found) {
foreach ($elements as &$element) {
if (!is_array($element) || !isset($element['id']) || (string) $element['id'] !== (string) $element_id) {
continue;
}
$current = isset($element['settings']) && is_array($element['settings']) ? $element['settings'] : array();
$element['settings'] = array_merge($current, $settings);
$found = true;
return;
}
unset($element);
}
/**
* Update one Beaver Builder node.
*
* @param array $nodes Nodes keyed by id.
* @param string $element_id Node id.
* @param array $settings Settings.
* @param bool $found Set when updated.
* @return void
*/
private static function update_beaver(&$nodes, $element_id, $settings, &$found) {
foreach ($nodes as $key => $node) {
$node_id = (string) $key;
if (is_object($node) && isset($node->node)) {
$node_id = (string) $node->node;
} elseif (is_array($node) && isset($node['node'])) {
$node_id = (string) $node['node'];
}
if ($node_id !== (string) $element_id) {
continue;
}
if (is_object($node)) {
$current = isset($node->settings) ? (array) $node->settings : array();
$node->settings = (object) array_merge($current, $settings);
$nodes[$key] = $node;
} elseif (is_array($node)) {
$current = isset($node['settings']) ? (array) $node['settings'] : array();
$node['settings'] = array_merge($current, $settings);
$nodes[$key] = $node;
}
$found = true;
return;
}
}
/**
* Load a Pagelayer document.
*
* The visual editor stores Gutenberg blocks in post_content and a timestamp
* in pagelayer-data. The abilities API may store a node tree in that meta
* instead. Blocks win when both exist, because that is what the front end renders.
*
* @param WP_Post $post Document.
* @return array|WP_Error
*/
private static function load_pagelayer($post) {
$content = (string) $post->post_content;
$converted = false;
if (strpos($content, 'pagelayer/') === false && strpos($content, '[pl_') !== false && function_exists('pagelayer_do_shortcode_to_block')) {
$content = pagelayer_do_shortcode_to_block($content);
$converted = true;
}
$blocks = function_exists('parse_blocks') ? parse_blocks($content) : array();
$meta = get_post_meta($post->ID, 'pagelayer-data', true);
$tree = self::pagelayer_node_tree($meta);
if (self::pagelayer_blocks_present($blocks)) {
return array(
'data' => self::pagelayer_blocks_to_nodes($blocks),
'raw' => $blocks,
'blocks' => $blocks,
'source' => 'blocks',
'sync_meta' => $tree !== null,
'converted' => $converted,
);
}
if ($tree !== null) {
return array(
'data' => $tree,
'raw' => $tree,
'source' => 'meta',
);
}
return array(
'data' => array(),
'raw' => array(),
'blocks' => array(),
'source' => 'blocks',
);
}
/**
* Persist a Pagelayer document.
*
* @param WP_Post $post Document.
* @param array $data Updated blocks or node tree.
* @param array $loaded Original load payload.
* @return true|WP_Error
*/
private static function save_pagelayer($post, $data, $loaded) {
if (isset($loaded['source']) && $loaded['source'] === 'meta') {
update_post_meta($post->ID, 'pagelayer-data', $data);
$content = self::pagelayer_serialize_nodes($data);
$result = wp_update_post(wp_slash(array(
'ID' => $post->ID,
'post_content' => $content,
)), true);
return is_wp_error($result) ? $result : true;
}
if (!function_exists('serialize_blocks')) {
return Helper::error('onemcp_failed', __('WordPress cannot serialize Pagelayer blocks on this site.', 'onemcp-pro'));
}
$content = serialize_blocks(is_array($data) ? $data : array());
$result = wp_update_post(wp_slash(array(
'ID' => $post->ID,
'post_content' => $content,
)), true);
if (is_wp_error($result)) {
return $result;
}
if (!empty($loaded['sync_meta'])) {
update_post_meta($post->ID, 'pagelayer-data', self::pagelayer_blocks_to_nodes($data));
}
return true;
}
/**
* Update one Pagelayer element and save the document.
*
* @param WP_Post $post Document.
* @param string $element_id Element id or @path.
* @param array $settings Attributes to merge. content replaces inner HTML.
* @param array $loaded Load payload.
* @return array|WP_Error
*/
private static function pagelayer_update_document($post, $element_id, $settings, $loaded) {
$found = false;
if (isset($loaded['source']) && $loaded['source'] === 'meta') {
$data = isset($loaded['data']) && is_array($loaded['data']) ? $loaded['data'] : array();
self::pagelayer_update_nodes($data, $element_id, $settings, $found);
if (!$found) {
return Helper::error('onemcp_not_found', __('That element was not found in the document.', 'onemcp-pro'));
}
$saved = self::save('pagelayer', $post, $data, $loaded);
} else {
$blocks = isset($loaded['blocks']) && is_array($loaded['blocks']) ? $loaded['blocks'] : array();
self::pagelayer_update_blocks($blocks, $element_id, $settings, $found);
if (!$found) {
return Helper::error('onemcp_not_found', __('That element was not found in the document.', 'onemcp-pro'));
}
$saved = self::save('pagelayer', $post, $blocks, $loaded);
}
if (is_wp_error($saved)) {
return $saved;
}
return array(
'builder' => 'pagelayer',
'post_id' => (int) $post->ID,
'element_id' => $element_id,
'updated' => true,
);
}
/**
* A stored pagelayer-data value that is a layout tree, not the editor timestamp.
*
* @param mixed $meta Meta value.
* @return array|null
*/
private static function pagelayer_node_tree($meta) {
if (is_string($meta)) {
$meta = Helper::decode_meta($meta);
}
if (!is_array($meta) || $meta === array()) {
return null;
}
if (isset($meta['pagelayer_data']) && is_array($meta['pagelayer_data'])) {
$meta = $meta['pagelayer_data'];
}
$first = reset($meta);
if (!is_array($first) || empty($first['tag']) || !is_string($first['tag'])) {
return null;
}
return array_values($meta);
}
/**
* Whether parsed blocks include at least one Pagelayer block.
*
* @param array $blocks Parsed blocks.
* @return bool
*/
private static function pagelayer_blocks_present($blocks) {
if (!is_array($blocks)) {
return false;
}
foreach ($blocks as $block) {
if (self::pagelayer_block_tag($block) !== '') {
return true;
}
if (is_array($block) && !empty($block['innerBlocks']) && self::pagelayer_blocks_present($block['innerBlocks'])) {
return true;
}
}
return false;
}
/**
* Widget tag from a parsed block name, or an empty string.
*
* @param mixed $block Parsed block.
* @return string
*/
private static function pagelayer_block_tag($block) {
if (!is_array($block) || empty($block['blockName']) || !is_string($block['blockName'])) {
return '';
}
$pos = strpos($block['blockName'], 'pagelayer/');
if ($pos === false) {
return '';
}
return substr($block['blockName'], $pos + 10);
}
/**
* Whether an outline id matches this element.
*
* @param string $id pagelayer-id, possibly empty.
* @param string $path Positional path such as 0.1.
* @param string $element_id Requested id.
* @return bool
*/
private static function pagelayer_id_matches($id, $path, $element_id) {
$element_id = (string) $element_id;
if ($id !== '' && $id === $element_id) {
return true;
}
return $element_id === '@' . $path || $element_id === $path;
}
/**
* Short visible text for one element.
*
* @param array $attrs Attributes.
* @param string $html Inner HTML.
* @return string
*/
private static function pagelayer_preview_text($attrs, $html) {
$text = '';
if (is_string($html) && trim($html) !== '') {
$text = Helper::trim_text($html, 80);
}
if ($text === '' && is_array($attrs)) {
foreach (array('text', 'heading', 'title', 'service_heading', 'service_text', 'quote_content', 'cite', 'caption') as $key) {
if (!empty($attrs[$key]) && is_string($attrs[$key])) {
$text = Helper::trim_text($attrs[$key], 80);
break;
}
}
}
return $text;
}
/**
* Outline parsed Pagelayer blocks.
*
* @param array $blocks Parsed blocks.
* @param array> $out Collected rows.
* @param string $path Path prefix.
* @return array>
*/
private static function pagelayer_outline_blocks($blocks, &$out = array(), $path = '') {
if (!is_array($blocks)) {
return $out;
}
$index = 0;
foreach ($blocks as $block) {
$tag = self::pagelayer_block_tag($block);
if ($tag === '') {
continue;
}
if (count($out) >= 200) {
break;
}
$here = $path === '' ? (string) $index : $path . '.' . $index;
$attrs = (is_array($block) && isset($block['attrs']) && is_array($block['attrs'])) ? $block['attrs'] : array();
$id = !empty($attrs['pagelayer-id']) ? (string) $attrs['pagelayer-id'] : '@' . $here;
$row = array(
'id' => $id,
'type' => $tag,
'path' => $here,
);
if ($tag === 'pl_col' && isset($attrs['col'])) {
$row['col'] = (string) $attrs['col'];
}
$html = (is_array($block) && isset($block['innerHTML'])) ? (string) $block['innerHTML'] : '';
$text = self::pagelayer_preview_text($attrs, $html);
if ($text !== '') {
$row['text'] = $text;
}
$out[] = $row;
if (is_array($block) && !empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
self::pagelayer_outline_blocks($block['innerBlocks'], $out, $here);
}
$index++;
}
return $out;
}
/**
* Outline a Pagelayer node tree.
*
* @param array $nodes Nodes.
* @param array> $out Collected rows.
* @param string $path Path prefix.
* @return array>
*/
private static function pagelayer_outline_nodes($nodes, &$out = array(), $path = '') {
if (!is_array($nodes)) {
return $out;
}
$index = 0;
foreach ($nodes as $node) {
if (!is_array($node) || empty($node['tag']) || !is_string($node['tag'])) {
continue;
}
if (count($out) >= 200) {
break;
}
$here = $path === '' ? (string) $index : $path . '.' . $index;
$attrs = isset($node['attrs']) && is_array($node['attrs']) ? $node['attrs'] : array();
$id = !empty($attrs['pagelayer-id']) ? (string) $attrs['pagelayer-id'] : '@' . $here;
$row = array(
'id' => $id,
'type' => $node['tag'],
'path' => $here,
);
if ($node['tag'] === 'pl_col' && isset($attrs['col'])) {
$row['col'] = (string) $attrs['col'];
}
$html = (isset($node['content']) && is_string($node['content'])) ? $node['content'] : '';
$text = self::pagelayer_preview_text($attrs, $html);
if ($text !== '') {
$row['text'] = $text;
}
$out[] = $row;
if (isset($node['content']) && is_array($node['content'])) {
self::pagelayer_outline_nodes($node['content'], $out, $here);
}
$index++;
}
return $out;
}
/**
* Merge attributes into one parsed block.
*
* @param array $blocks Parsed blocks.
* @param string $element_id Element id or @path.
* @param array $settings Settings.
* @param bool $found Set when updated.
* @param string $path Path prefix.
* @return void
*/
private static function pagelayer_update_blocks(&$blocks, $element_id, $settings, &$found, $path = '') {
if (!is_array($blocks) || $found) {
return;
}
$index = 0;
foreach ($blocks as &$block) {
$tag = self::pagelayer_block_tag($block);
if ($tag === '') {
continue;
}
$here = $path === '' ? (string) $index : $path . '.' . $index;
$attrs = (isset($block['attrs']) && is_array($block['attrs'])) ? $block['attrs'] : array();
$id = isset($attrs['pagelayer-id']) ? (string) $attrs['pagelayer-id'] : '';
if (self::pagelayer_id_matches($id, $here, $element_id)) {
self::pagelayer_apply_block_settings($block, $settings);
$found = true;
return;
}
if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
self::pagelayer_update_blocks($block['innerBlocks'], $element_id, $settings, $found, $here);
if ($found) {
return;
}
}
$index++;
}
unset($block);
}
/**
* Merge attributes into one node.
*
* @param array $nodes Node tree.
* @param string $element_id Element id or @path.
* @param array $settings Settings.
* @param bool $found Set when updated.
* @param string $path Path prefix.
* @return void
*/
private static function pagelayer_update_nodes(&$nodes, $element_id, $settings, &$found, $path = '') {
if (!is_array($nodes) || $found) {
return;
}
$index = 0;
foreach ($nodes as &$node) {
if (!is_array($node) || empty($node['tag']) || !is_string($node['tag'])) {
continue;
}
$here = $path === '' ? (string) $index : $path . '.' . $index;
$attrs = isset($node['attrs']) && is_array($node['attrs']) ? $node['attrs'] : array();
$id = isset($attrs['pagelayer-id']) ? (string) $attrs['pagelayer-id'] : '';
if (self::pagelayer_id_matches($id, $here, $element_id)) {
self::pagelayer_apply_node_settings($node, $settings);
$found = true;
return;
}
if (isset($node['content']) && is_array($node['content'])) {
self::pagelayer_update_nodes($node['content'], $element_id, $settings, $found, $here);
if ($found) {
return;
}
}
$index++;
}
unset($node);
}
/**
* Write settings onto a parsed block.
*
* @param array $block Parsed block.
* @param array $settings Settings.
* @return void
*/
private static function pagelayer_apply_block_settings(&$block, $settings) {
$incoming = $settings;
if (array_key_exists('content', $incoming) && is_string($incoming['content']) && empty($block['innerBlocks'])) {
$block['innerHTML'] = $incoming['content'];
$block['innerContent'] = array($incoming['content']);
unset($incoming['content']);
} else {
unset($incoming['content']);
}
if (!isset($block['attrs']) || !is_array($block['attrs'])) {
$block['attrs'] = array();
}
foreach ($incoming as $key => $value) {
if (!is_string($key) || $key === '') {
continue;
}
$block['attrs'][$key] = $value;
}
}
/**
* Write settings onto a node.
*
* @param array $node Node.
* @param array $settings Settings.
* @return void
*/
private static function pagelayer_apply_node_settings(&$node, $settings) {
$incoming = $settings;
if (array_key_exists('content', $incoming) && is_string($incoming['content']) && (!isset($node['content']) || is_string($node['content']))) {
$node['content'] = $incoming['content'];
}
unset($incoming['content']);
if (!isset($node['attrs']) || !is_array($node['attrs'])) {
$node['attrs'] = array();
}
foreach ($incoming as $key => $value) {
if (!is_string($key) || $key === '') {
continue;
}
$node['attrs'][$key] = $value;
}
}
/**
* Turn parsed blocks into the node tree stored by the abilities API.
*
* @param array $blocks Parsed blocks.
* @return array>
*/
private static function pagelayer_blocks_to_nodes($blocks) {
$nodes = array();
if (!is_array($blocks)) {
return $nodes;
}
foreach ($blocks as $block) {
$tag = self::pagelayer_block_tag($block);
if ($tag === '') {
continue;
}
$node = array(
'tag' => $tag,
'attrs' => (isset($block['attrs']) && is_array($block['attrs'])) ? $block['attrs'] : array(),
);
if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
$children = self::pagelayer_blocks_to_nodes($block['innerBlocks']);
if ($children !== array()) {
$node['content'] = $children;
}
}
if (!isset($node['content'])) {
$html = isset($block['innerHTML']) ? (string) $block['innerHTML'] : '';
if (trim($html) !== '') {
$node['content'] = $html;
}
}
$nodes[] = $node;
}
return $nodes;
}
/**
* Serialize a node tree to Pagelayer block comments.
*
* @param array $nodes Nodes.
* @return string
*/
private static function pagelayer_serialize_nodes($nodes) {
if (class_exists('Pagelayer_Abilities_Register') && method_exists('Pagelayer_Abilities_Register', 'serialize_layout_to_blocks')) {
return (string) \Pagelayer_Abilities_Register::serialize_layout_to_blocks($nodes);
}
return self::pagelayer_serialize_nodes_fallback($nodes);
}
/**
* Block markup used when Pagelayer's own serializer is not loaded.
*
* @param array $nodes Nodes.
* @return string
*/
private static function pagelayer_serialize_nodes_fallback($nodes) {
if (!is_array($nodes)) {
return '';
}
$prefix = defined('PAGELAYER_BLOCK_PREFIX') ? PAGELAYER_BLOCK_PREFIX : 'wp';
$out = '';
foreach ($nodes as $node) {
if (!is_array($node) || empty($node['tag']) || !is_string($node['tag'])) {
continue;
}
$tag = $node['tag'];
if (strpos($tag, 'pl_') !== 0) {
$tag = 'pl_' . $tag;
}
$attrs = isset($node['attrs']) && is_array($node['attrs']) ? $node['attrs'] : array();
$name = $prefix . ':pagelayer/' . $tag;
$json = $attrs === array() ? '' : ' ' . wp_json_encode($attrs);
$inner = '';
if (isset($node['content']) && is_array($node['content'])) {
$inner = self::pagelayer_serialize_nodes_fallback($node['content']);
} elseif (isset($node['content']) && is_string($node['content'])) {
$inner = $node['content'];
}
if ($inner === '') {
$out .= '\n";
} else {
$out .= '' . $inner . '\n";
}
}
return $out;
}
}