'error', ); $args = wp_parse_args($args, $defaults); $name = $args['name']; // Validated here rather than at either output site because both of them carry it: // the embed script is enabled by the query-string copy but runs the copy it reads // back off the div's data-after attribute. This is also the only guard on the // [juicer] path -- the Elementor widget sanitizes its own settings, but a // shortcode written straight into post content reaches this point unfiltered. if (isset($args['after'])) { $args['after'] = juicer_sanitize_after_callback($args['after']); if ($args['after'] === '') { unset($args['after']); } } // Legacy backbone feeds load embed-no-jquery.js, which still references // jQuery (the "nojquery" flag means "don't bundle it, use the page's // jQuery"). Keep jQuery enqueued so themes that don't load it on every // page still get it when the shortcode runs. Feed 2.0 doesn't need it // but enqueueing is idempotent and other plugins/themes typically load // jQuery anyway. wp_enqueue_script('jquery'); // Output
+ ', esc_attr($name), $div_data_attrs, esc_url($script_url) ); } } // The embed script evaluates this value as a function *body*, not as a name it looks up, // so it is the one feed attribute that becomes executable code on the visitor's page. // A call is therefore the form that has always done anything: a bare name on its own is // an expression statement that never invokes. // // Accepted: an optionally dotted name, optionally called with no argument or with the // callback's own 'event' -- myCallback, myCallback(), window.MyApp.render(event). // // The argument list is restricted to 'event' rather than to identifiers generally, // because 'new Function' resolves every other identifier against the global scope: // permitting them would accept alert(document.cookie) as readily as myCallback(event). // With this restriction a value can name a function the page already defines and hand it // the event, and cannot express anything else. // // Bare names stay accepted because they have always been emitted and rejecting them would // change working pages; as a function body they remain the no-ops they have always been. function juicer_sanitize_after_callback($value) { if (!is_string($value)) { return ''; } $identifier = '[A-Za-z_$][A-Za-z0-9_$]*'; $name = $identifier . '(?:\.' . $identifier . ')*'; $pattern = '/^' . $name . '(?:\s*\(\s*(?:event)?\s*\))?\s*;?$/'; $value = trim($value); return preg_match($pattern, $value) ? $value : ''; } // Builds the key=value pairs appended to the embed script URL. // // Values are URL-encoded, not HTML-escaped: these become query-string parameters, so a // value containing '&' or '#' would otherwise start a new parameter or truncate the URL // at a fragment. A filter of "Instagram,#tbt" silently dropped every parameter after it. function generate_attributes($array) { $attrs = array(); foreach ($array as $key => $val) { if ($key == 'name') { continue; } // empty() rather than a strict comparison, to keep the long-standing behaviour // that '0' and '' are both omitted from the URL. if (empty($val)) { continue; } $clean_key = str_replace('data-', '', $key); array_push($attrs, rawurlencode($clean_key) . '=' . rawurlencode($val)); } return $attrs; } function juicer_feed($args) { $feed = new Juicer_Feed(); echo $feed->render($args); } function juicer_shortcode($args) { extract(shortcode_atts(array( 'name' => 'error', ), $args )); $feed = new Juicer_Feed(); return $feed->render($args); } add_shortcode('juicer', 'juicer_shortcode'); function juicer_activate() { // Only set cookie if headers haven't been sent if (!headers_sent()) { $expires = time() + HOUR_IN_SECONDS; // Deliberately not httponly: the settings page clears this cookie from JavaScript, // and it cannot clear it server-side because the page is rendered long after the // headers are sent. The value is a non-secret first-run flag, so keeping it // readable costs nothing. if (version_compare(PHP_VERSION, '7.3', '>=')) { setcookie('juicer_welcome', 'true', array( 'expires' => $expires, 'path' => '/', 'secure' => is_ssl(), 'httponly' => false, 'samesite' => 'Lax', )); } else { // The options array, and with it samesite, needs PHP 7.3. setcookie('juicer_welcome', 'true', $expires, '/', '', is_ssl(), false); } } } register_activation_hook(__FILE__, 'juicer_activate'); // Setup menu for admin section function juicer_set_admin_menu() { $hook_suffix = add_menu_page( esc_html__('Juicer', 'juicer'), esc_html__('Juicer', 'juicer'), 'manage_options', 'juicer-settings', 'juicer_set_settings_page', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjIiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2MiA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQ4LjYwOTUgMTMuNDI4NUM1MS4wMDMzIDEzLjQyODUgNTIuOTMzOCAxMS40OTggNTIuOTMzOCA5LjEwNDIzVjYuMzI0MzJDNTIuODU2NiAzLjkzMDUxIDUxLjAwMzMgMi4wMDAwMiA0OC42MDk1IDIuMDAwMDJDNDYuMjE1NyAyLjAwMDAyIDQ0LjI4NTIgMy45MzA1MSA0NC4yODUyIDYuMzI0MzJWOS4xODE0NUM0NC4yODUyIDExLjQ5OCA0Ni4yMTU3IDEzLjQyODUgNDguNjA5NSAxMy40Mjg1WiIgZmlsbD0iIzlEQTJBNyIvPgo8cGF0aCBkPSJNNTIuODU2OSA0MS4yMjc1VjIzLjAwMzdDNTIuODU2OSAyMC44NDE1IDUwLjkyNjQgMTkuMTQyNyA0OC41MzI2IDE5LjE0MjdDNDYuMTM4OCAxOS4xNDI3IDQ0LjIwODMgMjAuOTE4OCA0NC4yMDgzIDIzLjAwMzdWMzMuNDI4M1YzNS41OTA1QzQ0LjIwODMgMzcuOTg0MyA0Mi4yNzc4IDM5LjkxNDggMzkuODg0IDM5LjkxNDhDMzcuNDkwMiAzOS45MTQ4IDM1LjYzNjkgMzcuOTg0MyAzNS41NTk3IDM1LjY2NzdWMzQuODk1NVYzNC4yMDA1SDM1LjQ4MjVDMzUuMzI4IDMyLjExNTYgMzMuNTUyIDMwLjU3MTIgMzEuNDY3MSAzMC41NzEyQzI5LjMwNDkgMzAuNTcxMiAyNy42MDYxIDMyLjExNTYgMjcuMjIgMzQuMTIzM0gyNy4xNDI4VjM0LjgxODNWMzcuNjc1NEMyNy4xNDI4IDQwLjA2OTIgMjUuMjEyMyA0MS45MjI1IDIyLjgxODUgNDEuOTIyNUMyMC40MjQ3IDQxLjkyMjUgMTguNDk0MiAzOS45OTIgMTguNDk0MiAzNy41OTgyVjMzLjM1MTFDMTMuMzk3NyAzMy4yNzM5IDEwIDM0LjQzMjIgMTAgNDEuMjI3NUMxMCA1Mi43MzMzIDE5LjU3NTIgNjEuOTk5NiAzMS40NjcxIDYxLjk5OTZDNDMuMjgxNyA2Mi4wNzY4IDUyLjg1NjkgNTIuNzMzMyA1Mi44NTY5IDQxLjIyNzVaIiBmaWxsPSIjOURBMkE3Ii8+CjxwYXRoIGQ9Ik0zMS40NjY5IDI0Ljg1N0MzMy44NjA3IDI0Ljg1NyAzNS43OTEyIDIyLjkyNjUgMzUuNzkxMiAyMC41MzI3VjE3LjY3NTVDMzUuNzkxMiAxNS4yODE3IDMzLjg2MDcgMTMuMzUxMiAzMS40NjY5IDEzLjM1MTJDMjkuMDczMSAxMy4zNTEyIDI3LjE0MjYgMTUuMjgxNyAyNy4xNDI2IDE3LjY3NTVWMjAuNTMyN0MyNy4xNDI2IDIyLjkyNjUgMjkuMDczMSAyNC44NTcgMzEuNDY2OSAyNC44NTdaIiBmaWxsPSIjOURBMkE3Ii8+CjxwYXRoIGQ9Ik0yMy41MTMxIDQyLjg0OTNDMjMuMzU4NyA0Mi41NDA1IDIzLjIwNDIgNDIuMzA4OCAyMy4wNDk4IDQxLjk5OTlDMjIuOTcyNiA0MS45OTk5IDIyLjk3MjYgNDEuOTk5OSAyMi44OTU0IDQxLjk5OTlDMjAuNTAxNSA0MS45OTk5IDE4LjU3MTEgNDAuMDY5NCAxOC41NzExIDM3LjY3NTZWMzMuNDI4NUMxNi4xNzcyIDMzLjM1MTMgMTQuMTY5NSAzMy41ODMgMTIuNzAyNCAzNC40MzI0QzEyLjM5MzUgMzUuNDM2MyAxMi4yMzkgMzYuNTk0NiAxMi4yMzkgMzguMDYxN0MxMi4yMzkgNDkuNTY3NSAyMS44MTQzIDU4LjgzMzggMzMuNzA2MSA1OC44MzM4QzM5LjM0MzEgNTguODMzOCA0NC41OTQxIDU3LjA1NzggNDguNDU1MSA1My44MTQ1QzQ5Ljk5OTUgNTEuODg0IDUxLjIzNSA0OS42NDQ3IDUyLjAwNzIgNDcuMTczNkM0My4wNDk3IDUzLjk2OSAyOS44NDUxIDUyLjg4NzkgMjMuNTEzMSA0Mi44NDkzWiIgZmlsbD0iIzlEQTJBNyIvPgo8cGF0aCBkPSJNMjEuMjc0MSA0NS45MzhDMTkuNDIwOCA0My4wMDM2IDE4Ljg4MDMgNDAuNjg3IDE4LjcyNTggMzguNzU2NUMxOC42NDg2IDM4LjQ0NzcgMTguNTcxNCAzOC4wNjE2IDE4LjU3MTQgMzcuNzUyN1YzMy41MDU2QzEzLjM5NzcgMzMuMjczOSAxMCAzNC41MDk1IDEwIDQxLjIyNzZDMTAgNTIuNzMzMyAxOS41NzUyIDYxLjk5OTcgMzEuNDY3MSA2MS45OTk3QzQxLjExOTUgNjEuOTk5NyA0OS4zMDQ4IDU1LjgyMjEgNTIuMDA3NSA0Ny4yNTA3QzQzLjEyNzIgNTYuMjg1NCAyOC4yMjM4IDU2Ljk4MDQgMjEuMjc0MSA0NS45MzhaIiBmaWxsPSIjOURBMkE3Ii8+Cjwvc3ZnPgo=', 99 ); } add_action('admin_menu', 'juicer_set_admin_menu'); // Load custom admin CSS function load_custom_wp_admin_style() { wp_enqueue_style('juicer-admin-css', plugin_dir_url(__FILE__) . 'includes/admin/css/admin.css', array(), JUICER_VERSION); } add_action('admin_enqueue_scripts', 'load_custom_wp_admin_style'); // Load custom admin JavaScript and localize function load_custom_wp_admin_script($hook_suffix) { // Check if we're on the Juicer settings page or any admin page if ($hook_suffix === 'toplevel_page_juicer-settings' || $hook_suffix === 'index.php') { wp_enqueue_script('juicer-admin-js', plugin_dir_url(__FILE__) . 'includes/admin/js/admin.js', array('jquery'), JUICER_VERSION, true); // Localize the script with your data wp_localize_script('juicer-admin-js', 'juicer_admin', array( 'ajax_url' => admin_url('admin-ajax.php'), 'security' => wp_create_nonce('juicer_nonce') )); } } add_action('admin_enqueue_scripts', 'load_custom_wp_admin_script'); // Setup guide page function juicer_set_settings_page() { include('includes/admin/settings.php'); } // Plugin setup guide link function juicer_plugin_action_links($links) { $links[] = '' . __('Setup guide') . ''; return $links; } add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'juicer_plugin_action_links'); // Plugin review notice // Check for feed existence and set option function juicer_check_feed_existence() { $host_url = home_url(); $response = wp_remote_get('https://www.juicer.io/api/hosts?hostname=' . $host_url); // Default to no feeds existing update_option('juicer_feed_exists', false); if (!is_wp_error($response)) { $response_code = wp_remote_retrieve_response_code($response); if ($response_code !== 404) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); // json_decode returns null on malformed JSON, and the endpoint is not // guaranteed to answer with a list, so the shape is checked before iterating. if (!is_array($data)) { return; } foreach ($data as $item) { if (is_array($item) && isset($item['feed_id'])) { // Update the option to true as feed exists update_option('juicer_feed_exists', true); break; } } } } } add_action('admin_init', 'juicer_check_feed_existence'); // Display review notice function juicer_review_notice() { // Get the current screen object to check the hook suffix $current_screen = get_current_screen(); $hook_suffix = $current_screen->base; // Check if the current page is one of the allowed pages if ($hook_suffix === 'toplevel_page_juicer-settings' || $hook_suffix === 'dashboard') { if (get_option('juicer_feed_exists') && juicer_should_show_review_notice()) { echo '
'; echo '
'; echo '
'; echo ''; echo '
'; echo '
'; echo 'Thanks for using Juicer!'; echo '

If you enjoy our plugin, would you consider leaving us a 5-star review on Wordpress.org?

'; echo '
'; echo 'Sure, I’d love to '; echo ' '; echo ''; echo '
'; echo '
'; echo '
'; echo '
'; } } } add_action('admin_notices', 'juicer_review_notice'); // AJAX handler for dismissing the review notice function juicer_dismiss_review_notice() { check_ajax_referer('juicer_nonce', 'security'); if (current_user_can('manage_options')) { if (isset($_POST['dismiss_type'])) { $dismiss_type = sanitize_text_field($_POST['dismiss_type']); if ($dismiss_type === 'permanent') { // Never show again update_option('juicer_review_permanently_dismissed', 'yes'); wp_send_json_success('Permanently dismissed.'); } elseif ($dismiss_type === 'temporary') { // Dismiss for 7 days update_option('juicer_review_dismissed', time() + 7 * DAY_IN_SECONDS); wp_send_json_success('Temporarily dismissed.'); } else { wp_send_json_error('Invalid dismiss type.'); } } else { wp_send_json_error('Dismiss type not set.'); } } else { wp_send_json_error('Permission denied.'); } wp_die(); } add_action('wp_ajax_juicer_dismiss_review_notice', 'juicer_dismiss_review_notice'); // Check if the review notice should be displayed function juicer_should_show_review_notice() { if (get_option('juicer_review_permanently_dismissed') === 'yes') { return false; } $dismissed_until = get_option('juicer_review_dismissed'); if ($dismissed_until && time() < $dismissed_until) { return false; } return true; } // Register the Elementor widget function register_juicer_elementor_widget($widgets_manager) { if (defined('ELEMENTOR_VERSION') && class_exists('Elementor\Widget_Base')) { require_once plugin_dir_path(__FILE__) . 'includes/elementor/elementor-widget.php'; $widgets_manager->register(new \Elementor_Juicer_Widget()); } } add_action('elementor/widgets/register', 'register_juicer_elementor_widget'); // Enqueue custom CSS for Elementor editor function enqueue_juicer_elementor_editor_styles() { wp_enqueue_style( 'juicer-elementor-editor', plugin_dir_url(__FILE__) . 'includes/elementor/juicer-elementor.css', array(), JUICER_VERSION ); } add_action('elementor/frontend/after_enqueue_styles', 'enqueue_juicer_elementor_editor_styles'); add_action('elementor/editor/after_enqueue_styles', 'enqueue_juicer_elementor_editor_styles'); // The date picker library is bundled with the plugin instead of loaded from a CDN, so // wp-admin runs only code shipped in this release: there is no third-party origin to be // compromised, to go down, or to see the editor's requests. The two files are // daterangepicker 3.1.0 exactly as published on npm (MIT, Dan Grossman), copied verbatim. // // This replaces a pinned jsDelivr URL guarded by Subresource Integrity, which could not // hold: npm publishes no daterangepicker.min.js, so jsDelivr minified one on the fly with // Terser. Those bytes -- and so the hash -- change whenever jsDelivr upgrades Terser, and // SRI fails closed, so the picker would break in the editor with nothing logged. function juicer_register_daterangepicker_assets() { // Each handle is guarded separately: 'daterangepicker' is a generic name another // plugin may already own, and bailing out wholesale would leave the two Juicer-owned // handles unregistered, silently breaking the picker. if (!wp_script_is('daterangepicker', 'registered')) { wp_register_script('daterangepicker', plugin_dir_url(__FILE__) . 'includes/elementor/daterangepicker.js', array('jquery', 'moment'), JUICER_VERSION, true); } if (!wp_style_is('daterangepicker-css', 'registered')) { wp_register_style('daterangepicker-css', plugin_dir_url(__FILE__) . 'includes/elementor/daterangepicker.css', array(), JUICER_VERSION); } if (!wp_script_is('juicer-daterangepicker-init', 'registered')) { wp_register_script('juicer-daterangepicker-init', plugin_dir_url(__FILE__) . 'includes/elementor/daterangepicker-init.js', array('jquery', 'daterangepicker'), JUICER_VERSION, true); } } // Editor only: the date picker is a widget-panel control, so the library is never // needed on rendered pages. Registering it on the frontend hook too would ship a // third-party script to every visitor of a page using the widget. add_action('elementor/editor/before_enqueue_scripts', 'juicer_register_daterangepicker_assets'); function juicer_editor_has_juicer_widget() { // class_exists, not function_exists: function_exists never resolves a static method, // so checking for '\Elementor\Plugin::instance' would always be false. if (!isset($_GET['post']) || !class_exists('\Elementor\Plugin')) { return false; } $post_id = absint($_GET['post']); if (!$post_id) { return false; } $document = \Elementor\Plugin::instance()->documents->get($post_id); if (!$document) { return false; } $data = $document->get_elements_data(); return juicer_find_widget_in_data($data, 'juicer_widget'); } function juicer_find_widget_in_data($elements, $widget_type) { if (!is_array($elements)) { return false; } foreach ($elements as $element) { if (isset($element['widgetType']) && $element['widgetType'] === $widget_type) { return true; } if (!empty($element['elements']) && juicer_find_widget_in_data($element['elements'], $widget_type)) { return true; } } return false; } function juicer_enqueue_daterangepicker_in_editor() { if (!juicer_editor_has_juicer_widget()) { return; } wp_enqueue_script('moment'); wp_enqueue_script('daterangepicker'); wp_enqueue_style('daterangepicker-css'); wp_enqueue_script('juicer-daterangepicker-init'); } add_action('elementor/editor/after_enqueue_scripts', 'juicer_enqueue_daterangepicker_in_editor'); ?>