settings = $settings; $this->sanitizer = $sanitizer; $this->queue = $queue; $this->log = $log; $this->pages = $pages; $this->view = $view; } /** * Register hooks. * * @return void */ public function boot(): void { add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'admin_post_' . self::RESET_ACTION, array( $this, 'restore_defaults' ) ); add_action( 'admin_post_' . self::MODE_ACTION, array( $this, 'switch_mode' ) ); } /** * The mode the screen is in. * * @return string */ private function mode(): string { $mode = (string) get_option( self::MODE_OPTION, self::MODES[0] ); return in_array( $mode, self::MODES, true ) ? $mode : self::MODES[0]; } /** * Switch between simple and advanced mode, and come back to the screen. * * @return void */ public function switch_mode(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'adpix-analytics' ), '', array( 'response' => 403 ) ); } check_admin_referer( self::MODE_ACTION ); $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : ''; if ( in_array( $mode, self::MODES, true ) ) { update_option( self::MODE_OPTION, $mode, true ); } wp_safe_redirect( add_query_arg( 'page', self::PAGE, admin_url( 'admin.php' ) ) ); exit; } /** * The site's roles, as slug => display name. * * Read from wp_roles() rather than a fixed list, so roles a membership or shop plugin added are * offered too — those are usually the ones a site actually wants to exclude. * * @return array */ private function roles(): array { $names = (array) wp_roles()->get_names(); return array_map( 'translate_user_role', $names ); } /** * Put every setting back to its shipped default. * * The connection details are kept. Restoring defaults is something people reach for when a * measurement setting has gone wrong, and making that gesture also delete a property-wide write * credential — which cannot be recovered from this screen — would turn a small mistake into a * support ticket. The button says so. * * @return void */ public function restore_defaults(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'adpix-analytics' ), '', array( 'response' => 403 ) ); } check_admin_referer( self::RESET_ACTION ); $current = $this->settings->load(); $fresh = Settings::from_array( array() ); foreach ( self::PRESERVED as $key ) { $fresh = $fresh->with( array( $key => $current->get( $key ) ) ); } $this->settings->save( $fresh ); add_settings_error( Settings::OPTION, 'adpix_analytics_reset', __( 'Settings were restored to their defaults. Your connection details were kept.', 'adpix-analytics' ), 'success' ); // The same handoff options.php uses: the notices are stashed for the request after the redirect, // which is the only one that can display them. set_transient( 'settings_errors', get_settings_errors(), 30 ); wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE, 'settings-updated' => 'true', ), admin_url( 'admin.php' ) ) ); exit; } /** * Title for the browser tab and the page heading. * * @return string */ public function page_title(): string { return __( 'AdPix Analytics', 'adpix-analytics' ); } /** * Label in the sidebar. * * @return string */ public function menu_title(): string { return __( 'Settings', 'adpix-analytics' ); } /** * Capability required to open it. * * @return string */ public function capability(): string { return self::CAPABILITY; } /** * Menu slug. * * @return string */ public function slug(): string { return self::PAGE; } /** * Record the hook suffix WordPress gave this screen. * * @param string $hook Hook suffix. * @return void */ public function set_hook( string $hook ): void { $this->hook = $hook; } /** * Register the option and its sanitize callback. * * @return void */ public function register_settings(): void { register_setting( self::GROUP, Settings::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( $this->sanitizer, 'sanitize' ), 'default' => Settings::defaults(), // Never exposed through the REST API: the option holds the server key, and a credential // that can write to the whole property has no business being readable over HTTP. 'show_in_rest' => false, ) ); } /** * Load the admin assets, on this screen only. * * @param string $hook Current admin page hook. * @return void */ public function enqueue( $hook ): void { // Compared against the suffix WordPress handed back at registration, never against one built // from the menu title: that title is translated, and its sanitized form differs by language. if ( '' === $this->hook || $this->hook !== $hook ) { return; } // Dashicons labels the tab strip. Core registers it, and admin screens usually load it already; // declaring the dependency means the icons cannot arrive before the font that draws them. wp_enqueue_style( 'adpix-analytics-admin', ADPIX_ANALYTICS_URL . 'assets/css/admin.css', array( 'dashicons' ), ADPIX_ANALYTICS_VERSION ); /* * The tab strip is an enhancement: every panel is in the document on every load, and the tab * script is what decides which one is on screen. These two rules are the fallback for when * that script never runs - they drop the strip and put every panel back - and they hang off * the no-js class core puts on the admin body and removes again from its own inline script, * so they apply only in that case. Added through wp_add_inline_style() rather than printed * in the template, so the rules travel with the stylesheet they belong to. */ wp_add_inline_style( 'adpix-analytics-admin', $this->no_script_css() ); wp_enqueue_script( 'adpix-analytics-admin', ADPIX_ANALYTICS_URL . 'assets/js/admin.js', array(), ADPIX_ANALYTICS_VERSION, true ); wp_localize_script( 'adpix-analytics-admin', 'adpixAnalyticsAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( AjaxController::NONCE ), 'testing' => __( 'Testing the connection…', 'adpix-analytics' ), 'failed' => __( 'The connection test could not be completed.', 'adpix-analytics' ), 'clearing' => __( 'Clearing…', 'adpix-analytics' ), 'confirmReset' => __( 'Restore every setting to its default? Your connection details will be kept.', 'adpix-analytics' ), 'pageSearch' => __( 'Search by title…', 'adpix-analytics' ), 'pageNone' => __( 'No matching pages.', 'adpix-analytics' ), /* translators: %s: page title. */ 'pageRemove' => __( 'Remove %s', 'adpix-analytics' ), ) ); } /** * The rules that undo the tab strip when the script behind it never runs. * * @return string CSS, without a style element around it. */ private function no_script_css(): string { return 'body.no-js .adpix-app .adpix-tabs { display: none; }' . 'body.no-js .adpix-app .adpix-panel[hidden] { display: block; }'; } /** * Render the screen. * * @return void */ public function render(): void { // The menu already gates the sidebar entry, but the callback is reachable by anyone who // guesses the URL, so the capability is checked where the page is actually produced. if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to manage AdPix Analytics settings.', 'adpix-analytics' ) ); } $settings = $this->settings->load(); $this->view->render( 'settings-page', array( 'settings' => $settings, 'option_name' => Settings::OPTION, 'group' => self::GROUP, 'page' => self::PAGE, 'masked_key' => SettingsSanitizer::mask( $settings->str( 'server_key' ) ), 'masked_token' => SettingsSanitizer::mask( $settings->str( 'api_token' ) ), 'stats_url' => add_query_arg( 'page', StatsController::PAGE, admin_url( 'admin.php' ) ), 'queue_label' => $this->queue->label(), 'queue_size' => $this->queue->size(), 'log_entries' => $this->log->entries(), 'is_connected' => $settings->is_connected(), 'roles' => $this->roles(), 'excluded' => $this->pages->describe( $settings->excluded_post_ids() ), 'can_edit_code' => current_user_can( 'unfiltered_html' ), 'reset_action' => self::RESET_ACTION, 'mode' => $this->mode(), 'mode_action' => self::MODE_ACTION, 'mode_field' => SettingsSanitizer::MODE_FIELD, // Passed in rather than read from a constant inside the template, so the view keeps // depending only on what it was given. 'logo_url' => ADPIX_ANALYTICS_URL . 'assets/img/logo.svg', 'version' => ADPIX_ANALYTICS_VERSION, ) ); } }