/** * Meta API: WP_Meta_Query class * * @package WordPress * @subpackage Meta * @since 4.4.0 */ /** * Core class used to implement meta queries for the Meta API. * * Used for generating SQL clauses that filter a primary query according to metadata keys and values. * * WP_Meta_Query is a helper that allows primary query classes, such as WP_Query and WP_User_Query, * * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached * to the primary SQL query string. * * @since 3.2.0 * @package WordPress * @subpackage Meta */ class WP_Meta_Query { /** * Array of metadata queries. * * See WP_Meta_Query::__construct() for information on meta query arguments. * * @since 3.2.0 * @access public * @var array */ public $queries = array(); /** * The relation between the queries. Can be one of 'AND' or 'OR'. * * @since 3.2.0 * @access public * @var string */ public $relation; /** * Database table to query for the metadata. * * @since 4.1.0 * @access public * @var string */ public $meta_table; /** * Column in meta_table that represents the ID of the object the metadata belongs to. * * @since 4.1.0 * @access public * @var string */ public $meta_id_column; /** * Database table that where the metadata's objects are stored (eg $wpdb->users). * * @since 4.1.0 * @access public * @var string */ public $primary_table; /** * Column in primary_table that represents the ID of the object. * * @since 4.1.0 * @access public * @var string */ public $primary_id_column; /** * A flat list of table aliases used in JOIN clauses. * * @since 4.1.0 * @access protected * @var array */ protected $table_aliases = array(); /** * A flat list of clauses, keyed by clause 'name'. * * @since 4.2.0 * @access protected * @var array */ protected $clauses = array(); /** * Whether the query contains any OR relations. * * @since 4.3.0 * @access protected * @var bool */ protected $has_or_relation = false; /** * Constructor. * * @since 3.2.0 * @since 4.2.0 Introduced support for naming query clauses by associative array keys. * * @access public * * @param array $meta_query { * Array of meta query clauses. When first-order clauses or sub-clauses use strings as * their array keys, they may be referenced in the 'orderby' parameter of the parent query. * * @type string $relation Optional. The MySQL keyword used to join * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'. * @type array { * Optional. An array of first-order clause parameters, or another fully-formed meta query. * * @type string $key Meta key to filter by. * @type string $value Meta value to filter by. * @type string $compare MySQL operator used for comparing the $value. Accepts '=', * '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP', * 'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'. * Default is 'IN' when `$value` is an array, '=' otherwise. * @type string $type MySQL data type that the meta_value column will be CAST to for * comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', * 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'. * Default is 'CHAR'. * } * } */ public function __construct( $meta_query = false ) { if ( !$meta_query ) return; if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) { $this->relation = 'OR'; } else { $this->relation = 'AND'; } $this->queries = $this->sanitize_query( $meta_query ); } /** * Ensure the 'meta_query' argument passed to the class constructor is well-formed. * * Eliminates empty items and ensures that a 'relation' is set. * * @since 4.1.0 * @access public * * @param array $queries Array of query clauses. * @return array Sanitized array of query clauses. */ public function sanitize_query( $queries ) { $clean_queries = array(); if ( ! is_array( $queries ) ) { return $clean_queries; } foreach ( $queries as $key => $query ) { if ( 'relation' === $key ) { $relation = $query; } elseif ( ! is_array( $query ) ) { continue; // First-order clause. } elseif ( $this->is_first_order_clause( $query ) ) { if ( isset( $query['value'] ) && array() === $query['value'] ) { unset( $query['value'] ); } $clean_queries[ $key ] = $query; // Otherwise, it's a nested query, so we recurse. } else { $cleaned_query = $this->sanitize_query( $query ); if ( ! empty( $cleaned_query ) ) { $clean_queries[ $key ] = $cleaned_query; } } } if ( empty( $clean_queries ) ) { return $clean_queries; } // Sanitize the 'relation' key provided in the query. if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) { $clean_queries['relation'] = 'OR'; $this->has_or_relation = true; /* * If there is only a single clause, call the relation 'OR'. * This value will not actually be used to join clauses, but it * simplifies the logic around combining key-only queries. */ } elseif ( 1 === count( $clean_queries ) ) { $clean_queries['relation'] = 'OR'; // Default to AND. } else { $clean_queries['relation'] = 'AND'; } return $clean_queries; } /** * Determine whether a query clause is first-order. * * A first-order meta query clause is one that has either a 'key' or * a 'value' array key. * * @since 4.1.0 * @access protected * * @param array $query Meta query arguments. * @return bool Whether the query clause is a first-order clause. */ protected function is_first_order_clause( $query ) { return isset( $query['key'] ) || isset( $query['value'] ); } /** * Constructs a meta query based on 'meta_*' query vars * * @since 3.2.0 * @access public * * @param array $qv The query variables */ public function parse_query_vars( $qv ) { $meta_query = array(); /* * For orderby=meta_value to work correctly, simple query needs to be * first (so that its table join is against an unaliased meta table) and * needs to be its own clause (so it doesn't interfere with the logic of * the rest of the meta_query). */ $primary_meta_query = array(); foreach ( array( 'key', 'compare', 'type' ) as $key ) { if ( ! empty( $qv[ "meta_$key" ] ) ) { $primary_meta_query[ $key ] = $qv[ "meta_$key" ]; } } // WP_Query sets 'meta_value' = '' by default. if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) { $primary_meta_query['value'] = $qv['meta_value']; } $existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array(); if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) { $meta_query = array( 'relation' => 'AND', $primary_meta_query, $existing_meta_query, ); } elseif ( ! empty( $primary_meta_query ) ) { $meta_query = array( $primary_meta_query, ); } elseif ( ! empty( $existing_meta_query ) ) { $meta_query = $existing_meta_query; } $this->__construct( $meta_query ); } /** * Return the appropriate alias for the given meta type if applicable. * * @since 3.7.0 * @access public * * @param string $type MySQL type to cast meta_value. * @return string MySQL type. */ public function get_cast_for_type( $type = '' ) { if ( empty( $type ) ) return 'CHAR'; $meta_type = strtoupper( $type ); if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) return 'CHAR'; if ( 'NUMERIC' == $meta_type ) $meta_type = 'SIGNED'; return $meta_type; } /** * Generates SQL clauses to be appended to a main query. * * @since 3.2.0 * @access public * * @param string $type Type of meta, eg 'user', 'post'. * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). * @param string $primary_id_column ID column for the filtered object in $primary_table. * @param object $context Optional. The main query object. * @return false|array { * Array containing JOIN and WHERE SQL clauses to append to the main query. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) { if ( ! $meta_table = _get_meta_table( $type ) ) { return false; } $this->table_aliases = array(); $this->meta_table = $meta_table; $this->meta_id_column = sanitize_key( $type . '_id' ); $this->primary_table = $primary_table; $this->primary_id_column = $primary_id_column; $sql = $this->get_sql_clauses(); /* * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should * be LEFT. Otherwise posts with no metadata will be excluded from results. */ if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) { $sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] ); } /** * Filters the meta query's generated SQL. * * @since 3.1.0 * * @param array $clauses Array containing the query's JOIN and WHERE clauses. * @param array $queries Array of meta queries. * @param string $type Type of meta. * @param string $primary_table Primary table. * @param string $primary_id_column Primary column ID. * @param object $context The main query object. */ return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) ); } /** * Generate SQL clauses to be appended to a main query. * * Called by the public WP_Meta_Query::get_sql(), this method is abstracted * out to maintain parity with the other Query classes. * * @since 4.1.0 * @access protected * * @return array { * Array containing JOIN and WHERE SQL clauses to append to the main query. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_clauses() { /* * $queries are passed by reference to get_sql_for_query() for recursion. * To keep $this->queries unaltered, pass a copy. */ $queries = $this->queries; $sql = $this->get_sql_for_query( $queries ); if ( ! empty( $sql['where'] ) ) { $sql['where'] = ' AND ' . $sql['where']; } return $sql; } /** * Generate SQL clauses for a single query array. * * If nested subqueries are found, this method recurses the tree to * produce the properly nested SQL. * * @since 4.1.0 * @access protected * * @param array $query Query to parse, passed by reference. * @param int $depth Optional. Number of tree levels deep we currently are. * Used to calculate indentation. Default 0. * @return array { * Array containing JOIN and WHERE SQL clauses to append to a single query array. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_for_query( &$query, $depth = 0 ) { $sql_chunks = array( 'join' => array(), 'where' => array(), ); $sql = array( 'join' => '', 'where' => '', ); $indent = ''; for ( $i = 0; $i < $depth; $i++ ) { $indent .= " "; } foreach ( $query as $key => &$clause ) { if ( 'relation' === $key ) { $relation = $query['relation']; } elseif ( is_array( $clause ) ) { // This is a first-order clause. if ( $this->is_first_order_clause( $clause ) ) { $clause_sql = $this->get_sql_for_clause( $clause, $query, $key ); $where_count = count( $clause_sql['where'] ); if ( ! $where_count ) { $sql_chunks['where'][] = ''; } elseif ( 1 === $where_count ) { $sql_chunks['where'][] = $clause_sql['where'][0]; } else { $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; } $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); // This is a subquery, so we recurse. } else { $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); $sql_chunks['where'][] = $clause_sql['where']; $sql_chunks['join'][] = $clause_sql['join']; } } } // Filter to remove empties. $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); if ( empty( $relation ) ) { $relation = 'AND'; } // Filter duplicate JOIN clauses and combine into a single string. if ( ! empty( $sql_chunks['join'] ) ) { $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); } // Generate a single WHERE clause with proper brackets and indentation. if ( ! empty( $sql_chunks['where'] ) ) { $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; } return $sql; } /** * Generate SQL JOIN and WHERE clauses for a first-order query clause. * * "First-order" means that it's an array with a 'key' or 'value'. * * @since 4.1.0 * @access public * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $clause Query clause, passed by reference. * @param array $parent_query Parent query array. * @param string $clause_key Optional. The array key used to name the clause in the original `$meta_query` * parameters. If not provided, a key will be generated automatically. * @return array { * Array containing JOIN and WHERE SQL clauses to append to a first-order query. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) { global $wpdb; $sql_chunks = array( 'where' => array(), 'join' => array(), ); if ( isset( $clause['compare'] ) ) { $clause['compare'] = strtoupper( $clause['compare'] ); } else { $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '='; } if ( ! in_array( $clause['compare'], array( '=', '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'EXISTS', 'NOT EXISTS', 'REGEXP', 'NOT REGEXP', 'RLIKE' ) ) ) { $clause['compare'] = '='; } $meta_compare = $clause['compare']; // First build the JOIN clause, if one is required. $join = ''; // We prefer to avoid joins if possible. Look for an existing join compatible with this clause. $alias = $this->find_compatible_table_alias( $clause, $parent_query ); if ( false === $alias ) { $i = count( $this->table_aliases ); $alias = $i ? 'mt' . $i : $this->meta_table; // JOIN clauses for NOT EXISTS have their own syntax. if ( 'NOT EXISTS' === $meta_compare ) { $join .= " LEFT JOIN $this->meta_table"; $join .= $i ? " AS $alias" : ''; $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] ); // All other JOIN clauses. } else { $join .= " INNER JOIN $this->meta_table"; $join .= $i ? " AS $alias" : ''; $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )"; } $this->table_aliases[] = $alias; $sql_chunks['join'][] = $join; } // Save the alias to this clause, for future siblings to find. $clause['alias'] = $alias; // Determine the data type. $_meta_type = isset( $clause['type'] ) ? $clause['type'] : ''; $meta_type = $this->get_cast_for_type( $_meta_type ); $clause['cast'] = $meta_type; // Fallback for clause keys is the table alias. Key must be a string. if ( is_int( $clause_key ) || ! $clause_key ) { $clause_key = $clause['alias']; } // Ensure unique clause keys, so none are overwritten. $iterator = 1; $clause_key_base = $clause_key; while ( isset( $this->clauses[ $clause_key ] ) ) { $clause_key = $clause_key_base . '-' . $iterator; $iterator++; } // Store the clause in our flat array. $this->clauses[ $clause_key ] =& $clause; // Next, build the WHERE clause. // meta_key. if ( array_key_exists( 'key', $clause ) ) { if ( 'NOT EXISTS' === $meta_compare ) { $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL'; } else { $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); } } // meta_value. if ( array_key_exists( 'value', $clause ) ) { $meta_value = $clause['value']; if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) { if ( ! is_array( $meta_value ) ) { $meta_value = preg_split( '/[,\s]+/', $meta_value ); } } else { $meta_value = trim( $meta_value ); } switch ( $meta_compare ) { case 'IN' : case 'NOT IN' : $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')'; $where = $wpdb->prepare( $meta_compare_string, $meta_value ); break; case 'BETWEEN' : case 'NOT BETWEEN' : $meta_value = array_slice( $meta_value, 0, 2 ); $where = $wpdb->prepare( '%s AND %s', $meta_value ); break; case 'LIKE' : case 'NOT LIKE' : $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%'; $where = $wpdb->prepare( '%s', $meta_value ); break; // EXISTS with a value is interpreted as '='. case 'EXISTS' : $meta_compare = '='; $where = $wpdb->prepare( '%s', $meta_value ); break; // 'value' is ignored for NOT EXISTS. case 'NOT EXISTS' : $where = ''; break; default : $where = $wpdb->prepare( '%s', $meta_value ); break; } if ( $where ) { if ( 'CHAR' === $meta_type ) { $sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}"; } else { $sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}"; } } } /* * Multiple WHERE clauses (for meta_key and meta_value) should * be joined in parentheses. */ if ( 1 < count( $sql_chunks['where'] ) ) { $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' ); } return $sql_chunks; } /** * Get a flattened list of sanitized meta clauses. * * This array should be used for clause lookup, as when the table alias and CAST type must be determined for * a value of 'orderby' corresponding to a meta clause. * * @since 4.2.0 * @access public * * @return array Meta clauses. */ public function get_clauses() { return $this->clauses; } /** * Identify an existing table alias that is compatible with the current * query clause. * * We avoid unnecessary table joins by allowing each clause to look for * an existing table alias that is compatible with the query that it * needs to perform. * * An existing alias is compatible if (a) it is a sibling of `$clause` * (ie, it's under the scope of the same relation), and (b) the combination * of operator and relation between the clauses allows for a shared table join. * In the case of WP_Meta_Query, this only applies to 'IN' clauses that are * connected by the relation 'OR'. * * @since 4.1.0 * @access protected * * @param array $clause Query clause. * @param array $parent_query Parent query of $clause. * @return string|bool Table alias if found, otherwise false. */ protected function find_compatible_table_alias( $clause, $parent_query ) { $alias = false; foreach ( $parent_query as $sibling ) { // If the sibling has no alias yet, there's nothing to check. if ( empty( $sibling['alias'] ) ) { continue; } // We're only interested in siblings that are first-order clauses. if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) { continue; } $compatible_compares = array(); // Clauses connected by OR can share joins as long as they have "positive" operators. if ( 'OR' === $parent_query['relation'] ) { $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' ); // Clauses joined by AND with "negative" operators share a join only if they also share a key. } elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) { $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' ); } $clause_compare = strtoupper( $clause['compare'] ); $sibling_compare = strtoupper( $sibling['compare'] ); if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) { $alias = $sibling['alias']; break; } } /** * Filters the table alias identified as compatible with the current clause. * * @since 4.1.0 * * @param string|bool $alias Table alias, or false if none was found. * @param array $clause First-order query clause. * @param array $parent_query Parent of $clause. * @param object $this WP_Meta_Query object. */ return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ) ; } /** * Checks whether the current query has any OR relations. * * In some cases, the presence of an OR relation somewhere in the query will require * the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current * method can be used in these cases to determine whether such a clause is necessary. * * @since 4.3.0 * * @return bool True if the query contains any `OR` relations, otherwise false. */ public function has_or_relation() { return $this->has_or_relation; } } /** * Requests for PHP * * Inspired by Requests for Python. * * Based on concepts from SimplePie_File, RequestCore and WP_Http. * * @package Requests */ /** * Requests for PHP * * Inspired by Requests for Python. * * Based on concepts from SimplePie_File, RequestCore and WP_Http. * * @package Requests */ class Requests { /** * POST method * * @var string */ const POST = 'POST'; /** * PUT method * * @var string */ const PUT = 'PUT'; /** * GET method * * @var string */ const GET = 'GET'; /** * HEAD method * * @var string */ const HEAD = 'HEAD'; /** * DELETE method * * @var string */ const DELETE = 'DELETE'; /** * OPTIONS method * * @var string */ const OPTIONS = 'OPTIONS'; /** * TRACE method * * @var string */ const TRACE = 'TRACE'; /** * PATCH method * * @link https://tools.ietf.org/html/rfc5789 * @var string */ const PATCH = 'PATCH'; /** * Default size of buffer size to read streams * * @var integer */ const BUFFER_SIZE = 1160; /** * Current version of Requests * * @var string */ const VERSION = '1.7'; /** * Registered transport classes * * @var array */ protected static $transports = array(); /** * Selected transport name * * Use {@see get_transport()} instead * * @var array */ public static $transport = array(); /** * Default certificate path. * * @see Requests::get_certificate_path() * @see Requests::set_certificate_path() * * @var string */ protected static $certificate_path; /** * This is a static class, do not instantiate it * * @codeCoverageIgnore */ private function __construct() {} /** * Autoloader for Requests * * Register this with {@see register_autoloader()} if you'd like to avoid * having to create your own. * * (You can also use `spl_autoload_register` directly if you'd prefer.) * * @codeCoverageIgnore * * @param string $class Class name to load */ public static function autoloader($class) { // Check that the class starts with "Requests" if (strpos($class, 'Requests') !== 0) { return; } $file = str_replace('_', '/', $class); if (file_exists(dirname(__FILE__) . '/' . $file . '.php')) { require_once(dirname(__FILE__) . '/' . $file . '.php'); } } /** * Register the built-in autoloader * * @codeCoverageIgnore */ public static function register_autoloader() { spl_autoload_register(array('Requests', 'autoloader')); } /** * Register a transport * * @param string $transport Transport class to add, must support the Requests_Transport interface */ public static function add_transport($transport) { if (empty(self::$transports)) { self::$transports = array( 'Requests_Transport_cURL', 'Requests_Transport_fsockopen', ); } self::$transports = array_merge(self::$transports, array($transport)); } /** * Get a working transport * * @throws Requests_Exception If no valid transport is found (`notransport`) * @return Requests_Transport */ protected static function get_transport($capabilities = array()) { // Caching code, don't bother testing coverage // @codeCoverageIgnoreStart // array of capabilities as a string to be used as an array key ksort($capabilities); $cap_string = serialize($capabilities); // Don't search for a transport if it's already been done for these $capabilities if (isset(self::$transport[$cap_string]) && self::$transport[$cap_string] !== null) { return new self::$transport[$cap_string](); } // @codeCoverageIgnoreEnd if (empty(self::$transports)) { self::$transports = array( 'Requests_Transport_cURL', 'Requests_Transport_fsockopen', ); } // Find us a working transport foreach (self::$transports as $class) { if (!class_exists($class)) { continue; } $result = call_user_func(array($class, 'test'), $capabilities); if ($result) { self::$transport[$cap_string] = $class; break; } } if (self::$transport[$cap_string] === null) { throw new Requests_Exception('No working transports found', 'notransport', self::$transports); } return new self::$transport[$cap_string](); } /**#@+ * @see request() * @param string $url * @param array $headers * @param array $options * @return Requests_Response */ /** * Send a GET request */ public static function get($url, $headers = array(), $options = array()) { return self::request($url, $headers, null, self::GET, $options); } /** * Send a HEAD request */ public static function head($url, $headers = array(), $options = array()) { return self::request($url, $headers, null, self::HEAD, $options); } /** * Send a DELETE request */ public static function delete($url, $headers = array(), $options = array()) { return self::request($url, $headers, null, self::DELETE, $options); } /** * Send a TRACE request */ public static function trace($url, $headers = array(), $options = array()) { return self::request($url, $headers, null, self::TRACE, $options); } /**#@-*/ /**#@+ * @see request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return Requests_Response */ /** * Send a POST request */ public static function post($url, $headers = array(), $data = array(), $options = array()) { return self::request($url, $headers, $data, self::POST, $options); } /** * Send a PUT request */ public static function put($url, $headers = array(), $data = array(), $options = array()) { return self::request($url, $headers, $data, self::PUT, $options); } /** * Send an OPTIONS request */ public static function options($url, $headers = array(), $data = array(), $options = array()) { return self::request($url, $headers, $data, self::OPTIONS, $options); } /** * Send a PATCH request * * Note: Unlike {@see post} and {@see put}, `$headers` is required, as the * specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public static function patch($url, $headers, $data = array(), $options = array()) { return self::request($url, $headers, $data, self::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * The `$options` parameter takes an associative array with the following * options: * * - `timeout`: How long should we wait for a response? * Note: for cURL, a minimum of 1 second applies, as DNS resolution * operates at second-resolution only. * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `connect_timeout`: How long should we wait while trying to connect? * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `useragent`: Useragent to send to the server * (string, default: php-requests/$version) * - `follow_redirects`: Should we follow 3xx redirects? * (boolean, default: true) * - `redirects`: How many times should we redirect before erroring? * (integer, default: 10) * - `blocking`: Should we block processing on this request? * (boolean, default: true) * - `filename`: File to stream the body to instead. * (string|boolean, default: false) * - `auth`: Authentication handler or array of user/password details to use * for Basic authentication * (Requests_Auth|array|boolean, default: false) * - `proxy`: Proxy details to use for proxy by-passing and authentication * (Requests_Proxy|array|string|boolean, default: false) * - `max_bytes`: Limit for the response body size. * (integer|boolean, default: false) * - `idn`: Enable IDN parsing * (boolean, default: true) * - `transport`: Custom transport. Either a class name, or a * transport object. Defaults to the first working transport from * {@see getTransport()} * (string|Requests_Transport, default: {@see getTransport()}) * - `hooks`: Hooks handler. * (Requests_Hooker, default: new Requests_Hooks()) * - `verify`: Should we verify SSL certificates? Allows passing in a custom * certificate file as a string. (Using true uses the system-wide root * certificate store instead, but this may have different behaviour * across transports.) * (string|boolean, default: library/Requests/Transport/cacert.pem) * - `verifyname`: Should we verify the common name in the SSL certificate? * (boolean: default, true) * - `data_format`: How should we send the `$data` parameter? * (string, one of 'query' or 'body', default: 'query' for * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) * * @throws Requests_Exception On invalid URLs (`nonhttp`) * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use Requests constants) * @param array $options Options for the request (see description for more information) * @return Requests_Response */ public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) { if (empty($options['type'])) { $options['type'] = $type; } $options = array_merge(self::get_default_options(), $options); self::set_defaults($url, $headers, $data, $type, $options); $options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options)); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $need_ssl = (0 === stripos($url, 'https://')); $capabilities = array('ssl' => $need_ssl); $transport = self::get_transport($capabilities); } $response = $transport->request($url, $headers, $data, $options); $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options)); return self::parse_response($response, $url, $headers, $data, $options); } /** * Send multiple HTTP requests simultaneously * * The `$requests` parameter takes an associative or indexed array of * request fields. The key of each request can be used to match up the * request with the returned data, or with the request passed into your * `multiple.request.complete` callback. * * The request fields value is an associative array with the following keys: * * - `url`: Request URL Same as the `$url` parameter to * {@see Requests::request} * (string, required) * - `headers`: Associative array of header fields. Same as the `$headers` * parameter to {@see Requests::request} * (array, default: `array()`) * - `data`: Associative array of data fields or a string. Same as the * `$data` parameter to {@see Requests::request} * (array|string, default: `array()`) * - `type`: HTTP request type (use Requests constants). Same as the `$type` * parameter to {@see Requests::request} * (string, default: `Requests::GET`) * - `cookies`: Associative array of cookie name to value, or cookie jar. * (array|Requests_Cookie_Jar) * * If the `$options` parameter is specified, individual requests will * inherit options from it. This can be used to use a single hooking system, * or set all the types to `Requests::POST`, for example. * * In addition, the `$options` parameter takes the following global options: * * - `complete`: A callback for when a request is complete. Takes two * parameters, a Requests_Response/Requests_Exception reference, and the * ID from the request array (Note: this can also be overridden on a * per-request basis, although that's a little silly) * (callback) * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see Requests::request}) * @return array Responses (either Requests_Response or a Requests_Exception object) */ public static function request_multiple($requests, $options = array()) { $options = array_merge(self::get_default_options(true), $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); if (!empty($options['complete'])) { $options['hooks']->register('multiple.request.complete', $options['complete']); } } foreach ($requests as $id => &$request) { if (!isset($request['headers'])) { $request['headers'] = array(); } if (!isset($request['data'])) { $request['data'] = array(); } if (!isset($request['type'])) { $request['type'] = self::GET; } if (!isset($request['options'])) { $request['options'] = $options; $request['options']['type'] = $request['type']; } else { if (empty($request['options']['type'])) { $request['options']['type'] = $request['type']; } $request['options'] = array_merge($options, $request['options']); } self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); if (!empty($request['options']['complete'])) { $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); } } } unset($request); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $transport = self::get_transport(); } $responses = $transport->request_multiple($requests, $options); foreach ($responses as $id => &$response) { // If our hook got messed with somehow, ensure we end up with the // correct response if (is_string($response)) { $request = $requests[$id]; self::parse_multiple($response, $request); $request['options']['hooks']->dispatch('multiple.request.complete', array(&$response, $id)); } } return $responses; } /** * Get the default options * * @see Requests::request() for values returned by this method * @param boolean $multirequest Is this a multirequest? * @return array Default option values */ protected static function get_default_options($multirequest = false) { $defaults = array( 'timeout' => 10, 'connect_timeout' => 10, 'useragent' => 'php-requests/' . self::VERSION, 'protocol_version' => 1.1, 'redirected' => 0, 'redirects' => 10, 'follow_redirects' => true, 'blocking' => true, 'type' => self::GET, 'filename' => false, 'auth' => false, 'proxy' => false, 'cookies' => false, 'max_bytes' => false, 'idn' => true, 'hooks' => null, 'transport' => null, 'verify' => Requests::get_certificate_path(), 'verifyname' => true, ); if ($multirequest !== false) { $defaults['complete'] = null; } return $defaults; } /** * Get default certificate path. * * @return string Default certificate path. */ public static function get_certificate_path() { if ( ! empty( Requests::$certificate_path ) ) { return Requests::$certificate_path; } return dirname(__FILE__) . '/Requests/Transport/cacert.pem'; } /** * Set default certificate path. * * @param string $path Certificate path, pointing to a PEM file. */ public static function set_certificate_path( $path ) { Requests::$certificate_path = $path; } /** * Set the default values * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type * @param array $options Options for the request * @return array $options */ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { throw new Requests_Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); } if (empty($options['hooks'])) { $options['hooks'] = new Requests_Hooks(); } if (is_array($options['auth'])) { $options['auth'] = new Requests_Auth_Basic($options['auth']); } if ($options['auth'] !== false) { $options['auth']->register($options['hooks']); } if (is_string($options['proxy']) || is_array($options['proxy'])) { $options['proxy'] = new Requests_Proxy_HTTP($options['proxy']); } if ($options['proxy'] !== false) { $options['proxy']->register($options['hooks']); } if (is_array($options['cookies'])) { $options['cookies'] = new Requests_Cookie_Jar($options['cookies']); } elseif (empty($options['cookies'])) { $options['cookies'] = new Requests_Cookie_Jar(); } if ($options['cookies'] !== false) { $options['cookies']->register($options['hooks']); } if ($options['idn'] !== false) { $iri = new Requests_IRI($url); $iri->host = Requests_IDNAEncoder::encode($iri->ihost); $url = $iri->uri; } // Massage the type to ensure we support it. $type = strtoupper($type); if (!isset($options['data_format'])) { if (in_array($type, array(self::HEAD, self::GET, self::DELETE))) { $options['data_format'] = 'query'; } else { $options['data_format'] = 'body'; } } } /** * HTTP response parser * * @throws Requests_Exception On missing head/body separator (`requests.no_crlf_separator`) * @throws Requests_Exception On missing head/body separator (`noversion`) * @throws Requests_Exception On missing head/body separator (`toomanyredirects`) * * @param string $headers Full response text including headers and body * @param string $url Original request URL * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects * @return Requests_Response */ protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { $return = new Requests_Response(); if (!$options['blocking']) { return $return; } $return->raw = $headers; $return->url = $url; if (!$options['filename']) { if (($pos = strpos($headers, "\r\n\r\n")) === false) { // Crap! throw new Requests_Exception('Missing header/body separator', 'requests.no_crlf_separator'); } $headers = substr($return->raw, 0, $pos); $return->body = substr($return->raw, $pos + strlen("\n\r\n\r")); } else { $return->body = ''; } // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) $headers = str_replace("\r\n", "\n", $headers); // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) $headers = preg_replace('/\n[ \t]/', ' ', $headers); $headers = explode("\n", $headers); preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); if (empty($matches)) { throw new Requests_Exception('Response could not be parsed', 'noversion', $headers); } $return->protocol_version = (float) $matches[1]; $return->status_code = (int) $matches[2]; if ($return->status_code >= 200 && $return->status_code < 300) { $return->success = true; } foreach ($headers as $header) { list($key, $value) = explode(':', $header, 2); $value = trim($value); preg_replace('#(\s+)#i', ' ', $value); $return->headers[$key] = $value; } if (isset($return->headers['transfer-encoding'])) { $return->body = self::decode_chunked($return->body); unset($return->headers['transfer-encoding']); } if (isset($return->headers['content-encoding'])) { $return->body = self::decompress($return->body); } //fsockopen and cURL compatibility if (isset($return->headers['connection'])) { unset($return->headers['connection']); } $options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options)); if ($return->is_redirect() && $options['follow_redirects'] === true) { if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { if ($return->status_code === 303) { $options['type'] = self::GET; } $options['redirected']++; $location = $return->headers['location']; if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { // relative redirect, for compatibility make it absolute $location = Requests_IRI::absolutize($url, $location); $location = $location->uri; } $hook_args = array( &$location, &$req_headers, &$req_data, &$options, $return ); $options['hooks']->dispatch('requests.before_redirect', $hook_args); $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); $redirected->history[] = $return; return $redirected; } elseif ($options['redirected'] >= $options['redirects']) { throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return); } } $return->redirects = $options['redirected']; $options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options)); return $return; } /** * Callback for `transport.internal.parse_response` * * Internal use only. Converts a raw HTTP response to a Requests_Response * while still executing a multiple request. * * @param string $response Full response text including headers and body (will be overwritten with Response instance) * @param array $request Request data as passed into {@see Requests::request_multiple()} * @return null `$response` is either set to a Requests_Response instance, or a Requests_Exception object */ public static function parse_multiple(&$response, $request) { try { $url = $request['url']; $headers = $request['headers']; $data = $request['data']; $options = $request['options']; $response = self::parse_response($response, $url, $headers, $data, $options); } catch (Requests_Exception $e) { $response = $e; } } /** * Decoded a chunked body as per RFC 2616 * * @see https://tools.ietf.org/html/rfc2616#section-3.6.1 * @param string $data Chunked body * @return string Decoded body */ protected static function decode_chunked($data) { if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { return $data; } $decoded = ''; $encoded = $data; while (true) { $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); if (!$is_chunked) { // Looks like it's not chunked after all return $data; } $length = hexdec(trim($matches[1])); if ($length === 0) { // Ignore trailer headers return $decoded; } $chunk_length = strlen($matches[0]); $decoded .= substr($encoded, $chunk_length, $length); $encoded = substr($encoded, $chunk_length + $length + 2); if (trim($encoded) === '0' || empty($encoded)) { return $decoded; } } // We'll never actually get down here // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd /** * Convert a key => value array to a 'key: value' array for headers * * @param array $array Dictionary of header values * @return array List of headers */ public static function flatten($array) { $return = array(); foreach ($array as $key => $value) { $return[] = sprintf('%s: %s', $key, $value); } return $return; } /** * Convert a key => value array to a 'key: value' array for headers * * @codeCoverageIgnore * @deprecated Misspelling of {@see Requests::flatten} * @param array $array Dictionary of header values * @return array List of headers */ public static function flattern($array) { return self::flatten($array); } /** * Decompress an encoded body * * Implements gzip, compress and deflate. Guesses which it is by attempting * to decode. * * @param string $data Compressed data in one of the above formats * @return string Decompressed string */ public static function decompress($data) { if (substr($data, 0, 2) !== "\x1f\x8b" && substr($data, 0, 2) !== "\x78\x9c") { // Not actually compressed. Probably cURL ruining this for us. return $data; } if (function_exists('gzdecode') && ($decoded = @gzdecode($data)) !== false) { return $decoded; } elseif (function_exists('gzinflate') && ($decoded = @gzinflate($data)) !== false) { return $decoded; } elseif (($decoded = self::compatible_gzinflate($data)) !== false) { return $decoded; } elseif (function_exists('gzuncompress') && ($decoded = @gzuncompress($data)) !== false) { return $decoded; } return $data; } /** * Decompression of deflated string while staying compatible with the majority of servers. * * Certain Servers will return deflated data with headers which PHP's gzinflate() * function cannot handle out of the box. The following function has been created from * various snippets on the gzinflate() PHP documentation. * * Warning: Magic numbers within. Due to the potential different formats that the compressed * data may be returned in, some "magic offsets" are needed to ensure proper decompression * takes place. For a simple progmatic way to determine the magic offset in use, see: * https://core.trac.wordpress.org/ticket/18273 * * @since 2.8.1 * @link https://core.trac.wordpress.org/ticket/18273 * @link https://secure.php.net/manual/en/function.gzinflate.php#70875 * @link https://secure.php.net/manual/en/function.gzinflate.php#77336 * * @param string $gzData String to decompress. * @return string|bool False on failure. */ public static function compatible_gzinflate($gzData) { // Compressed data might contain a full zlib header, if so strip it for // gzinflate() if (substr($gzData, 0, 3) == "\x1f\x8b\x08") { $i = 10; $flg = ord(substr($gzData, 3, 1)); if ($flg > 0) { if ($flg & 4) { list($xlen) = unpack('v', substr($gzData, $i, 2)); $i = $i + 2 + $xlen; } if ($flg & 8) { $i = strpos($gzData, "\0", $i) + 1; } if ($flg & 16) { $i = strpos($gzData, "\0", $i) + 1; } if ($flg & 2) { $i = $i + 2; } } $decompressed = self::compatible_gzinflate(substr($gzData, $i)); if (false !== $decompressed) { return $decompressed; } } // If the data is Huffman Encoded, we must first strip the leading 2 // byte Huffman marker for gzinflate() // The response is Huffman coded by many compressors such as // java.util.zip.Deflater, Ruby’s Zlib::Deflate, and .NET's // System.IO.Compression.DeflateStream. // // See https://decompres.blogspot.com/ for a quick explanation of this // data type $huffman_encoded = false; // low nibble of first byte should be 0x08 list(, $first_nibble) = unpack('h', $gzData); // First 2 bytes should be divisible by 0x1F list(, $first_two_bytes) = unpack('n', $gzData); if (0x08 == $first_nibble && 0 == ($first_two_bytes % 0x1F)) { $huffman_encoded = true; } if ($huffman_encoded) { if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) { return $decompressed; } } if ("\x50\x4b\x03\x04" == substr($gzData, 0, 4)) { // ZIP file format header // Offset 6: 2 bytes, General-purpose field // Offset 26: 2 bytes, filename length // Offset 28: 2 bytes, optional field length // Offset 30: Filename field, followed by optional field, followed // immediately by data list(, $general_purpose_flag) = unpack('v', substr($gzData, 6, 2)); // If the file has been compressed on the fly, 0x08 bit is set of // the general purpose field. We can use this to differentiate // between a compressed document, and a ZIP file $zip_compressed_on_the_fly = (0x08 == (0x08 & $general_purpose_flag)); if (!$zip_compressed_on_the_fly) { // Don't attempt to decode a compressed zip file return $gzData; } // Determine the first byte of data, based on the above ZIP header // offsets: $first_file_start = array_sum(unpack('v2', substr($gzData, 26, 4))); if (false !== ($decompressed = @gzinflate(substr($gzData, 30 + $first_file_start)))) { return $decompressed; } return false; } // Finally fall back to straight gzinflate if (false !== ($decompressed = @gzinflate($gzData))) { return $decompressed; } // Fallback for all above failing, not expected, but included for // debugging and preventing regressions and to track stats if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) { return $decompressed; } return false; } public static function match_domain($host, $reference) { // Check for a direct match if ($host === $reference) { return true; } // Calculate the valid wildcard match if the host is not an IP address // Also validates that the host has 3 parts or more, as per Firefox's // ruleset. $parts = explode('.', $host); if (ip2long($host) === false && count($parts) >= 3) { $parts[0] = '*'; $wildcard = implode('.', $parts); if ($wildcard === $reference) { return true; } } return false; } }