From c32f302a0ac8653d596d37393f026334f1b74486 Mon Sep 17 00:00:00 2001 From: Sybre Waaijer Date: Fri, 29 May 2020 01:01:50 +0200 Subject: [PATCH] Added access control constants. --- bootstrap/define.php | 19 +++++ bootstrap/update.php | 2 +- .../trunk/inc/classes/front.class.php | 83 ++++--------------- .../local/trunk/inc/classes/admin.class.php | 7 +- .../trunk/inc/classes/settings.class.php | 4 +- .../trunk/inc/traits/secure-post.trait.php | 3 +- extensions/premium/local/trunk/local.php | 4 +- extensions/premium/local/trunk/readme.md | 6 ++ .../monitor/trunk/inc/classes/admin.class.php | 30 ++++--- extensions/premium/monitor/trunk/monitor.php | 4 +- extensions/premium/monitor/trunk/readme.md | 6 ++ inc/classes/adminpages.class.php | 11 ++- inc/classes/ajax.class.php | 40 ++++----- inc/classes/core.class.php | 20 ++--- inc/classes/extensionsettings.class.php | 4 +- inc/classes/loadadmin.class.php | 6 +- inc/classes/panes.class.php | 2 +- inc/functions/api.php | 59 +++++++------ inc/traits/core/ui.trait.php | 18 ++-- inc/traits/manager/extensions.trait.php | 12 +-- lib/js/tsfem.js | 36 ++++---- lib/js/tsfem.min.js | 2 +- readme.txt | 20 +++-- the-seo-framework-extension-manager.php | 4 +- 24 files changed, 199 insertions(+), 203 deletions(-) diff --git a/bootstrap/define.php b/bootstrap/define.php index b62c40ad..04c8ef12 100644 --- a/bootstrap/define.php +++ b/bootstrap/define.php @@ -63,12 +63,31 @@ /** * The user role required to access the extension overview page. * + * == WARNING == + * When this constant is used incorrectly, you can expose your site to + * unforeseen security risks. We assume the role supplied here is lower than the webmaster's; + * for example, in a WPMU environment. However, proceed with caution. + * * @since 2.0.0 * @param string */ defined( 'TSF_EXTENSION_MANAGER_MAIN_ADMIN_ROLE' ) or define( 'TSF_EXTENSION_MANAGER_MAIN_ADMIN_ROLE', 'manage_options' ); +/** + * The user role required to access the extension settings. + * + * == WARNING == + * When this constant is used incorrectly, you can expose your site to + * unforeseen security risks. We assume the role supplied here is lower than the webmaster's; + * for example, in a WPMU environment. However, proceed with caution. + * + * @since 2.4.0 + * @param string + */ +defined( 'TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE' ) + or define( 'TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE', 'manage_options' ); + /** * The API version to use. * diff --git a/bootstrap/update.php b/bootstrap/update.php index 8a0f7e5f..6291ef47 100644 --- a/bootstrap/update.php +++ b/bootstrap/update.php @@ -272,7 +272,7 @@ function _clear_update_cache() { * * @since 2.0.0 * @since 2.0.2 Added more cache, because some sites disable transients completely... - * @since 2.3.2 Can now fetch required (and available) locale files. + * @since 2.4.0 Can now fetch required (and available) locale files. * @access private * @see WP Core \wp_update_plugins() * @staticvar $runtimecache. diff --git a/extensions/essentials/articles/trunk/inc/classes/front.class.php b/extensions/essentials/articles/trunk/inc/classes/front.class.php index 713f2f71..7cdcb729 100644 --- a/extensions/essentials/articles/trunk/inc/classes/front.class.php +++ b/extensions/essentials/articles/trunk/inc/classes/front.class.php @@ -486,82 +486,29 @@ private function get_article_image() { */ private function get_article_image_params() { - $id = $this->get_current_id(); - $min_width = $this->is_amp() ? 1200 : 696; - if ( version_compare( THE_SEO_FRAMEWORK_VERSION, '4.0.0', '>=' ) ) { - - $images = []; - - // TODO: Do we want to take images from the content? Users have complained about this... - // ... We'd have to implement (and revoke) a filter, however. - foreach ( \the_seo_framework()->get_image_details( null, false, 'schema', true ) as $image ) { - - if ( ! $image['url'] ) continue; - - if ( $image['width'] && $image['width'] >= $min_width ) { - $images[] = [ - '@type' => 'ImageObject', - 'url' => $image['url'], - 'width' => $image['width'], - 'height' => $image['height'], - ]; - } else { - $images[] = $image['url']; - } - } - - return count( $images ) > 1 ? $images : reset( $images ); - } else { - if ( $url = \the_seo_framework()->get_social_image_url_from_post_meta( $id, true ) ) { - - //* TSF 2.9+ - $dimensions = \the_seo_framework()->image_dimensions; - - $d = ! empty( $dimensions[ $id ] ) ? $dimensions[ $id ] : false; - if ( $d ) { - $w = $d['width']; - $h = $d['height']; - } else { - $w = 0; - $h = 0; - } - - if ( ! $w ) { - return $url; - } elseif ( $w >= $min_width ) { - return [ - '@type' => 'ImageObject', - 'url' => $url, - 'width' => $w, - 'height' => $h, - ]; - } - } - - //* Don't use `\the_seo_framework()->get_image_from_post_thumbnail` because it will overwrite vars. - if ( $_img_id = \get_post_thumbnail_id( $id ) ) { + $images = []; - $_src = \wp_get_attachment_image_src( $_img_id, 'full', false ); + // TODO: Do we want to take images from the content? Users have complained about this... + // ... We'd have to implement (and revoke) a filter, however. + foreach ( \the_seo_framework()->get_image_details( null, false, 'schema', true ) as $image ) { - if ( is_array( $_src ) && count( $_src ) >= 3 ) { - $url = $_src[0]; - $w = $_src[1]; - $h = $_src[2]; + if ( ! $image['url'] ) continue; - if ( $w >= $min_width ) - return [ - '@type' => 'ImageObject', - 'url' => $url, - 'width' => $w, - 'height' => $h, - ]; - } + if ( $image['width'] && $image['width'] >= $min_width ) { + $images[] = [ + '@type' => 'ImageObject', + 'url' => $image['url'], + 'width' => $image['width'], + 'height' => $image['height'], + ]; + } else { + $images[] = $image['url']; } } - return []; + return count( $images ) > 1 ? $images : reset( $images ); } /** diff --git a/extensions/premium/local/trunk/inc/classes/admin.class.php b/extensions/premium/local/trunk/inc/classes/admin.class.php index b72c5447..f3b0469e 100644 --- a/extensions/premium/local/trunk/inc/classes/admin.class.php +++ b/extensions/premium/local/trunk/inc/classes/admin.class.php @@ -92,12 +92,13 @@ private function load_admin_actions() { * Initializes extension menu. * * @since 1.0.0 + * @since 1.1.7 The extension access level is now controlled via another constant. * @uses \the_seo_framework()->load_options variable. Applies filters 'the_seo_framework_load_options' - * @uses \tsf_extension_manager()->can_do_settings() + * @uses \TSF_Extension_Manager\can_do_extension_settings() * @access private */ public function _init_menu() { - if ( \tsf_extension_manager()->can_do_settings() && \the_seo_framework()->load_options ) + if ( \TSF_Extension_Manager\can_do_extension_settings() && \the_seo_framework()->load_options ) \add_action( 'admin_menu', [ $this, '_add_menu_link' ], 20 ); } @@ -116,7 +117,7 @@ public function _add_menu_link() { 'parent_slug' => \the_seo_framework()->seo_settings_page_slug, 'page_title' => 'Local', 'menu_title' => 'Local', - 'capability' => 'manage_options', + 'capability' => TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE, 'menu_slug' => $this->local_page_slug, 'callback' => [ $this, '_output_local_settings_page' ], ]; diff --git a/extensions/premium/local/trunk/inc/classes/settings.class.php b/extensions/premium/local/trunk/inc/classes/settings.class.php index aa6120a0..f8ac283a 100644 --- a/extensions/premium/local/trunk/inc/classes/settings.class.php +++ b/extensions/premium/local/trunk/inc/classes/settings.class.php @@ -297,6 +297,7 @@ private function init_tsfem_ui() { * Also registers TSF scripts, for TT (tooltip) support. * * @since 1.1.3 + * @since 1.1.7 The extension access level for nonce generation now controlled via another constant. * @access private * @internal * @@ -332,7 +333,8 @@ public function _register_local_scripts( $scripts ) { 'l10n' => [ 'name' => 'tsfem_e_localL10n', 'data' => [ - 'nonce' => \wp_create_nonce( 'tsfem-e-local-ajax-nonce' ), + // This won't ever run when the user can't. But, sanity. + 'nonce' => \TSF_Extension_Manager\can_do_extension_settings() ? \wp_create_nonce( 'tsfem-e-local-ajax-nonce' ) : '', 'i18n' => [ 'fixForm' => \esc_html__( 'Please correct the form fields before validating the markup.', 'the-seo-framework-extension-manager' ), 'testNewWindow' => \esc_html__( 'The markup tester will be opened in a new window.', 'the-seo-framework-extension-manager' ), diff --git a/extensions/premium/local/trunk/inc/traits/secure-post.trait.php b/extensions/premium/local/trunk/inc/traits/secure-post.trait.php index 4c9cbca6..e4ee7ac1 100644 --- a/extensions/premium/local/trunk/inc/traits/secure-post.trait.php +++ b/extensions/premium/local/trunk/inc/traits/secure-post.trait.php @@ -237,13 +237,14 @@ private function get_registered_iterator_callbacks() { * Prepares AJAX form validation checks. * * @since 1.0.0 + * @since 1.1.7 The extension access level is now controlled via another constant. * @see $this->send_ajax_form_json_validation() * @access private */ public function _prepare_ajax_form_json_validation() { if ( \wp_doing_ajax() ) : - if ( \tsf_extension_manager()->can_do_settings() ) : + if ( \TSF_Extension_Manager\can_do_extension_settings() ) : if ( \check_ajax_referer( 'tsfem-e-local-ajax-nonce', 'nonce', false ) ) { $this->send_ajax_form_json_validation(); } diff --git a/extensions/premium/local/trunk/local.php b/extensions/premium/local/trunk/local.php index 6d54e0e2..fecfb327 100644 --- a/extensions/premium/local/trunk/local.php +++ b/extensions/premium/local/trunk/local.php @@ -9,7 +9,7 @@ * Extension Name: Local * Extension URI: https://theseoframework.com/extensions/local/ * Extension Description: The Local extension lets you set up important local business information for search engines to consume. - * Extension Version: 1.1.6 + * Extension Version: 1.1.7 * Extension Author: Sybre Waaijer * Extension Author URI: https://cyberwire.nl/ * Extension License: GPLv3 @@ -41,7 +41,7 @@ * @since 1.0.0 * NOTE: The presence does NOT guarantee the extension is loaded!!! */ -define( 'TSFEM_E_LOCAL_VERSION', '1.1.6' ); +define( 'TSFEM_E_LOCAL_VERSION', '1.1.7' ); /** * The extension database version. diff --git a/extensions/premium/local/trunk/readme.md b/extensions/premium/local/trunk/readme.md index cd1fd499..04ae06c8 100644 --- a/extensions/premium/local/trunk/readme.md +++ b/extensions/premium/local/trunk/readme.md @@ -126,6 +126,12 @@ Also, when department URLs are filled in, then each specific department's data w ## Changelog +### 1.1.7 + +[tsfep-release time="-1"] + +* **Changed:** This extension's admin access can now be controlled via the global constant `TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE`. + ### 1.1.6 [tsfep-release time="May 15th, 2020"] diff --git a/extensions/premium/monitor/trunk/inc/classes/admin.class.php b/extensions/premium/monitor/trunk/inc/classes/admin.class.php index f5cd465c..c94afca9 100644 --- a/extensions/premium/monitor/trunk/inc/classes/admin.class.php +++ b/extensions/premium/monitor/trunk/inc/classes/admin.class.php @@ -216,13 +216,14 @@ private function construct() { * Initializes extension menu. * * @since 1.0.0 + * @since 1.2.6 The extension access level is now controlled via another constant. * @uses \the_seo_framework()->load_options variable. Applies filters 'the_seo_framework_load_options' - * @uses \tsf_extension_manager()->can_do_settings() + * @uses \TSF_Extension_Manager\can_do_extension_settings() * @access private */ public function _init_menu() { - if ( \tsf_extension_manager()->can_do_settings() && \the_seo_framework()->load_options ) + if ( \TSF_Extension_Manager\can_do_extension_settings() && \the_seo_framework()->load_options ) \add_action( 'admin_menu', [ $this, '_add_menu_link' ], 100 ); } @@ -241,7 +242,7 @@ public function _add_menu_link() { 'parent_slug' => \the_seo_framework()->seo_settings_page_slug, 'page_title' => 'Monitor', 'menu_title' => 'Monitor', - 'capability' => 'manage_options', + 'capability' => TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE, 'menu_slug' => $this->monitor_page_slug, 'callback' => [ $this, '_init_monitor_page' ], ]; @@ -370,6 +371,7 @@ public function _handle_update_post() { * nonce can or has been been verified. * * @since 1.0.0 + * @since 1.2.6 The extension access level is now controlled via another constant. * @staticvar bool $validated Determines whether the nonce has already been verified. * * @param string $key The nonce action used for caching. @@ -383,7 +385,7 @@ protected function handle_update_nonce( $key = 'default', $check_post = true ) { if ( isset( $validated[ $key ] ) ) return $validated[ $key ]; - if ( ! \tsf_extension_manager()->can_do_settings() ) + if ( ! \TSF_Extension_Manager\can_do_extension_settings() ) return $validated[ $key ] = false; if ( $check_post ) { @@ -416,15 +418,16 @@ protected function handle_update_nonce( $key = 'default', $check_post = true ) { * Updates settings. * * @since 1.1.0 + * @since 1.2.6 The extension access level is now controlled via another constant. * @access private */ public function _wp_ajax_update_settings() { if ( \wp_doing_ajax() ) : - $tsfem = \tsf_extension_manager(); - if ( $tsfem->can_do_settings() ) : + if ( \TSF_Extension_Manager\can_do_extension_settings() ) : + $tsfem = \tsf_extension_manager(); $option = ''; - $send = []; + $send = []; if ( \check_ajax_referer( 'tsfem-e-monitor-ajax-nonce', 'nonce', false ) ) { //= Option is cleaned and requires unpacking. $option = isset( $_POST['option'] ) ? $tsfem->s_ajax_string( $_POST['option'] ) : ''; // Sanitization, input var OK. @@ -463,13 +466,14 @@ public function _wp_ajax_update_settings() { * Fetches Monitor Data through AJAX and echos the output through AJAX response. * * @since 1.0.0 + * @since 1.2.6 The extension access level is now controlled via another constant. * @TODO update to newer ajax handler. * @access private */ public function _wp_ajax_fetch_data() { if ( \wp_doing_ajax() ) : - if ( \tsf_extension_manager()->can_do_settings() ) : + if ( \TSF_Extension_Manager\can_do_extension_settings() ) : $timeout = null; @@ -549,13 +553,14 @@ public function _wp_ajax_fetch_data() { * Requests Monitor to crawl the site and echos the output through AJAX response. * * @since 1.0.0 + * @since 1.2.6 The extension access level is now controlled via another constant. * @TODO update to newer ajax handler. * @access private */ public function _wp_ajax_request_crawl() { if ( \wp_doing_ajax() ) : - if ( \tsf_extension_manager()->can_do_settings() ) : + if ( \TSF_Extension_Manager\can_do_extension_settings() ) : $timeout = null; @@ -631,13 +636,13 @@ public function _wp_ajax_request_crawl() { * Returns required fix fields through AJAX request. * * @since 1.0.0 + * @since 1.2.6 The extension access level is now controlled via another constant. * @access private */ public function _wp_ajax_get_requires_fix() { if ( \wp_doing_ajax() ) { - if ( \tsf_extension_manager()->can_do_settings() ) { - + if ( \TSF_Extension_Manager\can_do_extension_settings() ) { $send = []; if ( \check_ajax_referer( 'tsfem-e-monitor-ajax-nonce', 'nonce', false ) ) { //* Initialize menu hooks. @@ -710,7 +715,8 @@ public function _register_monitor_scripts( $scripts ) { 'l10n' => [ 'name' => 'tsfem_e_monitorL10n', 'data' => [ - 'nonce' => \wp_create_nonce( 'tsfem-e-monitor-ajax-nonce' ), + // This won't ever run when the user can't. But, sanity. + 'nonce' => \TSF_Extension_Manager\can_do_extension_settings() ? \wp_create_nonce( 'tsfem-e-monitor-ajax-nonce' ) : '', 'remote_data_timeout' => $this->get_remote_data_timeout(), 'remote_crawl_timeout' => $this->get_remote_crawl_timeout(), ], diff --git a/extensions/premium/monitor/trunk/monitor.php b/extensions/premium/monitor/trunk/monitor.php index 5c2351b8..8737a3c5 100644 --- a/extensions/premium/monitor/trunk/monitor.php +++ b/extensions/premium/monitor/trunk/monitor.php @@ -9,7 +9,7 @@ * Extension Name: Monitor * Extension URI: https://theseoframework.com/extensions/monitor/ * Extension Description: The Monitor extension keeps track of your website's SEO optimizations and statistics. - * Extension Version: 1.2.5 + * Extension Version: 1.2.6 * Extension Author: Sybre Waaijer * Extension Author URI: https://cyberwire.nl/ * Extension License: GPLv3 @@ -41,7 +41,7 @@ * @since 1.0.0 * NOTE: The presence does NOT guarantee the extension is loaded!!! */ -define( 'TSFEM_E_MONITOR_VERSION', '1.2.5' ); +define( 'TSFEM_E_MONITOR_VERSION', '1.2.6' ); /** * The extension file, absolute unix path. diff --git a/extensions/premium/monitor/trunk/readme.md b/extensions/premium/monitor/trunk/readme.md index b7d0c7d3..e97d229c 100644 --- a/extensions/premium/monitor/trunk/readme.md +++ b/extensions/premium/monitor/trunk/readme.md @@ -83,6 +83,12 @@ If you just initiated a crawl request, you might receive outdated or incorrect d *Because Monitor is a heavily dependent two-part system, these changes are annotated through Extension and API nodes.* +### 1.2.6 + +[tsfep-release time="-1"] + +* **Changed:** This extension's admin access can now be controlled via the global constant `TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE`. + ### 1.2.5 [tsfep-release time="May 15th, 2020"] diff --git a/inc/classes/adminpages.class.php b/inc/classes/adminpages.class.php index 142835a0..042836a1 100644 --- a/inc/classes/adminpages.class.php +++ b/inc/classes/adminpages.class.php @@ -90,6 +90,7 @@ private function construct() { * * @since 1.0.0 * @since 2.0.0 Now uses \TSF_Extension_Manager\can_do_manager_settings() + * @since 2.4.0 Removed security check, and offloads it to WordPress. * @uses \the_seo_framework()->load_options variable. Applies filters 'the_seo_framework_load_options' * @access private * @@ -97,9 +98,6 @@ private function construct() { */ final public function _init_menu() { - if ( ! \TSF_Extension_Manager\can_do_manager_settings() ) - return; - if ( $this->is_plugin_in_network_mode() ) { //* TODO. // \add_action( 'network_admin_menu', [ $this, 'add_network_menu_link' ], 11 ); @@ -115,6 +113,7 @@ final public function _init_menu() { * * @since 1.0.0 * @since 1.5.2 Added TSF v3.1 compat. + * @since 2.4.0 Added menu access control check for notification display. * @uses \the_seo_framework()->seo_settings_page_slug * @access private */ @@ -129,7 +128,11 @@ final public function _add_menu_link() { 'callback' => [ $this, '_init_extension_manager_page' ], ]; - $notice_count = count( \get_option( $this->error_notice_option, false ) ?: [] ); + if ( \TSF_Extension_Manager\can_do_manager_settings() ) { + $notice_count = count( \get_option( $this->error_notice_option, false ) ?: [] ); + } else { + $notice_count = 0; + } if ( $notice_count ) { /** diff --git a/inc/classes/ajax.class.php b/inc/classes/ajax.class.php index e8a5b92b..6743362b 100644 --- a/inc/classes/ajax.class.php +++ b/inc/classes/ajax.class.php @@ -95,17 +95,6 @@ public static function get( $type = '' ) { return false; } - /** - * Determines if the current user can access settings. - * - * @since 2.1.0 - * - * @return bool - */ - private static function can_do_settings() { - return \tsf_extension_manager()->can_do_settings(); - } - /** * Loads actions. * @@ -139,16 +128,21 @@ private static function load_actions() { /** * Send AJAX notices. If any. * + * WARNING: This method has WEAK access control. Do not store data! + * * @since 1.3.0 - * @see static:;build_ajax_dismissible_notice() + * @since 2.4.0 The access level is now controlled via an extra constant. + * @see static::build_ajax_dismissible_notice() * @access private */ public static function _wp_ajax_get_dismissible_notice() { if ( ! static::$_validated ) return; - if ( ! static::can_do_settings() ) return; + if ( + ! ( \TSF_Extension_Manager\can_do_manager_settings() || \TSF_Extension_Manager\can_do_extension_settings() ) + ) return; - if ( \check_ajax_referer( 'tsfem-ajax-nonce', 'nonce', false ) ) { + if ( \check_ajax_referer( 'tsfem-ajax-insecure-nonce', 'nonce', false ) ) { $notice_data = static::build_ajax_dismissible_notice(); } @@ -191,13 +185,14 @@ public static function _wp_ajax_inpost_get_dismissible_notice() { * Exits when done. * * @since 1.3.0 + * @since 2.4.0 The extension access level is now controlled via another constant. * @uses class TSF_Extension_Manager\FormGenerator * @access private */ public static function _wp_ajax_tsfemForm_iterate() { if ( ! static::$_validated ) return; - if ( ! static::can_do_settings() || ! \check_ajax_referer( 'tsfem-form-nonce', 'nonce', false ) ) { + if ( ! \TSF_Extension_Manager\can_do_extension_settings() || ! \check_ajax_referer( 'tsfem-form-nonce', 'nonce', false ) ) { static::$tsfem->send_json( [ 'results' => static::$instance->get_ajax_notice( false, 9002 ) ], 'failure' ); exit; } @@ -227,13 +222,14 @@ public static function _wp_ajax_tsfemForm_iterate() { * * @since 1.3.0 * @since 2.2.0 Now handles basic invalid POST data checks, so extensions don't have to. + * @since 2.4.0 The extension access level is now controlled via another constant. * @uses class TSF_Extension_Manager\FormGenerator * @access private */ public static function _wp_ajax_tsfemForm_save() { if ( ! static::$_validated ) return; - if ( ! static::can_do_settings() || ! \check_ajax_referer( 'tsfem-form-nonce', 'nonce', false ) ) { + if ( ! \TSF_Extension_Manager\can_do_extension_settings() || ! \check_ajax_referer( 'tsfem-form-nonce', 'nonce', false ) ) { static::$tsfem->send_json( [ 'results' => static::$instance->get_ajax_notice( false, 9003 ) ], 'failure' ); exit; } @@ -258,6 +254,7 @@ public static function _wp_ajax_tsfemForm_save() { * On failure, it returns an AJAX error code. * * @since 1.3.0 + * @since 2.4.0 The extension access level is now controlled via another constant. * @see class TSF_Extension_Manager\FormGenerator * @access private */ @@ -265,16 +262,11 @@ public static function _wp_ajax_tsfemForm_get_geocode() { if ( ! static::$_validated ) return; - if ( ! static::can_do_settings() ) { + if ( ! \TSF_Extension_Manager\can_do_extension_settings() || ! \check_ajax_referer( 'tsfem-form-nonce', 'nonce', false ) ) { static::$tsfem->send_json( [ 'results' => static::$instance->get_ajax_notice( false, 9004 ) ], 'failure' ); exit; } - if ( ! \check_ajax_referer( 'tsfem-form-nonce', 'nonce', false ) ) { - static::$tsfem->send_json( [], 'failure' ); - exit; - } - $send = []; //= Input gets forwarded to secure location. Sanitization happens externally. @@ -376,6 +368,8 @@ public static function _wp_ajax_tsfemForm_get_geocode() { /** * Builds AJAX notices. * + * WARNING: This method has WEAK access control prior being called. Do not store data! + * * @since 1.5.0 * @uses trait TSF_Extension_Manager\Error * @access private @@ -383,9 +377,9 @@ public static function _wp_ajax_tsfemForm_get_geocode() { private static function build_ajax_dismissible_notice() { // phpcs:disable, WordPress.Security.NonceVerification.Missing -- Caller must check for this. + $data = []; $data['key'] = (int) static::$tsfem->coalesce_var( $_POST['tsfem-notice-key'], false ); - if ( $data['key'] ) { $notice = static::$instance->get_error_notice( $data['key'] ); diff --git a/inc/classes/core.class.php b/inc/classes/core.class.php index c5d6d293..c8e34a96 100644 --- a/inc/classes/core.class.php +++ b/inc/classes/core.class.php @@ -164,7 +164,8 @@ final public function _init_extensions() { //* Some AJAX functions require Extension layout traits to be loaded. if ( \is_admin() && \wp_doing_ajax() ) { - if ( \check_ajax_referer( 'tsfem-ajax-nonce', 'nonce', false ) ) + // This should not ever be a security issue. However, sanity. + if ( \TSF_Extension_Manager\can_do_manager_settings() && \check_ajax_referer( 'tsfem-ajax-nonce', 'nonce', false ) ) $this->ajax_is_tsf_extension_manager_page( true ); } @@ -363,7 +364,9 @@ final public function send_html( $html, $type = 'success' ) { * Note that the URL can't be generated if the menu pages aren't set. * * @since 1.2.0 + * @since 2.4.0 Added nonce capability requirement, extra sanity for when the caller fails to do so. * @access private + * @ignore unused. Leftover from the never-released Transporter * * @param array $args - Required : { * 'options_key' => string The extension options key, @@ -372,6 +375,7 @@ final public function send_html( $html, $type = 'success' ) { * 'nonce_name' => string The extension POST actions nonce name, * 'request_name' => string The extension desired POST action request index key name, * 'nonce_action' => string The extesnion desired POST action request full name, + * 'capability' => string The extesnion desired user capability, * } * @return array|bool False on failure; array containing the jQuery.post object. */ @@ -384,6 +388,7 @@ final public function _get_ajax_post_object( array $args ) { 'nonce_name', 'request_name', 'nonce_action', + 'capability', ]; //* If the required keys aren't found, bail. @@ -410,7 +415,7 @@ final public function _get_ajax_post_object( array $args ) { 'nonce-action' => $args['request_name'], ], ], - $args['nonce_name'] => \wp_create_nonce( $args['nonce_action'] ), + $args['nonce_name'] => \current_user_can( $required['capability'] ) ? \wp_create_nonce( $args['nonce_action'] ) : '', '_wp_http_referer' => \esc_attr( \wp_unslash( $_SERVER['REQUEST_URI'] ) ), // input var & sanitization ok. ], ]; @@ -968,17 +973,6 @@ final public function get_hash_type() { return $type; } - /** - * Returns the minimum role required to adjust and access settings. - * - * @since 1.0.0 - * - * @return string The minimum required capability for extensions Settings. - */ - final public function can_do_settings() { - return \TSF_Extension_Manager\can_do_settings(); - } - /** * Determines whether the plugin is network activated. * diff --git a/inc/classes/extensionsettings.class.php b/inc/classes/extensionsettings.class.php index e13d2163..ed4e086b 100644 --- a/inc/classes/extensionsettings.class.php +++ b/inc/classes/extensionsettings.class.php @@ -208,7 +208,7 @@ private function prepare_settings() { * @access private */ public function _init_menu() { - if ( \tsf_extension_manager()->can_do_settings() && \the_seo_framework()->load_options ) + if ( \TSF_Extension_Manager\can_do_extension_settings() && \the_seo_framework()->load_options ) \add_action( 'admin_menu', [ $this, '_add_menu_link' ], 12 ); } @@ -224,7 +224,7 @@ public function _add_menu_link() { 'parent_slug' => \the_seo_framework()->seo_settings_page_slug, 'page_title' => \__( 'Extension Settings', 'the-seo-framework-extension-manager' ), 'menu_title' => \__( 'Extension Settings', 'the-seo-framework-extension-manager' ), - 'capability' => 'manage_options', + 'capability' => TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE, 'menu_slug' => static::$settings_page_slug, 'callback' => [ $this, '_output_settings_page' ], ]; diff --git a/inc/classes/loadadmin.class.php b/inc/classes/loadadmin.class.php index 9fccd0f7..5e2264f8 100644 --- a/inc/classes/loadadmin.class.php +++ b/inc/classes/loadadmin.class.php @@ -121,7 +121,7 @@ public function _check_constant_activation() { */ public function check_external_blocking() { - if ( ! $this->is_tsf_extension_manager_page() || ! $this->can_do_settings() ) + if ( ! $this->is_tsf_extension_manager_page() || ! \TSF_Extension_Manager\can_do_manager_settings() ) return; if ( ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL ) @@ -303,7 +303,7 @@ protected function handle_update_nonce( $key = 'default', $check_post = true ) { if ( isset( $validated[ $key ] ) ) return $validated[ $key ]; - if ( ! $this->can_do_settings() ) + if ( ! \TSF_Extension_Manager\can_do_manager_settings() ) return $validated[ $key ] = false; if ( $check_post ) { @@ -336,7 +336,7 @@ protected function handle_update_nonce( $key = 'default', $check_post = true ) { */ public function do_activation_notice() { - if ( $this->is_plugin_activated() || ! $this->can_do_settings() || $this->is_tsf_extension_manager_page() ) + if ( $this->is_plugin_activated() || ! \TSF_Extension_Manager\can_do_manager_settings() || $this->is_tsf_extension_manager_page() ) return; $text = \__( 'Your extensions are only three clicks away', 'the-seo-framework-extension-manager' ); diff --git a/inc/classes/panes.class.php b/inc/classes/panes.class.php index 569855fd..bd2e0f1e 100644 --- a/inc/classes/panes.class.php +++ b/inc/classes/panes.class.php @@ -390,7 +390,7 @@ final public function _wp_ajax_tsfem_update_extension_desc_footer() { $header = Extensions::get( 'ajax_get_extension_header', $slug ); if ( ! empty( $header['MenuSlug'] ) ) { - $this->_set_ajax_menu_link( $header['MenuSlug'] ); + $this->_set_ajax_menu_link( $header['MenuSlug'], TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE ); } endif; diff --git a/inc/functions/api.php b/inc/functions/api.php index d429b679..9f5d55b9 100644 --- a/inc/functions/api.php +++ b/inc/functions/api.php @@ -58,52 +58,49 @@ function tsf_extension_manager_db_version( $member = 'core' ) { namespace TSF_Extension_Manager { /** - * Returns the minimum role required to adjust and access settings. + * Returns true when the user can control extension options. * * @since 1.0.0 * @since 1.5.0 Added filter. + * @since 2.4.0 1: Removed filter. + * 2: Now uses constant `TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE` + * * @staticvar bool $cache * - * @return string The minimum required capability for extension installation. + * @return bool The minimum required capability for extension management. */ - function can_do_settings() { - - static $cache = null; - - /** - * Allows for conditionally adjusting the "can do settings" role. - * - * @NOTE: - * We don't recommend conditioning this lower, as some functionality may appear broken. - * This should be set to a role that also implies 'manage_options'. - * @NOTE WANRING: - * Conditioning this higher might impose 'security' risks, where admins, still with - * 'manage_options' capabilities, may perform certain actions, regardless of this state. - * - * We could alleviate these issues by dynamically fetching roles when this is true, - * but that'll create unpredictable behavior, which we won't allow. - * - * @NOTE: Don't try to act smart by always returning true. This function is used - * where can_do_manager_settings() isn't. - * - * @since 1.5.0 - * @param bool $can_do_settings Whether the user can access and modify settings. - */ - return isset( $cache ) - ? $cache - : $cache = \apply_filters( 'tsf_extension_manager_can_manage_options', \current_user_can( 'manage_options' ) ); + function can_do_extension_settings() { + return \current_user_can( TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE ); } /** - * Returns the minimum role required to adjust and access the main settings. + * Returns true when the user can control manager (main) settings. + * - Extensions overview. + * - Extension (de)activation. + * - API connection management. + * - Feed activation. * * @since 2.0.0 - * @uses \TSF_Extension_Manager\can_do_settings() This must pass, too. * * @return bool */ function can_do_manager_settings() { - return can_do_settings() && \current_user_can( TSF_EXTENSION_MANAGER_MAIN_ADMIN_ROLE ); + return \current_user_can( TSF_EXTENSION_MANAGER_MAIN_ADMIN_ROLE ); + } + + /** + * Returns the minimum role required to adjust and access general extension settings. + * + * @since 1.0.0 + * @since 1.5.0 Added filter. + * @since 2.4.0 Deprecated without warning. + * @deprecated + * @staticvar bool $cache + * + * @return string The minimum required capability for extension installation. + */ + function can_do_settings() { + return can_do_manager_settings(); } /** diff --git a/inc/traits/core/ui.trait.php b/inc/traits/core/ui.trait.php index 272f0c04..5b2ec137 100644 --- a/inc/traits/core/ui.trait.php +++ b/inc/traits/core/ui.trait.php @@ -216,6 +216,7 @@ final public function _register_default_scripts( $scripts ) { if ( has_run( __METHOD__ ) ) return; \the_seo_framework()->init_admin_scripts(); + $tsfem = \tsf_extension_manager(); $scripts::register( [ [ @@ -240,10 +241,11 @@ final public function _register_default_scripts( $scripts ) { 'l10n' => [ 'name' => 'tsfemL10n', 'data' => [ - 'nonce' => \wp_create_nonce( 'tsfem-ajax-nonce' ), - 'debug' => (bool) WP_DEBUG, - 'rtl' => (bool) \is_rtl(), - 'i18n' => [ + 'nonce' => \TSF_Extension_Manager\can_do_manager_settings() ? \wp_create_nonce( 'tsfem-ajax-nonce' ) : '', + 'insecureNonce' => \TSF_Extension_Manager\can_do_extension_settings() || \TSF_Extension_Manager\can_do_manager_settings() ? \wp_create_nonce( 'tsfem-ajax-insecure-nonce' ) : '', + 'debug' => (bool) WP_DEBUG, + 'rtl' => (bool) \is_rtl(), + 'i18n' => [ 'Activate' => \esc_html__( 'Activate', 'the-seo-framework-extension-manager' ), 'Deactivate' => \esc_html__( 'Deactivate', 'the-seo-framework-extension-manager' ), 'InvalidResponse' => \esc_html__( 'Received invalid AJAX response.', 'the-seo-framework-extension-manager' ), @@ -256,7 +258,7 @@ final public function _register_default_scripts( $scripts ) { ], ], 'tmpl' => [ - 'file' => \tsf_extension_manager()->get_template_location( 'fbtopnotice' ), + 'file' => $tsfem->get_template_location( 'fbtopnotice' ), ], ], ] ); @@ -265,10 +267,12 @@ final public function _register_default_scripts( $scripts ) { /** * Registers form scripts. * + * Should only be used by extensions, not the manager! + * * @since 1.3.0 - * @since 2.0.0 Now uses \TSF_Extension_Manager\can_do_settings() for nonce creation. * @since 2.0.2 : 1. Now uses TSF's Scripts module. * 2. Now returns void + * @since 2.4.0 The access level for nonce generation now controlled via another constant. * @access protected * @internal * @@ -301,7 +305,7 @@ final protected function register_form_scripts( $scripts ) { 'l10n' => [ 'name' => 'tsfemFormL10n', 'data' => [ - 'nonce' => \TSF_Extension_Manager\can_do_settings() ? \wp_create_nonce( 'tsfem-form-nonce' ) : '', + 'nonce' => \TSF_Extension_Manager\can_do_extension_settings() ? \wp_create_nonce( 'tsfem-form-nonce' ) : '', 'callee' => get_class( $this ), //! Don't use __CLASS__, we require the core instance. 'i18n' => [ //* TODO categorize diff --git a/inc/traits/manager/extensions.trait.php b/inc/traits/manager/extensions.trait.php index abc06c0f..d8c3e284 100644 --- a/inc/traits/manager/extensions.trait.php +++ b/inc/traits/manager/extensions.trait.php @@ -85,7 +85,7 @@ private static function get_extensions() { 'area' => 'audit, content, keywords', 'author' => 'Sybre Waaijer', 'party' => 'first', - 'last_updated' => '1590589374', + 'last_updated' => '1590706430', 'requires' => '4.9', 'tested' => '5.4', 'requires_tsf' => '4.0', @@ -137,7 +137,7 @@ private static function get_extensions() { 'area' => 'business', 'author' => 'Sybre Waaijer', 'party' => 'first', - 'last_updated' => '1589479187', + 'last_updated' => '1590706430', 'requires' => '4.9', 'tested' => '5.4', 'requires_tsf' => '4.0', @@ -163,7 +163,7 @@ private static function get_extensions() { 'area' => 'uptime, syntax', 'author' => 'Sybre Waaijer', 'party' => 'first', - 'last_updated' => '1589479187', + 'last_updated' => '1590706430', 'requires' => '4.9', 'tested' => '5.4', 'requires_tsf' => '4.0', @@ -224,9 +224,9 @@ private static function get_extensions() { */ private static function get_external_extensions_checksum() { return [ - 'sha256' => '253d30cc10ddebd63ace15895cd9c0e7f9947fb5b37b35b8992fd342b818a517', - 'sha1' => 'a6418f4c133aba3c2c2b94dc5d5d9a51cd95347c', - 'md5' => '6155124f3ba50da8b3d2a230e690700f', + 'sha256' => 'ff8c20a775a14284a12751cfbb94d44738543ca928b1dd4e32974b0b407c9d69', + 'sha1' => '8efc0239444ea4117ec7f1df69c13dd74a527344', + 'md5' => 'fd8c0ac460eca1ff5894107eaa4a72d2', ]; } diff --git a/lib/js/tsfem.js b/lib/js/tsfem.js index 9a3b29c6..880e5be9 100644 --- a/lib/js/tsfem.js +++ b/lib/js/tsfem.js @@ -39,14 +39,21 @@ window.tsfem = { * @access private * @type {string|null} nonce Ajax nonce */ - nonce : tsfemL10n.nonce, + nonce: tsfemL10n.nonce, + + /** + * @since 2.4.0 + * @access private + * @type {string|null} nonce Insecure Ajax nonce + */ + insecureNonce: tsfemL10n.insecureNonce, /** * @since 1.0.0 * @access private * @param {object|null} i18n Localized strings */ - i18n : tsfemL10n.i18n, + i18n: tsfemL10n.i18n, /** * @since 1.0.0 @@ -54,7 +61,7 @@ window.tsfem = { * @access public * @param {boolean|undefined|null} rtl RTL enabled */ - rtl : tsfemL10n.rtl, + rtl: tsfemL10n.rtl, /** * @since 1.0.0 @@ -62,7 +69,7 @@ window.tsfem = { * @access public * @param {boolean|undefined|null} debug Debugging enabled */ - debug : tsfemL10n.debug, + debug: tsfemL10n.debug, /** * @since 1.0.0 @@ -70,21 +77,21 @@ window.tsfem = { * @access public * @param {boolean} touchBuffer Maintains touch-buffer */ - touchBuffer : false, + touchBuffer: false, /** * @since 1.3.0 * @access private * @param {boolean} noticeBuffer Maintains notice loader buffer */ - noticeBuffer : false, + noticeBuffer: false, /** * @since 1.3.0 * @access private * @param {boolean} navWarn Whether to warn the user on navigation. */ - navWarn : false, + navWarn: false, /** * Sets touch buffer to set ms. After which it resets. @@ -214,8 +221,8 @@ window.tsfem = { url: ajaxurl, dataType: 'json', data: { - 'action': 'tsfem_enable_feeds', - 'nonce': tsfem.nonce, + action: 'tsfem_enable_feeds', + nonce: tsfem.nonce, }, timeout: 12000, async: true, @@ -342,10 +349,10 @@ window.tsfem = { url: ajaxurl, dataType: 'json', data: { - 'action' : 'tsfem_update_extension', - 'nonce' : tsfem.nonce, - 'slug' : actionSlug, - 'case' : actionCase, + action: 'tsfem_update_extension', + nonce: tsfem.nonce, + slug: actionSlug, + case: actionCase, }, timeout: 10000, async: true, @@ -792,6 +799,7 @@ window.tsfem = { * * @since 1.3.0 * @since 1.5.0 Now uses fallback notices on fatal AJAX error. + * @since 2.4.0 Now uses a lower-level nonce. * @access public * * @function @@ -819,7 +827,7 @@ window.tsfem = { datatype: 'json', data: { action: 'tsfem_get_dismissible_notice', - nonce: tsfem.nonce, + nonce: tsfem.insecureNonce, 'tsfem-notice-key': noticeKey, 'tsfem-notice-has-msg': hasMsg, }, diff --git a/lib/js/tsfem.min.js b/lib/js/tsfem.min.js index 635f28b7..411889da 100644 --- a/lib/js/tsfem.min.js +++ b/lib/js/tsfem.min.js @@ -1 +1 @@ -'use strict';window.tsfem={nonce:tsfemL10n.nonce,i18n:tsfemL10n.i18n,rtl:tsfemL10n.rtl,debug:tsfemL10n.debug,touchBuffer:!1,noticeBuffer:!1,navWarn:!1,setTouchBuffer:function(a){tsfem.touchBuffer=!0,setTimeout(()=>{tsfem.touchBuffer=!1},a)},setAjaxLoader:function(a){jQuery(a).toggleClass("tsfem-loading")},unsetAjaxLoader:function(a,b,c,d){let e="tsfem-success",f=2500;b?2===b&&(e="tsfem-unknown",f=7500):(e="tsfem-error",f=d?2e4:1e4),f=c?2*f:f,d?jQuery(a).removeClass("tsfem-loading").addClass(e).html(c).fadeOut(f):(c=jQuery("").html(c).text(),jQuery(a).removeClass("tsfem-loading").addClass(e).text(c).fadeOut(f))},resetAjaxLoader:function(a){jQuery(a).stop().empty().prop("class","tsfem-ajax").css({opacity:"1",display:"initial"}).prop("style","")},updateFeed:function(a){let b="tsfem-button-disabled",c=jQuery(a.target),d="#tsfem-feed-ajax",e=0;if(c.prop("disabled"))return;c.addClass("tsfem-button-disabled"),c.prop("disabled",!0),tsfem.resetAjaxLoader("#tsfem-feed-ajax"),tsfem.setAjaxLoader("#tsfem-feed-ajax");const f=()=>{c.removeClass(b),c.prop("disabled",!1),tsfem.updatedResponse(d,e,tsfem.i18n.UnknownError,0)};jQuery.ajax({method:"POST",url:ajaxurl,dataType:"json",data:{action:"tsfem_enable_feeds",nonce:tsfem.nonce},timeout:12e3,async:!0}).done(a=>{var b=Math.round,c=Math.pow;a=tsfem.convertJSONResponse(a),tsfem.debug&&console.log(a);let g=a&&a.data||void 0,h=a&&a.type||void 0;if("success"===h&&g){let a=g.content;switch(a.status){case"success":e=1,jQuery(".tsfem-trends-wrap").empty().css("opacity",0).append(a.wrap).animate({opacity:1},{queue:!0,duration:250});let f=400,g=a.data.length,h=0;for(let a=1;a{f=b(f/c(1+a/2/100,2)),setTimeout(()=>{jQuery(d).hide().appendTo(".tsfem-feed-wrap").slideDown(f)},f/2*a)}),setTimeout(()=>{tsfem.updatedResponse(d,e,"",0)},h);break;case"parse_error":case"unknown_error":default:jQuery(".tsfem-trends-wrap").empty().css("opacity",0).append(a.error_output).css("opacity",1).find(".tsfem-feed-wrap").css({opacity:0}).animate({opacity:1},{queue:!0,duration:2e3}),e="unknown_error"===a.status?2:0,setTimeout(()=>{tsfem.updatedResponse(d,e,tsfem.i18n.UnknownError,0)},1e3);}}else"unknown"===a.type?(e=2,f()):f()}).fail((a,e,f)=>{let g=tsfem.getAjaxError(a,e,f);c.removeClass(b),c.prop("disabled",!1),tsfem.updatedResponse(d,0,g,0)})},updateExtension:function(a){let b="tsfem-button-disabled",c=jQuery(a.target);if(c.prop("disabled")||c.hasClass("tsfem-button-disabled"))return;let d=jQuery(".tsfem-button-extension-activate, .tsfem-button-extension-deactivate").not(jQuery(".tsfem-button-disabled")),e=c.data("slug"),f=c.data("case"),g="#tsfem-extensions-ajax",h=0,i="",j=0,k="";d.map(()=>{jQuery(this).addClass(b),jQuery(this).prop("disabled",!0)}),tsfem.resetAjaxLoader(g),tsfem.setAjaxLoader(g),jQuery.ajax({method:"POST",url:ajaxurl,dataType:"json",data:{action:"tsfem_update_extension",nonce:tsfem.nonce,slug:e,case:f},timeout:1e4,async:!0}).done(a=>{a=tsfem.convertJSONResponse(a),tsfem.debug&&console.log(a);let b=a&&a.data||void 0,d=a&&a.type||void 0;if(!b||!d)k=tsfem.i18n.UnknownError;else{let a=b.results&&b.results.code||void 0,d=b.results&&b.results.success||void 0;if(k=b.results&&b.results.notice||void 0,"activate"===f)switch(a){case 10001:case 10002:case 10003:case 10004:case 10006:case 10007:case 10013:case 10014:h=0,j=a;break;case 10005:h=0;let d=b&&b.fatal_error||void 0;i=d,j=a;break;case 10008:case 10010:case 10012:h=1,c.removeClass("tsfem-button tsfem-button-extension-activate").addClass("tsfem-button-primary tsfem-button-primary-dark tsfem-button-extension-deactivate"),c.data("case","deactivate"),c.text(tsfem.i18n.Deactivate),jQuery("#"+e+"-extension-entry").removeClass("tsfem-extension-deactivated").addClass("tsfem-extension-activated"),tsfem.updateExtensionDescFooter(e,f);break;case 10009:h=2,j=a;break;default:h=0,k=tsfem.i18n.UnknownError;}else"deactivate"===f&&(11001===a?(h=1,c.removeClass("tsfem-button-primary tsfem-button-primary-dark tsfem-button-extension-deactivate").addClass("tsfem-button tsfem-button-extension-activate"),c.data("case","activate"),c.text(tsfem.i18n.Activate),jQuery("#"+e+"-extension-entry").removeClass("tsfem-extension-activated").addClass("tsfem-extension-deactivated"),tsfem.updateExtensionDescFooter(e,f)):11002===a||11003===a||11004===a?(h=0,j=a):(h=0,k=tsfem.i18n.UnknownError))}}).fail((a,b,c)=>{k=tsfem.getAjaxError(a,b,c),tsfem.setTopNotice(1071100),c&&tsfem.setTopNotice(-1,"jQ error: "+c)}).always(()=>{tsfem.updatedResponse(g,h,k,0),d.removeClass(b),d.prop("disabled",!1),c.focus(),j&&tsfem.setTopNotice(j,i)})},convertJSONResponse:function(a){let b=a&&a.json||void 0,c=1===b;if(!c){let b=a;try{a=JSON.parse(a),c=!0}catch(a){c=!1}c||(a=b)}return a},updatedResponse:function(a,b,c,d){switch(b){case 0:case 1:case 2:tsfem.unsetAjaxLoader(a,b,c,d);break;default:tsfem.resetAjaxLoader(a);}},getAjaxError:function(a,b,c){tsfem.debug&&(console.log(a.responseText),console.log(c));let d="";switch(c){case"abort":case"timeout":d=tsfem.i18n.TimeoutError;break;case"Bad Request":d=tsfem.i18n.BadRequest;break;case"Internal Server Error":d=tsfem.i18n.FatalError;break;case"parsererror":d=tsfem.i18n.ParseError;break;default:d=tsfem.i18n.UnknownError;}return d},unexpectedAjaxErrorNotice:function(a){a=tsfem.convertJSONResponse(a)||void 0;let b=a&&a.data||void 0;tsfem.debug&&console.log(a),b&&"results"in b&&"code"in b.results&&tsfem.setTopNotice(b.results.code,b.results.notice)},matosa:function(a){var b=null,c="";if(function a(d,e){if(e++,"object"==typeof d){let f,g;for(f in d)g=d[f];b=g,c+=1===e?f+a(g,e):"["+f+"]"+a(g,e)}else if(1===e)return b=null,c=!1;return c}(a,0),!1===c)return!1;let d={};return d[c]=b,d},updateExtensionDescFooter:function(a,b){jQuery.ajax({method:"POST",url:ajaxurl,dataType:"json",data:{action:"tsfem_update_extension_desc_footer",nonce:tsfem.nonce,slug:a,case:b},timeout:7e3,async:!0}).done(c=>{c=tsfem.convertJSONResponse(c),tsfem.debug&&console.log(c);let d=c&&c.data||void 0,e=c&&c.type||void 0;if(!d)return;let f=jQuery("#"+a+"-extension-entry .tsfem-extension-description-footer"),g="activate"===b?"up":"down";f.addClass("tsfem-flip-hide-"+g),setTimeout(()=>{f.empty().append(d),tsfTT.triggerReset()},250),setTimeout(()=>{f.addClass("tsfem-flip-show-"+g)},500),setTimeout(()=>{f.removeClass("tsfem-flip-hide-"+g+" tsfem-flip-show-"+g)},750)}).fail((a,b,c)=>{tsfem.debug&&(console.log(a.responseText),console.log(c))})},preventDefault:function(a){a.preventDefault(),a.stopPropagation()},engageSwitcher:function(){jQuery(window).off("click.tsfemResetSwitcher").on("click.tsfemResetSwitcher",a=>{let b=jQuery(".tsfem-switch-button-container > input[type=\"checkbox\"]:checked");if("undefined"!=typeof b&&0jQuery(a.target).closest(c).length&&(b.prop("checked",!1),jQuery(window).off("click.tsfemResetSwitcher"))}})},registerNavWarn:function(){tsfem.navWarn=!0},mustNavWarn:function(){return!!tsfem.navWarn},setDismissNoticeListener:function(){const a=a=>{jQuery(a.target).closest(".tsfem-notice").slideUp(200,function(){this.remove()})};jQuery(".tsfem-dismiss").off("click",a).on("click",a)},setTopNotice:function(a,b){if(tsfem.noticeBuffer)return void window.setTimeout(()=>{tsfem.setTopNotice(a,b)},500);tsfem.noticeBuffer=!0;let c=b?1:0;jQuery.ajax({method:"POST",url:ajaxurl,datatype:"json",data:{action:"tsfem_get_dismissible_notice",nonce:tsfem.nonce,"tsfem-notice-key":a,"tsfem-notice-has-msg":c},timeout:7e3,async:!0}).done(a=>{a=tsfem.convertJSONResponse(a),tsfem.debug&&console.log(a);let d=a&&a.data||void 0,e=a&&a.type||void 0;if(!d||!e||"undefined"==typeof d.notice);else{let a="";c?(a=jQuery(d.notice),tsfem.rtl?a.find("p").first().prepend(b+" "):a.find("p").first().append(" "+b)):a=d.notice,tsfem.appendTopNotice(a)}}).fail((d,e,f)=>{tsfem.debug&&(console.log(d.responseText),console.log(f));let g=c?wp.template("tsfem-fbtopnotice-msg"):wp.template("tsfem-fbtopnotice"),h=g({code:a,msg:b});tsfem.appendTopNotice(h)}).always(()=>{tsfem.noticeBuffer=!1})},appendTopNotice:function(a){let b=jQuery(".tsfem-notice-wrap"),c=b.children(".tsfem-notice, .tsfem-notice-wrap .notice"),d={duration:200,queue:!1};b.css("willChange","contents"),1this.remove()}))}),jQuery(a).hide().appendTo(b).slideDown(jQuery.extend(d,{complete:()=>b.css("maxHeight","")})),tsfem.setDismissNoticeListener()},dialog:function(a){let b=a.title||"",c=a.text||"",d=a.select||"",e=a.confirm||"",f=a.cancel||"",g={};if(g.mask=document.createElement("div"),g.mask.className="tsfem-modal-mask",g.maskNoScroll=document.createElement("div"),g.maskNoScroll.className="tsfem-modal-mask-noscroll",g.mask.appendChild(g.maskNoScroll),g.container=document.createElement("div"),g.container.className="tsfem-modal-container",g.dialogWrap=document.createElement("div"),g.dialogWrap.className="tsfem-modal-dialog-wrap",g.dialogWrap.style.marginLeft=document.getElementById("adminmenuwrap").offsetWidth+"px",g.dialogWrap.style.marginTop=document.getElementById("wpadminbar").offsetHeight+"px",g.dialog=document.createElement("div"),g.dialog.className="tsfem-modal-dialog",g.trap=document.createElement("div"),g.trap.className="tsfem-modal-trap",g.trap.tabIndex=0,g.bottomTrap=g.trap.cloneNode(!1),g.dialog.appendChild(g.trap),g.x=document.createElement("div"),g.x.className="tsfem-modal-dismiss",g.x.addEventListener("click",function(){window.dispatchEvent(new Event("tsfem_modalCancel"))}),g.dialog.appendChild(g.x),b&&(g.titleWrap=document.createElement("div"),g.titleWrap.className="tsfem-modal-title",g.titleWrapTitle=document.createElement("h4"),g.titleWrapTitle.innerHTML=b,g.titleWrap.appendChild(g.titleWrapTitle),g.dialog.appendChild(g.titleWrap)),g.inner=document.createElement("div"),g.inner.className="tsfem-modal-inner",c){if(g.textWrap=document.createElement("div"),g.textWrap.className="tsfem-modal-text",Array.isArray(c))for(let a in c)g.textWrapContent=document.createElement("p"),g.textWrapContent.innerHTML=c[a],g.textWrap.appendChild(g.textWrapContent);else g.textWrapContent=document.createElement("p"),g.textWrapContent.innerHTML=c,g.textWrap.appendChild(g.textWrapContent);g.inner.appendChild(g.textWrap)}let h=!1;if(d){h=!0,g.selectWrap=document.createElement("div"),g.selectWrap.className="tsfem-modal-select";let a={};a.wrap=document.createElement("div"),a.wrap.className="tsfem-modal-select-option",a.radio=document.createElement("input"),a.radio.setAttribute("type","radio"),a.radio.setAttribute("name","tsfem-modal-select-option-group"),a.radio.tabIndex=0,a.label=document.createElement("label"),function(){for(let b in d){let c=a.wrap.cloneNode(!0),e=a.radio.cloneNode(!1),f=a.label.cloneNode(!1);e.setAttribute("value",b),f.innerHTML=d[b],0==b&&(e.checked=!0);let h="tsfem-dialog-option-"+b;e.setAttribute("id",h),f.setAttribute("for",h),c.appendChild(e),c.appendChild(f),g.selectWrap.appendChild(c)}}(),g.inner.appendChild(g.selectWrap)}g.dialog.appendChild(g.inner),(e||f)&&(g.buttonWrap=document.createElement("div"),g.buttonWrap.className="tsfem-modal-buttons",e&&(g.confirmButton=document.createElement("button"),g.confirmButton.className="tsfem-modal-confirm tsfem-button-small",g.confirmButton.className+=h?" tsfem-button-primary tsfem-button-primary-bright":" tsfem-button",g.confirmButton.innerHTML=e,g.confirmButton.addEventListener("click",function(){let a;h&&(a={detail:{checked:document.querySelector(".tsfem-modal-select input:checked").value}}),window.dispatchEvent(new CustomEvent("tsfem_modalConfirm",a))}),g.buttonWrap.appendChild(g.confirmButton)),f&&(g.cancelButton=document.createElement("button"),g.cancelButton.className="tsfem-modal-cancel tsfem-button tsfem-button-small",g.cancelButton.innerHTML=f,g.cancelButton.addEventListener("click",()=>{window.dispatchEvent(new Event("tsfem_modalCancel"))}),g.buttonWrap.appendChild(g.cancelButton)),g.dialog.appendChild(g.buttonWrap)),g.dialog.appendChild(g.bottomTrap),g.dialogWrap.appendChild(g.dialog),g.container.appendChild(g.dialogWrap),document.body.appendChild(g.mask),document.body.appendChild(g.container);const i=()=>{g.trap.focus()};g.trap.addEventListener("focus",i),g.bottomTrap.addEventListener("focus",i),g.trap.focus(),tsfem.fadeIn(g.mask),tsfem.fadeIn(g.container);const j=a=>{a.preventDefault()};g.maskNoScroll.addEventListener("wheel",j),g.maskNoScroll.addEventListener("touchmove",j);const k=function(){g.dialogWrap.style.marginLeft=document.getElementById("adminmenuwrap").offsetWidth+"px",g.dialogWrap.style.marginTop=document.getElementById("wpadminbar").offsetHeight+"px"};window.addEventListener("resize",k);const l=()=>{g.maskNoScroll.removeEventListener("wheel",j),g.maskNoScroll.removeEventListener("touchmove",j),window.removeEventListener("tsfem_modalCancel",l),window.removeEventListener("tsfem_modalConfirm",l),window.removeEventListener("resize",k),tsfem.fadeOut(g.mask,250,()=>g.mask.remove()),tsfem.fadeOut(g.container,250,()=>g.container.remove())};window.addEventListener("tsfem_modalCancel",l),window.addEventListener("tsfem_modalConfirm",l)},fadeIn:function(a,b,c,d){if(void 0===a||!a instanceof HTMLElement)return;if(!a.style||!("opacity"in a.style))return;b=b||250,d=!(void 0!==d)||d;let e,f,g=0,h=0,i=3;d?f=()=>{h=(g+=i)/100,a.style.display=null,a.style.opacity=h,1<=h&&(clearInterval(e),a.style.opacity=1,"function"==typeof c&&c())}:(g=100,f=()=>{h=(g-=i)/100,a.style.opacity=h,0>=h&&(clearInterval(e),a.style.opacity=0,setTimeout(()=>{a.style.display="none"},0),"function"==typeof c&&c())}),e=setInterval(f,b/100)},fadeOut:function(a,b,c){tsfem.fadeIn(a,b,c,!1)},ready:function(a){a("#wpbody-content").find(".updated, .error, .notice-error, .notice-warning").appendTo(".tsfem-notice-wrap"),a("a#tsfem-enable-feeds").on("click",tsfem.updateFeed),a(".tsfem-button-extension-activate, .tsfem-button-extension-deactivate").on("click",tsfem.updateExtension),a(".tsfem-button-disabled").on("click",tsfem.preventDefault),a(".tsfem-switch-button-container-wrap").on("click","label",tsfem.engageSwitcher),a(document.body).ready(tsfem.setDismissNoticeListener)}},jQuery(tsfem.ready); +'use strict';window.tsfem={nonce:tsfemL10n.nonce,insecureNonce:tsfemL10n.insecureNonce,i18n:tsfemL10n.i18n,rtl:tsfemL10n.rtl,debug:tsfemL10n.debug,touchBuffer:!1,noticeBuffer:!1,navWarn:!1,setTouchBuffer:function(a){tsfem.touchBuffer=!0,setTimeout(()=>{tsfem.touchBuffer=!1},a)},setAjaxLoader:function(a){jQuery(a).toggleClass("tsfem-loading")},unsetAjaxLoader:function(a,b,c,d){let e="tsfem-success",f=2500;b?2===b&&(e="tsfem-unknown",f=7500):(e="tsfem-error",f=d?2e4:1e4),f=c?2*f:f,d?jQuery(a).removeClass("tsfem-loading").addClass(e).html(c).fadeOut(f):(c=jQuery("").html(c).text(),jQuery(a).removeClass("tsfem-loading").addClass(e).text(c).fadeOut(f))},resetAjaxLoader:function(a){jQuery(a).stop().empty().prop("class","tsfem-ajax").css({opacity:"1",display:"initial"}).prop("style","")},updateFeed:function(a){let b="tsfem-button-disabled",c=jQuery(a.target),d="#tsfem-feed-ajax",e=0;if(c.prop("disabled"))return;c.addClass("tsfem-button-disabled"),c.prop("disabled",!0),tsfem.resetAjaxLoader("#tsfem-feed-ajax"),tsfem.setAjaxLoader("#tsfem-feed-ajax");const f=()=>{c.removeClass(b),c.prop("disabled",!1),tsfem.updatedResponse(d,e,tsfem.i18n.UnknownError,0)};jQuery.ajax({method:"POST",url:ajaxurl,dataType:"json",data:{action:"tsfem_enable_feeds",nonce:tsfem.nonce},timeout:12e3,async:!0}).done(a=>{var b=Math.round,c=Math.pow;a=tsfem.convertJSONResponse(a),tsfem.debug&&console.log(a);let g=a&&a.data||void 0,h=a&&a.type||void 0;if("success"===h&&g){let a=g.content;switch(a.status){case"success":e=1,jQuery(".tsfem-trends-wrap").empty().css("opacity",0).append(a.wrap).animate({opacity:1},{queue:!0,duration:250});let f=400,g=a.data.length,h=0;for(let a=1;a{f=b(f/c(1+a/2/100,2)),setTimeout(()=>{jQuery(d).hide().appendTo(".tsfem-feed-wrap").slideDown(f)},f/2*a)}),setTimeout(()=>{tsfem.updatedResponse(d,e,"",0)},h);break;case"parse_error":case"unknown_error":default:jQuery(".tsfem-trends-wrap").empty().css("opacity",0).append(a.error_output).css("opacity",1).find(".tsfem-feed-wrap").css({opacity:0}).animate({opacity:1},{queue:!0,duration:2e3}),e="unknown_error"===a.status?2:0,setTimeout(()=>{tsfem.updatedResponse(d,e,tsfem.i18n.UnknownError,0)},1e3);}}else"unknown"===a.type?(e=2,f()):f()}).fail((a,e,f)=>{let g=tsfem.getAjaxError(a,e,f);c.removeClass(b),c.prop("disabled",!1),tsfem.updatedResponse(d,0,g,0)})},updateExtension:function(a){let b="tsfem-button-disabled",c=jQuery(a.target);if(c.prop("disabled")||c.hasClass("tsfem-button-disabled"))return;let d=jQuery(".tsfem-button-extension-activate, .tsfem-button-extension-deactivate").not(jQuery(".tsfem-button-disabled")),e=c.data("slug"),f=c.data("case"),g="#tsfem-extensions-ajax",h=0,i="",j=0,k="";d.map(()=>{jQuery(this).addClass(b),jQuery(this).prop("disabled",!0)}),tsfem.resetAjaxLoader(g),tsfem.setAjaxLoader(g),jQuery.ajax({method:"POST",url:ajaxurl,dataType:"json",data:{action:"tsfem_update_extension",nonce:tsfem.nonce,slug:e,case:f},timeout:1e4,async:!0}).done(a=>{a=tsfem.convertJSONResponse(a),tsfem.debug&&console.log(a);let b=a&&a.data||void 0,d=a&&a.type||void 0;if(!b||!d)k=tsfem.i18n.UnknownError;else{let a=b.results&&b.results.code||void 0,d=b.results&&b.results.success||void 0;if(k=b.results&&b.results.notice||void 0,"activate"===f)switch(a){case 10001:case 10002:case 10003:case 10004:case 10006:case 10007:case 10013:case 10014:h=0,j=a;break;case 10005:h=0;let d=b&&b.fatal_error||void 0;i=d,j=a;break;case 10008:case 10010:case 10012:h=1,c.removeClass("tsfem-button tsfem-button-extension-activate").addClass("tsfem-button-primary tsfem-button-primary-dark tsfem-button-extension-deactivate"),c.data("case","deactivate"),c.text(tsfem.i18n.Deactivate),jQuery("#"+e+"-extension-entry").removeClass("tsfem-extension-deactivated").addClass("tsfem-extension-activated"),tsfem.updateExtensionDescFooter(e,f);break;case 10009:h=2,j=a;break;default:h=0,k=tsfem.i18n.UnknownError;}else"deactivate"===f&&(11001===a?(h=1,c.removeClass("tsfem-button-primary tsfem-button-primary-dark tsfem-button-extension-deactivate").addClass("tsfem-button tsfem-button-extension-activate"),c.data("case","activate"),c.text(tsfem.i18n.Activate),jQuery("#"+e+"-extension-entry").removeClass("tsfem-extension-activated").addClass("tsfem-extension-deactivated"),tsfem.updateExtensionDescFooter(e,f)):11002===a||11003===a||11004===a?(h=0,j=a):(h=0,k=tsfem.i18n.UnknownError))}}).fail((a,b,c)=>{k=tsfem.getAjaxError(a,b,c),tsfem.setTopNotice(1071100),c&&tsfem.setTopNotice(-1,"jQ error: "+c)}).always(()=>{tsfem.updatedResponse(g,h,k,0),d.removeClass(b),d.prop("disabled",!1),c.focus(),j&&tsfem.setTopNotice(j,i)})},convertJSONResponse:function(a){let b=a&&a.json||void 0,c=1===b;if(!c){let b=a;try{a=JSON.parse(a),c=!0}catch(a){c=!1}c||(a=b)}return a},updatedResponse:function(a,b,c,d){switch(b){case 0:case 1:case 2:tsfem.unsetAjaxLoader(a,b,c,d);break;default:tsfem.resetAjaxLoader(a);}},getAjaxError:function(a,b,c){tsfem.debug&&(console.log(a.responseText),console.log(c));let d="";switch(c){case"abort":case"timeout":d=tsfem.i18n.TimeoutError;break;case"Bad Request":d=tsfem.i18n.BadRequest;break;case"Internal Server Error":d=tsfem.i18n.FatalError;break;case"parsererror":d=tsfem.i18n.ParseError;break;default:d=tsfem.i18n.UnknownError;}return d},unexpectedAjaxErrorNotice:function(a){a=tsfem.convertJSONResponse(a)||void 0;let b=a&&a.data||void 0;tsfem.debug&&console.log(a),b&&"results"in b&&"code"in b.results&&tsfem.setTopNotice(b.results.code,b.results.notice)},matosa:function(a){var b=null,c="";if(function a(d,e){if(e++,"object"==typeof d){let f,g;for(f in d)g=d[f];b=g,c+=1===e?f+a(g,e):"["+f+"]"+a(g,e)}else if(1===e)return b=null,c=!1;return c}(a,0),!1===c)return!1;let d={};return d[c]=b,d},updateExtensionDescFooter:function(a,b){jQuery.ajax({method:"POST",url:ajaxurl,dataType:"json",data:{action:"tsfem_update_extension_desc_footer",nonce:tsfem.nonce,slug:a,case:b},timeout:7e3,async:!0}).done(c=>{c=tsfem.convertJSONResponse(c),tsfem.debug&&console.log(c);let d=c&&c.data||void 0,e=c&&c.type||void 0;if(!d)return;let f=jQuery("#"+a+"-extension-entry .tsfem-extension-description-footer"),g="activate"===b?"up":"down";f.addClass("tsfem-flip-hide-"+g),setTimeout(()=>{f.empty().append(d),tsfTT.triggerReset()},250),setTimeout(()=>{f.addClass("tsfem-flip-show-"+g)},500),setTimeout(()=>{f.removeClass("tsfem-flip-hide-"+g+" tsfem-flip-show-"+g)},750)}).fail((a,b,c)=>{tsfem.debug&&(console.log(a.responseText),console.log(c))})},preventDefault:function(a){a.preventDefault(),a.stopPropagation()},engageSwitcher:function(){jQuery(window).off("click.tsfemResetSwitcher").on("click.tsfemResetSwitcher",a=>{let b=jQuery(".tsfem-switch-button-container > input[type=\"checkbox\"]:checked");if("undefined"!=typeof b&&0jQuery(a.target).closest(c).length&&(b.prop("checked",!1),jQuery(window).off("click.tsfemResetSwitcher"))}})},registerNavWarn:function(){tsfem.navWarn=!0},mustNavWarn:function(){return!!tsfem.navWarn},setDismissNoticeListener:function(){const a=a=>{jQuery(a.target).closest(".tsfem-notice").slideUp(200,function(){this.remove()})};jQuery(".tsfem-dismiss").off("click",a).on("click",a)},setTopNotice:function(a,b){if(tsfem.noticeBuffer)return void window.setTimeout(()=>{tsfem.setTopNotice(a,b)},500);tsfem.noticeBuffer=!0;let c=b?1:0;jQuery.ajax({method:"POST",url:ajaxurl,datatype:"json",data:{action:"tsfem_get_dismissible_notice",nonce:tsfem.insecureNonce,"tsfem-notice-key":a,"tsfem-notice-has-msg":c},timeout:7e3,async:!0}).done(a=>{a=tsfem.convertJSONResponse(a),tsfem.debug&&console.log(a);let d=a&&a.data||void 0,e=a&&a.type||void 0;if(!d||!e||"undefined"==typeof d.notice);else{let a="";c?(a=jQuery(d.notice),tsfem.rtl?a.find("p").first().prepend(b+" "):a.find("p").first().append(" "+b)):a=d.notice,tsfem.appendTopNotice(a)}}).fail((d,e,f)=>{tsfem.debug&&(console.log(d.responseText),console.log(f));let g=c?wp.template("tsfem-fbtopnotice-msg"):wp.template("tsfem-fbtopnotice"),h=g({code:a,msg:b});tsfem.appendTopNotice(h)}).always(()=>{tsfem.noticeBuffer=!1})},appendTopNotice:function(a){let b=jQuery(".tsfem-notice-wrap"),c=b.children(".tsfem-notice, .tsfem-notice-wrap .notice"),d={duration:200,queue:!1};b.css("willChange","contents"),1this.remove()}))}),jQuery(a).hide().appendTo(b).slideDown(jQuery.extend(d,{complete:()=>b.css("maxHeight","")})),tsfem.setDismissNoticeListener()},dialog:function(a){let b=a.title||"",c=a.text||"",d=a.select||"",e=a.confirm||"",f=a.cancel||"",g={};if(g.mask=document.createElement("div"),g.mask.className="tsfem-modal-mask",g.maskNoScroll=document.createElement("div"),g.maskNoScroll.className="tsfem-modal-mask-noscroll",g.mask.appendChild(g.maskNoScroll),g.container=document.createElement("div"),g.container.className="tsfem-modal-container",g.dialogWrap=document.createElement("div"),g.dialogWrap.className="tsfem-modal-dialog-wrap",g.dialogWrap.style.marginLeft=document.getElementById("adminmenuwrap").offsetWidth+"px",g.dialogWrap.style.marginTop=document.getElementById("wpadminbar").offsetHeight+"px",g.dialog=document.createElement("div"),g.dialog.className="tsfem-modal-dialog",g.trap=document.createElement("div"),g.trap.className="tsfem-modal-trap",g.trap.tabIndex=0,g.bottomTrap=g.trap.cloneNode(!1),g.dialog.appendChild(g.trap),g.x=document.createElement("div"),g.x.className="tsfem-modal-dismiss",g.x.addEventListener("click",function(){window.dispatchEvent(new Event("tsfem_modalCancel"))}),g.dialog.appendChild(g.x),b&&(g.titleWrap=document.createElement("div"),g.titleWrap.className="tsfem-modal-title",g.titleWrapTitle=document.createElement("h4"),g.titleWrapTitle.innerHTML=b,g.titleWrap.appendChild(g.titleWrapTitle),g.dialog.appendChild(g.titleWrap)),g.inner=document.createElement("div"),g.inner.className="tsfem-modal-inner",c){if(g.textWrap=document.createElement("div"),g.textWrap.className="tsfem-modal-text",Array.isArray(c))for(let a in c)g.textWrapContent=document.createElement("p"),g.textWrapContent.innerHTML=c[a],g.textWrap.appendChild(g.textWrapContent);else g.textWrapContent=document.createElement("p"),g.textWrapContent.innerHTML=c,g.textWrap.appendChild(g.textWrapContent);g.inner.appendChild(g.textWrap)}let h=!1;if(d){h=!0,g.selectWrap=document.createElement("div"),g.selectWrap.className="tsfem-modal-select";let a={};a.wrap=document.createElement("div"),a.wrap.className="tsfem-modal-select-option",a.radio=document.createElement("input"),a.radio.setAttribute("type","radio"),a.radio.setAttribute("name","tsfem-modal-select-option-group"),a.radio.tabIndex=0,a.label=document.createElement("label"),function(){for(let b in d){let c=a.wrap.cloneNode(!0),e=a.radio.cloneNode(!1),f=a.label.cloneNode(!1);e.setAttribute("value",b),f.innerHTML=d[b],0==b&&(e.checked=!0);let h="tsfem-dialog-option-"+b;e.setAttribute("id",h),f.setAttribute("for",h),c.appendChild(e),c.appendChild(f),g.selectWrap.appendChild(c)}}(),g.inner.appendChild(g.selectWrap)}g.dialog.appendChild(g.inner),(e||f)&&(g.buttonWrap=document.createElement("div"),g.buttonWrap.className="tsfem-modal-buttons",e&&(g.confirmButton=document.createElement("button"),g.confirmButton.className="tsfem-modal-confirm tsfem-button-small",g.confirmButton.className+=h?" tsfem-button-primary tsfem-button-primary-bright":" tsfem-button",g.confirmButton.innerHTML=e,g.confirmButton.addEventListener("click",function(){let a;h&&(a={detail:{checked:document.querySelector(".tsfem-modal-select input:checked").value}}),window.dispatchEvent(new CustomEvent("tsfem_modalConfirm",a))}),g.buttonWrap.appendChild(g.confirmButton)),f&&(g.cancelButton=document.createElement("button"),g.cancelButton.className="tsfem-modal-cancel tsfem-button tsfem-button-small",g.cancelButton.innerHTML=f,g.cancelButton.addEventListener("click",()=>{window.dispatchEvent(new Event("tsfem_modalCancel"))}),g.buttonWrap.appendChild(g.cancelButton)),g.dialog.appendChild(g.buttonWrap)),g.dialog.appendChild(g.bottomTrap),g.dialogWrap.appendChild(g.dialog),g.container.appendChild(g.dialogWrap),document.body.appendChild(g.mask),document.body.appendChild(g.container);const i=()=>{g.trap.focus()};g.trap.addEventListener("focus",i),g.bottomTrap.addEventListener("focus",i),g.trap.focus(),tsfem.fadeIn(g.mask),tsfem.fadeIn(g.container);const j=a=>{a.preventDefault()};g.maskNoScroll.addEventListener("wheel",j),g.maskNoScroll.addEventListener("touchmove",j);const k=function(){g.dialogWrap.style.marginLeft=document.getElementById("adminmenuwrap").offsetWidth+"px",g.dialogWrap.style.marginTop=document.getElementById("wpadminbar").offsetHeight+"px"};window.addEventListener("resize",k);const l=()=>{g.maskNoScroll.removeEventListener("wheel",j),g.maskNoScroll.removeEventListener("touchmove",j),window.removeEventListener("tsfem_modalCancel",l),window.removeEventListener("tsfem_modalConfirm",l),window.removeEventListener("resize",k),tsfem.fadeOut(g.mask,250,()=>g.mask.remove()),tsfem.fadeOut(g.container,250,()=>g.container.remove())};window.addEventListener("tsfem_modalCancel",l),window.addEventListener("tsfem_modalConfirm",l)},fadeIn:function(a,b,c,d){if(void 0===a||!a instanceof HTMLElement)return;if(!a.style||!("opacity"in a.style))return;b=b||250,d=!(void 0!==d)||d;let e,f,g=0,h=0,i=3;d?f=()=>{h=(g+=i)/100,a.style.display=null,a.style.opacity=h,1<=h&&(clearInterval(e),a.style.opacity=1,"function"==typeof c&&c())}:(g=100,f=()=>{h=(g-=i)/100,a.style.opacity=h,0>=h&&(clearInterval(e),a.style.opacity=0,setTimeout(()=>{a.style.display="none"},0),"function"==typeof c&&c())}),e=setInterval(f,b/100)},fadeOut:function(a,b,c){tsfem.fadeIn(a,b,c,!1)},ready:function(a){a("#wpbody-content").find(".updated, .error, .notice-error, .notice-warning").appendTo(".tsfem-notice-wrap"),a("a#tsfem-enable-feeds").on("click",tsfem.updateFeed),a(".tsfem-button-extension-activate, .tsfem-button-extension-deactivate").on("click",tsfem.updateExtension),a(".tsfem-button-disabled").on("click",tsfem.preventDefault),a(".tsfem-switch-button-container-wrap").on("click","label",tsfem.engageSwitcher),a(document.body).ready(tsfem.setDismissNoticeListener)}},jQuery(tsfem.ready); diff --git a/readme.txt b/readme.txt index 38b4f468..509262ea 100644 --- a/readme.txt +++ b/readme.txt @@ -41,7 +41,7 @@ Please refer to [the installation instructions on our website](https://kb.theseo == Changelog == -= 2.3.2 = += 2.4.0 = **Release date:** @@ -49,16 +49,20 @@ Please refer to [the installation instructions on our website](https://kb.theseo **Feature highlights:** -* In this update we reduced the plugin package size by 30%. Thanks to offloading translation files elsewhere on our servers, this reduction saves you bandwidth and speeds up plugin installation. +* In this update, we reduced the plugin package size by 30%. Thanks to offloading translation files elsewhere on our servers, this reduction saves you bandwidth and speeds up plugin installation. * During Extension Manager plugin update requests, your WordPress website may now download new and updated translation files independently. Which files are requested is based on your site's supported languages. +* We upgraded the extension API endpoint, which allows for reverse inflection lookups via the Focus extension, for 7 languages! +* We removed a filter that directed admin access control. We found that it wasn't secure enough (by our insane standards); so, use the new constant definition, instead. With that constant, you can now independently control extension and manager access. TODO update privacy policy to reflect these changes (we now request your site's installed locale and installed translation file details of Extension Manager): Information TSFEM sends to Us: (3) The plugin (at version 2.0.0 or later) may request plugin updates from our servers. While succesfully doing so, it sends us your WordPress version number, the PHP version number, the installed TSFEM plugin version number, your website’s IP address, and your website’s home URL. We collect this data for aggregating usage statistics, and to provide your site with the latest compatible version. The aggregated statistics will always be anonymized. **Detailed log:** -View the [detailed v2.3.2 changelog](https://theseoframework.com/?p=TODO). +View the [detailed v2.4.0 changelog](https://theseoframework.com/?p=TODO). +* **Added:** New constant `TSF_EXTENSION_MANAGER_EXTENSION_ADMIN_ROLE`, that allows you to modify the access level of the extension settings in `wp-config.php` or a mu-plugin. + * **Note:** Use `TSF_EXTENSION_MANAGER_MAIN_ADMIN_ROLE` to control the role required for managing the extension activation and API connection options. * **Changed:** The plugin extension API now reaches our new version 2.1 endpoint, from 2.0. * Version 2.0 will remain available for the unforeseeable future. * **Changed:** The plugin updater API now reaches our new version 1.1 endpoint, from 1.0. @@ -66,16 +70,20 @@ View the [detailed v2.3.2 changelog](https://theseoframework.com/?p=TODO). * TODO **Updated:** Plugin translation POT file contains a few adjusted strings. * **Removed:** We no longer ship the pomo translation files with the plugin. * However, the `/language/` folder still works as before, and manually inserted files therein supersede the update-service provided translations. +* **Removed:** Filter `tsf_extension_manager_can_manage_options` has been removed as it superimposes a security issue due to its nature in discrepancy, incoherency, and inconsistency. Use the constants instead; they can be defined only once, alleviating these issues altoghether. +* **Removed:** Method `tsf_extension_manager()->can_do_settings()`. Use the access control API functions, instead. * TODO update pricing page language support. +* TODO update privacy policy (although not pressing, since we don't require any new person-identifying information). **Updated extensions:** * [Articles at version 2.0.4](https://theseoframework.com/extensions/articles/#changelog) * [Focus at version 1.4.0](https://theseoframework.com/extensions/focus/#changelog) - * **Fixed:** TODO When parsing, synonyms now strip the content for future inflection lookups, and vice versa. - * This greatly affects performance on systems with more than 3 threads (80%+users?), since we must do this part of the parsing synchronously... - * **Added:** TODO (maybe later) Added a select-all button for inflections and synonyms. +* [Local at version 1.1.7](https://theseoframework.com/extensions/local/#changelog) +* [Monitor at version 1.2.6](https://theseoframework.com/extensions/monitor/#changelog) + * TODO **Fixed:** when no current data can be processed (version discrepancy local vs from the server), a helpful message is now shown. + * This only happens in development, though.... = 2.3.1 = diff --git a/the-seo-framework-extension-manager.php b/the-seo-framework-extension-manager.php index 5ee42f93..bfd7a897 100644 --- a/the-seo-framework-extension-manager.php +++ b/the-seo-framework-extension-manager.php @@ -3,7 +3,7 @@ * Plugin Name: The SEO Framework - Extension Manager * Plugin URI: https://theseoframework.com/extension-manager/ * Description: Add more powerful SEO features to The SEO Framework. Right from your WordPress dashboard. - * Version: 2.3.2-beta-5 + * Version: 2.4.0-beta-5 * Author: The SEO Framework Team * Author URI: https://theseoframework.com/ * License: GPLv3 @@ -44,7 +44,7 @@ * * @since 1.0.0 */ -define( 'TSF_EXTENSION_MANAGER_VERSION', '2.3.2' ); +define( 'TSF_EXTENSION_MANAGER_VERSION', '2.4.0' ); /** * The plugin's database version.