> */ public static function fields() { return array( 'headline' => array( 'label' => __( 'Headline / Review Title', 'advanced-testimonial' ), 'type' => 'text', ), 'video' => array( 'label' => __( 'Video URL (YouTube, Vimeo or MP4)', 'advanced-testimonial' ), 'type' => 'url', ), 'company' => array( 'label' => __( 'Company', 'advanced-testimonial' ), 'type' => 'text', ), 'designation' => array( 'label' => __( 'Designation / Job Title', 'advanced-testimonial' ), 'type' => 'text', ), 'location' => array( 'label' => __( 'Location', 'advanced-testimonial' ), 'type' => 'text', ), 'website' => array( 'label' => __( 'Website', 'advanced-testimonial' ), 'type' => 'url', ), 'email' => array( 'label' => __( 'Email (optional)', 'advanced-testimonial' ), 'type' => 'email', ), 'rating' => array( 'label' => __( 'Rating', 'advanced-testimonial' ), 'type' => 'rating', ), 'review_date' => array( 'label' => __( 'Review Date', 'advanced-testimonial' ), 'type' => 'date', ), 'verified' => array( 'label' => __( 'Verified Customer', 'advanced-testimonial' ), 'type' => 'checkbox', ), 'company_logo' => array( 'label' => __( 'Company Logo', 'advanced-testimonial' ), 'type' => 'media', ), 'social_twitter' => array( 'label' => __( 'X / Twitter URL', 'advanced-testimonial' ), 'type' => 'url', ), 'social_linkedin' => array( 'label' => __( 'LinkedIn URL', 'advanced-testimonial' ), 'type' => 'url', ), 'social_facebook' => array( 'label' => __( 'Facebook URL', 'advanced-testimonial' ), 'type' => 'url', ), ); } /** * Register the meta box. * * @return void */ public function add_meta_box() { add_meta_box( 'advanced-testimonial-details', __( 'Testimonial Details', 'advanced-testimonial' ), array( $this, 'render' ), CPT::POST_TYPE, 'normal', 'high' ); } /** * Render the meta box. * * @param \WP_Post $post Current post. * @return void */ public function render( $post ) { wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME ); echo '

' . esc_html__( 'The post title is the client name, the editor is the review text, and the featured image is the client photo.', 'advanced-testimonial' ) . '

'; echo '
'; foreach ( self::fields() as $name => $field ) { $value = get_post_meta( $post->ID, Helpers::meta_key( $name ), true ); $id = 'at-field-' . $name; echo '
'; echo ''; echo '
'; $this->render_control( $name, $field, $value, $id ); echo '
'; echo '
'; } echo '
'; } /** * Render a single field control. * * @param string $name Field name. * @param array $field Field definition. * @param mixed $value Stored value. * @param string $id Element id. * @return void */ private function render_control( $name, $field, $value, $id ) { $key = Helpers::meta_key( $name ); switch ( $field['type'] ) { case 'rating': $rating_val = Helpers::clamp_rating( $value ); echo ''; break; case 'checkbox': echo ''; break; case 'media': $attachment_id = (int) $value; $preview = $attachment_id ? wp_get_attachment_image( $attachment_id, 'thumbnail', false, array( 'class' => 'at-media-preview-img' ) ) : ''; echo '
'; echo ''; echo '
' . $preview . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_get_attachment_image() returns safe markup. echo ' '; echo ''; echo '
'; break; case 'url': echo ''; break; case 'email': echo ''; break; case 'date': echo ''; break; default: echo ''; break; } } /** * Persist the submitted values. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. * @return void */ public function save( $post_id, $post ) { unset( $post ); // Required by the save_post hook signature; only the ID is used. if ( ! isset( $_POST[ self::NONCE_NAME ] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST[ self::NONCE_NAME ] ) ), self::NONCE_ACTION ) ) { return; } if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } foreach ( self::fields() as $name => $field ) { $key = Helpers::meta_key( $name ); $raw = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized per field type in sanitize_value(). $sanitized = self::sanitize_value( $field['type'], $raw ); if ( '' === $sanitized || ( 'media' === $field['type'] && 0 === $sanitized ) || ( 'checkbox' === $field['type'] && '' === $sanitized ) ) { delete_post_meta( $post_id, $key ); } else { update_post_meta( $post_id, $key, $sanitized ); } } } /** * Sanitize a value according to its field type. * * Public and static so register_post_meta() can reuse the exact same * rules for REST writes — one sanitizer, two entry points. * * @param string $type Field type. * @param mixed $raw Raw submitted value. * @return mixed */ public static function sanitize_value( $type, $raw ) { switch ( $type ) { case 'rating': return Helpers::clamp_rating( $raw ); case 'media': return absint( $raw ); case 'checkbox': return ( '1' === (string) $raw ) ? '1' : ''; case 'url': return esc_url_raw( trim( (string) $raw ) ); case 'email': return sanitize_email( (string) $raw ); case 'date': $date = sanitize_text_field( (string) $raw ); return ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) ? $date : ''; default: return sanitize_text_field( (string) $raw ); } } }