Skip to content

Instantly share code, notes, and snippets.

@DaveyJake
Created October 10, 2024 09:17
Show Gist options
  • Save DaveyJake/f94944ce67920b058b713a3d71a916a2 to your computer and use it in GitHub Desktop.
Save DaveyJake/f94944ce67920b058b713a3d71a916a2 to your computer and use it in GitHub Desktop.

Revisions

  1. DaveyJake created this gist Oct 10, 2024.
    515 changes: 515 additions & 0 deletions class-search-rets.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,515 @@
    <?php
    /**
    * RETS API: Properties
    *
    * Make a request to retrieve any/all specified properties.
    *
    * @package Search
    * @subpackage RETS
    */

    (defined( 'ABSPATH' ) && defined( 'RETS_API_URL' ) && defined( 'RETS_AUTH_KEY' )) || exit;

    // Nonce support.
    require ABSPATH . 'wp-includes/pluggable.php';

    /**
    * Initialize the API class.
    *
    * @since 1.0.0
    */
    class Search_RETS {
    /**
    * Property or pricing type. Default 'all'.
    *
    * @since 1.0.0
    *
    * @var string
    */
    public $pr_type = 'all';

    /**
    * Listing status. Default 'all'.
    *
    * @since 1.0.0
    *
    * @var string
    */
    public $listing_status = 'all';

    /**
    * RETS API default query parameters.
    *
    * @since 1.0.0
    *
    * @var array
    */
    public $query = array(
    'q' => '',
    'status' => 'all',
    'type' => 'all',
    'minbaths' => '',
    'minbeds' => '',
    'minprice' => '',
    'maxprice' => '',
    'minyear' => '',
    'maxyear' => '',
    'page' => '',
    );

    /**
    * RETS API URL.
    *
    * @since 1.0.0
    *
    * @var string
    */
    public $api_url = RETS_API_URL;

    /**
    * RETS API property statuses.
    *
    * @since 1.0.0
    *
    * @var array
    */
    public $property_statuses = array( 'Active', 'ActiveUnderContract' );

    /**
    * RETS API property types.
    *
    * @since 1.0.0
    *
    * @var array
    */
    public $property_types = array(
    'RES' => 'residential',
    'MLF' => 'multifamily',
    'MBL' => 'mobilehome',
    'CND' => 'condominium',
    'CRE' => 'commercial',
    'LND' => 'land',
    'FRM' => 'farm',
    );

    /**
    * Final query parameters.
    *
    * @since 1.0.0
    *
    * @var array
    */
    public $args = array();

    /**
    * Final endpoint URL.
    *
    * @since 1.0.0
    *
    * @var string
    */
    public $url;

    /**
    * Flag for property view type. True if single. Default false.
    *
    * @since 1.0.0
    *
    * @var bool
    */
    public $is_single = false;

    /**
    * MLS ID for viewing a single property only.
    *
    * @since 1.0.0
    *
    * @var string
    */
    public $mls_id;

    /**
    * Primary constructor.
    *
    * @since 1.0.0
    */
    public function __construct() {
    if ( isset( $_REQUEST['nonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'search-rets' ) ) {
    // Check for MLS ID.
    if ( isset( $_REQUEST['mls_id'] ) ) {
    $this->mls_id = sanitize_text_field( wp_unslash( $_REQUEST['mls_id'] ) );
    $this->is_single = true;
    }

    // If we're looking at multiple properties on a Google Map...
    if ( false === $this->is_single ) {
    // Property status.
    if ( isset( $_REQUEST['listing_status'] ) ) {
    $this->listing_status = sanitize_text_field( wp_unslash( $_REQUEST['listing_status'] ) );
    }

    // Property or pricing type.
    if ( isset( $_REQUEST['pr_type'] ) ) {
    $this->pr_type = sanitize_text_field( wp_unslash( $_REQUEST['pr_type'] ) );
    }

    // Default parameters.
    if ( 'all' === $this->listing_status ) {
    $this->args['status'] = implode( '&status=', $this->property_statuses );
    } else {
    $this->args['status'] = $this->listing_status;
    }

    if ( 'all' === $this->pr_type ) {
    $this->args['type'] = implode( '&type=', array_values( $this->property_types ) );
    } else {
    $this->args['type'] = $this->pr_type;
    }

    if ( isset( $_REQUEST['min_baths'] ) ) {
    $this->args['minbaths'] = sanitize_text_field( wp_unslash( $_REQUEST['min_baths'] ) );
    } else {
    $this->args['minbaths'] = '';
    }

    if ( isset( $_REQUEST['max_baths'] ) ) {
    $this->args['maxbaths'] = sanitize_text_field( wp_unslash( $_REQUEST['max_baths'] ) );
    } else {
    $this->args['maxbaths'] = '';
    }

    if ( isset( $_REQUEST['min_beds'] ) ) {
    $this->args['minbeds'] = sanitize_text_field( wp_unslash( $_REQUEST['min_beds'] ) );
    } else {
    $this->args['minbeds'] = '';
    }

    if ( isset( $_REQUEST['max_beds'] ) ) {
    $this->args['maxbeds'] = sanitize_text_field( wp_unslash( $_REQUEST['max_beds'] ) );
    } else {
    $this->args['maxbeds'] = '';
    }

    if ( isset( $_REQUEST['min_price'] ) ) {
    $this->args['minprice'] = sanitize_text_field( wp_unslash( $_REQUEST['min_price'] ) );
    } else {
    $this->args['minprice'] = '';
    }

    if ( isset( $_REQUEST['max_price'] ) ) {
    $this->args['maxprice'] = sanitize_text_field( wp_unslash( $_REQUEST['max_price'] ) );
    } else {
    $this->args['maxprice'] = '';
    }

    if ( isset( $_REQUEST['year_min'] ) ) {
    $this->args['minyear'] = sanitize_text_field( wp_unslash( $_REQUEST['year_min'] ) );
    } else {
    $this->args['minyear'] = '';
    }

    if ( isset( $_REQUEST['year_max'] ) ) {
    $this->args['maxyear'] = sanitize_text_field( wp_unslash( $_REQUEST['year_max'] ) );
    } else {
    $this->args['maxyear'] = '';
    }

    if ( isset( $_REQUEST['location'] ) ) {
    $this->args['q'] = sanitize_text_field( wp_unslash( $_REQUEST['location'] ) );
    } else {
    $this->args['q'] = '';
    }

    if ( isset( $_REQUEST['post_type'] ) ) {
    $this->args['post_type'] = sanitize_text_field( wp_unslash( $_REQUEST['post_type'] ) );
    } else {
    $this->args['post_type'] = '';
    }

    // Max results limit.
    $this->args['limit'] = '27';

    // Don't count.
    $this->args['count'] = 'false';

    // Pagination.
    if ( isset( $_REQUEST['page'] ) ) {
    $this->args['page'] = sanitize_text_field( wp_unslash( $_REQUEST['page'] ) );
    } else {
    $this->args['page'] = '1';
    }

    // Remove all empty, NULL and boolean false values.
    $this->args = array_filter( $this->args );

    // Final collection endpoint URL.
    $this->url = $this->parse_api_url( $this->api_url, $this->args );
    } else {
    // If we're looking at a single property on its own page...
    $this->url = sprintf( '%s/%s', $this->api_url, $this->mls_id );
    }//end if

    add_action( 'wp_ajax_search_rets', array( $this, 'rets_api_request' ) );
    add_action( 'wp_ajax_nopriv_search_rets', array( $this, 'rets_api_request' ) );
    }//end if
    }

    /**
    * Make the request and parse the response.
    *
    * @since 1.0.0
    */
    public function rets_api_request() {
    if ( defined( 'DOING_AJAX' ) && DOING_AJAX
    && isset( $_REQUEST['nonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'search-rets' )
    ) {
    $args = array();

    $transient_key = '';

    $parts = wp_parse_url( $this->url );

    if ( ! empty( $parts['query'] ) ) {
    $queries = preg_split( '/\&/', $parts['query'] );

    foreach ( $queries as $_query ) {
    $query = preg_split( '/=/', $_query );

    $args[ $query[0] ] = $query[1];
    }
    }

    if ( ! empty( $args ) ) {
    if ( isset( $args['q'] ) ) {
    $transient_key .= preg_replace( '/[^a-zA-Z0-9]/', '_', $args['q'] ) . '_';
    }

    if ( isset( $_REQUEST['page'] ) ) {
    $transient_key .= sanitize_text_field( wp_unslash( $_REQUEST['page'] ) );
    }
    } else {
    $transient_key = 'default';
    }

    $transient = sprintf( 'listings_%s', $transient_key );

    $result = get_transient( $transient );

    if ( false === $result ) {
    // Ensure nothing's cached.
    delete_transient( $transient );

    // Request headers.
    $auth = array(
    'headers' => array(
    'Authorization' => 'Basic ' . RETS_AUTH_KEY,
    'Accept' => 'application/vnd.simplyrets-v0.1+json',
    ),
    );

    $request = wp_remote_get( $this->url, $auth );

    if ( empty( $request ) || is_wp_error( $request ) ) {
    if ( empty( $request ) ) {
    $error_data = array( 'message' => wp_remote_retrieve_response_message( $request ) );
    } elseif ( is_wp_error( $request ) ) {
    $error_data = array( 'message' => $request->get_error_message() );
    }

    wp_send_json_error( $error_data );
    wp_die();
    }

    $response = wp_remote_retrieve_body( $request );

    if ( empty( $response ) ) {
    wp_send_json_error( array( 'message' => wp_remote_retrieve_response_message( $response ) ) );
    wp_die();
    } elseif ( is_wp_error( $response ) ) {
    wp_send_json_error( array( 'message' => $response->get_error_message() ) );
    wp_die();
    } else {
    set_transient( $transient, $response, 2 * DAY_IN_SECONDS );

    $api = $this->format_api_response( $response );

    wp_send_json_success( $api );
    wp_die();
    }
    } elseif ( empty( $result ) ) {
    delete_transient( $transient );
    wp_send_json_error( array( 'message' => 'Response was good but contained no data.' ) );
    wp_die();
    } else {
    $api = $this->format_api_response( $result );

    wp_send_json_success( $api );
    wp_die();
    }//end if
    }//end if

    wp_die();
    }

    /**
    * Format the raw API response to suit our needs and update the `properties`
    * database table.
    *
    * @since 1.0.0
    *
    * @param string $response Raw JSON data.
    *
    * @return object Custom, formatted API response.
    */
    private function format_api_response( $response ) {
    $data = json_decode( $response );

    $api = array();
    $slug2id = array();
    $already_parsed = array();

    foreach ( $data as $i => $d ) {
    if ( ! is_object( $d ) ) {
    continue;
    }

    if ( ! in_array( $d->property->type, array_keys( $this->property_types ), true ) ) {
    continue;
    }

    // Ensure there are no duplicate entries by checking the `$already_parsed` array.
    if ( empty( $d->mlsId ) || in_array( $d->mlsId, $already_parsed, true ) ) {
    continue;
    }

    if ( $d->listPrice < 1 ) {
    continue;
    }

    $mls_id = $d->mlsId;

    if ( ! empty( $d->geo ) ) {
    $geo = array(
    'lat' => isset( $d->geo->lat ) ? $d->geo->lat : '',
    'lng' => isset( $d->geo->lng ) ? $d->geo->lng : '',
    );
    }

    $title = preg_replace( array_keys( $this->keywords ), array_values( $this->keywords ), $d->address->full ) . ', ' . $d->address->city . ', ' . $d->address->state . ' ' . $d->address->postalCode;
    $slug = sanitize_title( $title );
    $index = 0;

    $address = preg_replace( array_keys( $this->keywords ), array_values( $this->keywords ), $d->address->full ) . ',<br />' . $d->address->city . ', ' . $d->address->state . ' ' . $d->address->postalCode;

    if ( ! empty( $d->photos ) ) {
    $total = count( $d->photos );

    if ( $total > 1 ) {
    $_index = wp_rand( 1, $total );
    $index = $_index - 1;
    }
    }

    $api[] = array(
    'mls_id' => (string) $mls_id,
    'listing_id' => $d->listingId,
    'slug' => $slug,
    'agent' => $d->agent,
    'title' => $title,
    'address' => $address,
    'baths' => ! empty( $d->property->bathrooms ) ? $d->property->bathrooms : '',
    'beds' => ! empty( $d->property->bedrooms ) ? $d->property->bedrooms : '',
    'city' => $d->address->city,
    'state' => $d->address->state,
    'zip' => $d->address->postalCode,
    'geo' => ! empty( $geo ) ? $geo : null,
    'move_in' => get_gmt_from_date( $d->listDate, 'U' ),
    'office' => ! empty( $d->office ) ? $d->office : '',
    'photos' => ! empty( $d->photos ) ? $d->photos : '',
    'featured' => isset( $d->photos[ $index ] ) ? $d->photos[ $index ] : '',
    'price_int' => absint( preg_replace( '/[^0-9]+/', '', $d->listPrice ) ),
    'price_usd' => number_format_i18n( $d->listPrice ),
    'remarks' => ! empty( $d->remarks ) ? $d->remarks : '',
    'sqft' => ! empty( $d->property->area ) ? $d->property->area : '',
    'status' => ! empty( $d->mls->status ) ? $d->mls->status : '',
    'type' => $this->property_types[ $d->property->type ],
    'type_text' => $d->property->subTypeText,
    );

    $slug2id[ $slug ] = $mls_id;
    $already_parsed[] = $mls_id;
    }//end foreach

    foreach ( $slug2id as $slug => $mls ) {
    update_property( $slug, $mls );
    }

    return $api;
    }

    /**
    * Build the URL.
    *
    * @since 1.0.0
    * @access private
    *
    * @see Search_RETS::parse_query_params()
    *
    * @param string $url The API URL.
    * @param array|int $params Filter input values.
    *
    * @return string|bool The API URL if successful. False if not.
    */
    private function parse_api_url( $url, $params ) {
    $params = $this->parse_query_params( $params );

    if ( is_int( $params ) ) {
    return sprintf( '%s/%d', $url, $params );
    } elseif ( ! empty( $params ) ) {
    return add_query_arg( $params, $url );
    } else {
    return $url;
    }
    }

    /**
    * Parse the API query parameters.
    *
    * @since 1.0.0
    * @access private
    *
    * @see Search_RETS::parse_url()
    *
    * @param array $params URL query parameters.
    *
    * @return int|array The parameter values.
    */
    private function parse_query_params( $params ) {
    if ( is_int( $params ) ) {
    return $params;
    }

    $final = array();

    // Begin parsing keyword search.
    foreach ( (array) $params as $param => $value ) {
    if ( 'q' === $param && ! empty( $value ) ) {
    if ( preg_match( '/\+/', $value ) ) {
    $parts = array_map( 'trim', explode( '+', $value ) );

    $final['q'] = implode( '&q=', $parts );
    } elseif ( is_string( $value ) ) {
    $final['q'] = $value;
    } elseif ( absint( $value ) > 0 ) {
    $final['q'] = absint( $value );
    }
    } elseif ( ( ! empty( $value ) || 'all' !== $value ) && 'post_type' !== $param && 'page' !== $param ) {
    $final[ $param ] = $value;
    }
    }

    return $final;
    }
    }

    $GLOBALS['search_rets'] = new Search_RETS();