File: //proc/thread-self/cwd/wp-content/plugins/cornerstone/includes/classes/Services/DynamicContent.php
<?php
namespace Themeco\Cornerstone\Services;
// use cs_expand_content anywhere you would normally use do_shortcode but also want dynamic content to expand
// use dynamic content anywhere you know is already inside do_shortcode or apply_filters('the_content')
class DynamicContent implements Service {
const PATTERN = "/{{dc:([\w.-]*):?([\w.-]*)(.*?)}}/";
const ANY_DC_PATTERN = '/{[{%#]/';
// Pattern to match that there is no
// inner dynamic like {{dc:post:{{dc:post:name}}}}
const HAS_INNER_CONTENT = "/{{[^}]*{dc:.*}}/";
const INNER_PATTERN = "/{{[^{]*?((?!{{.*}}))}}/";
const MAX_DEPTH = 100;
const FILTER_PREFIX = "cs_dynamic_content_";
protected $fields = array();
protected $groups = array();
protected $dynamicOptions = [];
protected $cache = array();
protected $recursion_depth = 0;
protected $max_recursion_depth;
protected $vm;
public function __construct(Vm $vm) {
$this->vm = $vm;
}
public function setup() {
add_action( 'init', [ $this, 'init' ] );
$this->max_recursion_depth = apply_filters( 'cs_dynamic_content_max_recursion_depth', 1000 );
}
public function init() {
CS()->component( 'Dynamic_Content_Post' );
CS()->component( 'Dynamic_Content_Term' );
CS()->component( 'Dynamic_Content_User' );
CS()->component( 'Dynamic_Content_Query' );
CS()->component( 'Dynamic_Content_Looper' );
CS()->component( 'Dynamic_Content_Url' );
CS()->component( 'Dynamic_Content_Rivet' );
do_action( 'cs_dynamic_content_register' );
CS()->component( 'Dynamic_Content_Global' );
// Dynamic Content
add_filter( 'cs_decode_shortcode_attribute', 'cs_dynamic_content_string' );
add_filter( 'get_the_excerpt', 'cs_dynamic_content_string', 100 );
add_filter( 'cs_css_post_process', 'cs_dynamic_content_string' );
add_filter( 'widget_text', 'cs_dynamic_content_string', 0 ); // hopefully before do_shortcode
// Setup cs_expand_content function
add_filter( 'cs_expand_content', 'cs_dynamic_content_string' ); // First run dynamic content
// removing this and only running inside the text element for now
add_filter( 'cs_expand_content', 'wp_filter_content_tags' ); // run applicable WordPress filters (only wp_filter_content_tags for now)
add_filter( 'cs_expand_content', 'do_shortcode', 11 ); // run do_shortcode on the final result
add_filter( 'cs_defer_html', 'cs_expand_content' );
// Parameters / Globals
add_filter('cs_dynamic_content_param', [$this, 'supplyFieldParam' ], 10, 3 );
add_filter('cs_dynamic_content_p', [$this, 'supplyFieldParam' ], 10, 3 );
add_filter('cs_dynamic_content_g', [$this, 'supplyGlobalFieldParam' ], 10, 3 );
add_action('cs_dynamic_content_setup', [$this, 'setupParameters'], 20);
}
public function setupParameters() {
cornerstone_dynamic_content_register_group(array(
'name' => 'param',
'aliases' => ['p'],
'label' => csi18n('app.dc.group-title-parameters')
));
cornerstone_dynamic_content_register_field(array(
'name' => 'element',
'group' => 'param',
'label' => csi18n( 'app.dc.parameters.element' ),
'controls' => array( array(
'key' => 'param',
'type' => 'text',
'label' => csi18n('app.dc.name')
) ),
'deep' => true,
'twig_format' => '{{ param.$param }}',
'format' => '{{dc:param:$param}}'
));
}
public function postProcessValue( $input ) {
// String
if (is_string( $input)) {
// Global family, weight, and color process
return preg_replace_callback('/global-((?:ff|fw|color)):([|:.\w-]+)/', function($matches) {
switch ($matches[1]) {
case 'color': return cornerstone('GlobalColors')->cssPostProcessColor( $matches[0] );
case 'ff': return cornerstone('GlobalFonts')->cssPostProcessFontFamily($matches[2]);
case 'fw': return cornerstone('GlobalFonts')->cssPostProcessFontWeight($matches[2]);
default: return '';
}
}, $input );
}
// Preprocess all keys if array
if (is_array($input)) {
foreach ($input as $key => $value) {
$input[$key] = $this->postProcessValue($value);
}
}
// Fallback
return $input;
}
/**
* Paramter output
* {{dc:p}}
*/
public function supplyFieldParam( $result, $field, $args ) {
// If in global custom css
if (apply_filters("cs_dynamic_content_global_parameter_context", false)) {
return $this->vm->getCssGlobalParameterByPath($field);
}
// Using a parameter in TSS or CSS
// Used by Twig
if (apply_filters('cs_dynamic_content_parameters_as_css', false) && empty($args['raw'])) {
$val = $this->vm->getCssParameterByPath($field);
// This is not a isVar parameter
if (!empty($val)) {
return $val;
}
}
return $this->postProcessValue( $this->vm->getParameterByPath($field) );
}
/**
* Parameter output
* {{dc:g}}
*/
public function supplyGlobalFieldParam( $result, $field, $args ) {
// Using a parameter in TSS or CSS
// Used by Twig
if (apply_filters('cs_dynamic_content_parameters_as_css', false) && empty($args['raw'])) {
$val = $this->vm->getCssGlobalParameterByPath($field);
// This is not a isVar parameter
if (!empty($val)) {
return $val;
}
}
return $this->postProcessValue( $this->vm->getGlobalParameterByPath($field) );
}
/**
* Main processing to output
*/
public function run( $content, $asString = true ) {
// Processes inner pattern
$content = $this->processInnerPattern($content);
if ( ! $asString ) {
list( $result, $args ) = $this->expand( $this->find_matches( $content ) );
return $result;
}
$content = apply_filters( 'cs_dynamic_content_before_expand_string', $content );
if ( ! is_string( $content ) ) {
return $content;
}
// for {{dc}}
$content = $this->renderString($content);
$content = apply_filters('cs_dynamic_content_after_render', $content);
return $content;
}
/**
* For {{dc:post:{{dc:post:null fallback="title"}}}}
* This makes sure {{dc:post:null}} gets processed first
* And returns and then sends to normal processing
*/
public function processInnerPattern($content, $depth = 0) {
// Type safety
if ( ! is_string( $content ) ) {
return $content;
}
preg_match(static::HAS_INNER_CONTENT, $content, $matches);
// No match standard
if (empty($matches)) {
return $content;
}
// Max depth reached
if ($depth > static::MAX_DEPTH) {
trigger_error("Max depth reached for inner dynamic content processing : Depth " . static::MAX_DEPTH);
return $content;
}
// Inner {{dc:post:title}} with NO inner dynamic content
$inner = preg_replace_callback(static::INNER_PATTERN, function($match) {
// Internal process to full string
return $this->renderString($match[0]);
}, $content);
// Process till no inner patterns found
return $this->processInnerPattern($inner, ++$depth);
}
/**
* Raw template to string output
*/
private function renderString($content) {
// for {{dc}}
$content = preg_replace_callback(
static::PATTERN,
array( $this, 'expand_string_callback' ),
$content
);
// For encoded version (e.g. Subject, Telephone)
return preg_replace_callback(
'/%7B%7Bdc:([\w.-]*):?([\w.-]*)(.*?)%7D%7D/',
array( $this, 'expand_string_callback_encoded' ),
$content
);
}
public function register_field($field) {
if (isset($field['name']) && isset($field['group']) ) {
$key = $field['group'] . ':' . $field['name'];
$this->fields[$key] = $field;
}
// Auto setup filter
if (isset($field['filter']) && is_callable($field['filter'])) {
$key = static::FILTER_PREFIX . $field['group'] . '_' . $field['name'];
add_filter($key, $field['filter'], 10, 2);
}
}
public function register_group( $group ) {
if (isset($group['name']) ) {
$this->groups[$group['name']] = cs_define_defaults( $group, array(
'label' => 'Group',
'aliases' => [],
) );
}
}
public function get_dynamic_fields() {
do_action( 'cs_dynamic_content_setup' );
return apply_filters( 'cs_dynamic_content_ui', array(
'fields' => $this->fields,
'groups' => $this->groups
) );
}
public function find_matches( $content ) {
preg_match(static::PATTERN, trim( $content ), $matches );
return $matches;
}
// Empty check for find_matches
public function has_dynamic_content($content) {
return !empty(
preg_match(static::ANY_DC_PATTERN, trim( $content ), $matches )
);
}
public function parse_string( $content ) {
return $this->parse_matches( $this->find_matches( $content ) );
}
public function expand_string_callback_encoded( $matches ){
return $this->expand_string_callback( $matches, true );
}
public function parse_matches( $matches ) {
$type = isset($matches[1]) ? $matches[1] : '';
$field = isset($matches[2]) ? $matches[2] : '';
$args = isset($matches[3]) ? $this->parse_args( trim($matches[3]) ) : array();
return [ $type, $field, $args ];
}
public function expand( $matches ) {
list( $type, $field, $args ) = $this->parse_matches( $matches );
$result = apply_filters( static::FILTER_PREFIX . "{$type}", '', $field, $args );
$result = apply_filters( static::FILTER_PREFIX . "{$type}_{$field}", $result, $args );
return [ $result, $args ];
}
public function expand_string_callback( $matches, $encode_uri = false ) {
list( $result, $args ) = $this->expand( $matches );
if ( ! $result && $result !== '0' && isset( $args['fallback'] ) ) {
// DC Bandaid usage of fallback as dc
if (strpos($args['fallback'], 'dc:') === 0) {
$args['fallback'] = "{{" . $args['fallback'] . "}}";
}
$result = $args['fallback'];
}
// if URL must be encoded
if ( $encode_uri ) {
$result = rawurlencode( $result );
}
if ( isset( $args['type'] ) ) {
switch ($args['type']) {
case 'json':
// pretty_print JSON arg
$prettyPrint = cs_get_array_value($args, 'pretty_print', false)
? JSON_PRETTY_PRINT
: 0;
$result = json_encode( $result, $prettyPrint );
break;
case 'date':
// Format to use or default
$format = !empty( $args['format'] ) ? $args['format'] : get_option('date_format');
// if already numeric allow to go through date_i18n
// as a timestamp will not work here
if (is_string($result)) {
$result = strtotime( $result );
}
$result = date_i18n( $format, $result);
break;
case 'image':
$result = \cs_resolve_image_source($result);
break;
case 'image-css-url':
$result = 'url(' . \cs_resolve_image_source($result) . ')';
break;
case 'file':
$local = (bool)cs_get_array_value($args, 'local', false);
$result = \cs_resolve_attachment_source($result, $local);
break;
case 'time':
$result = date_i18n( isset( $args['format'] ) ? $args['format'] : get_option('time_format'), strtotime( $result ) );
break;
case 'object':
// Grab by key
if (( isset($args['key']) || isset($args['_key']) ) && is_array($result)) {
// For using with meta and other dc
// that uses `key`, but cannot support
// dot syntax
$key = !isset($args['_key'])
? $args['key']
: $args['_key'];
// Grab path from values
$result = cs_get_path($result, $key);
}
break;
}
}
if ( is_scalar( $result ) && ! is_string( $result ) ) {
$result = (string) $result;
}
if ( is_array( $result ) ) {
$strings = array_filter( $result, 'is_scalar' );
$result = implode( isset( $args['delimiter'] ) ? $args['delimiter'] : ',', $strings );
}
if ( is_string( $result ) ) {
$result = apply_filters( 'cs_dynamic_content_string_result', $result, $args );
$this->recursion_depth++;
if ($this->recursion_depth > $this->max_recursion_depth) {
trigger_error( 'Dynamic Content stopped proccessing after a depth of 100. It is likely that you have self referencing dynamic content', E_USER_WARNING );
return $result;
}
$result = cs_expand_content( $result );
$this->recursion_depth--;
}
// Null type explictly to empty string
if (is_null($result)) {
return '';
}
// Expected return is string
// and something has gone wrong
if (!is_scalar($result)) {
if (WP_DEBUG) {
trigger_error(
'Dynamic Content return value not scalar : ' . gettype($result),
E_USER_WARNING
);
}
return "";
}
return (string)$result;
}
public function parse_args( $arg_string ) {
$args = array();
// un wptexturize
$arg_string = str_replace( '“', '"', $arg_string ); // opening curly double quote
$arg_string = str_replace( '”', '"', $arg_string ); // closing curly double quote
$arg_string = str_replace( '’', "'", $arg_string ); // apostrophe
$arg_string = str_replace( '′', "'", $arg_string ); // prime
$arg_string = str_replace( '″', '"', $arg_string ); // double prime
$arg_string = str_replace( '‘', "'", $arg_string ); // opening curly single quote
$arg_string = str_replace( '’', "'", $arg_string ); // closing curly single quote
$arg_string = str_replace( '\\"', '"', $arg_string ); // double escaped string (like a json escape)
$arg_string = str_replace('&quot;', '"', $arg_string );
$arg_string = str_replace('&apos;', ''', $arg_string );
$arg_string = preg_replace_callback( '%(?:[^\\\](?:(")|(')))%', array( $this, 'parse_args_normalize_quotes' ), $arg_string);
$arg_string = preg_replace_callback( '%(?:[^\\\](?:(")|(')))%', array( $this, 'parse_args_normalize_quotes' ), $arg_string);
preg_match_all( '%(\w+)\s*=\s*(?:"((?:[^"\\\]++|\\\.)*+)"|\'((?:[^\'\\\]++|\\\.)*+)\')%', $arg_string, $matches );
foreach( $matches[1] as $index => $key ) {
$arg = '';
if ( isset( $matches[2][$index] ) && static::isValidValue($matches[2][$index])) $arg = $matches[2][$index];
if ( isset( $matches[3][$index] ) && static::isValidValue($matches[3][$index])) $arg = $matches[3][$index];
$args[$key] = str_replace( '\}', '}', wp_unslash( $arg ) );
}
return $args;
}
// Allow '0' to be a valid value
public static function isValidValue($value) {
return $value || $value === "0";
}
public function parse_args_normalize_quotes($matches) {
return str_replace(''', "'", str_replace('"', '"', $matches[0]));
}
public function get_contextual_post( $args = [] ) {
// see Cornerstone_Dynamic_Content_Post::supply_field
global $post;
return $this->get_cached_post(
isset( $args['post'] ) ? $args['post'] : apply_filters( 'cs_dynamic_content_post_id', is_a( $post, 'WP_Post' ) ? $post->ID : get_the_ID() )
);
}
public function get_cached_post( $post_id ) {
$key = "post:" . (in_array( $post_id, ['prev', 'next', 'parent' ]) ? get_the_ID() . ':' . $post_id : $post_id);
if ( ! isset( $this->cache[$key] ) ) {
$post = null;
$current_id = get_the_ID();
if ( 'next' === $post_id ) {
$next = get_next_post();
if ( is_a( $next, 'WP_Post' ) && $current_id !== $next->ID ) {
$post = $next;
}
} elseif ( 'prev' === $post_id ) {
$prev = get_previous_post();
if ( $prev && is_a( $prev, 'WP_Post' ) && $current_id !== $prev->ID ) {
$post = $prev;
}
} elseif ( 'parent' === $post_id ) {
$wp_post = get_post();
if (!empty($wp_post->post_parent)) {
$post = get_post($wp_post->post_parent);
}
} elseif ( (int) $post_id > 0 ) {
$post = get_post( (int) $post_id );
}
$this->cache[$key] = is_a( $post, 'WP_Post' ) ? $post : null;
}
return $this->cache[$key];
}
public function get_contextual_term( $args = [] ) {
// Resolve term by ID
if ( isset( $args['term'] ) ) {
$key = 'term:' . $args['term'];
if ( ! isset( $this->cache[$key] ) ) {
$this->cache[$key] = get_term( (int) $args['term'] );
}
return $this->cache[$key];
}
// Get the current looper data and queried object
$data = CS()->component('Looper_Manager')->get_current_data();
$queried = get_queried_object();
// Priority 1: Return the current term from looper data
if ( is_a( $data, 'WP_Term') ) {
return $data;
}
// Priority 2: Return the current queried object if is a term
if (is_a( $queried, 'WP_Term') ) {
return $queried;
}
// Priority 3: If we are looping over a post, use its first term
if (is_a( $data, 'WP_Post') ) {
return $this->get_cached_first_term_for_post( $data );
}
// Priority 4: If the current query is a post, use its first term
if (is_a( $queried, 'WP_Post') ) {
return $this->get_cached_first_term_for_post( $queried );
}
return null;
}
public function get_cached_first_term_for_post( $post ) {
$key = 'term-post:' . $post->ID;
if ( ! isset( $this->cache[$key] ) ) {
$first_post_term = $this->get_first_term_for_post( $post );
$this->cache[$key] = $first_post_term ? $first_post_term : null;
}
return $this->cache[$key];
}
public function get_first_term_for_post( $post ) {
if ($post->post_type === 'post') {
$post_cat = get_the_category( $post->id );
if ( isset( $post_cat[0] ) && is_a( $post_cat[0], 'WP_Term' ) ) {
return $post_cat[0];
}
$post_tag = get_the_tags( $post->id );
if ( isset( $post_tag[0] ) && is_a( $post_tag[0], 'WP_Term' ) ) {
return $post_tag[0];
}
}
// WooCommerce product terms setup
// @TODO move to WooCommerce integration
if ($post->post_type === 'product' && class_exists('WooCommerce')) {
$product_cat = get_the_terms( $post->ID, 'product_cat' );
if ( isset( $product_cat[0] ) && is_a( $product_cat[0], 'WP_Term' ) ) {
return $product_cat[0];
}
$product_tag = get_the_terms( $post->ID, 'product_tag' );
if ( isset( $product_tag[0] ) && is_a( $product_tag[0], 'WP_Term' ) ) {
return $product_tag[0];
}
}
return null;
}
public function get_contextual_product( $args = [] ) {
if ( isset( $args['product'] ) ) {
$product_id = $args['product'];
} else {
global $product;
$product_id = empty($product) || !is_object($product)
? get_the_ID()
: $product->get_id();
}
return $this->get_cached_product( $product_id );
}
public function get_cached_product( $product_id ) {
$key = "product:$product_id";
if ( ! isset( $this->cache[$key] ) ) {
$this->cache[$key] = wc_get_product( $product_id );
}
return $this->cache[$key];
}
public function get_contextual_author( $args ) {
// If no global post, that means
// author page has been reset
global $post;
// No global post try from author page
if ((!$post || !empty($args['global'])) && is_author()) {
$author_id = get_query_var( 'author' );
if(!empty($author_id)){
return $this->get_cached_user( $author_id );
}
}
// From contextual post
$contextualPost = $this->get_contextual_post( $args );
if (empty($contextualPost)) {
return null;
}
// From contextual
return $this->get_cached_user( get_post_field( 'post_author', $contextualPost ) );
}
public function get_contextual_user( $args = [] ) {
// Return a specific user
if ( isset( $args['user'] ) ) {
$user_id = $args['user'];
if ('author' === $user_id ) {
global $post;
$user_id = get_post_field( 'post_author', apply_filters( 'cs_dynamic_content_post_id', $post->ID ) );
}
return $this->get_cached_user( $user_id );
}
// Return user if it is the current context in the Looper Provider
$data = CS()->component('Looper_Manager')->get_current_data();
if ( is_a( $data, 'WP_User') ) {
return $data;
}
// Return the logged in user
return $this->get_cached_user( get_current_user_id() );
}
public function get_cached_user( $user_id ) {
if ( !$user_id ) {
return null;
}
$key = "user:$user_id";
if ( ! isset( $this->cache[$key] ) ) {
$user = get_user_by( 'ID', (int) $user_id );
if ( ! $user ) {
return null;
}
$this->cache[$key] = $user;
}
return $this->cache[$key];
}
/**
* Options to be used in Dynamic Content popup
*/
public function registerDynamicOptions($type, $controls) {
$this->dynamicOptions[$type] = $controls;
// Auto setup filters if passed
if (!empty($controls['filter']) && is_callable($controls['filter'])) {
add_filter("cs_dynamic_options_" . $type, $controls['filter'], 10, 2);
add_filter("cornerstone_app_choices_" . $type, $controls['filter'], 10, 2);
}
}
/**
* Get dynamic option controls
* used in late data
*/
public function getDynamicOptions() {
return $this->dynamicOptions;
}
}