null, 'type' => null, 'internal' => false, 'source' => '', ]; /** * Processed media items for output to client. * * @var object[string] { * Media item properties. * * @index string Unique ID (system-generated). * * @see $media_item_template. * } */ private $media_items = []; /** * Collection of unprocessed media items. * * @var array { * @type object[string] $props { * Media item properties. * * @index string Unique ID (system-generated). * * @see $media_item_template * } * @type string[string] $uri { * Cached URIs. * * @index string URI. * * @type string Item ID (points to item in `props` array) * } * } */ private $media_items_raw = [ 'props' => [], 'uri' => [], ]; /** * Manage excluded content * @var object */ private $exclude = null; private $groups = array( 'auto' => 0, 'manual' => array(), ); /** * Validated URIs * Caches validation of parsed URIs * > Key: URI * > Value: (bool) TRUE if valid * @var array */ private $validated_uris = array(); /* Widget properties */ /** * Used to track if widget is currently being processed or not * Set to Widget ID currently being processed * @var bool|string */ private $widget_processing = false; /** * Parameters for widget being processed * @param array */ private $widget_processing_params = null; /** * Manage nested widget processing * Used to avoid premature widget output * @var int */ private $widget_processing_level = 0; /** * Constructor */ public function __construct() { parent::__construct(); // Init instances $this->fields = new SLB_Fields(); $this->themes = new SLB_Themes( $this ); if ( ! is_admin() ) { $this->template_tags = new SLB_Template_Tags( $this ); } } /* Init */ public function _init() { parent::_init(); $this->util->do_action( 'init' ); } /** * Declare client files (scripts, styles) * @uses parent::_client_files() * @return void */ protected function _client_files( $files = null ) { $js_path = 'client/js/'; $js_path .= ( SLB_DEV ) ? 'dev' : 'prod'; $files = array( 'scripts' => array( 'core' => array( 'file' => "$js_path/lib.core.js", 'deps' => 'jquery', 'enqueue' => false, 'in_footer' => true, ), 'view' => array( 'file' => "$js_path/lib.view.js", 'deps' => array( '[core]' ), 'context' => array( array( 'public', $this->m( 'is_request_valid' ) ) ), 'in_footer' => true, ), ), 'styles' => array( 'core' => array( 'file' => 'client/css/app.css', 'context' => array( 'public' ), ), ), ); parent::_client_files( $files ); } /** * Register hooks * @uses parent::_hooks() */ protected function _hooks() { parent::_hooks(); /* Admin */ add_action( 'admin_menu', $this->m( 'admin_menus' ) ); /* Init */ add_action( 'wp', $this->m( '_hooks_init' ) ); } /** * Initialize hooks. * * @return void */ public function _hooks_init() { if ( ! $this->is_enabled() ) { return; } // Callback for hooks that need to have method added to end of stack. $cb_hooks_add_last = $this->m( 'hooks_add_last' ); // Init lightbox add_action( 'wp_footer', $this->m( 'client_footer' ) ); $this->util->add_action( 'footer_script', $this->m( 'client_init' ), 1 ); $this->util->add_filter( 'footer_script', $this->m( 'client_script_media' ), 2 ); // Link activation add_filter( 'the_content', $cb_hooks_add_last ); add_filter( 'get_post_galleries', $cb_hooks_add_last ); $this->util->add_filter( 'post_process_links', $this->m( 'activate_groups' ), 11 ); $this->util->add_filter( 'validate_uri_regex', $this->m( 'validate_uri_regex_default' ), 1 ); // Content exclusion $this->util->add_filter( 'pre_process_links', $this->m( 'exclude_content' ) ); $this->util->add_filter( 'pre_exclude_content', $this->m( 'exclude_shortcodes' ) ); $this->util->add_filter( 'post_process_links', $this->m( 'restore_excluded_content' ) ); // Grouping if ( $this->options->get_bool( 'group_post' ) ) { $this->util->add_filter( 'get_group_id', $this->m( 'post_group_id' ), 1 ); } // Shortcode grouping if ( $this->options->get_bool( 'group_gallery' ) ) { add_filter( 'the_content', $this->m( 'group_shortcodes' ), 1 ); } // Widgets if ( $this->options->get_bool( 'enabled_widget' ) ) { add_action( 'dynamic_sidebar_before', $this->m( 'widget_process_nested' ) ); add_action( 'dynamic_sidebar', $cb_hooks_add_last ); add_filter( 'dynamic_sidebar_params', $this->m( 'widget_process_inter' ), 1 ); add_action( 'dynamic_sidebar_after', $cb_hooks_add_last ); add_action( 'dynamic_sidebar_after', $cb_hooks_add_last ); } else { add_action( 'dynamic_sidebar_before', $this->m( 'widget_block_start' ) ); add_action( 'dynamic_sidebar_after', $this->m( 'widget_block_finish' ) ); } // Menus if ( $this->options->get_bool( 'enabled_menu' ) ) { add_filter( 'wp_nav_menu', $cb_hooks_add_last ); } } /** * Adds hook(s) to end of filter/action stack. * * @param string $data Optional. Data being filtered. * @return string Filtered content. */ public function hooks_add_last( $data = null ) { global $wp_filter; $tag = current_filter(); // Stop processing on invalid hook. if ( empty( $tag ) || ! is_string( $tag ) ) { return $data; } // Get lowest priority for filter. $max_priority = max( array_keys( $wp_filter[ $tag ]->callbacks ) ); // Add priority offset if ( $max_priority < PHP_INT_MAX ) { $max_priority += min( 123, PHP_INT_MAX - $max_priority ); } switch ( $tag ) { case 'the_content': add_filter( $tag, $this->m( 'activate_links' ), $max_priority ); break; case 'get_post_galleries': add_filter( $tag, $this->m( 'activate_galleries' ), $max_priority ); break; case 'dynamic_sidebar': add_action( $tag, $this->m( 'widget_process_start' ), $max_priority ); break; case 'dynamic_sidebar_after': add_action( $tag, $this->m( 'widget_process_finish' ), $max_priority ); add_action( $tag, $this->m( 'widget_process_nested_finish' ), $max_priority ); break; case 'wp_nav_menu': add_filter( $tag, $this->m( 'menu_process' ), $max_priority, 2 ); break; } // Remove init hook. remove_filter( $tag, $this->m( __FUNCTION__ ) ); // Return content (for filters). return $data; } /** * Add post ID to link group ID * @uses `SLB::get_group_id` filter * @param array $group_segments Group ID segments * @return array Modified group ID segments */ public function post_group_id( $group_segments ) { if ( in_the_loop() ) { // Prepend post ID to group ID $post = get_post(); if ( $post ) { array_unshift( $group_segments, $post->ID ); } } return $group_segments; } /** * Init options */ protected function _options() { // Setup options $opts = array( 'groups' => array( 'activation' => array( 'title' => __( 'Activation', 'simple-lightbox' ), 'priority' => 10, ), 'grouping' => array( 'title' => __( 'Grouping', 'simple-lightbox' ), 'priority' => 20, ), 'ui' => array( 'title' => __( 'UI', 'simple-lightbox' ), 'priority' => 30, ), 'labels' => array( 'title' => __( 'Labels', 'simple-lightbox' ), 'priority' => 40, ), ), 'items' => array( 'enabled' => array( 'title' => __( 'Enable Lightbox Functionality', 'simple-lightbox' ), 'default' => true, 'group' => array( 'activation', 10 ), ), 'enabled_home' => array( 'title' => __( 'Enable on Home page', 'simple-lightbox' ), 'default' => true, 'group' => array( 'activation', 20 ), ), 'enabled_post' => array( 'title' => __( 'Enable on Single Posts', 'simple-lightbox' ), 'default' => true, 'group' => array( 'activation', 30 ), ), 'enabled_page' => array( 'title' => __( 'Enable on Pages', 'simple-lightbox' ), 'default' => true, 'group' => array( 'activation', 40 ), ), 'enabled_archive' => array( 'title' => __( 'Enable on Archive Pages (tags, categories, etc.)', 'simple-lightbox' ), 'default' => true, 'group' => array( 'activation', 50 ), ), 'enabled_widget' => array( 'title' => __( 'Enable for Widgets', 'simple-lightbox' ), 'default' => false, 'group' => array( 'activation', 60 ), ), 'enabled_menu' => array( 'title' => __( 'Enable for Menus', 'simple-lightbox' ), 'default' => false, 'group' => array( 'activation', 60 ), ), 'group_links' => array( 'title' => __( 'Group items (for displaying as a slideshow)', 'simple-lightbox' ), 'default' => true, 'group' => array( 'grouping', 10 ), ), 'group_post' => array( 'title' => __( 'Group items by Post (e.g. on pages with multiple posts)', 'simple-lightbox' ), 'default' => true, 'group' => array( 'grouping', 20 ), ), 'group_gallery' => array( 'title' => __( 'Group gallery items separately', 'simple-lightbox' ), 'default' => false, 'group' => array( 'grouping', 30 ), ), 'group_widget' => array( 'title' => __( 'Group widget items separately', 'simple-lightbox' ), 'default' => false, 'group' => array( 'grouping', 40 ), ), 'group_menu' => array( 'title' => __( 'Group menu items separately', 'simple-lightbox' ), 'default' => false, 'group' => array( 'grouping', 50 ), ), 'ui_autofit' => array( 'title' => __( 'Resize lightbox to fit in window', 'simple-lightbox' ), 'default' => true, 'group' => array( 'ui', 10 ), 'in_client' => true, ), 'ui_animate' => array( 'title' => __( 'Enable animations', 'simple-lightbox' ), 'default' => true, 'group' => array( 'ui', 20 ), 'in_client' => true, ), 'slideshow_autostart' => array( 'title' => __( 'Start Slideshow Automatically', 'simple-lightbox' ), 'default' => true, 'group' => array( 'ui', 30 ), 'in_client' => true, ), 'slideshow_duration' => array( 'title' => __( 'Slide Duration (Seconds)', 'simple-lightbox' ), 'default' => '6', 'attr' => array( 'size' => 3, 'maxlength' => 3, ), 'group' => array( 'ui', 40 ), 'in_client' => true, ), 'group_loop' => array( 'title' => __( 'Loop through items', 'simple-lightbox' ), 'default' => true, 'group' => array( 'ui', 50 ), 'in_client' => true, ), 'ui_overlay_opacity' => array( 'title' => __( 'Overlay Opacity (0 - 1)', 'simple-lightbox' ), 'default' => '0.8', 'attr' => array( 'size' => 3, 'maxlength' => 3, ), 'group' => array( 'ui', 60 ), 'in_client' => true, ), 'ui_title_default' => array( 'title' => __( 'Enable default title', 'simple-lightbox' ), 'default' => false, 'group' => array( 'ui', 70 ), 'in_client' => true, ), 'txt_loading' => array( 'title' => __( 'Loading indicator', 'simple-lightbox' ), 'default' => 'Loading', 'group' => array( 'labels', 20 ), ), 'txt_close' => array( 'title' => __( 'Close button', 'simple-lightbox' ), 'default' => 'Close', 'group' => array( 'labels', 10 ), ), 'txt_nav_next' => array( 'title' => __( 'Next Item button', 'simple-lightbox' ), 'default' => 'Next', 'group' => array( 'labels', 30 ), ), 'txt_nav_prev' => array( 'title' => __( 'Previous Item button', 'simple-lightbox' ), 'default' => 'Previous', 'group' => array( 'labels', 40 ), ), 'txt_slideshow_start' => array( 'title' => __( 'Start Slideshow button', 'simple-lightbox' ), 'default' => 'Start slideshow', 'group' => array( 'labels', 50 ), ), 'txt_slideshow_stop' => array( 'title' => __( 'Stop Slideshow button', 'simple-lightbox' ), 'default' => 'Stop slideshow', 'group' => array( 'labels', 60 ), ), 'txt_group_status' => array( 'title' => __( 'Slideshow status format', 'simple-lightbox' ), 'default' => 'Item %current% of %total%', 'group' => array( 'labels', 70 ), ), ), 'legacy' => array( 'header_activation' => null, 'header_enabled' => null, 'header_strings' => null, 'header_ui' => null, 'activate_attachments' => null, 'validate_links' => null, 'enabled_compat' => null, 'enabled_single' => array( 'enabled_post', 'enabled_page' ), 'enabled_caption' => null, 'enabled_desc' => null, 'ui_enabled_caption' => null, 'ui_caption_src' => null, 'ui_enabled_desc' => null, 'caption_src' => null, 'animate' => 'ui_animate', 'overlay_opacity' => 'ui_overlay_opacity', 'loop' => 'group_loop', 'autostart' => 'slideshow_autostart', 'duration' => 'slideshow_duration', 'txt_numDisplayPrefix' => null, 'txt_numDisplaySeparator' => null, 'txt_closeLink' => 'txt_link_close', 'txt_nextLink' => 'txt_link_next', 'txt_prevLink' => 'txt_link_prev', 'txt_startSlideshow' => 'txt_slideshow_start', 'txt_stopSlideshow' => 'txt_slideshow_stop', 'txt_loadingMsg' => 'txt_loading', 'txt_link_next' => 'txt_nav_next', 'txt_link_prev' => 'txt_nav_prev', 'txt_link_close' => 'txt_close', ), ); parent::_set_options( $opts ); } /* Methods */ /*-** Admin **-*/ /** * Add admin menus * @uses this->admin->add_theme_page */ function admin_menus() { // Build options page $lbls_opts = array( 'menu' => __( 'Lightbox', 'simple-lightbox' ), 'header' => __( 'Lightbox Settings', 'simple-lightbox' ), 'plugin_action' => __( 'Settings', 'simple-lightbox' ), ); $pg_opts = $this->admin->add_theme_page( 'options', $lbls_opts ) ->require_form() ->add_content( 'options', 'Options', $this->options ); // Add Support information $support = $this->util->get_plugin_info( 'SupportURI' ); if ( ! empty( $support ) ) { $pg_opts->add_content( 'support', __( 'Feedback & Support', 'simple-lightbox' ), $this->m( 'theme_page_callback_support' ), 'secondary' ); } // Add Actions $lbls_reset = array( 'title' => __( 'Reset', 'simple-lightbox' ), 'confirm' => __( 'Are you sure you want to reset Simple Lightbox\'s settings?', 'simple-lightbox' ), 'success' => __( 'Settings have been reset', 'simple-lightbox' ), 'failure' => __( 'Settings were not reset', 'simple-lightbox' ), ); $this->admin->add_action( 'reset', $lbls_reset, $this->options ); } /** * Support information */ public function theme_page_callback_support() { // Description $desc = __( '

Simple Lightbox thrives on your feedback!

Click the button below to get help, request a feature, or provide some feedback!

', 'simple-lightbox' ); echo $desc; // Link $lnk_uri = $this->util->get_plugin_info( 'SupportURI' ); $lnk_txt = __( 'Get Support & Provide Feedback', 'simple-lightbox' ); echo $this->util->build_html_link( $lnk_uri, $lnk_txt, array( 'target' => '_blank', 'class' => 'button', ) ); } /*-** Functionality **-*/ /** * Checks whether lightbox is currently enabled/disabled * @return bool TRUE if lightbox is currently enabled, FALSE otherwise */ function is_enabled() { static $ret = null; if ( is_null( $ret ) ) { $ret = ( ! is_admin() && $this->options->get_bool( 'enabled' ) && ! is_feed() ) ? true : false; if ( $ret ) { $opt = ''; // Determine option to check if ( is_home() || is_front_page() ) { $opt = 'home'; } elseif ( is_singular() ) { $opt = ( is_page() ) ? 'page' : 'post'; } elseif ( is_archive() || is_search() ) { $opt = 'archive'; } // Check sub-option if ( ! empty( $opt ) ) { // Prefix option name. $opt = 'enabled_' . $opt; if ( $this->options->has( $opt ) ) { $ret = $this->options->get_bool( $opt ); } } } } // Filter return value if ( ! is_admin() ) { $ret = $this->util->apply_filters( 'is_enabled', $ret ); } // Return value (force boolean) return ! ! $ret; } /** * Make sure content is valid for processing/activation * * @param string $content Content to validate * @return bool TRUE if content is valid (FALSE otherwise) */ protected function is_content_valid( $content ) { // Invalid hooks if ( doing_filter( 'get_the_excerpt' ) ) { return false; } // Non-string value if ( ! is_string( $content ) ) { return false; } // Empty string $content = trim( $content ); if ( empty( $content ) ) { return false; } // Content is valid return $this->util->apply_filters( 'is_content_valid', true, $content ); } /** * Activates galleries extracted from post * @see get_post_galleries() * @param array $galleries A list of galleries in post * @return A list of galleries with links activated */ function activate_galleries( $galleries ) { // Validate if ( empty( $galleries ) ) { return $galleries; } // Check galleries for HTML output $gallery = reset( $galleries ); if ( is_array( $gallery ) ) { return $galleries; } // Activate galleries $group = ( $this->options->get_bool( 'group_gallery' ) ) ? true : null; foreach ( $galleries as $key => $val ) { if ( ! is_null( $group ) ) { $group = 'gallery_' . $key; } // Activate links in gallery $gallery = $this->process_links( $val, $group ); // Save modified gallery $galleries[ $key ] = $gallery; } return $galleries; } /** * Scans post content for image links and activates them * * Lightbox will not be activated for feeds * @param string $content Content to activate * @param string (optonal) $group Group ID for content * @return string Post content */ public function activate_links( $content, $group = null ) { // Validate content if ( ! $this->is_content_valid( $content ) ) { return $content; } // Filter content before processing links $content = $this->util->apply_filters( 'pre_process_links', $content ); // Process links $content = $this->process_links( $content, $group ); // Filter content after processing links $content = $this->util->apply_filters( 'post_process_links', $content ); return $content; } /** * Process links in content * @global obj $wpdb DB instance * @global obj $post Current post * @param string $content Text containing links * @param string (optional) $group Group to add links to (Default: none) * @return string Content with processed links */ protected function process_links( $content, $group = null ) { // Extract links $links = $this->get_links( $content, true ); // Do not process content without links if ( empty( $links ) ) { return $content; } // Process links static $protocol = array( 'http://', 'https://' ); static $qv_att = 'attachment_id'; static $uri_origin = null; if ( ! is_array( $uri_origin ) ) { $uri_parts = array_fill_keys( array( 'scheme', 'host', 'path' ), '' ); $uri_origin = wp_parse_args( wp_parse_url( strtolower( home_url() ) ), $uri_parts ); } static $uri_proto = null; if ( empty( $uri_proto ) ) { $uri_proto = (object) array( 'raw' => '', 'source' => '', 'parts' => '', ); } $uri_parts_required = array( 'host' => '' ); // Setup group properties $g_props = (object) array( 'enabled' => $this->options->get_bool( 'group_links' ), 'attr' => 'group', 'base' => '', 'legacy_prefix' => 'lightbox[', 'legacy_suffix' => ']', ); if ( $g_props->enabled ) { $g_props->base = ( is_scalar( $group ) ) ? trim( strval( $group ) ) : ''; } // Initialize content handlers if ( ! ( $this->handlers instanceof SLB_Content_Handlers ) ) { $this->handlers = new SLB_Content_Handlers( $this ); } // Iterate through and activate supported links foreach ( $links as $link ) { // Init vars $pid = 0; $link_new = $link; $uri = clone $uri_proto; $type = false; $props_extra = array(); $key = null; $internal = false; // Parse link attributes $attrs = $this->util->parse_attribute_string( $link_new, array( 'href' => '' ) ); // Get URI $uri->raw = $attrs['href']; // Stop processing invalid links if ( ! $this->validate_uri( $uri->raw ) || $this->has_attribute( $attrs, 'active' ) // Previously-processed. ) { continue; } // Normalize URI (make absolute) $uri->source = WP_HTTP::make_absolute_url( $uri->raw, $uri_origin['scheme'] . '://' . $uri_origin['host'] ); // URI cached? $key = $this->get_media_item_id( $uri->source ); // Internal URI? (e.g. attachments) if ( ! $key ) { $uri->parts = array_merge( $uri_parts_required, (array) wp_parse_url( $uri->source ) ); $internal = ( $uri->parts['host'] === $uri_origin['host'] ) ? true : false; // Attachment? if ( $internal && is_local_attachment( $uri->source ) ) { $pid = url_to_postid( $uri->source ); $src = wp_get_attachment_url( $pid ); if ( ! ! $src ) { $uri->source = $src; $props_extra['id'] = $pid; // Check cache for attachment source URI $key = $this->get_media_item_id( $uri->source ); } unset( $src ); } } // Determine content type if ( ! $key ) { // Get handler match $hdl_result = $this->handlers->match( $uri->source ); if ( ! ! $hdl_result->handler ) { $type = $hdl_result->handler->get_id(); $props_extra = $hdl_result->props; // Updated source URI if ( isset( $props_extra['uri'] ) ) { $uri->source = $props_extra['uri']; unset( $props_extra['uri'] ); } } // Cache valid item if ( ! ! $type ) { $key = $this->cache_media_item( $uri, $type, $internal, $props_extra ); } } // Stop processing invalid links if ( ! $key ) { // Cache invalid URI $this->validated_uris[ $uri->source ] = false; if ( $uri->raw !== $uri->source ) { $this->validated_uris[ $uri->raw ] = false; } continue; } // Activate link $this->set_attribute( $attrs, 'active' ); $this->set_attribute( $attrs, 'asset', $key ); // Mark internal links if ( $internal ) { $this->set_attribute( $attrs, 'internal', $pid ); } // Set group (if enabled) if ( $g_props->enabled ) { $group = array(); // Get preset group attribute $g = ( $this->has_attribute( $attrs, $g_props->attr ) ) ? $this->get_attribute( $attrs, $g_props->attr ) : ''; if ( ! empty( $g ) ) { $group[] = $g; } elseif ( ! empty( $g_props->base ) ) { $group[] = $g_props->base; } /** * Filter group ID components * * @see process_links() * * @param array $group Components used to build group ID */ $group = $this->util->apply_filters( 'get_group_id', $group ); // Default group if ( empty( $group ) || ! is_array( $group ) ) { $group = $this->get_prefix(); } else { $group = implode( '_', $group ); } // Set group attribute $this->set_attribute( $attrs, $g_props->attr, $group ); unset( $g ); } // Filter attributes $attrs = $this->util->apply_filters( 'process_link_attributes', $attrs ); // Update link in content $link_new = 'util->build_attribute_string( $attrs ) . '>'; $content = str_replace( $link, $link_new, $content ); } // Handle widget content if ( ! ! $this->widget_processing && 'the_content' === current_filter() ) { $content = $this->exclude_wrap( $content ); } return $content; } /** * Retrieve HTML links in content * @param string $content Content to get links from * @param bool (optional) $unique Remove duplicates from returned links (Default: FALSE) * @return array Links in content */ function get_links( $content, $unique = false ) { $rgx = "/\).)*\shref=[^\>\<]++\>/i"; $links = []; preg_match_all( $rgx, $content, $links ); $links = $links[0]; if ( $unique ) { $links = array_unique( $links ); } return $links; } /** * Validate URI * Matches specified URI against internal & external regex patterns * URI is **invalid** if it matches a regex * * @param string $uri URI to validate * @return bool TRUE if URI is valid */ protected function validate_uri( $uri ) { static $patterns = null; // Previously-validated URI if ( isset( $this->validated_uris[ $uri ] ) ) { return $this->validated_uris[ $uri ]; } $valid = true; // Boilerplate validation if ( empty( $uri ) // Empty || 0 === strpos( $uri, '#' ) // Anchor ) { $valid = false; } // Regex matching if ( $valid ) { // Get patterns if ( is_null( $patterns ) ) { $patterns = $this->util->apply_filters( 'validate_uri_regex', array() ); } // Iterate through patterns until match found foreach ( $patterns as $pattern ) { if ( 1 === preg_match( $pattern, $uri ) ) { $valid = false; break; } } } // Cache $this->validated_uris[ $uri ] = $valid; return $valid; } /** * Add URI validation regex pattern * @param */ public function validate_uri_regex_default( $patterns ) { $patterns[] = '@^https?://[^/]*(wikipedia|wikimedia)\.org/wiki/file:.*$@i'; return $patterns; } /* Client */ /** * Checks if output should be loaded in current request * @uses `is_enabled()` * @uses `has_cached_media_items()` * @return bool TRUE if output is being loaded into client */ public function is_request_valid() { return ( $this->is_enabled() && $this->has_cached_media_items() ) ? true : false; } /** * Sets options/settings to initialize lightbox functionality on page load * @return void */ function client_init( $client_script ) { // Get options $options = $this->options->build_client_output(); // Load UI Strings $labels = $this->build_labels(); if ( ! empty( $labels ) ) { $options['ui_labels'] = $labels; } // Build client output $client_script[] = $this->util->call_client_method( 'View.init', $options ); return $client_script; } /** * Output code in footer * > Media attachment URLs * @uses `_wp_attached_file` to match attachment ID to URI * @uses `_wp_attachment_metadata` to retrieve attachment metadata */ function client_footer() { if ( ! $this->has_cached_media_items() ) { return false; } // Set up hooks add_action( 'wp_print_footer_scripts', $this->m( 'client_footer_script' ) ); // Build client output $this->util->do_action( 'footer' ); } /** * Output client footer scripts */ function client_footer_script() { $client_script = $this->util->apply_filters( 'footer_script', array() ); if ( ! empty( $client_script ) ) { echo $this->util->build_script_element( $client_script, 'footer', true, true ); } } /** * Add media information to client output * * @param array $client_script Client script commands. * @return array Modified script commands. * TODO Refactor */ public function client_script_media( $client_script ) { global $wpdb; // Init. $this->media_items = $this->get_cached_media_items(); // Extract internal links for additional processing. $m_internals = []; foreach ( $this->media_items as $key => $p ) { if ( $p->internal ) { $m_internals[ $key ] =& $this->media_items[ $key ]; } } // Cleanup. unset( $key, $p ); // Process internal links. if ( ! empty( $m_internals ) ) { $uris_base = []; $uri_prefix = wp_upload_dir(); $uri_prefix = $this->util->normalize_path( $uri_prefix['baseurl'], true ); foreach ( $m_internals as $key => $p ) { // Prepare internal links. // Create relative URIs for attachment data retrieval. if ( ! $p->id && strpos( $p->source, $uri_prefix ) === 0 ) { $uris_base[ str_replace( $uri_prefix, '', $p->source ) ] = $key; } } // Cleanup. unset( $key, $p ); // Retrieve attachment IDs. $uris_flat = "('" . implode( "','", array_keys( $uris_base ) ) . "')"; $q = $wpdb->prepare( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE `meta_key` = %s AND LOWER(`meta_value`) IN $uris_flat LIMIT %d", '_wp_attached_file', count( $uris_base ) ); $pids = $wpdb->get_results( $q ); // Match IDs to URIs. if ( $pids ) { foreach ( $pids as $pd ) { $file =& $pd->meta_value; if ( isset( $uris_base[ $file ] ) ) { $m_internals[ $uris_base[ $file ] ]->id = absint( $pd->post_id ); } } } // Cleanup. unset( $uris_base, $uris_flat, $q, $pids, $pd, $file ); } // Process items with attachment IDs. $pids = []; foreach ( $this->media_items as $key => $p ) { // Add post ID to query. if ( ! ! $p->id ) { // Create array for ID (support multiple URIs per ID). if ( ! isset( $pids[ $p->id ] ) ) { $pids[ $p->id ] = []; } // Add URI to ID. $pids[ $p->id ][] = $key; } } // Cleanup. unset( $key, $p ); // Retrieve attachment properties. if ( ! empty( $pids ) ) { $pids_flat = array_keys( $pids ); // Retrieve attachment post data. $atts = get_posts( array( 'post_type' => 'attachment', 'include' => $pids_flat, ) ); // Process attachments. if ( $atts ) { $props_post_map = [ 'title' => 'post_title', 'caption' => 'post_excerpt', 'description' => 'post_content', ]; foreach ( $atts as $att ) { $data = []; // Remap post data to metadata. foreach ( $props_post_map as $props_post_key => $props_post_source ) { $data[ $props_post_key ] = $att->{$props_post_source}; } // Cleanup. unset( $props_post_key, $props_post_source ); // Save data to corresponding media item(s). if ( isset( $pids[ $att->ID ] ) ) { foreach ( $pids[ $att->ID ] as $key ) { $this->media_items[ $key ] = (object) array_merge( (array) $this->media_items[ $key ], $data ); } } } // Cleanup. unset( $att, $data ); } // Cleanup. unset( $atts, $atts_meta, $m, $a, $uri, $pids, $pids_flat ); } // Filter media items. $this->media_items = $this->util->apply_filters( 'media_items', $this->media_items ); // Build client output. $obj = 'View.assets'; $client_script[] = $this->util->extend_client_object( $obj, $this->media_items ); return $client_script; } /*-** Media **-*/ /** * Cache media properties for later processing * @uses array self::$media_items_raw Stores media items for output * @param object $uri URI to cache * Members * > raw: Raw Link URI * > source: Source URI (e.g. for attachment URIs) * @param string $type Media type (image, attachment, etc.) * @param bool $internal TRUE if media is internal (e.g. attachment) * @param array $props (optional) Properties to store for item (Default: NULL) * @return string Unique ID for cached media item */ private function cache_media_item( $uri, $type, $internal, $props = null ) { // Validate. if ( ! is_object( $uri ) || ! is_string( $type ) ) { return false; } // Check if URI already cached. $key = $this->get_media_item_id( $uri->source ); // Cache new item. if ( null === $key ) { // Generate Unique ID. do { $key = mt_rand(); } while ( isset( $this->media_items_raw['props'][ $key ] ) ); // Build properties object. $i = $this->media_item_template; if ( is_array( $props ) && ! empty( $props ) ) { $i = array_merge( $i, $props ); } $i = array_merge( $i, [ 'type' => $type, 'source' => $uri->source, 'internal' => $internal, ] ); // Cache item properties. $this->media_items_raw['props'][ $key ] = (object) $i; // Cache Source URI (point to properties object). $this->media_items_raw['uri'][ $uri->source ] = $key; } return $key; } /** * Retrieve ID for media item * @uses self::$media_items_raw * @param string $uri Media item URI * @return string|null Media item ID (Default: NULL if URI doesn't exist in collection) */ private function get_media_item_id( $uri ) { if ( $this->media_item_cached( $uri ) ) { return $this->media_items_raw['uri'][ $uri ]; } return null; } /** * Checks if media item has already been cached * @param string $uri URI of media item * @return boolean Whether media item has been cached */ private function media_item_cached( $uri ) { return ( is_string( $uri ) && ! empty( $uri ) && isset( $this->media_items_raw['uri'][ $uri ] ) ) ? true : false; } /** * Retrieve cached media item * @param string $uri Media item URI * @return object|null Media item properties (NULL if not set) */ private function get_cached_media_item( $uri ) { $key = $this->get_media_item_id( $uri ); if ( null !== $key ) { return $this->media_items_raw['props'][ $key ]; } return null; } /** * Retrieve cached media items (properties) * @uses self::$media_items_raw * @return array Cached media items (objects) */ private function &get_cached_media_items() { return $this->media_items_raw['props']; } /** * Check if media items have been cached * @return boolean */ private function has_cached_media_items() { return ( empty( $this->media_items_raw['props'] ) ) ? false : true; } /*-** Exclusion **-*/ /** * Retrieve exclude object * Initialize object properties if necessary * @return object Exclude properties */ private function get_exclude() { // Initialize exclude data if ( ! is_object( $this->exclude ) ) { $this->exclude = (object) array( 'tags' => $this->get_exclude_tags(), 'ph' => $this->get_exclude_placeholder(), 'group_default' => 'default', 'cache' => array(), ); } return $this->exclude; } /** * Get exclusion tags (open/close) * Example: open => [slb_exclude], close => [/slb_exclude] * * @return object Exclusion tags */ private function get_exclude_tags() { static $tags = null; if ( null === $tags ) { /* Init tag elements */ $tags = (object) [ // Tag base. 'base' => $this->add_prefix( 'exclude' ), ]; // Opening tag. $tags->open = $this->util->add_wrapper( $tags->base ); // Closing tag. $tags->close = $this->util->add_wrapper( $tags->base, '[/', ']' ); /* Build tag search pattern */ // Pattern delimeter. $dlm = '#'; // Pattern flags. $flags = 's'; // Tag search pattern. $tags->search = $dlm . preg_quote( $tags->open, $dlm ) . '(.*?)' . preg_quote( $tags->close, $dlm ) . $dlm . $flags; } return $tags; } /** * Get exclusion tag ("[slb_exclude]") * @uses `get_exclude_tags()` to retrieve tag * * @param string $type (optional) Tag to retrieve (open or close) * @return string Exclusion tag */ private function get_exclude_tag( $type = 'open' ) { // Validate $tags = $this->get_exclude_tags(); if ( ! isset( $tags->{$type} ) ) { $type = 'open'; } return $tags->{$type}; } /** * Build exclude placeholder * @return object Exclude placeholder properties */ private function get_exclude_placeholder() { static $ph; if ( ! is_object( $ph ) ) { $ph = (object) array( 'base' => $this->add_prefix( 'exclude_temp' ), 'open' => '{{', 'close' => '}}', 'attrs' => array( 'group' => '', 'key' => '', ), ); // Search Patterns $sub = '(.+?)'; $dlm = '#'; $flags = 's'; $ph->search = $dlm . preg_quote( $ph->open, $dlm ) . $ph->base . '\s+' . $sub . preg_quote( $ph->close, $dlm ) . $dlm . $flags; $ph->search_group = str_replace( $sub, '(group="%s"\s+.?)', $ph->search ); // Templates $attr_string = ''; foreach ( $ph->attrs as $attr => $val ) { $attr_string .= ' ' . $attr . '="%s"'; } $ph->template = $ph->open . $ph->base . $attr_string . $ph->close; } return $ph; } /** * Wrap content in exclusion tags * @uses `get_exclude_tag()` to wrap content with exclusion tag * @param string $content Content to exclude * @return string Content wrapped in exclusion tags */ private function exclude_wrap( $content ) { // Validate if ( ! is_string( $content ) ) { $content = ''; } // Wrap $tags = $this->get_exclude_tags(); return $tags->open . $content . $tags->close; } /** * Remove excluded content * Caches content for restoring later * @param string $content Content to remove excluded content from * @return string Updated content */ public function exclude_content( $content, $group = null ) { $ex = $this->get_exclude(); // Setup cache if ( ! is_string( $group ) || empty( $group ) ) { $group = $ex->group_default; } if ( ! isset( $ex->cache[ $group ] ) ) { $ex->cache[ $group ] = array(); } $cache =& $ex->cache[ $group ]; $content = $this->util->apply_filters( 'pre_exclude_content', $content ); // Search content $matches = null; if ( false !== strpos( $content, $ex->tags->open ) && preg_match_all( $ex->tags->search, $content, $matches ) ) { // Determine index $idx = ( ! ! end( $cache ) ) ? key( $cache ) : -1; $ph = array(); foreach ( $matches[1] as $midx => $match ) { // Update index $idx++; // Cache content $cache[ $idx ] = $match; // Build placeholder $ph[] = sprintf( $ex->ph->template, $group, $idx ); } unset( $midx, $match ); // Replace content with placeholder $content = str_replace( $matches[0], $ph, $content ); // Cleanup unset( $matches, $ph ); } return $content; } /** * Exclude shortcodes from link activation * @param string $content Content to exclude shortcodes from * @return string Content with shortcodes excluded */ public function exclude_shortcodes( $content ) { // Get shortcodes to exclude $shortcodes = $this->util->apply_filters( 'exclude_shortcodes', array( $this->add_prefix( 'group' ) ) ); // Set callback $shortcodes = array_fill_keys( $shortcodes, $this->m( 'exclude_shortcodes_handler' ) ); return $this->util->do_shortcode( $content, $shortcodes ); } /** * Wrap shortcode in exclude tags * @uses Util->make_shortcode() to rebuild original shortcode * * @param array $attr Shortcode attributes * @param string $content Content enclosed in shortcode * @param string $tag Shortcode name * @return string Excluded shortcode */ public function exclude_shortcodes_handler( $attr, $content, $tag ) { $code = $this->util->make_shortcode( $tag, $attr, $content ); // Exclude shortcode return $this->exclude_wrap( $code ); } /** * Restore excluded content * @param string $content Content to restore excluded content to * @return string Content with excluded content restored */ public function restore_excluded_content( $content, $group = null ) { $ex = $this->get_exclude(); // Setup cache if ( ! is_string( $group ) || empty( $group ) ) { $group = $ex->group_default; } // Nothing to restore if cache group doesn't exist if ( ! isset( $ex->cache[ $group ] ) ) { return $content; } $cache =& $ex->cache[ $group ]; // Search content for placeholders $matches = null; if ( false !== strpos( $content, $ex->ph->open . $ex->ph->base ) && preg_match_all( $ex->ph->search, $content, $matches ) ) { // Restore placeholders foreach ( $matches[1] as $idx => $ph ) { // Parse placeholder attributes $attrs = $this->util->parse_attribute_string( $ph, $ex->ph->attrs ); // Validate if ( $attrs['group'] !== $group ) { continue; } // Restore content $attrs['key'] = intval( $attrs['key'] ); $key = $attrs['key']; if ( isset( $cache[ $key ] ) ) { $content = str_replace( $matches[0][ $idx ], $cache[ $key ], $content ); } } // Cleanup unset( $idx, $ph, $matches, $key ); } return $content; } /*-** Grouping **-*/ /** * Builds wrapper for grouping * @return string Format for wrapping content in group */ function group_get_wrapper() { static $fmt = null; if ( is_null( $fmt ) ) { $fmt = $this->util->make_shortcode( $this->add_prefix( 'group' ), null, '%s' ); } return $fmt; } /** * Wraps shortcodes for automatic grouping * @uses `the_content` Filter hook * @uses group_shortcodes_handler to Wrap shortcodes for grouping * @param string $content Post content * @return string Modified post content */ function group_shortcodes( $content ) { if ( ! $this->is_content_valid( $content ) ) { return $content; } // Setup shortcodes to wrap $shortcodes = $this->util->apply_filters( 'group_shortcodes', array( 'gallery', 'nggallery' ) ); // Set custom callback $shortcodes = array_fill_keys( $shortcodes, $this->m( 'group_shortcodes_handler' ) ); // Process gallery shortcodes return $this->util->do_shortcode( $content, $shortcodes ); } /** * Groups shortcodes for later processing * @param array $attr Shortcode attributes * @param string $content Content enclosed in shortcode * @param string $tag Shortcode name * @return string Grouped shortcode */ function group_shortcodes_handler( $attr, $content, $tag ) { $code = $this->util->make_shortcode( $tag, $attr, $content ); // Wrap shortcode return sprintf( $this->group_get_wrapper(), $code ); } /** * Activate groups in content * @param string $content Content to activate * @return string Updated content */ public function activate_groups( $content ) { return $this->util->do_shortcode( $content, array( $this->add_prefix( 'group' ) => $this->m( 'activate_groups_handler' ) ) ); } /** * Groups shortcodes for later processing * @param array $attr Shortcode attributes * @param string $content Content enclosed in shortcode * @param string $tag Shortcode name * @return string Grouped shortcode */ function activate_groups_handler( $attr, $content, $tag ) { // Get Group ID // Custom group if ( isset( $attr['id'] ) ) { $group = $attr['id']; trim( $group ); } // Automatically-generated group if ( empty( $group ) ) { $group = 'auto_' . ( ++$this->groups['auto'] ); } return $this->process_links( $content, $group ); } /*-** Widgets **-*/ /** * Set widget up for processing/activation * Buffers widget output for further processing * @param array $widget_args Widget arguments * @return void */ public function widget_process_start( $widget_args ) { // Do not continue if a widget is currently being processed (avoid nested processing) if ( 0 < $this->widget_processing_level ) { return; } // Start widget processing $this->widget_processing = true; $this->widget_processing_params = $widget_args; // Enable widget grouping if ( $this->options->get_bool( 'group_widget' ) ) { $this->util->add_filter( 'get_group_id', $this->m( 'widget_group_id' ) ); } // Begin output buffer ob_start(); } /** * Handles inter-widget processing * After widget output generated, Before next widget starts * @param array $params New widget parameters */ public function widget_process_inter( $params ) { $this->widget_process_finish(); return $params; } /** * Complete widget processing * Activate widget output * @uses $widget_processing * @uses $widget_processing_level * @uses $widget_processing_params * @return void */ public function widget_process_finish() { /** * Stop processing on conditions: * - No widget is being processed * - Processing a nested widget */ if ( ! $this->widget_processing || 0 < $this->widget_processing_level ) { return; } // Activate widget output $out = $this->activate_links( ob_get_clean() ); // Clear grouping callback if ( $this->options->get_bool( 'group_widget' ) ) { $this->util->remove_filter( 'get_group_id', $this->m( 'widget_group_id' ) ); } // End widget processing $this->widget_processing = false; $this->widget_processing_params = null; // Output widget echo $out; } /** * Add widget ID to link group ID * Widget ID precedes all other group segments * @uses `SLB::get_group_id` filter * @param array $group_segments Group ID segments * @return array Modified group ID segments */ public function widget_group_id( $group_segments ) { // Add current widget ID to group ID if ( isset( $this->widget_processing_params['id'] ) ) { array_unshift( $group_segments, $this->widget_processing_params['id'] ); } return $group_segments; } /** * Handles nested activation in widgets * @uses widget_processing * @uses $widget_processing_level * @return void */ public function widget_process_nested() { // Stop if no widget is being processed if ( ! $this->widget_processing ) { return; } // Increment nesting level $this->widget_processing_level++; } /** * Mark the end of a nested widget * @uses $widget_processing_level */ public function widget_process_nested_finish() { // Decrement nesting level if ( 0 < $this->widget_processing_level ) { $this->widget_processing_level--; } } /** * Begin blocking widget activation * @return void */ public function widget_block_start() { $this->util->add_filter( 'is_content_valid', $this->m( 'widget_block_handle' ) ); } /** * Stop blocking widget activation * @return void */ public function widget_block_finish() { $this->util->remove_filter( 'is_content_valid', $this->m( 'widget_block_handle' ) ); } /** * Handle widget activation blocking */ public function widget_block_handle( $is_content_valid ) { return false; } /*-** Menus **-*/ /** * Process navigation menu links * * @see wp_nav_menu()/filter: wp_nav_menu * * @param string $nav_menu HTML content for navigation menu. * @param object $args Navigation menu's arguments. */ public function menu_process( $nav_menu, $args ) { // Grouping if ( $this->options->get_bool( 'group_menu' ) ) { // Generate group ID for menu $group = 'menu'; $sep = '_'; if ( ! empty( $args->menu_id ) ) { $group .= $sep . $args->menu_id; } elseif ( ! empty( $args->menu ) ) { $group .= $sep . ( ( is_object( $args->menu ) ) ? $args->menu->slug : $args->menu ); } $group = $this->group_id_unique( $group ); } else { $group = null; } // Process menu $nav_menu = $this->activate_links( $nav_menu, $group ); return $nav_menu; } /** * Generate unique group ID * * @param string $group Group ID to check * @return string Unique group ID */ public function group_id_unique( $group ) { static $groups = array(); while ( in_array( $group, $groups, true ) ) { $patt = '#-(\d+)$#'; if ( preg_match( $patt, $group, $matches ) ) { $group = preg_replace( $patt, '-' . ( ++$matches[1] ), $group ); } else { $group = $group . '-1'; } } // Add final group ID to array $groups[] = $group; return $group; } /*-** Helpers **-*/ /** * Build attribute name * Makes sure name is only prefixed once * @param string $name (optional) Attribute base name * @return string Formatted attribute name */ function make_attribute_name( $name = '' ) { // Validate if ( ! is_string( $name ) ) { $name = ''; } else { $name = trim( $name ); } // Setup $sep = '-'; $top = 'data'; // Generate valid name if ( strpos( $name, $top . $sep . $this->get_prefix() ) !== 0 ) { $name = $top . $sep . $this->add_prefix( $name, $sep ); } return $name; } /** * Sets attribute value. * * Attribute is added to array if it does not already exist. * * @param string|array $attrs Array to set attribute on (Passed by reference). * @param string $name Name of attribute to set. * @param scalar $value Optional. Attribute value. * @return array Updated attributes. */ function set_attribute( &$attrs, $name, $value = true ) { // Validate $attrs = $this->get_attributes( $attrs, false ); // Stop if attribute name or value is invalid. if ( ! is_string( $name ) || empty( trim( $name ) ) || ! is_scalar( $value ) ) { return $attrs; } // Set attribute value. $attrs = array_merge( $attrs, array( $this->make_attribute_name( $name ) => $value ) ); return $attrs; } /** * Converts attribute string into array. * * @param string|array $attrs Attribute string to convert. Associative array also accepted. * @param bool $internal Optional. Return only internal attributes. Default True. * @return array Associative array of attributes (`attribute-name => attribute-value`). */ function get_attributes( $attrs, $internal = true ) { // Parse attribute string. $attrs = $this->util->parse_attribute_string( $attrs ); // Include only internal attributes (if necessary). if ( ! ! $internal && ! empty( $attrs ) ) { $prefix = $this->make_attribute_name(); $attrs = array_filter( $attrs, function( $key ) use ( $prefix ) { return ( strpos( $key, $prefix ) === 0 ); }, ARRAY_FILTER_USE_KEY ); } return $attrs; } /** * Retrieves an attributes value from attribute string or array. * * @param string|array $attrs Attribute string to retrieve attribute value from. * Associative array also accepted. * @param string $attr Attribute to retrieve value for. * @param bool $internal Optional. Retrieve internal attribute. Default true. * @return scalar|null Attribute value. Null if attribute is invalid or does not exist. */ function get_attribute( $attrs, $attr, $internal = true ) { // Validate. $invalid = null; if ( ! is_string( $attr ) || empty( trim( $attr ) ) ) { return $invalid; } $attrs = $this->get_attributes( $attrs, $internal ); if ( empty( $attrs ) ) { return $invalid; } // Format attribute name. $attr = ( ! ! $internal ) ? $this->make_attribute_name( $attr ) : trim( $attr ); // Stop if attribute does not exist or value is invalid. if ( ! isset( $attrs[ $attr ] ) || ! is_scalar( $attrs[ $attr ] ) ) { return $invalid; } // Retreive value. $ret = $attrs[ $attr ]; // Validate value (type-specific). if ( is_string( $ret ) ) { $ret = trim( $ret ); if ( '' === $ret ) { return $invalid; } } return $ret; } /** * Checks if attribute exists. * * @param string|array $attrs Attribute string to retrieve attribute value from. * Associative array also accepted. * @param string $attr Attribute to retrieve value for. * @param bool $internal Optional. Retrieve internal attribute. Default true. * @return bool True if attribute exists. False if attribute does not exist. */ function has_attribute( $attrs, $attr, $internal = true ) { return ( null !== $this->get_attribute( $attrs, $attr, $internal ) ); } /** * Build JS object of UI strings when initializing lightbox * @return array UI strings */ private function build_labels() { $ret = array(); /* Get all UI options */ $prefix = 'txt_'; $opt_strings = array_filter( array_keys( $this->options->get_items() ), function ( $opt ) use ( $prefix ) { return ( strpos( $opt, $prefix ) === 0 ); } ); if ( count( $opt_strings ) ) { /* Build array of UI options */ foreach ( $opt_strings as $key ) { $name = substr( $key, strlen( $prefix ) ); $ret[ $name ] = $this->options->get_value( $key ); } } return $ret; } }