WordPress, Password Protected Data in Endpoints

I recently worked on a small Web Application using Vue on the front-end and WordPress as the backend.  The client had a static site, wanted a blog, as well as CPT for loading specific type projects (not viewable on the blog, but the static site).  Some of these projects were also password protected, and when loading the data on the front-end I was in an interesting dilemma.

I needed to do the following:

  • return X and all other required info
  • if X has a password, do not return specific field data
    • the user would click a button, a modal would appear
    • they would enter a password
    • if it’s correct, I’d then return the data in that endpoint that missing

It was my first real chance to dive into Vue in a client project, making it all the more excited.  I’ve listed the basic endpoint code below, so if you come across this and find yourself needing something similar from the WordPress endpoint, you’ll get off to a more efficient start!

In this particular case, there was CPT that is extended and only returns X.

<?php if( ! defined( 'ABSPATH' ) ) exit;

if( class_exists( 'WP_REST_Posts_Controller' ) ){

  Class WP_REST_YOURNAME_Posts_Controller extends WP_REST_Posts_Controller {

    /**
     * WP_REST_YOURNAME_Posts_Controller constructor.
     */
    public function __construct() {

      parent::__construct( 'post-type' );
      $this->namespace = 'your-thing/v1';
    }

    /**
     * Register our custom route
     *
     * We
     */
    public function register_routes() {

      register_rest_route( $this->namespace, '/blahblah', array(
        array(
          'methods'             => WP_REST_Server::READABLE,
          'callback'            => array( $this, 'get_items' ),
          'permission_callback' => array( $this, 'get_items_permissions_check' ),
          'args'                => $this->get_collection_params(),
        ),
        'schema' => array( $this, 'get_public_item_schema' ),
      ) );
    }

    /**
     * Get items to return in API call
     *
     * This method is used to override the parent get_items method so we can format,
     * add custom fields, and return the data the way we want.
     *
     * @param \WP_REST_Request $request
     *
     * @return \WP_Error|\WP_REST_Response
     */
    public function get_items( $request ) {

      /**
       * Use parent get_items to pull all post data and retain support
       * for all arguments
       */
      $posts = parent::get_items( $request );

      /**
       * As long as data is returned, we can now loop through it and customize the output,
       * add custom fields, etc.
       */
      if ( $posts instanceof WP_REST_Response && isset( $posts->data ) && ! empty( $posts->data ) ) {

        $new_post_data = [];

        /**
         * Loop through all posts, creating a new array of data with only the information we
         * want to return.
         */
        foreach( $posts->data as $post_index => $post_data ) {
          
          $new_post = [
            'id'    => $this->extract_post_data( 'id', $post_data ),
            'title' => $this->extract_post_data( 'title', $post_data ),
            'link'  => $this->extract_post_data( 'link', $post_data ),
            'slug'  => $this->extract_post_data( 'slug', $post_data ),
          ];

          if ( ! empty( $post_data['first_hidden_field'] ) ) {
            $new_post['first_hidden_field'] = $post_data['first_hidden_field'];
            $new_post['second_hidden_field'] = $post_data['second_hidden_field'];
          }


          $new_post_data[ $post_index ] = $new_post;

          /**
           * Support for adding custom field values from ACF
           */
          if ( function_exists( 'get_fields' ) ) {

            /**
             * Get all ACF custom fields and add to post data
             *
             * @see https://www.advancedcustomfields.com/resources/get_fields/
             */
            $custom_fields = get_fields( $post_data[ 'id' ] );

            // As long as there are custom fields, loop through each adding it to the new data to return
            if ( ! empty( $custom_fields ) ) {
              foreach( $custom_fields as $custom_field_key => $custom_field_value ) {
                
                if($custom_field_key != 'first_hidden_field' && $custom_field_key != 'second_hidden_field'){
                  $new_post_data[ $post_index ][ $custom_field_key ] = $custom_field_value;
                }
                // $new_post_data[ $post_index ][ $custom_field_key ] = $custom_field_value;
              }
            }

          }

          


        }

        /**
         * Redeclare data with our new formatted data, to return via API
         */
        $posts->data = $new_post_data;
      }

      return $posts;
    }

    /**
     * Password fun
     */
    public function prepare_item_for_response( $post, $request ) {
      $response = parent::prepare_item_for_response( $post, $request );
      $data = $response->get_data();
      $custom_fields = get_fields( $post );
      $protected_fields = [ 'first_hidden_field', 'second_hidden_field' ];
      // loop through all custom fields
      foreach ( $custom_fields as $key => $value ) {
        // check if the field is protected
        if ( in_array( $key, $protected_fields ) ) {
          // check if post is password protected
          if ( post_password_required( $post ) ) {
            // check if password is correct
            if ( ! self::can_access_password_content( $post, $request ) ) {
              // password incorrect -> set the field to "protected"
              $data[ $key ] = '';
              continue;
            }
          }
        }
        // return the field if:
        //   - field is not protected
        //   - post password is not required
        //   - post password is correct
        $data[ $key ] = $value;
      }
      $response->set_data( $data );
      return $response;
    }



    /**
     * Return value for specific key from passed data
     *
     * This method is used to return a value (if found in post data), or return
     * an empty string if not found.
     *
     * @param $key
     * @param $data
     *
     * @return mixed|string
     */
    public function extract_post_data( $key, $data ) {

      // If passed data is not an array, it's empty, or the requested key does not exist, return empty string
      if ( ! is_array( $data ) || empty( $data ) || ! isset( $data[ $key ] ) ) return '';

      // If value is not an array, go ahead and return the value
      if ( ! is_array( $data[ $key ] ) ) {
        return $data[ $key ];
      } elseif ( array_key_exists( 'rendered', $data[ $key ] ) ) {
        // If value is an array, chances are the value is under the 'rendered' key, if so return that value
        return $data[ $key ][ 'rendered' ];
      }

      // All else fails, return empty string.
      return '';
    }
  }
  
  /**
   * Since we're extending the existing post API class, we need to initialize our
   * class on the rest_api_init action
   */
  add_action( 'rest_api_init', 'init_your_dope_thing');

  /**
   * Custom function to initialize our extending class. Used a function to make sure
   * code is compatible with older versions of PHP (instead of using anonymous function)
   */
  function init_your_dope_thing(){
    $something_fancy = new WP_REST_YOURNAME_Posts_Controller();
    $something_fancy->register_routes();



  }
}