      1: <?php
      2:
      3: /**
      4:  * A RESTian_Request represents a specific HTTP request
      5:  */
      6: class RESTian_Request {
      7:   /**
      8:    * @var bool|array
      9:    */
     10:   protected $_credentials = false;
     11:   /**
     12:    * @var bool|array
     13:    */
     14:   protected $_grant = false;
     15:   /**
     16:    * @var array
     17:    */
     18:   protected $_headers = array();
     19:   /**
     20:    * @var RESTian_Service Optional to override the one set in the API, if needed.
     21:    */
     22:   protected $_auth_service = false;
     23:   /**
     24:    * @var RESTian_Client
     25:    */
     26:   var $client;
     27:   /**
     28:    * @var RESTian_Service
     29:    */
     30:   var $service;
     31:   /**
     32:    * @var string
     33:    */
     34:   var $http_method = 'GET';
     35:   /**
     36:    * @var bool Specifies that SSL should not be verified by default.
     37:    * When true it is often too problematic for WordPress plugins.
     38:    */
     39:   var $sslverify = false;
     40:   /**
     41:    * @var RESTian_Response
     42:    */
     43:   var $response;
     44:   /**
     45:    * @var array
     46:    */
     47:   var $vars;
     48:   /**
     49:    * @var string
     50:    */
     51:   var $body;
     52:
     53:   /**
     54:    * @TODO These should be moved to RESPONSE, not be in REQUEST.
     55:    */
     56:   var $omit_body = false;
     57:   var $omit_result = false;
     58:
     59:   /**
     60:    * @param array $vars
     61:    * @param array $args
     62:    */
     63:   function __construct( $vars = array(), $args = array() ) {
     64:
     65:     if ( is_null( $vars ) )
     66:       $vars = array();
     67:
     68:     if ( is_null( $args ) )
     69:       $args = array();
     70:     else if ( is_string( $args ) )
     71:       $args = RESTian::parse_args( $args );
     72:
     73:     /**
     74:      * Copy properties in from $args, if they exist.
     75:      */
     76:     foreach( $args as $property => $value )
     77:       if ( property_exists(  $this, $property ) )
     78:         $this->$property = $value;
     79:
     80:     /**
     81:      * Do these late $args cannot override them.
     82:      */
     83:     if ( isset( $this->service->client ) ) {
     84:       $this->client = $this->service->client;
     85:     }
     86:
     87:     if ( isset( $args['credentials'] ) ) {
     88:       $this->set_credentials( $args['credentials'] );
     89:     }
     90:
     91:     if ( isset( $args['grant'] ) ) {
     92:       $this->set_grant( $args['grant'] );
     93:     }
     94:
     95:     if ( isset( $args['headers'] ) ) {
     96:       $this->add_headers( $args['headers'] );
     97:     }
     98:
     99:     if ( is_array( $vars ) ) {
    100:       $this->vars = $vars;
    101:     } else {
    102:       $this->body = $vars;
    103:     }
    104:
    105:   }
    106:
    107:   /**
    108:    * Check $this->auth to determine if it contains credentials.
    109:    *
    110:    * Does NOT verify if credentials are valid, only that it has them.
    111:    *
    112:    * This class will be extended when we have a proper use-case for extension.
    113:    *
    114:    * @return bool
    115:    */
    116:   function has_credentials() {
    117:     $auth_provider = $this->client->get_auth_provider();
    118:     return $auth_provider->is_credentials( $this->_credentials );
    119:   }
    120:
    121:   /**
    122:    * @return bool|array
    123:    */
    124:   function get_credentials() {
    125:     return $this->_credentials;
    126:   }
    127:
    128:   /**
    129:    * @param $credentials
    130:    *
    131:    * @return mixed
    132:    */
    133:   function set_credentials( $credentials ) {
    134:     $this->_credentials = $credentials;
    135:   }
    136:
    137:   /**
    138:    * @return bool|array
    139:    */
    140:   function get_grant() {
    141:     return $this->_grant;
    142:   }
    143:
    144:   /**
    145:    * @param $grant
    146:    *
    147:    * @return mixed
    148:    */
    149:   function set_grant( $grant ) {
    150:     $this->_grant = $grant;
    151:   }
    152:
    153:   /**
    154:    * @return bool|RESTian_Service
    155:    */
    156:   function get_auth_service() {
    157:     $this->client->initialize_client();
    158:     if ( ! $this->_auth_service ) {
    159:       $this->_auth_service = $this->client->get_auth_service();
    160:     }
    161:     return $this->_auth_service;
    162:   }
    163:   /**
    164:    * @param $name
    165:    * @param $value
    166:    */
    167:   function add_header( $name, $value ) {
    168:     $this->_headers[$name] = $value;
    169:   }
    170:
    171:   /**
    172:    * @param array $headers
    173:    */
    174:   function add_headers( $headers = array() ) {
    175:     $this->_headers = array_merge( $this->_headers, $headers );
    176:   }
    177:
    178:   /**
    179:    * @return array
    180:    */
    181:   function get_headers() {
    182:     return $this->_headers;
    183:   }
    184:
    185:   /**
    186:    *
    187:    */
    188:   function clear_headers() {
    189:     $this->_headers = array();
    190:   }
    191:
    192:   /**
    193:    * @return bool|string
    194:    */
    195:   function get_body() {
    196:     $body = false;
    197:     if ( preg_match( '#^(POST|PUT)$#i', $this->http_method ) ) {
    198:       if ( $this->body ) {
    199:         $body = $this->body;
    200:       } else if ( $settings = $this->service->get_request_settings() ) {
    201:         if ( RESTian::expand_content_type( 'form' ) == $settings->content_type ) {
    202:           $body = http_build_query( $this->vars );
    203:         } else if ( count( $this->vars ) && RESTian::expand_content_type( 'json' ) == $settings->content_type ) {
    204:           $body = json_encode( (object)$this->vars );
    205:         }
    206:       } else if ( count( $this->vars ) ) {
    207:         $body = http_build_query( $this->vars );
    208:         $this->vars = null;
    209:       }
    210:     }
    211:     return $body;
    212:   }
    213:
    214:   /**
    215:    * Returns HTTP headers as expected by CURL.
    216:    *
    217:    * Returns numeric indexed array where value contains header name and header value as "{$name}: {$value}"
    218:    *
    219:    * @return array
    220:    */
    221:   function get_curl_headers() {
    222:     $headers = array();
    223:     foreach( $this->_headers as $name => $value )
    224:       $headers[] = "{$name}: {$value}";
    225:     return $headers;
    226:   }
    227:
    228:   /**
    229:    * @return bool|string
    230:    * @throws Exception
    231:    */
    232:   function get_url() {
    233:     $service_url = $this->client->get_service_url( $this->service );
    234:     if ( count( $this->vars ) ) {
    235:       $query_vars = $this->vars;
    236:       foreach( $query_vars as $name => $value ) {
    237:         /**
    238:          * @var array $matches Get all URL path var matches into an array
    239:          */
    240:         preg_match_all( '#([^{]+)\{([^}]+)\}#', $this->service->path, $matches );
    241:         $path_vars = array_flip( $matches[2] );
    242:         if ( isset( $path_vars[$name] ) ) {
    243:           $var = $this->client->get_var( $name );
    244:           $value = $var->apply_transforms( $value );
    245:           $service_url = str_replace( "{{$name}}", $value, $service_url );
    246:           unset( $query_vars[$name] );
    247:         }
    248:       }
    249:       $service_url .= '?' . http_build_query( $query_vars );
    250:     }
    251:     return $service_url;
    252:   }
    253:
    254:   /**
    255:    * @param $vars
    256:    * @param $template
    257:    *
    258:    * @return bool|string
    259:    */
    260:   private function _to_application_form_url_encoded( $vars, $template = '' ) {
    261:       $query_vars = $this->vars;
    262:       foreach( $query_vars as $name => $value ) {
    263:         /**
    264:          * @var array $matches Get all URL path var matches into an array
    265:          */
    266:         preg_match_all( '#([^{]+)\{([^}]+)\}#', $this->service->path, $matches );
    267:         $path_vars = array_flip( $matches[2] );
    268:         if ( isset( $path_vars[$name] ) ) {
    269:           $var = $this->client->get_var( $name );
    270:           $value = $var->apply_transforms( $value );
    271:           $template = str_replace( "{{$name}}", $value, $template );
    272:           unset( $query_vars[$name] );
    273:         }
    274:       }
    275:     $result = http_build_query( $query_vars );
    276:     return $result;
    277:   }
    278:   /**
    279:    * Returns true if RESTian can safely assume that we have authenticated in past with existing credentials.
    280:    *
    281:    * This does NOT mean we ARE authenticated but that we should ASSUME we are and try doing calls without
    282:    * first authenticating. This functionality is defined because the client (often a WordPress plugin) may
    283:    * have captured grant info from a prior page load where this class did authenticate, but this class is not
    284:    * in control of maintaining that grant info so we can only assume it is correct if the client of this code
    285:    * tells us it is by giving us grant info that the auth provider validates. Another use-case where our assumption
    286:    * will fail is if the access_key expires or has since been revoked.
    287:    *
    288:    * @return bool
    289:    */
    290:   function has_grant() {
    291:     return $this->client->get_auth_provider()->is_grant( $this->_grant );
    292:   }
    293:   /**
    294:    * Call the API.
    295:    *
    296:    * On success (HTTP status == 200) $this->error_code and $this->error_msg will be false.
    297:    * On failure (HTTP status != 200) $this->error_code and $this->error_msg will contain error information.
    298:    *
    299:    * On success or failure,  $this->response will contain the response captured by HTTP agent
    300:    * except when username and password are not passed as part of auth.
    301:    *
    302:    * @return object|RESTian_Response
    303:    */
    304:   function make_request() {
    305:     $response = new RESTian_Response( array( 'request' => $this ) );
    306:     $api = $this->client;
    307:     /**
    308:      * Assign request & response to API so they are accessible inside the auth_provider
    309:      */
    310:     $api->request = $this;
    311:     $api->response = $response;
    312:     $auth_provider = $api->get_auth_provider();
    313:     if ( $this->needs_authentication() && ! $this->has_authentication() ) {
    314:       $response->set_error( 'NO_AUTH', $this->service );
    315:     } else {
    316:       $http_agent = RESTian::get_new_http_agent( $this->client->http_agent );
    317:       $this->assign_settings();
    318:       $response = $http_agent->make_request( $this, $response );
    319:       if ( $response->is_http_error() ) {
    320:         /**
    321:          * See if we can provide more than one error type here.
    322:          */
    323:         $msg = 'There was a problem reaching %s when calling the %s. Please try again later or contact the site\'s administrator.';
    324:         $response->set_error( 'API_FAIL', sprintf( $msg, $this->client->api_name, $this->service->service_name ) );
    325:       } else {
    326:         if ( 'authenticate' == $response->request->service->service_name ) {
    327:           $handled = $auth_provider->authenticated( $response );
    328:         } else {
    329:           $handled = $auth_provider->handle_response( $response );
    330:         }
    331:         if ( ! $handled ) {
    332:           // @todo Add more HTTP status code responses as we better understand the use-cases.
    333:           switch ( $response->status_code ) {
    334:             case '200':
    335:               /**
    336:                * @var RESTian_Parser_Base $parser
    337:                */
    338:               $parser = RESTian::get_new_parser( $this->service->content_type, $this, $response );
    339:               if ( $parser instanceof RESTian_Parser_Base )
    340:                 $response->data = $parser->parse( $response->body );
    341:               break;
    342:             case '401':
    343:               $response->set_error( 'BAD_AUTH', $this->service );
    344:               break;
    345:             default:
    346:               /**
    347:                * See if we can provide more than one error type here.
    348:                */
    349:               $response->set_error( 'UNKNOWN', 'Unexpected API response code: ' . $response->status_code );
    350:               break;
    351:           }
    352:         }
    353:         if ( $this->omit_body )
    354:           $response->body = null;
    355:         if ( $this->omit_result )
    356:           $response->result = null;
    357:       }
    358:     }
    359:     return $response;
    360:   }
    361:
    362:   /**
    363:    * @return bool
    364:    */
    365:   function needs_authentication() {
    366:     return $this->service != $this->get_auth_service() && $this->service->needs_authentication;
    367:   }
    368:
    369:   /**
    370:    * @return bool
    371:    */
    372:   function has_authentication() {
    373:     $auth_provider = $this->client->get_auth_provider();
    374:     return $auth_provider ? $auth_provider->is_grant( $this->get_grant() ) : false;
    375:   }
    376:
    377:   /**
    378:    * @return bool|RESTian_Settings
    379:    */
    380:   function get_settings() {
    381:     return $this->service->get_request_settings();
    382:   }
    383:   /**
    384:     * @return bool
    385:     */
    386:   function get_content_type() {
    387:     $content_type = false;
    388:     if ( $settings = $this->get_settings() ) {
    389:       if ( $settings->content_type )
    390:         $content_type = $settings->content_type;
    391:       if ( $content_type && $settings->charset )
    392:         $content_type .= "; charset={$settings->charset}";
    393:     }
    394:     return $content_type;
    395:   }
    396:
    397:   function assign_settings() {
    398:     if ( $settings = $this->service->get_request_settings() ) {
    399:
    400:       if ( $settings->http_method )
    401:         $this->http_method = $settings->http_method;
    402:
    403:     }
    404:
    405:   }
    406:
    407: }
    408:
    409: