array( 'type' => 'bool', 'default' => true, ), ); } /** * Field labels. * * @param string $key Field key. * @return string */ public function field_label( $key ) { switch ( $key ) { case 'admin_bar': return __( 'Show Edit CSS in the admin bar', 'adminkeep' ); default: return ''; } } /** * Settings with their defaults filled in. * * @param array $settings Stored settings. * @return array{admin_bar: bool} */ public static function resolve( array $settings ) { return array( 'admin_bar' => ! isset( $settings['admin_bar'] ) || (bool) $settings['admin_bar'], ); } /** * What switching this on actually does. * * @return string[] */ public function how_it_works() { return array( __( 'Adds Appearance → Custom CSS, with the same code editor WordPress uses for its own CSS box.', 'adminkeep' ), __( 'Adds Edit CSS to the admin bar on your site: a code panel beside the page that restyles it as you type. You can hide the admin bar item and open the panel from Appearance → Custom CSS instead.', 'adminkeep' ), __( 'Prints your CSS on the front end of the site, on whichever theme is active. Switching themes does not lose it.', 'adminkeep' ), __( 'Your theme\'s own Additional CSS is left alone. Both can be used; this one prints second, so it wins when the two disagree.', 'adminkeep' ), __( 'Anything that looks like HTML is refused when you save, so a pasted snippet cannot break out of the style tag.', 'adminkeep' ), __( 'Switching this off stops the CSS printing and removes the menu item. The CSS itself is kept until you uninstall.', 'adminkeep' ), ); } /** * Where the settings panel sends you to do the actual work. * * @return array{url?: string, label?: string} Empty when this user could not open the screen. */ public function panel_link() { if ( ! CustomCssScreen::can_edit() ) { return array(); } return array( 'url' => CustomCssScreen::url(), 'label' => __( 'Edit CSS', 'adminkeep' ), ); } /** * The editing screen. * * @return CustomCssScreen */ public function screen() { if ( null === $this->screen ) { $this->screen = new CustomCssScreen(); } return $this->screen; } /** * The front-end editing panel. * * @return LiveCssPanel */ public function panel() { if ( null === $this->panel ) { $this->panel = new LiveCssPanel(); } return $this->panel; } /** * Register hooks. * * @param array $settings Stored settings. * @return void */ public function boot( array $settings ) { $resolved = self::resolve( $settings ); add_action( 'wp_head', array( $this, 'print_css' ), self::HEAD_PRIORITY ); add_action( 'rest_api_init', array( $this, 'register_rest_route' ) ); if ( is_admin() ) { $this->screen()->boot(); } else { $this->panel()->boot( $resolved['admin_bar'] ); } } /** * The stored CSS. * * @return string */ public static function css() { $css = get_option( self::OPTION, '' ); return is_string( $css ) ? $css : ''; } /** * Check CSS the way core checks its own Additional CSS: no markup. * * @param string $css Raw CSS. * @return true|\WP_Error */ public static function validate( $css ) { if ( preg_match( '# \WP_REST_Server::READABLE, 'callback' => array( $this, 'rest_get' ), 'permission_callback' => array( $this, 'rest_can_edit' ), ), array( 'methods' => \WP_REST_Server::EDITABLE, 'callback' => array( $this, 'rest_save' ), 'permission_callback' => array( $this, 'rest_can_edit' ), 'args' => array( 'css' => array( 'type' => 'string', 'required' => true, // Named explicitly: in a hand-written args array the type alone is not // enforced, and an array or number would otherwise reach the callback. 'validate_callback' => 'rest_validate_request_arg', ), ), ), ) ); } /** * The screen's gate: edit_theme_options and edit_css together. * * @return bool|\WP_Error */ public function rest_can_edit() { if ( CustomCssScreen::can_edit() ) { return true; } return new \WP_Error( 'adminkeep_css_forbidden', __( 'You are not allowed to edit CSS on this site.', 'adminkeep' ), array( 'status' => rest_authorization_required_code() ) ); } /** * GET: the stored CSS, byte for byte. * * @return \WP_REST_Response */ public function rest_get() { return rest_ensure_response( array( 'css' => self::css() ) ); } /** * POST: replace the stored CSS. * * Refused CSS is a 400 carrying the same message the screen shows, and the stored CSS is left * as it was. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function rest_save( $request ) { $saved = self::save( (string) $request->get_param( 'css' ) ); if ( is_wp_error( $saved ) ) { $saved->add_data( array( 'status' => 400 ) ); return $saved; } return rest_ensure_response( array( 'css' => $saved ) ); } /** * Print the CSS into the head. * * @return void */ public function print_css() { $css = trim( self::css() ); // With the panel open the tag is printed even when empty: it is what the preview writes // into, and keeping it here keeps the preview at the same place in the cascade. if ( '' === $css && ! LiveCssPanel::is_open() ) { return; } // CSS cannot be HTML-escaped without corrupting it (child combinators, quoted strings). // Stripping tags is what core does for Additional CSS; markup was already refused on save. echo '' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } }