HeaderFooterCodeScreen::url(), 'label' => __( 'Edit code', 'adminkeep' ), ); } /** * The editing screen. * * @return HeaderFooterCodeScreen */ public function screen() { if ( null === $this->screen ) { $this->screen = new HeaderFooterCodeScreen(); } return $this->screen; } /** * Register hooks. * * An admin request gets the screen and nothing else: the print hooks are front-end hooks and * have no business being registered there. * * @param array $settings Resolved settings. Unused — this feature has none. * @return void */ public function boot( array $settings ) { if ( is_admin() ) { $this->screen()->boot(); return; } add_action( 'wp_head', array( $this, 'print_head' ), self::HEAD_PRIORITY ); add_action( 'wp_body_open', array( $this, 'print_body' ) ); add_action( 'wp_footer', array( $this, 'print_footer' ), self::FOOTER_PRIORITY ); } /** * Translated name of a box, for the screen and for messages. * * @param string $location head, body or footer. * @return string */ public static function location_label( $location ) { switch ( $location ) { case 'head': return __( 'Head', 'adminkeep' ); case 'body': return __( 'Body open', 'adminkeep' ); case 'footer': return __( 'Footer', 'adminkeep' ); } return (string) $location; } /** * All three boxes, always well formed, whatever the option holds. * * A box never saved has empty code and a changed_at of 0. * * @return array */ public static function codes() { $stored = get_option( self::OPTION, array() ); $stored = is_array( $stored ) ? $stored : array(); $codes = array(); foreach ( self::LOCATIONS as $location ) { $box = isset( $stored[ $location ] ) && is_array( $stored[ $location ] ) ? $stored[ $location ] : array(); $codes[ $location ] = array( 'code' => isset( $box['code'] ) && is_string( $box['code'] ) ? $box['code'] : '', 'changed_by' => isset( $box['changed_by'] ) ? absint( $box['changed_by'] ) : 0, 'changed_at' => isset( $box['changed_at'] ) ? absint( $box['changed_at'] ) : 0, ); } return $codes; } /** * Check boxes before anything is stored: the size cap, measured in bytes since bytes are what * the database stores, and valid UTF-8, which a strict database refuses or cuts short. * * @param array $codes Location => code. Unknown locations are ignored. * @return true|\WP_Error */ public static function validate( array $codes ) { foreach ( self::LOCATIONS as $location ) { if ( ! isset( $codes[ $location ] ) ) { continue; } $bytes = strlen( (string) $codes[ $location ] ); if ( $bytes > self::MAX_BYTES ) { return new \WP_Error( 'adminkeep_code_too_long', sprintf( /* translators: 1: box name, 2: its size in bytes, 3: the limit in bytes, 4: the limit as a readable size such as "64 KB". */ __( 'The %1$s box holds %2$s bytes; one box can hold up to %3$s bytes (%4$s). Nothing was saved. For a large script, upload it as a file and load it with a script tag\'s src attribute instead of pasting it in.', 'adminkeep' ), self::location_label( $location ), number_format_i18n( $bytes ), number_format_i18n( self::MAX_BYTES ), size_format( self::MAX_BYTES ) ) ); } // wp_check_invalid_utf8() hands back an empty string for invalid UTF-8, and the text // unchanged when it is valid or the site does not use UTF-8 at all. if ( wp_check_invalid_utf8( (string) $codes[ $location ] ) !== (string) $codes[ $location ] ) { return new \WP_Error( 'adminkeep_code_invalid_utf8', sprintf( /* translators: %s: box name. */ __( 'The %s box contains characters that are not valid UTF-8, which the database may refuse or cut short. Nothing was saved. Copy the snippet again from where it came from, or save it as UTF-8 first.', 'adminkeep' ), self::location_label( $location ) ) ); } } return true; } /** * Store boxes, stamping only the ones whose code actually changed. The one write path, used by * the screen and by `wp adminkeep code set`. * * @param array $codes Location => code. A location left out is kept as it is. * @param int $user_id Who is saving; 0 when WP-CLI runs without a user. * @return true|\WP_Error Error when a box is too long or the database refused the write. */ public static function save( array $codes, $user_id ) { foreach ( $codes as $location => $code ) { $codes[ $location ] = str_replace( "\r\n", "\n", (string) $code ); } $valid = self::validate( $codes ); if ( is_wp_error( $valid ) ) { return $valid; } $next = self::codes(); $changed = false; foreach ( self::LOCATIONS as $location ) { if ( ! isset( $codes[ $location ] ) || $codes[ $location ] === $next[ $location ]['code'] ) { continue; } $next[ $location ] = array( 'code' => $codes[ $location ], 'changed_by' => absint( $user_id ), 'changed_at' => time(), ); $changed = true; } if ( ! $changed ) { return true; } global $wpdb; // Start clean, so any error reported below is this write's and not a leftover from an // earlier query on the same request. $wpdb->last_error = ''; if ( update_option( self::OPTION, $next, true ) ) { return true; } // update_option() answers false for a refused write, but also when the row already holds // this exact value and the UPDATE touches nothing: a double-clicked Save whose first // request landed a moment earlier. Only the row itself can tell the two apart. $error = (string) $wpdb->last_error; $row = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", self::OPTION ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- The cache is what cannot be trusted here. if ( null !== $row && maybe_unserialize( $row ) === $next ) { // The cache still holds the old value, since update_option() stopped before refreshing it. wp_cache_delete( self::OPTION, 'options' ); wp_cache_delete( 'alloptions', 'options' ); return true; } $message = __( 'The code could not be saved: the database did not accept the change.', 'adminkeep' ); if ( '' !== $error ) { /* translators: %s: the database's own error message. */ $message .= ' ' . sprintf( __( 'It said: %s', 'adminkeep' ), $error ); } return new \WP_Error( 'adminkeep_code_not_saved', $message ); } /** * Print the Head box. * * @return void */ public function print_head() { $this->print_location( 'head' ); } /** * Print the Body open box. * * @return void */ public function print_body() { $this->print_location( 'body' ); } /** * Print the Footer box. * * @return void */ public function print_footer() { $this->print_location( 'footer' ); } /** * Print one box, unless it is empty or this is an admin request. * * @param string $location head, body or footer. * @return void */ private function print_location( $location ) { // boot() does not hook these in the admin; this covers anything that fires the front-end // hooks from an admin request all the same. if ( is_admin() ) { return; } $code = self::codes()[ $location ]['code']; if ( '' === trim( $code ) ) { return; } // Printed as saved, deliberately: this is the site owner's own markup and script, and any // escaping would break it. Only users holding unfiltered_html can save it. echo $code . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw by design; saving needs unfiltered_html. } }