, message: string}|null */ private $refused = null; /** * Whether the current user may edit the site's header and footer code. * * @return bool */ public static function can_edit() { foreach ( self::CAPABILITIES as $capability ) { if ( ! current_user_can( $capability ) ) { return false; } } return true; } /** * URL of the screen. * * @return string */ public static function url() { return admin_url( 'themes.php?page=' . self::PAGE ); } /** * Id of a box's textarea, which the code editor attaches to. * * @param string $location head, body or footer. * @return string */ public static function editor_id( $location ) { return 'adminkeep-code-' . $location; } /** * Id of the "Last changed by …" line under a box, which the save script rewrites. * * @param string $location head, body or footer. * @return string */ public static function line_id( $location ) { return 'adminkeep-code-' . $location . '-changed'; } /** * Register hooks. * * @return void */ public function boot() { add_action( 'admin_menu', array( $this, 'register_page' ) ); add_action( 'admin_init', array( $this, 'handle_save' ), 10, 0 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_editor' ) ); } /** * Add the page under Appearance. * * A menu item takes one capability and this screen needs two, so a user who would only be * refused on arrival is not shown the entry at all. * * @return void */ public function register_page() { if ( ! self::can_edit() ) { return; } $this->hook = (string) add_submenu_page( 'themes.php', __( 'Header & Footer Code', 'adminkeep' ), __( 'Header & Footer Code', 'adminkeep' ), 'manage_options', self::PAGE, array( $this, 'render' ) ); } /** * Load WordPress's own code editor, once for all three boxes, and the save script, on this * screen only. * * When the user has switched syntax highlighting off in their profile, * wp_enqueue_code_editor() returns false and the plain textareas are the editors. The save * script loads either way: it reads whichever of the two is there. * * @param string $hook_suffix Current admin page. * @return void */ public function enqueue_editor( $hook_suffix ) { if ( '' === $this->hook || $hook_suffix !== $this->hook ) { return; } $this->enqueue_save_script(); $settings = wp_enqueue_code_editor( array( 'type' => 'text/html' ) ); if ( false === $settings ) { return; } $ids = array(); foreach ( HeaderFooterCode::LOCATIONS as $location ) { $ids[] = self::editor_id( $location ); } wp_add_inline_script( 'code-editor', sprintf( 'jQuery( function() { var settings = %2$s; %1$s.forEach( function( id ) { wp.codeEditor.initialize( id, settings ); } ); } );', wp_json_encode( $ids ), wp_json_encode( $settings ) ) ); wp_add_inline_style( 'code-editor', '.adminkeep-code-screen .CodeMirror{height:16em;border:1px solid #c3c4c7;}' ); } /** * Load the save script, with what it needs to rewrite each box's "Last changed" line. * * The route answers with each box's stamp as numbers: a user id and a timestamp. The line is * turned back into words in the browser rather than by the route, so the route's shape stays * the data it is for everyone else. A save from this screen stamps the boxes it changed with * the current user, whose name is sent along; the date is formatted by core's wp-date with the * site's own format and timezone, which is what wp_date() uses here. Another user's stamp can * only appear if someone saved the same box between this page loading and the save; the script * looks that name up through core's users route, and leaves the line alone if it cannot. * * @return void */ private function enqueue_save_script() { $fields = array(); $boxes = array(); foreach ( HeaderFooterCode::codes() as $location => $box ) { $fields[ $location ] = self::editor_id( $location ); $boxes[ $location ] = array( 'line' => self::line_id( $location ), 'by' => $box['changed_by'], 'at' => $box['changed_at'], ); } $user_id = get_current_user_id(); CodeScreenSave::enqueue( array( 'route' => esc_url_raw( rest_url( RestController::NAMESPACE_V1 . '/header-footer-code' ) ), 'form' => self::FORM_ID, 'fields' => $fields, 'i18n' => array( 'saved' => __( 'Code saved.', 'adminkeep' ) ), 'lines' => array( 'template' => self::line_template(), 'format' => get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), 'user' => array( 'id' => $user_id, 'name' => self::who( $user_id ), ), 'cli' => self::who( 0 ), 'users' => esc_url_raw( rest_url( 'wp/v2/users/' ) ), 'boxes' => $boxes, ), ), array( 'wp-date' ) ); } /** * Save a submission. * * @return void */ public function handle_save() { if ( empty( $_POST[ self::SAVE_FIELD ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked below, once this is known to be our form. return; } if ( ! self::can_edit() ) { wp_die( esc_html__( 'You are not allowed to edit the header and footer code on this site.', 'adminkeep' ), '', 403 ); } check_admin_referer( self::NONCE ); // Not sanitized: this is raw HTML and script by design, from a user who holds // unfiltered_html. The feature checks size and encoding on save, and normalises line endings. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw HTML by design; see above. $posted = isset( $_POST[ self::CODE_FIELD ] ) && is_array( $_POST[ self::CODE_FIELD ] ) ? wp_unslash( $_POST[ self::CODE_FIELD ] ) : array(); $codes = array(); foreach ( HeaderFooterCode::LOCATIONS as $location ) { if ( isset( $posted[ $location ] ) && is_string( $posted[ $location ] ) ) { $codes[ $location ] = $posted[ $location ]; } } $saved = HeaderFooterCode::save( $codes, get_current_user_id() ); if ( is_wp_error( $saved ) ) { $shown = array(); foreach ( HeaderFooterCode::codes() as $location => $box ) { $shown[ $location ] = isset( $codes[ $location ] ) ? $codes[ $location ] : $box['code']; } $this->refused = array( 'codes' => $shown, 'message' => $saved->get_error_message(), ); return; } wp_safe_redirect( add_query_arg( 'updated', '1', self::url() ) ); exit; } /** * Render the screen. * * @return void */ public function render() { if ( ! self::can_edit() ) { return; } $stored = HeaderFooterCode::codes(); echo '
%s
', esc_html__( 'Code saved here is printed on the front end of your site, on every page and exactly as you enter it. It is not added to the admin or the login page. Broken markup here can break your pages, so paste snippets as the service that issued them gave them to you.', 'adminkeep' ) ); echo '